~0x44/nova/bug838466

« back to all changes in this revision

Viewing changes to vendor/boto/boto/utils.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/
 
2
#
 
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-
 
9
# lowing conditions:
 
10
#
 
11
# The above copyright notice and this permission notice shall be included
 
12
# in all copies or substantial portions of the Software.
 
13
#
 
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
 
20
# IN THE SOFTWARE.
 
21
 
 
22
#
 
23
# Parts of this code were copied or derived from sample code supplied by AWS.
 
24
# The following notice applies to that code.
 
25
#
 
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
 
33
#  affiliates.
 
34
 
 
35
"""
 
36
Some handy utility functions used by several classes.
 
37
"""
 
38
 
 
39
import re
 
40
import urllib
 
41
import urllib2
 
42
import subprocess
 
43
import StringIO
 
44
import time
 
45
import logging.handlers
 
46
import boto
 
47
import tempfile
 
48
import smtplib
 
49
import datetime
 
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
 
55
 
 
56
try:
 
57
    import hashlib
 
58
    _hashfn = hashlib.sha512
 
59
except ImportError:
 
60
    import md5
 
61
    _hashfn = md5.md5
 
62
 
 
63
METADATA_PREFIX = 'x-amz-meta-'
 
64
AMAZON_HEADER_PREFIX = 'x-amz-'
 
65
 
 
66
# generates the aws canonical string for the given parameters
 
67
def canonical_string(method, path, headers, expires=None):
 
68
    interesting_headers = {}
 
69
    for key in headers:
 
70
        lk = key.lower()
 
71
        if lk in ['content-md5', 'content-type', 'date'] or lk.startswith(AMAZON_HEADER_PREFIX):
 
72
            interesting_headers[lk] = headers[key].strip()
 
73
 
 
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'] = ''
 
79
 
 
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'] = ''
 
83
 
 
84
    # if you're using expires for query string auth, then it trumps date
 
85
    # (and x-amz-date)
 
86
    if expires:
 
87
        interesting_headers['date'] = str(expires)
 
88
 
 
89
    sorted_header_keys = interesting_headers.keys()
 
90
    sorted_header_keys.sort()
 
91
 
 
92
    buf = "%s\n" % method
 
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)
 
97
        else:
 
98
            buf += "%s\n" % val
 
99
 
 
100
    # don't include anything after the first ? in the resource...
 
101
    buf += "%s" % path.split('?')[0]
 
102
 
 
103
    # ...unless there is an acl or torrent parameter
 
104
    if re.search("[&?]acl($|=|&)", path):
 
105
        buf += "?acl"
 
106
    elif re.search("[&?]logging($|=|&)", path):
 
107
        buf += "?logging"
 
108
    elif re.search("[&?]torrent($|=|&)", path):
 
109
        buf += "?torrent"
 
110
    elif re.search("[&?]location($|=|&)", path):
 
111
        buf += "?location"
 
112
    elif re.search("[&?]requestPayment($|=|&)", path):
 
113
        buf += "?requestPayment"
 
114
    elif re.search("[&?]versions($|=|&)", path):
 
115
        buf += "?versions"
 
116
    elif re.search("[&?]versioning($|=|&)", path):
 
117
        buf += "?versioning"
 
118
    else:
 
119
        m = re.search("[&?]versionId=([^&]+)($|=|&)", path)
 
120
        if m:
 
121
            buf += '?versionId=' + m.group(1)
 
122
 
 
123
    return buf
 
124
 
 
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',
 
130
                         'date', 'expires']:
 
131
            final_headers[k] = metadata[k]
 
132
        else:
 
133
            final_headers[METADATA_PREFIX + k] = metadata[k]
 
134
 
 
135
    return final_headers
 
136
 
 
137
def get_aws_metadata(headers):
 
138
    metadata = {}
 
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')
 
143
            del headers[hkey]
 
144
    return metadata
 
145
 
 
146
def retry_url(url, retry_on_404=True):
 
