mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-02 17:33:37 +00:00
1098 lines
36 KiB
Python
1098 lines
36 KiB
Python
# Copyright (c) 2006-2012 Mitch Garnaat http://garnaat.org/
|
|
# Copyright (c) 2010, Eucalyptus Systems, Inc.
|
|
# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
|
|
# All rights reserved.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
# copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish, dis-
|
|
# tribute, sublicense, and/or sell copies of the Software, and to permit
|
|
# persons to whom the Software is furnished to do so, subject to the fol-
|
|
# lowing conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included
|
|
# in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
|
|
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
|
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
# IN THE SOFTWARE.
|
|
|
|
#
|
|
# Parts of this code were copied or derived from sample code supplied by AWS.
|
|
# The following notice applies to that code.
|
|
#
|
|
# This software code is made available "AS IS" without warranties of any
|
|
# kind. You may copy, display, modify and redistribute the software
|
|
# code either by itself or as incorporated into your code; provided that
|
|
# you do not remove any proprietary notices. Your use of this software
|
|
# code is at your own risk and you waive any claim against Amazon
|
|
# Digital Services, Inc. or its affiliates with respect to your use of
|
|
# this software code. (c) 2006 Amazon Digital Services, Inc. or its
|
|
# affiliates.
|
|
|
|
"""
|
|
Some handy utility functions used by several classes.
|
|
"""
|
|
|
|
import subprocess
|
|
import time
|
|
import logging.handlers
|
|
import boto
|
|
import boto.provider
|
|
import tempfile
|
|
import random
|
|
import smtplib
|
|
import datetime
|
|
import re
|
|
import email.mime.multipart
|
|
import email.mime.base
|
|
import email.mime.text
|
|
import email.utils
|
|
import email.encoders
|
|
import gzip
|
|
import threading
|
|
import locale
|
|
from boto.compat import six, StringIO, urllib, encodebytes
|
|
|
|
from contextlib import contextmanager
|
|
|
|
from hashlib import md5, sha512
|
|
_hashfn = sha512
|
|
|
|
from boto.compat import json
|
|
|
|
try:
|
|
from boto.compat.json import JSONDecodeError
|
|
except ImportError:
|
|
JSONDecodeError = ValueError
|
|
|
|
# List of Query String Arguments of Interest
|
|
qsa_of_interest = ['acl', 'cors', 'defaultObjectAcl', 'location', 'logging',
|
|
'partNumber', 'policy', 'requestPayment', 'torrent',
|
|
'versioning', 'versionId', 'versions', 'website',
|
|
'uploads', 'uploadId', 'response-content-type',
|
|
'response-content-language', 'response-expires',
|
|
'response-cache-control', 'response-content-disposition',
|
|
'response-content-encoding', 'delete', 'lifecycle',
|
|
'tagging', 'restore',
|
|
# storageClass is a QSA for buckets in Google Cloud Storage.
|
|
# (StorageClass is associated to individual keys in S3, but
|
|
# having it listed here should cause no problems because
|
|
# GET bucket?storageClass is not part of the S3 API.)
|
|
'storageClass',
|
|
# websiteConfig is a QSA for buckets in Google Cloud
|
|
# Storage.
|
|
'websiteConfig',
|
|
# compose is a QSA for objects in Google Cloud Storage.
|
|
'compose',
|
|
# billing is a QSA for buckets in Google Cloud Storage.
|
|
'billing',
|
|
# userProject is a QSA for requests in Google Cloud Storage.
|
|
'userProject',
|
|
# encryptionConfig is a QSA for requests in Google Cloud
|
|
# Storage.
|
|
'encryptionConfig']
|
|
|
|
|
|
_first_cap_regex = re.compile('(.)([A-Z][a-z]+)')
|
|
_number_cap_regex = re.compile('([a-z])([0-9]+)')
|
|
_end_cap_regex = re.compile('([a-z0-9])([A-Z])')
|
|
|
|
|
|
def unquote_v(nv):
|
|
if len(nv) == 1:
|
|
return nv
|
|
else:
|
|
return (nv[0], urllib.parse.unquote(nv[1]))
|
|
|
|
|
|
def canonical_string(method, path, headers, expires=None,
|
|
provider=None):
|
|
"""
|
|
Generates the aws canonical string for the given parameters
|
|
"""
|
|
if not provider:
|
|
provider = boto.provider.get_default()
|
|
interesting_headers = {}
|
|
for key in headers:
|
|
lk = key.lower()
|
|
if headers[key] is not None and \
|
|
(lk in ['content-md5', 'content-type', 'date'] or
|
|
lk.startswith(provider.header_prefix)):
|
|
interesting_headers[lk] = str(headers[key]).strip()
|
|
|
|
# these keys get empty strings if they don't exist
|
|
if 'content-type' not in interesting_headers:
|
|
interesting_headers['content-type'] = ''
|
|
if 'content-md5' not in interesting_headers:
|
|
interesting_headers['content-md5'] = ''
|
|
|
|
# just in case someone used this. it's not necessary in this lib.
|
|
if provider.date_header in interesting_headers:
|
|
interesting_headers['date'] = ''
|
|
|
|
# if you're using expires for query string auth, then it trumps date
|
|
# (and provider.date_header)
|
|
if expires:
|
|
interesting_headers['date'] = str(expires)
|
|
|
|
sorted_header_keys = sorted(interesting_headers.keys())
|
|
|
|
buf = "%s\n" % method
|
|
for key in sorted_header_keys:
|
|
val = interesting_headers[key]
|
|
if key.startswith(provider.header_prefix):
|
|
buf += "%s:%s\n" % (key, val)
|
|
else:
|
|
buf += "%s\n" % val
|
|
|
|
# don't include anything after the first ? in the resource...
|
|
# unless it is one of the QSA of interest, defined above
|
|
t = path.split('?')
|
|
buf += t[0]
|
|
|
|
if len(t) > 1:
|
|
qsa = t[1].split('&')
|
|
qsa = [a.split('=', 1) for a in qsa]
|
|
qsa = [unquote_v(a) for a in qsa if a[0] in qsa_of_interest]
|
|
if len(qsa) > 0:
|
|
qsa.sort(key=lambda x: x[0])
|
|
qsa = ['='.join(a) for a in qsa]
|
|
buf += '?'
|
|
buf += '&'.join(qsa)
|
|
|
|
return buf
|
|
|
|
|
|
def merge_meta(headers, metadata, provider=None):
|
|
if not provider:
|
|
provider = boto.provider.get_default()
|
|
metadata_prefix = provider.metadata_prefix
|
|
final_headers = headers.copy()
|
|
for k in metadata.keys():
|
|
if k.lower() in boto.s3.key.Key.base_user_settable_fields:
|
|
final_headers[k] = metadata[k]
|
|
else:
|
|
final_headers[metadata_prefix + k] = metadata[k]
|
|
|
|
return final_headers
|
|
|
|
|
|
def get_aws_metadata(headers, provider=None):
|
|
if not provider:
|
|
provider = boto.provider.get_default()
|
|
metadata_prefix = provider.metadata_prefix
|
|
metadata = {}
|
|
for hkey in headers.keys():
|
|
if hkey.lower().startswith(metadata_prefix):
|
|
val = urllib.parse.unquote(headers[hkey])
|
|
if isinstance(val, bytes):
|
|
try:
|
|
val = val.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
# Just leave the value as-is
|
|
pass
|
|
metadata[hkey[len(metadata_prefix):]] = val
|
|
del headers[hkey]
|
|
return metadata
|
|
|
|
|
|
def retry_url(url, retry_on_404=True, num_retries=10, timeout=None):
|
|
"""
|
|
Retry a url. This is specifically used for accessing the metadata
|
|
service on an instance. Since this address should never be proxied
|
|
(for security reasons), we create a ProxyHandler with a NULL
|
|
dictionary to override any proxy settings in the environment.
|
|
"""
|
|
for i in range(0, num_retries):
|
|
try:
|
|
proxy_handler = urllib.request.ProxyHandler({})
|
|
opener = urllib.request.build_opener(proxy_handler)
|
|
req = urllib.request.Request(url)
|
|
r = opener.open(req, timeout=timeout)
|
|
result = r.read()
|
|
|
|
if(not isinstance(result, six.string_types) and
|
|
hasattr(result, 'decode')):
|
|
result = result.decode('utf-8')
|
|
|
|
return result
|
|
except urllib.error.HTTPError as e:
|
|
code = e.getcode()
|
|
if code == 404 and not retry_on_404:
|
|
return ''
|
|
except Exception as e:
|
|
boto.log.exception('Caught exception reading instance data')
|
|
# If not on the last iteration of the loop then sleep.
|
|
if i + 1 != num_retries:
|
|
boto.log.debug('Sleeping before retrying')
|
|
time.sleep(min(2 ** i,
|
|
boto.config.get('Boto', 'max_retry_delay', 60)))
|
|
boto.log.error('Unable to read instance data, giving up')
|
|
return ''
|
|
|
|
|
|
def _get_instance_metadata(url, num_retries, timeout=None):
|
|
return LazyLoadMetadata(url, num_retries, timeout)
|
|
|
|
|
|
class LazyLoadMetadata(dict):
|
|
def __init__(self, url, num_retries, timeout=None):
|
|
self._url = url
|
|
self._num_retries = num_retries
|
|
self._leaves = {}
|
|
self._dicts = []
|
|
self._timeout = timeout
|
|
data = boto.utils.retry_url(self._url, num_retries=self._num_retries, timeout=self._timeout)
|
|
if data:
|
|
fields = data.split('\n')
|
|
for field in fields:
|
|
if field.endswith('/'):
|
|
key = field[0:-1]
|
|
self._dicts.append(key)
|
|
else:
|
|
p = field.find('=')
|
|
if p > 0:
|
|
key = field[p + 1:]
|
|
resource = field[0:p] + '/openssh-key'
|
|
else:
|
|
key = resource = field
|
|
self._leaves[key] = resource
|
|
self[key] = None
|
|
|
|
def _materialize(self):
|
|
for key in self:
|
|
self[key]
|
|
|
|
def __getitem__(self, key):
|
|
if key not in self:
|
|
# allow dict to throw the KeyError
|
|
return super(LazyLoadMetadata, self).__getitem__(key)
|
|
|
|
# already loaded
|
|
val = super(LazyLoadMetadata, self).__getitem__(key)
|
|
if val is not None:
|
|
return val
|
|
|
|
if key in self._leaves:
|
|
resource = self._leaves[key]
|
|
last_exception = None
|
|
|
|
for i in range(0, self._num_retries):
|
|
try:
|
|
val = boto.utils.retry_url(
|
|
self._url + urllib.parse.quote(resource,
|
|
safe="/:"),
|
|
num_retries=self._num_retries,
|
|
timeout=self._timeout)
|
|
if val and val[0] == '{':
|
|
val = json.loads(val)
|
|
break
|
|
else:
|
|
p = val.find('\n')
|
|
if p > 0:
|
|
val = val.split('\n')
|
|
break
|
|
|
|
except JSONDecodeError as e:
|
|
boto.log.debug(
|
|
"encountered '%s' exception: %s" % (
|
|
e.__class__.__name__, e))
|
|
boto.log.debug(
|
|
'corrupted JSON data found: %s' % val)
|
|
last_exception = e
|
|
|
|
except Exception as e:
|
|
boto.log.debug("encountered unretryable" +
|
|
" '%s' exception, re-raising" % (
|
|
e.__class__.__name__))
|
|
last_exception = e
|
|
raise
|
|
|
|
boto.log.error("Caught exception reading meta data" +
|
|
" for the '%s' try" % (i + 1))
|
|
|
|
if i + 1 != self._num_retries:
|
|
next_sleep = min(
|
|
random.random() * 2 ** i,
|
|
boto.config.get('Boto', 'max_retry_delay', 60))
|
|
time.sleep(next_sleep)
|
|
else:
|
|
boto.log.error('Unable to read meta data, giving up')
|
|
boto.log.error(
|
|
"encountered '%s' exception: %s" % (
|
|
last_exception.__class__.__name__, last_exception))
|
|
raise last_exception
|
|
|
|
self[key] = val
|
|
elif key in self._dicts:
|
|
self[key] = LazyLoadMetadata(self._url + key + '/',
|
|
self._num_retries)
|
|
|
|
return super(LazyLoadMetadata, self).__getitem__(key)
|
|
|
|
def get(self, key, default=None):
|
|
try:
|
|
return self[key]
|
|
except KeyError:
|
|
return default
|
|
|
|
def values(self):
|
|
self._materialize()
|
|
return super(LazyLoadMetadata, self).values()
|
|
|
|
def items(self):
|
|
self._materialize()
|
|
return super(LazyLoadMetadata, self).items()
|
|
|
|
def __str__(self):
|
|
self._materialize()
|
|
return super(LazyLoadMetadata, self).__str__()
|
|
|
|
def __repr__(self):
|
|
self._materialize()
|
|
return super(LazyLoadMetadata, self).__repr__()
|
|
|
|
|
|
def _build_instance_metadata_url(url, version, path):
|
|
"""
|
|
Builds an EC2 metadata URL for fetching information about an instance.
|
|
|
|
Example:
|
|
|
|
>>> _build_instance_metadata_url('http://169.254.169.254', 'latest', 'meta-data/')
|
|
http://169.254.169.254/latest/meta-data/
|
|
|
|
:type url: string
|
|
:param url: URL to metadata service, e.g. 'http://169.254.169.254'
|
|
|
|
:type version: string
|
|
:param version: Version of the metadata to get, e.g. 'latest'
|
|
|
|
:type path: string
|
|
:param path: Path of the metadata to get, e.g. 'meta-data/'. If a trailing
|
|
slash is required it must be passed in with the path.
|
|
|
|
:return: The full metadata URL
|
|
"""
|
|
return '%s/%s/%s' % (url, version, path)
|
|
|
|
|
|
def get_instance_metadata(version='latest', url='http://169.254.169.254',
|
|
data='meta-data/', timeout=None, num_retries=5):
|
|
"""
|
|
Returns the instance metadata as a nested Python dictionary.
|
|
Simple values (e.g. local_hostname, hostname, etc.) will be
|
|
stored as string values. Values such as ancestor-ami-ids will
|
|
be stored in the dict as a list of string values. More complex
|
|
fields such as public-keys and will be stored as nested dicts.
|
|
|
|
If the timeout is specified, the connection to the specified url
|
|
will time out after the specified number of seconds.
|
|
|
|
"""
|
|
try:
|
|
metadata_url = _build_instance_metadata_url(url, version, data)
|
|
return _get_instance_metadata(metadata_url, num_retries=num_retries, timeout=timeout)
|
|
except urllib.error.URLError:
|
|
boto.log.exception("Exception caught when trying to retrieve "
|
|
"instance metadata for: %s", data)
|
|
return None
|
|
|
|
|
|
def get_instance_identity(version='latest', url='http://169.254.169.254',
|
|
timeout=None, num_retries=5):
|
|
"""
|
|
Returns the instance identity as a nested Python dictionary.
|
|
"""
|
|
iid = {}
|
|
base_url = _build_instance_metadata_url(url, version,
|
|
'dynamic/instance-identity/')
|
|
try:
|
|
data = retry_url(base_url, num_retries=num_retries, timeout=timeout)
|
|
fields = data.split('\n')
|
|
for field in fields:
|
|
val = retry_url(base_url + '/' + field + '/', num_retries=num_retries, timeout=timeout)
|
|
if val[0] == '{':
|
|
val = json.loads(val)
|
|
if field:
|
|
iid[field] = val
|
|
return iid
|
|
except urllib.error.URLError:
|
|
return None
|
|
|
|
|
|
def get_instance_userdata(version='latest', sep=None,
|
|
url='http://169.254.169.254', timeout=None, num_retries=5):
|
|
ud_url = _build_instance_metadata_url(url, version, 'user-data')
|
|
user_data = retry_url(ud_url, retry_on_404=False, num_retries=num_retries, timeout=timeout)
|
|
if user_data:
|
|
if sep:
|
|
l = user_data.split(sep)
|
|
user_data = {}
|
|
for nvpair in l:
|
|
t = nvpair.split('=')
|
|
user_data[t[0].strip()] = t[1].strip()
|
|
return user_data
|
|
|
|
ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
|
|
ISO8601_MS = '%Y-%m-%dT%H:%M:%S.%fZ'
|
|
RFC1123 = '%a, %d %b %Y %H:%M:%S %Z'
|
|
LOCALE_LOCK = threading.Lock()
|
|
|
|
|
|
@contextmanager
|
|
def setlocale(name):
|
|
"""
|
|
A context manager to set the locale in a threadsafe manner.
|
|
"""
|
|
with LOCALE_LOCK:
|
|
saved = locale.setlocale(locale.LC_ALL)
|
|
|
|
try:
|
|
yield locale.setlocale(locale.LC_ALL, name)
|
|
finally:
|
|
locale.setlocale(locale.LC_ALL, saved)
|
|
|
|
|
|
def get_ts(ts=None):
|
|
if not ts:
|
|
ts = time.gmtime()
|
|
return time.strftime(ISO8601, ts)
|
|
|
|
|
|
def parse_ts(ts):
|
|
with setlocale('C'):
|
|
ts = ts.strip()
|
|
try:
|
|
dt = datetime.datetime.strptime(ts, ISO8601)
|
|
return dt
|
|
except ValueError:
|
|
try:
|
|
dt = datetime.datetime.strptime(ts, ISO8601_MS)
|
|
return dt
|
|
except ValueError:
|
|
dt = datetime.datetime.strptime(ts, RFC1123)
|
|
return dt
|
|
|
|
|
|
def find_class(module_name, class_name=None):
|
|
if class_name:
|
|
module_name = "%s.%s" % (module_name, class_name)
|
|
modules = module_name.split('.')
|
|
c = None
|
|
|
|
try:
|
|
for m in modules[1:]:
|
|
if c:
|
|
c = getattr(c, m)
|
|
else:
|
|
c = getattr(__import__(".".join(modules[0:-1])), m)
|
|
return c
|
|
except:
|
|
return None
|
|
|
|
|
|
def update_dme(username, password, dme_id, ip_address):
|
|
"""
|
|
Update your Dynamic DNS record with DNSMadeEasy.com
|
|
"""
|
|
dme_url = 'https://www.dnsmadeeasy.com/servlet/updateip'
|
|
dme_url += '?username=%s&password=%s&id=%s&ip=%s'
|
|
s = urllib.request.urlopen(dme_url % (username, password, dme_id, ip_address))
|
|
return s.read()
|
|
|
|
|
|
def fetch_file(uri, file=None, username=None, password=None):
|
|
"""
|
|
Fetch a file based on the URI provided.
|
|
If you do not pass in a file pointer a tempfile.NamedTemporaryFile,
|
|
or None if the file could not be retrieved is returned.
|
|
The URI can be either an HTTP url, or "s3://bucket_name/key_name"
|
|
"""
|
|
boto.log.info('Fetching %s' % uri)
|
|
if file is None:
|
|
file = tempfile.NamedTemporaryFile()
|
|
try:
|
|
if uri.startswith('s3://'):
|
|
bucket_name, key_name = uri[len('s3://'):].split('/', 1)
|
|
c = boto.connect_s3(aws_access_key_id=username,
|
|
aws_secret_access_key=password)
|
|
bucket = c.get_bucket(bucket_name)
|
|
key = bucket.get_key(key_name)
|
|
key.get_contents_to_file(file)
|
|
else:
|
|
if username and password:
|
|
passman = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
|
passman.add_password(None, uri, username, password)
|
|
authhandler = urllib.request.HTTPBasicAuthHandler(passman)
|
|
opener = urllib.request.build_opener(authhandler)
|
|
urllib.request.install_opener(opener)
|
|
s = urllib.request.urlopen(uri)
|
|
file.write(s.read())
|
|
file.seek(0)
|
|
except:
|
|
raise
|
|
boto.log.exception('Problem Retrieving file: %s' % uri)
|
|
file = None
|
|
return file
|
|
|
|
|
|
class ShellCommand(object):
|
|
|
|
def __init__(self, command, wait=True, fail_fast=False, cwd=None):
|
|
self.exit_code = 0
|
|
self.command = command
|
|
self.log_fp = StringIO()
|
|
self.wait = wait
|
|
self.fail_fast = fail_fast
|
|
self.run(cwd=cwd)
|
|
|
|
def run(self, cwd=None):
|
|
boto.log.info('running:%s' % self.command)
|
|
self.process = subprocess.Popen(self.command, shell=True,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
cwd=cwd)
|
|
if(self.wait):
|
|
while self.process.poll() is None:
|
|
time.sleep(1)
|
|
t = self.process.communicate()
|
|
self.log_fp.write(t[0])
|
|
self.log_fp.write(t[1])
|
|
boto.log.info(self.log_fp.getvalue())
|
|
self.exit_code = self.process.returncode
|
|
|
|
if self.fail_fast and self.exit_code != 0:
|
|
raise Exception("Command " + self.command +
|
|
" failed with status " + self.exit_code)
|
|
|
|
return self.exit_code
|
|
|
|
def setReadOnly(self, value):
|
|
raise AttributeError
|
|
|
|
def getStatus(self):
|
|
return self.exit_code
|
|
|
|
status = property(getStatus, setReadOnly, None,
|
|
'The exit code for the command')
|
|
|
|
def getOutput(self):
|
|
return self.log_fp.getvalue()
|
|
|
|
output = property(getOutput, setReadOnly, None,
|
|
'The STDIN and STDERR output of the command')
|
|
|
|
|
|
class AuthSMTPHandler(logging.handlers.SMTPHandler):
|
|
"""
|
|
This class extends the SMTPHandler in the standard Python logging module
|
|
to accept a username and password on the constructor and to then use those
|
|
credentials to authenticate with the SMTP server. To use this, you could
|
|
add something like this in your boto config file:
|
|
|
|
[handler_hand07]
|
|
class=boto.utils.AuthSMTPHandler
|
|
level=WARN
|
|
formatter=form07
|
|
args=('localhost', 'username', 'password', 'from@abc', ['user1@abc', 'user2@xyz'], 'Logger Subject')
|
|
"""
|
|
|
|
def __init__(self, mailhost, username, password,
|
|
fromaddr, toaddrs, subject):
|
|
"""
|
|
Initialize the handler.
|
|
|
|
We have extended the constructor to accept a username/password
|
|
for SMTP authentication.
|
|
"""
|
|
super(AuthSMTPHandler, self).__init__(mailhost, fromaddr,
|
|
toaddrs, subject)
|
|
self.username = username
|
|
self.password = password
|
|
|
|
def emit(self, record):
|
|
"""
|
|
Emit a record.
|
|
|
|
Format the record and send it to the specified addressees.
|
|
It would be really nice if I could add authorization to this class
|
|
without having to resort to cut and paste inheritance but, no.
|
|
"""
|
|
try:
|
|
port = self.mailport
|
|
if not port:
|
|
port = smtplib.SMTP_PORT
|
|
smtp = smtplib.SMTP(self.mailhost, port)
|
|
smtp.login(self.username, self.password)
|
|
msg = self.format(record)
|
|
msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" % (
|
|
self.fromaddr,
|
|
','.join(self.toaddrs),
|
|
self.getSubject(record),
|
|
email.utils.formatdate(), msg)
|
|
smtp.sendmail(self.fromaddr, self.toaddrs, msg)
|
|
smtp.quit()
|
|
except (KeyboardInterrupt, SystemExit):
|
|
raise
|
|
except:
|
|
self.handleError(record)
|
|
|
|
|
|
class LRUCache(dict):
|
|
"""A dictionary-like object that stores only a certain number of items, and
|
|
discards its least recently used item when full.
|
|
|
|
>>> cache = LRUCache(3)
|
|
>>> cache['A'] = 0
|
|
>>> cache['B'] = 1
|
|
>>> cache['C'] = 2
|
|
>>> len(cache)
|
|
3
|
|
|
|
>>> cache['A']
|
|
0
|
|
|
|
Adding new items to the cache does not increase its size. Instead, the least
|
|
recently used item is dropped:
|
|
|
|
>>> cache['D'] = 3
|
|
>>> len(cache)
|
|
3
|
|
>>> 'B' in cache
|
|
False
|
|
|
|
Iterating over the cache returns the keys, starting with the most recently
|
|
used:
|
|
|
|
>>> for key in cache:
|
|
... print key
|
|
D
|
|
A
|
|
C
|
|
|
|
This code is based on the LRUCache class from Genshi which is based on
|
|
`Myghty <http://www.myghty.org>`_'s LRUCache from ``myghtyutils.util``,
|
|
written by Mike Bayer and released under the MIT license (Genshi uses the
|
|
BSD License).
|
|
"""
|
|
|
|
class _Item(object):
|
|
def __init__(self, key, value):
|
|
self.previous = self.next = None
|
|
self.key = key
|
|
self.value = value
|
|
|
|
def __repr__(self):
|
|
return repr(self.value)
|
|
|
|
def __init__(self, capacity):
|
|
self._dict = dict()
|
|
self.capacity = capacity
|
|
self.head = None
|
|
self.tail = None
|
|
|
|
def __contains__(self, key):
|
|
return key in self._dict
|
|
|
|
def __iter__(self):
|
|
cur = self.head
|
|
while cur:
|
|
yield cur.key
|
|
cur = cur.next
|
|
|
|
def __len__(self):
|
|
return len(self._dict)
|
|
|
|
def __getitem__(self, key):
|
|
item = self._dict[key]
|
|
self._update_item(item)
|
|
return item.value
|
|
|
|
def __setitem__(self, key, value):
|
|
item = self._dict.get(key)
|
|
if item is None:
|
|
item = self._Item(key, value)
|
|
self._dict[key] = item
|
|
self._insert_item(item)
|
|
else:
|
|
item.value = value
|
|
self._update_item(item)
|
|
self._manage_size()
|
|
|
|
def __repr__(self):
|
|
return repr(self._dict)
|
|
|
|
def _insert_item(self, item):
|
|
item.previous = None
|
|
item.next = self.head
|
|
if self.head is not None:
|
|
self.head.previous = item
|
|
else:
|
|
self.tail = item
|
|
self.head = item
|
|
self._manage_size()
|
|
|
|
def _manage_size(self):
|
|
while len(self._dict) > self.capacity:
|
|
del self._dict[self.tail.key]
|
|
if self.tail != self.head:
|
|
self.tail = self.tail.previous
|
|
self.tail.next = None
|
|
else:
|
|
self.head = self.tail = None
|
|
|
|
def _update_item(self, item):
|
|
if self.head == item:
|
|
return
|
|
|
|
previous = item.previous
|
|
previous.next = item.next
|
|
if item.next is not None:
|
|
item.next.previous = previous
|
|
else:
|
|
self.tail = previous
|
|
|
|
item.previous = None
|
|
item.next = self.head
|
|
self.head.previous = self.head = item
|
|
|
|
|
|
class Password(object):
|
|
"""
|
|
Password object that stores itself as hashed.
|
|
Hash defaults to SHA512 if available, MD5 otherwise.
|
|
"""
|
|
hashfunc = _hashfn
|
|
|
|
def __init__(self, str=None, hashfunc=None):
|
|
"""
|
|
Load the string from an initial value, this should be the
|
|
raw hashed password.
|
|
"""
|
|
self.str = str
|
|
if hashfunc:
|
|
self.hashfunc = hashfunc
|
|
|
|
def set(self, value):
|
|
if not isinstance(value, bytes):
|
|
value = value.encode('utf-8')
|
|
self.str = self.hashfunc(value).hexdigest()
|
|
|
|
def __str__(self):
|
|
return str(self.str)
|
|
|
|
def __eq__(self, other):
|
|
if other is None:
|
|
return False
|
|
if not isinstance(other, bytes):
|
|
other = other.encode('utf-8')
|
|
return str(self.hashfunc(other).hexdigest()) == str(self.str)
|
|
|
|
def __len__(self):
|
|
if self.str:
|
|
return len(self.str)
|
|
else:
|
|
return 0
|
|
|
|
|
|
def notify(subject, body=None, html_body=None, to_string=None,
|
|
attachments=None, append_instance_id=True):
|
|
attachments = attachments or []
|
|
if append_instance_id:
|
|
subject = "[%s] %s" % (
|
|
boto.config.get_value("Instance", "instance-id"), subject)
|
|
if not to_string:
|
|
to_string = boto.config.get_value('Notification', 'smtp_to', None)
|
|
if to_string:
|
|
try:
|
|
from_string = boto.config.get_value('Notification',
|
|
'smtp_from', 'boto')
|
|
msg = email.mime.multipart.MIMEMultipart()
|
|
msg['From'] = from_string
|
|
msg['Reply-To'] = from_string
|
|
msg['To'] = to_string
|
|
msg['Date'] = email.utils.formatdate(localtime=True)
|
|
msg['Subject'] = subject
|
|
|
|
if body:
|
|
msg.attach(email.mime.text.MIMEText(body))
|
|
|
|
if html_body:
|
|
part = email.mime.base.MIMEBase('text', 'html')
|
|
part.set_payload(html_body)
|
|
email.encoders.encode_base64(part)
|
|
msg.attach(part)
|
|
|
|
for part in attachments:
|
|
msg.attach(part)
|
|
|
|
smtp_host = boto.config.get_value('Notification',
|
|
'smtp_host', 'localhost')
|
|
|
|
# Alternate port support
|
|
if boto.config.get_value("Notification", "smtp_port"):
|
|
server = smtplib.SMTP(smtp_host, int(
|
|
boto.config.get_value("Notification", "smtp_port")))
|
|
else:
|
|
server = smtplib.SMTP(smtp_host)
|
|
|
|
# TLS support
|
|
if boto.config.getbool("Notification", "smtp_tls"):
|
|
server.ehlo()
|
|
server.starttls()
|
|
server.ehlo()
|
|
smtp_user = boto.config.get_value('Notification', 'smtp_user', '')
|
|
smtp_pass = boto.config.get_value('Notification', 'smtp_pass', '')
|
|
if smtp_user:
|
|
server.login(smtp_user, smtp_pass)
|
|
server.sendmail(from_string, to_string, msg.as_string())
|
|
server.quit()
|
|
except:
|
|
boto.log.exception('notify failed')
|
|
|
|
|
|
def get_utf8_value(value):
|
|
if not six.PY2 and isinstance(value, bytes):
|
|
return value
|
|
|
|
if not isinstance(value, six.string_types):
|
|
value = six.text_type(value)
|
|
|
|
if isinstance(value, six.text_type):
|
|
value = value.encode('utf-8')
|
|
|
|
return value
|
|
|
|
|
|
def mklist(value):
|
|
if not isinstance(value, list):
|
|
if isinstance(value, tuple):
|
|
value = list(value)
|
|
else:
|
|
value = [value]
|
|
return value
|
|
|
|
|
|
def pythonize_name(name):
|
|
"""Convert camel case to a "pythonic" name.
|
|
|
|
Examples::
|
|
|
|
pythonize_name('CamelCase') -> 'camel_case'
|
|
pythonize_name('already_pythonized') -> 'already_pythonized'
|
|
pythonize_name('HTTPRequest') -> 'http_request'
|
|
pythonize_name('HTTPStatus200Ok') -> 'http_status_200_ok'
|
|
pythonize_name('UPPER') -> 'upper'
|
|
pythonize_name('') -> ''
|
|
|
|
"""
|
|
s1 = _first_cap_regex.sub(r'\1_\2', name)
|
|
s2 = _number_cap_regex.sub(r'\1_\2', s1)
|
|
return _end_cap_regex.sub(r'\1_\2', s2).lower()
|
|
|
|
|
|
def write_mime_multipart(content, compress=False, deftype='text/plain', delimiter=':'):
|
|
"""Description:
|
|
:param content: A list of tuples of name-content pairs. This is used
|
|
instead of a dict to ensure that scripts run in order
|
|
:type list of tuples:
|
|
|
|
:param compress: Use gzip to compress the scripts, defaults to no compression
|
|
:type bool:
|
|
|
|
:param deftype: The type that should be assumed if nothing else can be figured out
|
|
:type str:
|
|
|
|
:param delimiter: mime delimiter
|
|
:type str:
|
|
|
|
:return: Final mime multipart
|
|
:rtype: str:
|
|
"""
|
|
wrapper = email.mime.multipart.MIMEMultipart()
|
|
for name, con in content:
|
|
definite_type = guess_mime_type(con, deftype)
|
|
maintype, subtype = definite_type.split('/', 1)
|
|
if maintype == 'text':
|
|
mime_con = email.mime.text.MIMEText(con, _subtype=subtype)
|
|
else:
|
|
mime_con = email.mime.base.MIMEBase(maintype, subtype)
|
|
mime_con.set_payload(con)
|
|
# Encode the payload using Base64
|
|
email.encoders.encode_base64(mime_con)
|
|
mime_con.add_header('Content-Disposition', 'attachment', filename=name)
|
|
wrapper.attach(mime_con)
|
|
rcontent = wrapper.as_string()
|
|
|
|
if compress:
|
|
buf = StringIO()
|
|
gz = gzip.GzipFile(mode='wb', fileobj=buf)
|
|
try:
|
|
gz.write(rcontent)
|
|
finally:
|
|
gz.close()
|
|
rcontent = buf.getvalue()
|
|
|
|
return rcontent
|
|
|
|
|
|
def guess_mime_type(content, deftype):
|
|
"""Description: Guess the mime type of a block of text
|
|
:param content: content we're finding the type of
|
|
:type str:
|
|
|
|
:param deftype: Default mime type
|
|
:type str:
|
|
|
|
:rtype: <type>:
|
|
:return: <description>
|
|
"""
|
|
# Mappings recognized by cloudinit
|
|
starts_with_mappings = {
|
|
'#include': 'text/x-include-url',
|
|
'#!': 'text/x-shellscript',
|
|
'#cloud-config': 'text/cloud-config',
|
|
'#upstart-job': 'text/upstart-job',
|
|
'#part-handler': 'text/part-handler',
|
|
'#cloud-boothook': 'text/cloud-boothook'
|
|
}
|
|
rtype = deftype
|
|
for possible_type, mimetype in starts_with_mappings.items():
|
|
if content.startswith(possible_type):
|
|
rtype = mimetype
|
|
break
|
|
return(rtype)
|
|
|
|
|
|
def compute_md5(fp, buf_size=8192, size=None):
|
|
"""
|
|
Compute MD5 hash on passed file and return results in a tuple of values.
|
|
|
|
:type fp: file
|
|
:param fp: File pointer to the file to MD5 hash. The file pointer
|
|
will be reset to its current location before the
|
|
method returns.
|
|
|
|
:type buf_size: integer
|
|
:param buf_size: Number of bytes per read request.
|
|
|
|
:type size: int
|
|
:param size: (optional) The Maximum number of bytes to read from
|
|
the file pointer (fp). This is useful when uploading
|
|
a file in multiple parts where the file is being
|
|
split inplace into different parts. Less bytes may
|
|
be available.
|
|
|
|
:rtype: tuple
|
|
:return: A tuple containing the hex digest version of the MD5 hash
|
|
as the first element, the base64 encoded version of the
|
|
plain digest as the second element and the data size as
|
|
the third element.
|
|
"""
|
|
return compute_hash(fp, buf_size, size, hash_algorithm=md5)
|
|
|
|
|
|
def compute_hash(fp, buf_size=8192, size=None, hash_algorithm=md5):
|
|
hash_obj = hash_algorithm()
|
|
spos = fp.tell()
|
|
if size and size < buf_size:
|
|
s = fp.read(size)
|
|
else:
|
|
s = fp.read(buf_size)
|
|
while s:
|
|
if not isinstance(s, bytes):
|
|
s = s.encode('utf-8')
|
|
hash_obj.update(s)
|
|
if size:
|
|
size -= len(s)
|
|
if size <= 0:
|
|
break
|
|
if size and size < buf_size:
|
|
s = fp.read(size)
|
|
else:
|
|
s = fp.read(buf_size)
|
|
hex_digest = hash_obj.hexdigest()
|
|
base64_digest = encodebytes(hash_obj.digest()).decode('utf-8')
|
|
if base64_digest[-1] == '\n':
|
|
base64_digest = base64_digest[0:-1]
|
|
# data_size based on bytes read.
|
|
data_size = fp.tell() - spos
|
|
fp.seek(spos)
|
|
return (hex_digest, base64_digest, data_size)
|
|
|
|
|
|
def find_matching_headers(name, headers):
|
|
"""
|
|
Takes a specific header name and a dict of headers {"name": "value"}.
|
|
Returns a list of matching header names, case-insensitive.
|
|
|
|
"""
|
|
return [h for h in headers if h.lower() == name.lower()]
|
|
|
|
|
|
def merge_headers_by_name(name, headers):
|
|
"""
|
|
Takes a specific header name and a dict of headers {"name": "value"}.
|
|
Returns a string of all header values, comma-separated, that match the
|
|
input header name, case-insensitive.
|
|
|
|
"""
|
|
matching_headers = find_matching_headers(name, headers)
|
|
return ','.join(str(headers[h]) for h in matching_headers
|
|
if headers[h] is not None)
|
|
|
|
|
|
class RequestHook(object):
|
|
"""
|
|
This can be extended and supplied to the connection object
|
|
to gain access to request and response object after the request completes.
|
|
One use for this would be to implement some specific request logging.
|
|
"""
|
|
def handle_request_data(self, request, response, error=False):
|
|
pass
|
|
|
|
|
|
def host_is_ipv6(hostname):
|
|
"""
|
|
Detect (naively) if the hostname is an IPV6 host.
|
|
Return a boolean.
|
|
"""
|
|
# empty strings or anything that is not a string is automatically not an
|
|
# IPV6 address
|
|
if not hostname or not isinstance(hostname, str):
|
|
return False
|
|
|
|
if hostname.startswith('['):
|
|
return True
|
|
|
|
if len(hostname.split(':')) > 2:
|
|
return True
|
|
|
|
# Anything else that doesn't start with brackets or doesn't have more than
|
|
# one ':' should not be an IPV6 address. This is very naive but the rest of
|
|
# the connection chain should error accordingly for typos or ill formed
|
|
# addresses
|
|
return False
|
|
|
|
|
|
def parse_host(hostname):
|
|
"""
|
|
Given a hostname that may have a port name, ensure that the port is trimmed
|
|
returning only the host, including hostnames that are IPV6 and may include
|
|
brackets.
|
|
"""
|
|
# ensure that hostname does not have any whitespaces
|
|
hostname = hostname.strip()
|
|
|
|
if host_is_ipv6(hostname):
|
|
return hostname.split(']:', 1)[0].strip('[]')
|
|
else:
|
|
return hostname.split(':', 1)[0]
|