Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/boto/utils.py @ 5:9b1c78e6ba9c draft default tip
"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
| author | shellac |
|---|---|
| date | Mon, 01 Jun 2020 08:59:25 -0400 |
| parents | 79f47841a781 |
| children |
comparison
equal
deleted
inserted
replaced
| 4:79f47841a781 | 5:9b1c78e6ba9c |
|---|---|
| 1 # Copyright (c) 2006-2012 Mitch Garnaat http://garnaat.org/ | |
| 2 # Copyright (c) 2010, Eucalyptus Systems, Inc. | |
| 3 # Copyright (c) 2012 Amazon.com, Inc. or its affiliates. | |
| 4 # All rights reserved. | |
| 5 # | |
| 6 # Permission is hereby granted, free of charge, to any person obtaining a | |
| 7 # copy of this software and associated documentation files (the | |
| 8 # "Software"), to deal in the Software without restriction, including | |
| 9 # without limitation the rights to use, copy, modify, merge, publish, dis- | |
| 10 # tribute, sublicense, and/or sell copies of the Software, and to permit | |
| 11 # persons to whom the Software is furnished to do so, subject to the fol- | |
| 12 # lowing conditions: | |
| 13 # | |
| 14 # The above copyright notice and this permission notice shall be included | |
| 15 # in all copies or substantial portions of the Software. | |
| 16 # | |
| 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| 18 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- | |
| 19 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT | |
| 20 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 21 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | |
| 23 # IN THE SOFTWARE. | |
| 24 | |
| 25 # | |
| 26 # Parts of this code were copied or derived from sample code supplied by AWS. | |
| 27 # The following notice applies to that code. | |
| 28 # | |
| 29 # This software code is made available "AS IS" without warranties of any | |
| 30 # kind. You may copy, display, modify and redistribute the software | |
| 31 # code either by itself or as incorporated into your code; provided that | |
| 32 # you do not remove any proprietary notices. Your use of this software | |
| 33 # code is at your own risk and you waive any claim against Amazon | |
| 34 # Digital Services, Inc. or its affiliates with respect to your use of | |
| 35 # this software code. (c) 2006 Amazon Digital Services, Inc. or its | |
| 36 # affiliates. | |
| 37 | |
| 38 """ | |
| 39 Some handy utility functions used by several classes. | |
| 40 """ | |
| 41 | |
| 42 import subprocess | |
| 43 import time | |
| 44 import logging.handlers | |
| 45 import boto | |
| 46 import boto.provider | |
| 47 import tempfile | |
| 48 import random | |
| 49 import smtplib | |
| 50 import datetime | |
| 51 import re | |
| 52 import email.mime.multipart | |
| 53 import email.mime.base | |
| 54 import email.mime.text | |
| 55 import email.utils | |
| 56 import email.encoders | |
| 57 import gzip | |
| 58 import threading | |
| 59 import locale | |
| 60 from boto.compat import six, StringIO, urllib, encodebytes | |
| 61 | |
| 62 from contextlib import contextmanager | |
| 63 | |
| 64 from hashlib import md5, sha512 | |
| 65 _hashfn = sha512 | |
| 66 | |
| 67 from boto.compat import json | |
| 68 | |
| 69 try: | |
| 70 from boto.compat.json import JSONDecodeError | |
| 71 except ImportError: | |
| 72 JSONDecodeError = ValueError | |
| 73 | |
| 74 # List of Query String Arguments of Interest | |
| 75 qsa_of_interest = ['acl', 'cors', 'defaultObjectAcl', 'location', 'logging', | |
| 76 'partNumber', 'policy', 'requestPayment', 'torrent', | |
| 77 'versioning', 'versionId', 'versions', 'website', | |
| 78 'uploads', 'uploadId', 'response-content-type', | |
| 79 'response-content-language', 'response-expires', | |
| 80 'response-cache-control', 'response-content-disposition', | |
| 81 'response-content-encoding', 'delete', 'lifecycle', | |
| 82 'tagging', 'restore', | |
| 83 # storageClass is a QSA for buckets in Google Cloud Storage. | |
| 84 # (StorageClass is associated to individual keys in S3, but | |
| 85 # having it listed here should cause no problems because | |
| 86 # GET bucket?storageClass is not part of the S3 API.) | |
| 87 'storageClass', | |
| 88 # websiteConfig is a QSA for buckets in Google Cloud | |
| 89 # Storage. | |
| 90 'websiteConfig', | |
| 91 # compose is a QSA for objects in Google Cloud Storage. | |
| 92 'compose', | |
| 93 # billing is a QSA for buckets in Google Cloud Storage. | |
| 94 'billing', | |
| 95 # userProject is a QSA for requests in Google Cloud Storage. | |
| 96 'userProject', | |
| 97 # encryptionConfig is a QSA for requests in Google Cloud | |
| 98 # Storage. | |
| 99 'encryptionConfig'] | |
| 100 | |
| 101 | |
| 102 _first_cap_regex = re.compile('(.)([A-Z][a-z]+)') | |
| 103 _number_cap_regex = re.compile('([a-z])([0-9]+)') | |
| 104 _end_cap_regex = re.compile('([a-z0-9])([A-Z])') | |
| 105 | |
| 106 | |
| 107 def unquote_v(nv): | |
| 108 if len(nv) == 1: | |
| 109 return nv | |
| 110 else: | |
| 111 return (nv[0], urllib.parse.unquote(nv[1])) | |
| 112 | |
| 113 | |
| 114 def canonical_string(method, path, headers, expires=None, | |
| 115 provider=None): | |
| 116 """ | |
| 117 Generates the aws canonical string for the given parameters | |
| 118 """ | |
| 119 if not provider: | |
| 120 provider = boto.provider.get_default() | |
| 121 interesting_headers = {} | |
| 122 for key in headers: | |
| 123 lk = key.lower() | |
| 124 if headers[key] is not None and \ | |
| 125 (lk in ['content-md5', 'content-type', 'date'] or | |
| 126 lk.startswith(provider.header_prefix)): | |
| 127 interesting_headers[lk] = str(headers[key]).strip() | |
| 128 | |
| 129 # these keys get empty strings if they don't exist | |
| 130 if 'content-type' not in interesting_headers: | |
| 131 interesting_headers['content-type'] = '' | |
| 132 if 'content-md5' not in interesting_headers: | |
| 133 interesting_headers['content-md5'] = '' | |
| 134 | |
| 135 # just in case someone used this. it's not necessary in this lib. | |
| 136 if provider.date_header in interesting_headers: | |
| 137 interesting_headers['date'] = '' | |
| 138 | |
| 139 # if you're using expires for query string auth, then it trumps date | |
| 140 # (and provider.date_header) | |
| 141 if expires: | |
| 142 interesting_headers['date'] = str(expires) | |
| 143 | |
| 144 sorted_header_keys = sorted(interesting_headers.keys()) | |
| 145 | |
| 146 buf = "%s\n" % method | |
| 147 for key in sorted_header_keys: | |
| 148 val = interesting_headers[key] | |
| 149 if key.startswith(provider.header_prefix): | |
| 150 buf += "%s:%s\n" % (key, val) | |
| 151 else: | |
| 152 buf += "%s\n" % val | |
| 153 | |
| 154 # don't include anything after the first ? in the resource... | |
| 155 # unless it is one of the QSA of interest, defined above | |
| 156 t = path.split('?') | |
| 157 buf += t[0] | |
| 158 | |
| 159 if len(t) > 1: | |
| 160 qsa = t[1].split('&') | |
| 161 qsa = [a.split('=', 1) for a in qsa] | |
| 162 qsa = [unquote_v(a) for a in qsa if a[0] in qsa_of_interest] | |
| 163 if len(qsa) > 0: | |
| 164 qsa.sort(key=lambda x: x[0]) | |
| 165 qsa = ['='.join(a) for a in qsa] | |
| 166 buf += '?' | |
| 167 buf += '&'.join(qsa) | |
| 168 | |
| 169 return buf | |
| 170 | |
| 171 | |
| 172 def merge_meta(headers, metadata, provider=None): | |
| 173 if not provider: | |
| 174 provider = boto.provider.get_default() | |
| 175 metadata_prefix = provider.metadata_prefix | |
| 176 final_headers = headers.copy() | |
| 177 for k in metadata.keys(): | |
| 178 if k.lower() in boto.s3.key.Key.base_user_settable_fields: | |
| 179 final_headers[k] = metadata[k] | |
| 180 else: | |
| 181 final_headers[metadata_prefix + k] = metadata[k] | |
| 182 | |
| 183 return final_headers | |
| 184 | |
| 185 | |
| 186 def get_aws_metadata(headers, provider=None): | |
| 187 if not provider: | |
| 188 provider = boto.provider.get_default() | |
| 189 metadata_prefix = provider.metadata_prefix | |
| 190 metadata = {} | |
| 191 for hkey in headers.keys(): | |
| 192 if hkey.lower().startswith(metadata_prefix): | |
| 193 val = urllib.parse.unquote(headers[hkey]) | |
| 194 if isinstance(val, bytes): | |
| 195 try: | |
| 196 val = val.decode('utf-8') | |
| 197 except UnicodeDecodeError: | |
| 198 # Just leave the value as-is | |
| 199 pass | |
| 200 metadata[hkey[len(metadata_prefix):]] = val | |
| 201 del headers[hkey] | |
| 202 return metadata | |
| 203 | |
| 204 | |
| 205 def retry_url(url, retry_on_404=True, num_retries=10, timeout=None): | |
| 206 """ | |
| 207 Retry a url. This is specifically used for accessing the metadata | |
| 208 service on an instance. Since this address should never be proxied | |
| 209 (for security reasons), we create a ProxyHandler with a NULL | |
| 210 dictionary to override any proxy settings in the environment. | |
| 211 """ | |
| 212 for i in range(0, num_retries): | |
| 213 try: | |
| 214 proxy_handler = urllib.request.ProxyHandler({}) | |
| 215 opener = urllib.request.build_opener(proxy_handler) | |
| 216 req = urllib.request.Request(url) | |
| 217 r = opener.open(req, timeout=timeout) | |
| 218 result = r.read() | |
| 219 | |
| 220 if(not isinstance(result, six.string_types) and | |
| 221 hasattr(result, 'decode')): | |
| 222 result = result.decode('utf-8') | |
| 223 | |
| 224 return result | |
| 225 except urllib.error.HTTPError as e: | |
| 226 code = e.getcode() | |
| 227 if code == 404 and not retry_on_404: | |
| 228 return '' | |
| 229 except Exception as e: | |
| 230 boto.log.exception('Caught exception reading instance data') | |
| 231 # If not on the last iteration of the loop then sleep. | |
| 232 if i + 1 != num_retries: | |
| 233 boto.log.debug('Sleeping before retrying') | |
| 234 time.sleep(min(2 ** i, | |
| 235 boto.config.get('Boto', 'max_retry_delay', 60))) | |
| 236 boto.log.error('Unable to read instance data, giving up') | |
| 237 return '' | |
| 238 | |
| 239 | |
| 240 def _get_instance_metadata(url, num_retries, timeout=None): | |
| 241 return LazyLoadMetadata(url, num_retries, timeout) | |
| 242 | |
| 243 | |
| 244 class LazyLoadMetadata(dict): | |
| 245 def __init__(self, url, num_retries, timeout=None): | |
| 246 self._url = url | |
| 247 self._num_retries = num_retries | |
| 248 self._leaves = {} | |
| 249 self._dicts = [] | |
| 250 self._timeout = timeout | |
| 251 data = boto.utils.retry_url(self._url, num_retries=self._num_retries, timeout=self._timeout) | |
| 252 if data: | |
| 253 fields = data.split('\n') | |
| 254 for field in fields: | |
| 255 if field.endswith('/'): | |
| 256 key = field[0:-1] | |
| 257 self._dicts.append(key) | |
| 258 else: | |
| 259 p = field.find('=') | |
| 260 if p > 0: | |
| 261 key = field[p + 1:] | |
| 262 resource = field[0:p] + '/openssh-key' | |
| 263 else: | |
| 264 key = resource = field | |
| 265 self._leaves[key] = resource | |
| 266 self[key] = None | |
| 267 | |
| 268 def _materialize(self): | |
| 269 for key in self: | |
| 270 self[key] | |
| 271 | |
| 272 def __getitem__(self, key): | |
| 273 if key not in self: | |
| 274 # allow dict to throw the KeyError | |
| 275 return super(LazyLoadMetadata, self).__getitem__(key) | |
| 276 | |
| 277 # already loaded | |
| 278 val = super(LazyLoadMetadata, self).__getitem__(key) | |
| 279 if val is not None: | |
| 280 return val | |
| 281 | |
| 282 if key in self._leaves: | |
| 283 resource = self._leaves[key] | |
| 284 last_exception = None | |
| 285 | |
| 286 for i in range(0, self._num_retries): | |
| 287 try: | |
| 288 val = boto.utils.retry_url( | |
| 289 self._url + urllib.parse.quote(resource, | |
| 290 safe="/:"), | |
| 291 num_retries=self._num_retries, | |
| 292 timeout=self._timeout) | |
| 293 if val and val[0] == '{': | |
| 294 val = json.loads(val) | |
| 295 break | |
| 296 else: | |
| 297 p = val.find('\n') | |
| 298 if p > 0: | |
| 299 val = val.split('\n') | |
| 300 break | |
| 301 | |
| 302 except JSONDecodeError as e: | |
| 303 boto.log.debug( | |
| 304 "encountered '%s' exception: %s" % ( | |
| 305 e.__class__.__name__, e)) | |
| 306 boto.log.debug( | |
| 307 'corrupted JSON data found: %s' % val) | |
| 308 last_exception = e | |
| 309 | |
| 310 except Exception as e: | |
| 311 boto.log.debug("encountered unretryable" + | |
| 312 " '%s' exception, re-raising" % ( | |
| 313 e.__class__.__name__)) | |
| 314 last_exception = e | |
| 315 raise | |
| 316 | |
| 317 boto.log.error("Caught exception reading meta data" + | |
| 318 " for the '%s' try" % (i + 1)) | |
| 319 | |
| 320 if i + 1 != self._num_retries: | |
| 321 next_sleep = min( | |
| 322 random.random() * 2 ** i, | |
| 323 boto.config.get('Boto', 'max_retry_delay', 60)) | |
| 324 time.sleep(next_sleep) | |
| 325 else: | |
| 326 boto.log.error('Unable to read meta data, giving up') | |
| 327 boto.log.error( | |
| 328 "encountered '%s' exception: %s" % ( | |
| 329 last_exception.__class__.__name__, last_exception)) | |
| 330 raise last_exception | |
| 331 | |
| 332 self[key] = val | |
| 333 elif key in self._dicts: | |
| 334 self[key] = LazyLoadMetadata(self._url + key + '/', | |
| 335 self._num_retries) | |
| 336 | |
| 337 return super(LazyLoadMetadata, self).__getitem__(key) | |
| 338 | |
| 339 def get(self, key, default=None): | |
| 340 try: | |
| 341 return self[key] | |
| 342 except KeyError: | |
| 343 return default | |
| 344 | |
| 345 def values(self): | |
| 346 self._materialize() | |
| 347 return super(LazyLoadMetadata, self).values() | |
| 348 | |
| 349 def items(self): | |
| 350 self._materialize() | |
| 351 return super(LazyLoadMetadata, self).items() | |
| 352 | |
| 353 def __str__(self): | |
| 354 self._materialize() | |
| 355 return super(LazyLoadMetadata, self).__str__() | |
| 356 | |
| 357 def __repr__(self): | |
| 358 self._materialize() | |
| 359 return super(LazyLoadMetadata, self).__repr__() | |
| 360 | |
| 361 | |
| 362 def _build_instance_metadata_url(url, version, path): | |
| 363 """ | |
| 364 Builds an EC2 metadata URL for fetching information about an instance. | |
| 365 | |
| 366 Example: | |
| 367 | |
| 368 >>> _build_instance_metadata_url('http://169.254.169.254', 'latest', 'meta-data/') | |
| 369 http://169.254.169.254/latest/meta-data/ | |
| 370 | |
| 371 :type url: string | |
| 372 :param url: URL to metadata service, e.g. 'http://169.254.169.254' | |
| 373 | |
| 374 :type version: string | |
| 375 :param version: Version of the metadata to get, e.g. 'latest' | |
| 376 | |
| 377 :type path: string | |
| 378 :param path: Path of the metadata to get, e.g. 'meta-data/'. If a trailing | |
| 379 slash is required it must be passed in with the path. | |
| 380 | |
| 381 :return: The full metadata URL | |
| 382 """ | |
| 383 return '%s/%s/%s' % (url, version, path) | |
| 384 | |
| 385 | |
| 386 def get_instance_metadata(version='latest', url='http://169.254.169.254', | |
| 387 data='meta-data/', timeout=None, num_retries=5): | |
| 388 """ | |
| 389 Returns the instance metadata as a nested Python dictionary. | |
| 390 Simple values (e.g. local_hostname, hostname, etc.) will be | |
| 391 stored as string values. Values such as ancestor-ami-ids will | |
| 392 be stored in the dict as a list of string values. More complex | |
| 393 fields such as public-keys and will be stored as nested dicts. | |
| 394 | |
| 395 If the timeout is specified, the connection to the specified url | |
| 396 will time out after the specified number of seconds. | |
| 397 | |
| 398 """ | |
| 399 try: | |
| 400 metadata_url = _build_instance_metadata_url(url, version, data) | |
| 401 return _get_instance_metadata(metadata_url, num_retries=num_retries, timeout=timeout) | |
| 402 except urllib.error.URLError: | |
| 403 boto.log.exception("Exception caught when trying to retrieve " | |
| 404 "instance metadata for: %s", data) | |
| 405 return None | |
| 406 | |
| 407 | |
| 408 def get_instance_identity(version='latest', url='http://169.254.169.254', | |
| 409 timeout=None, num_retries=5): | |
| 410 """ | |
| 411 Returns the instance identity as a nested Python dictionary. | |
| 412 """ | |
| 413 iid = {} | |
| 414 base_url = _build_instance_metadata_url(url, version, | |
| 415 'dynamic/instance-identity/') | |
| 416 try: | |
| 417 data = retry_url(base_url, num_retries=num_retries, timeout=timeout) | |
| 418 fields = data.split('\n') | |
| 419 for field in fields: | |
| 420 val = retry_url(base_url + '/' + field + '/', num_retries=num_retries, timeout=timeout) | |
| 421 if val[0] == '{': | |
| 422 val = json.loads(val) | |
| 423 if field: | |
| 424 iid[field] = val | |
| 425 return iid | |
| 426 except urllib.error.URLError: | |
| 427 return None | |
| 428 | |
| 429 | |
| 430 def get_instance_userdata(version='latest', sep=None, | |
| 431 url='http://169.254.169.254', timeout=None, num_retries=5): | |
| 432 ud_url = _build_instance_metadata_url(url, version, 'user-data') | |
| 433 user_data = retry_url(ud_url, retry_on_404=False, num_retries=num_retries, timeout=timeout) | |
| 434 if user_data: | |
| 435 if sep: | |
| 436 l = user_data.split(sep) | |
| 437 user_data = {} | |
| 438 for nvpair in l: | |
| 439 t = nvpair.split('=') | |
| 440 user_data[t[0].strip()] = t[1].strip() | |
| 441 return user_data | |
| 442 | |
| 443 ISO8601 = '%Y-%m-%dT%H:%M:%SZ' | |
| 444 ISO8601_MS = '%Y-%m-%dT%H:%M:%S.%fZ' | |
| 445 RFC1123 = '%a, %d %b %Y %H:%M:%S %Z' | |
| 446 LOCALE_LOCK = threading.Lock() | |
| 447 | |
| 448 | |
| 449 @contextmanager | |
| 450 def setlocale(name): | |
| 451 """ | |
| 452 A context manager to set the locale in a threadsafe manner. | |
| 453 """ | |
| 454 with LOCALE_LOCK: | |
| 455 saved = locale.setlocale(locale.LC_ALL) | |
| 456 | |
| 457 try: | |
| 458 yield locale.setlocale(locale.LC_ALL, name) | |
| 459 finally: | |
| 460 locale.setlocale(locale.LC_ALL, saved) | |
| 461 | |
| 462 | |
| 463 def get_ts(ts=None): | |
| 464 if not ts: | |
| 465 ts = time.gmtime() | |
| 466 return time.strftime(ISO8601, ts) | |
| 467 | |
| 468 | |
| 469 def parse_ts(ts): | |
| 470 with setlocale('C'): | |
| 471 ts = ts.strip() | |
| 472 try: | |
| 473 dt = datetime.datetime.strptime(ts, ISO8601) | |
| 474 return dt | |
| 475 except ValueError: | |
| 476 try: | |
| 477 dt = datetime.datetime.strptime(ts, ISO8601_MS) | |
| 478 return dt | |
| 479 except ValueError: | |
| 480 dt = datetime.datetime.strptime(ts, RFC1123) | |
| 481 return dt | |
| 482 | |
| 483 | |
| 484 def find_class(module_name, class_name=None): | |
| 485 if class_name: | |
| 486 module_name = "%s.%s" % (module_name, class_name) | |
| 487 modules = module_name.split('.') | |
| 488 c = None | |
| 489 | |
| 490 try: | |
| 491 for m in modules[1:]: | |
| 492 if c: | |
| 493 c = getattr(c, m) | |
| 494 else: | |
| 495 c = getattr(__import__(".".join(modules[0:-1])), m) | |
| 496 return c | |
| 497 except: | |
| 498 return None | |
| 499 | |
| 500 | |
| 501 def update_dme(username, password, dme_id, ip_address): | |
| 502 """ | |
| 503 Update your Dynamic DNS record with DNSMadeEasy.com | |
| 504 """ | |
| 505 dme_url = 'https://www.dnsmadeeasy.com/servlet/updateip' | |
| 506 dme_url += '?username=%s&password=%s&id=%s&ip=%s' | |
| 507 s = urllib.request.urlopen(dme_url % (username, password, dme_id, ip_address)) | |
| 508 return s.read() | |
| 509 | |
| 510 | |
| 511 def fetch_file(uri, file=None, username=None, password=None): | |
| 512 """ | |
| 513 Fetch a file based on the URI provided. | |
| 514 If you do not pass in a file pointer a tempfile.NamedTemporaryFile, | |
| 515 or None if the file could not be retrieved is returned. | |
| 516 The URI can be either an HTTP url, or "s3://bucket_name/key_name" | |
| 517 """ | |
| 518 boto.log.info('Fetching %s' % uri) | |
| 519 if file is None: | |
| 520 file = tempfile.NamedTemporaryFile() | |
| 521 try: | |
| 522 if uri.startswith('s3://'): | |
| 523 bucket_name, key_name = uri[len('s3://'):].split('/', 1) | |
| 524 c = boto.connect_s3(aws_access_key_id=username, | |
| 525 aws_secret_access_key=password) | |
| 526 bucket = c.get_bucket(bucket_name) | |
| 527 key = bucket.get_key(key_name) | |
| 528 key.get_contents_to_file(file) | |
| 529 else: | |
| 530 if username and password: | |
| 531 passman = urllib.request.HTTPPasswordMgrWithDefaultRealm() | |
| 532 passman.add_password(None, uri, username, password) | |
| 533 authhandler = urllib.request.HTTPBasicAuthHandler(passman) | |
| 534 opener = urllib.request.build_opener(authhandler) | |
| 535 urllib.request.install_opener(opener) | |
| 536 s = urllib.request.urlopen(uri) | |
| 537 file.write(s.read()) | |
| 538 file.seek(0) | |
| 539 except: | |
| 540 raise | |
| 541 boto.log.exception('Problem Retrieving file: %s' % uri) | |
| 542 file = None | |
| 543 return file | |
| 544 | |
| 545 | |
| 546 class ShellCommand(object): | |
| 547 | |
| 548 def __init__(self, command, wait=True, fail_fast=False, cwd=None): | |
| 549 self.exit_code = 0 | |
| 550 self.command = command | |
| 551 self.log_fp = StringIO() | |
| 552 self.wait = wait | |
| 553 self.fail_fast = fail_fast | |
| 554 self.run(cwd=cwd) | |
| 555 | |
| 556 def run(self, cwd=None): | |
| 557 boto.log.info('running:%s' % self.command) | |
| 558 self.process = subprocess.Popen(self.command, shell=True, | |
| 559 stdin=subprocess.PIPE, | |
| 560 stdout=subprocess.PIPE, | |
| 561 stderr=subprocess.PIPE, | |
| 562 cwd=cwd) | |
| 563 if(self.wait): | |
| 564 while self.process.poll() is None: | |
| 565 time.sleep(1) | |
| 566 t = self.process.communicate() | |
| 567 self.log_fp.write(t[0]) | |
| 568 self.log_fp.write(t[1]) | |
| 569 boto.log.info(self.log_fp.getvalue()) | |
| 570 self.exit_code = self.process.returncode | |
| 571 | |
| 572 if self.fail_fast and self.exit_code != 0: | |
| 573 raise Exception("Command " + self.command + | |
| 574 " failed with status " + self.exit_code) | |
| 575 | |
| 576 return self.exit_code | |
| 577 | |
| 578 def setReadOnly(self, value): | |
| 579 raise AttributeError | |
| 580 | |
| 581 def getStatus(self): | |
| 582 return self.exit_code | |
| 583 | |
| 584 status = property(getStatus, setReadOnly, None, | |
| 585 'The exit code for the command') | |
| 586 | |
| 587 def getOutput(self): | |
| 588 return self.log_fp.getvalue() | |
| 589 | |
| 590 output = property(getOutput, setReadOnly, None, | |
| 591 'The STDIN and STDERR output of the command') | |
| 592 | |
| 593 | |
| 594 class AuthSMTPHandler(logging.handlers.SMTPHandler): | |
| 595 """ | |
| 596 This class extends the SMTPHandler in the standard Python logging module | |
| 597 to accept a username and password on the constructor and to then use those | |
| 598 credentials to authenticate with the SMTP server. To use this, you could | |
| 599 add something like this in your boto config file: | |
| 600 | |
| 601 [handler_hand07] | |
| 602 class=boto.utils.AuthSMTPHandler | |
| 603 level=WARN | |
| 604 formatter=form07 | |
| 605 args=('localhost', 'username', 'password', 'from@abc', ['user1@abc', 'user2@xyz'], 'Logger Subject') | |
| 606 """ | |
| 607 | |
| 608 def __init__(self, mailhost, username, password, | |
| 609 fromaddr, toaddrs, subject): | |
| 610 """ | |
| 611 Initialize the handler. | |
| 612 | |
| 613 We have extended the constructor to accept a username/password | |
| 614 for SMTP authentication. | |
| 615 """ | |
| 616 super(AuthSMTPHandler, self).__init__(mailhost, fromaddr, | |
| 617 toaddrs, subject) | |
| 618 self.username = username | |
| 619 self.password = password | |
| 620 | |
| 621 def emit(self, record): | |
| 622 """ | |
| 623 Emit a record. | |
| 624 | |
| 625 Format the record and send it to the specified addressees. | |
| 626 It would be really nice if I could add authorization to this class | |
| 627 without having to resort to cut and paste inheritance but, no. | |
| 628 """ | |
| 629 try: | |
| 630 port = self.mailport | |
| 631 if not port: | |
| 632 port = smtplib.SMTP_PORT | |
| 633 smtp = smtplib.SMTP(self.mailhost, port) | |
| 634 smtp.login(self.username, self.password) | |
| 635 msg = self.format(record) | |
| 636 msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" % ( | |
| 637 self.fromaddr, | |
| 638 ','.join(self.toaddrs), | |
| 639 self.getSubject(record), | |
| 640 email.utils.formatdate(), msg) | |
| 641 smtp.sendmail(self.fromaddr, self.toaddrs, msg) | |
| 642 smtp.quit() | |
| 643 except (KeyboardInterrupt, SystemExit): | |
| 644 raise | |
| 645 except: | |
| 646 self.handleError(record) | |
| 647 | |
| 648 | |
| 649 class LRUCache(dict): | |
| 650 """A dictionary-like object that stores only a certain number of items, and | |
| 651 discards its least recently used item when full. | |
| 652 | |
| 653 >>> cache = LRUCache(3) | |
| 654 >>> cache['A'] = 0 | |
| 655 >>> cache['B'] = 1 | |
| 656 >>> cache['C'] = 2 | |
| 657 >>> len(cache) | |
| 658 3 | |
| 659 | |
| 660 >>> cache['A'] | |
| 661 0 | |
| 662 | |
| 663 Adding new items to the cache does not increase its size. Instead, the least | |
| 664 recently used item is dropped: | |
| 665 | |
| 666 >>> cache['D'] = 3 | |
| 667 >>> len(cache) | |
| 668 3 | |
| 669 >>> 'B' in cache | |
| 670 False | |
| 671 | |
| 672 Iterating over the cache returns the keys, starting with the most recently | |
| 673 used: | |
| 674 | |
| 675 >>> for key in cache: | |
| 676 ... print key | |
| 677 D | |
| 678 A | |
| 679 C | |
| 680 | |
| 681 This code is based on the LRUCache class from Genshi which is based on | |
| 682 `Myghty <http://www.myghty.org>`_'s LRUCache from ``myghtyutils.util``, | |
| 683 written by Mike Bayer and released under the MIT license (Genshi uses the | |
| 684 BSD License). | |
| 685 """ | |
| 686 | |
| 687 class _Item(object): | |
| 688 def __init__(self, key, value): | |
| 689 self.previous = self.next = None | |
| 690 self.key = key | |
| 691 self.value = value | |
| 692 | |
| 693 def __repr__(self): | |
| 694 return repr(self.value) | |
| 695 | |
| 696 def __init__(self, capacity): | |
| 697 self._dict = dict() | |
| 698 self.capacity = capacity | |
| 699 self.head = None | |
| 700 self.tail = None | |
| 701 | |
| 702 def __contains__(self, key): | |
| 703 return key in self._dict | |
| 704 | |
| 705 def __iter__(self): | |
| 706 cur = self.head | |
| 707 while cur: | |
| 708 yield cur.key | |
| 709 cur = cur.next | |
| 710 | |
| 711 def __len__(self): | |
| 712 return len(self._dict) | |
| 713 | |
| 714 def __getitem__(self, key): | |
| 715 item = self._dict[key] | |
| 716 self._update_item(item) | |
| 717 return item.value | |
| 718 | |
| 719 def __setitem__(self, key, value): | |
| 720 item = self._dict.get(key) | |
| 721 if item is None: | |
| 722 item = self._Item(key, value) | |
| 723 self._dict[key] = item | |
| 724 self._insert_item(item) | |
| 725 else: | |
| 726 item.value = value | |
| 727 self._update_item(item) | |
| 728 self._manage_size() | |
| 729 | |
| 730 def __repr__(self): | |
| 731 return repr(self._dict) | |
| 732 | |
| 733 def _insert_item(self, item): | |
| 734 item.previous = None | |
| 735 item.next = self.head | |
| 736 if self.head is not None: | |
| 737 self.head.previous = item | |
| 738 else: | |
| 739 self.tail = item | |
| 740 self.head = item | |
| 741 self._manage_size() | |
| 742 | |
| 743 def _manage_size(self): | |
| 744 while len(self._dict) > self.capacity: | |
| 745 del self._dict[self.tail.key] | |
| 746 if self.tail != self.head: | |
| 747 self.tail = self.tail.previous | |
| 748 self.tail.next = None | |
| 749 else: | |
| 750 self.head = self.tail = None | |
| 751 | |
| 752 def _update_item(self, item): | |
| 753 if self.head == item: | |
| 754 return | |
| 755 | |
| 756 previous = item.previous | |
| 757 previous.next = item.next | |
| 758 if item.next is not None: | |
| 759 item.next.previous = previous | |
| 760 else: | |
| 761 self.tail = previous | |
| 762 | |
| 763 item.previous = None | |
| 764 item.next = self.head | |
| 765 self.head.previous = self.head = item | |
| 766 | |
| 767 | |
| 768 class Password(object): | |
| 769 """ | |
| 770 Password object that stores itself as hashed. | |
| 771 Hash defaults to SHA512 if available, MD5 otherwise. | |
| 772 """ | |
| 773 hashfunc = _hashfn | |
| 774 | |
| 775 def __init__(self, str=None, hashfunc=None): | |
| 776 """ | |
| 777 Load the string from an initial value, this should be the | |
| 778 raw hashed password. | |
| 779 """ | |
| 780 self.str = str | |
| 781 if hashfunc: | |
| 782 self.hashfunc = hashfunc | |
| 783 | |
| 784 def set(self, value): | |
| 785 if not isinstance(value, bytes): | |
| 786 value = value.encode('utf-8') | |
| 787 self.str = self.hashfunc(value).hexdigest() | |
| 788 | |
| 789 def __str__(self): | |
| 790 return str(self.str) | |
| 791 | |
| 792 def __eq__(self, other): | |
| 793 if other is None: | |
| 794 return False | |
| 795 if not isinstance(other, bytes): | |
| 796 other = other.encode('utf-8') | |
| 797 return str(self.hashfunc(other).hexdigest()) == str(self.str) | |
| 798 | |
| 799 def __len__(self): | |
| 800 if self.str: | |
| 801 return len(self.str) | |
| 802 else: | |
| 803 return 0 | |
| 804 | |
| 805 | |
| 806 def notify(subject, body=None, html_body=None, to_string=None, | |
| 807 attachments=None, append_instance_id=True): | |
| 808 attachments = attachments or [] | |
| 809 if append_instance_id: | |
| 810 subject = "[%s] %s" % ( | |
| 811 boto.config.get_value("Instance", "instance-id"), subject) | |
| 812 if not to_string: | |
| 813 to_string = boto.config.get_value('Notification', 'smtp_to', None) | |
| 814 if to_string: | |
| 815 try: | |
| 816 from_string = boto.config.get_value('Notification', | |
| 817 'smtp_from', 'boto') | |
| 818 msg = email.mime.multipart.MIMEMultipart() | |
| 819 msg['From'] = from_string | |
| 820 msg['Reply-To'] = from_string | |
| 821 msg['To'] = to_string | |
| 822 msg['Date'] = email.utils.formatdate(localtime=True) | |
| 823 msg['Subject'] = subject | |
| 824 | |
| 825 if body: | |
| 826 msg.attach(email.mime.text.MIMEText(body)) | |
| 827 | |
| 828 if html_body: | |
| 829 part = email.mime.base.MIMEBase('text', 'html') | |
| 830 part.set_payload(html_body) | |
| 831 email.encoders.encode_base64(part) | |
| 832 msg.attach(part) | |
| 833 | |
| 834 for part in attachments: | |
| 835 msg.attach(part) | |
| 836 | |
| 837 smtp_host = boto.config.get_value('Notification', | |
| 838 'smtp_host', 'localhost') | |
| 839 | |
| 840 # Alternate port support | |
| 841 if boto.config.get_value("Notification", "smtp_port"): | |
| 842 server = smtplib.SMTP(smtp_host, int( | |
| 843 boto.config.get_value("Notification", "smtp_port"))) | |
| 844 else: | |
| 845 server = smtplib.SMTP(smtp_host) | |
| 846 | |
| 847 # TLS support | |
| 848 if boto.config.getbool("Notification", "smtp_tls"): | |
| 849 server.ehlo() | |
| 850 server.starttls() | |
| 851 server.ehlo() | |
| 852 smtp_user = boto.config.get_value('Notification', 'smtp_user', '') | |
| 853 smtp_pass = boto.config.get_value('Notification', 'smtp_pass', '') | |
| 854 if smtp_user: | |
| 855 server.login(smtp_user, smtp_pass) | |
| 856 server.sendmail(from_string, to_string, msg.as_string()) | |
| 857 server.quit() | |
| 858 except: | |
| 859 boto.log.exception('notify failed') | |
| 860 | |
| 861 | |
| 862 def get_utf8_value(value): | |
| 863 if not six.PY2 and isinstance(value, bytes): | |
| 864 return value | |
| 865 | |
| 866 if not isinstance(value, six.string_types): | |
| 867 value = six.text_type(value) | |
| 868 | |
| 869 if isinstance(value, six.text_type): | |
| 870 value = value.encode('utf-8') | |
| 871 | |
| 872 return value | |
| 873 | |
| 874 | |
| 875 def mklist(value): | |
| 876 if not isinstance(value, list): | |
| 877 if isinstance(value, tuple): | |
| 878 value = list(value) | |
| 879 else: | |
| 880 value = [value] | |
| 881 return value | |
| 882 | |
| 883 | |
| 884 def pythonize_name(name): | |
| 885 """Convert camel case to a "pythonic" name. | |
| 886 | |
| 887 Examples:: | |
| 888 | |
| 889 pythonize_name('CamelCase') -> 'camel_case' | |
| 890 pythonize_name('already_pythonized') -> 'already_pythonized' | |
| 891 pythonize_name('HTTPRequest') -> 'http_request' | |
| 892 pythonize_name('HTTPStatus200Ok') -> 'http_status_200_ok' | |
| 893 pythonize_name('UPPER') -> 'upper' | |
| 894 pythonize_name('') -> '' | |
| 895 | |
| 896 """ | |
| 897 s1 = _first_cap_regex.sub(r'\1_\2', name) | |
| 898 s2 = _number_cap_regex.sub(r'\1_\2', s1) | |
| 899 return _end_cap_regex.sub(r'\1_\2', s2).lower() | |
| 900 | |
| 901 | |
| 902 def write_mime_multipart(content, compress=False, deftype='text/plain', delimiter=':'): | |
| 903 """Description: | |
| 904 :param content: A list of tuples of name-content pairs. This is used | |
| 905 instead of a dict to ensure that scripts run in order | |
| 906 :type list of tuples: | |
| 907 | |
| 908 :param compress: Use gzip to compress the scripts, defaults to no compression | |
| 909 :type bool: | |
| 910 | |
| 911 :param deftype: The type that should be assumed if nothing else can be figured out | |
| 912 :type str: | |
| 913 | |
| 914 :param delimiter: mime delimiter | |
| 915 :type str: | |
| 916 | |
| 917 :return: Final mime multipart | |
| 918 :rtype: str: | |
| 919 """ | |
| 920 wrapper = email.mime.multipart.MIMEMultipart() | |
| 921 for name, con in content: | |
| 922 definite_type = guess_mime_type(con, deftype) | |
| 923 maintype, subtype = definite_type.split('/', 1) | |
| 924 if maintype == 'text': | |
| 925 mime_con = email.mime.text.MIMEText(con, _subtype=subtype) | |
| 926 else: | |
| 927 mime_con = email.mime.base.MIMEBase(maintype, subtype) | |
| 928 mime_con.set_payload(con) | |
| 929 # Encode the payload using Base64 | |
| 930 email.encoders.encode_base64(mime_con) | |
| 931 mime_con.add_header('Content-Disposition', 'attachment', filename=name) | |
| 932 wrapper.attach(mime_con) | |
| 933 rcontent = wrapper.as_string() | |
| 934 | |
| 935 if compress: | |
| 936 buf = StringIO() | |
| 937 gz = gzip.GzipFile(mode='wb', fileobj=buf) | |
| 938 try: | |
| 939 gz.write(rcontent) | |
| 940 finally: | |
| 941 gz.close() | |
| 942 rcontent = buf.getvalue() | |
| 943 | |
| 944 return rcontent | |
| 945 | |
| 946 | |
| 947 def guess_mime_type(content, deftype): | |
| 948 """Description: Guess the mime type of a block of text | |
| 949 :param content: content we're finding the type of | |
| 950 :type str: | |
| 951 | |
| 952 :param deftype: Default mime type | |
| 953 :type str: | |
| 954 | |
| 955 :rtype: <type>: | |
| 956 :return: <description> | |
| 957 """ | |
| 958 # Mappings recognized by cloudinit | |
| 959 starts_with_mappings = { | |
| 960 '#include': 'text/x-include-url', | |
| 961 '#!': 'text/x-shellscript', | |
| 962 '#cloud-config': 'text/cloud-config', | |
| 963 '#upstart-job': 'text/upstart-job', | |
| 964 '#part-handler': 'text/part-handler', | |
| 965 '#cloud-boothook': 'text/cloud-boothook' | |
| 966 } | |
| 967 rtype = deftype | |
| 968 for possible_type, mimetype in starts_with_mappings.items(): | |
| 969 if content.startswith(possible_type): | |
| 970 rtype = mimetype | |
| 971 break | |
| 972 return(rtype) | |
| 973 | |
| 974 | |
| 975 def compute_md5(fp, buf_size=8192, size=None): | |
| 976 """ | |
| 977 Compute MD5 hash on passed file and return results in a tuple of values. | |
| 978 | |
| 979 :type fp: file | |
| 980 :param fp: File pointer to the file to MD5 hash. The file pointer | |
| 981 will be reset to its current location before the | |
| 982 method returns. | |
| 983 | |
| 984 :type buf_size: integer | |
| 985 :param buf_size: Number of bytes per read request. | |
| 986 | |
| 987 :type size: int | |
| 988 :param size: (optional) The Maximum number of bytes to read from | |
| 989 the file pointer (fp). This is useful when uploading | |
| 990 a file in multiple parts where the file is being | |
| 991 split inplace into different parts. Less bytes may | |
| 992 be available. | |
| 993 | |
| 994 :rtype: tuple | |
| 995 :return: A tuple containing the hex digest version of the MD5 hash | |
| 996 as the first element, the base64 encoded version of the | |
| 997 plain digest as the second element and the data size as | |
| 998 the third element. | |
| 999 """ | |
| 1000 return compute_hash(fp, buf_size, size, hash_algorithm=md5) | |
| 1001 | |
| 1002 | |
| 1003 def compute_hash(fp, buf_size=8192, size=None, hash_algorithm=md5): | |
| 1004 hash_obj = hash_algorithm() | |
| 1005 spos = fp.tell() | |
| 1006 if size and size < buf_size: | |
| 1007 s = fp.read(size) | |
| 1008 else: | |
| 1009 s = fp.read(buf_size) | |
| 1010 while s: | |
| 1011 if not isinstance(s, bytes): | |
| 1012 s = s.encode('utf-8') | |
| 1013 hash_obj.update(s) | |
| 1014 if size: | |
| 1015 size -= len(s) | |
| 1016 if size <= 0: | |
| 1017 break | |
| 1018 if size and size < buf_size: | |
| 1019 s = fp.read(size) | |
| 1020 else: | |
| 1021 s = fp.read(buf_size) | |
| 1022 hex_digest = hash_obj.hexdigest() | |
| 1023 base64_digest = encodebytes(hash_obj.digest()).decode('utf-8') | |
| 1024 if base64_digest[-1] == '\n': | |
| 1025 base64_digest = base64_digest[0:-1] | |
| 1026 # data_size based on bytes read. | |
| 1027 data_size = fp.tell() - spos | |
| 1028 fp.seek(spos) | |
| 1029 return (hex_digest, base64_digest, data_size) | |
| 1030 | |
| 1031 | |
| 1032 def find_matching_headers(name, headers): | |
| 1033 """ | |
| 1034 Takes a specific header name and a dict of headers {"name": "value"}. | |
| 1035 Returns a list of matching header names, case-insensitive. | |
| 1036 | |
| 1037 """ | |
| 1038 return [h for h in headers if h.lower() == name.lower()] | |
| 1039 | |
| 1040 | |
| 1041 def merge_headers_by_name(name, headers): | |
| 1042 """ | |
| 1043 Takes a specific header name and a dict of headers {"name": "value"}. | |
| 1044 Returns a string of all header values, comma-separated, that match the | |
| 1045 input header name, case-insensitive. | |
| 1046 | |
| 1047 """ | |
| 1048 matching_headers = find_matching_headers(name, headers) | |
| 1049 return ','.join(str(headers[h]) for h in matching_headers | |
| 1050 if headers[h] is not None) | |
| 1051 | |
| 1052 | |
| 1053 class RequestHook(object): | |
| 1054 """ | |
| 1055 This can be extended and supplied to the connection object | |
| 1056 to gain access to request and response object after the request completes. | |
| 1057 One use for this would be to implement some specific request logging. | |
| 1058 """ | |
| 1059 def handle_request_data(self, request, response, error=False): | |
| 1060 pass | |
| 1061 | |
| 1062 | |
| 1063 def host_is_ipv6(hostname): | |
| 1064 """ | |
| 1065 Detect (naively) if the hostname is an IPV6 host. | |
| 1066 Return a boolean. | |
| 1067 """ | |
| 1068 # empty strings or anything that is not a string is automatically not an | |
| 1069 # IPV6 address | |
| 1070 if not hostname or not isinstance(hostname, str): | |
| 1071 return False | |
| 1072 | |
| 1073 if hostname.startswith('['): | |
| 1074 return True | |
| 1075 | |
| 1076 if len(hostname.split(':')) > 2: | |
| 1077 return True | |
| 1078 | |
| 1079 # Anything else that doesn't start with brackets or doesn't have more than | |
| 1080 # one ':' should not be an IPV6 address. This is very naive but the rest of | |
| 1081 # the connection chain should error accordingly for typos or ill formed | |
| 1082 # addresses | |
| 1083 return False | |
| 1084 | |
| 1085 | |
| 1086 def parse_host(hostname): | |
| 1087 """ | |
| 1088 Given a hostname that may have a port name, ensure that the port is trimmed | |
| 1089 returning only the host, including hostnames that are IPV6 and may include | |
| 1090 brackets. | |
| 1091 """ | |
| 1092 # ensure that hostname does not have any whitespaces | |
| 1093 hostname = hostname.strip() | |
| 1094 | |
| 1095 if host_is_ipv6(hostname): | |
| 1096 return hostname.split(']:', 1)[0].strip('[]') | |
| 1097 else: | |
| 1098 return hostname.split(':', 1)[0] |