147
    for i in range(0, 10):
 
148
        try:
 
149
            req = urllib2.Request(url)
 
150
            resp = urllib2.urlopen(req)
 
151
            return resp.read()
 
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'):
 
155
                code = e.getcode()
 
156
            else:
 
157
                code = e.code
 
158
            if code == 404 and not retry_on_404:
 
159
                return ''
 
160
        except:
 
161
            pass
 
162
        boto.log.exception('Caught exception reading instance data')
 
163
        time.sleep(2**i)
 
164
    boto.log.error('Unable to read instance data, giving up')
 
165
    return ''
 
166
 
 
167
def _get_instance_metadata(url):
 
168
    d = {}
 
169
    data = retry_url(url)
 
170
    if data:
 
171
        fields = data.split('\n')
 
172
        for field in fields:
 
173
            if field.endswith('/'):
 
174
                d[field[0:-1]] = _get_instance_metadata(url + field)
 
175
            else:
 
176
                p = field.find('=')
 
177
                if p > 0:
 
178
                    key = field[p+1:]
 
179
                    resource = field[0:p] + '/openssh-key'
 
180
                else:
 
181
                    key = resource = field
 
182
                val = retry_url(url + resource)
 
183
                p = val.find('\n')
 
184
                if p > 0:
 
185
                    val = val.split('\n')
 
186
                d[key] = val
 
187
    return d
 
188
 
 
189
def get_instance_metadata(version='latest'):
 
190
    """
 
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.
 
196
    """
 
197
    url = 'http://169.254.169.254/%s/meta-data/' % version
 
198
    return _get_instance_metadata(url)
 
199
 
 
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)
 
203
    if user_data:
 
204
        if sep:
 
205
            l = user_data.split(sep)
 
206
            user_data = {}
 
207
            for nvpair in l:
 
208
                t = nvpair.split('=')
 
209
                user_data[t[0].strip()] = t[1].strip()
 
210
    return user_data
 
211
 
 
212
ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
 
213
    
 
214
def get_ts(ts=None):
 
215
    if not ts:
 
216
        ts = time.gmtime()
 
217
    return time.strftime(ISO8601, ts)
 
218
 
 
219
def parse_ts(ts):
 
220
    return datetime.datetime.strptime(ts, ISO8601)
 
221
 
 
222
def find_class(module_name, class_name=None):
 
223
    if class_name:
 
224
        module_name = "%s.%s" % (module_name, class_name)
 
225
    modules = module_name.split('.')
 
226
    c = None
 
227
 
 
228
    try:
 
229
        for m in modules[1:]:
 
230
            if c:
 
231
                c = getattr(c, m)
 
232
            else:
 
233
                c = getattr(__import__(".".join(modules[0:-1])), m)
 
234
        return c
 
235
    except:
 
236
        return None
 
237
    
 
238
def update_dme(username, password, dme_id, ip_address):
 
239
    """
 
240
    Update your Dynamic DNS record with DNSMadeEasy.com
 
241
    """
 
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))
 
245
    return s.read()
 
246
 
 
247
def fetch_file(uri, file=None, username=None, password=None):
 
248
    """
 
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"
 
253
    """
 
254
    boto.log.info('Fetching %s' % uri)
 
255
    if file == None:
 
256
        file = tempfile.NamedTemporaryFile()
 
257
    try:
 
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)
 
264
        else:
 
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)
 
272
            file.write(s.read())
 
273
        file.seek(0)
 
274
    except:
 
275
        raise
 
276
        boto.log.exception('Problem Retrieving file: %s' % uri)
 
277
        file = None
 
278
    return file
 
279
 
 
280
class ShellCommand(object):
 
281
 
 
282
    def __init__(self, command, wait=True):
 
283
        self.exit_code = 0
 
284
        self.command = command
 
285
        self.log_fp = StringIO.StringIO()
 
286
        self.wait = wait
 
287
        self.run()
 
