Upgrade boto to 2.32.1

This commit is contained in:
Matthew Jones
2014-08-06 13:57:39 -04:00
parent 6f6f8675f9
commit 7899463ac8
183 changed files with 8546 additions and 2028 deletions

View File

@@ -7,7 +7,7 @@ anyjson==0.3.3 (anyjson/*)
argparse==1.2.1 (argparse.py, needed for Python 2.6 support) argparse==1.2.1 (argparse.py, needed for Python 2.6 support)
Babel==1.3 (babel/*, excluded bin/pybabel) Babel==1.3 (babel/*, excluded bin/pybabel)
billiard==3.3.0.16 (billiard/*, funtests/*, excluded _billiard.so) billiard==3.3.0.16 (billiard/*, funtests/*, excluded _billiard.so)
boto==2.27.0 (boto/*, excluded bin/asadmin, bin/bundle_image, bin/cfadmin, boto==2.32.1 (boto/*, excluded bin/asadmin, bin/bundle_image, bin/cfadmin,
bin/cq, bin/cwutil, bin/dynamodb_dump, bin/dynamodb_load, bin/elbadmin, bin/cq, bin/cwutil, bin/dynamodb_dump, bin/dynamodb_load, bin/elbadmin,
bin/fetch_file, bin/glacier, bin/instance_events, bin/kill_instance, bin/fetch_file, bin/glacier, bin/instance_events, bin/kill_instance,
bin/launch_instance, bin/list_instances, bin/lss3, bin/mturk, bin/launch_instance, bin/list_instances, bin/lss3, bin/mturk,

View File

@@ -34,10 +34,11 @@ import re
import sys import sys
import logging import logging
import logging.config import logging.config
import urlparse
from boto.compat import urlparse
from boto.exception import InvalidUriError from boto.exception import InvalidUriError
__version__ = '2.27.0' __version__ = '2.32.1'
Version = __version__ # for backware compatibility Version = __version__ # for backware compatibility
# http://bugs.python.org/issue7980 # http://bugs.python.org/issue7980
@@ -492,7 +493,7 @@ def connect_ec2_endpoint(url, aws_access_key_id=None,
""" """
from boto.ec2.regioninfo import RegionInfo from boto.ec2.regioninfo import RegionInfo
purl = urlparse.urlparse(url) purl = urlparse(url)
kwargs['port'] = purl.port kwargs['port'] = purl.port
kwargs['host'] = purl.hostname kwargs['host'] = purl.hostname
kwargs['path'] = purl.path kwargs['path'] = purl.path
@@ -653,7 +654,7 @@ def connect_cloudsearch(aws_access_key_id=None,
:type aws_secret_access_key: string :type aws_secret_access_key: string
:param aws_secret_access_key: Your AWS Secret Access Key :param aws_secret_access_key: Your AWS Secret Access Key
:rtype: :class:`boto.ec2.autoscale.CloudSearchConnection` :rtype: :class:`boto.cloudsearch.layer2.Layer2`
:return: A connection to Amazon's CloudSearch service :return: A connection to Amazon's CloudSearch service
""" """
from boto.cloudsearch.layer2 import Layer2 from boto.cloudsearch.layer2 import Layer2
@@ -661,6 +662,24 @@ def connect_cloudsearch(aws_access_key_id=None,
**kwargs) **kwargs)
def connect_cloudsearch2(aws_access_key_id=None,
aws_secret_access_key=None,
**kwargs):
"""
:type aws_access_key_id: string
:param aws_access_key_id: Your AWS Access Key ID
:type aws_secret_access_key: string
:param aws_secret_access_key: Your AWS Secret Access Key
:rtype: :class:`boto.cloudsearch2.layer2.Layer2`
:return: A connection to Amazon's CloudSearch2 service
"""
from boto.cloudsearch2.layer2 import Layer2
return Layer2(aws_access_key_id, aws_secret_access_key,
**kwargs)
def connect_beanstalk(aws_access_key_id=None, def connect_beanstalk(aws_access_key_id=None,
aws_secret_access_key=None, aws_secret_access_key=None,
**kwargs): **kwargs):
@@ -817,6 +836,28 @@ def connect_kinesis(aws_access_key_id=None,
**kwargs **kwargs
) )
def connect_logs(aws_access_key_id=None,
aws_secret_access_key=None,
**kwargs):
"""
Connect to Amazon CloudWatch Logs
:type aws_access_key_id: string
:param aws_access_key_id: Your AWS Access Key ID
:type aws_secret_access_key: string
:param aws_secret_access_key: Your AWS Secret Access Key
rtype: :class:`boto.kinesis.layer1.CloudWatchLogsConnection`
:return: A connection to the Amazon CloudWatch Logs service
"""
from boto.logs.layer1 import CloudWatchLogsConnection
return CloudWatchLogsConnection(
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
**kwargs
)
def storage_uri(uri_str, default_scheme='file', debug=0, validate=True, def storage_uri(uri_str, default_scheme='file', debug=0, validate=True,
bucket_storage_uri_class=BucketStorageUri, bucket_storage_uri_class=BucketStorageUri,
suppress_consec_slashes=True, is_latest=False): suppress_consec_slashes=True, is_latest=False):
@@ -861,7 +902,7 @@ def storage_uri(uri_str, default_scheme='file', debug=0, validate=True,
version_id = None version_id = None
generation = None generation = None
# Manually parse URI components instead of using urlparse.urlparse because # Manually parse URI components instead of using urlparse because
# what we're calling URIs don't really fit the standard syntax for URIs # what we're calling URIs don't really fit the standard syntax for URIs
# (the latter includes an optional host/net location part). # (the latter includes an optional host/net location part).
end_scheme_idx = uri_str.find('://') end_scheme_idx = uri_str.find('://')

View File

@@ -39,10 +39,9 @@ import hmac
import os import os
import sys import sys
import time import time
import urllib
import urlparse
import posixpath import posixpath
from boto.compat import urllib, encodebytes
from boto.auth_handler import AuthHandler from boto.auth_handler import AuthHandler
from boto.exception import BotoClientError from boto.exception import BotoClientError
@@ -65,9 +64,10 @@ class HmacKeys(object):
def update_provider(self, provider): def update_provider(self, provider):
self._provider = provider self._provider = provider
self._hmac = hmac.new(self._provider.secret_key, digestmod=sha) self._hmac = hmac.new(self._provider.secret_key.encode('utf-8'),
digestmod=sha)
if sha256: if sha256:
self._hmac_256 = hmac.new(self._provider.secret_key, self._hmac_256 = hmac.new(self._provider.secret_key.encode('utf-8'),
digestmod=sha256) digestmod=sha256)
else: else:
self._hmac_256 = None self._hmac_256 = None
@@ -83,13 +83,13 @@ class HmacKeys(object):
digestmod = sha256 digestmod = sha256
else: else:
digestmod = sha digestmod = sha
return hmac.new(self._provider.secret_key, return hmac.new(self._provider.secret_key.encode('utf-8'),
digestmod=digestmod) digestmod=digestmod)
def sign_string(self, string_to_sign): def sign_string(self, string_to_sign):
new_hmac = self._get_hmac() new_hmac = self._get_hmac()
new_hmac.update(string_to_sign) new_hmac.update(string_to_sign.encode('utf-8'))
return base64.encodestring(new_hmac.digest()).strip() return encodebytes(new_hmac.digest()).decode('utf-8').strip()
def __getstate__(self): def __getstate__(self):
pickled_dict = copy.copy(self.__dict__) pickled_dict = copy.copy(self.__dict__)
@@ -271,7 +271,7 @@ class HmacAuthV3HTTPHandler(AuthHandler, HmacKeys):
req.headers['X-Amz-Security-Token'] = self._provider.security_token req.headers['X-Amz-Security-Token'] = self._provider.security_token
string_to_sign, headers_to_sign = self.string_to_sign(req) string_to_sign, headers_to_sign = self.string_to_sign(req)
boto.log.debug('StringToSign:\n%s' % string_to_sign) boto.log.debug('StringToSign:\n%s' % string_to_sign)
hash_value = sha256(string_to_sign).digest() hash_value = sha256(string_to_sign.encode('utf-8')).digest()
b64_hmac = self.sign_string(hash_value) b64_hmac = self.sign_string(hash_value)
s = "AWS3 AWSAccessKeyId=%s," % self._provider.access_key s = "AWS3 AWSAccessKeyId=%s," % self._provider.access_key
s += "Algorithm=%s," % self.algorithm() s += "Algorithm=%s," % self.algorithm()
@@ -298,6 +298,9 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
self.region_name = region_name self.region_name = region_name
def _sign(self, key, msg, hex=False): def _sign(self, key, msg, hex=False):
if not isinstance(key, bytes):
key = key.encode('utf-8')
if hex: if hex:
sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest() sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest()
else: else:
@@ -310,7 +313,6 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
in the StringToSign. in the StringToSign.
""" """
host_header_value = self.host_header(self.host, http_request) host_header_value = self.host_header(self.host, http_request)
headers_to_sign = {}
headers_to_sign = {'Host': host_header_value} headers_to_sign = {'Host': host_header_value}
for name, value in http_request.headers.items(): for name, value in http_request.headers.items():
lname = name.lower() lname = name.lower()
@@ -330,8 +332,8 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
pairs = [] pairs = []
for pname in parameter_names: for pname in parameter_names:
pval = boto.utils.get_utf8_value(http_request.params[pname]) pval = boto.utils.get_utf8_value(http_request.params[pname])
pairs.append(urllib.quote(pname, safe='') + '=' + pairs.append(urllib.parse.quote(pname, safe='') + '=' +
urllib.quote(pval, safe='-_~')) urllib.parse.quote(pval, safe='-_~'))
return '&'.join(pairs) return '&'.join(pairs)
def canonical_query_string(self, http_request): def canonical_query_string(self, http_request):
@@ -342,8 +344,8 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
l = [] l = []
for param in sorted(http_request.params): for param in sorted(http_request.params):
value = boto.utils.get_utf8_value(http_request.params[param]) value = boto.utils.get_utf8_value(http_request.params[param])
l.append('%s=%s' % (urllib.quote(param, safe='-_.~'), l.append('%s=%s' % (urllib.parse.quote(param, safe='-_.~'),
urllib.quote(value, safe='-_.~'))) urllib.parse.quote(value.decode('utf-8'), safe='-_.~')))
return '&'.join(l) return '&'.join(l)
def canonical_headers(self, headers_to_sign): def canonical_headers(self, headers_to_sign):
@@ -376,7 +378,7 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
# in windows normpath('/') will be '\\' so we chane it back to '/' # in windows normpath('/') will be '\\' so we chane it back to '/'
normalized = posixpath.normpath(path).replace('\\','/') normalized = posixpath.normpath(path).replace('\\','/')
# Then urlencode whatever's left. # Then urlencode whatever's left.
encoded = urllib.quote(normalized) encoded = urllib.parse.quote(normalized)
if len(path) > 1 and path.endswith('/'): if len(path) > 1 and path.endswith('/'):
encoded += '/' encoded += '/'
return encoded return encoded
@@ -388,7 +390,9 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
# the entire body into memory. # the entire body into memory.
if hasattr(body, 'seek') and hasattr(body, 'read'): if hasattr(body, 'seek') and hasattr(body, 'read'):
return boto.utils.compute_hash(body, hash_algorithm=sha256)[0] return boto.utils.compute_hash(body, hash_algorithm=sha256)[0]
return sha256(http_request.body).hexdigest() elif not isinstance(body, bytes):
body = body.encode('utf-8')
return sha256(body).hexdigest()
def canonical_request(self, http_request): def canonical_request(self, http_request):
cr = [http_request.method.upper()] cr = [http_request.method.upper()]
@@ -462,7 +466,7 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
sts = ['AWS4-HMAC-SHA256'] sts = ['AWS4-HMAC-SHA256']
sts.append(http_request.headers['X-Amz-Date']) sts.append(http_request.headers['X-Amz-Date'])
sts.append(self.credential_scope(http_request)) sts.append(self.credential_scope(http_request))
sts.append(sha256(canonical_request).hexdigest()) sts.append(sha256(canonical_request.encode('utf-8')).hexdigest())
return '\n'.join(sts) return '\n'.join(sts)
def signature(self, http_request, string_to_sign): def signature(self, http_request, string_to_sign):
@@ -538,11 +542,11 @@ class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler):
def canonical_uri(self, http_request): def canonical_uri(self, http_request):
# S3 does **NOT** do path normalization that SigV4 typically does. # S3 does **NOT** do path normalization that SigV4 typically does.
# Urlencode the path, **NOT** ``auth_path`` (because vhosting). # Urlencode the path, **NOT** ``auth_path`` (because vhosting).
path = urlparse.urlparse(http_request.path) path = urllib.parse.urlparse(http_request.path)
# Because some quoting may have already been applied, let's back it out. # Because some quoting may have already been applied, let's back it out.
unquoted = urllib.unquote(path.path) unquoted = urllib.parse.unquote(path.path)
# Requote, this time addressing all characters. # Requote, this time addressing all characters.
encoded = urllib.quote(unquoted) encoded = urllib.parse.quote(unquoted)
return encoded return encoded
def host_header(self, host, http_request): def host_header(self, host, http_request):
@@ -558,7 +562,6 @@ class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler):
in the StringToSign. in the StringToSign.
""" """
host_header_value = self.host_header(self.host, http_request) host_header_value = self.host_header(self.host, http_request)
headers_to_sign = {}
headers_to_sign = {'Host': host_header_value} headers_to_sign = {'Host': host_header_value}
for name, value in http_request.headers.items(): for name, value in http_request.headers.items():
lname = name.lower() lname = name.lower()
@@ -602,6 +605,11 @@ class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler):
if part == 's3': if part == 's3':
# If it's by itself, the region is the previous part. # If it's by itself, the region is the previous part.
region_name = parts[-offset] region_name = parts[-offset]
# Unless it's Vhosted classic
if region_name == 'amazonaws':
region_name = 'us-east-1'
break break
elif part.startswith('s3-'): elif part.startswith('s3-'):
region_name = self.clean_region_name(part) region_name = self.clean_region_name(part)
@@ -628,14 +636,14 @@ class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler):
# **ON** the ``path/auth_path``. # **ON** the ``path/auth_path``.
# Rip them apart, so the ``auth_path/params`` can be signed # Rip them apart, so the ``auth_path/params`` can be signed
# appropriately. # appropriately.
parsed_path = urlparse.urlparse(modified_req.auth_path) parsed_path = urllib.parse.urlparse(modified_req.auth_path)
modified_req.auth_path = parsed_path.path modified_req.auth_path = parsed_path.path
if modified_req.params is None: if modified_req.params is None:
modified_req.params = {} modified_req.params = {}
raw_qs = parsed_path.query raw_qs = parsed_path.query
existing_qs = urlparse.parse_qs( existing_qs = urllib.parse.parse_qs(
raw_qs, raw_qs,
keep_blank_values=True keep_blank_values=True
) )
@@ -666,6 +674,54 @@ class S3HmacAuthV4Handler(HmacAuthV4Handler, AuthHandler):
req = self.mangle_path_and_params(req) req = self.mangle_path_and_params(req)
return super(S3HmacAuthV4Handler, self).add_auth(req, **kwargs) return super(S3HmacAuthV4Handler, self).add_auth(req, **kwargs)
def presign(self, req, expires, iso_date=None):
"""
Presign a request using SigV4 query params. Takes in an HTTP request
and an expiration time in seconds and returns a URL.
http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
"""
if iso_date is None:
iso_date = datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')
region = self.determine_region_name(req.host)
service = self.determine_service_name(req.host)
params = {
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
'X-Amz-Credential': '%s/%s/%s/%s/aws4_request' % (
self._provider.access_key,
iso_date[:8],
region,
service
),
'X-Amz-Date': iso_date,
'X-Amz-Expires': expires,
'X-Amz-SignedHeaders': 'host'
}
if self._provider.security_token:
params['X-Amz-Security-Token'] = self._provider.security_token
req.params.update(params)
cr = self.canonical_request(req)
# We need to replace the payload SHA with a constant
cr = '\n'.join(cr.split('\n')[:-1]) + '\nUNSIGNED-PAYLOAD'
# Date header is expected for string_to_sign, but unused otherwise
req.headers['X-Amz-Date'] = iso_date
sts = self.string_to_sign(req, cr)
signature = self.signature(req, sts)
# Add signature to params now that we have it
req.params['X-Amz-Signature'] = signature
return 'https://%s%s?%s' % (req.host, req.path,
urllib.parse.urlencode(req.params))
class QueryAuthHandler(AuthHandler): class QueryAuthHandler(AuthHandler):
""" """
@@ -679,16 +735,16 @@ class QueryAuthHandler(AuthHandler):
capability = ['pure-query'] capability = ['pure-query']
def _escape_value(self, value): def _escape_value(self, value):
# Would normally be ``return urllib.quote(value)``. # Would normally be ``return urllib.parse.quote(value)``.
return value return value
def _build_query_string(self, params): def _build_query_string(self, params):
keys = params.keys() keys = list(params.keys())
keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) keys.sort(key=lambda x: x.lower())
pairs = [] pairs = []
for key in keys: for key in keys:
val = boto.utils.get_utf8_value(params[key]) val = boto.utils.get_utf8_value(params[key])
pairs.append(key + '=' + self._escape_value(val)) pairs.append(key + '=' + self._escape_value(val.decode('utf-8')))
return '&'.join(pairs) return '&'.join(pairs)
def add_auth(self, http_request, **kwargs): def add_auth(self, http_request, **kwargs):
@@ -725,7 +781,7 @@ class QuerySignatureHelper(HmacKeys):
boto.log.debug('query_string: %s Signature: %s' % (qs, signature)) boto.log.debug('query_string: %s Signature: %s' % (qs, signature))
if http_request.method == 'POST': if http_request.method == 'POST':
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
http_request.body = qs + '&Signature=' + urllib.quote_plus(signature) http_request.body = qs + '&Signature=' + urllib.parse.quote_plus(signature)
http_request.headers['Content-Length'] = str(len(http_request.body)) http_request.headers['Content-Length'] = str(len(http_request.body))
else: else:
http_request.body = '' http_request.body = ''
@@ -733,7 +789,7 @@ class QuerySignatureHelper(HmacKeys):
# already be there, we need to get rid of that and rebuild it # already be there, we need to get rid of that and rebuild it
http_request.path = http_request.path.split('?')[0] http_request.path = http_request.path.split('?')[0]
http_request.path = (http_request.path + '?' + qs + http_request.path = (http_request.path + '?' + qs +
'&Signature=' + urllib.quote_plus(signature)) '&Signature=' + urllib.parse.quote_plus(signature))
class QuerySignatureV0AuthHandler(QuerySignatureHelper, AuthHandler): class QuerySignatureV0AuthHandler(QuerySignatureHelper, AuthHandler):
@@ -746,13 +802,13 @@ class QuerySignatureV0AuthHandler(QuerySignatureHelper, AuthHandler):
boto.log.debug('using _calc_signature_0') boto.log.debug('using _calc_signature_0')
hmac = self._get_hmac() hmac = self._get_hmac()
s = params['Action'] + params['Timestamp'] s = params['Action'] + params['Timestamp']
hmac.update(s) hmac.update(s.encode('utf-8'))
keys = params.keys() keys = params.keys()
keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
pairs = [] pairs = []
for key in keys: for key in keys:
val = boto.utils.get_utf8_value(params[key]) val = boto.utils.get_utf8_value(params[key])
pairs.append(key + '=' + urllib.quote(val)) pairs.append(key + '=' + urllib.parse.quote(val))
qs = '&'.join(pairs) qs = '&'.join(pairs)
return (qs, base64.b64encode(hmac.digest())) return (qs, base64.b64encode(hmac.digest()))
@@ -777,10 +833,10 @@ class QuerySignatureV1AuthHandler(QuerySignatureHelper, AuthHandler):
keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
pairs = [] pairs = []
for key in keys: for key in keys:
hmac.update(key) hmac.update(key.encode('utf-8'))
val = boto.utils.get_utf8_value(params[key]) val = boto.utils.get_utf8_value(params[key])
hmac.update(val) hmac.update(val)
pairs.append(key + '=' + urllib.quote(val)) pairs.append(key + '=' + urllib.parse.quote(val))
qs = '&'.join(pairs) qs = '&'.join(pairs)
return (qs, base64.b64encode(hmac.digest())) return (qs, base64.b64encode(hmac.digest()))
@@ -803,13 +859,13 @@ class QuerySignatureV2AuthHandler(QuerySignatureHelper, AuthHandler):
pairs = [] pairs = []
for key in keys: for key in keys:
val = boto.utils.get_utf8_value(params[key]) val = boto.utils.get_utf8_value(params[key])
pairs.append(urllib.quote(key, safe='') + '=' + pairs.append(urllib.parse.quote(key, safe='') + '=' +
urllib.quote(val, safe='-_~')) urllib.parse.quote(val, safe='-_~'))
qs = '&'.join(pairs) qs = '&'.join(pairs)
boto.log.debug('query string: %s' % qs) boto.log.debug('query string: %s' % qs)
string_to_sign += qs string_to_sign += qs
boto.log.debug('string_to_sign: %s' % string_to_sign) boto.log.debug('string_to_sign: %s' % string_to_sign)
hmac.update(string_to_sign) hmac.update(string_to_sign.encode('utf-8'))
b64 = base64.b64encode(hmac.digest()) b64 = base64.b64encode(hmac.digest())
boto.log.debug('len(b64)=%d' % len(b64)) boto.log.debug('len(b64)=%d' % len(b64))
boto.log.debug('base64 encoded digest: %s' % b64) boto.log.debug('base64 encoded digest: %s' % b64)
@@ -841,7 +897,7 @@ class POSTPathQSV2AuthHandler(QuerySignatureV2AuthHandler, AuthHandler):
# already be there, we need to get rid of that and rebuild it # already be there, we need to get rid of that and rebuild it
req.path = req.path.split('?')[0] req.path = req.path.split('?')[0]
req.path = (req.path + '?' + qs + req.path = (req.path + '?' + qs +
'&Signature=' + urllib.quote_plus(signature)) '&Signature=' + urllib.parse.quote_plus(signature))
def get_auth_handler(host, config, provider, requested_capability=None): def get_auth_handler(host, config, provider, requested_capability=None):
@@ -904,6 +960,9 @@ def detect_potential_sigv4(func):
return ['hmac-v4'] return ['hmac-v4']
if hasattr(self, 'region'): if hasattr(self, 'region'):
# If you're making changes here, you should also check
# ``boto/iam/connection.py``, as several things there are also
# endpoint-related.
if getattr(self.region, 'endpoint', ''): if getattr(self.region, 'endpoint', ''):
if '.cn-' in self.region.endpoint: if '.cn-' in self.region.endpoint:
return ['hmac-v4'] return ['hmac-v4']
@@ -921,6 +980,9 @@ def detect_potential_s3sigv4(func):
return ['hmac-v4-s3'] return ['hmac-v4-s3']
if hasattr(self, 'host'): if hasattr(self, 'host'):
# If you're making changes here, you should also check
# ``boto/iam/connection.py``, as several things there are also
# endpoint-related.
if '.cn-' in self.host: if '.cn-' in self.host:
return ['hmac-v4-s3'] return ['hmac-v4-s3']

View File

@@ -23,7 +23,7 @@
Defines an interface which all Auth handlers need to implement. Defines an interface which all Auth handlers need to implement.
""" """
from plugin import Plugin from boto.plugin import Plugin
class NotReadyToAuthenticate(Exception): class NotReadyToAuthenticate(Exception):
pass pass

View File

@@ -4,12 +4,14 @@ from boto.exception import BotoServerError
def simple(e): def simple(e):
err = json.loads(e.error_message) code = e.code
code = err['Error']['Code']
if code.endswith('Exception'):
code = code.rstrip('Exception')
try: try:
# Dynamically get the error class. # Dynamically get the error class.
simple_e = getattr(sys.modules[__name__], code)(e, err) simple_e = getattr(sys.modules[__name__], code)(e)
except AttributeError: except AttributeError:
# Return original exception on failure. # Return original exception on failure.
return e return e
@@ -18,12 +20,9 @@ def simple(e):
class SimpleException(BotoServerError): class SimpleException(BotoServerError):
def __init__(self, e, err): def __init__(self, e):
super(SimpleException, self).__init__(e.status, e.reason, e.body) super(SimpleException, self).__init__(e.status, e.reason, e.body)
self.body = e.error_message self.error_message = self.message
self.request_id = err['RequestId']
self.error_code = err['Error']['Code']
self.error_message = err['Error']['Message']
def __repr__(self): def __repr__(self):
return self.__class__.__name__ + ': ' + self.error_message return self.__class__.__name__ + ': ' + self.error_message

View File

@@ -63,7 +63,7 @@ class Layer1(AWSQueryConnection):
def _get_response(self, action, params, path='/', verb='GET'): def _get_response(self, action, params, path='/', verb='GET'):
params['ContentType'] = 'JSON' params['ContentType'] = 'JSON'
response = self.make_request(action, params, path, verb) response = self.make_request(action, params, path, verb)
body = response.read() body = response.read().decode('utf-8')
boto.log.debug(body) boto.log.debug(body)
if response.status == 200: if response.status == 200:
return json.loads(body) return json.loads(body)
@@ -351,9 +351,9 @@ class Layer1(AWSQueryConnection):
self.build_list_params(params, options_to_remove, self.build_list_params(params, options_to_remove,
'OptionsToRemove.member') 'OptionsToRemove.member')
if tier_name and tier_type and tier_version: if tier_name and tier_type and tier_version:
params['Tier.member.Name'] = tier_name params['Tier.Name'] = tier_name
params['Tier.member.Type'] = tier_type params['Tier.Type'] = tier_type
params['Tier.member.Version'] = tier_version params['Tier.Version'] = tier_version
return self._get_response('CreateEnvironment', params) return self._get_response('CreateEnvironment', params)
def create_storage_location(self): def create_storage_location(self):
@@ -1138,9 +1138,9 @@ class Layer1(AWSQueryConnection):
self.build_list_params(params, options_to_remove, self.build_list_params(params, options_to_remove,
'OptionsToRemove.member') 'OptionsToRemove.member')
if tier_name and tier_type and tier_version: if tier_name and tier_type and tier_version:
params['Tier.member.Name'] = tier_name params['Tier.Name'] = tier_name
params['Tier.member.Type'] = tier_type params['Tier.Type'] = tier_type
params['Tier.member.Version'] = tier_version params['Tier.Version'] = tier_version
return self._get_response('UpdateEnvironment', params) return self._get_response('UpdateEnvironment', params)
def validate_configuration_settings(self, application_name, def validate_configuration_settings(self, application_name,

View File

@@ -1,5 +1,6 @@
"""Classify responses from layer1 and strict type values.""" """Classify responses from layer1 and strict type values."""
from datetime import datetime from datetime import datetime
from boto.compat import six
class BaseObject(object): class BaseObject(object):
@@ -7,7 +8,7 @@ class BaseObject(object):
def __repr__(self): def __repr__(self):
result = self.__class__.__name__ + '{ ' result = self.__class__.__name__ + '{ '
counter = 0 counter = 0
for key, value in self.__dict__.iteritems(): for key, value in six.iteritems(self.__dict__):
# first iteration no comma # first iteration no comma
counter += 1 counter += 1
if counter > 1: if counter > 1:

View File

@@ -9,7 +9,7 @@ def beanstalk_wrapper(func, name):
def _wrapped_low_level_api(*args, **kwargs): def _wrapped_low_level_api(*args, **kwargs):
try: try:
response = func(*args, **kwargs) response = func(*args, **kwargs)
except BotoServerError, e: except BotoServerError as e:
raise exception.simple(e) raise exception.simple(e)
# Turn 'this_is_a_function_name' into 'ThisIsAFunctionNameResponse'. # Turn 'this_is_a_function_name' into 'ThisIsAFunctionNameResponse'.
cls_name = ''.join([part.capitalize() for part in name.split('_')]) + 'Response' cls_name = ''.join([part.capitalize() for part in name.split('_')]) + 'Response'

View File

@@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
from connection import CloudFormationConnection from boto.cloudformation.connection import CloudFormationConnection
from boto.regioninfo import RegionInfo, get_regions, load_regions from boto.regioninfo import RegionInfo, get_regions, load_regions
RegionData = load_regions().get('cloudformation') RegionData = load_regions().get('cloudformation')

View File

@@ -271,7 +271,7 @@ class CloudFormationConnection(AWSQueryConnection):
:return: Parsed JSON response data :return: Parsed JSON response data
""" """
response = self.make_request(call, params, path, method) response = self.make_request(call, params, path, method)
body = response.read() body = response.read().decode('utf-8')
if response.status == 200: if response.status == 200:
body = json.loads(body) body = json.loads(body)
return body return body

View File

@@ -22,7 +22,7 @@
import uuid import uuid
import base64 import base64
import time import time
from boto.compat import json from boto.compat import six, json
from boto.cloudfront.identity import OriginAccessIdentity from boto.cloudfront.identity import OriginAccessIdentity
from boto.cloudfront.object import Object, StreamingObject from boto.cloudfront.object import Object, StreamingObject
from boto.cloudfront.signers import ActiveTrustedSigners, TrustedSigners from boto.cloudfront.signers import ActiveTrustedSigners, TrustedSigners
@@ -665,7 +665,7 @@ class Distribution(object):
raise ValueError("You must specify one of private_key_file or private_key_string") raise ValueError("You must specify one of private_key_file or private_key_string")
# If private_key_file is a file name, open it and read it # If private_key_file is a file name, open it and read it
if private_key_string is None: if private_key_string is None:
if isinstance(private_key_file, basestring): if isinstance(private_key_file, six.string_types):
with open(private_key_file, 'r') as file_handle: with open(private_key_file, 'r') as file_handle:
private_key_string = file_handle.read() private_key_string = file_handle.read()
# Otherwise, treat it like a file # Otherwise, treat it like a file

View File

@@ -20,8 +20,8 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
import uuid import uuid
import urllib
from boto.compat import urllib
from boto.resultset import ResultSet from boto.resultset import ResultSet
@@ -71,7 +71,7 @@ class InvalidationBatch(object):
"""Escape a path, make sure it begins with a slash and contains no invalid characters""" """Escape a path, make sure it begins with a slash and contains no invalid characters"""
if not p[0] == "/": if not p[0] == "/":
p = "/%s" % p p = "/%s" % p
return urllib.quote(p) return urllib.parse.quote(p)
def to_xml(self): def to_xml(self):
"""Get this batch as XML""" """Get this batch as XML"""

View File

@@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
from identity import OriginAccessIdentity from boto.cloudfront.identity import OriginAccessIdentity
def get_oai_value(origin_access_identity): def get_oai_value(origin_access_identity):
if isinstance(origin_access_identity, OriginAccessIdentity): if isinstance(origin_access_identity, OriginAccessIdentity):

View File

@@ -221,13 +221,15 @@ class CommitResponse(object):
self.doc_service = doc_service self.doc_service = doc_service
self.sdf = sdf self.sdf = sdf
_body = response.content.decode('utf-8')
try: try:
self.content = json.loads(response.content) self.content = json.loads(_body)
except: except:
boto.log.error('Error indexing documents.\nResponse Content:\n{0}\n\n' boto.log.error('Error indexing documents.\nResponse Content:\n{0}\n\n'
'SDF:\n{1}'.format(response.content, self.sdf)) 'SDF:\n{1}'.format(_body, self.sdf))
raise boto.exception.BotoServerError(self.response.status_code, '', raise boto.exception.BotoServerError(self.response.status_code, '',
body=response.content) body=_body)
self.status = self.content['status'] self.status = self.content['status']
if self.status == 'error': if self.status == 'error':
@@ -238,6 +240,9 @@ class CommitResponse(object):
raise EncodingError("Illegal Unicode character in document") raise EncodingError("Illegal Unicode character in document")
elif e == "The Content-Length is too long": elif e == "The Content-Length is too long":
raise ContentTooLongError("Content was too long") raise ContentTooLongError("Content was too long")
if 'adds' not in self.content or 'deletes' not in self.content:
raise SearchServiceException("Error indexing documents"
" => %s" % self.content.get('message', ''))
else: else:
self.errors = [] self.errors = []

View File

@@ -24,12 +24,12 @@
import boto import boto
from boto.compat import json from boto.compat import json
from .optionstatus import OptionStatus from boto.cloudsearch.optionstatus import OptionStatus
from .optionstatus import IndexFieldStatus from boto.cloudsearch.optionstatus import IndexFieldStatus
from .optionstatus import ServicePoliciesStatus from boto.cloudsearch.optionstatus import ServicePoliciesStatus
from .optionstatus import RankExpressionStatus from boto.cloudsearch.optionstatus import RankExpressionStatus
from .document import DocumentServiceConnection from boto.cloudsearch.document import DocumentServiceConnection
from .search import SearchConnection from boto.cloudsearch.search import SearchConnection
def handle_bool(value): def handle_bool(value):
if value in [True, 'true', 'True', 'TRUE', 1]: if value in [True, 'true', 'True', 'TRUE', 1]:

View File

@@ -680,7 +680,7 @@ class Layer1(AWSQueryConnection):
'update_stemming_options_result', 'update_stemming_options_result',
'stems') 'stems')
params = {'DomainName': domain_name, params = {'DomainName': domain_name,
'Stems': stems} 'Stems': stems}
return self.get_response(doc_path, 'UpdateStemmingOptions', return self.get_response(doc_path, 'UpdateStemmingOptions',
params, verb='POST') params, verb='POST')

View File

@@ -22,8 +22,8 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
from .layer1 import Layer1 from boto.cloudsearch.layer1 import Layer1
from .domain import Domain from boto.cloudsearch.domain import Domain
class Layer2(object): class Layer2(object):

View File

@@ -22,9 +22,7 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
from math import ceil from math import ceil
import time from boto.compat import json, map, six
import boto
from boto.compat import json
import requests import requests
@@ -52,7 +50,7 @@ class SearchResults(object):
self.facets = {} self.facets = {}
if 'facets' in attrs: if 'facets' in attrs:
for (facet, values) in attrs['facets'].iteritems(): for (facet, values) in attrs['facets'].items():
if 'constraints' in values: if 'constraints' in values:
self.facets[facet] = dict((k, v) for (k, v) in map(lambda x: (x['value'], x['count']), values['constraints'])) self.facets[facet] = dict((k, v) for (k, v) in map(lambda x: (x['value'], x['count']), values['constraints']))
@@ -129,19 +127,19 @@ class Query(object):
params['facet'] = ','.join(self.facet) params['facet'] = ','.join(self.facet)
if self.facet_constraints: if self.facet_constraints:
for k, v in self.facet_constraints.iteritems(): for k, v in six.iteritems(self.facet_constraints):
params['facet-%s-constraints' % k] = v params['facet-%s-constraints' % k] = v
if self.facet_sort: if self.facet_sort:
for k, v in self.facet_sort.iteritems(): for k, v in six.iteritems(self.facet_sort):
params['facet-%s-sort' % k] = v params['facet-%s-sort' % k] = v
if self.facet_top_n: if self.facet_top_n:
for k, v in self.facet_top_n.iteritems(): for k, v in six.iteritems(self.facet_top_n):
params['facet-%s-top-n' % k] = v params['facet-%s-top-n' % k] = v
if self.t: if self.t:
for k, v in self.t.iteritems(): for k, v in six.iteritems(self.t):
params['t-%s' % k] = v params['t-%s' % k] = v
return params return params
@@ -288,19 +286,20 @@ class SearchConnection(object):
params = query.to_params() params = query.to_params()
r = requests.get(url, params=params) r = requests.get(url, params=params)
body = r.content.decode('utf-8')
try: try:
data = json.loads(r.content) data = json.loads(body)
except ValueError, e: except ValueError as e:
if r.status_code == 403: if r.status_code == 403:
msg = '' msg = ''
import re import re
g = re.search('<html><body><h1>403 Forbidden</h1>([^<]+)<', r.content) g = re.search('<html><body><h1>403 Forbidden</h1>([^<]+)<', body)
try: try:
msg = ': %s' % (g.groups()[0].strip()) msg = ': %s' % (g.groups()[0].strip())
except AttributeError: except AttributeError:
pass pass
raise SearchServiceException('Authentication error from Amazon%s' % msg) raise SearchServiceException('Authentication error from Amazon%s' % msg)
raise SearchServiceException("Got non-json response from Amazon. %s" % r.content, query) raise SearchServiceException("Got non-json response from Amazon. %s" % body, query)
if 'messages' in data and 'error' in data: if 'messages' in data and 'error' in data:
for m in data['messages']: for m in data['messages']:

View File

@@ -72,4 +72,3 @@ class SourceAttribute(object):
valid = '|'.join(self.ValidDataFunctions) valid = '|'.join(self.ValidDataFunctions)
raise ValueError('data_function must be one of: %s' % valid) raise ValueError('data_function must be one of: %s' % valid)
self._data_function = value self._data_function = value

View File

@@ -0,0 +1,42 @@
# Copyright (c) 2014 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.
from boto.regioninfo import get_regions
def regions():
"""
Get all available regions for the Amazon CloudSearch service.
:rtype: list
:return: A list of :class:`boto.regioninfo.RegionInfo`
"""
import boto.cloudsearch2.layer1
return get_regions(
'cloudsearch',
connection_cls=boto.cloudsearch2.layer1.CloudSearchConnection
)
def connect_to_region(region_name, **kw_params):
for region in regions():
if region.name == region_name:
return region.connect(**kw_params)
return None

View File

@@ -0,0 +1,275 @@
# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
# Copyright (c) 2014 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.
#
import boto.exception
from boto.compat import json
import requests
import boto
class SearchServiceException(Exception):
pass
class CommitMismatchError(Exception):
# Let's do some extra work and let the user handle errors on his/her own.
errors = None
class EncodingError(Exception):
"""
Content sent for Cloud Search indexing was incorrectly encoded.
This usually happens when a document is marked as unicode but non-unicode
characters are present.
"""
pass
class ContentTooLongError(Exception):
"""
Content sent for Cloud Search indexing was too long
This will usually happen when documents queued for indexing add up to more
than the limit allowed per upload batch (5MB)
"""
pass
class DocumentServiceConnection(object):
"""
A CloudSearch document service.
The DocumentServiceConection is used to add, remove and update documents in
CloudSearch. Commands are uploaded to CloudSearch in SDF (Search Document
Format).
To generate an appropriate SDF, use :func:`add` to add or update documents,
as well as :func:`delete` to remove documents.
Once the set of documents is ready to be index, use :func:`commit` to send
the commands to CloudSearch.
If there are a lot of documents to index, it may be preferable to split the
generation of SDF data and the actual uploading into CloudSearch. Retrieve
the current SDF with :func:`get_sdf`. If this file is the uploaded into S3,
it can be retrieved back afterwards for upload into CloudSearch using
:func:`add_sdf_from_s3`.
The SDF is not cleared after a :func:`commit`. If you wish to continue
using the DocumentServiceConnection for another batch upload of commands,
you will need to :func:`clear_sdf` first to stop the previous batch of
commands from being uploaded again.
"""
def __init__(self, domain=None, endpoint=None):
self.domain = domain
self.endpoint = endpoint
if not self.endpoint:
self.endpoint = domain.doc_service_endpoint
self.documents_batch = []
self._sdf = None
def add(self, _id, fields):
"""
Add a document to be processed by the DocumentService
The document will not actually be added until :func:`commit` is called
:type _id: string
:param _id: A unique ID used to refer to this document.
:type fields: dict
:param fields: A dictionary of key-value pairs to be uploaded .
"""
d = {'type': 'add', 'id': _id, 'fields': fields}
self.documents_batch.append(d)
def delete(self, _id):
"""
Schedule a document to be removed from the CloudSearch service
The document will not actually be scheduled for removal until
:func:`commit` is called
:type _id: string
:param _id: The unique ID of this document.
"""
d = {'type': 'delete', 'id': _id}
self.documents_batch.append(d)
def get_sdf(self):
"""
Generate the working set of documents in Search Data Format (SDF)
:rtype: string
:returns: JSON-formatted string of the documents in SDF
"""
return self._sdf if self._sdf else json.dumps(self.documents_batch)
def clear_sdf(self):
"""
Clear the working documents from this DocumentServiceConnection
This should be used after :func:`commit` if the connection will be
reused for another set of documents.
"""
self._sdf = None
self.documents_batch = []
def add_sdf_from_s3(self, key_obj):
"""
Load an SDF from S3
Using this method will result in documents added through
:func:`add` and :func:`delete` being ignored.
:type key_obj: :class:`boto.s3.key.Key`
:param key_obj: An S3 key which contains an SDF
"""
#@todo:: (lucas) would be nice if this could just take an s3://uri..."
self._sdf = key_obj.get_contents_as_string()
def commit(self):
"""
Actually send an SDF to CloudSearch for processing
If an SDF file has been explicitly loaded it will be used. Otherwise,
documents added through :func:`add` and :func:`delete` will be used.
:rtype: :class:`CommitResponse`
:returns: A summary of documents added and deleted
"""
sdf = self.get_sdf()
if ': null' in sdf:
boto.log.error('null value in sdf detected. This will probably '
'raise 500 error.')
index = sdf.index(': null')
boto.log.error(sdf[index - 100:index + 100])
api_version = '2013-01-01'
if self.domain:
api_version = self.domain.layer1.APIVersion
url = "http://%s/%s/documents/batch" % (self.endpoint, api_version)
# Keep-alive is automatic in a post-1.0 requests world.
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=20,
pool_maxsize=50,
max_retries=5
)
session.mount('http://', adapter)
session.mount('https://', adapter)
r = session.post(url, data=sdf,
headers={'Content-Type': 'application/json'})
return CommitResponse(r, self, sdf)
class CommitResponse(object):
"""Wrapper for response to Cloudsearch document batch commit.
:type response: :class:`requests.models.Response`
:param response: Response from Cloudsearch /documents/batch API
:type doc_service: :class:`boto.cloudsearch2.document.DocumentServiceConnection`
:param doc_service: Object containing the documents posted and methods to
retry
:raises: :class:`boto.exception.BotoServerError`
:raises: :class:`boto.cloudsearch2.document.SearchServiceException`
:raises: :class:`boto.cloudsearch2.document.EncodingError`
:raises: :class:`boto.cloudsearch2.document.ContentTooLongError`
"""
def __init__(self, response, doc_service, sdf):
self.response = response
self.doc_service = doc_service
self.sdf = sdf
_body = response.content.decode('utf-8')
try:
self.content = json.loads(_body)
except:
boto.log.error('Error indexing documents.\nResponse Content:\n{0}'
'\n\nSDF:\n{1}'.format(_body, self.sdf))
raise boto.exception.BotoServerError(self.response.status_code, '',
body=_body)
self.status = self.content['status']
if self.status == 'error':
self.errors = [e.get('message') for e in self.content.get('errors',
[])]
for e in self.errors:
if "Illegal Unicode character" in e:
raise EncodingError("Illegal Unicode character in document")
elif e == "The Content-Length is too long":
raise ContentTooLongError("Content was too long")
else:
self.errors = []
self.adds = self.content['adds']
self.deletes = self.content['deletes']
self._check_num_ops('add', self.adds)
self._check_num_ops('delete', self.deletes)
def _check_num_ops(self, type_, response_num):
"""Raise exception if number of ops in response doesn't match commit
:type type_: str
:param type_: Type of commit operation: 'add' or 'delete'
:type response_num: int
:param response_num: Number of adds or deletes in the response.
:raises: :class:`boto.cloudsearch2.document.CommitMismatchError`
"""
commit_num = len([d for d in self.doc_service.documents_batch
if d['type'] == type_])
if response_num != commit_num:
boto.log.debug(self.response.content)
# There will always be a commit mismatch error if there is any
# errors on cloudsearch. self.errors gets lost when this
# CommitMismatchError is raised. Whoever is using boto has no idea
# why their commit failed. They can't even notify the user of the
# cause by parsing the error messages from amazon. So let's
# attach the self.errors to the exceptions if we already spent
# time and effort collecting them out of the response.
exc = CommitMismatchError(
'Incorrect number of {0}s returned. Commit: {1} Response: {2}'
.format(type_, commit_num, response_num)
)
exc.errors = self.errors
raise exc

View File

@@ -0,0 +1,542 @@
# Copyright (c) 2014 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.
#
from boto.cloudsearch2.optionstatus import IndexFieldStatus
from boto.cloudsearch2.optionstatus import ServicePoliciesStatus
from boto.cloudsearch2.optionstatus import ExpressionStatus
from boto.cloudsearch2.optionstatus import AvailabilityOptionsStatus
from boto.cloudsearch2.optionstatus import ScalingParametersStatus
from boto.cloudsearch2.document import DocumentServiceConnection
from boto.cloudsearch2.search import SearchConnection
def handle_bool(value):
if value in [True, 'true', 'True', 'TRUE', 1]:
return True
return False
class Domain(object):
"""
A Cloudsearch domain.
:ivar name: The name of the domain.
:ivar id: The internally generated unique identifier for the domain.
:ivar created: A boolean which is True if the domain is
created. It can take several minutes to initialize a domain
when CreateDomain is called. Newly created search domains are
returned with a False value for Created until domain creation
is complete
:ivar deleted: A boolean which is True if the search domain has
been deleted. The system must clean up resources dedicated to
the search domain when delete is called. Newly deleted
search domains are returned from list_domains with a True
value for deleted for several minutes until resource cleanup
is complete.
:ivar processing: True if processing is being done to activate the
current domain configuration.
:ivar num_searchable_docs: The number of documents that have been
submittted to the domain and indexed.
:ivar requires_index_document: True if index_documents needs to be
called to activate the current domain configuration.
:ivar search_instance_count: The number of search instances that are
available to process search requests.
:ivar search_instance_type: The instance type that is being used to
process search requests.
:ivar search_partition_count: The number of partitions across which
the search index is spread.
"""
def __init__(self, layer1, data):
"""
Constructor - Create a domain object from a layer1 and data params
:type layer1: :class:`boto.cloudsearch2.layer1.Layer1` object
:param layer1: A :class:`boto.cloudsearch2.layer1.Layer1` object
which is used to perform operations on the domain.
"""
self.layer1 = layer1
self.update_from_data(data)
def update_from_data(self, data):
self.created = data['Created']
self.deleted = data['Deleted']
self.processing = data['Processing']
self.requires_index_documents = data['RequiresIndexDocuments']
self.domain_id = data['DomainId']
self.domain_name = data['DomainName']
self.search_instance_count = data['SearchInstanceCount']
self.search_instance_type = data.get('SearchInstanceType', None)
self.search_partition_count = data['SearchPartitionCount']
self._doc_service = data['DocService']
self._service_arn = data['ARN']
self._search_service = data['SearchService']
@property
def service_arn(self):
return self._service_arn
@property
def doc_service_endpoint(self):
return self._doc_service['Endpoint']
@property
def search_service_endpoint(self):
return self._search_service['Endpoint']
@property
def created(self):
return self._created
@created.setter
def created(self, value):
self._created = handle_bool(value)
@property
def deleted(self):
return self._deleted
@deleted.setter
def deleted(self, value):
self._deleted = handle_bool(value)
@property
def processing(self):
return self._processing
@processing.setter
def processing(self, value):
self._processing = handle_bool(value)
@property
def requires_index_documents(self):
return self._requires_index_documents
@requires_index_documents.setter
def requires_index_documents(self, value):
self._requires_index_documents = handle_bool(value)
@property
def search_partition_count(self):
return self._search_partition_count
@search_partition_count.setter
def search_partition_count(self, value):
self._search_partition_count = int(value)
@property
def search_instance_count(self):
return self._search_instance_count
@search_instance_count.setter
def search_instance_count(self, value):
self._search_instance_count = int(value)
@property
def name(self):
return self.domain_name
@property
def id(self):
return self.domain_id
def delete(self):
"""
Delete this domain and all index data associated with it.
"""
return self.layer1.delete_domain(self.name)
def get_analysis_schemes(self):
"""
Return a list of Analysis Scheme objects.
"""
return self.layer1.describe_analysis_schemes(self.name)
def get_availability_options(self):
"""
Return a :class:`boto.cloudsearch2.option.AvailabilityOptionsStatus`
object representing the currently defined availability options for
the domain.
:return: OptionsStatus object
:rtype: :class:`boto.cloudsearch2.option.AvailabilityOptionsStatus`
object
"""
return AvailabilityOptionsStatus(
self, refresh_fn=self.layer1.describe_availability_options,
refresh_key=['DescribeAvailabilityOptionsResponse',
'DescribeAvailabilityOptionsResult',
'AvailabilityOptions'],
save_fn=self.layer1.update_availability_options)
def get_scaling_options(self):
"""
Return a :class:`boto.cloudsearch2.option.ScalingParametersStatus`
object representing the currently defined scaling options for the
domain.
:return: ScalingParametersStatus object
:rtype: :class:`boto.cloudsearch2.option.ScalingParametersStatus`
object
"""
return ScalingParametersStatus(
self, refresh_fn=self.layer1.describe_scaling_parameters,
refresh_key=['DescribeScalingParametersResponse',
'DescribeScalingParametersResult',
'ScalingParameters'],
save_fn=self.layer1.update_scaling_parameters)
def get_access_policies(self):
"""
Return a :class:`boto.cloudsearch2.option.ServicePoliciesStatus`
object representing the currently defined access policies for the
domain.
:return: ServicePoliciesStatus object
:rtype: :class:`boto.cloudsearch2.option.ServicePoliciesStatus` object
"""
return ServicePoliciesStatus(
self, refresh_fn=self.layer1.describe_service_access_policies,
refresh_key=['DescribeServiceAccessPoliciesResponse',
'DescribeServiceAccessPoliciesResult',
'AccessPolicies'],
save_fn=self.layer1.update_service_access_policies)
def index_documents(self):
"""
Tells the search domain to start indexing its documents using
the latest text processing options and IndexFields. This
operation must be invoked to make options whose OptionStatus
has OptionState of RequiresIndexDocuments visible in search
results.
"""
self.layer1.index_documents(self.name)
def get_index_fields(self, field_names=None):
"""
Return a list of index fields defined for this domain.
:return: list of IndexFieldStatus objects
:rtype: list of :class:`boto.cloudsearch2.option.IndexFieldStatus`
object
"""
data = self.layer1.describe_index_fields(self.name, field_names)
data = (data['DescribeIndexFieldsResponse']
['DescribeIndexFieldsResult']
['IndexFields'])
return [IndexFieldStatus(self, d) for d in data]
def create_index_field(self, field_name, field_type,
default='', facet=False, returnable=False,
searchable=False, sortable=False,
highlight=False, source_field=None,
analysis_scheme=None):
"""
Defines an ``IndexField``, either replacing an existing
definition or creating a new one.
:type field_name: string
:param field_name: The name of a field in the search index.
:type field_type: string
:param field_type: The type of field. Valid values are
int | double | literal | text | date | latlon |
int-array | double-array | literal-array | text-array | date-array
:type default: string or int
:param default: The default value for the field. If the
field is of type ``int`` this should be an integer value.
Otherwise, it's a string.
:type facet: bool
:param facet: A boolean to indicate whether facets
are enabled for this field or not. Does not apply to
fields of type ``int, int-array, text, text-array``.
:type returnable: bool
:param returnable: A boolean to indicate whether values
of this field can be returned in search results or
used in ranking.
:type searchable: bool
:param searchable: A boolean to indicate whether search
is enabled for this field or not.
:type sortable: bool
:param sortable: A boolean to indicate whether sorting
is enabled for this field or not. Does not apply to
fields of array types.
:type highlight: bool
:param highlight: A boolean to indicate whether highlighting
is enabled for this field or not. Does not apply to
fields of type ``double, int, date, latlon``
:type source_field: list of strings or string
:param source_field: For array types, this is the list of fields
to treat as the source. For singular types, pass a string only.
:type analysis_scheme: string
:param analysis_scheme: The analysis scheme to use for this field.
Only applies to ``text | text-array`` field types
:return: IndexFieldStatus objects
:rtype: :class:`boto.cloudsearch2.option.IndexFieldStatus` object
:raises: BaseException, InternalException, LimitExceededException,
InvalidTypeException, ResourceNotFoundException
"""
index = {
'IndexFieldName': field_name,
'IndexFieldType': field_type
}
if field_type == 'literal':
index['LiteralOptions'] = {
'FacetEnabled': facet,
'ReturnEnabled': returnable,
'SearchEnabled': searchable,
'SortEnabled': sortable
}
if default:
index['LiteralOptions']['DefaultValue'] = default
if source_field:
index['LiteralOptions']['SourceField'] = source_field
elif field_type == 'literal-array':
index['LiteralArrayOptions'] = {
'FacetEnabled': facet,
'ReturnEnabled': returnable,
'SearchEnabled': searchable
}
if default:
index['LiteralArrayOptions']['DefaultValue'] = default
if source_field:
index['LiteralArrayOptions']['SourceFields'] = \
','.join(source_field)
elif field_type == 'int':
index['IntOptions'] = {
'DefaultValue': default,
'FacetEnabled': facet,
'ReturnEnabled': returnable,
'SearchEnabled': searchable,
'SortEnabled': sortable
}
if default:
index['IntOptions']['DefaultValue'] = default
if source_field:
index['IntOptions']['SourceField'] = source_field
elif field_type == 'int-array':
index['IntArrayOptions'] = {
'FacetEnabled': facet,
'ReturnEnabled': returnable,
'SearchEnabled': searchable
}
if default:
index['IntArrayOptions']['DefaultValue'] = default
if source_field:
index['IntArrayOptions']['SourceFields'] = \
','.join(source_field)
elif field_type == 'date':
index['DateOptions'] = {
'FacetEnabled': facet,
'ReturnEnabled': returnable,
'SearchEnabled': searchable,
'SortEnabled': sortable
}
if default:
index['DateOptions']['DefaultValue'] = default
if source_field:
index['DateOptions']['SourceField'] = source_field
elif field_type == 'date-array':
index['DateArrayOptions'] = {
'FacetEnabled': facet,
'ReturnEnabled': returnable,
'SearchEnabled': searchable
}
if default:
index['DateArrayOptions']['DefaultValue'] = default
if source_field:
index['DateArrayOptions']['SourceFields'] = \
','.join(source_field)
elif field_type == 'double':
index['DoubleOptions'] = {
'FacetEnabled': facet,
'ReturnEnabled': returnable,
'SearchEnabled': searchable,
'SortEnabled': sortable
}
if default:
index['DoubleOptions']['DefaultValue'] = default
if source_field:
index['DoubleOptions']['SourceField'] = source_field
elif field_type == 'double-array':
index['DoubleArrayOptions'] = {
'FacetEnabled': facet,
'ReturnEnabled': returnable,
'SearchEnabled': searchable
}
if default:
index['DoubleArrayOptions']['DefaultValue'] = default
if source_field:
index['DoubleArrayOptions']['SourceFields'] = \
','.join(source_field)
elif field_type == 'text':
index['TextOptions'] = {
'ReturnEnabled': returnable,
'HighlightEnabled': highlight,
'SortEnabled': sortable
}
if default:
index['TextOptions']['DefaultValue'] = default
if source_field:
index['TextOptions']['SourceField'] = source_field
if analysis_scheme:
index['TextOptions']['AnalysisScheme'] = analysis_scheme
elif field_type == 'text-array':
index['TextArrayOptions'] = {
'ReturnEnabled': returnable,
'HighlightEnabled': highlight
}
if default:
index['TextArrayOptions']['DefaultValue'] = default
if source_field:
index['TextArrayOptions']['SourceFields'] = \
','.join(source_field)
if analysis_scheme:
index['TextArrayOptions']['AnalysisScheme'] = analysis_scheme
elif field_type == 'latlon':
index['LatLonOptions'] = {
'FacetEnabled': facet,
'ReturnEnabled': returnable,
'SearchEnabled': searchable,
'SortEnabled': sortable
}
if default:
index['LatLonOptions']['DefaultValue'] = default
if source_field:
index['LatLonOptions']['SourceField'] = source_field
data = self.layer1.define_index_field(self.name, index)
data = (data['DefineIndexFieldResponse']
['DefineIndexFieldResult']
['IndexField'])
return IndexFieldStatus(self, data,
self.layer1.describe_index_fields)
def get_expressions(self, names=None):
"""
Return a list of rank expressions defined for this domain.
:return: list of ExpressionStatus objects
:rtype: list of :class:`boto.cloudsearch2.option.ExpressionStatus`
object
"""
fn = self.layer1.describe_expressions
data = fn(self.name, names)
data = (data['DescribeExpressionsResponse']
['DescribeExpressionsResult']
['Expressions'])
return [ExpressionStatus(self, d, fn) for d in data]
def create_expression(self, name, value):
"""
Create a new expression.
:type name: string
:param name: The name of an expression for processing
during a search request.
:type value: string
:param value: The expression to evaluate for ranking
or thresholding while processing a search request. The
Expression syntax is based on JavaScript expressions
and supports:
* Single value, sort enabled numeric fields (int, double, date)
* Other expressions
* The _score variable, which references a document's relevance
score
* The _time variable, which references the current epoch time
* Integer, floating point, hex, and octal literals
* Arithmetic operators: + - * / %
* Bitwise operators: | & ^ ~ << >> >>>
* Boolean operators (including the ternary operator): && || ! ?:
* Comparison operators: < <= == >= >
* Mathematical functions: abs ceil exp floor ln log2 log10 logn
max min pow sqrt pow
* Trigonometric functions: acos acosh asin asinh atan atan2 atanh
cos cosh sin sinh tanh tan
* The haversin distance function
Expressions always return an integer value from 0 to the maximum
64-bit signed integer value (2^63 - 1). Intermediate results are
calculated as double-precision floating point values and the return
value is rounded to the nearest integer. If the expression is
invalid or evaluates to a negative value, it returns 0. If the
expression evaluates to a value greater than the maximum, it
returns the maximum value.
The source data for an Expression can be the name of an
IndexField of type int or double, another Expression or the
reserved name _score. The _score source is
defined to return as a double from 0 to 10.0 (inclusive) to
indicate how relevant a document is to the search request,
taking into account repetition of search terms in the
document and proximity of search terms to each other in
each matching IndexField in the document.
For more information about using rank expressions to
customize ranking, see the Amazon CloudSearch Developer
Guide.
:return: ExpressionStatus object
:rtype: :class:`boto.cloudsearch2.option.ExpressionStatus` object
:raises: BaseException, InternalException, LimitExceededException,
InvalidTypeException, ResourceNotFoundException
"""
data = self.layer1.define_expression(self.name, name, value)
data = (data['DefineExpressionResponse']
['DefineExpressionResult']
['Expression'])
return ExpressionStatus(self, data,
self.layer1.describe_expressions)
def get_document_service(self):
return DocumentServiceConnection(domain=self)
def get_search_service(self):
return SearchConnection(domain=self)
def __repr__(self):
return '<Domain: %s>' % self.domain_name

View File

@@ -0,0 +1,46 @@
"""
Exceptions that are specific to the cloudsearch2 module.
"""
from boto.exception import BotoServerError
class InvalidTypeException(BotoServerError):
"""
Raised when an invalid record type is passed to CloudSearch.
"""
pass
class LimitExceededException(BotoServerError):
"""
Raised when a limit has been exceeded.
"""
pass
class InternalException(BotoServerError):
"""
A generic server-side error.
"""
pass
class DisabledOperationException(BotoServerError):
"""
Raised when an operation has been disabled.
"""
pass
class ResourceNotFoundException(BotoServerError):
"""
Raised when a requested resource does not exist.
"""
pass
class BaseException(BotoServerError):
"""
A generic server-side error.
"""
pass

View File

@@ -0,0 +1,779 @@
# Copyright (c) 2014 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.
#
import boto
from boto.connection import AWSQueryConnection
from boto.regioninfo import RegionInfo
from boto.exception import JSONResponseError
from boto.cloudsearch2 import exceptions
from boto.compat import json
class CloudSearchConnection(AWSQueryConnection):
"""
Amazon CloudSearch Configuration Service
You use the Amazon CloudSearch configuration service to create,
configure, and manage search domains. Configuration service
requests are submitted using the AWS Query protocol. AWS Query
requests are HTTP or HTTPS requests submitted via HTTP GET or POST
with a query parameter named Action.
The endpoint for configuration service requests is region-
specific: cloudsearch. region .amazonaws.com. For example,
cloudsearch.us-east-1.amazonaws.com. For a current list of
supported regions and endpoints, see `Regions and Endpoints`_.
"""
APIVersion = "2013-01-01"
DefaultRegionName = "us-east-1"
DefaultRegionEndpoint = "cloudsearch.us-east-1.amazonaws.com"
ResponseError = JSONResponseError
_faults = {
"InvalidTypeException": exceptions.InvalidTypeException,
"LimitExceededException": exceptions.LimitExceededException,
"InternalException": exceptions.InternalException,
"DisabledOperationException": exceptions.DisabledOperationException,
"ResourceNotFoundException": exceptions.ResourceNotFoundException,
"BaseException": exceptions.BaseException,
}
def __init__(self, **kwargs):
region = kwargs.pop('region', None)
if not region:
region = RegionInfo(self, self.DefaultRegionName,
self.DefaultRegionEndpoint)
if 'host' not in kwargs or kwargs['host'] is None:
kwargs['host'] = region.endpoint
super(CloudSearchConnection, self).__init__(**kwargs)
self.region = region
def _required_auth_capability(self):
return ['hmac-v4']
def build_suggesters(self, domain_name):
"""
Indexes the search suggestions.
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
"""
params = {'DomainName': domain_name, }
return self._make_request(
action='BuildSuggesters',
verb='POST',
path='/', params=params)
def create_domain(self, domain_name):
"""
Creates a new search domain. For more information, see
`Creating a Search Domain`_ in the Amazon CloudSearch
Developer Guide .
:type domain_name: string
:param domain_name: A name for the domain you are creating. Allowed
characters are a-z (lower-case letters), 0-9, and hyphen (-).
Domain names must start with a letter or number and be at least 3
and no more than 28 characters long.
"""
params = {'DomainName': domain_name, }
return self._make_request(
action='CreateDomain',
verb='POST',
path='/', params=params)
def define_analysis_scheme(self, domain_name, analysis_scheme):
"""
Configures an analysis scheme for a domain. An analysis scheme
defines language-specific text processing options for a `text`
field. For more information, see `Configuring Analysis
Schemes`_ in the Amazon CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type analysis_scheme: dict
:param analysis_scheme: Configuration information for an analysis
scheme. Each analysis scheme has a unique name and specifies the
language of the text to be processed. The following options can be
configured for an analysis scheme: `Synonyms`, `Stopwords`,
`StemmingDictionary`, and `AlgorithmicStemming`.
"""
params = {'DomainName': domain_name, }
self.build_complex_param(params, 'AnalysisScheme',
analysis_scheme)
return self._make_request(
action='DefineAnalysisScheme',
verb='POST',
path='/', params=params)
def define_expression(self, domain_name, expression):
"""
Configures an `Expression` for the search domain. Used to
create new expressions and modify existing ones. If the
expression exists, the new configuration replaces the old one.
For more information, see `Configuring Expressions`_ in the
Amazon CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type expression: dict
:param expression: A named expression that can be evaluated at search
time. Can be used for sorting and filtering search results and
constructing other expressions.
"""
params = {'DomainName': domain_name, }
self.build_complex_param(params, 'Expression',
expression)
return self._make_request(
action='DefineExpression',
verb='POST',
path='/', params=params)
def define_index_field(self, domain_name, index_field):
"""
Configures an `IndexField` for the search domain. Used to
create new fields and modify existing ones. You must specify
the name of the domain you are configuring and an index field
configuration. The index field configuration specifies a
unique name, the index field type, and the options you want to
configure for the field. The options you can specify depend on
the `IndexFieldType`. If the field exists, the new
configuration replaces the old one. For more information, see
`Configuring Index Fields`_ in the Amazon CloudSearch
Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type index_field: dict
:param index_field: The index field and field options you want to
configure.
"""
params = {'DomainName': domain_name, }
self.build_complex_param(params, 'IndexField',
index_field)
return self._make_request(
action='DefineIndexField',
verb='POST',
path='/', params=params)
def define_suggester(self, domain_name, suggester):
"""
Configures a suggester for a domain. A suggester enables you
to display possible matches before users finish typing their
queries. When you configure a suggester, you must specify the
name of the text field you want to search for possible matches
and a unique name for the suggester. For more information, see
`Getting Search Suggestions`_ in the Amazon CloudSearch
Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type suggester: dict
:param suggester: Configuration information for a search suggester.
Each suggester has a unique name and specifies the text field you
want to use for suggestions. The following options can be
configured for a suggester: `FuzzyMatching`, `SortExpression`.
"""
params = {'DomainName': domain_name, }
self.build_complex_param(params, 'Suggester',
suggester)
return self._make_request(
action='DefineSuggester',
verb='POST',
path='/', params=params)
def delete_analysis_scheme(self, domain_name, analysis_scheme_name):
"""
Deletes an analysis scheme. For more information, see
`Configuring Analysis Schemes`_ in the Amazon CloudSearch
Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type analysis_scheme_name: string
:param analysis_scheme_name: The name of the analysis scheme you want
to delete.
"""
params = {
'DomainName': domain_name,
'AnalysisSchemeName': analysis_scheme_name,
}
return self._make_request(
action='DeleteAnalysisScheme',
verb='POST',
path='/', params=params)
def delete_domain(self, domain_name):
"""
Permanently deletes a search domain and all of its data. Once
a domain has been deleted, it cannot be recovered. For more
information, see `Deleting a Search Domain`_ in the Amazon
CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: The name of the domain you want to permanently
delete.
"""
params = {'DomainName': domain_name, }
return self._make_request(
action='DeleteDomain',
verb='POST',
path='/', params=params)
def delete_expression(self, domain_name, expression_name):
"""
Removes an `Expression` from the search domain. For more
information, see `Configuring Expressions`_ in the Amazon
CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type expression_name: string
:param expression_name: The name of the `Expression` to delete.
"""
params = {
'DomainName': domain_name,
'ExpressionName': expression_name,
}
return self._make_request(
action='DeleteExpression',
verb='POST',
path='/', params=params)
def delete_index_field(self, domain_name, index_field_name):
"""
Removes an `IndexField` from the search domain. For more
information, see `Configuring Index Fields`_ in the Amazon
CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type index_field_name: string
:param index_field_name: The name of the index field your want to
remove from the domain's indexing options.
"""
params = {
'DomainName': domain_name,
'IndexFieldName': index_field_name,
}
return self._make_request(
action='DeleteIndexField',
verb='POST',
path='/', params=params)
def delete_suggester(self, domain_name, suggester_name):
"""
Deletes a suggester. For more information, see `Getting Search
Suggestions`_ in the Amazon CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type suggester_name: string
:param suggester_name: Specifies the name of the suggester you want to
delete.
"""
params = {
'DomainName': domain_name,
'SuggesterName': suggester_name,
}
return self._make_request(
action='DeleteSuggester',
verb='POST',
path='/', params=params)
def describe_analysis_schemes(self, domain_name,
analysis_scheme_names=None, deployed=None):
"""
Gets the analysis schemes configured for a domain. An analysis
scheme defines language-specific text processing options for a
`text` field. Can be limited to specific analysis schemes by
name. By default, shows all analysis schemes and includes any
pending changes to the configuration. Set the `Deployed`
option to `True` to show the active configuration and exclude
pending changes. For more information, see `Configuring
Analysis Schemes`_ in the Amazon CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: The name of the domain you want to describe.
:type analysis_scheme_names: list
:param analysis_scheme_names: The analysis schemes you want to
describe.
:type deployed: boolean
:param deployed: Whether to display the deployed configuration (
`True`) or include any pending changes ( `False`). Defaults to
`False`.
"""
params = {'DomainName': domain_name, }
if analysis_scheme_names is not None:
self.build_list_params(params,
analysis_scheme_names,
'AnalysisSchemeNames.member')
if deployed is not None:
params['Deployed'] = str(
deployed).lower()
return self._make_request(
action='DescribeAnalysisSchemes',
verb='POST',
path='/', params=params)
def describe_availability_options(self, domain_name, deployed=None):
"""
Gets the availability options configured for a domain. By
default, shows the configuration with any pending changes. Set
the `Deployed` option to `True` to show the active
configuration and exclude pending changes. For more
information, see `Configuring Availability Options`_ in the
Amazon CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: The name of the domain you want to describe.
:type deployed: boolean
:param deployed: Whether to display the deployed configuration (
`True`) or include any pending changes ( `False`). Defaults to
`False`.
"""
params = {'DomainName': domain_name, }
if deployed is not None:
params['Deployed'] = str(
deployed).lower()
return self._make_request(
action='DescribeAvailabilityOptions',
verb='POST',
path='/', params=params)
def describe_domains(self, domain_names=None):
"""
Gets information about the search domains owned by this
account. Can be limited to specific domains. Shows all domains
by default. For more information, see `Getting Information
about a Search Domain`_ in the Amazon CloudSearch Developer
Guide .
:type domain_names: list
:param domain_names: The names of the domains you want to include in
the response.
"""
params = {}
if domain_names is not None:
self.build_list_params(params,
domain_names,
'DomainNames.member')
return self._make_request(
action='DescribeDomains',
verb='POST',
path='/', params=params)
def describe_expressions(self, domain_name, expression_names=None,
deployed=None):
"""
Gets the expressions configured for the search domain. Can be
limited to specific expressions by name. By default, shows all
expressions and includes any pending changes to the
configuration. Set the `Deployed` option to `True` to show the
active configuration and exclude pending changes. For more
information, see `Configuring Expressions`_ in the Amazon
CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: The name of the domain you want to describe.
:type expression_names: list
:param expression_names: Limits the `DescribeExpressions` response to
the specified expressions. If not specified, all expressions are
shown.
:type deployed: boolean
:param deployed: Whether to display the deployed configuration (
`True`) or include any pending changes ( `False`). Defaults to
`False`.
"""
params = {'DomainName': domain_name, }
if expression_names is not None:
self.build_list_params(params,
expression_names,
'ExpressionNames.member')
if deployed is not None:
params['Deployed'] = str(
deployed).lower()
return self._make_request(
action='DescribeExpressions',
verb='POST',
path='/', params=params)
def describe_index_fields(self, domain_name, field_names=None,
deployed=None):
"""
Gets information about the index fields configured for the
search domain. Can be limited to specific fields by name. By
default, shows all fields and includes any pending changes to
the configuration. Set the `Deployed` option to `True` to show
the active configuration and exclude pending changes. For more
information, see `Getting Domain Information`_ in the Amazon
CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: The name of the domain you want to describe.
:type field_names: list
:param field_names: A list of the index fields you want to describe. If
not specified, information is returned for all configured index
fields.
:type deployed: boolean
:param deployed: Whether to display the deployed configuration (
`True`) or include any pending changes ( `False`). Defaults to
`False`.
"""
params = {'DomainName': domain_name, }
if field_names is not None:
self.build_list_params(params,
field_names,
'FieldNames.member')
if deployed is not None:
params['Deployed'] = str(
deployed).lower()
return self._make_request(
action='DescribeIndexFields',
verb='POST',
path='/', params=params)
def describe_scaling_parameters(self, domain_name):
"""
Gets the scaling parameters configured for a domain. A
domain's scaling parameters specify the desired search
instance type and replication count. For more information, see
`Configuring Scaling Options`_ in the Amazon CloudSearch
Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
"""
params = {'DomainName': domain_name, }
return self._make_request(
action='DescribeScalingParameters',
verb='POST',
path='/', params=params)
def describe_service_access_policies(self, domain_name, deployed=None):
"""
Gets information about the access policies that control access
to the domain's document and search endpoints. By default,
shows the configuration with any pending changes. Set the
`Deployed` option to `True` to show the active configuration
and exclude pending changes. For more information, see
`Configuring Access for a Search Domain`_ in the Amazon
CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: The name of the domain you want to describe.
:type deployed: boolean
:param deployed: Whether to display the deployed configuration (
`True`) or include any pending changes ( `False`). Defaults to
`False`.
"""
params = {'DomainName': domain_name, }
if deployed is not None:
params['Deployed'] = str(
deployed).lower()
return self._make_request(
action='DescribeServiceAccessPolicies',
verb='POST',
path='/', params=params)
def describe_suggesters(self, domain_name, suggester_names=None,
deployed=None):
"""
Gets the suggesters configured for a domain. A suggester
enables you to display possible matches before users finish
typing their queries. Can be limited to specific suggesters by
name. By default, shows all suggesters and includes any
pending changes to the configuration. Set the `Deployed`
option to `True` to show the active configuration and exclude
pending changes. For more information, see `Getting Search
Suggestions`_ in the Amazon CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: The name of the domain you want to describe.
:type suggester_names: list
:param suggester_names: The suggesters you want to describe.
:type deployed: boolean
:param deployed: Whether to display the deployed configuration (
`True`) or include any pending changes ( `False`). Defaults to
`False`.
"""
params = {'DomainName': domain_name, }
if suggester_names is not None:
self.build_list_params(params,
suggester_names,
'SuggesterNames.member')
if deployed is not None:
params['Deployed'] = str(
deployed).lower()
return self._make_request(
action='DescribeSuggesters',
verb='POST',
path='/', params=params)
def index_documents(self, domain_name):
"""
Tells the search domain to start indexing its documents using
the latest indexing options. This operation must be invoked to
activate options whose OptionStatus is
`RequiresIndexDocuments`.
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
"""
params = {'DomainName': domain_name, }
return self._make_request(
action='IndexDocuments',
verb='POST',
path='/', params=params)
def list_domain_names(self):
"""
Lists all search domains owned by an account.
"""
params = {}
return self._make_request(
action='ListDomainNames',
verb='POST',
path='/', params=params)
def update_availability_options(self, domain_name, multi_az):
"""
Configures the availability options for a domain. Enabling the
Multi-AZ option expands an Amazon CloudSearch domain to an
additional Availability Zone in the same Region to increase
fault tolerance in the event of a service disruption. Changes
to the Multi-AZ option can take about half an hour to become
active. For more information, see `Configuring Availability
Options`_ in the Amazon CloudSearch Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type multi_az: boolean
:param multi_az: You expand an existing search domain to a second
Availability Zone by setting the Multi-AZ option to true.
Similarly, you can turn off the Multi-AZ option to downgrade the
domain to a single Availability Zone by setting the Multi-AZ option
to `False`.
"""
params = {'DomainName': domain_name, 'MultiAZ': multi_az, }
return self._make_request(
action='UpdateAvailabilityOptions',
verb='POST',
path='/', params=params)
def update_scaling_parameters(self, domain_name, scaling_parameters):
"""
Configures scaling parameters for a domain. A domain's scaling
parameters specify the desired search instance type and
replication count. Amazon CloudSearch will still automatically
scale your domain based on the volume of data and traffic, but
not below the desired instance type and replication count. If
the Multi-AZ option is enabled, these values control the
resources used per Availability Zone. For more information,
see `Configuring Scaling Options`_ in the Amazon CloudSearch
Developer Guide .
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type scaling_parameters: dict
:param scaling_parameters: The desired instance type and desired number
of replicas of each index partition.
"""
params = {'DomainName': domain_name, }
self.build_complex_param(params, 'ScalingParameters',
scaling_parameters)
return self._make_request(
action='UpdateScalingParameters',
verb='POST',
path='/', params=params)
def update_service_access_policies(self, domain_name, access_policies):
"""
Configures the access rules that control access to the
domain's document and search endpoints. For more information,
see ` Configuring Access for an Amazon CloudSearch Domain`_.
:type domain_name: string
:param domain_name: A string that represents the name of a domain.
Domain names are unique across the domains owned by an account
within an AWS region. Domain names start with a letter or number
and can contain the following characters: a-z (lowercase), 0-9, and
- (hyphen).
:type access_policies: string
:param access_policies: The access rules you want to configure. These
rules replace any existing rules.
"""
params = {
'DomainName': domain_name,
'AccessPolicies': access_policies,
}
return self._make_request(
action='UpdateServiceAccessPolicies',
verb='POST',
path='/', params=params)
def build_complex_param(self, params, label, value):
"""Serialize a structure.
For example::
param_type = 'structure'
label = 'IndexField'
value = {'IndexFieldName': 'a', 'IntOptions': {'DefaultValue': 5}}
would result in the params dict being updated with these params::
IndexField.IndexFieldName = a
IndexField.IntOptions.DefaultValue = 5
:type params: dict
:param params: The params dict. The complex list params
will be added to this dict.
:type label: str
:param label: String label for param key
:type value: any
:param value: The value to serialize
"""
for k, v in value.items():
if isinstance(v, dict):
for k2, v2 in v.items():
self.build_complex_param(params, label + '.' + k, v)
elif isinstance(v, bool):
params['%s.%s' % (label, k)] = v and 'true' or 'false'
else:
params['%s.%s' % (label, k)] = v
def _make_request(self, action, verb, path, params):
params['ContentType'] = 'JSON'
response = self.make_request(action=action, verb='POST',
path='/', params=params)
body = response.read().decode('utf-8')
boto.log.debug(body)
if response.status == 200:
return json.loads(body)
else:
json_body = json.loads(body)
fault_name = json_body.get('Error', {}).get('Code', None)
exception_class = self._faults.get(fault_name, self.ResponseError)
raise exception_class(response.status, response.reason,
body=json_body)

View File

@@ -0,0 +1,93 @@
# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
# 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.
#
from boto.cloudsearch2.layer1 import CloudSearchConnection
from boto.cloudsearch2.domain import Domain
from boto.compat import six
class Layer2(object):
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
is_secure=True, port=None, proxy=None, proxy_port=None,
host=None, debug=0, session_token=None, region=None,
validate_certs=True):
if isinstance(region, six.string_types):
import boto.cloudsearch2
for region_info in boto.cloudsearch2.regions():
if region_info.name == region:
region = region_info
break
self.layer1 = CloudSearchConnection(
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
is_secure=is_secure,
port=port,
proxy=proxy,
proxy_port=proxy_port,
host=host,
debug=debug,
security_token=session_token,
region=region,
validate_certs=validate_certs)
def list_domains(self, domain_names=None):
"""
Return a list of objects for each domain defined in the
current account.
:rtype: list of :class:`boto.cloudsearch2.domain.Domain`
"""
domain_data = self.layer1.describe_domains(domain_names)
domain_data = (domain_data['DescribeDomainsResponse']
['DescribeDomainsResult']
['DomainStatusList'])
return [Domain(self.layer1, data) for data in domain_data]
def create_domain(self, domain_name):
"""
Create a new CloudSearch domain and return the corresponding object.
:return: Domain object, or None if the domain isn't found
:rtype: :class:`boto.cloudsearch2.domain.Domain`
"""
data = self.layer1.create_domain(domain_name)
return Domain(self.layer1, data['CreateDomainResponse']
['CreateDomainResult']
['DomainStatus'])
def lookup(self, domain_name):
"""
Lookup a single domain
:param domain_name: The name of the domain to look up
:type domain_name: str
:return: Domain object, or None if the domain isn't found
:rtype: :class:`boto.cloudsearch2.domain.Domain`
"""
domains = self.list_domains(domain_names=[domain_name])
if len(domains) > 0:
return domains[0]

View File

@@ -0,0 +1,233 @@
# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
# 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.
#
from boto.compat import json
class OptionStatus(dict):
"""
Presents a combination of status field (defined below) which are
accessed as attributes and option values which are stored in the
native Python dictionary. In this class, the option values are
merged from a JSON object that is stored as the Option part of
the object.
:ivar domain_name: The name of the domain this option is associated with.
:ivar create_date: A timestamp for when this option was created.
:ivar state: The state of processing a change to an option.
Possible values:
* RequiresIndexDocuments: the option's latest value will not
be visible in searches until IndexDocuments has been called
and indexing is complete.
* Processing: the option's latest value is not yet visible in
all searches but is in the process of being activated.
* Active: the option's latest value is completely visible.
:ivar update_date: A timestamp for when this option was updated.
:ivar update_version: A unique integer that indicates when this
option was last updated.
"""
def __init__(self, domain, data=None, refresh_fn=None, refresh_key=None,
save_fn=None):
self.domain = domain
self.refresh_fn = refresh_fn
self.refresh_key = refresh_key
self.save_fn = save_fn
self.refresh(data)
def _update_status(self, status):
self.creation_date = status['CreationDate']
self.status = status['State']
self.update_date = status['UpdateDate']
self.update_version = int(status['UpdateVersion'])
def _update_options(self, options):
if options:
self.update(options)
def refresh(self, data=None):
"""
Refresh the local state of the object. You can either pass
new state data in as the parameter ``data`` or, if that parameter
is omitted, the state data will be retrieved from CloudSearch.
"""
if not data:
if self.refresh_fn:
data = self.refresh_fn(self.domain.name)
if data and self.refresh_key:
# Attempt to pull out the right nested bag of data
for key in self.refresh_key:
data = data[key]
if data:
self._update_status(data['Status'])
self._update_options(data['Options'])
def to_json(self):
"""
Return the JSON representation of the options as a string.
"""
return json.dumps(self)
def save(self):
"""
Write the current state of the local object back to the
CloudSearch service.
"""
if self.save_fn:
data = self.save_fn(self.domain.name, self.to_json())
self.refresh(data)
class IndexFieldStatus(OptionStatus):
def save(self):
pass
class AvailabilityOptionsStatus(OptionStatus):
def save(self):
pass
class ScalingParametersStatus(IndexFieldStatus):
pass
class ExpressionStatus(IndexFieldStatus):
pass
class ServicePoliciesStatus(OptionStatus):
def new_statement(self, arn, ip):
"""
Returns a new policy statement that will allow
access to the service described by ``arn`` by the
ip specified in ``ip``.
:type arn: string
:param arn: The Amazon Resource Notation identifier for the
service you wish to provide access to. This would be
either the search service or the document service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
return {
"Effect": "Allow",
"Action": "*", # Docs say use GET, but denies unless *
"Resource": arn,
"Condition": {
"IpAddress": {
"aws:SourceIp": [ip]
}
}
}
def _allow_ip(self, arn, ip):
if 'Statement' not in self:
s = self.new_statement(arn, ip)
self['Statement'] = [s]
self.save()
else:
add_statement = True
for statement in self['Statement']:
if statement['Resource'] == arn:
for condition_name in statement['Condition']:
if condition_name == 'IpAddress':
add_statement = False
condition = statement['Condition'][condition_name]
if ip not in condition['aws:SourceIp']:
condition['aws:SourceIp'].append(ip)
if add_statement:
s = self.new_statement(arn, ip)
self['Statement'].append(s)
self.save()
def allow_search_ip(self, ip):
"""
Add the provided ip address or CIDR block to the list of
allowable address for the search service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
arn = self.domain.service_arn
self._allow_ip(arn, ip)
def allow_doc_ip(self, ip):
"""
Add the provided ip address or CIDR block to the list of
allowable address for the document service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
arn = self.domain.service_arn
self._allow_ip(arn, ip)
def _disallow_ip(self, arn, ip):
if 'Statement' not in self:
return
need_update = False
for statement in self['Statement']:
if statement['Resource'] == arn:
for condition_name in statement['Condition']:
if condition_name == 'IpAddress':
condition = statement['Condition'][condition_name]
if ip in condition['aws:SourceIp']:
condition['aws:SourceIp'].remove(ip)
need_update = True
if need_update:
self.save()
def disallow_search_ip(self, ip):
"""
Remove the provided ip address or CIDR block from the list of
allowable address for the search service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
arn = self.domain.service_arn
self._disallow_ip(arn, ip)
def disallow_doc_ip(self, ip):
"""
Remove the provided ip address or CIDR block from the list of
allowable address for the document service.
:type ip: string
:param ip: An IP address or CIDR block you wish to grant access
to.
"""
arn = self.domain.service_arn
self._disallow_ip(arn, ip)

View File

@@ -0,0 +1,363 @@
# Copyright (c) 2014 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.
#
from math import ceil
from boto.compat import json, map, six
import requests
SIMPLE = 'simple'
STRUCTURED = 'structured'
LUCENE = 'lucene'
DISMAX = 'dismax'
class SearchServiceException(Exception):
pass
class SearchResults(object):
def __init__(self, **attrs):
self.rid = attrs['status']['rid']
self.time_ms = attrs['status']['time-ms']
self.hits = attrs['hits']['found']
self.docs = attrs['hits']['hit']
self.start = attrs['hits']['start']
self.query = attrs['query']
self.search_service = attrs['search_service']
self.facets = {}
if 'facets' in attrs:
for (facet, values) in attrs['facets'].items():
if 'buckets' in values:
self.facets[facet] = dict((k, v) for (k, v) in map(lambda x: (x['value'], x['count']), values.get('buckets', [])))
self.num_pages_needed = ceil(self.hits / self.query.real_size)
def __len__(self):
return len(self.docs)
def __iter__(self):
return iter(self.docs)
def next_page(self):
"""Call Cloudsearch to get the next page of search results
:rtype: :class:`boto.cloudsearch2.search.SearchResults`
:return: the following page of search results
"""
if self.query.page <= self.num_pages_needed:
self.query.start += self.query.real_size
self.query.page += 1
return self.search_service(self.query)
else:
raise StopIteration
class Query(object):
RESULTS_PER_PAGE = 500
def __init__(self, q=None, parser=None, fq=None, expr=None,
return_fields=None, size=10, start=0, sort=None,
facet=None, highlight=None, partial=None, options=None):
self.q = q
self.parser = parser
self.fq = fq
self.expr = expr or {}
self.sort = sort or []
self.return_fields = return_fields or []
self.start = start
self.facet = facet or {}
self.highlight = highlight or {}
self.partial = partial
self.options = options
self.page = 0
self.update_size(size)
def update_size(self, new_size):
self.size = new_size
self.real_size = Query.RESULTS_PER_PAGE if (self.size >
Query.RESULTS_PER_PAGE or self.size == 0) else self.size
def to_params(self):
"""Transform search parameters from instance properties to a dictionary
:rtype: dict
:return: search parameters
"""
params = {'start': self.start, 'size': self.real_size}
if self.q:
params['q'] = self.q
if self.parser:
params['q.parser'] = self.parser
if self.fq:
params['fq'] = self.fq
if self.expr:
for k, v in six.iteritems(self.expr):
params['expr.%s' % k] = v
if self.facet:
for k, v in six.iteritems(self.facet):
if not isinstance(v, six.string_types):
v = json.dumps(v)
params['facet.%s' % k] = v
if self.highlight:
for k, v in six.iteritems(self.highlight):
params['highlight.%s' % k] = v
if self.options:
params['options'] = self.options
if self.return_fields:
params['return'] = ','.join(self.return_fields)
if self.partial is not None:
params['partial'] = self.partial
if self.sort:
params['sort'] = ','.join(self.sort)
return params
class SearchConnection(object):
def __init__(self, domain=None, endpoint=None):
self.domain = domain
self.endpoint = endpoint
self.session = requests.Session()
if not endpoint:
self.endpoint = domain.search_service_endpoint
def build_query(self, q=None, parser=None, fq=None, rank=None, return_fields=None,
size=10, start=0, facet=None, highlight=None, sort=None,
partial=None, options=None):
return Query(q=q, parser=parser, fq=fq, expr=rank, return_fields=return_fields,
size=size, start=start, facet=facet, highlight=highlight,
sort=sort, partial=partial, options=options)
def search(self, q=None, parser=None, fq=None, rank=None, return_fields=None,
size=10, start=0, facet=None, highlight=None, sort=None, partial=None,
options=None):
"""
Send a query to CloudSearch
Each search query should use at least the q or bq argument to specify
the search parameter. The other options are used to specify the
criteria of the search.
:type q: string
:param q: A string to search the default search fields for.
:type parser: string
:param parser: The parser to use. 'simple', 'structured', 'lucene', 'dismax'
:type fq: string
:param fq: The filter query to use.
:type sort: List of strings
:param sort: A list of fields or rank expressions used to order the
search results. Order is handled by adding 'desc' or 'asc' after the field name.
``['year desc', 'author asc']``
:type return_fields: List of strings
:param return_fields: A list of fields which should be returned by the
search. If this field is not specified, only IDs will be returned.
``['headline']``
:type size: int
:param size: Number of search results to specify
:type start: int
:param start: Offset of the first search result to return (can be used
for paging)
:type facet: dict
:param facet: Dictionary of fields for which facets should be returned
The facet value is string of JSON options
``{'year': '{sort:"bucket", size:3}', 'genres': '{buckets:["Action","Adventure","Sci-Fi"]}'}``
:type highlight: dict
:param highlight: Dictionary of fields for which highlights should be returned
The facet value is string of JSON options
``{'genres': '{format:'text',max_phrases:2,pre_tag:'<b>',post_tag:'</b>'}'}``
:type partial: bool
:param partial: Should partial results from a partioned service be returned if
one or more index partitions are unreachable.
:type options: str
:param options: Options for the query parser specified in *parser*.
Specified as a string in JSON format.
``{fields: ['title^5', 'description']}``
:rtype: :class:`boto.cloudsearch2.search.SearchResults`
:return: Returns the results of this search
The following examples all assume we have indexed a set of documents
with fields: *author*, *date*, *headline*
A simple search will look for documents whose default text search
fields will contain the search word exactly:
>>> search(q='Tim') # Return documents with the word Tim in them (but not Timothy)
A simple search with more keywords will return documents whose default
text search fields contain the search strings together or separately.
>>> search(q='Tim apple') # Will match "tim" and "apple"
More complex searches require the boolean search operator.
Wildcard searches can be used to search for any words that start with
the search string.
>>> search(q="'Tim*'") # Return documents with words like Tim or Timothy)
Search terms can also be combined. Allowed operators are "and", "or",
"not", "field", "optional", "token", "phrase", or "filter"
>>> search(q="(and 'Tim' (field author 'John Smith'))", parser='structured')
Facets allow you to show classification information about the search
results. For example, you can retrieve the authors who have written
about Tim with a max of 3
>>> search(q='Tim', facet={'Author': '{sort:"bucket", size:3}'})
"""
query = self.build_query(q=q, parser=parser, fq=fq, rank=rank,
return_fields=return_fields,
size=size, start=start, facet=facet,
highlight=highlight, sort=sort,
partial=partial, options=options)
return self(query)
def __call__(self, query):
"""Make a call to CloudSearch
:type query: :class:`boto.cloudsearch2.search.Query`
:param query: A group of search criteria
:rtype: :class:`boto.cloudsearch2.search.SearchResults`
:return: search results
"""
api_version = '2013-01-01'
if self.domain:
api_version = self.domain.layer1.APIVersion
url = "http://%s/%s/search" % (self.endpoint, api_version)
params = query.to_params()
r = self.session.get(url, params=params)
_body = r.content.decode('utf-8')
try:
data = json.loads(_body)
except ValueError:
if r.status_code == 403:
msg = ''
import re
g = re.search('<html><body><h1>403 Forbidden</h1>([^<]+)<', _body)
try:
msg = ': %s' % (g.groups()[0].strip())
except AttributeError:
pass
raise SearchServiceException('Authentication error from Amazon%s' % msg)
raise SearchServiceException("Got non-json response from Amazon. %s" % _body, query)
if 'messages' in data and 'error' in data:
for m in data['messages']:
if m['severity'] == 'fatal':
raise SearchServiceException("Error processing search %s "
"=> %s" % (params, m['message']), query)
elif 'error' in data:
raise SearchServiceException("Unknown error processing search %s"
% json.dumps(data), query)
data['query'] = query
data['search_service'] = self
return SearchResults(**data)
def get_all_paged(self, query, per_page):
"""Get a generator to iterate over all pages of search results
:type query: :class:`boto.cloudsearch2.search.Query`
:param query: A group of search criteria
:type per_page: int
:param per_page: Number of docs in each :class:`boto.cloudsearch2.search.SearchResults` object.
:rtype: generator
:return: Generator containing :class:`boto.cloudsearch2.search.SearchResults`
"""
query.update_size(per_page)
page = 0
num_pages_needed = 0
while page <= num_pages_needed:
results = self(query)
num_pages_needed = results.num_pages_needed
yield results
query.start += query.real_size
page += 1
def get_all_hits(self, query):
"""Get a generator to iterate over all search results
Transparently handles the results paging from Cloudsearch
search results so even if you have many thousands of results
you can iterate over all results in a reasonably efficient
manner.
:type query: :class:`boto.cloudsearch2.search.Query`
:param query: A group of search criteria
:rtype: generator
:return: All docs matching query
"""
page = 0
num_pages_needed = 0
while page <= num_pages_needed:
results = self(query)
num_pages_needed = results.num_pages_needed
for doc in results:
yield doc
query.start += query.real_size
page += 1
def get_num_hits(self, query):
"""Return the total number of hits for query
:type query: :class:`boto.cloudsearch2.search.Query`
:param query: a group of search criteria
:rtype: int
:return: Total number of hits for query
"""
query.update_size(1)
return self(query).hits

View File

@@ -20,16 +20,12 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
try:
import json
except ImportError:
import simplejson as json
import boto import boto
from boto.connection import AWSQueryConnection from boto.connection import AWSQueryConnection
from boto.regioninfo import RegionInfo from boto.regioninfo import RegionInfo
from boto.exception import JSONResponseError from boto.exception import JSONResponseError
from boto.cloudtrail import exceptions from boto.cloudtrail import exceptions
from boto.compat import json
class CloudTrailConnection(AWSQueryConnection): class CloudTrailConnection(AWSQueryConnection):
@@ -344,7 +340,7 @@ class CloudTrailConnection(AWSQueryConnection):
headers=headers, data=body) headers=headers, data=body)
response = self._mexe(http_request, sender=None, response = self._mexe(http_request, sender=None,
override_num_retries=10) override_num_retries=10)
response_body = response.read() response_body = response.read().decode('utf-8')
boto.log.debug(response_body) boto.log.debug(response_body)
if response.status == 200: if response.status == 200:
if response_body: if response_body:

View File

@@ -19,6 +19,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
import os
# This allows boto modules to say "from boto.compat import json". This is # This allows boto modules to say "from boto.compat import json". This is
# preferred so that all modules don't have to repeat this idiom. # preferred so that all modules don't have to repeat this idiom.
@@ -26,3 +27,41 @@ try:
import simplejson as json import simplejson as json
except ImportError: except ImportError:
import json import json
# Switch to use encodebytes, which deprecates encodestring in Python 3
try:
from base64 import encodebytes
except ImportError:
from base64 import encodestring as encodebytes
# If running in Google App Engine there is no "user" and
# os.path.expanduser() will fail. Attempt to detect this case and use a
# no-op expanduser function in this case.
try:
os.path.expanduser('~')
expanduser = os.path.expanduser
except (AttributeError, ImportError):
# This is probably running on App Engine.
expanduser = (lambda x: x)
from boto.vendored import six
from boto.vendored.six import BytesIO, StringIO
from boto.vendored.six.moves import filter, http_client, map, _thread, \
urllib, zip
from boto.vendored.six.moves.queue import Queue
from boto.vendored.six.moves.urllib.parse import parse_qs, quote, unquote, \
urlparse, urlsplit
from boto.vendored.six.moves.urllib.request import urlopen
if six.PY3:
# StandardError was removed, so use the base exception type instead
StandardError = Exception
long_type = int
from configparser import ConfigParser
else:
StandardError = StandardError
long_type = long
from ConfigParser import SafeConfigParser as ConfigParser

View File

@@ -42,32 +42,26 @@
""" """
Handles basic connections to AWS Handles basic connections to AWS
""" """
from __future__ import with_statement
import base64
from datetime import datetime from datetime import datetime
import errno import errno
import httplib
import os import os
import Queue
import random import random
import re import re
import socket import socket
import sys import sys
import time import time
import urllib
import urlparse
import xml.sax import xml.sax
import copy import copy
import auth from boto import auth
import auth_handler from boto import auth_handler
import boto import boto
import boto.utils import boto.utils
import boto.handler import boto.handler
import boto.cacerts import boto.cacerts
from boto import config, UserAgent from boto import config, UserAgent
from boto.compat import six, http_client, urlparse, quote, encodebytes
from boto.exception import AWSConnectionError from boto.exception import AWSConnectionError
from boto.exception import BotoClientError from boto.exception import BotoClientError
from boto.exception import BotoServerError from boto.exception import BotoServerError
@@ -165,7 +159,7 @@ class HostConnectionPool(object):
def _conn_ready(self, conn): def _conn_ready(self, conn):
""" """
There is a nice state diagram at the top of httplib.py. It There is a nice state diagram at the top of http_client.py. It
indicates that once the response headers have been read (which indicates that once the response headers have been read (which
_mexe does before adding the connection to the pool), a _mexe does before adding the connection to the pool), a
response is attached to the connection, and it stays there response is attached to the connection, and it stays there
@@ -370,11 +364,13 @@ class HTTPRequest(object):
self.headers, self.body)) self.headers, self.body))
def authorize(self, connection, **kwargs): def authorize(self, connection, **kwargs):
for key in self.headers: if not getattr(self, '_headers_quoted', False):
val = self.headers[key] for key in self.headers:
if isinstance(val, unicode): val = self.headers[key]
safe = '!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~' if isinstance(val, six.text_type):
self.headers[key] = urllib.quote_plus(val.encode('utf-8'), safe) safe = '!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~'
self.headers[key] = quote(val.encode('utf-8'), safe)
setattr(self, '_headers_quoted', True)
connection._auth_handler.add_auth(self, **kwargs) connection._auth_handler.add_auth(self, **kwargs)
@@ -384,20 +380,20 @@ class HTTPRequest(object):
if 'Content-Length' not in self.headers: if 'Content-Length' not in self.headers:
if 'Transfer-Encoding' not in self.headers or \ if 'Transfer-Encoding' not in self.headers or \
self.headers['Transfer-Encoding'] != 'chunked': self.headers['Transfer-Encoding'] != 'chunked':
self.headers['Content-Length'] = str(len(self.body)) self.headers['Content-Length'] = len(self.body)
class HTTPResponse(httplib.HTTPResponse): class HTTPResponse(http_client.HTTPResponse):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
httplib.HTTPResponse.__init__(self, *args, **kwargs) http_client.HTTPResponse.__init__(self, *args, **kwargs)
self._cached_response = '' self._cached_response = ''
def read(self, amt=None): def read(self, amt=None):
"""Read the response. """Read the response.
This method does not have the same behavior as This method does not have the same behavior as
httplib.HTTPResponse.read. Instead, if this method is called with http_client.HTTPResponse.read. Instead, if this method is called with
no ``amt`` arg, then the response body will be cached. Subsequent no ``amt`` arg, then the response body will be cached. Subsequent
calls to ``read()`` with no args **will return the cached response**. calls to ``read()`` with no args **will return the cached response**.
@@ -410,10 +406,10 @@ class HTTPResponse(httplib.HTTPResponse):
# will return the full body. Note that this behavior only # will return the full body. Note that this behavior only
# happens if the amt arg is not specified. # happens if the amt arg is not specified.
if not self._cached_response: if not self._cached_response:
self._cached_response = httplib.HTTPResponse.read(self) self._cached_response = http_client.HTTPResponse.read(self)
return self._cached_response return self._cached_response
else: else:
return httplib.HTTPResponse.read(self, amt) return http_client.HTTPResponse.read(self, amt)
class AWSAuthConnection(object): class AWSAuthConnection(object):
@@ -446,7 +442,7 @@ class AWSAuthConnection(object):
:type https_connection_factory: list or tuple :type https_connection_factory: list or tuple
:param https_connection_factory: A pair of an HTTP connection :param https_connection_factory: A pair of an HTTP connection
factory and the exceptions to catch. The factory should have factory and the exceptions to catch. The factory should have
a similar interface to L{httplib.HTTPSConnection}. a similar interface to L{http_client.HTTPSConnection}.
:param str proxy: Address/hostname for a proxy server :param str proxy: Address/hostname for a proxy server
@@ -505,9 +501,9 @@ class AWSAuthConnection(object):
self.port = PORTS_BY_SECURITY[is_secure] self.port = PORTS_BY_SECURITY[is_secure]
self.handle_proxy(proxy, proxy_port, proxy_user, proxy_pass) self.handle_proxy(proxy, proxy_port, proxy_user, proxy_pass)
# define exceptions from httplib that we want to catch and retry # define exceptions from http_client that we want to catch and retry
self.http_exceptions = (httplib.HTTPException, socket.error, self.http_exceptions = (http_client.HTTPException, socket.error,
socket.gaierror, httplib.BadStatusLine) socket.gaierror, http_client.BadStatusLine)
# define subclasses of the above that are not retryable. # define subclasses of the above that are not retryable.
self.http_unretryable_exceptions = [] self.http_unretryable_exceptions = []
if HAVE_HTTPS_CONNECTION: if HAVE_HTTPS_CONNECTION:
@@ -528,12 +524,12 @@ class AWSAuthConnection(object):
self.host = host self.host = host
self.path = path self.path = path
# if the value passed in for debug # if the value passed in for debug
if not isinstance(debug, (int, long)): if not isinstance(debug, six.integer_types):
debug = 0 debug = 0
self.debug = config.getint('Boto', 'debug', debug) self.debug = config.getint('Boto', 'debug', debug)
self.host_header = None self.host_header = None
# Timeout used to tell httplib how long to wait for socket timeouts. # Timeout used to tell http_client how long to wait for socket timeouts.
# Default is to leave timeout unchanged, which will in turn result in # Default is to leave timeout unchanged, which will in turn result in
# the socket's default global timeout being used. To specify a # the socket's default global timeout being used. To specify a
# timeout, set http_socket_timeout in Boto config. Regardless, # timeout, set http_socket_timeout in Boto config. Regardless,
@@ -651,7 +647,7 @@ class AWSAuthConnection(object):
signature_host = self.host signature_host = self.host
else: else:
# This unfortunate little hack can be attributed to # This unfortunate little hack can be attributed to
# a difference in the 2.6 version of httplib. In old # a difference in the 2.6 version of http_client. In old
# versions, it would append ":443" to the hostname sent # versions, it would append ":443" to the hostname sent
# in the Host header and so we needed to make sure we # in the Host header and so we needed to make sure we
# did the same when calculating the V2 signature. In 2.6 # did the same when calculating the V2 signature. In 2.6
@@ -693,8 +689,8 @@ class AWSAuthConnection(object):
self.proxy_pass = config.get_value('Boto', 'proxy_pass', None) self.proxy_pass = config.get_value('Boto', 'proxy_pass', None)
if not self.proxy_port and self.proxy: if not self.proxy_port and self.proxy:
print "http_proxy environment variable does not specify " \ print("http_proxy environment variable does not specify " \
"a port, using default" "a port, using default")
self.proxy_port = self.port self.proxy_port = self.port
self.no_proxy = os.environ.get('no_proxy', '') or os.environ.get('NO_PROXY', '') self.no_proxy = os.environ.get('no_proxy', '') or os.environ.get('NO_PROXY', '')
@@ -755,7 +751,7 @@ class AWSAuthConnection(object):
host, ca_certs=self.ca_certificates_file, host, ca_certs=self.ca_certificates_file,
**http_connection_kwargs) **http_connection_kwargs)
else: else:
connection = httplib.HTTPSConnection(host, connection = http_client.HTTPSConnection(host,
**http_connection_kwargs) **http_connection_kwargs)
else: else:
boto.log.debug('establishing HTTP connection: kwargs=%s' % boto.log.debug('establishing HTTP connection: kwargs=%s' %
@@ -766,7 +762,7 @@ class AWSAuthConnection(object):
connection = self.https_connection_factory(host, connection = self.https_connection_factory(host,
**http_connection_kwargs) **http_connection_kwargs)
else: else:
connection = httplib.HTTPConnection(host, connection = http_client.HTTPConnection(host,
**http_connection_kwargs) **http_connection_kwargs)
if self.debug > 1: if self.debug > 1:
connection.set_debuglevel(self.debug) connection.set_debuglevel(self.debug)
@@ -788,13 +784,13 @@ class AWSAuthConnection(object):
host = '%s:%d' % (host, port) host = '%s:%d' % (host, port)
else: else:
host = '%s:%d' % (self.host, self.port) host = '%s:%d' % (self.host, self.port)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Seems properly to use timeout for connect too
try: timeout = self.http_connection_kwargs.get("timeout")
sock.connect((self.proxy, int(self.proxy_port))) if timeout is not None:
if "timeout" in self.http_connection_kwargs: sock = socket.create_connection((self.proxy,
sock.settimeout(self.http_connection_kwargs["timeout"]) int(self.proxy_port)), timeout)
except: else:
raise sock = socket.create_connection((self.proxy, int(self.proxy_port)))
boto.log.debug("Proxy connection: CONNECT %s HTTP/1.0\r\n", host) boto.log.debug("Proxy connection: CONNECT %s HTTP/1.0\r\n", host)
sock.sendall("CONNECT %s HTTP/1.0\r\n" % host) sock.sendall("CONNECT %s HTTP/1.0\r\n" % host)
sock.sendall("User-Agent: %s\r\n" % UserAgent) sock.sendall("User-Agent: %s\r\n" % UserAgent)
@@ -807,7 +803,7 @@ class AWSAuthConnection(object):
sock.sendall("\r\n") sock.sendall("\r\n")
else: else:
sock.sendall("\r\n") sock.sendall("\r\n")
resp = httplib.HTTPResponse(sock, strict=True, debuglevel=self.debug) resp = http_client.HTTPResponse(sock, strict=True, debuglevel=self.debug)
resp.begin() resp.begin()
if resp.status != 200: if resp.status != 200:
@@ -821,7 +817,7 @@ class AWSAuthConnection(object):
# We can safely close the response, it duped the original socket # We can safely close the response, it duped the original socket
resp.close() resp.close()
h = httplib.HTTPConnection(host) h = http_client.HTTPConnection(host)
if self.https_validate_certificates and HAVE_HTTPS_CONNECTION: if self.https_validate_certificates and HAVE_HTTPS_CONNECTION:
msg = "wrapping ssl socket for proxied connection; " msg = "wrapping ssl socket for proxied connection; "
@@ -843,11 +839,11 @@ class AWSAuthConnection(object):
hostname, cert, 'hostname mismatch') hostname, cert, 'hostname mismatch')
else: else:
# Fallback for old Python without ssl.wrap_socket # Fallback for old Python without ssl.wrap_socket
if hasattr(httplib, 'ssl'): if hasattr(http_client, 'ssl'):
sslSock = httplib.ssl.SSLSocket(sock) sslSock = http_client.ssl.SSLSocket(sock)
else: else:
sslSock = socket.ssl(sock, None, None) sslSock = socket.ssl(sock, None, None)
sslSock = httplib.FakeSocket(sock, sslSock) sslSock = http_client.FakeSocket(sock, sslSock)
# This is a bit unclean # This is a bit unclean
h.sock = sslSock h.sock = sslSock
@@ -858,7 +854,7 @@ class AWSAuthConnection(object):
return path return path
def get_proxy_auth_header(self): def get_proxy_auth_header(self):
auth = base64.encodestring(self.proxy_user + ':' + self.proxy_pass) auth = encodebytes(self.proxy_user + ':' + self.proxy_pass)
return {'Proxy-Authorization': 'Basic %s' % auth} return {'Proxy-Authorization': 'Basic %s' % auth}
def set_host_header(self, request): def set_host_header(self, request):
@@ -899,9 +895,16 @@ class AWSAuthConnection(object):
i = 0 i = 0
connection = self.get_http_connection(request.host, request.port, connection = self.get_http_connection(request.host, request.port,
self.is_secure) self.is_secure)
# Convert body to bytes if needed
if not isinstance(request.body, bytes) and hasattr(request.body,
'encode'):
request.body = request.body.encode('utf-8')
while i <= num_retries: while i <= num_retries:
# Use binary exponential backoff to desynchronize client requests. # Use binary exponential backoff to desynchronize client requests.
next_sleep = random.random() * (2 ** i) next_sleep = min(random.random() * (2 ** i),
boto.config.get('Boto', 'max_retry_delay', 60))
try: try:
# we now re-sign each request before it is retried # we now re-sign each request before it is retried
boto.log.debug('Token: %s' % self.provider.security_token) boto.log.debug('Token: %s' % self.provider.security_token)
@@ -913,6 +916,7 @@ class AWSAuthConnection(object):
if 's3' not in self._required_auth_capability(): if 's3' not in self._required_auth_capability():
if not getattr(self, 'anon', False): if not getattr(self, 'anon', False):
self.set_host_header(request) self.set_host_header(request)
boto.log.debug('Final headers: %s' % request.headers)
request.start_time = datetime.now() request.start_time = datetime.now()
if callable(sender): if callable(sender):
response = sender(connection, request.method, request.path, response = sender(connection, request.method, request.path,
@@ -921,9 +925,10 @@ class AWSAuthConnection(object):
connection.request(request.method, request.path, connection.request(request.method, request.path,
request.body, request.headers) request.body, request.headers)
response = connection.getresponse() response = connection.getresponse()
boto.log.debug('Response headers: %s' % response.getheaders())
location = response.getheader('location') location = response.getheader('location')
# -- gross hack -- # -- gross hack --
# httplib gets confused with chunked responses to HEAD requests # http_client gets confused with chunked responses to HEAD requests
# so I have to fake it out # so I have to fake it out
if request.method == 'HEAD' and getattr(response, if request.method == 'HEAD' and getattr(response,
'chunked', False): 'chunked', False):
@@ -941,6 +946,8 @@ class AWSAuthConnection(object):
msg += 'Retrying in %3.1f seconds' % next_sleep msg += 'Retrying in %3.1f seconds' % next_sleep
boto.log.debug(msg) boto.log.debug(msg)
body = response.read() body = response.read()
if isinstance(body, bytes):
body = body.decode('utf-8')
elif response.status < 300 or response.status >= 400 or \ elif response.status < 300 or response.status >= 400 or \
not location: not location:
# don't return connection to the pool if response contains # don't return connection to the pool if response contains
@@ -959,7 +966,7 @@ class AWSAuthConnection(object):
return response return response
else: else:
scheme, request.host, request.path, \ scheme, request.host, request.path, \
params, query, fragment = urlparse.urlparse(location) params, query, fragment = urlparse(location)
if query: if query:
request.path += '?' + query request.path += '?' + query
# urlparse can return both host and port in netloc, so if # urlparse can return both host and port in netloc, so if
@@ -974,12 +981,12 @@ class AWSAuthConnection(object):
scheme == 'https') scheme == 'https')
response = None response = None
continue continue
except PleaseRetryException, e: except PleaseRetryException as e:
boto.log.debug('encountered a retry exception: %s' % e) boto.log.debug('encountered a retry exception: %s' % e)
connection = self.new_http_connection(request.host, request.port, connection = self.new_http_connection(request.host, request.port,
self.is_secure) self.is_secure)
response = e.response response = e.response
except self.http_exceptions, e: except self.http_exceptions as e:
for unretryable in self.http_unretryable_exceptions: for unretryable in self.http_unretryable_exceptions:
if isinstance(e, unretryable): if isinstance(e, unretryable):
boto.log.debug( boto.log.debug(
@@ -1089,7 +1096,7 @@ class AWSQueryConnection(AWSAuthConnection):
return self._mexe(http_request) return self._mexe(http_request)
def build_list_params(self, params, items, label): def build_list_params(self, params, items, label):
if isinstance(items, basestring): if isinstance(items, six.string_types):
items = [items] items = [items]
for i in range(1, len(items) + 1): for i in range(1, len(items) + 1):
params['%s.%d' % (label, i)] = items[i - 1] params['%s.%d' % (label, i)] = items[i - 1]
@@ -1149,6 +1156,8 @@ class AWSQueryConnection(AWSAuthConnection):
elif response.status == 200: elif response.status == 200:
rs = ResultSet(markers) rs = ResultSet(markers)
h = boto.handler.XmlHandler(rs, parent) h = boto.handler.XmlHandler(rs, parent)
if isinstance(body, six.text_type):
body = body.encode('utf-8')
xml.sax.parseString(body, h) xml.sax.parseString(body, h)
return rs return rs
else: else:
@@ -1169,6 +1178,8 @@ class AWSQueryConnection(AWSAuthConnection):
elif response.status == 200: elif response.status == 200:
obj = cls(parent) obj = cls(parent)
h = boto.handler.XmlHandler(obj, parent) h = boto.handler.XmlHandler(obj, parent)
if isinstance(body, six.text_type):
body = body.encode('utf-8')
xml.sax.parseString(body, h) xml.sax.parseString(body, h)
return obj return obj
else: else:

View File

@@ -627,7 +627,7 @@ class DataPipelineConnection(AWSQueryConnection):
headers=headers, data=body) headers=headers, data=body)
response = self._mexe(http_request, sender=None, response = self._mexe(http_request, sender=None,
override_num_retries=10) override_num_retries=10)
response_body = response.read() response_body = response.read().decode('utf-8')
boto.log.debug(response_body) boto.log.debug(response_body)
if response.status == 200: if response.status == 200:
if response_body: if response_body:

View File

@@ -20,16 +20,12 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
try:
import json
except ImportError:
import simplejson as json
import boto import boto
from boto.connection import AWSQueryConnection from boto.connection import AWSQueryConnection
from boto.regioninfo import RegionInfo from boto.regioninfo import RegionInfo
from boto.exception import JSONResponseError from boto.exception import JSONResponseError
from boto.directconnect import exceptions from boto.directconnect import exceptions
from boto.compat import json
class DirectConnectConnection(AWSQueryConnection): class DirectConnectConnection(AWSQueryConnection):
@@ -619,7 +615,7 @@ class DirectConnectConnection(AWSQueryConnection):
headers=headers, data=body) headers=headers, data=body)
response = self._mexe(http_request, sender=None, response = self._mexe(http_request, sender=None,
override_num_retries=10) override_num_retries=10)
response_body = response.read() response_body = response.read().decode('utf-8')
boto.log.debug(response_body) boto.log.debug(response_body)
if response.status == 200: if response.status == 200:
if response_body: if response_body:

View File

@@ -20,6 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
from boto.compat import six
class Batch(object): class Batch(object):
@@ -176,7 +177,7 @@ class BatchList(list):
if not self.unprocessed: if not self.unprocessed:
return None return None
for table_name, table_req in self.unprocessed.iteritems(): for table_name, table_req in six.iteritems(self.unprocessed):
table_keys = table_req['Keys'] table_keys = table_req['Keys']
table = self.layer2.get_table(table_name) table = self.layer2.get_table(table_name)
@@ -196,7 +197,6 @@ class BatchList(list):
return self.submit() return self.submit()
def submit(self): def submit(self):
res = self.layer2.batch_get_item(self) res = self.layer2.batch_get_item(self)
if 'UnprocessedKeys' in res: if 'UnprocessedKeys' in res:
@@ -259,4 +259,3 @@ class BatchWriteList(list):
table_name, batch_dict = batch.to_dict() table_name, batch_dict = batch.to_dict()
d[table_name] = batch_dict d[table_name] = batch_dict
return d return d

View File

@@ -122,14 +122,14 @@ class Layer1(AWSAuthConnection):
boto.log.debug('RequestId: %s' % request_id) boto.log.debug('RequestId: %s' % request_id)
boto.perflog.debug('%s: id=%s time=%sms', boto.perflog.debug('%s: id=%s time=%sms',
headers['X-Amz-Target'], request_id, int(elapsed)) headers['X-Amz-Target'], request_id, int(elapsed))
response_body = response.read() response_body = response.read().decode('utf-8')
boto.log.debug(response_body) boto.log.debug(response_body)
return json.loads(response_body, object_hook=object_hook) return json.loads(response_body, object_hook=object_hook)
def _retry_handler(self, response, i, next_sleep): def _retry_handler(self, response, i, next_sleep):
status = None status = None
if response.status == 400: if response.status == 400:
response_body = response.read() response_body = response.read().decode('utf-8')
boto.log.debug(response_body) boto.log.debug(response_body)
data = json.loads(response_body) data = json.loads(response_body)
if self.ThruputError in data.get('__type'): if self.ThruputError in data.get('__type'):
@@ -160,7 +160,7 @@ class Layer1(AWSAuthConnection):
expected_crc32 = response.getheader('x-amz-crc32') expected_crc32 = response.getheader('x-amz-crc32')
if self._validate_checksums and expected_crc32 is not None: if self._validate_checksums and expected_crc32 is not None:
boto.log.debug('Validating crc32 checksum for body: %s', boto.log.debug('Validating crc32 checksum for body: %s',
response.read()) response.read().decode('utf-8'))
actual_crc32 = crc32(response.read()) & 0xffffffff actual_crc32 = crc32(response.read()) & 0xffffffff
expected_crc32 = int(expected_crc32) expected_crc32 = int(expected_crc32)
if actual_crc32 != expected_crc32: if actual_crc32 != expected_crc32:
@@ -173,7 +173,8 @@ class Layer1(AWSAuthConnection):
if i == 0: if i == 0:
next_sleep = 0 next_sleep = 0
else: else:
next_sleep = 0.05 * (2 ** i) next_sleep = min(0.05 * (2 ** i),
boto.config.get('Boto', 'max_retry_delay', 60))
return next_sleep return next_sleep
def list_tables(self, limit=None, start_table=None): def list_tables(self, limit=None, start_table=None):

View File

@@ -264,13 +264,13 @@ class Layer2(object):
""" """
dynamodb_key = {} dynamodb_key = {}
dynamodb_value = self.dynamizer.encode(hash_key) dynamodb_value = self.dynamizer.encode(hash_key)
if dynamodb_value.keys()[0] != schema.hash_key_type: if list(dynamodb_value.keys())[0] != schema.hash_key_type:
msg = 'Hashkey must be of type: %s' % schema.hash_key_type msg = 'Hashkey must be of type: %s' % schema.hash_key_type
raise TypeError(msg) raise TypeError(msg)
dynamodb_key['HashKeyElement'] = dynamodb_value dynamodb_key['HashKeyElement'] = dynamodb_value
if range_key is not None: if range_key is not None:
dynamodb_value = self.dynamizer.encode(range_key) dynamodb_value = self.dynamizer.encode(range_key)
if dynamodb_value.keys()[0] != schema.range_key_type: if list(dynamodb_value.keys())[0] != schema.range_key_type:
msg = 'RangeKey must be of type: %s' % schema.range_key_type msg = 'RangeKey must be of type: %s' % schema.range_key_type
raise TypeError(msg) raise TypeError(msg)
dynamodb_key['RangeKeyElement'] = dynamodb_value dynamodb_key['RangeKeyElement'] = dynamodb_value

View File

@@ -47,9 +47,9 @@ class TableBatchGenerator(object):
self.consistent_read = consistent_read self.consistent_read = consistent_read
def _queue_unprocessed(self, res): def _queue_unprocessed(self, res):
if not u'UnprocessedKeys' in res: if u'UnprocessedKeys' not in res:
return return
if not self.table.name in res[u'UnprocessedKeys']: if self.table.name not in res[u'UnprocessedKeys']:
return return
keys = res[u'UnprocessedKeys'][self.table.name][u'Keys'] keys = res[u'UnprocessedKeys'][self.table.name][u'Keys']
@@ -68,7 +68,7 @@ class TableBatchGenerator(object):
res = batch.submit() res = batch.submit()
# parse the results # parse the results
if not self.table.name in res[u'Responses']: if self.table.name not in res[u'Responses']:
continue continue
self.consumed_units += res[u'Responses'][self.table.name][u'ConsumedCapacityUnits'] self.consumed_units += res[u'Responses'][self.table.name][u'ConsumedCapacityUnits']
for elem in res[u'Responses'][self.table.name][u'Items']: for elem in res[u'Responses'][self.table.name][u'Items']:

View File

@@ -27,7 +27,8 @@ Python types and vice-versa.
import base64 import base64
from decimal import (Decimal, DecimalException, Context, from decimal import (Decimal, DecimalException, Context,
Clamped, Overflow, Inexact, Underflow, Rounded) Clamped, Overflow, Inexact, Underflow, Rounded)
from exceptions import DynamoDBNumberError from boto.dynamodb.exceptions import DynamoDBNumberError
from boto.compat import filter, map, six, long_type
DYNAMODB_CONTEXT = Context( DYNAMODB_CONTEXT = Context(
@@ -51,17 +52,25 @@ def float_to_decimal(f):
def is_num(n): def is_num(n):
types = (int, long, float, bool, Decimal) types = (int, long_type, float, bool, Decimal)
return isinstance(n, types) or n in types return isinstance(n, types) or n in types
def is_str(n): if six.PY2:
return isinstance(n, basestring) or (isinstance(n, type) and def is_str(n):
issubclass(n, basestring)) return (isinstance(n, basestring) or
isinstance(n, type) and issubclass(n, basestring))
def is_binary(n):
return isinstance(n, Binary)
def is_binary(n): else: # PY3
return isinstance(n, Binary) def is_str(n):
return (isinstance(n, str) or
isinstance(n, type) and issubclass(n, str))
def is_binary(n):
return isinstance(n, bytes) # Binary is subclass of bytes.
def serialize_num(val): def serialize_num(val):
@@ -103,7 +112,7 @@ def get_dynamodb_type(val):
dynamodb_type = 'SS' dynamodb_type = 'SS'
elif False not in map(is_binary, val): elif False not in map(is_binary, val):
dynamodb_type = 'BS' dynamodb_type = 'BS'
elif isinstance(val, Binary): elif is_binary(val):
dynamodb_type = 'B' dynamodb_type = 'B'
if dynamodb_type is None: if dynamodb_type is None:
msg = 'Unsupported type "%s" for value "%s"' % (type(val), val) msg = 'Unsupported type "%s" for value "%s"' % (type(val), val)
@@ -124,43 +133,62 @@ def dynamize_value(val):
elif dynamodb_type == 'S': elif dynamodb_type == 'S':
val = {dynamodb_type: val} val = {dynamodb_type: val}
elif dynamodb_type == 'NS': elif dynamodb_type == 'NS':
val = {dynamodb_type: map(serialize_num, val)} val = {dynamodb_type: list(map(serialize_num, val))}
elif dynamodb_type == 'SS': elif dynamodb_type == 'SS':
val = {dynamodb_type: [n for n in val]} val = {dynamodb_type: [n for n in val]}
elif dynamodb_type == 'B': elif dynamodb_type == 'B':
if isinstance(val, bytes):
val = Binary(val)
val = {dynamodb_type: val.encode()} val = {dynamodb_type: val.encode()}
elif dynamodb_type == 'BS': elif dynamodb_type == 'BS':
val = {dynamodb_type: [n.encode() for n in val]} val = {dynamodb_type: [n.encode() for n in val]}
return val return val
class Binary(object): if six.PY2:
def __init__(self, value): class Binary(object):
if not isinstance(value, basestring): def __init__(self, value):
raise TypeError('Value must be a string of binary data!') if not isinstance(value, (bytes, six.text_type)):
raise TypeError('Value must be a string of binary data!')
if not isinstance(value, bytes):
value = value.encode("utf-8")
self.value = value self.value = value
def encode(self): def encode(self):
return base64.b64encode(self.value) return base64.b64encode(self.value).decode('utf-8')
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, Binary): if isinstance(other, Binary):
return self.value == other.value return self.value == other.value
else: else:
return self.value == other return self.value == other
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
def __repr__(self): def __repr__(self):
return 'Binary(%s)' % self.value return 'Binary(%r)' % self.value
def __str__(self): def __str__(self):
return self.value return self.value
def __hash__(self): def __hash__(self):
return hash(self.value) return hash(self.value)
else:
class Binary(bytes):
def encode(self):
return base64.b64encode(self).decode('utf-8')
@property
def value(self):
# This matches the public API of the Python 2 version,
# but just returns itself since it is already a bytes
# instance.
return bytes(self)
def __repr__(self):
return 'Binary(%r)' % self.value
def item_object_hook(dct): def item_object_hook(dct):
@@ -244,28 +272,30 @@ class Dynamizer(object):
n = str(float_to_decimal(attr)) n = str(float_to_decimal(attr))
else: else:
n = str(DYNAMODB_CONTEXT.create_decimal(attr)) n = str(DYNAMODB_CONTEXT.create_decimal(attr))
if filter(lambda x: x in n, ('Infinity', 'NaN')): if list(filter(lambda x: x in n, ('Infinity', 'NaN'))):
raise TypeError('Infinity and NaN not supported') raise TypeError('Infinity and NaN not supported')
return n return n
except (TypeError, DecimalException), e: except (TypeError, DecimalException) as e:
msg = '{0} numeric for `{1}`\n{2}'.format( msg = '{0} numeric for `{1}`\n{2}'.format(
e.__class__.__name__, attr, str(e) or '') e.__class__.__name__, attr, str(e) or '')
raise DynamoDBNumberError(msg) raise DynamoDBNumberError(msg)
def _encode_s(self, attr): def _encode_s(self, attr):
if isinstance(attr, unicode): if isinstance(attr, bytes):
attr = attr.encode('utf-8') attr = attr.decode('utf-8')
elif not isinstance(attr, str): elif not isinstance(attr, six.text_type):
attr = str(attr) attr = str(attr)
return attr return attr
def _encode_ns(self, attr): def _encode_ns(self, attr):
return map(self._encode_n, attr) return list(map(self._encode_n, attr))
def _encode_ss(self, attr): def _encode_ss(self, attr):
return [self._encode_s(n) for n in attr] return [self._encode_s(n) for n in attr]
def _encode_b(self, attr): def _encode_b(self, attr):
if isinstance(attr, bytes):
attr = Binary(attr)
return attr.encode() return attr.encode()
def _encode_bs(self, attr): def _encode_bs(self, attr):
@@ -279,7 +309,7 @@ class Dynamizer(object):
""" """
if len(attr) > 1 or not attr: if len(attr) > 1 or not attr:
return attr return attr
dynamodb_type = attr.keys()[0] dynamodb_type = list(attr.keys())[0]
if dynamodb_type.lower() == dynamodb_type: if dynamodb_type.lower() == dynamodb_type:
# It's not an actual type, just a single character attr that # It's not an actual type, just a single character attr that
# overlaps with the DDB types. Return it. # overlaps with the DDB types. Return it.

View File

@@ -108,9 +108,11 @@ class Item(object):
def __contains__(self, key): def __contains__(self, key):
return key in self._data return key in self._data
def __nonzero__(self): def __bool__(self):
return bool(self._data) return bool(self._data)
__nonzero__ = __bool__
def _determine_alterations(self): def _determine_alterations(self):
""" """
Checks the ``-orig_data`` against the ``_data`` to determine what Checks the ``-orig_data`` against the ``_data`` to determine what
@@ -256,7 +258,7 @@ class Item(object):
expects = {} expects = {}
if fields is None: if fields is None:
fields = self._data.keys() + self._orig_data.keys() fields = list(self._data.keys()) + list(self._orig_data.keys())
# Only uniques. # Only uniques.
fields = set(fields) fields = set(fields)

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,7 @@ class ResultSet(object):
def __iter__(self): def __iter__(self):
return self return self
def next(self): def __next__(self):
self._offset += 1 self._offset += 1
if self._offset >= len(self._results): if self._offset >= len(self._results):
@@ -78,6 +78,8 @@ class ResultSet(object):
else: else:
raise StopIteration() raise StopIteration()
next = __next__
def to_call(self, the_callable, *args, **kwargs): def to_call(self, the_callable, *args, **kwargs):
""" """
Sets up the callable & any arguments to run it with. Sets up the callable & any arguments to run it with.
@@ -106,7 +108,7 @@ class ResultSet(object):
# DDB api calls use (which limit page size, not the overall result set). # DDB api calls use (which limit page size, not the overall result set).
self._limit = kwargs.pop('limit', None) self._limit = kwargs.pop('limit', None)
if self._limit < 0: if self._limit is not None and self._limit < 0:
self._limit = None self._limit = None
self.the_callable = the_callable self.the_callable = the_callable
@@ -130,7 +132,7 @@ class ResultSet(object):
# If the page size is greater than limit set them # If the page size is greater than limit set them
# to the same value # to the same value
if self._limit and self._max_page_size > self._limit: if self._limit and self._max_page_size and self._max_page_size > self._limit:
self._max_page_size = self._limit self._max_page_size = self._limit
# Put in the max page size. # Put in the max page size.

View File

@@ -7,7 +7,8 @@ from boto.dynamodb2.fields import (HashKey, RangeKey,
from boto.dynamodb2.items import Item from boto.dynamodb2.items import Item
from boto.dynamodb2.layer1 import DynamoDBConnection from boto.dynamodb2.layer1 import DynamoDBConnection
from boto.dynamodb2.results import ResultSet, BatchGetResultSet from boto.dynamodb2.results import ResultSet, BatchGetResultSet
from boto.dynamodb2.types import Dynamizer, FILTER_OPERATORS, QUERY_OPERATORS from boto.dynamodb2.types import (Dynamizer, FILTER_OPERATORS, QUERY_OPERATORS,
STRING)
from boto.exception import JSONResponseError from boto.exception import JSONResponseError
@@ -232,18 +233,29 @@ class Table(object):
) )
return table return table
def _introspect_schema(self, raw_schema): def _introspect_schema(self, raw_schema, raw_attributes=None):
""" """
Given a raw schema structure back from a DynamoDB response, parse Given a raw schema structure back from a DynamoDB response, parse
out & build the high-level Python objects that represent them. out & build the high-level Python objects that represent them.
""" """
schema = [] schema = []
sane_attributes = {}
if raw_attributes:
for field in raw_attributes:
sane_attributes[field['AttributeName']] = field['AttributeType']
for field in raw_schema: for field in raw_schema:
data_type = sane_attributes.get(field['AttributeName'], STRING)
if field['KeyType'] == 'HASH': if field['KeyType'] == 'HASH':
schema.append(HashKey(field['AttributeName'])) schema.append(
HashKey(field['AttributeName'], data_type=data_type)
)
elif field['KeyType'] == 'RANGE': elif field['KeyType'] == 'RANGE':
schema.append(RangeKey(field['AttributeName'])) schema.append(
RangeKey(field['AttributeName'], data_type=data_type)
)
else: else:
raise exceptions.UnknownSchemaFieldError( raise exceptions.UnknownSchemaFieldError(
"%s was seen, but is unknown. Please report this at " "%s was seen, but is unknown. Please report this at "
@@ -280,7 +292,7 @@ class Table(object):
) )
name = field['IndexName'] name = field['IndexName']
kwargs['parts'] = self._introspect_schema(field['KeySchema']) kwargs['parts'] = self._introspect_schema(field['KeySchema'], None)
indexes.append(index_klass(name, **kwargs)) indexes.append(index_klass(name, **kwargs))
return indexes return indexes
@@ -319,7 +331,8 @@ class Table(object):
if not self.schema: if not self.schema:
# Since we have the data, build the schema. # Since we have the data, build the schema.
raw_schema = result['Table'].get('KeySchema', []) raw_schema = result['Table'].get('KeySchema', [])
self.schema = self._introspect_schema(raw_schema) raw_attributes = result['Table'].get('AttributeDefinitions', [])
self.schema = self._introspect_schema(raw_schema, raw_attributes)
if not self.indexes: if not self.indexes:
# Build the index information as well. # Build the index information as well.
@@ -635,16 +648,35 @@ class Table(object):
self.connection.update_item(self.table_name, raw_key, item_data, **kwargs) self.connection.update_item(self.table_name, raw_key, item_data, **kwargs)
return True return True
def delete_item(self, **kwargs): def delete_item(self, expected=None, conditional_operator=None, **kwargs):
""" """
Deletes an item in DynamoDB. Deletes a single item. You can perform a conditional delete operation
that deletes the item if it exists, or if it has an expected attribute
value.
Conditional deletes are useful for only deleting items if specific
conditions are met. If those conditions are met, DynamoDB performs
the delete. Otherwise, the item is not deleted.
To specify the expected attribute values of the item, you can pass a
dictionary of conditions to ``expected``. Each condition should follow
the pattern ``<attributename>__<comparison_operator>=<value_to_expect>``.
**IMPORTANT** - Be careful when using this method, there is no undo. **IMPORTANT** - Be careful when using this method, there is no undo.
To specify the key of the item you'd like to get, you can specify the To specify the key of the item you'd like to get, you can specify the
key attributes as kwargs. key attributes as kwargs.
Returns ``True`` on success. Optionally accepts an ``expected`` parameter which is a dictionary of
expected attribute value conditions.
Optionally accepts a ``conditional_operator`` which applies to the
expected attribute value conditions:
+ `AND` - If all of the conditions evaluate to true (default)
+ `OR` - True if at least one condition evaluates to true
Returns ``True`` on success, ``False`` on failed conditional delete.
Example:: Example::
@@ -663,9 +695,21 @@ class Table(object):
... }) ... })
True True
# Conditional delete
>>> users.delete_item(username='johndoe',
... expected={'balance__eq': 0})
True
""" """
expected = self._build_filters(expected, using=FILTER_OPERATORS)
raw_key = self._encode_keys(kwargs) raw_key = self._encode_keys(kwargs)
self.connection.delete_item(self.table_name, raw_key)
try:
self.connection.delete_item(self.table_name, raw_key,
expected=expected,
conditional_operator=conditional_operator)
except exceptions.ConditionalCheckFailedException:
return False
return True return True
def get_key_fields(self): def get_key_fields(self):
@@ -744,6 +788,9 @@ class Table(object):
An internal method for taking query/scan-style ``**kwargs`` & turning An internal method for taking query/scan-style ``**kwargs`` & turning
them into the raw structure DynamoDB expects for filtering. them into the raw structure DynamoDB expects for filtering.
""" """
if filter_kwargs is None:
return
filters = {} filters = {}
for field_and_op, value in filter_kwargs.items(): for field_and_op, value in filter_kwargs.items():
@@ -803,17 +850,34 @@ class Table(object):
def query(self, limit=None, index=None, reverse=False, consistent=False, def query(self, limit=None, index=None, reverse=False, consistent=False,
attributes=None, max_page_size=None, **filter_kwargs): attributes=None, max_page_size=None, **filter_kwargs):
""" """
**WARNING:** This method is provided **strictly** for
backward-compatibility. It returns results in an incorrect order.
If you are writing new code, please use ``Table.query_2``.
"""
reverse = not reverse
return self.query_2(limit=limit, index=index, reverse=reverse,
consistent=consistent, attributes=attributes,
max_page_size=max_page_size, **filter_kwargs)
def query_2(self, limit=None, index=None, reverse=False,
consistent=False, attributes=None, max_page_size=None,
query_filter=None, conditional_operator=None,
**filter_kwargs):
"""
Queries for a set of matching items in a DynamoDB table. Queries for a set of matching items in a DynamoDB table.
Queries can be performed against a hash key, a hash+range key or Queries can be performed against a hash key, a hash+range key or
against any data stored in your local secondary indexes. against any data stored in your local secondary indexes. Query filters
can be used to filter on arbitrary fields.
**Note** - You can not query against arbitrary fields within the data **Note** - You can not query against arbitrary fields within the data
stored in DynamoDB. stored in DynamoDB unless you specify ``query_filter`` values.
To specify the filters of the items you'd like to get, you can specify To specify the filters of the items you'd like to get, you can specify
the filters as kwargs. Each filter kwarg should follow the pattern the filters as kwargs. Each filter kwarg should follow the pattern
``<fieldname>__<filter_operation>=<value_to_look_for>``. ``<fieldname>__<filter_operation>=<value_to_look_for>``. Query filters
are specified in the same way.
Optionally accepts a ``limit`` parameter, which should be an integer Optionally accepts a ``limit`` parameter, which should be an integer
count of the total number of items to return. (Default: ``None`` - count of the total number of items to return. (Default: ``None`` -
@@ -824,7 +888,7 @@ class Table(object):
(Default: ``None``) (Default: ``None``)
Optionally accepts a ``reverse`` parameter, which will present the Optionally accepts a ``reverse`` parameter, which will present the
results in reverse order. (Default: ``None`` - normal order) results in reverse order. (Default: ``False`` - normal order)
Optionally accepts a ``consistent`` parameter, which should be a Optionally accepts a ``consistent`` parameter, which should be a
boolean. If you provide ``True``, it will force a consistent read of boolean. If you provide ``True``, it will force a consistent read of
@@ -842,6 +906,15 @@ class Table(object):
the scan from drowning out other queries. (Default: ``None`` - the scan from drowning out other queries. (Default: ``None`` -
fetch as many as DynamoDB will return) fetch as many as DynamoDB will return)
Optionally accepts a ``query_filter`` which is a dictionary of filter
conditions against any arbitrary field in the returned data.
Optionally accepts a ``conditional_operator`` which applies to the
query filter conditions:
+ `AND` - True if all filter conditions evaluate to true (default)
+ `OR` - True if at least one filter condition evaluates to true
Returns a ``ResultSet``, which transparently handles the pagination of Returns a ``ResultSet``, which transparently handles the pagination of
results you get back. results you get back.
@@ -880,6 +953,18 @@ class Table(object):
'John' 'John'
'Fred' 'Fred'
# Filter by non-indexed field(s)
>>> results = users.query(
... last_name__eq='Doe',
... reverse=True,
... query_filter={
... 'first_name__beginswith': 'A'
... }
... )
>>> for res in results:
... print res['first_name'] + ' ' + res['last_name']
'Alice Doe'
""" """
if self.schema: if self.schema:
if len(self.schema) == 1: if len(self.schema) == 1:
@@ -908,20 +993,26 @@ class Table(object):
'consistent': consistent, 'consistent': consistent,
'select': select, 'select': select,
'attributes_to_get': attributes, 'attributes_to_get': attributes,
'query_filter': query_filter,
'conditional_operator': conditional_operator,
}) })
results.to_call(self._query, **kwargs) results.to_call(self._query, **kwargs)
return results return results
def query_count(self, index=None, consistent=False, **filter_kwargs): def query_count(self, index=None, consistent=False, conditional_operator=None,
query_filter=None, scan_index_forward=True, limit=None,
**filter_kwargs):
""" """
Queries the exact count of matching items in a DynamoDB table. Queries the exact count of matching items in a DynamoDB table.
Queries can be performed against a hash key, a hash+range key or Queries can be performed against a hash key, a hash+range key or
against any data stored in your local secondary indexes. against any data stored in your local secondary indexes. Query filters
can be used to filter on arbitrary fields.
To specify the filters of the items you'd like to get, you can specify To specify the filters of the items you'd like to get, you can specify
the filters as kwargs. Each filter kwarg should follow the pattern the filters as kwargs. Each filter kwarg should follow the pattern
``<fieldname>__<filter_operation>=<value_to_look_for>``. ``<fieldname>__<filter_operation>=<value_to_look_for>``. Query filters
are specified in the same way.
Optionally accepts an ``index`` parameter, which should be a string of Optionally accepts an ``index`` parameter, which should be a string of
name of the local secondary index you want to query against. name of the local secondary index you want to query against.
@@ -932,9 +1023,34 @@ class Table(object):
the data (more expensive). (Default: ``False`` - use eventually the data (more expensive). (Default: ``False`` - use eventually
consistent reads) consistent reads)
Optionally accepts a ``query_filter`` which is a dictionary of filter
conditions against any arbitrary field in the returned data.
Optionally accepts a ``conditional_operator`` which applies to the
query filter conditions:
+ `AND` - True if all filter conditions evaluate to true (default)
+ `OR` - True if at least one filter condition evaluates to true
Returns an integer which represents the exact amount of matched Returns an integer which represents the exact amount of matched
items. items.
:type scan_index_forward: boolean
:param scan_index_forward: Specifies ascending (true) or descending
(false) traversal of the index. DynamoDB returns results reflecting
the requested order determined by the range key. If the data type
is Number, the results are returned in numeric order. For String,
the results are returned in order of ASCII character code values.
For Binary, DynamoDB treats each byte of the binary data as
unsigned when it compares binary values.
If ScanIndexForward is not specified, the results are returned in
ascending order.
:type limit: integer
:param limit: The maximum number of items to evaluate (not necessarily
the number of matching items).
Example:: Example::
# Look for last names equal to "Doe". # Look for last names equal to "Doe".
@@ -956,18 +1072,27 @@ class Table(object):
using=QUERY_OPERATORS using=QUERY_OPERATORS
) )
built_query_filter = self._build_filters(
query_filter,
using=FILTER_OPERATORS
)
raw_results = self.connection.query( raw_results = self.connection.query(
self.table_name, self.table_name,
index_name=index, index_name=index,
consistent_read=consistent, consistent_read=consistent,
select='COUNT', select='COUNT',
key_conditions=key_conditions, key_conditions=key_conditions,
query_filter=built_query_filter,
conditional_operator=conditional_operator,
limit=limit,
scan_index_forward=scan_index_forward,
) )
return int(raw_results.get('Count', 0)) return int(raw_results.get('Count', 0))
def _query(self, limit=None, index=None, reverse=False, consistent=False, def _query(self, limit=None, index=None, reverse=False, consistent=False,
exclusive_start_key=None, select=None, attributes_to_get=None, exclusive_start_key=None, select=None, attributes_to_get=None,
**filter_kwargs): query_filter=None, conditional_operator=None, **filter_kwargs):
""" """
The internal method that performs the actual queries. Used extensively The internal method that performs the actual queries. Used extensively
by ``ResultSet`` to perform each (paginated) request. by ``ResultSet`` to perform each (paginated) request.
@@ -975,12 +1100,15 @@ class Table(object):
kwargs = { kwargs = {
'limit': limit, 'limit': limit,
'index_name': index, 'index_name': index,
'scan_index_forward': reverse,
'consistent_read': consistent, 'consistent_read': consistent,
'select': select, 'select': select,
'attributes_to_get': attributes_to_get 'attributes_to_get': attributes_to_get,
'conditional_operator': conditional_operator,
} }
if reverse:
kwargs['scan_index_forward'] = False
if exclusive_start_key: if exclusive_start_key:
kwargs['exclusive_start_key'] = {} kwargs['exclusive_start_key'] = {}
@@ -994,6 +1122,11 @@ class Table(object):
using=QUERY_OPERATORS using=QUERY_OPERATORS
) )
kwargs['query_filter'] = self._build_filters(
query_filter,
using=FILTER_OPERATORS
)
raw_results = self.connection.query( raw_results = self.connection.query(
self.table_name, self.table_name,
**kwargs **kwargs
@@ -1020,13 +1153,14 @@ class Table(object):
} }
def scan(self, limit=None, segment=None, total_segments=None, def scan(self, limit=None, segment=None, total_segments=None,
max_page_size=None, attributes=None, **filter_kwargs): max_page_size=None, attributes=None, conditional_operator=None,
**filter_kwargs):
""" """
Scans across all items within a DynamoDB table. Scans across all items within a DynamoDB table.
Scans can be performed against a hash key or a hash+range key. You can Scans can be performed against a hash key or a hash+range key. You can
additionally filter the results after the table has been read but additionally filter the results after the table has been read but
before the response is returned. before the response is returned by using query filters.
To specify the filters of the items you'd like to get, you can specify To specify the filters of the items you'd like to get, you can specify
the filters as kwargs. Each filter kwarg should follow the pattern the filters as kwargs. Each filter kwarg should follow the pattern
@@ -1091,12 +1225,14 @@ class Table(object):
'segment': segment, 'segment': segment,
'total_segments': total_segments, 'total_segments': total_segments,
'attributes': attributes, 'attributes': attributes,
'conditional_operator': conditional_operator,
}) })
results.to_call(self._scan, **kwargs) results.to_call(self._scan, **kwargs)
return results return results
def _scan(self, limit=None, exclusive_start_key=None, segment=None, def _scan(self, limit=None, exclusive_start_key=None, segment=None,
total_segments=None, attributes=None, **filter_kwargs): total_segments=None, attributes=None, conditional_operator=None,
**filter_kwargs):
""" """
The internal method that performs the actual scan. Used extensively The internal method that performs the actual scan. Used extensively
by ``ResultSet`` to perform each (paginated) request. by ``ResultSet`` to perform each (paginated) request.
@@ -1106,6 +1242,7 @@ class Table(object):
'segment': segment, 'segment': segment,
'total_segments': total_segments, 'total_segments': total_segments,
'attributes_to_get': attributes, 'attributes_to_get': attributes,
'conditional_operator': conditional_operator,
} }
if exclusive_start_key: if exclusive_start_key:
@@ -1146,7 +1283,7 @@ class Table(object):
'last_key': last_key, 'last_key': last_key,
} }
def batch_get(self, keys, consistent=False): def batch_get(self, keys, consistent=False, attributes=None):
""" """
Fetches many specific items in batch from a table. Fetches many specific items in batch from a table.
@@ -1157,6 +1294,10 @@ class Table(object):
boolean. If you provide ``True``, a strongly consistent read will be boolean. If you provide ``True``, a strongly consistent read will be
used. (Default: False) used. (Default: False)
Optionally accepts an ``attributes`` parameter, which should be a
tuple. If you provide any attributes only these will be fetched
from DynamoDB.
Returns a ``ResultSet``, which transparently handles the pagination of Returns a ``ResultSet``, which transparently handles the pagination of
results you get back. results you get back.
@@ -1183,10 +1324,10 @@ class Table(object):
# We pass the keys to the constructor instead, so it can maintain it's # We pass the keys to the constructor instead, so it can maintain it's
# own internal state as to what keys have been processed. # own internal state as to what keys have been processed.
results = BatchGetResultSet(keys=keys, max_batch_get=self.max_batch_get) results = BatchGetResultSet(keys=keys, max_batch_get=self.max_batch_get)
results.to_call(self._batch_get, consistent=False) results.to_call(self._batch_get, consistent=consistent, attributes=attributes)
return results return results
def _batch_get(self, keys, consistent=False): def _batch_get(self, keys, consistent=False, attributes=None):
""" """
The internal method that performs the actual batch get. Used extensively The internal method that performs the actual batch get. Used extensively
by ``BatchGetResultSet`` to perform each (paginated) request. by ``BatchGetResultSet`` to perform each (paginated) request.
@@ -1200,6 +1341,9 @@ class Table(object):
if consistent: if consistent:
items[self.table_name]['ConsistentRead'] = True items[self.table_name]['ConsistentRead'] = True
if attributes is not None:
items[self.table_name]['AttributesToGet'] = attributes
for key_data in keys: for key_data in keys:
raw_key = {} raw_key = {}

View File

@@ -45,6 +45,7 @@ from boto.ec2.autoscale.instance import Instance
from boto.ec2.autoscale.scheduled import ScheduledUpdateGroupAction from boto.ec2.autoscale.scheduled import ScheduledUpdateGroupAction
from boto.ec2.autoscale.tag import Tag from boto.ec2.autoscale.tag import Tag
from boto.ec2.autoscale.limits import AccountLimits from boto.ec2.autoscale.limits import AccountLimits
from boto.compat import six
RegionData = load_regions().get('autoscaling', {}) RegionData = load_regions().get('autoscaling', {})
@@ -134,15 +135,15 @@ class AutoScaleConnection(AWSQueryConnection):
['us-east-1b',...] ['us-east-1b',...]
""" """
# different from EC2 list params # different from EC2 list params
for i in xrange(1, len(items) + 1): for i in range(1, len(items) + 1):
if isinstance(items[i - 1], dict): if isinstance(items[i - 1], dict):
for k, v in items[i - 1].iteritems(): for k, v in six.iteritems(items[i - 1]):
if isinstance(v, dict): if isinstance(v, dict):
for kk, vv in v.iteritems(): for kk, vv in six.iteritems(v):
params['%s.member.%d.%s.%s' % (label, i, k, kk)] = vv params['%s.member.%d.%s.%s' % (label, i, k, kk)] = vv
else: else:
params['%s.member.%d.%s' % (label, i, k)] = v params['%s.member.%d.%s' % (label, i, k)] = v
elif isinstance(items[i - 1], basestring): elif isinstance(items[i - 1], six.string_types):
params['%s.member.%d' % (label, i)] = items[i - 1] params['%s.member.%d' % (label, i)] = items[i - 1]
def _update_group(self, op, as_group): def _update_group(self, op, as_group):
@@ -221,7 +222,7 @@ class AutoScaleConnection(AWSQueryConnection):
if launch_config.key_name: if launch_config.key_name:
params['KeyName'] = launch_config.key_name params['KeyName'] = launch_config.key_name
if launch_config.user_data: if launch_config.user_data:
params['UserData'] = base64.b64encode(launch_config.user_data) params['UserData'] = base64.b64encode(launch_config.user_data).decode('utf-8')
if launch_config.kernel_id: if launch_config.kernel_id:
params['KernelId'] = launch_config.kernel_id params['KernelId'] = launch_config.kernel_id
if launch_config.ramdisk_id: if launch_config.ramdisk_id:

View File

@@ -18,7 +18,6 @@
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 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 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
import boto.ec2 import boto.ec2
from boto.sdb.db.property import StringProperty, IntegerProperty from boto.sdb.db.property import StringProperty, IntegerProperty
from boto.manage import propget from boto.manage import propget
@@ -66,19 +65,19 @@ if __name__ == "__main__":
obj.get(params) obj.get(params)
offerings = obj.ec2.get_all_reserved_instances_offerings(instance_type=params['instance_type'], offerings = obj.ec2.get_all_reserved_instances_offerings(instance_type=params['instance_type'],
availability_zone=params['zone'].name) availability_zone=params['zone'].name)
print '\nThe following Reserved Instances Offerings are available:\n' print('\nThe following Reserved Instances Offerings are available:\n')
for offering in offerings: for offering in offerings:
offering.describe() offering.describe()
prop = StringProperty(name='offering', verbose_name='Offering', prop = StringProperty(name='offering', verbose_name='Offering',
choices=offerings) choices=offerings)
offering = propget.get(prop) offering = propget.get(prop)
print '\nYou have chosen this offering:' print('\nYou have chosen this offering:')
offering.describe() offering.describe()
unit_price = float(offering.fixed_price) unit_price = float(offering.fixed_price)
total_price = unit_price * params['quantity'] total_price = unit_price * params['quantity']
print '!!! You are about to purchase %d of these offerings for a total of $%.2f !!!' % (params['quantity'], total_price) print('!!! You are about to purchase %d of these offerings for a total of $%.2f !!!' % (params['quantity'], total_price))
answer = raw_input('Are you sure you want to do this? If so, enter YES: ') answer = raw_input('Are you sure you want to do this? If so, enter YES: ')
if answer.strip().lower() == 'yes': if answer.strip().lower() == 'yes':
offering.purchase(params['quantity']) offering.purchase(params['quantity'])
else: else:
print 'Purchase cancelled' print('Purchase cancelled')

View File

@@ -23,7 +23,7 @@
This module provides an interface to the Elastic Compute Cloud (EC2) This module provides an interface to the Elastic Compute Cloud (EC2)
CloudWatch service from AWS. CloudWatch service from AWS.
""" """
from boto.compat import json from boto.compat import json, map, six, zip
from boto.connection import AWSQueryConnection from boto.connection import AWSQueryConnection
from boto.ec2.cloudwatch.metric import Metric from boto.ec2.cloudwatch.metric import Metric
from boto.ec2.cloudwatch.alarm import MetricAlarm, MetricAlarms, AlarmHistoryItem from boto.ec2.cloudwatch.alarm import MetricAlarm, MetricAlarms, AlarmHistoryItem
@@ -110,7 +110,7 @@ class CloudWatchConnection(AWSQueryConnection):
for dim_name in dimension: for dim_name in dimension:
dim_value = dimension[dim_name] dim_value = dimension[dim_name]
if dim_value: if dim_value:
if isinstance(dim_value, basestring): if isinstance(dim_value, six.string_types):
dim_value = [dim_value] dim_value = [dim_value]
for value in dim_value: for value in dim_value:
params['%s.%d.Name' % (prefix, i+1)] = dim_name params['%s.%d.Name' % (prefix, i+1)] = dim_name
@@ -121,12 +121,12 @@ class CloudWatchConnection(AWSQueryConnection):
i += 1 i += 1
def build_list_params(self, params, items, label): def build_list_params(self, params, items, label):
if isinstance(items, basestring): if isinstance(items, six.string_types):
items = [items] items = [items]
for index, item in enumerate(items): for index, item in enumerate(items):
i = index + 1 i = index + 1
if isinstance(item, dict): if isinstance(item, dict):
for k, v in item.iteritems(): for k, v in six.iteritems(item):
params[label % (i, 'Name')] = k params[label % (i, 'Name')] = k
if v is not None: if v is not None:
params[label % (i, 'Value')] = v params[label % (i, 'Value')] = v
@@ -171,7 +171,7 @@ class CloudWatchConnection(AWSQueryConnection):
else: else:
raise Exception('Must specify a value or statistics to put.') raise Exception('Must specify a value or statistics to put.')
for key, val in metric_data.iteritems(): for key, val in six.iteritems(metric_data):
params['MetricData.member.%d.%s' % (index + 1, key)] = val params['MetricData.member.%d.%s' % (index + 1, key)] = val
def get_metric_statistics(self, period, start_time, end_time, metric_name, def get_metric_statistics(self, period, start_time, end_time, metric_name,

View File

@@ -21,10 +21,10 @@
# #
from datetime import datetime from datetime import datetime
from boto.resultset import ResultSet
from boto.ec2.cloudwatch.listelement import ListElement from boto.ec2.cloudwatch.listelement import ListElement
from boto.ec2.cloudwatch.dimension import Dimension from boto.ec2.cloudwatch.dimension import Dimension
from boto.compat import json from boto.compat import json
from boto.compat import six
class MetricAlarms(list): class MetricAlarms(list):
@@ -57,7 +57,7 @@ class MetricAlarm(object):
'<': 'LessThanThreshold', '<': 'LessThanThreshold',
'<=': 'LessThanOrEqualToThreshold', '<=': 'LessThanOrEqualToThreshold',
} }
_rev_cmp_map = dict((v, k) for (k, v) in _cmp_map.iteritems()) _rev_cmp_map = dict((v, k) for (k, v) in six.iteritems(_cmp_map))
def __init__(self, connection=None, name=None, metric=None, def __init__(self, connection=None, name=None, metric=None,
namespace=None, statistic=None, comparison=None, namespace=None, statistic=None, comparison=None,
@@ -320,4 +320,3 @@ class AlarmHistoryItem(object):
'%Y-%m-%dT%H:%M:%S.%fZ') '%Y-%m-%dT%H:%M:%S.%fZ')
except ValueError: except ValueError:
self.timestamp = datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') self.timestamp = datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')

View File

@@ -37,4 +37,3 @@ class Datapoint(dict):
self[name] = datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') self[name] = datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
elif name != 'member': elif name != 'member':
self[name] = value self[name] = value

View File

@@ -35,4 +35,3 @@ class Dimension(dict):
self[self._name] = [value] self[self._name] = [value]
else: else:
setattr(self, name, value) setattr(self, name, value)

View File

@@ -27,5 +27,3 @@ class ListElement(list):
def endElement(self, name, value, connection): def endElement(self, name, value, connection):
if name == 'member': if name == 'member':
self.append(value) self.append(value)

View File

@@ -65,13 +65,14 @@ from boto.ec2.networkinterface import NetworkInterface
from boto.ec2.attributes import AccountAttribute, VPCAttribute from boto.ec2.attributes import AccountAttribute, VPCAttribute
from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType
from boto.exception import EC2ResponseError from boto.exception import EC2ResponseError
from boto.compat import six
#boto.set_stream_logger('ec2') #boto.set_stream_logger('ec2')
class EC2Connection(AWSQueryConnection): class EC2Connection(AWSQueryConnection):
APIVersion = boto.config.get('Boto', 'ec2_version', '2013-10-15') APIVersion = boto.config.get('Boto', 'ec2_version', '2014-05-01')
DefaultRegionName = boto.config.get('Boto', 'ec2_region_name', 'us-east-1') DefaultRegionName = boto.config.get('Boto', 'ec2_region_name', 'us-east-1')
DefaultRegionEndpoint = boto.config.get('Boto', 'ec2_region_endpoint', DefaultRegionEndpoint = boto.config.get('Boto', 'ec2_region_endpoint',
'ec2.us-east-1.amazonaws.com') 'ec2.us-east-1.amazonaws.com')
@@ -109,7 +110,7 @@ class EC2Connection(AWSQueryConnection):
def get_params(self): def get_params(self):
""" """
Returns a dictionary containing the value of of all of the keyword Returns a dictionary containing the value of all of the keyword
arguments passed when constructing this connection. arguments passed when constructing this connection.
""" """
param_names = ['aws_access_key_id', 'aws_secret_access_key', param_names = ['aws_access_key_id', 'aws_secret_access_key',
@@ -122,6 +123,9 @@ class EC2Connection(AWSQueryConnection):
return params return params
def build_filter_params(self, params, filters): def build_filter_params(self, params, filters):
if not isinstance(filters, dict):
filters = dict(filters)
i = 1 i = 1
for name in filters: for name in filters:
aws_name = name aws_name = name
@@ -266,7 +270,8 @@ class EC2Connection(AWSQueryConnection):
root_device_name=None, block_device_map=None, root_device_name=None, block_device_map=None,
dry_run=False, virtualization_type=None, dry_run=False, virtualization_type=None,
sriov_net_support=None, sriov_net_support=None,
snapshot_id=None): snapshot_id=None,
delete_root_volume_on_termination=False):
""" """
Register an image. Register an image.
@@ -315,6 +320,12 @@ class EC2Connection(AWSQueryConnection):
as root device for the image. Mutually exclusive with as root device for the image. Mutually exclusive with
block_device_map, requires root_device_name block_device_map, requires root_device_name
:type delete_root_volume_on_termination: bool
:param delete_root_volume_on_termination: Whether to delete the root
volume of the image after instance termination. Only applies when
creating image from snapshot_id. Defaults to False. Note that
leaving volumes behind after instance termination is not free.
:rtype: string :rtype: string
:return: The new image id :return: The new image id
""" """
@@ -334,7 +345,8 @@ class EC2Connection(AWSQueryConnection):
if root_device_name: if root_device_name:
params['RootDeviceName'] = root_device_name params['RootDeviceName'] = root_device_name
if snapshot_id: if snapshot_id:
root_vol = BlockDeviceType(snapshot_id=snapshot_id) root_vol = BlockDeviceType(snapshot_id=snapshot_id,
delete_on_termination=delete_root_volume_on_termination)
block_device_map = BlockDeviceMapping() block_device_map = BlockDeviceMapping()
block_device_map[root_device_name] = root_vol block_device_map[root_device_name] = root_vol
if block_device_map: if block_device_map:
@@ -602,15 +614,24 @@ class EC2Connection(AWSQueryConnection):
:rtype: list :rtype: list
:return: A list of :class:`boto.ec2.instance.Instance` :return: A list of :class:`boto.ec2.instance.Instance`
""" """
reservations = self.get_all_reservations(instance_ids=instance_ids, next_token = None
filters=filters, retval = []
dry_run=dry_run, while True:
max_results=max_results) reservations = self.get_all_reservations(instance_ids=instance_ids,
return [instance for reservation in reservations filters=filters,
for instance in reservation.instances] dry_run=dry_run,
max_results=max_results,
next_token=next_token)
retval.extend([instance for reservation in reservations for
instance in reservation.instances])
next_token = reservations.next_token
if not next_token:
break
return retval
def get_all_reservations(self, instance_ids=None, filters=None, def get_all_reservations(self, instance_ids=None, filters=None,
dry_run=False, max_results=None): dry_run=False, max_results=None, next_token=None):
""" """
Retrieve all the instance reservations associated with your account. Retrieve all the instance reservations associated with your account.
@@ -632,6 +653,10 @@ class EC2Connection(AWSQueryConnection):
:param max_results: The maximum number of paginated instance :param max_results: The maximum number of paginated instance
items per response. items per response.
:type next_token: str
:param next_token: A string specifying the next paginated set
of results to return.
:rtype: list :rtype: list
:return: A list of :class:`boto.ec2.instance.Reservation` :return: A list of :class:`boto.ec2.instance.Reservation`
""" """
@@ -652,12 +677,15 @@ class EC2Connection(AWSQueryConnection):
params['DryRun'] = 'true' params['DryRun'] = 'true'
if max_results is not None: if max_results is not None:
params['MaxResults'] = max_results params['MaxResults'] = max_results
if next_token:
params['NextToken'] = next_token
return self.get_list('DescribeInstances', params, return self.get_list('DescribeInstances', params,
[('item', Reservation)], verb='POST') [('item', Reservation)], verb='POST')
def get_all_instance_status(self, instance_ids=None, def get_all_instance_status(self, instance_ids=None,
max_results=None, next_token=None, max_results=None, next_token=None,
filters=None, dry_run=False): filters=None, dry_run=False,
include_all_instances=False):
""" """
Retrieve all the instances in your account scheduled for maintenance. Retrieve all the instances in your account scheduled for maintenance.
@@ -685,6 +713,11 @@ class EC2Connection(AWSQueryConnection):
:type dry_run: bool :type dry_run: bool
:param dry_run: Set to True if the operation should not actually run. :param dry_run: Set to True if the operation should not actually run.
:type include_all_instances: bool
:param include_all_instances: Set to True if all
instances should be returned. (Only running
instances are included by default.)
:rtype: list :rtype: list
:return: A list of instances that have maintenance scheduled. :return: A list of instances that have maintenance scheduled.
""" """
@@ -699,6 +732,8 @@ class EC2Connection(AWSQueryConnection):
self.build_filter_params(params, filters) self.build_filter_params(params, filters)
if dry_run: if dry_run:
params['DryRun'] = 'true' params['DryRun'] = 'true'
if include_all_instances:
params['IncludeAllInstances'] = 'true'
return self.get_object('DescribeInstanceStatus', params, return self.get_object('DescribeInstanceStatus', params,
InstanceStatusSet, verb='POST') InstanceStatusSet, verb='POST')
@@ -775,6 +810,9 @@ class EC2Connection(AWSQueryConnection):
* i2.2xlarge * i2.2xlarge
* i2.4xlarge * i2.4xlarge
* i2.8xlarge * i2.8xlarge
* t2.micro
* t2.small
* t2.medium
:type placement: string :type placement: string
:param placement: The Availability Zone to launch the instance into. :param placement: The Availability Zone to launch the instance into.
@@ -788,7 +826,7 @@ class EC2Connection(AWSQueryConnection):
instances. instances.
:type monitoring_enabled: bool :type monitoring_enabled: bool
:param monitoring_enabled: Enable CloudWatch monitoring on :param monitoring_enabled: Enable detailed CloudWatch monitoring on
the instance. the instance.
:type subnet_id: string :type subnet_id: string
@@ -856,9 +894,9 @@ class EC2Connection(AWSQueryConnection):
provide optimal EBS I/O performance. This optimization provide optimal EBS I/O performance. This optimization
isn't available with all instance types. isn't available with all instance types.
:type network_interfaces: list :type network_interfaces: :class:`boto.ec2.networkinterface.NetworkInterfaceCollection`
:param network_interfaces: A list of :param network_interfaces: A NetworkInterfaceCollection data
:class:`boto.ec2.networkinterface.NetworkInterfaceSpecification` structure containing the ENI specifications for the instance.
:type dry_run: bool :type dry_run: bool
:param dry_run: Set to True if the operation should not actually run. :param dry_run: Set to True if the operation should not actually run.
@@ -889,7 +927,9 @@ class EC2Connection(AWSQueryConnection):
l.append(group) l.append(group)
self.build_list_params(params, l, 'SecurityGroup') self.build_list_params(params, l, 'SecurityGroup')
if user_data: if user_data:
params['UserData'] = base64.b64encode(user_data) if isinstance(user_data, six.text_type):
user_data = user_data.encode('utf-8')
params['UserData'] = base64.b64encode(user_data).decode('utf-8')
if addressing_type: if addressing_type:
params['AddressingType'] = addressing_type params['AddressingType'] = addressing_type
if instance_type: if instance_type:
@@ -1470,6 +1510,9 @@ class EC2Connection(AWSQueryConnection):
* i2.2xlarge * i2.2xlarge
* i2.4xlarge * i2.4xlarge
* i2.8xlarge * i2.8xlarge
* t2.micro
* t2.small
* t2.medium
:type placement: string :type placement: string
:param placement: The availability zone in which to launch :param placement: The availability zone in which to launch
@@ -1484,7 +1527,7 @@ class EC2Connection(AWSQueryConnection):
instances instances
:type monitoring_enabled: bool :type monitoring_enabled: bool
:param monitoring_enabled: Enable CloudWatch monitoring on :param monitoring_enabled: Enable detailed CloudWatch monitoring on
the instance. the instance.
:type subnet_id: string :type subnet_id: string
@@ -2223,8 +2266,8 @@ class EC2Connection(AWSQueryConnection):
params['DryRun'] = 'true' params['DryRun'] = 'true'
return self.get_status('ModifyVolumeAttribute', params, verb='POST') return self.get_status('ModifyVolumeAttribute', params, verb='POST')
def create_volume(self, size, zone, snapshot=None, def create_volume(self, size, zone, snapshot=None, volume_type=None,
volume_type=None, iops=None, dry_run=False): iops=None, encrypted=False, dry_run=False):
""" """
Create a new EBS Volume. Create a new EBS Volume.
@@ -2240,12 +2283,16 @@ class EC2Connection(AWSQueryConnection):
:type volume_type: string :type volume_type: string
:param volume_type: The type of the volume. (optional). Valid :param volume_type: The type of the volume. (optional). Valid
values are: standard | io1. values are: standard | io1 | gp2.
:type iops: int :type iops: int
:param iops: The provisioned IOPs you want to associate with :param iops: The provisioned IOPS you want to associate with
this volume. (optional) this volume. (optional)
:type encrypted: bool
:param encrypted: Specifies whether the volume should be encrypted.
(optional)
:type dry_run: bool :type dry_run: bool
:param dry_run: Set to True if the operation should not actually run. :param dry_run: Set to True if the operation should not actually run.
@@ -2263,6 +2310,8 @@ class EC2Connection(AWSQueryConnection):
params['VolumeType'] = volume_type params['VolumeType'] = volume_type
if iops: if iops:
params['Iops'] = str(iops) params['Iops'] = str(iops)
if encrypted:
params['Encrypted'] = 'true'
if dry_run: if dry_run:
params['DryRun'] = 'true' params['DryRun'] = 'true'
return self.get_object('CreateVolume', params, Volume, verb='POST') return self.get_object('CreateVolume', params, Volume, verb='POST')
@@ -2790,7 +2839,7 @@ class EC2Connection(AWSQueryConnection):
keynames=[keyname], keynames=[keyname],
dry_run=dry_run dry_run=dry_run
)[0] )[0]
except self.ResponseError, e: except self.ResponseError as e:
if e.code == 'InvalidKeyPair.NotFound': if e.code == 'InvalidKeyPair.NotFound':
return None return None
else: else:
@@ -3819,7 +3868,7 @@ class EC2Connection(AWSQueryConnection):
def monitor_instances(self, instance_ids, dry_run=False): def monitor_instances(self, instance_ids, dry_run=False):
""" """
Enable CloudWatch monitoring for the supplied instances. Enable detailed CloudWatch monitoring for the supplied instances.
:type instance_id: list of strings :type instance_id: list of strings
:param instance_id: The instance ids :param instance_id: The instance ids
@@ -3840,7 +3889,7 @@ class EC2Connection(AWSQueryConnection):
def monitor_instance(self, instance_id, dry_run=False): def monitor_instance(self, instance_id, dry_run=False):
""" """
Deprecated Version, maintained for backward compatibility. Deprecated Version, maintained for backward compatibility.
Enable CloudWatch monitoring for the supplied instance. Enable detailed CloudWatch monitoring for the supplied instance.
:type instance_id: string :type instance_id: string
:param instance_id: The instance id :param instance_id: The instance id
@@ -3876,7 +3925,7 @@ class EC2Connection(AWSQueryConnection):
def unmonitor_instance(self, instance_id, dry_run=False): def unmonitor_instance(self, instance_id, dry_run=False):
""" """
Deprecated Version, maintained for backward compatibility. Deprecated Version, maintained for backward compatibility.
Disable CloudWatch monitoring for the supplied instance. Disable detailed CloudWatch monitoring for the supplied instance.
:type instance_id: string :type instance_id: string
:param instance_id: The instance id :param instance_id: The instance id
@@ -4184,11 +4233,14 @@ class EC2Connection(AWSQueryConnection):
# Network Interface methods # Network Interface methods
def get_all_network_interfaces(self, filters=None, dry_run=False): def get_all_network_interfaces(self, network_interface_ids=None, filters=None, dry_run=False):
""" """
Retrieve all of the Elastic Network Interfaces (ENI's) Retrieve all of the Elastic Network Interfaces (ENI's)
associated with your account. associated with your account.
:type network_interface_ids: list
:param network_interface_ids: a list of strings representing ENI IDs
:type filters: dict :type filters: dict
:param filters: Optional filters that can be used to limit :param filters: Optional filters that can be used to limit
the results returned. Filters are provided the results returned. Filters are provided
@@ -4206,6 +4258,8 @@ class EC2Connection(AWSQueryConnection):
:return: A list of :class:`boto.ec2.networkinterface.NetworkInterface` :return: A list of :class:`boto.ec2.networkinterface.NetworkInterface`
""" """
params = {} params = {}
if network_interface_ids:
self.build_list_params(params, network_interface_ids, 'NetworkInterfaceId')
if filters: if filters:
self.build_filter_params(params, filters) self.build_filter_params(params, filters)
if dry_run: if dry_run:
@@ -4339,7 +4393,8 @@ class EC2Connection(AWSQueryConnection):
""" """
:type dry_run: bool :type dry_run: bool
:param dry_run: Set to True if the operation should not actually run. :param dry_run: Set to True if the operation should not actually run.
:rtype: :class:`boto.ec2.image.CopyImage`
:return: Object containing the image_id of the copied image.
""" """
params = { params = {
'SourceRegion': source_region, 'SourceRegion': source_region,

View File

@@ -85,6 +85,27 @@ class TaggedEC2Object(EC2Object):
self.tags = TagSet() self.tags = TagSet()
self.tags[key] = value self.tags[key] = value
def add_tags(self, tags, dry_run=False):
"""
Add tags to this object. Tags are stored by AWS and can be used
to organize and filter resources. Adding tags involves a round-trip
to the EC2 service.
:type tags: dict
:param tags: A dictionary of key-value pairs for the tags being stored.
If for some tags you want only the name and no value, the
corresponding value for that tag name should be an empty
string.
"""
status = self.connection.create_tags(
[self.id],
tags,
dry_run=dry_run
)
if self.tags is None:
self.tags = TagSet()
self.tags.update(tags)
def remove_tag(self, key, value=None, dry_run=False): def remove_tag(self, key, value=None, dry_run=False):
""" """
Remove a tag from this object. Removing a tag involves a round-trip Remove a tag from this object. Removing a tag involves a round-trip
@@ -102,7 +123,7 @@ class TaggedEC2Object(EC2Object):
NOTE: There is an important distinction between NOTE: There is an important distinction between
a value of '' and a value of None. a value of '' and a value of None.
""" """
if value: if value is not None:
tags = {key : value} tags = {key : value}
else: else:
tags = [key] tags = [key]

View File

@@ -30,9 +30,9 @@ from boto.ec2.instanceinfo import InstanceInfo
from boto.ec2.elb.loadbalancer import LoadBalancer, LoadBalancerZones from boto.ec2.elb.loadbalancer import LoadBalancer, LoadBalancerZones
from boto.ec2.elb.instancestate import InstanceState from boto.ec2.elb.instancestate import InstanceState
from boto.ec2.elb.healthcheck import HealthCheck from boto.ec2.elb.healthcheck import HealthCheck
from boto.ec2.elb.listelement import ListElement
from boto.regioninfo import RegionInfo, get_regions, load_regions from boto.regioninfo import RegionInfo, get_regions, load_regions
import boto import boto
from boto.compat import six
RegionData = load_regions().get('elasticloadbalancing', {}) RegionData = load_regions().get('elasticloadbalancing', {})
@@ -68,8 +68,9 @@ class ELBConnection(AWSQueryConnection):
APIVersion = boto.config.get('Boto', 'elb_version', '2012-06-01') APIVersion = boto.config.get('Boto', 'elb_version', '2012-06-01')
DefaultRegionName = boto.config.get('Boto', 'elb_region_name', 'us-east-1') DefaultRegionName = boto.config.get('Boto', 'elb_region_name', 'us-east-1')
DefaultRegionEndpoint = boto.config.get('Boto', 'elb_region_endpoint', DefaultRegionEndpoint = boto.config.get(
'elasticloadbalancing.us-east-1.amazonaws.com') 'Boto', 'elb_region_endpoint',
'elasticloadbalancing.us-east-1.amazonaws.com')
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
is_secure=True, port=None, proxy=None, proxy_port=None, is_secure=True, port=None, proxy=None, proxy_port=None,
@@ -87,31 +88,37 @@ class ELBConnection(AWSQueryConnection):
self.DefaultRegionEndpoint) self.DefaultRegionEndpoint)
self.region = region self.region = region
super(ELBConnection, self).__init__(aws_access_key_id, super(ELBConnection, self).__init__(aws_access_key_id,
aws_secret_access_key, aws_secret_access_key,
is_secure, port, proxy, proxy_port, is_secure, port, proxy, proxy_port,
proxy_user, proxy_pass, proxy_user, proxy_pass,
self.region.endpoint, debug, self.region.endpoint, debug,
https_connection_factory, path, https_connection_factory, path,
security_token, security_token,
validate_certs=validate_certs, validate_certs=validate_certs,
profile_name=profile_name) profile_name=profile_name)
def _required_auth_capability(self): def _required_auth_capability(self):
return ['ec2'] return ['ec2']
def build_list_params(self, params, items, label): def build_list_params(self, params, items, label):
if isinstance(items, basestring): if isinstance(items, six.string_types):
items = [items] items = [items]
for index, item in enumerate(items): for index, item in enumerate(items):
params[label % (index + 1)] = item params[label % (index + 1)] = item
def get_all_load_balancers(self, load_balancer_names=None): def get_all_load_balancers(self, load_balancer_names=None, marker=None):
""" """
Retrieve all load balancers associated with your account. Retrieve all load balancers associated with your account.
:type load_balancer_names: list :type load_balancer_names: list
:keyword load_balancer_names: An optional list of load balancer names. :keyword load_balancer_names: An optional list of load balancer names.
:type marker: string
:param marker: Use this only when paginating results and only
in follow-up request after you've received a response
where the results are truncated. Set this to the value of
the Marker element in the response you just received.
:rtype: :py:class:`boto.resultset.ResultSet` :rtype: :py:class:`boto.resultset.ResultSet`
:return: A ResultSet containing instances of :return: A ResultSet containing instances of
:class:`boto.ec2.elb.loadbalancer.LoadBalancer` :class:`boto.ec2.elb.loadbalancer.LoadBalancer`
@@ -120,11 +127,16 @@ class ELBConnection(AWSQueryConnection):
if load_balancer_names: if load_balancer_names:
self.build_list_params(params, load_balancer_names, self.build_list_params(params, load_balancer_names,
'LoadBalancerNames.member.%d') 'LoadBalancerNames.member.%d')
if marker:
params['Marker'] = marker
return self.get_list('DescribeLoadBalancers', params, return self.get_list('DescribeLoadBalancers', params,
[('member', LoadBalancer)]) [('member', LoadBalancer)])
def create_load_balancer(self, name, zones, listeners=None, subnets=None, def create_load_balancer(self, name, zones, listeners=None, subnets=None,
security_groups=None, scheme='internet-facing', complex_listeners=None): security_groups=None, scheme='internet-facing',
complex_listeners=None):
""" """
Create a new load balancer for your account. By default the load Create a new load balancer for your account. By default the load
balancer will be created in EC2. To create a load balancer inside a balancer will be created in EC2. To create a load balancer inside a
@@ -170,13 +182,14 @@ class ELBConnection(AWSQueryConnection):
:type complex_listeners: List of tuples :type complex_listeners: List of tuples
:param complex_listeners: Each tuple contains four or five values, :param complex_listeners: Each tuple contains four or five values,
(LoadBalancerPortNumber, InstancePortNumber, Protocol, InstanceProtocol, (LoadBalancerPortNumber, InstancePortNumber, Protocol,
SSLCertificateId). InstanceProtocol, SSLCertificateId).
Where: Where:
- LoadBalancerPortNumber and InstancePortNumber are integer - LoadBalancerPortNumber and InstancePortNumber are integer
values between 1 and 65535 values between 1 and 65535
- Protocol and InstanceProtocol is a string containing either 'TCP', - Protocol and InstanceProtocol is a string containing
either 'TCP',
'SSL', 'HTTP', or 'HTTPS' 'SSL', 'HTTP', or 'HTTPS'
- SSLCertificateId is the ARN of an SSL certificate loaded into - SSLCertificateId is the ARN of an SSL certificate loaded into
AWS IAM AWS IAM
@@ -224,7 +237,7 @@ class ELBConnection(AWSQueryConnection):
if security_groups: if security_groups:
self.build_list_params(params, security_groups, self.build_list_params(params, security_groups,
'SecurityGroups.member.%d') 'SecurityGroups.member.%d')
load_balancer = self.get_object('CreateLoadBalancer', load_balancer = self.get_object('CreateLoadBalancer',
params, LoadBalancer) params, LoadBalancer)
@@ -235,7 +248,8 @@ class ELBConnection(AWSQueryConnection):
load_balancer.security_groups = security_groups load_balancer.security_groups = security_groups
return load_balancer return load_balancer
def create_load_balancer_listeners(self, name, listeners=None, complex_listeners=None): def create_load_balancer_listeners(self, name, listeners=None,
complex_listeners=None):
""" """
Creates a Listener (or group of listeners) for an existing Creates a Listener (or group of listeners) for an existing
Load Balancer Load Balancer
@@ -254,13 +268,14 @@ class ELBConnection(AWSQueryConnection):
:type complex_listeners: List of tuples :type complex_listeners: List of tuples
:param complex_listeners: Each tuple contains four or five values, :param complex_listeners: Each tuple contains four or five values,
(LoadBalancerPortNumber, InstancePortNumber, Protocol, InstanceProtocol, (LoadBalancerPortNumber, InstancePortNumber, Protocol,
SSLCertificateId). InstanceProtocol, SSLCertificateId).
Where: Where:
- LoadBalancerPortNumber and InstancePortNumber are integer - LoadBalancerPortNumber and InstancePortNumber are integer
values between 1 and 65535 values between 1 and 65535
- Protocol and InstanceProtocol is a string containing either 'TCP', - Protocol and InstanceProtocol is a string containing
either 'TCP',
'SSL', 'HTTP', or 'HTTPS' 'SSL', 'HTTP', or 'HTTPS'
- SSLCertificateId is the ARN of an SSL certificate loaded into - SSLCertificateId is the ARN of an SSL certificate loaded into
AWS IAM AWS IAM
@@ -347,7 +362,7 @@ class ELBConnection(AWSQueryConnection):
self.build_list_params(params, zones_to_add, self.build_list_params(params, zones_to_add,
'AvailabilityZones.member.%d') 'AvailabilityZones.member.%d')
obj = self.get_object('EnableAvailabilityZonesForLoadBalancer', obj = self.get_object('EnableAvailabilityZonesForLoadBalancer',
params, LoadBalancerZones) params, LoadBalancerZones)
return obj.zones return obj.zones
def disable_availability_zones(self, load_balancer_name, zones_to_remove): def disable_availability_zones(self, load_balancer_name, zones_to_remove):
@@ -372,7 +387,7 @@ class ELBConnection(AWSQueryConnection):
self.build_list_params(params, zones_to_remove, self.build_list_params(params, zones_to_remove,
'AvailabilityZones.member.%d') 'AvailabilityZones.member.%d')
obj = self.get_object('DisableAvailabilityZonesForLoadBalancer', obj = self.get_object('DisableAvailabilityZonesForLoadBalancer',
params, LoadBalancerZones) params, LoadBalancerZones)
return obj.zones return obj.zones
def modify_lb_attribute(self, load_balancer_name, attribute, value): def modify_lb_attribute(self, load_balancer_name, attribute, value):
@@ -386,6 +401,7 @@ class ELBConnection(AWSQueryConnection):
* crossZoneLoadBalancing - Boolean (true) * crossZoneLoadBalancing - Boolean (true)
* accessLog - :py:class:`AccessLogAttribute` instance * accessLog - :py:class:`AccessLogAttribute` instance
* connectionDraining - :py:class:`ConnectionDrainingAttribute` instance
:type value: string :type value: string
:param value: The new value for the attribute :param value: The new value for the attribute
@@ -415,6 +431,11 @@ class ELBConnection(AWSQueryConnection):
value.s3_bucket_prefix value.s3_bucket_prefix
params['LoadBalancerAttributes.AccessLog.EmitInterval'] = \ params['LoadBalancerAttributes.AccessLog.EmitInterval'] = \
value.emit_interval value.emit_interval
elif attribute.lower() == 'connectiondraining':
params['LoadBalancerAttributes.ConnectionDraining.Enabled'] = \
value.enabled and 'true' or 'false'
params['LoadBalancerAttributes.ConnectionDraining.Timeout'] = \
value.timeout
else: else:
raise ValueError('InvalidAttribute', attribute) raise ValueError('InvalidAttribute', attribute)
return self.get_status('ModifyLoadBalancerAttributes', params, return self.get_status('ModifyLoadBalancerAttributes', params,
@@ -445,14 +466,21 @@ class ELBConnection(AWSQueryConnection):
:type attribute: string :type attribute: string
:param attribute: The attribute you wish to see. :param attribute: The attribute you wish to see.
* accessLog - :py:class:`AccessLogAttribute` instance
* crossZoneLoadBalancing - Boolean * crossZoneLoadBalancing - Boolean
* connectionDraining - :py:class:`ConnectionDrainingAttribute`
instance
:rtype: Attribute dependent :rtype: Attribute dependent
:return: The new value for the attribute :return: The new value for the attribute
""" """
attributes = self.get_all_lb_attributes(load_balancer_name) attributes = self.get_all_lb_attributes(load_balancer_name)
if attribute.lower() == 'accesslog':
return attributes.access_log
if attribute.lower() == 'crosszoneloadbalancing': if attribute.lower() == 'crosszoneloadbalancing':
return attributes.cross_zone_load_balancing.enabled return attributes.cross_zone_load_balancing.enabled
if attribute.lower() == 'connectiondraining':
return attributes.connection_draining
return None return None
def register_instances(self, load_balancer_name, instances): def register_instances(self, load_balancer_name, instances):
@@ -601,17 +629,19 @@ class ELBConnection(AWSQueryConnection):
params['CookieExpirationPeriod'] = cookie_expiration_period params['CookieExpirationPeriod'] = cookie_expiration_period
return self.get_status('CreateLBCookieStickinessPolicy', params) return self.get_status('CreateLBCookieStickinessPolicy', params)
def create_lb_policy(self, lb_name, policy_name, policy_type, policy_attributes): def create_lb_policy(self, lb_name, policy_name, policy_type,
policy_attributes):
""" """
Creates a new policy that contais the necessary attributes depending on Creates a new policy that contains the necessary attributes
the policy type. Policies are settings that are saved for your load depending on the policy type. Policies are settings that are
balancer and that can be applied to the front-end listener, or saved for your load balancer and that can be applied to the
the back-end application server. front-end listener, or the back-end application server.
""" """
params = {'LoadBalancerName': lb_name, params = {'LoadBalancerName': lb_name,
'PolicyName': policy_name, 'PolicyName': policy_name,
'PolicyTypeName': policy_type} 'PolicyTypeName': policy_type}
for index, (name, value) in enumerate(policy_attributes.iteritems(), 1): for index, (name, value) in enumerate(six.iteritems(policy_attributes), 1):
params['PolicyAttributes.member.%d.AttributeName' % index] = name params['PolicyAttributes.member.%d.AttributeName' % index] = name
params['PolicyAttributes.member.%d.AttributeValue' % index] = value params['PolicyAttributes.member.%d.AttributeValue' % index] = value
else: else:
@@ -635,10 +665,14 @@ class ELBConnection(AWSQueryConnection):
""" """
params = {'LoadBalancerName': lb_name, params = {'LoadBalancerName': lb_name,
'LoadBalancerPort': lb_port} 'LoadBalancerPort': lb_port}
self.build_list_params(params, policies, 'PolicyNames.member.%d') if len(policies):
self.build_list_params(params, policies, 'PolicyNames.member.%d')
else:
params['PolicyNames'] = ''
return self.get_status('SetLoadBalancerPoliciesOfListener', params) return self.get_status('SetLoadBalancerPoliciesOfListener', params)
def set_lb_policies_of_backend_server(self, lb_name, instance_port, policies): def set_lb_policies_of_backend_server(self, lb_name, instance_port,
policies):
""" """
Replaces the current set of policies associated with a port on which Replaces the current set of policies associated with a port on which
the back-end server is listening with a new set of policies. the back-end server is listening with a new set of policies.
@@ -649,7 +683,8 @@ class ELBConnection(AWSQueryConnection):
self.build_list_params(params, policies, 'PolicyNames.member.%d') self.build_list_params(params, policies, 'PolicyNames.member.%d')
else: else:
params['PolicyNames'] = '' params['PolicyNames'] = ''
return self.get_status('SetLoadBalancerPoliciesForBackendServer', params) return self.get_status('SetLoadBalancerPoliciesForBackendServer',
params)
def apply_security_groups_to_lb(self, name, security_groups): def apply_security_groups_to_lb(self, name, security_groups):
""" """

View File

@@ -74,6 +74,31 @@ class AccessLogAttribute(object):
elif name == 'EmitInterval': elif name == 'EmitInterval':
self.emit_interval = int(value) self.emit_interval = int(value)
class ConnectionDrainingAttribute(object):
"""
Represents the ConnectionDraining segment of ELB attributes.
"""
def __init__(self, connection=None):
self.enabled = None
self.timeout = None
def __repr__(self):
return 'ConnectionDraining(%s, %s)' % (
self.enabled,
self.timeout
)
def startElement(self, name, attrs, connection):
pass
def endElement(self, name, value, connection):
if name == 'Enabled':
if value.lower() == 'true':
self.enabled = True
else:
self.enabled = False
elif name == 'Timeout':
self.timeout = int(value)
class LbAttributes(object): class LbAttributes(object):
""" """
@@ -84,17 +109,21 @@ class LbAttributes(object):
self.cross_zone_load_balancing = CrossZoneLoadBalancingAttribute( self.cross_zone_load_balancing = CrossZoneLoadBalancingAttribute(
self.connection) self.connection)
self.access_log = AccessLogAttribute(self.connection) self.access_log = AccessLogAttribute(self.connection)
self.connection_draining = ConnectionDrainingAttribute(self.connection)
def __repr__(self): def __repr__(self):
return 'LbAttributes(%s, %s)' % ( return 'LbAttributes(%s, %s, %s)' % (
repr(self.cross_zone_load_balancing), repr(self.cross_zone_load_balancing),
repr(self.access_log)) repr(self.access_log),
repr(self.connection_draining))
def startElement(self, name, attrs, connection): def startElement(self, name, attrs, connection):
if name == 'CrossZoneLoadBalancing': if name == 'CrossZoneLoadBalancing':
return self.cross_zone_load_balancing return self.cross_zone_load_balancing
if name == 'AccessLog': if name == 'AccessLog':
return self.access_log return self.access_log
if name == 'ConnectionDraining':
return self.connection_draining
def endElement(self, name, value, connection): def endElement(self, name, value, connection):
pass pass

View File

@@ -27,6 +27,7 @@ from boto.ec2.elb.policies import Policies, OtherPolicy
from boto.ec2.elb.securitygroup import SecurityGroup from boto.ec2.elb.securitygroup import SecurityGroup
from boto.ec2.instanceinfo import InstanceInfo from boto.ec2.instanceinfo import InstanceInfo
from boto.resultset import ResultSet from boto.resultset import ResultSet
from boto.compat import six
class Backend(object): class Backend(object):
@@ -67,6 +68,7 @@ class LoadBalancerZones(object):
def endElement(self, name, value, connection): def endElement(self, name, value, connection):
pass pass
class LoadBalancer(object): class LoadBalancer(object):
""" """
Represents an EC2 Load Balancer. Represents an EC2 Load Balancer.
@@ -82,6 +84,7 @@ class LoadBalancer(object):
check policy for this load balancer. check policy for this load balancer.
:ivar boto.ec2.elb.policies.Policies policies: Cookie stickiness and :ivar boto.ec2.elb.policies.Policies policies: Cookie stickiness and
other policies. other policies.
:ivar str name: The name of the Load Balancer.
:ivar str dns_name: The external DNS name for the balancer. :ivar str dns_name: The external DNS name for the balancer.
:ivar str created_time: A date+time string showing when the :ivar str created_time: A date+time string showing when the
load balancer was created. load balancer was created.
@@ -186,7 +189,7 @@ class LoadBalancer(object):
:param zones: The name of the zone(s) to add. :param zones: The name of the zone(s) to add.
""" """
if isinstance(zones, basestring): if isinstance(zones, six.string_types):
zones = [zones] zones = [zones]
new_zones = self.connection.enable_availability_zones(self.name, zones) new_zones = self.connection.enable_availability_zones(self.name, zones)
self.availability_zones = new_zones self.availability_zones = new_zones
@@ -199,9 +202,10 @@ class LoadBalancer(object):
:param zones: The name of the zone(s) to add. :param zones: The name of the zone(s) to add.
""" """
if isinstance(zones, basestring): if isinstance(zones, six.string_types):
zones = [zones] zones = [zones]
new_zones = self.connection.disable_availability_zones(self.name, zones) new_zones = self.connection.disable_availability_zones(
self.name, zones)
self.availability_zones = new_zones self.availability_zones = new_zones
def get_attributes(self, force=False): def get_attributes(self, force=False):
@@ -266,7 +270,7 @@ class LoadBalancer(object):
to add to this load balancer. to add to this load balancer.
""" """
if isinstance(instances, basestring): if isinstance(instances, six.string_types):
instances = [instances] instances = [instances]
new_instances = self.connection.register_instances(self.name, new_instances = self.connection.register_instances(self.name,
instances) instances)
@@ -281,7 +285,7 @@ class LoadBalancer(object):
to remove from this load balancer. to remove from this load balancer.
""" """
if isinstance(instances, basestring): if isinstance(instances, six.string_types):
instances = [instances] instances = [instances]
new_instances = self.connection.deregister_instances(self.name, new_instances = self.connection.deregister_instances(self.name,
instances) instances)
@@ -348,14 +352,13 @@ class LoadBalancer(object):
policies) policies)
def set_policies_of_backend_server(self, instance_port, policies): def set_policies_of_backend_server(self, instance_port, policies):
return self.connection.set_lb_policies_of_backend_server(self.name, return self.connection.set_lb_policies_of_backend_server(
instance_port, self.name, instance_port, policies)
policies)
def create_cookie_stickiness_policy(self, cookie_expiration_period, def create_cookie_stickiness_policy(self, cookie_expiration_period,
policy_name): policy_name):
return self.connection.create_lb_cookie_stickiness_policy(cookie_expiration_period, self.name, policy_name) return self.connection.create_lb_cookie_stickiness_policy(
cookie_expiration_period, self.name, policy_name)
def create_app_cookie_stickiness_policy(self, name, policy_name): def create_app_cookie_stickiness_policy(self, name, policy_name):
return self.connection.create_app_cookie_stickiness_policy(name, return self.connection.create_app_cookie_stickiness_policy(name,
@@ -363,12 +366,12 @@ class LoadBalancer(object):
policy_name) policy_name)
def set_listener_SSL_certificate(self, lb_port, ssl_certificate_id): def set_listener_SSL_certificate(self, lb_port, ssl_certificate_id):
return self.connection.set_lb_listener_SSL_certificate(self.name, return self.connection.set_lb_listener_SSL_certificate(
lb_port, self.name, lb_port, ssl_certificate_id)
ssl_certificate_id)
def create_lb_policy(self, policy_name, policy_type, policy_attribute): def create_lb_policy(self, policy_name, policy_type, policy_attribute):
return self.connection.create_lb_policy(self.name, policy_name, policy_type, policy_attribute) return self.connection.create_lb_policy(
self.name, policy_name, policy_type, policy_attribute)
def attach_subnets(self, subnets): def attach_subnets(self, subnets):
""" """
@@ -380,7 +383,7 @@ class LoadBalancer(object):
:param subnets: The name of the subnet(s) to add. :param subnets: The name of the subnet(s) to add.
""" """
if isinstance(subnets, basestring): if isinstance(subnets, six.string_types):
subnets = [subnets] subnets = [subnets]
new_subnets = self.connection.attach_lb_to_subnets(self.name, subnets) new_subnets = self.connection.attach_lb_to_subnets(self.name, subnets)
self.subnets = new_subnets self.subnets = new_subnets
@@ -393,9 +396,10 @@ class LoadBalancer(object):
:param subnets: The name of the subnet(s) to detach. :param subnets: The name of the subnet(s) to detach.
""" """
if isinstance(subnets, basestring): if isinstance(subnets, six.string_types):
subnets = [subnets] subnets = [subnets]
new_subnets = self.connection.detach_lb_from_subnets(self.name, subnets) new_subnets = self.connection.detach_lb_from_subnets(
self.name, subnets)
self.subnets = new_subnets self.subnets = new_subnets
def apply_security_groups(self, security_groups): def apply_security_groups(self, security_groups):
@@ -408,8 +412,8 @@ class LoadBalancer(object):
:param security_groups: The name of the security group(s) to add. :param security_groups: The name of the security group(s) to add.
""" """
if isinstance(security_groups, basestring): if isinstance(security_groups, six.string_types):
security_groups = [security_groups] security_groups = [security_groups]
new_sgs = self.connection.apply_security_groups_to_lb( new_sgs = self.connection.apply_security_groups_to_lb(
self.name, security_groups) self.name, security_groups)
self.security_groups = new_sgs self.security_groups = new_sgs

View File

@@ -233,6 +233,9 @@ class Image(TaggedEC2Object):
* i2.2xlarge * i2.2xlarge
* i2.4xlarge * i2.4xlarge
* i2.8xlarge * i2.8xlarge
* t2.micro
* t2.small
* t2.medium
:type placement: string :type placement: string
:param placement: The Availability Zone to launch the instance into. :param placement: The Availability Zone to launch the instance into.

View File

@@ -83,7 +83,7 @@ class KeyPair(EC2Object):
fp = open(file_path, 'wb') fp = open(file_path, 'wb')
fp.write(self.material) fp.write(self.material)
fp.close() fp.close()
os.chmod(file_path, 0600) os.chmod(file_path, 0o600)
return True return True
else: else:
raise BotoClientError('KeyPair contains no material') raise BotoClientError('KeyPair contains no material')

View File

@@ -167,6 +167,70 @@ class NetworkInterface(TaggedEC2Object):
else: else:
setattr(self, name, value) setattr(self, name, value)
def _update(self, updated):
self.__dict__.update(updated.__dict__)
def update(self, validate=False, dry_run=False):
"""
Update the data associated with this ENI by querying EC2.
:type validate: bool
:param validate: By default, if EC2 returns no data about the
ENI the update method returns quietly. If
the validate param is True, however, it will
raise a ValueError exception if no data is
returned from EC2.
"""
rs = self.connection.get_all_network_interfaces(
[self.id],
dry_run=dry_run
)
if len(rs) > 0:
self._update(rs[0])
elif validate:
raise ValueError('%s is not a valid ENI ID' % self.id)
return self.status
def attach(self, instance_id, device_index, dry_run=False):
"""
Attach this ENI to an EC2 instance.
:type instance_id: str
:param instance_id: The ID of the EC2 instance to which it will
be attached.
:type device_index: int
:param device_index: The interface nunber, N, on the instance (eg. ethN)
:rtype: bool
:return: True if successful
"""
return self.connection.attach_network_interface(
self.id,
instance_id,
device_index,
dry_run=dry_run
)
def detach(self, force=False, dry_run=False):
"""
Detach this ENI from an EC2 instance.
:type force: bool
:param force: Forces detachment if the previous detachment
attempt did not occur cleanly.
:rtype: bool
:return: True if successful
"""
attachment_id = getattr(self.attachment, 'id', None)
return self.connection.detach_network_interface(
attachment_id,
force,
dry_run=dry_run
)
def delete(self, dry_run=False): def delete(self, dry_run=False):
return self.connection.delete_network_interface( return self.connection.delete_network_interface(
self.id, self.id,

View File

@@ -18,7 +18,6 @@
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 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 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
from boto.resultset import ResultSet from boto.resultset import ResultSet
from boto.ec2.ec2object import EC2Object from boto.ec2.ec2object import EC2Object
from boto.utils import parse_ts from boto.utils import parse_ts
@@ -82,13 +81,13 @@ class ReservedInstancesOffering(EC2Object):
self.marketplace = True if value == 'true' else False self.marketplace = True if value == 'true' else False
def describe(self): def describe(self):
print 'ID=%s' % self.id print('ID=%s' % self.id)
print '\tInstance Type=%s' % self.instance_type print('\tInstance Type=%s' % self.instance_type)
print '\tZone=%s' % self.availability_zone print('\tZone=%s' % self.availability_zone)
print '\tDuration=%s' % self.duration print('\tDuration=%s' % self.duration)
print '\tFixed Price=%s' % self.fixed_price print('\tFixed Price=%s' % self.fixed_price)
print '\tUsage Price=%s' % self.usage_price print('\tUsage Price=%s' % self.usage_price)
print '\tDescription=%s' % self.description print('\tDescription=%s' % self.description)
def purchase(self, instance_count=1, dry_run=False): def purchase(self, instance_count=1, dry_run=False):
return self.connection.purchase_reserved_instance_offering( return self.connection.purchase_reserved_instance_offering(

View File

@@ -41,6 +41,7 @@ class Snapshot(TaggedEC2Object):
self.owner_alias = None self.owner_alias = None
self.volume_size = None self.volume_size = None
self.description = None self.description = None
self.encrypted = None
def __repr__(self): def __repr__(self):
return 'Snapshot:%s' % self.id return 'Snapshot:%s' % self.id
@@ -65,6 +66,8 @@ class Snapshot(TaggedEC2Object):
self.volume_size = value self.volume_size = value
elif name == 'description': elif name == 'description':
self.description = value self.description = value
elif name == 'encrypted':
self.encrypted = (value.lower() == 'true')
else: else:
setattr(self, name, value) setattr(self, name, value)
@@ -152,6 +155,7 @@ class Snapshot(TaggedEC2Object):
self.id, self.id,
volume_type, volume_type,
iops, iops,
self.encrypted,
dry_run=dry_run dry_run=dry_run
) )

View File

@@ -44,6 +44,7 @@ class Volume(TaggedEC2Object):
:ivar type: The type of volume (standard or consistent-iops) :ivar type: The type of volume (standard or consistent-iops)
:ivar iops: If this volume is of type consistent-iops, this is :ivar iops: If this volume is of type consistent-iops, this is
the number of IOPS provisioned (10-300). the number of IOPS provisioned (10-300).
:ivar encrypted: True if this volume is encrypted.
""" """
def __init__(self, connection=None): def __init__(self, connection=None):
@@ -57,6 +58,7 @@ class Volume(TaggedEC2Object):
self.zone = None self.zone = None
self.type = None self.type = None
self.iops = None self.iops = None
self.encrypted = None
def __repr__(self): def __repr__(self):
return 'Volume:%s' % self.id return 'Volume:%s' % self.id
@@ -92,6 +94,8 @@ class Volume(TaggedEC2Object):
self.type = value self.type = value
elif name == 'iops': elif name == 'iops':
self.iops = int(value) self.iops = int(value)
elif name == 'encrypted':
self.encrypted = (value.lower() == 'true')
else: else:
setattr(self, name, value) setattr(self, name, value)

View File

@@ -21,6 +21,7 @@
import boto import boto
from boto.connection import AWSQueryConnection, AWSAuthConnection from boto.connection import AWSQueryConnection, AWSAuthConnection
from boto.exception import BotoServerError
import time import time
import urllib import urllib
import xml.sax import xml.sax
@@ -61,20 +62,22 @@ class ECSConnection(AWSQueryConnection):
if page: if page:
params['ItemPage'] = page params['ItemPage'] = page
response = self.make_request(None, params, "/onca/xml") response = self.make_request(None, params, "/onca/xml")
body = response.read() body = response.read().decode('utf-8')
boto.log.debug(body) boto.log.debug(body)
if response.status != 200: if response.status != 200:
boto.log.error('%s %s' % (response.status, response.reason)) boto.log.error('%s %s' % (response.status, response.reason))
boto.log.error('%s' % body) boto.log.error('%s' % body)
raise self.ResponseError(response.status, response.reason, body) raise BotoServerError(response.status, response.reason, body)
if itemSet is None: if itemSet is None:
rs = ItemSet(self, action, params, page) rs = ItemSet(self, action, params, page)
else: else:
rs = itemSet rs = itemSet
h = handler.XmlHandler(rs, self) h = handler.XmlHandler(rs, self)
xml.sax.parseString(body, h) xml.sax.parseString(body.encode('utf-8'), h)
if not rs.is_valid:
raise BotoServerError(response.status, '{Code}: {Message}'.format(**rs.errors[0]))
return rs return rs
# #
@@ -91,3 +94,12 @@ class ECSConnection(AWSQueryConnection):
""" """
params['SearchIndex'] = search_index params['SearchIndex'] = search_index
return self.get_response('ItemSearch', params) return self.get_response('ItemSearch', params)
def item_lookup(self, **params):
"""
Returns items that satisfy the lookup query.
For a full list of parameters, see:
http://s3.amazonaws.com/awsdocs/Associates/2011-08-01/prod-adv-api-dg-2011-08-01.pdf
"""
return self.get_response('ItemLookup', params)

View File

@@ -22,7 +22,7 @@
import xml.sax import xml.sax
import cgi import cgi
from StringIO import StringIO from boto.compat import six, StringIO
class ResponseGroup(xml.sax.ContentHandler): class ResponseGroup(xml.sax.ContentHandler):
"""A Generic "Response Group", which can """A Generic "Response Group", which can
@@ -90,14 +90,14 @@ class Item(ResponseGroup):
def __init__(self, connection=None): def __init__(self, connection=None):
"""Initialize this Item""" """Initialize this Item"""
super(Item, self).__init__(connection, "Item") ResponseGroup.__init__(self, connection, "Item")
class ItemSet(ResponseGroup): class ItemSet(ResponseGroup):
"""A special ResponseGroup that has built-in paging, and """A special ResponseGroup that has built-in paging, and
only creates new Items on the "Item" tag""" only creates new Items on the "Item" tag"""
def __init__(self, connection, action, params, page=0): def __init__(self, connection, action, params, page=0):
super(ItemSet, self).__init__(connection, "Items") ResponseGroup.__init__(self, connection, "Items")
self.objs = [] self.objs = []
self.iter = None self.iter = None
self.page = page self.page = page
@@ -106,6 +106,8 @@ class ItemSet(ResponseGroup):
self.curItem = None self.curItem = None
self.total_results = 0 self.total_results = 0
self.total_pages = 0 self.total_pages = 0
self.is_valid = False
self.errors = []
def startElement(self, name, attrs, connection): def startElement(self, name, attrs, connection):
if name == "Item": if name == "Item":
@@ -119,7 +121,14 @@ class ItemSet(ResponseGroup):
self.total_results = value self.total_results = value
elif name == 'TotalPages': elif name == 'TotalPages':
self.total_pages = value self.total_pages = value
elif name == "Item": elif name == 'IsValid':
if value == 'True':
self.is_valid = True
elif name == 'Code':
self.errors.append({'Code': value, 'Message': None})
elif name == 'Message':
self.errors[-1]['Message'] = value
elif name == 'Item':
self.objs.append(self.curItem) self.objs.append(self.curItem)
self._xml.write(self.curItem.to_xml()) self._xml.write(self.curItem.to_xml())
self.curItem = None self.curItem = None
@@ -127,22 +136,24 @@ class ItemSet(ResponseGroup):
self.curItem.endElement(name, value, connection) self.curItem.endElement(name, value, connection)
return None return None
def next(self): def __next__(self):
"""Special paging functionality""" """Special paging functionality"""
if self.iter is None: if self.iter is None:
self.iter = iter(self.objs) self.iter = iter(self.objs)
try: try:
return self.iter.next() return next(self.iter)
except StopIteration: except StopIteration:
self.iter = None self.iter = None
self.objs = [] self.objs = []
if int(self.page) < int(self.total_pages): if int(self.page) < int(self.total_pages):
self.page += 1 self.page += 1
self._connection.get_response(self.action, self.params, self.page, self) self._connection.get_response(self.action, self.params, self.page, self)
return self.next() return next(self)
else: else:
raise raise
next = __next__
def __iter__(self): def __iter__(self):
return self return self
@@ -150,4 +161,4 @@ class ItemSet(ResponseGroup):
"""Override to first fetch everything""" """Override to first fetch everything"""
for item in self: for item in self:
pass pass
return super(ItemSet, self).to_xml() return ResponseGroup.to_xml(self)

View File

@@ -1657,7 +1657,7 @@ class ElastiCacheConnection(AWSQueryConnection):
params['ContentType'] = 'JSON' params['ContentType'] = 'JSON'
response = self.make_request(action=action, verb='POST', response = self.make_request(action=action, verb='POST',
path='/', params=params) path='/', params=params)
body = response.read() body = response.read().decode('utf-8')
boto.log.debug(body) boto.log.debug(body)
if response.status == 200: if response.status == 200:
return json.loads(body) return json.loads(body)

View File

@@ -923,7 +923,7 @@ class ElasticTranscoderConnection(AWSAuthConnection):
headers = {} headers = {}
response = super(ElasticTranscoderConnection, self).make_request( response = super(ElasticTranscoderConnection, self).make_request(
verb, resource, headers=headers, data=data) verb, resource, headers=headers, data=data)
body = json.load(response) body = json.loads(response.read().decode('utf-8'))
if response.status == expected_status: if response.status == expected_status:
return body return body
else: else:

View File

@@ -26,9 +26,9 @@
This module provies an interface to the Elastic MapReduce (EMR) This module provies an interface to the Elastic MapReduce (EMR)
service from AWS. service from AWS.
""" """
from connection import EmrConnection from boto.emr.connection import EmrConnection
from step import Step, StreamingStep, JarStep from boto.emr.step import Step, StreamingStep, JarStep
from bootstrap_action import BootstrapAction from boto.emr.bootstrap_action import BootstrapAction
from boto.regioninfo import RegionInfo, get_regions from boto.regioninfo import RegionInfo, get_regions

View File

@@ -20,12 +20,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
from boto.compat import six
class BootstrapAction(object): class BootstrapAction(object):
def __init__(self, name, path, bootstrap_action_args): def __init__(self, name, path, bootstrap_action_args):
self.name = name self.name = name
self.path = path self.path = path
if isinstance(bootstrap_action_args, basestring): if isinstance(bootstrap_action_args, six.string_types):
bootstrap_action_args = [bootstrap_action_args] bootstrap_action_args = [bootstrap_action_args]
self.bootstrap_action_args = bootstrap_action_args self.bootstrap_action_args = bootstrap_action_args

View File

@@ -37,6 +37,7 @@ from boto.emr.emrobject import AddInstanceGroupsResponse, BootstrapActionList, \
from boto.emr.step import JarStep from boto.emr.step import JarStep
from boto.connection import AWSQueryConnection from boto.connection import AWSQueryConnection
from boto.exception import EmrResponseError from boto.exception import EmrResponseError
from boto.compat import six
class EmrConnection(AWSQueryConnection): class EmrConnection(AWSQueryConnection):
@@ -281,7 +282,7 @@ class EmrConnection(AWSQueryConnection):
value for that tag should be the empty string value for that tag should be the empty string
(e.g. '') or None. (e.g. '') or None.
""" """
assert isinstance(resource_id, basestring) assert isinstance(resource_id, six.string_types)
params = { params = {
'ResourceId': resource_id, 'ResourceId': resource_id,
} }
@@ -333,7 +334,7 @@ class EmrConnection(AWSQueryConnection):
:type steps: list(boto.emr.Step) :type steps: list(boto.emr.Step)
:param steps: A list of steps to add to the job :param steps: A list of steps to add to the job
""" """
if not isinstance(steps, types.ListType): if not isinstance(steps, list):
steps = [steps] steps = [steps]
params = {} params = {}
params['JobFlowId'] = jobflow_id params['JobFlowId'] = jobflow_id
@@ -356,7 +357,7 @@ class EmrConnection(AWSQueryConnection):
:type instance_groups: list(boto.emr.InstanceGroup) :type instance_groups: list(boto.emr.InstanceGroup)
:param instance_groups: A list of instance groups to add to the job :param instance_groups: A list of instance groups to add to the job
""" """
if not isinstance(instance_groups, types.ListType): if not isinstance(instance_groups, list):
instance_groups = [instance_groups] instance_groups = [instance_groups]
params = {} params = {}
params['JobFlowId'] = jobflow_id params['JobFlowId'] = jobflow_id
@@ -377,9 +378,9 @@ class EmrConnection(AWSQueryConnection):
:type new_sizes: list(int) :type new_sizes: list(int)
:param new_sizes: A list of the new sizes for each instance group :param new_sizes: A list of the new sizes for each instance group
""" """
if not isinstance(instance_group_ids, types.ListType): if not isinstance(instance_group_ids, list):
instance_group_ids = [instance_group_ids] instance_group_ids = [instance_group_ids]
if not isinstance(new_sizes, types.ListType): if not isinstance(new_sizes, list):
new_sizes = [new_sizes] new_sizes = [new_sizes]
instance_groups = zip(instance_group_ids, new_sizes) instance_groups = zip(instance_group_ids, new_sizes)
@@ -409,7 +410,8 @@ class EmrConnection(AWSQueryConnection):
ami_version=None, ami_version=None,
api_params=None, api_params=None,
visible_to_all_users=None, visible_to_all_users=None,
job_flow_role=None): job_flow_role=None,
service_role=None):
""" """
Runs a job flow Runs a job flow
:type name: str :type name: str
@@ -491,6 +493,10 @@ class EmrConnection(AWSQueryConnection):
``EMRJobflowDefault``. In order to use the default role, ``EMRJobflowDefault``. In order to use the default role,
you must have already created it using the CLI. you must have already created it using the CLI.
:type service_role: str
:param service_role: The IAM role that will be assumed by the Amazon
EMR service to access AWS resources on your behalf.
:rtype: str :rtype: str
:return: The jobflow id :return: The jobflow id
""" """
@@ -524,7 +530,7 @@ class EmrConnection(AWSQueryConnection):
# Instance group args (for spot instances or a heterogenous cluster) # Instance group args (for spot instances or a heterogenous cluster)
list_args = self._build_instance_group_list_args(instance_groups) list_args = self._build_instance_group_list_args(instance_groups)
instance_params = dict( instance_params = dict(
('Instances.%s' % k, v) for k, v in list_args.iteritems() ('Instances.%s' % k, v) for k, v in six.iteritems(list_args)
) )
params.update(instance_params) params.update(instance_params)
@@ -553,7 +559,7 @@ class EmrConnection(AWSQueryConnection):
params['AdditionalInfo'] = additional_info params['AdditionalInfo'] = additional_info
if api_params: if api_params:
for key, value in api_params.iteritems(): for key, value in six.iteritems(api_params):
if value is None: if value is None:
params.pop(key, None) params.pop(key, None)
else: else:
@@ -568,6 +574,9 @@ class EmrConnection(AWSQueryConnection):
if job_flow_role is not None: if job_flow_role is not None:
params['JobFlowRole'] = job_flow_role params['JobFlowRole'] = job_flow_role
if service_role is not None:
params['ServiceRole'] = service_role
response = self.get_object( response = self.get_object(
'RunJobFlow', params, RunJobFlowResponse, verb='POST') 'RunJobFlow', params, RunJobFlowResponse, verb='POST')
return response.jobflowid return response.jobflowid
@@ -641,27 +650,27 @@ class EmrConnection(AWSQueryConnection):
return step_params return step_params
def _build_bootstrap_action_list(self, bootstrap_actions): def _build_bootstrap_action_list(self, bootstrap_actions):
if not isinstance(bootstrap_actions, types.ListType): if not isinstance(bootstrap_actions, list):
bootstrap_actions = [bootstrap_actions] bootstrap_actions = [bootstrap_actions]
params = {} params = {}
for i, bootstrap_action in enumerate(bootstrap_actions): for i, bootstrap_action in enumerate(bootstrap_actions):
for key, value in bootstrap_action.iteritems(): for key, value in six.iteritems(bootstrap_action):
params['BootstrapActions.member.%s.%s' % (i + 1, key)] = value params['BootstrapActions.member.%s.%s' % (i + 1, key)] = value
return params return params
def _build_step_list(self, steps): def _build_step_list(self, steps):
if not isinstance(steps, types.ListType): if not isinstance(steps, list):
steps = [steps] steps = [steps]
params = {} params = {}
for i, step in enumerate(steps): for i, step in enumerate(steps):
for key, value in step.iteritems(): for key, value in six.iteritems(step):
params['Steps.member.%s.%s' % (i+1, key)] = value params['Steps.member.%s.%s' % (i+1, key)] = value
return params return params
def _build_string_list(self, field, items): def _build_string_list(self, field, items):
if not isinstance(items, types.ListType): if not isinstance(items, list):
items = [items] items = [items]
params = {} params = {}
@@ -673,7 +682,7 @@ class EmrConnection(AWSQueryConnection):
assert isinstance(tags, dict) assert isinstance(tags, dict)
params = {} params = {}
for i, key_value in enumerate(sorted(tags.iteritems()), start=1): for i, key_value in enumerate(sorted(six.iteritems(tags)), start=1):
key, value = key_value key, value = key_value
current_prefix = 'Tags.member.%s' % i current_prefix = 'Tags.member.%s' % i
params['%s.Key' % current_prefix] = key params['%s.Key' % current_prefix] = key
@@ -734,12 +743,12 @@ class EmrConnection(AWSQueryConnection):
a comparable dict for use in making a RunJobFlow or AddInstanceGroups a comparable dict for use in making a RunJobFlow or AddInstanceGroups
request. request.
""" """
if not isinstance(instance_groups, types.ListType): if not isinstance(instance_groups, list):
instance_groups = [instance_groups] instance_groups = [instance_groups]
params = {} params = {}
for i, instance_group in enumerate(instance_groups): for i, instance_group in enumerate(instance_groups):
ig_dict = self._build_instance_group_args(instance_group) ig_dict = self._build_instance_group_args(instance_group)
for key, value in ig_dict.iteritems(): for key, value in six.iteritems(ig_dict):
params['InstanceGroups.member.%d.%s' % (i+1, key)] = value params['InstanceGroups.member.%d.%s' % (i+1, key)] = value
return params return params

View File

@@ -301,7 +301,7 @@ class ClusterSummaryList(EmrObject):
class StepConfig(EmrObject): class StepConfig(EmrObject):
Fields = set([ Fields = set([
'Jar' 'Jar',
'MainClass' 'MainClass'
]) ])
@@ -434,11 +434,15 @@ class StepSummary(EmrObject):
def __init__(self, connection=None): def __init__(self, connection=None):
self.connection = connection self.connection = connection
self.status = None self.status = None
self.config = None
def startElement(self, name, attrs, connection): def startElement(self, name, attrs, connection):
if name == 'Status': if name == 'Status':
self.status = ClusterStatus() self.status = ClusterStatus()
return self.status return self.status
elif name == 'Config':
self.config = StepConfig()
return self.config
else: else:
return None return None

View File

@@ -20,6 +20,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
from boto.compat import six
class Step(object): class Step(object):
""" """
@@ -73,7 +75,7 @@ class JarStep(Step):
self._main_class = main_class self._main_class = main_class
self.action_on_failure = action_on_failure self.action_on_failure = action_on_failure
if isinstance(step_args, basestring): if isinstance(step_args, six.string_types):
step_args = [step_args] step_args = [step_args]
self.step_args = step_args self.step_args = step_args
@@ -143,7 +145,7 @@ class StreamingStep(Step):
self.output = output self.output = output
self._jar = jar self._jar = jar
if isinstance(step_args, basestring): if isinstance(step_args, six.string_types):
step_args = [step_args] step_args = [step_args]
self.step_args = step_args self.step_args = step_args

View File

@@ -26,10 +26,12 @@ Exception classes - Subclassing allows you to check for specific errors
""" """
import base64 import base64
import xml.sax import xml.sax
from boto import handler
from boto.compat import json
from boto.resultset import ResultSet
import boto
from boto import handler
from boto.compat import json, six, StandardError
from boto.resultset import ResultSet
class BotoClientError(StandardError): class BotoClientError(StandardError):
""" """
@@ -80,34 +82,56 @@ class BotoServerError(StandardError):
self.request_id = None self.request_id = None
self.error_code = None self.error_code = None
self._error_message = None self._error_message = None
self.message = ''
self.box_usage = None self.box_usage = None
if isinstance(self.body, bytes):
try:
self.body = self.body.decode('utf-8')
except UnicodeDecodeError:
boto.log.debug('Unable to decode body from bytes!')
# Attempt to parse the error response. If body isn't present, # Attempt to parse the error response. If body isn't present,
# then just ignore the error response. # then just ignore the error response.
if self.body: if self.body:
try: # Check if it looks like a ``dict``.
h = handler.XmlHandlerWrapper(self, self) if hasattr(self.body, 'items'):
h.parseString(self.body) # It's not a string, so trying to parse it will fail.
except (TypeError, xml.sax.SAXParseException), pe: # But since it's data, we can work with that.
# What if it's JSON? Let's try that. self.request_id = self.body.get('RequestId', None)
if 'Error' in self.body:
# XML-style
error = self.body.get('Error', {})
self.error_code = error.get('Code', None)
self.message = error.get('Message', None)
else:
# JSON-style.
self.message = self.body.get('message', None)
else:
try: try:
parsed = json.loads(self.body) h = handler.XmlHandlerWrapper(self, self)
h.parseString(self.body)
except (TypeError, xml.sax.SAXParseException) as pe:
# What if it's JSON? Let's try that.
try:
parsed = json.loads(self.body)
if 'RequestId' in parsed: if 'RequestId' in parsed:
self.request_id = parsed['RequestId'] self.request_id = parsed['RequestId']
if 'Error' in parsed: if 'Error' in parsed:
if 'Code' in parsed['Error']: if 'Code' in parsed['Error']:
self.error_code = parsed['Error']['Code'] self.error_code = parsed['Error']['Code']
if 'Message' in parsed['Error']: if 'Message' in parsed['Error']:
self.message = parsed['Error']['Message'] self.message = parsed['Error']['Message']
except ValueError: except (TypeError, ValueError):
# Remove unparsable message body so we don't include garbage # Remove unparsable message body so we don't include garbage
# in exception. But first, save self.body in self.error_message # in exception. But first, save self.body in self.error_message
# because occasionally we get error messages from Eucalyptus # because occasionally we get error messages from Eucalyptus
# that are just text strings that we want to preserve. # that are just text strings that we want to preserve.
self.message = self.body self.message = self.body
self.body = None self.body = None
def __getattr__(self, name): def __getattr__(self, name):
if name == 'error_message': if name == 'error_message':

View File

@@ -21,8 +21,8 @@
import boto import boto
from connection import FileConnection as Connection from boto.file.connection import FileConnection as Connection
from key import Key from boto.file.key import Key
from bucket import Bucket from boto.file.bucket import Bucket
__all__ = ['Connection', 'Key', 'Bucket'] __all__ = ['Connection', 'Key', 'Bucket']

View File

@@ -23,7 +23,7 @@
# File representation of bucket, for use with "file://" URIs. # File representation of bucket, for use with "file://" URIs.
import os import os
from key import Key from boto.file.key import Key
from boto.file.simpleresultset import SimpleResultSet from boto.file.simpleresultset import SimpleResultSet
from boto.s3.bucketlistresultset import BucketListResultSet from boto.s3.bucketlistresultset import BucketListResultSet

View File

@@ -21,7 +21,7 @@
# File representation of connection, for use with "file://" URIs. # File representation of connection, for use with "file://" URIs.
from bucket import Bucket from boto.file.bucket import Bucket
class FileConnection(object): class FileConnection(object):

View File

@@ -22,9 +22,11 @@
# File representation of key, for use with "file://" URIs. # File representation of key, for use with "file://" URIs.
import os, shutil, StringIO import os, shutil
import sys import sys
from boto.compat import StringIO
class Key(object): class Key(object):
KEY_STREAM_READABLE = 0x01 KEY_STREAM_READABLE = 0x01
@@ -182,7 +184,7 @@ class Key(object):
:returns: The contents of the file as a string :returns: The contents of the file as a string
""" """
fp = StringIO.StringIO() fp = StringIO()
self.get_contents_to_file(fp) self.get_contents_to_file(fp)
return fp.getvalue() return fp.getvalue()

View File

@@ -86,7 +86,7 @@ def needs_caller_reference(func):
def api_action(*api): def api_action(*api):
def decorator(func): def decorator(func):
action = ''.join(api or map(str.capitalize, func.func_name.split('_'))) action = ''.join(api or map(str.capitalize, func.__name__.split('_')))
response = ResponseFactory(action) response = ResponseFactory(action)
if hasattr(boto.fps.response, action + 'Response'): if hasattr(boto.fps.response, action + 'Response'):
response = getattr(boto.fps.response, action + 'Response') response = getattr(boto.fps.response, action + 'Response')

View File

@@ -22,6 +22,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
from decimal import Decimal from decimal import Decimal
from boto.compat import filter, map
def ResponseFactory(action): def ResponseFactory(action):

View File

@@ -19,21 +19,20 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
from __future__ import with_statement
import os import os
import math import math
import threading import threading
import hashlib import hashlib
import time import time
import logging import logging
from Queue import Queue, Empty from boto.compat import Queue
import binascii import binascii
from .utils import DEFAULT_PART_SIZE, minimum_part_size, chunk_hashes, \ from boto.glacier.utils import DEFAULT_PART_SIZE, minimum_part_size, \
tree_hash, bytes_to_hex chunk_hashes, tree_hash, bytes_to_hex
from .exceptions import UploadArchiveError, DownloadArchiveError, \ from boto.glacier.exceptions import UploadArchiveError, \
TreeHashDoesNotMatchError DownloadArchiveError, \
TreeHashDoesNotMatchError
_END_SENTINEL = object() _END_SENTINEL = object()
@@ -68,9 +67,9 @@ class ConcurrentTransferer(object):
def _add_work_items_to_queue(self, total_parts, worker_queue, part_size): def _add_work_items_to_queue(self, total_parts, worker_queue, part_size):
log.debug("Adding work items to queue.") log.debug("Adding work items to queue.")
for i in xrange(total_parts): for i in range(total_parts):
worker_queue.put((i, part_size)) worker_queue.put((i, part_size))
for i in xrange(self._num_threads): for i in range(self._num_threads):
worker_queue.put(_END_SENTINEL) worker_queue.put(_END_SENTINEL)
@@ -146,7 +145,7 @@ class ConcurrentUploader(ConcurrentTransferer):
try: try:
self._wait_for_upload_threads(hash_chunks, result_queue, self._wait_for_upload_threads(hash_chunks, result_queue,
total_parts) total_parts)
except UploadArchiveError, e: except UploadArchiveError as e:
log.debug("An error occurred while uploading an archive, " log.debug("An error occurred while uploading an archive, "
"aborting multipart upload.") "aborting multipart upload.")
self._api.abort_multipart_upload(self._vault_name, upload_id) self._api.abort_multipart_upload(self._vault_name, upload_id)
@@ -159,7 +158,7 @@ class ConcurrentUploader(ConcurrentTransferer):
return response['ArchiveId'] return response['ArchiveId']
def _wait_for_upload_threads(self, hash_chunks, result_queue, total_parts): def _wait_for_upload_threads(self, hash_chunks, result_queue, total_parts):
for _ in xrange(total_parts): for _ in range(total_parts):
result = result_queue.get() result = result_queue.get()
if isinstance(result, Exception): if isinstance(result, Exception):
log.debug("An error was found in the result queue, terminating " log.debug("An error was found in the result queue, terminating "
@@ -177,7 +176,7 @@ class ConcurrentUploader(ConcurrentTransferer):
def _start_upload_threads(self, result_queue, upload_id, worker_queue, def _start_upload_threads(self, result_queue, upload_id, worker_queue,
filename): filename):
log.debug("Starting threads.") log.debug("Starting threads.")
for _ in xrange(self._num_threads): for _ in range(self._num_threads):
thread = UploadWorkerThread(self._api, self._vault_name, filename, thread = UploadWorkerThread(self._api, self._vault_name, filename,
upload_id, worker_queue, result_queue) upload_id, worker_queue, result_queue)
time.sleep(0.2) time.sleep(0.2)
@@ -231,11 +230,11 @@ class UploadWorkerThread(TransferThread):
def _process_chunk(self, work): def _process_chunk(self, work):
result = None result = None
for i in xrange(self._num_retries + 1): for i in range(self._num_retries + 1):
try: try:
result = self._upload_chunk(work) result = self._upload_chunk(work)
break break
except self._retry_exceptions, e: except self._retry_exceptions as e:
log.error("Exception caught uploading part number %s for " log.error("Exception caught uploading part number %s for "
"vault %s, attempt: (%s / %s), filename: %s, " "vault %s, attempt: (%s / %s), filename: %s, "
"exception: %s, msg: %s", "exception: %s, msg: %s",
@@ -306,7 +305,7 @@ class ConcurrentDownloader(ConcurrentTransferer):
self._start_download_threads(result_queue, worker_queue) self._start_download_threads(result_queue, worker_queue)
try: try:
self._wait_for_download_threads(filename, result_queue, total_parts) self._wait_for_download_threads(filename, result_queue, total_parts)
except DownloadArchiveError, e: except DownloadArchiveError as e:
log.debug("An error occurred while downloading an archive: %s", e) log.debug("An error occurred while downloading an archive: %s", e)
raise e raise e
log.debug("Download completed.") log.debug("Download completed.")
@@ -324,7 +323,7 @@ class ConcurrentDownloader(ConcurrentTransferer):
""" """
hash_chunks = [None] * total_parts hash_chunks = [None] * total_parts
with open(filename, "wb") as f: with open(filename, "wb") as f:
for _ in xrange(total_parts): for _ in range(total_parts):
result = result_queue.get() result = result_queue.get()
if isinstance(result, Exception): if isinstance(result, Exception):
log.debug("An error was found in the result queue, " log.debug("An error was found in the result queue, "
@@ -352,7 +351,7 @@ class ConcurrentDownloader(ConcurrentTransferer):
def _start_download_threads(self, result_queue, worker_queue): def _start_download_threads(self, result_queue, worker_queue):
log.debug("Starting threads.") log.debug("Starting threads.")
for _ in xrange(self._num_threads): for _ in range(self._num_threads):
thread = DownloadWorkerThread(self._job, worker_queue, result_queue) thread = DownloadWorkerThread(self._job, worker_queue, result_queue)
time.sleep(0.2) time.sleep(0.2)
thread.start() thread.start()
@@ -393,11 +392,11 @@ class DownloadWorkerThread(TransferThread):
:param work: :param work:
""" """
result = None result = None
for _ in xrange(self._num_retries): for _ in range(self._num_retries):
try: try:
result = self._download_chunk(work) result = self._download_chunk(work)
break break
except self._retry_exceptions, e: except self._retry_exceptions as e:
log.error("Exception caught downloading part number %s for " log.error("Exception caught downloading part number %s for "
"job %s", work[0], self._job,) "job %s", work[0], self._job,)
time.sleep(self._time_between_retries) time.sleep(self._time_between_retries)

View File

@@ -20,12 +20,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
from __future__ import with_statement
import math import math
import socket import socket
from .exceptions import TreeHashDoesNotMatchError, DownloadArchiveError from boto.glacier.exceptions import TreeHashDoesNotMatchError, \
from .utils import tree_hash_from_str DownloadArchiveError
from boto.glacier.utils import tree_hash_from_str
class Job(object): class Job(object):
@@ -146,7 +146,7 @@ class Job(object):
def _download_to_fileob(self, fileobj, num_chunks, chunk_size, verify_hashes, def _download_to_fileob(self, fileobj, num_chunks, chunk_size, verify_hashes,
retry_exceptions): retry_exceptions):
for i in xrange(num_chunks): for i in range(num_chunks):
byte_range = ((i * chunk_size), ((i + 1) * chunk_size) - 1) byte_range = ((i * chunk_size), ((i + 1) * chunk_size) - 1)
data, expected_tree_hash = self._download_byte_range( data, expected_tree_hash = self._download_byte_range(
byte_range, retry_exceptions) byte_range, retry_exceptions)
@@ -163,13 +163,13 @@ class Job(object):
# You can occasionally get socket.errors when downloading # You can occasionally get socket.errors when downloading
# chunks from Glacier, so each chunk can be retried up # chunks from Glacier, so each chunk can be retried up
# to 5 times. # to 5 times.
for _ in xrange(5): for _ in range(5):
try: try:
response = self.get_output(byte_range) response = self.get_output(byte_range)
data = response.read() data = response.read()
expected_tree_hash = response['TreeHash'] expected_tree_hash = response['TreeHash']
return data, expected_tree_hash return data, expected_tree_hash
except retry_exceptions, e: except retry_exceptions as e:
continue continue
else: else:
raise DownloadArchiveError("There was an error downloading" raise DownloadArchiveError("There was an error downloading"

View File

@@ -27,9 +27,9 @@ import os
import boto.glacier import boto.glacier
from boto.compat import json from boto.compat import json
from boto.connection import AWSAuthConnection from boto.connection import AWSAuthConnection
from .exceptions import UnexpectedHTTPResponseError from boto.glacier.exceptions import UnexpectedHTTPResponseError
from .response import GlacierResponse from boto.glacier.response import GlacierResponse
from .utils import ResettingFileSender from boto.glacier.utils import ResettingFileSender
class Layer1(AWSAuthConnection): class Layer1(AWSAuthConnection):
@@ -89,12 +89,13 @@ class Layer1(AWSAuthConnection):
self.region = region self.region = region
self.account_id = account_id self.account_id = account_id
super(Layer1, self).__init__(region.endpoint, super(Layer1, self).__init__(region.endpoint,
aws_access_key_id, aws_secret_access_key, aws_access_key_id, aws_secret_access_key,
is_secure, port, proxy, proxy_port, is_secure, port, proxy, proxy_port,
proxy_user, proxy_pass, debug, proxy_user, proxy_pass, debug,
https_connection_factory, https_connection_factory,
path, provider, security_token, path, provider, security_token,
suppress_consec_slashes, profile_name=profile_name) suppress_consec_slashes,
profile_name=profile_name)
def _required_auth_capability(self): def _required_auth_capability(self):
return ['hmac-v4'] return ['hmac-v4']
@@ -107,10 +108,10 @@ class Layer1(AWSAuthConnection):
headers['x-amz-glacier-version'] = self.Version headers['x-amz-glacier-version'] = self.Version
uri = '/%s/%s' % (self.account_id, resource) uri = '/%s/%s' % (self.account_id, resource)
response = super(Layer1, self).make_request(verb, uri, response = super(Layer1, self).make_request(verb, uri,
params=params, params=params,
headers=headers, headers=headers,
sender=sender, sender=sender,
data=data) data=data)
if response.status in ok_responses: if response.status in ok_responses:
return GlacierResponse(response, response_headers) return GlacierResponse(response, response_headers)
else: else:
@@ -826,9 +827,9 @@ class Layer1(AWSAuthConnection):
else: else:
sender = None sender = None
return self.make_request('POST', uri, headers=headers, return self.make_request('POST', uri, headers=headers,
sender=sender, sender=sender,
data=archive, ok_responses=(201,), data=archive, ok_responses=(201,),
response_headers=response_headers) response_headers=response_headers)
def _is_file_like(self, archive): def _is_file_like(self, archive):
return hasattr(archive, 'seek') and hasattr(archive, 'tell') return hasattr(archive, 'seek') and hasattr(archive, 'tell')

View File

@@ -21,8 +21,8 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
from .layer1 import Layer1 from boto.glacier.layer1 import Layer1
from .vault import Vault from boto.glacier.vault import Vault
class Layer2(object): class Layer2(object):

View File

@@ -37,7 +37,7 @@ class GlacierResponse(dict):
for header_name, item_name in response_headers: for header_name, item_name in response_headers:
self[item_name] = http_response.getheader(header_name) self[item_name] = http_response.getheader(header_name)
if http_response.getheader('Content-Type') == 'application/json': if http_response.getheader('Content-Type') == 'application/json':
body = json.loads(http_response.read()) body = json.loads(http_response.read().decode('utf-8'))
self.update(body) self.update(body)
size = http_response.getheader('Content-Length', None) size = http_response.getheader('Content-Length', None)
if size is not None: if size is not None:

View File

@@ -21,6 +21,9 @@
# #
import hashlib import hashlib
import math import math
import binascii
from boto.compat import six
_MEGABYTE = 1024 * 1024 _MEGABYTE = 1024 * 1024
@@ -71,12 +74,12 @@ def minimum_part_size(size_in_bytes, default_part_size=DEFAULT_PART_SIZE):
def chunk_hashes(bytestring, chunk_size=_MEGABYTE): def chunk_hashes(bytestring, chunk_size=_MEGABYTE):
chunk_count = int(math.ceil(len(bytestring) / float(chunk_size))) chunk_count = int(math.ceil(len(bytestring) / float(chunk_size)))
hashes = [] hashes = []
for i in xrange(chunk_count): for i in range(chunk_count):
start = i * chunk_size start = i * chunk_size
end = (i + 1) * chunk_size end = (i + 1) * chunk_size
hashes.append(hashlib.sha256(bytestring[start:end]).digest()) hashes.append(hashlib.sha256(bytestring[start:end]).digest())
if not hashes: if not hashes:
return [hashlib.sha256('').digest()] return [hashlib.sha256(b'').digest()]
return hashes return hashes
@@ -121,20 +124,29 @@ def compute_hashes_from_fileobj(fileobj, chunk_size=1024 * 1024):
are returned in hex. are returned in hex.
""" """
# Python 3+, not binary
if six.PY3 and hasattr(fileobj, 'mode') and 'b' not in fileobj.mode:
raise ValueError('File-like object must be opened in binary mode!')
linear_hash = hashlib.sha256() linear_hash = hashlib.sha256()
chunks = [] chunks = []
chunk = fileobj.read(chunk_size) chunk = fileobj.read(chunk_size)
while chunk: while chunk:
# It's possible to get a file-like object that has no mode (checked
# above) and returns something other than bytes (e.g. str). So here
# we try to catch that and encode to bytes.
if not isinstance(chunk, bytes):
chunk = chunk.encode(getattr(fileobj, 'encoding', '') or 'utf-8')
linear_hash.update(chunk) linear_hash.update(chunk)
chunks.append(hashlib.sha256(chunk).digest()) chunks.append(hashlib.sha256(chunk).digest())
chunk = fileobj.read(chunk_size) chunk = fileobj.read(chunk_size)
if not chunks: if not chunks:
chunks = [hashlib.sha256('').digest()] chunks = [hashlib.sha256(b'').digest()]
return linear_hash.hexdigest(), bytes_to_hex(tree_hash(chunks)) return linear_hash.hexdigest(), bytes_to_hex(tree_hash(chunks))
def bytes_to_hex(str_as_bytes): def bytes_to_hex(str_as_bytes):
return ''.join(["%02x" % ord(x) for x in str_as_bytes]).strip() return binascii.hexlify(str_as_bytes)
def tree_hash_from_str(str_as_bytes): def tree_hash_from_str(str_as_bytes):

View File

@@ -21,12 +21,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
from __future__ import with_statement import codecs
from .exceptions import UploadArchiveError from boto.glacier.exceptions import UploadArchiveError
from .job import Job from boto.glacier.job import Job
from .writer import compute_hashes_from_fileobj, resume_file_upload, Writer from boto.glacier.writer import compute_hashes_from_fileobj, \
from .concurrent import ConcurrentUploader resume_file_upload, Writer
from .utils import minimum_part_size, DEFAULT_PART_SIZE from boto.glacier.concurrent import ConcurrentUploader
from boto.glacier.utils import minimum_part_size, DEFAULT_PART_SIZE
import os.path import os.path
@@ -54,8 +55,6 @@ class Vault(object):
if response_data: if response_data:
for response_name, attr_name, default in self.ResponseDataElements: for response_name, attr_name, default in self.ResponseDataElements:
value = response_data[response_name] value = response_data[response_name]
if isinstance(value, unicode):
value = value.encode('utf8')
setattr(self, attr_name, value) setattr(self, attr_name, value)
else: else:
for response_name, attr_name, default in self.ResponseDataElements: for response_name, attr_name, default in self.ResponseDataElements:
@@ -227,7 +226,7 @@ class Vault(object):
for part_desc in part_list_response['Parts']: for part_desc in part_list_response['Parts']:
part_index = self._range_string_to_part_index( part_index = self._range_string_to_part_index(
part_desc['RangeInBytes'], part_size) part_desc['RangeInBytes'], part_size)
part_tree_hash = part_desc['SHA256TreeHash'].decode('hex') part_tree_hash = codecs.decode(part_desc['SHA256TreeHash'], 'hex_codec')
part_hash_map[part_index] = part_tree_hash part_hash_map[part_index] = part_tree_hash
if not file_obj: if not file_obj:
@@ -343,9 +342,9 @@ class Vault(object):
rparams = {} rparams = {}
if start_date is not None: if start_date is not None:
rparams['StartDate'] = start_date.isoformat() rparams['StartDate'] = start_date.strftime('%Y-%m-%dT%H:%M:%S%Z')
if end_date is not None: if end_date is not None:
rparams['EndDate'] = end_date.isoformat() rparams['EndDate'] = end_date.strftime('%Y-%m-%dT%H:%M:%S%Z')
if limit is not None: if limit is not None:
rparams['Limit'] = limit rparams['Limit'] = limit

View File

@@ -53,7 +53,7 @@ class _Partitioner(object):
self._buffer_size = 0 self._buffer_size = 0
def write(self, data): def write(self, data):
if data == '': if data == b'':
return return
self._buffer.append(data) self._buffer.append(data)
self._buffer_size += len(data) self._buffer_size += len(data)
@@ -61,7 +61,7 @@ class _Partitioner(object):
self._send_part() self._send_part()
def _send_part(self): def _send_part(self):
data = ''.join(self._buffer) data = b''.join(self._buffer)
# Put back any data remaining over the part size into the # Put back any data remaining over the part size into the
# buffer # buffer
if len(data) > self.part_size: if len(data) > self.part_size:
@@ -164,7 +164,7 @@ class _Uploader(object):
def generate_parts_from_fobj(fobj, part_size): def generate_parts_from_fobj(fobj, part_size):
data = fobj.read(part_size) data = fobj.read(part_size)
while data: while data:
yield data yield data.encode('utf-8')
data = fobj.read(part_size) data = fobj.read(part_size)

View File

@@ -37,6 +37,7 @@ from boto.gs.key import Key as GSKey
from boto.s3.acl import Policy from boto.s3.acl import Policy
from boto.s3.bucket import Bucket as S3Bucket from boto.s3.bucket import Bucket as S3Bucket
from boto.utils import get_utf8_value from boto.utils import get_utf8_value
from boto.compat import six
# constants for http query args # constants for http query args
DEF_OBJ_ACL = 'defaultObjectAcl' DEF_OBJ_ACL = 'defaultObjectAcl'
@@ -100,12 +101,12 @@ class Bucket(S3Bucket):
if generation: if generation:
query_args_l.append('generation=%s' % generation) query_args_l.append('generation=%s' % generation)
if response_headers: if response_headers:
for rk, rv in response_headers.iteritems(): for rk, rv in six.iteritems(response_headers):
query_args_l.append('%s=%s' % (rk, urllib.quote(rv))) query_args_l.append('%s=%s' % (rk, urllib.quote(rv)))
try: try:
key, resp = self._get_key_internal(key_name, headers, key, resp = self._get_key_internal(key_name, headers,
query_args_l=query_args_l) query_args_l=query_args_l)
except GSResponseError, e: except GSResponseError as e:
if e.status == 403 and 'Forbidden' in e.reason: if e.status == 403 and 'Forbidden' in e.reason:
# If we failed getting an object, let the user know which object # If we failed getting an object, let the user know which object
# failed rather than just returning a generic 403. # failed rather than just returning a generic 403.

View File

@@ -156,7 +156,7 @@ class Cors(handler.ContentHandler):
s += '<' + collection + '>' s += '<' + collection + '>'
# If collection elements has type string, append atomic value, # If collection elements has type string, append atomic value,
# otherwise, append sequence of values in named tags. # otherwise, append sequence of values in named tags.
if isinstance(elements_or_value, types.StringTypes): if isinstance(elements_or_value, str):
s += elements_or_value s += elements_or_value
else: else:
for (name, value) in elements_or_value: for (name, value) in elements_or_value:

View File

@@ -23,7 +23,8 @@ import base64
import binascii import binascii
import os import os
import re import re
import StringIO
from boto.compat import StringIO
from boto.exception import BotoClientError from boto.exception import BotoClientError
from boto.s3.key import Key as S3Key from boto.s3.key import Key as S3Key
from boto.s3.keyfile import KeyFile from boto.s3.keyfile import KeyFile
@@ -410,19 +411,20 @@ class Key(S3Key):
contents. contents.
:type fp: file :type fp: file
:param fp: the file whose contents are to be uploaded :param fp: The file whose contents are to be uploaded.
:type headers: dict :type headers: dict
:param headers: additional HTTP headers to be sent with the PUT request. :param headers: (optional) Additional HTTP headers to be sent with the
PUT request.
:type replace: bool :type replace: bool
:param replace: If this parameter is False, the method will first check :param replace: (optional) If this parameter is False, the method will
to see if an object exists in the bucket with the same key. If it first check to see if an object exists in the bucket with the same
does, it won't overwrite it. The default value is True which will key. If it does, it won't overwrite it. The default value is True
overwrite the object. which will overwrite the object.
:type cb: function :type cb: function
:param cb: a callback function that will be called to report :param cb: (optional) Callback function that will be called to report
progress on the upload. The callback should accept two integer progress on the upload. The callback should accept two integer
parameters, the first representing the number of bytes that have parameters, the first representing the number of bytes that have
been successfully transmitted to GS and the second representing the been successfully transmitted to GS and the second representing the
@@ -435,43 +437,44 @@ class Key(S3Key):
during the file transfer. during the file transfer.
:type policy: :class:`boto.gs.acl.CannedACLStrings` :type policy: :class:`boto.gs.acl.CannedACLStrings`
:param policy: A canned ACL policy that will be applied to the new key :param policy: (optional) A canned ACL policy that will be applied to
in GS. the new key in GS.
:type md5: A tuple containing the hexdigest version of the MD5 checksum :type md5: tuple
of the file as the first element and the Base64-encoded version of :param md5: (optional) A tuple containing the hexdigest version of the
the plain checksum as the second element. This is the same format MD5 checksum of the file as the first element and the
returned by the compute_md5 method. Base64-encoded version of the plain checksum as the second element.
:param md5: If you need to compute the MD5 for any reason prior to This is the same format returned by the compute_md5 method.
upload, it's silly to have to do it twice so this param, if present,
will be used as the MD5 values of the file. Otherwise, the checksum
will be computed.
:type res_upload_handler: ResumableUploadHandler If you need to compute the MD5 for any reason prior to upload, it's
:param res_upload_handler: If provided, this handler will perform the silly to have to do it twice so this param, if present, will be
upload. used as the MD5 values of the file. Otherwise, the checksum will be
computed.
:type res_upload_handler: :py:class:`boto.gs.resumable_upload_handler.ResumableUploadHandler`
:param res_upload_handler: (optional) If provided, this handler will
perform the upload.
:type size: int :type size: int
:param size: (optional) The Maximum number of bytes to read from :param size: (optional) The Maximum number of bytes to read from the
the file pointer (fp). This is useful when uploading file pointer (fp). This is useful when uploading a file in multiple
a file in multiple parts where you are splitting the parts where you are splitting the file up into different ranges to
file up into different ranges to be uploaded. If not be uploaded. If not specified, the default behaviour is to read all
specified, the default behaviour is to read all bytes bytes from the file pointer. Less bytes may be available.
from the file pointer. Less bytes may be available.
Notes: Notes:
1. The "size" parameter currently cannot be used when 1. The "size" parameter currently cannot be used when a
a resumable upload handler is given but is still resumable upload handler is given but is still useful for
useful for uploading part of a file as implemented uploading part of a file as implemented by the parent class.
by the parent class. 2. At present Google Cloud Storage does not support multipart
2. At present Google Cloud Storage does not support uploads.
multipart uploads.
:type rewind: bool :type rewind: bool
:param rewind: (optional) If True, the file pointer (fp) will be :param rewind: (optional) If True, the file pointer (fp) will be
rewound to the start before any bytes are read from rewound to the start before any bytes are read from it. The default
it. The default behaviour is False which reads from behaviour is False which reads from the current position of the
the current position of the file pointer (fp). file pointer (fp).
:type if_generation: int :type if_generation: int
:param if_generation: (optional) If set to a generation number, the :param if_generation: (optional) If set to a generation number, the
@@ -588,44 +591,47 @@ class Key(S3Key):
parameters. parameters.
:type filename: string :type filename: string
:param filename: The name of the file that you want to put onto GS :param filename: The name of the file that you want to put onto GS.
:type headers: dict :type headers: dict
:param headers: Additional headers to pass along with the request to GS. :param headers: (optional) Additional headers to pass along with the
request to GS.
:type replace: bool :type replace: bool
:param replace: If True, replaces the contents of the file if it :param replace: (optional) If True, replaces the contents of the file
already exists. if it already exists.
:type cb: function :type cb: function
:param cb: (optional) a callback function that will be called to report :param cb: (optional) Callback function that will be called to report
progress on the download. The callback should accept two integer progress on the upload. The callback should accept two integer
parameters, the first representing the number of bytes that have parameters, the first representing the number of bytes that have
been successfully transmitted from GS and the second representing been successfully transmitted to GS and the second representing the
the total number of bytes that need to be transmitted. total number of bytes that need to be transmitted.
:type cb: int :type num_cb: int
:param num_cb: (optional) If a callback is specified with the cb :param num_cb: (optional) If a callback is specified with the cb
parameter this parameter determines the granularity of the callback parameter this parameter determines the granularity of the callback
by defining the maximum number of times the callback will be called by defining the maximum number of times the callback will be called
during the file transfer. during the file transfer.
:type policy: :class:`boto.gs.acl.CannedACLStrings` :type policy: :py:attribute:`boto.gs.acl.CannedACLStrings`
:param policy: A canned ACL policy that will be applied to the new key :param policy: (optional) A canned ACL policy that will be applied to
in GS. the new key in GS.
:type md5: A tuple containing the hexdigest version of the MD5 checksum :type md5: tuple
of the file as the first element and the Base64-encoded version of :param md5: (optional) A tuple containing the hexdigest version of the
the plain checksum as the second element. This is the same format MD5 checksum of the file as the first element and the
returned by the compute_md5 method. Base64-encoded version of the plain checksum as the second element.
:param md5: If you need to compute the MD5 for any reason prior to This is the same format returned by the compute_md5 method.
upload, it's silly to have to do it twice so this param, if present,
will be used as the MD5 values of the file. Otherwise, the checksum
will be computed.
:type res_upload_handler: ResumableUploadHandler If you need to compute the MD5 for any reason prior to upload, it's
:param res_upload_handler: If provided, this handler will perform the silly to have to do it twice so this param, if present, will be
upload. used as the MD5 values of the file. Otherwise, the checksum will be
computed.
:type res_upload_handler: :py:class:`boto.gs.resumable_upload_handler.ResumableUploadHandler`
:param res_upload_handler: (optional) If provided, this handler will
perform the upload.
:type if_generation: int :type if_generation: int
:param if_generation: (optional) If set to a generation number, the :param if_generation: (optional) If set to a generation number, the
@@ -699,7 +705,7 @@ class Key(S3Key):
self.md5 = None self.md5 = None
self.base64md5 = None self.base64md5 = None
fp = StringIO.StringIO(get_utf8_value(s)) fp = StringIO(get_utf8_value(s))
r = self.set_contents_from_file(fp, headers, replace, cb, num_cb, r = self.set_contents_from_file(fp, headers, replace, cb, num_cb,
policy, md5, policy, md5,
if_generation=if_generation) if_generation=if_generation)

View File

@@ -18,7 +18,6 @@
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 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 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
import errno import errno
import httplib import httplib
import os import os
@@ -27,16 +26,13 @@ import re
import socket import socket
import time import time
import urlparse import urlparse
from hashlib import md5
from boto import config, UserAgent from boto import config, UserAgent
from boto.connection import AWSAuthConnection from boto.connection import AWSAuthConnection
from boto.exception import InvalidUriError from boto.exception import InvalidUriError
from boto.exception import ResumableTransferDisposition from boto.exception import ResumableTransferDisposition
from boto.exception import ResumableUploadException from boto.exception import ResumableUploadException
from boto.s3.keyfile import KeyFile from boto.s3.keyfile import KeyFile
try:
from hashlib import md5
except ImportError:
from md5 import md5
""" """
Handler for Google Cloud Storage resumable uploads. See Handler for Google Cloud Storage resumable uploads. See
@@ -98,7 +94,7 @@ class ResumableUploadHandler(object):
f = open(self.tracker_file_name, 'r') f = open(self.tracker_file_name, 'r')
uri = f.readline().strip() uri = f.readline().strip()
self._set_tracker_uri(uri) self._set_tracker_uri(uri)
except IOError, e: except IOError as e:
# Ignore non-existent file (happens first time an upload # Ignore non-existent file (happens first time an upload
# is attempted on a file), but warn user for other errors. # is attempted on a file), but warn user for other errors.
if e.errno != errno.ENOENT: if e.errno != errno.ENOENT:
@@ -106,7 +102,7 @@ class ResumableUploadHandler(object):
print('Couldn\'t read URI tracker file (%s): %s. Restarting ' print('Couldn\'t read URI tracker file (%s): %s. Restarting '
'upload from scratch.' % 'upload from scratch.' %
(self.tracker_file_name, e.strerror)) (self.tracker_file_name, e.strerror))
except InvalidUriError, e: except InvalidUriError as e:
# Warn user, but proceed (will restart because # Warn user, but proceed (will restart because
# self.tracker_uri is None). # self.tracker_uri is None).
print('Invalid tracker URI (%s) found in URI tracker file ' print('Invalid tracker URI (%s) found in URI tracker file '
@@ -125,9 +121,9 @@ class ResumableUploadHandler(object):
f = None f = None
try: try:
with os.fdopen(os.open(self.tracker_file_name, with os.fdopen(os.open(self.tracker_file_name,
os.O_WRONLY | os.O_CREAT, 0600), 'w') as f: os.O_WRONLY | os.O_CREAT, 0o600), 'w') as f:
f.write(self.tracker_uri) f.write(self.tracker_uri)
except IOError, e: except IOError as e:
raise ResumableUploadException( raise ResumableUploadException(
'Couldn\'t write URI tracker file (%s): %s.\nThis can happen' 'Couldn\'t write URI tracker file (%s): %s.\nThis can happen'
'if you\'re using an incorrectly configured upload tool\n' 'if you\'re using an incorrectly configured upload tool\n'
@@ -256,7 +252,7 @@ class ResumableUploadHandler(object):
'Couldn\'t parse upload server state query response (%s)' % 'Couldn\'t parse upload server state query response (%s)' %
str(resp.getheaders()), ResumableTransferDisposition.START_OVER) str(resp.getheaders()), ResumableTransferDisposition.START_OVER)
if conn.debug >= 1: if conn.debug >= 1:
print 'Server has: Range: %d - %d.' % (server_start, server_end) print('Server has: Range: %d - %d.' % (server_start, server_end))
return (server_start, server_end) return (server_start, server_end)
def _start_new_resumable_upload(self, key, headers=None): def _start_new_resumable_upload(self, key, headers=None):
@@ -267,7 +263,7 @@ class ResumableUploadHandler(object):
""" """
conn = key.bucket.connection conn = key.bucket.connection
if conn.debug >= 1: if conn.debug >= 1:
print 'Starting new resumable upload.' print('Starting new resumable upload.')
self.server_has_bytes = 0 self.server_has_bytes = 0
# Start a new resumable upload by sending a POST request with an # Start a new resumable upload by sending a POST request with an
@@ -433,7 +429,7 @@ class ResumableUploadHandler(object):
# If the server already has some of the content, we need to # If the server already has some of the content, we need to
# update the digesters with the bytes that have already been # update the digesters with the bytes that have already been
# uploaded to ensure we get a complete hash in the end. # uploaded to ensure we get a complete hash in the end.
print 'Catching up hash digest(s) for resumed upload' print('Catching up hash digest(s) for resumed upload')
fp.seek(0) fp.seek(0)
# Read local file's bytes through position server has. For # Read local file's bytes through position server has. For
# example, if server has (0, 3) we want to read 3-0+1=4 bytes. # example, if server has (0, 3) we want to read 3-0+1=4 bytes.
@@ -453,10 +449,10 @@ class ResumableUploadHandler(object):
bytes_to_go -= len(chunk) bytes_to_go -= len(chunk)
if conn.debug >= 1: if conn.debug >= 1:
print 'Resuming transfer.' print('Resuming transfer.')
except ResumableUploadException, e: except ResumableUploadException as e:
if conn.debug >= 1: if conn.debug >= 1:
print 'Unable to resume transfer (%s).' % e.message print('Unable to resume transfer (%s).' % e.message)
self._start_new_resumable_upload(key, headers) self._start_new_resumable_upload(key, headers)
else: else:
self._start_new_resumable_upload(key, headers) self._start_new_resumable_upload(key, headers)
@@ -513,7 +509,7 @@ class ResumableUploadHandler(object):
change some of the file and not realize they have inconsistent data. change some of the file and not realize they have inconsistent data.
""" """
if key.bucket.connection.debug >= 1: if key.bucket.connection.debug >= 1:
print 'Checking md5 against etag.' print('Checking md5 against etag.')
if key.md5 != etag.strip('"\''): if key.md5 != etag.strip('"\''):
# Call key.open_read() before attempting to delete the # Call key.open_read() before attempting to delete the
# (incorrect-content) key, so we perform that request on a # (incorrect-content) key, so we perform that request on a
@@ -567,7 +563,7 @@ class ResumableUploadHandler(object):
# Use binary exponential backoff to desynchronize client requests. # Use binary exponential backoff to desynchronize client requests.
sleep_time_secs = random.random() * (2**self.progress_less_iterations) sleep_time_secs = random.random() * (2**self.progress_less_iterations)
if debug >= 1: if debug >= 1:
print ('Got retryable failure (%d progress-less in a row).\n' print('Got retryable failure (%d progress-less in a row).\n'
'Sleeping %3.1f seconds before re-trying' % 'Sleeping %3.1f seconds before re-trying' %
(self.progress_less_iterations, sleep_time_secs)) (self.progress_less_iterations, sleep_time_secs))
time.sleep(sleep_time_secs) time.sleep(sleep_time_secs)
@@ -664,9 +660,9 @@ class ResumableUploadHandler(object):
self._check_final_md5(key, etag) self._check_final_md5(key, etag)
key.generation = self.generation key.generation = self.generation
if debug >= 1: if debug >= 1:
print 'Resumable upload complete.' print('Resumable upload complete.')
return return
except self.RETRYABLE_EXCEPTIONS, e: except self.RETRYABLE_EXCEPTIONS as e:
if debug >= 1: if debug >= 1:
print('Caught exception (%s)' % e.__repr__()) print('Caught exception (%s)' % e.__repr__())
if isinstance(e, IOError) and e.errno == errno.EPIPE: if isinstance(e, IOError) and e.errno == errno.EPIPE:
@@ -676,7 +672,7 @@ class ResumableUploadHandler(object):
# the upload (which will cause a new connection to be # the upload (which will cause a new connection to be
# opened the next time an HTTP request is sent). # opened the next time an HTTP request is sent).
key.bucket.connection.connection.close() key.bucket.connection.connection.close()
except ResumableUploadException, e: except ResumableUploadException as e:
self.handle_resumable_upload_exception(e, debug) self.handle_resumable_upload_exception(e, debug)
self.track_progress_less_iterations(server_had_bytes_before_attempt, self.track_progress_less_iterations(server_had_bytes_before_attempt,

View File

@@ -19,9 +19,10 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
import StringIO
import xml.sax import xml.sax
from boto.compat import StringIO
class XmlHandler(xml.sax.ContentHandler): class XmlHandler(xml.sax.ContentHandler):
def __init__(self, root_node, connection): def __init__(self, root_node, connection):
@@ -55,4 +56,4 @@ class XmlHandlerWrapper(object):
self.parser.setFeature(xml.sax.handler.feature_external_ges, 0) self.parser.setFeature(xml.sax.handler.feature_external_ges, 0)
def parseString(self, content): def parseString(self, content):
return self.parser.parse(StringIO.StringIO(content)) return self.parser.parse(StringIO(content))

View File

@@ -19,14 +19,15 @@
"""Extensions to allow HTTPS requests with SSL certificate validation.""" """Extensions to allow HTTPS requests with SSL certificate validation."""
import httplib
import re import re
import socket import socket
import ssl import ssl
import boto import boto
class InvalidCertificateException(httplib.HTTPException): from boto.compat import six, http_client
class InvalidCertificateException(http_client.HTTPException):
"""Raised when a certificate is provided with an invalid hostname.""" """Raised when a certificate is provided with an invalid hostname."""
def __init__(self, host, cert, reason): def __init__(self, host, cert, reason):
@@ -36,7 +37,7 @@ class InvalidCertificateException(httplib.HTTPException):
host: The hostname the connection was made to. host: The hostname the connection was made to.
cert: The SSL certificate (as a dictionary) the host returned. cert: The SSL certificate (as a dictionary) the host returned.
""" """
httplib.HTTPException.__init__(self) http_client.HTTPException.__init__(self)
self.host = host self.host = host
self.cert = cert self.cert = cert
self.reason = reason self.reason = reason
@@ -79,10 +80,10 @@ def ValidateCertificateHostname(cert, hostname):
return False return False
class CertValidatingHTTPSConnection(httplib.HTTPConnection): class CertValidatingHTTPSConnection(http_client.HTTPConnection):
"""An HTTPConnection that connects over SSL and validates certificates.""" """An HTTPConnection that connects over SSL and validates certificates."""
default_port = httplib.HTTPS_PORT default_port = http_client.HTTPS_PORT
def __init__(self, host, port=default_port, key_file=None, cert_file=None, def __init__(self, host, port=default_port, key_file=None, cert_file=None,
ca_certs=None, strict=None, **kwargs): ca_certs=None, strict=None, **kwargs):
@@ -98,17 +99,23 @@ class CertValidatingHTTPSConnection(httplib.HTTPConnection):
strict: When true, causes BadStatusLine to be raised if the status line strict: When true, causes BadStatusLine to be raised if the status line
can't be parsed as a valid HTTP/1.0 or 1.1 status line. can't be parsed as a valid HTTP/1.0 or 1.1 status line.
""" """
httplib.HTTPConnection.__init__(self, host, port, strict, **kwargs) if six.PY2:
# Python 3.2 and newer have deprecated and removed the strict
# parameter. Since the params are supported as keyword arguments
# we conditionally add it here.
kwargs['strict'] = strict
http_client.HTTPConnection.__init__(self, host=host, port=port, **kwargs)
self.key_file = key_file self.key_file = key_file
self.cert_file = cert_file self.cert_file = cert_file
self.ca_certs = ca_certs self.ca_certs = ca_certs
def connect(self): def connect(self):
"Connect to a host on a given (SSL) port." "Connect to a host on a given (SSL) port."
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if hasattr(self, "timeout"):
if hasattr(self, "timeout") and self.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: sock = socket.create_connection((self.host, self.port), self.timeout)
sock.settimeout(self.timeout) else:
sock.connect((self.host, self.port)) sock = socket.create_connection((self.host, self.port))
msg = "wrapping ssl socket; " msg = "wrapping ssl socket; "
if self.ca_certs: if self.ca_certs:
msg += "CA certificate file=%s" %self.ca_certs msg += "CA certificate file=%s" %self.ca_certs

View File

@@ -21,16 +21,37 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
import boto import boto
import boto.jsonresponse import boto.jsonresponse
from boto.compat import json from boto.compat import json, six
from boto.resultset import ResultSet from boto.resultset import ResultSet
from boto.iam.summarymap import SummaryMap from boto.iam.summarymap import SummaryMap
from boto.connection import AWSQueryConnection from boto.connection import AWSQueryConnection
DEFAULT_POLICY_DOCUMENTS = {
ASSUME_ROLE_POLICY_DOCUMENT = json.dumps({ 'default': {
'Statement': [{'Principal': {'Service': ['ec2.amazonaws.com']}, 'Statement': [
'Effect': 'Allow', {
'Action': ['sts:AssumeRole']}]}) 'Principal': {
'Service': ['ec2.amazonaws.com']
},
'Effect': 'Allow',
'Action': ['sts:AssumeRole']
}
]
},
'amazonaws.com.cn': {
'Statement': [
{
'Principal': {
'Service': ['ec2.amazonaws.com.cn']
},
'Effect': 'Allow',
'Action': ['sts:AssumeRole']
}
]
},
}
# For backward-compatibility, we'll preserve this here.
ASSUME_ROLE_POLICY_DOCUMENT = json.dumps(DEFAULT_POLICY_DOCUMENTS['default'])
class IAMConnection(AWSQueryConnection): class IAMConnection(AWSQueryConnection):
@@ -1006,13 +1027,35 @@ class IAMConnection(AWSQueryConnection):
:param service: Default service to go to in the console. :param service: Default service to go to in the console.
""" """
alias = self.get_account_alias() alias = self.get_account_alias()
if not alias: if not alias:
raise Exception('No alias associated with this account. Please use iam.create_account_alias() first.') raise Exception('No alias associated with this account. Please use iam.create_account_alias() first.')
resp = alias.get('list_account_aliases_response', {})
result = resp.get('list_account_aliases_result', {})
aliases = result.get('account_aliases', [])
if not len(aliases):
raise Exception('No alias associated with this account. Please use iam.create_account_alias() first.')
# We'll just use the first one we find.
alias = aliases[0]
if self.host == 'iam.us-gov.amazonaws.com': if self.host == 'iam.us-gov.amazonaws.com':
return "https://%s.signin.amazonaws-us-gov.com/console/%s" % (alias, service) return "https://%s.signin.amazonaws-us-gov.com/console/%s" % (
alias,
service
)
elif self.host.endswith('amazonaws.com.cn'):
return "https://%s.signin.amazonaws.cn/console/%s" % (
alias,
service
)
else: else:
return "https://%s.signin.aws.amazon.com/console/%s" % (alias, service) return "https://%s.signin.aws.amazon.com/console/%s" % (
alias,
service
)
def get_account_summary(self): def get_account_summary(self):
""" """
@@ -1059,6 +1102,30 @@ class IAMConnection(AWSQueryConnection):
params['Path'] = path params['Path'] = path
return self.get_response('CreateInstanceProfile', params) return self.get_response('CreateInstanceProfile', params)
def _build_policy(self, assume_role_policy_document=None):
if assume_role_policy_document is not None:
if isinstance(assume_role_policy_document, six.string_types):
# Historically, they had to pass a string. If it's a string,
# assume the user has already handled it.
return assume_role_policy_document
else:
for tld, policy in DEFAULT_POLICY_DOCUMENTS.items():
if tld is 'default':
# Skip the default. We'll fall back to it if we don't find
# anything.
continue
if self.host and self.host.endswith(tld):
assume_role_policy_document = policy
break
if not assume_role_policy_document:
assume_role_policy_document = DEFAULT_POLICY_DOCUMENTS['default']
# Dump the policy (either user-supplied ``dict`` or one of the defaults)
return json.dumps(assume_role_policy_document)
def create_role(self, role_name, assume_role_policy_document=None, path=None): def create_role(self, role_name, assume_role_policy_document=None, path=None):
""" """
Creates a new role for your AWS account. Creates a new role for your AWS account.
@@ -1070,21 +1137,19 @@ class IAMConnection(AWSQueryConnection):
:type role_name: string :type role_name: string
:param role_name: Name of the role to create. :param role_name: Name of the role to create.
:type assume_role_policy_document: string :type assume_role_policy_document: ``string`` or ``dict``
:param assume_role_policy_document: The policy that grants an entity :param assume_role_policy_document: The policy that grants an entity
permission to assume the role. permission to assume the role.
:type path: string :type path: string
:param path: The path to the instance profile. :param path: The path to the role.
""" """
params = {'RoleName': role_name} params = {
if assume_role_policy_document is None: 'RoleName': role_name,
# This is the only valid assume_role_policy_document currently, so 'AssumeRolePolicyDocument': self._build_policy(
# this is used as a default value if no assume_role_policy_document assume_role_policy_document
# is provided. ),
params['AssumeRolePolicyDocument'] = ASSUME_ROLE_POLICY_DOCUMENT }
else:
params['AssumeRolePolicyDocument'] = assume_role_policy_document
if path is not None: if path is not None:
params['Path'] = path params['Path'] = path
return self.get_response('CreateRole', params) return self.get_response('CreateRole', params)
@@ -1375,7 +1440,7 @@ class IAMConnection(AWSQueryConnection):
Lists the SAML providers in the account. Lists the SAML providers in the account.
This operation requires `Signature Version 4`_. This operation requires `Signature Version 4`_.
""" """
return self.get_response('ListSAMLProviders', {}) return self.get_response('ListSAMLProviders', {}, list_marker='SAMLProviderList')
def get_saml_provider(self, saml_provider_arn): def get_saml_provider(self, saml_provider_arn):
""" """

View File

@@ -21,7 +21,7 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
import xml.sax import xml.sax
import utils from boto import utils
class XmlHandler(xml.sax.ContentHandler): class XmlHandler(xml.sax.ContentHandler):
@@ -49,6 +49,8 @@ class XmlHandler(xml.sax.ContentHandler):
self.current_text += content self.current_text += content
def parse(self, s): def parse(self, s):
if not isinstance(s, bytes):
s = s.encode('utf-8')
xml.sax.parseString(s, self) xml.sax.parseString(s, self)
class Element(dict): class Element(dict):

View File

@@ -20,11 +20,6 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
# #
try:
import json
except ImportError:
import simplejson as json
import base64 import base64
import boto import boto
@@ -32,6 +27,7 @@ from boto.connection import AWSQueryConnection
from boto.regioninfo import RegionInfo from boto.regioninfo import RegionInfo
from boto.exception import JSONResponseError from boto.exception import JSONResponseError
from boto.kinesis import exceptions from boto.kinesis import exceptions
from boto.compat import json
class KinesisConnection(AWSQueryConnection): class KinesisConnection(AWSQueryConnection):
@@ -293,7 +289,8 @@ class KinesisConnection(AWSQueryConnection):
# Base64 decode the data # Base64 decode the data
if b64_decode: if b64_decode:
for record in response.get('Records', []): for record in response.get('Records', []):
record['Data'] = base64.b64decode(record['Data']) record['Data'] = base64.b64decode(
record['Data'].encode('utf-8')).decode('utf-8')
return response return response
@@ -594,7 +591,8 @@ class KinesisConnection(AWSQueryConnection):
if sequence_number_for_ordering is not None: if sequence_number_for_ordering is not None:
params['SequenceNumberForOrdering'] = sequence_number_for_ordering params['SequenceNumberForOrdering'] = sequence_number_for_ordering
if b64_encode: if b64_encode:
params['Data'] = base64.b64encode(params['Data']) params['Data'] = base64.b64encode(
params['Data'].encode('utf-8')).decode('utf-8')
return self.make_request(action='PutRecord', return self.make_request(action='PutRecord',
body=json.dumps(params)) body=json.dumps(params))
@@ -695,7 +693,7 @@ class KinesisConnection(AWSQueryConnection):
headers=headers, data=body) headers=headers, data=body)
response = self._mexe(http_request, sender=None, response = self._mexe(http_request, sender=None,
override_num_retries=10) override_num_retries=10)
response_body = response.read() response_body = response.read().decode('utf-8')
boto.log.debug(response.getheaders()) boto.log.debug(response.getheaders())
boto.log.debug(response_body) boto.log.debug(response_body)
if response.status == 200: if response.status == 200:

View File

@@ -0,0 +1,41 @@
# Copyright (c) 2014 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.
#
from boto.regioninfo import get_regions
def regions():
"""
Get all available regions for the CloudWatch Logs service.
:rtype: list
:return: A list of :class:`boto.regioninfo.RegionInfo`
"""
from boto.logs.layer1 import CloudWatchLogsConnection
return get_regions('logs', connection_cls=CloudWatchLogsConnection)
def connect_to_region(region_name, **kw_params):
for region in regions():
if region.name == region_name:
return region.connect(**kw_params)
return None

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2012 Thomas Parslow http://almostobsolete.net/
#
# 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.
#
from boto.exception import BotoServerError
class LimitExceededException(BotoServerError):
pass
class DataAlreadyAcceptedException(BotoServerError):
pass
class ResourceInUseException(BotoServerError):
pass
class ServiceUnavailableException(BotoServerError):
pass
class InvalidParameterException(BotoServerError):
pass
class ResourceNotFoundException(BotoServerError):
pass
class ResourceAlreadyExistsException(BotoServerError):
pass
class OperationAbortedException(BotoServerError):
pass
class InvalidSequenceTokenException(BotoServerError):
pass

View File

@@ -0,0 +1,577 @@
# Copyright (c) 2014 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.
#
import boto
from boto.connection import AWSQueryConnection
from boto.regioninfo import RegionInfo
from boto.exception import JSONResponseError
from boto.logs import exceptions
from boto.compat import json
class CloudWatchLogsConnection(AWSQueryConnection):
"""
Amazon CloudWatch Logs Service API Reference
This is the Amazon CloudWatch Logs API Reference . Amazon
CloudWatch Logs is a managed service for real time monitoring and
archival of application logs. This guide provides detailed
information about Amazon CloudWatch Logs actions, data types,
parameters, and errors. For detailed information about Amazon
CloudWatch Logs features and their associated API calls, go to the
`Amazon CloudWatch Logs Developer Guide`_.
Use the following links to get started using the Amazon CloudWatch
API Reference :
+ `Actions`_: An alphabetical list of all Amazon CloudWatch Logs
actions.
+ `Data Types`_: An alphabetical list of all Amazon CloudWatch
Logs data types.
+ `Common Parameters`_: Parameters that all Query actions can use.
+ `Common Errors`_: Client and server errors that all actions can
return.
+ `Regions and Endpoints`_: Itemized regions and endpoints for all
AWS products.
In addition to using the Amazon CloudWatch Logs API, you can also
use the following SDKs and third-party libraries to access Amazon
CloudWatch Logs programmatically.
+ `AWS SDK for Java Documentation`_
+ `AWS SDK for .NET Documentation`_
+ `AWS SDK for PHP Documentation`_
+ `AWS SDK for Ruby Documentation`_
Developers in the AWS developer community also provide their own
libraries, which you can find at the following AWS developer
centers:
+ `AWS Java Developer Center`_
+ `AWS PHP Developer Center`_
+ `AWS Python Developer Center`_
+ `AWS Ruby Developer Center`_
+ `AWS Windows and .NET Developer Center`_
"""
APIVersion = "2014-03-28"
DefaultRegionName = "us-east-1"
DefaultRegionEndpoint = "logs.us-east-1.amazonaws.com"
ServiceName = "CloudWatchLogs"
TargetPrefix = "Logs_20140328"
ResponseError = JSONResponseError
_faults = {
"LimitExceededException": exceptions.LimitExceededException,
"DataAlreadyAcceptedException": exceptions.DataAlreadyAcceptedException,
"ResourceInUseException": exceptions.ResourceInUseException,
"ServiceUnavailableException": exceptions.ServiceUnavailableException,
"InvalidParameterException": exceptions.InvalidParameterException,
"ResourceNotFoundException": exceptions.ResourceNotFoundException,
"ResourceAlreadyExistsException": exceptions.ResourceAlreadyExistsException,
"OperationAbortedException": exceptions.OperationAbortedException,
"InvalidSequenceTokenException": exceptions.InvalidSequenceTokenException,
}
def __init__(self, **kwargs):
region = kwargs.pop('region', None)
if not region:
region = RegionInfo(self, self.DefaultRegionName,
self.DefaultRegionEndpoint)
if 'host' not in kwargs or kwargs['host'] is None:
kwargs['host'] = region.endpoint
super(CloudWatchLogsConnection, self).__init__(**kwargs)
self.region = region
def _required_auth_capability(self):
return ['hmac-v4']
def create_log_group(self, log_group_name):
"""
Creates a new log group with the specified name. The name of
the log group must be unique within a region for an AWS
account. You can create up to 100 log groups per account.
You must use the following guidelines when naming a log group:
+ Log group names can be between 1 and 512 characters long.
+ Allowed characters are az, AZ, 09, '_' (underscore), '-'
(hyphen), '/' (forward slash), and '.' (period).
Log groups are created with a default retention of 14 days.
The retention attribute allow you to configure the number of
days you want to retain log events in the specified log group.
See the `SetRetention` operation on how to modify the
retention of your log groups.
:type log_group_name: string
:param log_group_name:
"""
params = {'logGroupName': log_group_name, }
return self.make_request(action='CreateLogGroup',
body=json.dumps(params))
def create_log_stream(self, log_group_name, log_stream_name):
"""
Creates a new log stream in the specified log group. The name
of the log stream must be unique within the log group. There
is no limit on the number of log streams that can exist in a
log group.
You must use the following guidelines when naming a log
stream:
+ Log stream names can be between 1 and 512 characters long.
+ The ':' colon character is not allowed.
:type log_group_name: string
:param log_group_name:
:type log_stream_name: string
:param log_stream_name:
"""
params = {
'logGroupName': log_group_name,
'logStreamName': log_stream_name,
}
return self.make_request(action='CreateLogStream',
body=json.dumps(params))
def delete_log_group(self, log_group_name):
"""
Deletes the log group with the specified name. Amazon
CloudWatch Logs will delete a log group only if there are no
log streams and no metric filters associated with the log
group. If this condition is not satisfied, the request will
fail and the log group will not be deleted.
:type log_group_name: string
:param log_group_name:
"""
params = {'logGroupName': log_group_name, }
return self.make_request(action='DeleteLogGroup',
body=json.dumps(params))
def delete_log_stream(self, log_group_name, log_stream_name):
"""
Deletes a log stream and permanently deletes all the archived
log events associated with it.
:type log_group_name: string
:param log_group_name:
:type log_stream_name: string
:param log_stream_name:
"""
params = {
'logGroupName': log_group_name,
'logStreamName': log_stream_name,
}
return self.make_request(action='DeleteLogStream',
body=json.dumps(params))
def delete_metric_filter(self, log_group_name, filter_name):
"""
Deletes a metric filter associated with the specified log
group.
:type log_group_name: string
:param log_group_name:
:type filter_name: string
:param filter_name: The name of the metric filter.
"""
params = {
'logGroupName': log_group_name,
'filterName': filter_name,
}
return self.make_request(action='DeleteMetricFilter',
body=json.dumps(params))
def delete_retention_policy(self, log_group_name):
"""
:type log_group_name: string
:param log_group_name:
"""
params = {'logGroupName': log_group_name, }
return self.make_request(action='DeleteRetentionPolicy',
body=json.dumps(params))
def describe_log_groups(self, log_group_name_prefix=None,
next_token=None, limit=None):
"""
Returns all the log groups that are associated with the AWS
account making the request. The list returned in the response
is ASCII-sorted by log group name.
By default, this operation returns up to 50 log groups. If
there are more log groups to list, the response would contain
a `nextToken` value in the response body. You can also limit
the number of log groups returned in the response by
specifying the `limit` parameter in the request.
:type log_group_name_prefix: string
:param log_group_name_prefix:
:type next_token: string
:param next_token: A string token used for pagination that points to
the next page of results. It must be a value obtained from the
response of the previous `DescribeLogGroups` request.
:type limit: integer
:param limit: The maximum number of items returned in the response. If
you don't specify a value, the request would return up to 50 items.
"""
params = {}
if log_group_name_prefix is not None:
params['logGroupNamePrefix'] = log_group_name_prefix
if next_token is not None:
params['nextToken'] = next_token
if limit is not None:
params['limit'] = limit
return self.make_request(action='DescribeLogGroups',
body=json.dumps(params))
def describe_log_streams(self, log_group_name,
log_stream_name_prefix=None, next_token=None,
limit=None):
"""
Returns all the log streams that are associated with the
specified log group. The list returned in the response is
ASCII-sorted by log stream name.
By default, this operation returns up to 50 log streams. If
there are more log streams to list, the response would contain
a `nextToken` value in the response body. You can also limit
the number of log streams returned in the response by
specifying the `limit` parameter in the request.
:type log_group_name: string
:param log_group_name:
:type log_stream_name_prefix: string
:param log_stream_name_prefix:
:type next_token: string
:param next_token: A string token used for pagination that points to
the next page of results. It must be a value obtained from the
response of the previous `DescribeLogStreams` request.
:type limit: integer
:param limit: The maximum number of items returned in the response. If
you don't specify a value, the request would return up to 50 items.
"""
params = {'logGroupName': log_group_name, }
if log_stream_name_prefix is not None:
params['logStreamNamePrefix'] = log_stream_name_prefix
if next_token is not None:
params['nextToken'] = next_token
if limit is not None:
params['limit'] = limit
return self.make_request(action='DescribeLogStreams',
body=json.dumps(params))
def describe_metric_filters(self, log_group_name,
filter_name_prefix=None, next_token=None,
limit=None):
"""
Returns all the metrics filters associated with the specified
log group. The list returned in the response is ASCII-sorted
by filter name.
By default, this operation returns up to 50 metric filters. If
there are more metric filters to list, the response would
contain a `nextToken` value in the response body. You can also
limit the number of metric filters returned in the response by
specifying the `limit` parameter in the request.
:type log_group_name: string
:param log_group_name:
:type filter_name_prefix: string
:param filter_name_prefix: The name of the metric filter.
:type next_token: string
:param next_token: A string token used for pagination that points to
the next page of results. It must be a value obtained from the
response of the previous `DescribeMetricFilters` request.
:type limit: integer
:param limit: The maximum number of items returned in the response. If
you don't specify a value, the request would return up to 50 items.
"""
params = {'logGroupName': log_group_name, }
if filter_name_prefix is not None:
params['filterNamePrefix'] = filter_name_prefix
if next_token is not None:
params['nextToken'] = next_token
if limit is not None:
params['limit'] = limit
return self.make_request(action='DescribeMetricFilters',
body=json.dumps(params))
def get_log_events(self, log_group_name, log_stream_name,
start_time=None, end_time=None, next_token=None,
limit=None, start_from_head=None):
"""
Retrieves log events from the specified log stream. You can
provide an optional time range to filter the results on the
event `timestamp`.
By default, this operation returns as much log events as can
fit in a response size of 1MB, up to 10,000 log events. The
response will always include a `nextForwardToken` and a
`nextBackwardToken` in the response body. You can use any of
these tokens in subsequent `GetLogEvents` requests to paginate
through events in either forward or backward direction. You
can also limit the number of log events returned in the
response by specifying the `limit` parameter in the request.
:type log_group_name: string
:param log_group_name:
:type log_stream_name: string
:param log_stream_name:
:type start_time: long
:param start_time: A point in time expressed as the number milliseconds
since Jan 1, 1970 00:00:00 UTC.
:type end_time: long
:param end_time: A point in time expressed as the number milliseconds
since Jan 1, 1970 00:00:00 UTC.
:type next_token: string
:param next_token: A string token used for pagination that points to
the next page of results. It must be a value obtained from the
`nextForwardToken` or `nextBackwardToken` fields in the response of
the previous `GetLogEvents` request.
:type limit: integer
:param limit: The maximum number of log events returned in the
response. If you don't specify a value, the request would return as
much log events as can fit in a response size of 1MB, up to 10,000
log events.
:type start_from_head: boolean
:param start_from_head:
"""
params = {
'logGroupName': log_group_name,
'logStreamName': log_stream_name,
}
if start_time is not None:
params['startTime'] = start_time
if end_time is not None:
params['endTime'] = end_time
if next_token is not None:
params['nextToken'] = next_token
if limit is not None:
params['limit'] = limit
if start_from_head is not None:
params['startFromHead'] = start_from_head
return self.make_request(action='GetLogEvents',
body=json.dumps(params))
def put_log_events(self, log_group_name, log_stream_name, log_events,
sequence_token=None):
"""
Uploads a batch of log events to the specified log stream.
Every PutLogEvents request must include the `sequenceToken`
obtained from the response of the previous request. An upload
in a newly created log stream does not require a
`sequenceToken`.
The batch of events must satisfy the following constraints:
+ The maximum batch size is 32,768 bytes, and this size is
calculated as the sum of all event messages in UTF-8, plus 26
bytes for each log event.
+ None of the log events in the batch can be more than 2 hours
in the future.
+ None of the log events in the batch can be older than 14
days or the retention period of the log group.
+ The log events in the batch must be in chronological ordered
by their `timestamp`.
+ The maximum number of log events in a batch is 1,000.
:type log_group_name: string
:param log_group_name:
:type log_stream_name: string
:param log_stream_name:
:type log_events: list
:param log_events: A list of events belonging to a log stream.
:type sequence_token: string
:param sequence_token: A string token that must be obtained from the
response of the previous `PutLogEvents` request.
"""
params = {
'logGroupName': log_group_name,
'logStreamName': log_stream_name,
'logEvents': log_events,
}
if sequence_token is not None:
params['sequenceToken'] = sequence_token
return self.make_request(action='PutLogEvents',
body=json.dumps(params))
def put_metric_filter(self, log_group_name, filter_name, filter_pattern,
metric_transformations):
"""
Creates or updates a metric filter and associates it with the
specified log group. Metric filters allow you to configure
rules to extract metric data from log events ingested through
`PutLogEvents` requests.
:type log_group_name: string
:param log_group_name:
:type filter_name: string
:param filter_name: The name of the metric filter.
:type filter_pattern: string
:param filter_pattern:
:type metric_transformations: list
:param metric_transformations:
"""
params = {
'logGroupName': log_group_name,
'filterName': filter_name,
'filterPattern': filter_pattern,
'metricTransformations': metric_transformations,
}
return self.make_request(action='PutMetricFilter',
body=json.dumps(params))
def put_retention_policy(self, log_group_name, retention_in_days):
"""
:type log_group_name: string
:param log_group_name:
:type retention_in_days: integer
:param retention_in_days: Specifies the number of days you want to
retain log events in the specified log group. Possible values are:
1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 547, 730.
"""
params = {
'logGroupName': log_group_name,
'retentionInDays': retention_in_days,
}
return self.make_request(action='PutRetentionPolicy',
body=json.dumps(params))
def set_retention(self, log_group_name, retention_in_days):
"""
Sets the retention of the specified log group. Log groups are
created with a default retention of 14 days. The retention
attribute allow you to configure the number of days you want
to retain log events in the specified log group.
:type log_group_name: string
:param log_group_name:
:type retention_in_days: integer
:param retention_in_days: Specifies the number of days you want to
retain log events in the specified log group. Possible values are:
1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 547, 730.
"""
params = {
'logGroupName': log_group_name,
'retentionInDays': retention_in_days,
}
return self.make_request(action='SetRetention',
body=json.dumps(params))
def test_metric_filter(self, filter_pattern, log_event_messages):
"""
Tests the filter pattern of a metric filter against a sample
of log event messages. You can use this operation to validate
the correctness of a metric filter pattern.
:type filter_pattern: string
:param filter_pattern:
:type log_event_messages: list
:param log_event_messages:
"""
params = {
'filterPattern': filter_pattern,
'logEventMessages': log_event_messages,
}
return self.make_request(action='TestMetricFilter',
body=json.dumps(params))
def make_request(self, action, body):
headers = {
'X-Amz-Target': '%s.%s' % (self.TargetPrefix, action),
'Host': self.region.endpoint,
'Content-Type': 'application/x-amz-json-1.1',
'Content-Length': str(len(body)),
}
http_request = self.build_base_http_request(
method='POST', path='/', auth_path='/', params={},
headers=headers, data=body)
response = self._mexe(http_request, sender=None,
override_num_retries=10)
response_body = response.read().decode('utf-8')
boto.log.debug(response_body)
if response.status == 200:
if response_body:
return json.loads(response_body)
else:
json_body = json.loads(response_body)
fault_name = json_body.get('__type', None)
exception_class = self._faults.get(fault_name, self.ResponseError)
raise exception_class(response.status, response.reason,
body=json_body)

View File

@@ -18,20 +18,36 @@
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 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 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
"""
The cmdshell module uses the paramiko package to create SSH connections
to the servers that are represented by instance objects. The module has
functions for running commands, managing files, and opening interactive
shell sessions over those connections.
"""
from boto.mashups.interactive import interactive_shell from boto.mashups.interactive import interactive_shell
import boto import boto
import os import os
import time import time
import shutil import shutil
import StringIO
import paramiko import paramiko
import socket import socket
import subprocess import subprocess
from boto.compat import StringIO
class SSHClient(object): class SSHClient(object):
"""
This class creates a paramiko.SSHClient() object that represents
a session with an SSH server. You can use the SSHClient object to send
commands to the remote host and manipulate files on the remote host.
:ivar server: A Server object or FakeServer object.
:ivar host_key_file: The path to the user's .ssh key files.
:ivar uname: The username for the SSH connection. Default = 'root'.
:ivar timeout: The optional timeout variable for the TCP connection.
:ivar ssh_pwd: An optional password to use for authentication or for
unlocking the private key.
"""
def __init__(self, server, def __init__(self, server,
host_key_file='~/.ssh/known_hosts', host_key_file='~/.ssh/known_hosts',
uname='root', timeout=None, ssh_pwd=None): uname='root', timeout=None, ssh_pwd=None):
@@ -48,6 +64,12 @@ class SSHClient(object):
self.connect() self.connect()
def connect(self, num_retries=5): def connect(self, num_retries=5):
"""
Connect to an SSH server and authenticate with it.
:type num_retries: int
:param num_retries: The maximum number of connection attempts.
"""
retry = 0 retry = 0
while retry < num_retries: while retry < num_retries:
try: try:
@@ -56,53 +78,132 @@ class SSHClient(object):
pkey=self._pkey, pkey=self._pkey,
timeout=self._timeout) timeout=self._timeout)
return return
except socket.error, (value, message): except socket.error as xxx_todo_changeme:
(value, message) = xxx_todo_changeme.args
if value in (51, 61, 111): if value in (51, 61, 111):
print 'SSH Connection refused, will retry in 5 seconds' print('SSH Connection refused, will retry in 5 seconds')
time.sleep(5) time.sleep(5)
retry += 1 retry += 1
else: else:
raise raise
except paramiko.BadHostKeyException: except paramiko.BadHostKeyException:
print "%s has an entry in ~/.ssh/known_hosts and it doesn't match" % self.server.hostname print("%s has an entry in ~/.ssh/known_hosts and it doesn't match" % self.server.hostname)
print 'Edit that file to remove the entry and then hit return to try again' print('Edit that file to remove the entry and then hit return to try again')
raw_input('Hit Enter when ready') raw_input('Hit Enter when ready')
retry += 1 retry += 1
except EOFError: except EOFError:
print 'Unexpected Error from SSH Connection, retry in 5 seconds' print('Unexpected Error from SSH Connection, retry in 5 seconds')
time.sleep(5) time.sleep(5)
retry += 1 retry += 1
print 'Could not establish SSH connection' print('Could not establish SSH connection')
def open_sftp(self): def open_sftp(self):
"""
Open an SFTP session on the SSH server.
:rtype: :class:`paramiko.sftp_client.SFTPClient`
:return: An SFTP client object.
"""
return self._ssh_client.open_sftp() return self._ssh_client.open_sftp()
def get_file(self, src, dst): def get_file(self, src, dst):
"""
Open an SFTP session on the remote host, and copy a file from
the remote host to the specified path on the local host.
:type src: string
:param src: The path to the target file on the remote host.
:type dst: string
:param dst: The path on your local host where you want to
store the file.
"""
sftp_client = self.open_sftp() sftp_client = self.open_sftp()
sftp_client.get(src, dst) sftp_client.get(src, dst)
def put_file(self, src, dst): def put_file(self, src, dst):
"""
Open an SFTP session on the remote host, and copy a file from
the local host to the specified path on the remote host.
:type src: string
:param src: The path to the target file on your local host.
:type dst: string
:param dst: The path on the remote host where you want to store
the file.
"""
sftp_client = self.open_sftp() sftp_client = self.open_sftp()
sftp_client.put(src, dst) sftp_client.put(src, dst)
def open(self, filename, mode='r', bufsize=-1): def open(self, filename, mode='r', bufsize=-1):
""" """
Open a file on the remote system and return a file-like object. Open an SFTP session to the remote host, and open a file on
that host.
:type filename: string
:param filename: The path to the file on the remote host.
:type mode: string
:param mode: The file interaction mode.
:type bufsize: integer
:param bufsize: The file buffer size.
:rtype: :class:`paramiko.sftp_file.SFTPFile`
:return: A paramiko proxy object for a file on the remote server.
""" """
sftp_client = self.open_sftp() sftp_client = self.open_sftp()
return sftp_client.open(filename, mode, bufsize) return sftp_client.open(filename, mode, bufsize)
def listdir(self, path): def listdir(self, path):
"""
List all of the files and subdirectories at the specified path
on the remote host.
:type path: string
:param path: The base path from which to obtain the list.
:rtype: list
:return: A list of files and subdirectories at the specified path.
"""
sftp_client = self.open_sftp() sftp_client = self.open_sftp()
return sftp_client.listdir(path) return sftp_client.listdir(path)
def isdir(self, path): def isdir(self, path):
"""
Check the specified path on the remote host to determine if
it is a directory.
:type path: string
:param path: The path to the directory that you want to check.
:rtype: integer
:return: If the path is a directory, the function returns 1.
If the path is a file or an invalid path, the function
returns 0.
"""
status = self.run('[ -d %s ] || echo "FALSE"' % path) status = self.run('[ -d %s ] || echo "FALSE"' % path)
if status[1].startswith('FALSE'): if status[1].startswith('FALSE'):
return 0 return 0
return 1 return 1
def exists(self, path): def exists(self, path):
"""
Check the remote host for the specified path, or a file
at the specified path. This function returns 1 if the
path or the file exist on the remote host, and returns 0 if
the path or the file does not exist on the remote host.
:type path: string
:param path: The path to the directory or file that you want to check.
:rtype: integer
:return: If the path or the file exist, the function returns 1.
If the path or the file do not exist on the remote host,
the function returns 0.
"""
status = self.run('[ -a %s ] || echo "FALSE"' % path) status = self.run('[ -a %s ] || echo "FALSE"' % path)
if status[1].startswith('FALSE'): if status[1].startswith('FALSE'):
return 0 return 0
@@ -110,16 +211,22 @@ class SSHClient(object):
def shell(self): def shell(self):
""" """
Start an interactive shell session on the remote host. Start an interactive shell session with the remote host.
""" """
channel = self._ssh_client.invoke_shell() channel = self._ssh_client.invoke_shell()
interactive_shell(channel) interactive_shell(channel)
def run(self, command): def run(self, command):
""" """
Execute a command on the remote host. Return a tuple containing Run a command on the remote host.
an integer status and two strings, the first containing stdout
and the second containing stderr from the command. :type command: string
:param command: The command that you want to send to the remote host.
:rtype: tuple
:return: This function returns a tuple that contains an integer status,
the stdout from the command, and the stderr from the command.
""" """
boto.log.debug('running:%s on %s' % (command, self.server.instance_id)) boto.log.debug('running:%s on %s' % (command, self.server.instance_id))
status = 0 status = 0
@@ -138,8 +245,14 @@ class SSHClient(object):
def run_pty(self, command): def run_pty(self, command):
""" """
Execute a command on the remote host with a pseudo-terminal. Request a pseudo-terminal from a server, and execute a command on that
Returns a string containing the output of the command. server.
:type command: string
:param command: The command that you want to run on the remote host.
:rtype: :class:`paramiko.channel.Channel`
:return: An open channel object.
""" """
boto.log.debug('running:%s on %s' % (command, self.server.instance_id)) boto.log.debug('running:%s on %s' % (command, self.server.instance_id))
channel = self._ssh_client.get_transport().open_session() channel = self._ssh_client.get_transport().open_session()
@@ -148,38 +261,77 @@ class SSHClient(object):
return channel return channel
def close(self): def close(self):
"""
Close an SSH session and any open channels that are tied to it.
"""
transport = self._ssh_client.get_transport() transport = self._ssh_client.get_transport()
transport.close() transport.close()
self.server.reset_cmdshell() self.server.reset_cmdshell()
class LocalClient(object): class LocalClient(object):
"""
:ivar server: A Server object or FakeServer object.
:ivar host_key_file: The path to the user's .ssh key files.
:ivar uname: The username for the SSH connection. Default = 'root'.
"""
def __init__(self, server, host_key_file=None, uname='root'): def __init__(self, server, host_key_file=None, uname='root'):
self.server = server self.server = server
self.host_key_file = host_key_file self.host_key_file = host_key_file
self.uname = uname self.uname = uname
def get_file(self, src, dst): def get_file(self, src, dst):
"""
Copy a file from one directory to another.
"""
shutil.copyfile(src, dst) shutil.copyfile(src, dst)
def put_file(self, src, dst): def put_file(self, src, dst):
"""
Copy a file from one directory to another.
"""
shutil.copyfile(src, dst) shutil.copyfile(src, dst)
def listdir(self, path): def listdir(self, path):
"""
List all of the files and subdirectories at the specified path.
:rtype: list
:return: Return a list containing the names of the entries
in the directory given by path.
"""
return os.listdir(path) return os.listdir(path)
def isdir(self, path): def isdir(self, path):
"""
Check the specified path to determine if it is a directory.
:rtype: boolean
:return: Returns True if the path is an existing directory.
"""
return os.path.isdir(path) return os.path.isdir(path)
def exists(self, path): def exists(self, path):
"""
Check for the specified path, or check a file at the specified path.
:rtype: boolean
:return: If the path or the file exist, the function returns True.
"""
return os.path.exists(path) return os.path.exists(path)
def shell(self): def shell(self):
raise NotImplementedError('shell not supported with LocalClient') raise NotImplementedError('shell not supported with LocalClient')
def run(self): def run(self):
"""
Open a subprocess and run a command on the local host.
:rtype: tuple
:return: This function returns a tuple that contains an integer status
and a string with the combined stdout and stderr output.
"""
boto.log.info('running:%s' % self.command) boto.log.info('running:%s' % self.command)
log_fp = StringIO.StringIO() log_fp = StringIO()
process = subprocess.Popen(self.command, shell=True, stdin=subprocess.PIPE, process = subprocess.Popen(self.command, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=subprocess.PIPE, stderr=subprocess.PIPE)
while process.poll() is None: while process.poll() is None:
@@ -196,9 +348,13 @@ class LocalClient(object):
class FakeServer(object): class FakeServer(object):
""" """
A little class to fake out SSHClient (which is expecting a This object has a subset of the variables that are normally in a
:class`boto.manage.server.Server` instance. This allows us :class:`boto.manage.server.Server` object. You can use this FakeServer
to object to create a :class:`boto.manage.SSHClient` object if you
don't have a real Server object.
:ivar instance: A boto Instance object.
:ivar ssh_key_file: The path to the SSH key file.
""" """
def __init__(self, instance, ssh_key_file): def __init__(self, instance, ssh_key_file):
self.instance = instance self.instance = instance
@@ -207,6 +363,14 @@ class FakeServer(object):
self.instance_id = self.instance.id self.instance_id = self.instance.id
def start(server): def start(server):
"""
Connect to the specified server.
:return: If the server is local, the function returns a
:class:`boto.manage.cmdshell.LocalClient` object.
If the server is remote, the function returns a
:class:`boto.manage.cmdshell.SSHClient` object.
"""
instance_id = boto.config.get('Instance', 'instance-id', None) instance_id = boto.config.get('Instance', 'instance-id', None)
if instance_id == server.instance_id: if instance_id == server.instance_id:
return LocalClient(server) return LocalClient(server)
@@ -223,19 +387,19 @@ def sshclient_from_instance(instance, ssh_key_file,
:type instance: :class`boto.ec2.instance.Instance` object :type instance: :class`boto.ec2.instance.Instance` object
:param instance: The instance object. :param instance: The instance object.
:type ssh_key_file: str :type ssh_key_file: string
:param ssh_key_file: A path to the private key file used :param ssh_key_file: A path to the private key file that is
to log into instance. used to log into the instance.
:type host_key_file: str :type host_key_file: string
:param host_key_file: A path to the known_hosts file used :param host_key_file: A path to the known_hosts file used
by the SSH client. by the SSH client.
Defaults to ~/.ssh/known_hosts Defaults to ~/.ssh/known_hosts
:type user_name: str :type user_name: string
:param user_name: The username to use when logging into :param user_name: The username to use when logging into
the instance. Defaults to root. the instance. Defaults to root.
:type ssh_pwd: str :type ssh_pwd: string
:param ssh_pwd: The passphrase, if any, associated with :param ssh_pwd: The passphrase, if any, associated with
private key. private key.
""" """

View File

@@ -19,7 +19,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
def get(prop, choices=None): def get(prop, choices=None):
prompt = prop.verbose_name prompt = prop.verbose_name
if not prompt: if not prompt:
@@ -38,7 +37,7 @@ def get(prop, choices=None):
value = choices[i-1] value = choices[i-1]
if isinstance(value, tuple): if isinstance(value, tuple):
value = value[0] value = value[0]
print '[%d] %s' % (i, value) print('[%d] %s' % (i, value))
value = raw_input('%s [%d-%d]: ' % (prompt, min, max)) value = raw_input('%s [%d-%d]: ' % (prompt, min, max))
try: try:
int_value = int(value) int_value = int(value)
@@ -47,18 +46,18 @@ def get(prop, choices=None):
value = value[1] value = value[1]
valid = True valid = True
except ValueError: except ValueError:
print '%s is not a valid choice' % value print('%s is not a valid choice' % value)
except IndexError: except IndexError:
print '%s is not within the range[%d-%d]' % (min, max) print('%s is not within the range[%d-%d]' % (min, max))
else: else:
value = raw_input('%s: ' % prompt) value = raw_input('%s: ' % prompt)
try: try:
value = prop.validate(value) value = prop.validate(value)
if prop.empty(value) and prop.required: if prop.empty(value) and prop.required:
print 'A value is required' print('A value is required')
else: else:
valid = True valid = True
except: except:
print 'Invalid value: %s' % value print('Invalid value: %s' % value)
return value return value

View File

@@ -19,11 +19,10 @@
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # 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 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE. # IN THE SOFTWARE.
""" """
High-level abstraction of an EC2 server High-level abstraction of an EC2 server
""" """
from __future__ import with_statement
import boto.ec2 import boto.ec2
from boto.mashups.iobject import IObject from boto.mashups.iobject import IObject
from boto.pyami.config import BotoConfigPath, Config from boto.pyami.config import BotoConfigPath, Config
@@ -32,9 +31,10 @@ from boto.sdb.db.property import StringProperty, IntegerProperty, BooleanPropert
from boto.manage import propget from boto.manage import propget
from boto.ec2.zone import Zone from boto.ec2.zone import Zone
from boto.ec2.keypair import KeyPair from boto.ec2.keypair import KeyPair
import os, time, StringIO import os, time
from contextlib import closing from contextlib import closing
from boto.exception import EC2ResponseError from boto.exception import EC2ResponseError
from boto.compat import six, StringIO
InstanceTypes = ['m1.small', 'm1.large', 'm1.xlarge', InstanceTypes = ['m1.small', 'm1.large', 'm1.xlarge',
'c1.medium', 'c1.xlarge', 'c1.medium', 'c1.xlarge',
@@ -49,7 +49,7 @@ class Bundler(object):
self.ssh_client = SSHClient(server, uname=uname) self.ssh_client = SSHClient(server, uname=uname)
def copy_x509(self, key_file, cert_file): def copy_x509(self, key_file, cert_file):
print '\tcopying cert and pk over to /mnt directory on server' print('\tcopying cert and pk over to /mnt directory on server')
self.ssh_client.open_sftp() self.ssh_client.open_sftp()
path, name = os.path.split(key_file) path, name = os.path.split(key_file)
self.remote_key_file = '/mnt/%s' % name self.remote_key_file = '/mnt/%s' % name
@@ -57,7 +57,7 @@ class Bundler(object):
path, name = os.path.split(cert_file) path, name = os.path.split(cert_file)
self.remote_cert_file = '/mnt/%s' % name self.remote_cert_file = '/mnt/%s' % name
self.ssh_client.put_file(cert_file, self.remote_cert_file) self.ssh_client.put_file(cert_file, self.remote_cert_file)
print '...complete!' print('...complete!')
def bundle_image(self, prefix, size, ssh_key): def bundle_image(self, prefix, size, ssh_key):
command = "" command = ""
@@ -103,7 +103,7 @@ class Bundler(object):
ssh_key = self.server.get_ssh_key_file() ssh_key = self.server.get_ssh_key_file()
self.copy_x509(key_file, cert_file) self.copy_x509(key_file, cert_file)
if not fp: if not fp:
fp = StringIO.StringIO() fp = StringIO()
fp.write('sudo mv %s /mnt/boto.cfg; ' % BotoConfigPath) fp.write('sudo mv %s /mnt/boto.cfg; ' % BotoConfigPath)
fp.write('mv ~/.ssh/authorized_keys /mnt/authorized_keys; ') fp.write('mv ~/.ssh/authorized_keys /mnt/authorized_keys; ')
if clear_history: if clear_history:
@@ -115,13 +115,13 @@ class Bundler(object):
fp.write('sudo mv /mnt/boto.cfg %s; ' % BotoConfigPath) fp.write('sudo mv /mnt/boto.cfg %s; ' % BotoConfigPath)
fp.write('mv /mnt/authorized_keys ~/.ssh/authorized_keys') fp.write('mv /mnt/authorized_keys ~/.ssh/authorized_keys')
command = fp.getvalue() command = fp.getvalue()
print 'running the following command on the remote server:' print('running the following command on the remote server:')
print command print(command)
t = self.ssh_client.run(command) t = self.ssh_client.run(command)
print '\t%s' % t[0] print('\t%s' % t[0])
print '\t%s' % t[1] print('\t%s' % t[1])
print '...complete!' print('...complete!')
print 'registering image...' print('registering image...')
self.image_id = self.server.ec2.register_image(name=prefix, image_location='%s/%s.manifest.xml' % (bucket, prefix)) self.image_id = self.server.ec2.register_image(name=prefix, image_location='%s/%s.manifest.xml' % (bucket, prefix))
return self.image_id return self.image_id
@@ -250,7 +250,7 @@ class Server(Model):
instance_type = CalculatedProperty(verbose_name="Instance Type", calculated_type=str, use_method=True) instance_type = CalculatedProperty(verbose_name="Instance Type", calculated_type=str, use_method=True)
status = CalculatedProperty(verbose_name="Current Status", calculated_type=str, use_method=True) status = CalculatedProperty(verbose_name="Current Status", calculated_type=str, use_method=True)
launch_time = CalculatedProperty(verbose_name="Server Launch Time", calculated_type=str, use_method=True) launch_time = CalculatedProperty(verbose_name="Server Launch Time", calculated_type=str, use_method=True)
console_output = CalculatedProperty(verbose_name="Console Output", calculated_type=file, use_method=True) console_output = CalculatedProperty(verbose_name="Console Output", calculated_type=open, use_method=True)
packages = [] packages = []
plugins = [] plugins = []
@@ -305,7 +305,7 @@ class Server(Model):
# deal with possibly passed in logical volume: # deal with possibly passed in logical volume:
if logical_volume != None: if logical_volume != None:
cfg.set('EBS', 'logical_volume_name', logical_volume.name) cfg.set('EBS', 'logical_volume_name', logical_volume.name)
cfg_fp = StringIO.StringIO() cfg_fp = StringIO()
cfg.write(cfg_fp) cfg.write(cfg_fp)
# deal with the possibility that zone and/or keypair are strings read from the config file: # deal with the possibility that zone and/or keypair are strings read from the config file:
if isinstance(zone, Zone): if isinstance(zone, Zone):
@@ -325,14 +325,14 @@ class Server(Model):
instances = reservation.instances instances = reservation.instances
if elastic_ip is not None and instances.__len__() > 0: if elastic_ip is not None and instances.__len__() > 0:
instance = instances[0] instance = instances[0]
print 'Waiting for instance to start so we can set its elastic IP address...' print('Waiting for instance to start so we can set its elastic IP address...')
# Sometimes we get a message from ec2 that says that the instance does not exist. # Sometimes we get a message from ec2 that says that the instance does not exist.
# Hopefully the following delay will giv eec2 enough time to get to a stable state: # Hopefully the following delay will giv eec2 enough time to get to a stable state:
time.sleep(5) time.sleep(5)
while instance.update() != 'running': while instance.update() != 'running':
time.sleep(1) time.sleep(1)
instance.use_ip(elastic_ip) instance.use_ip(elastic_ip)
print 'set the elastic IP of the first instance to %s' % elastic_ip print('set the elastic IP of the first instance to %s' % elastic_ip)
for instance in instances: for instance in instances:
s = cls() s = cls()
s.ec2 = ec2 s.ec2 = ec2
@@ -381,7 +381,7 @@ class Server(Model):
for reservation in rs: for reservation in rs:
for instance in reservation.instances: for instance in reservation.instances:
try: try:
Server.find(instance_id=instance.id).next() next(Server.find(instance_id=instance.id))
boto.log.info('Server for %s already exists' % instance.id) boto.log.info('Server for %s already exists' % instance.id)
except StopIteration: except StopIteration:
s = cls() s = cls()
@@ -527,7 +527,7 @@ class Server(Model):
def get_cmdshell(self): def get_cmdshell(self):
if not self._cmdshell: if not self._cmdshell:
import cmdshell from boto.manage import cmdshell
self.get_ssh_key_file() self.get_ssh_key_file()
self._cmdshell = cmdshell.start(self) self._cmdshell = cmdshell.start(self)
return self._cmdshell return self._cmdshell

View File

@@ -23,7 +23,8 @@
import boto import boto
from boto.sdb.db.property import StringProperty, DateTimeProperty, IntegerProperty from boto.sdb.db.property import StringProperty, DateTimeProperty, IntegerProperty
from boto.sdb.db.model import Model from boto.sdb.db.model import Model
import datetime, subprocess, StringIO, time import datetime, subprocess, time
from boto.compat import StringIO
def check_hour(val): def check_hour(val):
if val == '*': if val == '*':
@@ -100,7 +101,7 @@ class Task(Model):
def _run(self, msg, vtimeout): def _run(self, msg, vtimeout):
boto.log.info('Task[%s] - running:%s' % (self.name, self.command)) boto.log.info('Task[%s] - running:%s' % (self.name, self.command))
log_fp = StringIO.StringIO() log_fp = StringIO()
process = subprocess.Popen(self.command, shell=True, stdin=subprocess.PIPE, process = subprocess.Popen(self.command, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=subprocess.PIPE, stderr=subprocess.PIPE)
nsecs = 5 nsecs = 5

View File

@@ -2,33 +2,33 @@ from boto.manage.server import Server
from boto.manage.volume import Volume from boto.manage.volume import Volume
import time import time
print '--> Creating New Volume' print('--> Creating New Volume')
volume = Volume.create() volume = Volume.create()
print volume print(volume)
print '--> Creating New Server' print('--> Creating New Server')
server_list = Server.create() server_list = Server.create()
server = server_list[0] server = server_list[0]
print server print(server)
print '----> Waiting for Server to start up' print('----> Waiting for Server to start up')
while server.status != 'running': while server.status != 'running':
print '*' print('*')
time.sleep(10) time.sleep(10)
print '----> Server is running' print('----> Server is running')
print '--> Run "df -k" on Server' print('--> Run "df -k" on Server')
status = server.run('df -k') status = server.run('df -k')
print status[1] print(status[1])
print '--> Now run volume.make_ready to make the volume ready to use on server' print('--> Now run volume.make_ready to make the volume ready to use on server')
volume.make_ready(server) volume.make_ready(server)
print '--> Run "df -k" on Server' print('--> Run "df -k" on Server')
status = server.run('df -k') status = server.run('df -k')
print status[1] print(status[1])
print '--> Do an "ls -al" on the new filesystem' print('--> Do an "ls -al" on the new filesystem')
status = server.run('ls -al %s' % volume.mount_point) status = server.run('ls -al %s' % volume.mount_point)
print status[1] print(status[1])

Some files were not shown because too many files have changed in this diff Show More