1
# Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/
3
# Permission is hereby granted, free of charge, to any person obtaining a
4
# copy of this software and associated documentation files (the
5
# "Software"), to deal in the Software without restriction, including
6
# without limitation the rights to use, copy, modify, merge, publish, dis-
7
# tribute, sublicense, and/or sell copies of the Software, and to permit
8
# persons to whom the Software is furnished to do so, subject to the fol-
11
# The above copyright notice and this permission notice shall be included
12
# in all copies or substantial portions of the Software.
14
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23
# Parts of this code were copied or derived from sample code supplied by AWS.
24
# The following notice applies to that code.
26
# This software code is made available "AS IS" without warranties of any
27
# kind. You may copy, display, modify and redistribute the software
28
# code either by itself or as incorporated into your code; provided that
29
# you do not remove any proprietary notices. Your use of this software
30
# code is at your own risk and you waive any claim against Amazon
31
# Digital Services, Inc. or its affiliates with respect to your use of
32
# this software code. (c) 2006 Amazon Digital Services, Inc. or its
36
Some handy utility functions used by several classes.
45
import logging.handlers
50
from email.MIMEMultipart import MIMEMultipart
51
from email.MIMEBase import MIMEBase
52
from email.MIMEText import MIMEText
53
from email.Utils import formatdate
54
from email import Encoders
58
_hashfn = hashlib.sha512
63
METADATA_PREFIX = 'x-amz-meta-'
64
AMAZON_HEADER_PREFIX = 'x-amz-'
66
# generates the aws canonical string for the given parameters
67
def canonical_string(method, path, headers, expires=None):
68
interesting_headers = {}
71
if lk in ['content-md5', 'content-type', 'date'] or lk.startswith(AMAZON_HEADER_PREFIX):
72
interesting_headers[lk] = headers[key].strip()
74
# these keys get empty strings if they don't exist
75
if not interesting_headers.has_key('content-type'):
76
interesting_headers['content-type'] = ''
77
if not interesting_headers.has_key('content-md5'):
78
interesting_headers['content-md5'] = ''
80
# just in case someone used this. it's not necessary in this lib.
81
if interesting_headers.has_key('x-amz-date'):
82
interesting_headers['date'] = ''
84
# if you're using expires for query string auth, then it trumps date
87
interesting_headers['date'] = str(expires)
89
sorted_header_keys = interesting_headers.keys()
90
sorted_header_keys.sort()
93
for key in sorted_header_keys:
94
val = interesting_headers[key]
95
if key.startswith(AMAZON_HEADER_PREFIX):
96
buf += "%s:%s\n" % (key, val)
100
# don't include anything after the first ? in the resource...
101
buf += "%s" % path.split('?')[0]
103
# ...unless there is an acl or torrent parameter
104
if re.search("[&?]acl($|=|&)", path):
106
elif re.search("[&?]logging($|=|&)", path):
108
elif re.search("[&?]torrent($|=|&)", path):
110
elif re.search("[&?]location($|=|&)", path):
112
elif re.search("[&?]requestPayment($|=|&)", path):
113
buf += "?requestPayment"
114
elif re.search("[&?]versions($|=|&)", path):
116
elif re.search("[&?]versioning($|=|&)", path):
119
m = re.search("[&?]versionId=([^&]+)($|=|&)", path)
121
buf += '?versionId=' + m.group(1)
125
def merge_meta(headers, metadata):
126
final_headers = headers.copy()
127
for k in metadata.keys():
128
if k.lower() in ['cache-control', 'content-md5', 'content-type',
129
'content-encoding', 'content-disposition',
131
final_headers[k] = metadata[k]
133
final_headers[METADATA_PREFIX + k] = metadata[k]
137
def get_aws_metadata(headers):
139
for hkey in headers.keys():
140
if hkey.lower().startswith(METADATA_PREFIX):
141
val = urllib.unquote_plus(headers[hkey])
142
metadata[hkey[len(METADATA_PREFIX):]] = unicode(val, 'utf-8')
146
def retry_url(url, retry_on_404=True):
147
for i in range(0, 10):
149
req = urllib2.Request(url)
150
resp = urllib2.urlopen(req)
152
except urllib2.HTTPError, e:
153
# in 2.6 you use getcode(), in 2.5 and earlier you use code
154
if hasattr(e, 'getcode'):
158
if code == 404 and not retry_on_404:
162
boto.log.exception('Caught exception reading instance data')
164
boto.log.error('Unable to read instance data, giving up')
167
def _get_instance_metadata(url):
169
data = retry_url(url)
171
fields = data.split('\n')
173
if field.endswith('/'):
174
d[field[0:-1]] = _get_instance_metadata(url + field)
179
resource = field[0:p] + '/openssh-key'
181
key = resource = field
182
val = retry_url(url + resource)
185
val = val.split('\n')
189
def get_instance_metadata(version='latest'):
191
Returns the instance metadata as a nested Python dictionary.
192
Simple values (e.g. local_hostname, hostname, etc.) will be
193
stored as string values. Values such as ancestor-ami-ids will
194
be stored in the dict as a list of string values. More complex
195
fields such as public-keys and will be stored as nested dicts.
197
url = 'http://169.254.169.254/%s/meta-data/' % version
198
return _get_instance_metadata(url)
200
def get_instance_userdata(version='latest', sep=None):
201
url = 'http://169.254.169.254/%s/user-data' % version
202
user_data = retry_url(url, retry_on_404=False)
205
l = user_data.split(sep)
208
t = nvpair.split('=')
209
user_data[t[0].strip()] = t[1].strip()
212
ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
217
return time.strftime(ISO8601, ts)
220
return datetime.datetime.strptime(ts, ISO8601)
222
def find_class(module_name, class_name=None):
224
module_name = "%s.%s" % (module_name, class_name)
225
modules = module_name.split('.')
229
for m in modules[1:]:
233
c = getattr(__import__(".".join(modules[0:-1])), m)
238
def update_dme(username, password, dme_id, ip_address):
240
Update your Dynamic DNS record with DNSMadeEasy.com
242
dme_url = 'https://www.dnsmadeeasy.com/servlet/updateip'
243
dme_url += '?username=%s&password=%s&id=%s&ip=%s'
244
s = urllib2.urlopen(dme_url % (username, password, dme_id, ip_address))
247
def fetch_file(uri, file=None, username=None, password=None):
249
Fetch a file based on the URI provided. If you do not pass in a file pointer
250
a tempfile.NamedTemporaryFile, or None if the file could not be
251
retrieved is returned.
252
The URI can be either an HTTP url, or "s3://bucket_name/key_name"
254
boto.log.info('Fetching %s' % uri)
256
file = tempfile.NamedTemporaryFile()
258
if uri.startswith('s3://'):
259
bucket_name, key_name = uri[len('s3://'):].split('/', 1)
260
c = boto.connect_s3()
261
bucket = c.get_bucket(bucket_name)
262
key = bucket.get_key(key_name)
263
key.get_contents_to_file(file)
265
if username and password:
266
passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
267
passman.add_password(None, uri, username, password)
268
authhandler = urllib2.HTTPBasicAuthHandler(passman)
269
opener = urllib2.build_opener(authhandler)
270
urllib2.install_opener(opener)
271
s = urllib2.urlopen(uri)
276
boto.log.exception('Problem Retrieving file: %s' % uri)
280
class ShellCommand(object):
282
def __init__(self, command, wait=True):
284
self.command = command
285
self.log_fp = StringIO.StringIO()
290
boto.log.info('running:%s' % self.command)
291
self.process = subprocess.Popen(self.command, shell=True, stdin=subprocess.PIPE,
292
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
294
while self.process.poll() == None:
296
t = self.process.communicate()
297
self.log_fp.write(t[0])
298
self.log_fp.write(t[1])
299
boto.log.info(self.log_fp.getvalue())
300
self.exit_code = self.process.returncode
301
return self.exit_code
303
def setReadOnly(self, value):
307
return self.exit_code
309
status = property(getStatus, setReadOnly, None, 'The exit code for the command')
312
return self.log_fp.getvalue()
314
output = property(getOutput, setReadOnly, None, 'The STDIN and STDERR output of the command')
316
class AuthSMTPHandler(logging.handlers.SMTPHandler):
318
This class extends the SMTPHandler in the standard Python logging module
319
to accept a username and password on the constructor and to then use those
320
credentials to authenticate with the SMTP server. To use this, you could
321
add something like this in your boto config file:
324
class=boto.utils.AuthSMTPHandler
327
args=('localhost', 'username', 'password', 'from@abc', ['user1@abc', 'user2@xyz'], 'Logger Subject')
330
def __init__(self, mailhost, username, password, fromaddr, toaddrs, subject):
332
Initialize the handler.
334
We have extended the constructor to accept a username/password
335
for SMTP authentication.
337
logging.handlers.SMTPHandler.__init__(self, mailhost, fromaddr, toaddrs, subject)
338
self.username = username
339
self.password = password
341
def emit(self, record):
345
Format the record and send it to the specified addressees.
346
It would be really nice if I could add authorization to this class
347
without having to resort to cut and paste inheritance but, no.
352
port = smtplib.SMTP_PORT
353
smtp = smtplib.SMTP(self.mailhost, port)
354
smtp.login(self.username, self.password)
355
msg = self.format(record)
356
msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" % (
358
','.join(self.toaddrs),
359
self.getSubject(record),
361
smtp.sendmail(self.fromaddr, self.toaddrs, msg)
363
except (KeyboardInterrupt, SystemExit):
366
self.handleError(record)
368
class LRUCache(dict):
369
"""A dictionary-like object that stores only a certain number of items, and
370
discards its least recently used item when full.
372
>>> cache = LRUCache(3)
382
Adding new items to the cache does not increase its size. Instead, the least
383
recently used item is dropped:
391
Iterating over the cache returns the keys, starting with the most recently
394
>>> for key in cache:
400
This code is based on the LRUCache class from Genshi which is based on
401
Mighty's LRUCache from ``myghtyutils.util``, written
402
by Mike Bayer and released under the MIT license (Genshi uses the
405
http://svn.myghty.org/myghtyutils/trunk/lib/myghtyutils/util.py
409
def __init__(self, key, value):
410
self.previous = self.next = None
414
return repr(self.value)
416
def __init__(self, capacity):
418
self.capacity = capacity
422
def __contains__(self, key):
423
return key in self._dict
432
return len(self._dict)
434
def __getitem__(self, key):
435
item = self._dict[key]
436
self._update_item(item)
439
def __setitem__(self, key, value):
440
item = self._dict.get(key)
442
item = self._Item(key, value)
443
self._dict[key] = item
444
self._insert_item(item)
447
self._update_item(item)
451
return repr(self._dict)
453
def _insert_item(self, item):
455
item.next = self.head
456
if self.head is not None:
457
self.head.previous = item
463
def _manage_size(self):
464
while len(self._dict) > self.capacity:
465
del self._dict[self.tail.key]
466
if self.tail != self.head:
467
self.tail = self.tail.previous
468
self.tail.next = None
470
self.head = self.tail = None
472
def _update_item(self, item):
473
if self.head == item:
476
previous = item.previous
477
previous.next = item.next
478
if item.next is not None:
479
item.next.previous = previous
484
item.next = self.head
485
self.head.previous = self.head = item
487
class Password(object):
489
Password object that stores itself as SHA512 hashed.
491
def __init__(self, str=None):
493
Load the string from an initial value, this should be the raw SHA512 hashed password
497
def set(self, value):
498
self.str = _hashfn(value).hexdigest()
503
def __eq__(self, other):
506
return str(_hashfn(other).hexdigest()) == str(self.str)
514
def notify(subject, body=None, html_body=None, to_string=None, attachments=[], append_instance_id=True):
515
if append_instance_id:
516
subject = "[%s] %s" % (boto.config.get_value("Instance", "instance-id"), subject)
518
to_string = boto.config.get_value('Notification', 'smtp_to', None)
521
from_string = boto.config.get_value('Notification', 'smtp_from', 'boto')
522
msg = MIMEMultipart()
523
msg['From'] = from_string
524
msg['To'] = to_string
525
msg['Date'] = formatdate(localtime=True)
526
msg['Subject'] = subject
529
msg.attach(MIMEText(body))
532
part = MIMEBase('text', 'html')
533
part.set_payload(html_body)
534
Encoders.encode_base64(part)
537
for part in attachments:
540
smtp_host = boto.config.get_value('Notification', 'smtp_host', 'localhost')
542
# Alternate port support
543
if boto.config.get_value("Notification", "smtp_port"):
544
server = smtplib.SMTP(smtp_host, int(boto.config.get_value("Notification", "smtp_port")))
546
server = smtplib.SMTP(smtp_host)
549
if boto.config.getbool("Notification", "smtp_tls"):
553
smtp_user = boto.config.get_value('Notification', 'smtp_user', '')
554
smtp_pass = boto.config.get_value('Notification', 'smtp_pass', '')
556
server.login(smtp_user, smtp_pass)
557
server.sendmail(from_string, to_string, msg.as_string())
560
boto.log.exception('notify failed')