288
 
 
289
    def run(self):
 
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)
 
293
        if(self.wait):
 
294
            while self.process.poll() == None:
 
295
                time.sleep(1)
 
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
 
302
 
 
303
    def setReadOnly(self, value):
 
304
        raise AttributeError
 
305
 
 
306
    def getStatus(self):
 
307
        return self.exit_code
 
308
 
 
309
    status = property(getStatus, setReadOnly, None, 'The exit code for the command')
 
310
 
 
311
    def getOutput(self):
 
312
        return self.log_fp.getvalue()
 
313
 
 
314
    output = property(getOutput, setReadOnly, None, 'The STDIN and STDERR output of the command')
 
315
 
 
316
class AuthSMTPHandler(logging.handlers.SMTPHandler):
 
317
    """
 
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:
 
322
    
 
323
    [handler_hand07]
 
324
    class=boto.utils.AuthSMTPHandler
 
325
    level=WARN
 
326
    formatter=form07
 
327
    args=('localhost', 'username', 'password', 'from@abc', ['user1@abc', 'user2@xyz'], 'Logger Subject')
 
328
    """
 
329
 
 
330
    def __init__(self, mailhost, username, password, fromaddr, toaddrs, subject):
 
331
        """
 
332
        Initialize the handler.
 
333
 
 
334
        We have extended the constructor to accept a username/password
 
335
        for SMTP authentication.
 
336
        """
 
337
        logging.handlers.SMTPHandler.__init__(self, mailhost, fromaddr, toaddrs, subject)
 
338
        self.username = username
 
339
        self.password = password
 
340
        
 
341
    def emit(self, record):
 
342
        """
 
343
        Emit a record.
 
344
 
 
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.
 
348
        """
 
349
        try:
 
350
            port = self.mailport
 
351
            if not port:
 
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" % (
 
357
                            self.fromaddr,
 
358
                            ','.join(self.toaddrs),
 
359
                            self.getSubject(record),
 
360
                            formatdate(), msg)
 
361
            smtp.sendmail(self.fromaddr, self.toaddrs, msg)
 
362
            smtp.quit()
 
363
        except (KeyboardInterrupt, SystemExit):
 
364
            raise
 
365
        except:
 
366
            self.handleError(record)
 
367
 
 
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.
 
371
    
 
372
    >>> cache = LRUCache(3)
 
373
    >>> cache['A'] = 0
 
374
    >>> cache['B'] = 1
 
375
    >>> cache['C'] = 2
 
376
    >>> len(cache)
 
377
    3
 
378
    
 
379
    >>> cache['A']
 
380
    0
 
381
    
 
382
    Adding new items to the cache does not increase its size. Instead, the least
 
383
    recently used item is dropped:
 
384
    
 
385
    >>> cache['D'] = 3
 
386
    >>> len(cache)
 
387
    3
 
388
    >>> 'B' in cache
 
389
    False
 
390
    
 
391
    Iterating over the cache returns the keys, starting with the most recently
 
392
    used:
 
393
    
 
394
    >>> for key in cache:
 
395
    ...     print key
 
396
    D
 
397
    A
 
398
    C
 
399
 
 
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
 
403
    BSD License). See:
 
404
 
 
405
      http://svn.myghty.org/myghtyutils/trunk/lib/myghtyutils/util.py
 
406
    """
 
407
 
 
408
    class _Item(object):
 
409
        def __init__(self, key, value):
 
410
            self.previous = self.next = None
 
411
            self.key = key
 
412
            self.value = value
 
413
        def __repr__(self):
 
414
            return repr(self.value)
 
415
 
 
416
    def __init__(self, capacity):
 
417
        self._dict = dict()
 
418
        self.capacity = capacity
 
419
        self.head = None
 
420
        self.tail = None
 
421
 
 
422
    def __contains__(self, key):
 
423
        return key in self._dict
 
424
 
 
425
    def __iter__(self):
 
426
        cur = self.head
 
427
        while cur:
 
428
            yield cur.key
 
429
            cur = cur.next
 
430
 
 
431
    def __len__(self):
 
432
        return len(self._dict)
 
433
 
 
434
    def __getitem__(self, key):
 
435
        item = self._dict[key]
 
436
        self._update_item(item)
 
437
        return item.value
 
438
 
 
439
    def __setitem__(self, key, value):
 
440
        item = self._dict.get(key)
 
441
        if item is None:
 
442
            item = self._Item(key, value)
 
443
            self._dict[key] = item
 
444
            self._insert_item(item)
 
445
        else:
 
446
            item.value = value
 
447
            self._update_item(item)
 
448
            self._manage_size()
 
449
 
 
450
    def __repr__(self):
 
451
        return repr(self._dict)
 
452
 
 
453
    def _insert_item(self, item):
 
454
        item.previous = None
 
455
        item.next = self.head
 
456
        if self.head is not None:
 
457
            self.head.previous = item
 
458
        else:
 
459
            self.tail = item
 
460
        self.head = item
 
461
        self._manage_size()
 
462
 
 
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
 
469
            else:
 
470
                self.head = self.tail = None
 
471
 
 
472
    def _update_item(self, item):
 
473
        if self.head == item:
 
474
            return
 
475
 
 
476
        previous = item.previous
 
477
        previous.next = item.next
 
478
        if item.next is not None:
 
479
            item.next.previous = previous
 
480
        else:
 
481
            self.tail = previous
 
482
 
 
483
        item.previous = None
 
484
        item.next = self.head
 
485
        self.head.previous = self.head = item
 
486
 
 
487
class Password(object):
 
488
    """
 
489
    Password object that stores itself as SHA512 hashed.
 
490
    """
 
491
    def __init__(self, str=None):
 
492
        """
 
493
        Load the string from an initial value, this should be the raw SHA512 hashed password
 
494
        """
 
495
        self.str = str
 
496
 
 
497
    def set(self, value):
 
498
        self.str = _hashfn(value).hexdigest()
 
499
   
 
500
    def __str__(self):
 
501
        return str(self.str)
 
502
   
 
503
    def __eq__(self, other):
 
504
        if other == None:
 
505
            return False
 
506
        return str(_hashfn(other).hexdigest()) == str(self.str)
 
507
 
 
508
    def __len__(self):
 
509
        if self.str:
 
510
            return len(self.str)
 
511
        else:
 
512
            return 0
 
513
 
 
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)
 
517
    if not to_string:
 
518
        to_string = boto.config.get_value('Notification', 'smtp_to', None)
 
519
    if to_string:
 
520
        try:
 
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
 
527
        
 
528
            if body:
 
529
                msg.attach(MIMEText(body))
 
530
 
 
531
            if html_body:
 
532
                part = MIMEBase('text', 'html')
 
533
                part.set_payload(html_body)
 
534
                Encoders.encode_base64(part)
 
535
                msg.attach(part)
 
536
 
 
537
            for part in attachments:
 
538
                msg.attach(part)
 
539
 
 
540
            smtp_host = boto.config.get_value('Notification', 'smtp_host', 'localhost')
 
541
 
 
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")))
 
545
            else:
 
546
                server = smtplib.SMTP(smtp_host)
 
547
 
 
548
            # TLS support
 
549
            if boto.config.getbool("Notification", "smtp_tls"):
 
550
                server.ehlo()
 
551
                server.starttls()
 
552
                server.ehlo()
 
553
            smtp_user = boto.config.get_value('Notification', 'smtp_user', '')
 
554
            smtp_pass = boto.config.get_value('Notification', 'smtp_pass', '')
 
555
            if smtp_user:
 
556
                server.login(smtp_user, smtp_pass)
 
557
            server.sendmail(from_string, to_string, msg.as_string())
 
558
            server.quit()
 
559
        except:
 
560
            boto.log.exception('notify failed')
 
561