summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst4
-rw-r--r--boto/__init__.py2
-rw-r--r--boto/auth.py11
-rw-r--r--boto/cloudfront/distribution.py9
-rw-r--r--boto/ec2/autoscale/group.py8
-rw-r--r--boto/ec2/autoscale/launchconfig.py2
-rw-r--r--boto/ec2/autoscale/policy.py9
-rw-r--r--boto/emr/emrobject.py3
-rw-r--r--boto/endpoints.json34
-rwxr-xr-xboto/gs/connection.py24
-rw-r--r--boto/mws/response.py2
-rw-r--r--boto/rds/__init__.py23
-rw-r--r--boto/rds/logfile.py46
-rw-r--r--boto/route53/connection.py4
-rw-r--r--boto/route53/record.py4
-rw-r--r--boto/route53/zone.py4
-rw-r--r--boto/s3/connection.py72
-rw-r--r--boto/sqs/connection.py2
-rw-r--r--docs/source/index.rst5
-rw-r--r--docs/source/ref/route53.rst4
-rw-r--r--docs/source/releasenotes/v2.25.0.rst57
-rw-r--r--docs/source/route53_tut.rst87
-rw-r--r--docs/source/s3_tut.rst23
-rw-r--r--tests/integration/mws/test.py5
-rw-r--r--tests/unit/auth/test_sigv4.py66
-rw-r--r--tests/unit/ec2/autoscale/test_group.py143
-rw-r--r--tests/unit/emr/test_connection.py458
-rw-r--r--tests/unit/glacier/test_concurrent.py4
-rw-r--r--tests/unit/rds/test_connection.py81
-rw-r--r--tests/unit/route53/test_connection.py198
-rw-r--r--tests/unit/s3/test_bucket.py14
-rw-r--r--tests/unit/s3/test_connection.py50
32 files changed, 1391 insertions, 67 deletions
diff --git a/README.rst b/README.rst
index 8459fc00..f23634ba 100644
--- a/README.rst
+++ b/README.rst
@@ -1,9 +1,9 @@
####
boto
####
-boto 2.24.0
+boto 2.25.0
-Released: 29-January-2014
+Released: 07-February-2014
.. image:: https://travis-ci.org/boto/boto.png?branch=develop
:target: https://travis-ci.org/boto/boto
diff --git a/boto/__init__.py b/boto/__init__.py
index 05cae44c..87177be4 100644
--- a/boto/__init__.py
+++ b/boto/__init__.py
@@ -37,7 +37,7 @@ import logging.config
import urlparse
from boto.exception import InvalidUriError
-__version__ = '2.24.0'
+__version__ = '2.25.0'
Version = __version__ # for backware compatibility
# http://bugs.python.org/issue7980
diff --git a/boto/auth.py b/boto/auth.py
index cc58e3c3..99699af7 100644
--- a/boto/auth.py
+++ b/boto/auth.py
@@ -499,7 +499,10 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys):
# Safe to modify req.path here since
# the signature will use req.auth_path.
req.path = req.path.split('?')[0]
- req.path = req.path + '?' + qs
+
+ if qs:
+ # Don't insert the '?' unless there's actually a query string
+ req.path = req.path + '?' + qs
canonical_request = self.canonical_request(req)
boto.log.debug('CanonicalRequest:\n%s' % canonical_request)
string_to_sign = self.string_to_sign(req, canonical_request)
@@ -893,6 +896,12 @@ def get_auth_handler(host, config, provider, requested_capability=None):
def detect_potential_sigv4(func):
def _wrapper(self):
+ if os.environ.get('EC2_USE_SIGV4', False):
+ return ['hmac-v4']
+
+ if boto.config.get('ec2', 'use-sigv4', False):
+ return ['hmac-v4']
+
if hasattr(self, 'region'):
if getattr(self.region, 'endpoint', ''):
if '.cn-' in self.region.endpoint:
diff --git a/boto/cloudfront/distribution.py b/boto/cloudfront/distribution.py
index 7664bdbf..5566bdfe 100644
--- a/boto/cloudfront/distribution.py
+++ b/boto/cloudfront/distribution.py
@@ -103,6 +103,9 @@ class DistributionConfig(object):
self.logging = logging
self.default_root_object = default_root_object
+ def __repr__(self):
+ return "DistributionConfig:%s" % self.origin
+
def to_xml(self):
s = '<?xml version="1.0" encoding="UTF-8"?>\n'
s += '<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2010-07-15/">\n'
@@ -234,6 +237,9 @@ class DistributionSummary(object):
self.etag = None
self.streaming = False
+ def __repr__(self):
+ return "DistributionSummary:%s" % self.domain_name
+
def startElement(self, name, attrs, connection):
if name == 'TrustedSigners':
self.trusted_signers = TrustedSigners()
@@ -295,6 +301,9 @@ class Distribution(object):
self._bucket = None
self._object_class = Object
+ def __repr__(self):
+ return "Distribution:%s" % self.domain_name
+
def startElement(self, name, attrs, connection):
if name == 'DistributionConfig':
self.config = DistributionConfig()
diff --git a/boto/ec2/autoscale/group.py b/boto/ec2/autoscale/group.py
index 58173459..c3c04127 100644
--- a/boto/ec2/autoscale/group.py
+++ b/boto/ec2/autoscale/group.py
@@ -146,9 +146,9 @@ class AutoScalingGroup(object):
:param placement_group: Physical location of your cluster placement
group created in Amazon EC2.
- :type vpc_zone_identifier: str
- :param vpc_zone_identifier: The subnet identifier of the Virtual
- Private Cloud.
+ :type vpc_zone_identifier: str or list
+ :param vpc_zone_identifier: A comma-separated string or python list of
+ the subnet identifiers of the Virtual Private Cloud.
:type tags: list
:param tags: List of :class:`boto.ec2.autoscale.tag.Tag`s
@@ -188,6 +188,8 @@ class AutoScalingGroup(object):
self.health_check_type = health_check_type
self.placement_group = placement_group
self.autoscaling_group_arn = None
+ if type(vpc_zone_identifier) is list:
+ vpc_zone_identifier = ','.join(vpc_zone_identifier)
self.vpc_zone_identifier = vpc_zone_identifier
self.instances = None
self.tags = tags or None
diff --git a/boto/ec2/autoscale/launchconfig.py b/boto/ec2/autoscale/launchconfig.py
index cc9e0cd0..5a887bf3 100644
--- a/boto/ec2/autoscale/launchconfig.py
+++ b/boto/ec2/autoscale/launchconfig.py
@@ -223,6 +223,8 @@ class LaunchConfiguration(object):
self.instance_profile_name = value
elif name == 'EbsOptimized':
self.ebs_optimized = True if value.lower() == 'true' else False
+ elif name == 'AssociatePublicIpAddress':
+ self.associate_public_ip_address = True if value.lower() == 'true' else False
elif name == 'VolumeType':
self.volume_type = value
elif name == 'DeleteOnTermination':
diff --git a/boto/ec2/autoscale/policy.py b/boto/ec2/autoscale/policy.py
index 0538557d..fbe7dc5f 100644
--- a/boto/ec2/autoscale/policy.py
+++ b/boto/ec2/autoscale/policy.py
@@ -47,16 +47,17 @@ class Alarm(object):
class AdjustmentType(object):
def __init__(self, connection=None):
self.connection = connection
- self.adjustment_types = ListElement([])
+ self.adjustment_type = None
def __repr__(self):
- return 'AdjustmentType:%s' % self.adjustment_types
+ return 'AdjustmentType:%s' % self.adjustment_type
def startElement(self, name, attrs, connection):
- if name == 'AdjustmentType':
- return self.adjustment_types
+ return
def endElement(self, name, value, connection):
+ if name == 'AdjustmentType':
+ self.adjustment_type = value
return
diff --git a/boto/emr/emrobject.py b/boto/emr/emrobject.py
index a397a55a..0906bfab 100644
--- a/boto/emr/emrobject.py
+++ b/boto/emr/emrobject.py
@@ -262,11 +262,12 @@ class Cluster(EmrObject):
if name == 'Status':
self.status = ClusterStatus()
return self.status
- elif name == 'EC2InstanceAttributes':
+ elif name == 'Ec2InstanceAttributes':
self.ec2instanceattributes = Ec2InstanceAttributes()
return self.ec2instanceattributes
elif name == 'Applications':
self.applications = ResultSet([('member', Application)])
+ return self.applications
elif name == 'Tags':
self.tags = ResultSet([('member', KeyValue)])
return self.tags
diff --git a/boto/endpoints.json b/boto/endpoints.json
index 2dbdff96..c15235c9 100644
--- a/boto/endpoints.json
+++ b/boto/endpoints.json
@@ -126,16 +126,16 @@
"us-west-2": "elasticloadbalancing.us-west-2.amazonaws.com"
},
"elasticmapreduce": {
- "ap-northeast-1": "elasticmapreduce.ap-northeast-1.amazonaws.com",
- "ap-southeast-1": "elasticmapreduce.ap-southeast-1.amazonaws.com",
- "ap-southeast-2": "elasticmapreduce.ap-southeast-2.amazonaws.com",
+ "ap-northeast-1": "ap-northeast-1.elasticmapreduce.amazonaws.com",
+ "ap-southeast-1": "ap-southeast-1.elasticmapreduce.amazonaws.com",
+ "ap-southeast-2": "ap-southeast-2.elasticmapreduce.amazonaws.com",
"cn-north-1": "elasticmapreduce.cn-north-1.amazonaws.com.cn",
"eu-west-1": "elasticmapreduce.eu-west-1.amazonaws.com",
- "sa-east-1": "elasticmapreduce.sa-east-1.amazonaws.com",
+ "sa-east-1": "sa-east-1.elasticmapreduce.amazonaws.com",
"us-east-1": "elasticmapreduce.us-east-1.amazonaws.com",
- "us-gov-west-1": "elasticmapreduce.us-gov-west-1.amazonaws.com",
- "us-west-1": "elasticmapreduce.us-west-1.amazonaws.com",
- "us-west-2": "elasticmapreduce.us-west-2.amazonaws.com"
+ "us-gov-west-1": "us-gov-west-1.elasticmapreduce.amazonaws.com",
+ "us-west-1": "us-west-1.elasticmapreduce.amazonaws.com",
+ "us-west-2": "us-west-2.elasticmapreduce.amazonaws.com"
},
"elastictranscoder": {
"ap-northeast-1": "elastictranscoder.ap-northeast-1.amazonaws.com",
@@ -186,7 +186,7 @@
"cn-north-1": "rds.cn-north-1.amazonaws.com.cn",
"eu-west-1": "rds.eu-west-1.amazonaws.com",
"sa-east-1": "rds.sa-east-1.amazonaws.com",
- "us-east-1": "rds.us-east-1.amazonaws.com",
+ "us-east-1": "rds.amazonaws.com",
"us-gov-west-1": "rds.us-gov-west-1.amazonaws.com",
"us-west-1": "rds.us-west-1.amazonaws.com",
"us-west-2": "rds.us-west-2.amazonaws.com"
@@ -249,16 +249,16 @@
"us-west-2": "sns.us-west-2.amazonaws.com"
},
"sqs": {
- "ap-northeast-1": "sqs.ap-northeast-1.amazonaws.com",
- "ap-southeast-1": "sqs.ap-southeast-1.amazonaws.com",
- "ap-southeast-2": "sqs.ap-southeast-2.amazonaws.com",
+ "ap-northeast-1": "ap-northeast-1.queue.amazonaws.com",
+ "ap-southeast-1": "ap-southeast-1.queue.amazonaws.com",
+ "ap-southeast-2": "ap-southeast-2.queue.amazonaws.com",
"cn-north-1": "sqs.cn-north-1.amazonaws.com.cn",
- "eu-west-1": "sqs.eu-west-1.amazonaws.com",
- "sa-east-1": "sqs.sa-east-1.amazonaws.com",
- "us-east-1": "sqs.us-east-1.amazonaws.com",
- "us-gov-west-1": "sqs.us-gov-west-1.amazonaws.com",
- "us-west-1": "sqs.us-west-1.amazonaws.com",
- "us-west-2": "sqs.us-west-2.amazonaws.com"
+ "eu-west-1": "eu-west-1.queue.amazonaws.com",
+ "sa-east-1": "sa-east-1.queue.amazonaws.com",
+ "us-east-1": "queue.amazonaws.com",
+ "us-gov-west-1": "us-gov-west-1.queue.amazonaws.com",
+ "us-west-1": "us-west-1.queue.amazonaws.com",
+ "us-west-2": "us-west-2.queue.amazonaws.com"
},
"storagegateway": {
"ap-northeast-1": "storagegateway.ap-northeast-1.amazonaws.com",
diff --git a/boto/gs/connection.py b/boto/gs/connection.py
index 104ed45d..c9a43bd6 100755
--- a/boto/gs/connection.py
+++ b/boto/gs/connection.py
@@ -103,3 +103,27 @@ class GSConnection(S3Connection):
raise self.provider.storage_response_error(
response.status, response.reason, body)
+ def get_bucket(self, bucket_name, validate=True, headers=None):
+ """
+ Retrieves a bucket by name.
+
+ If the bucket does not exist, an ``S3ResponseError`` will be raised. If
+ you are unsure if the bucket exists or not, you can use the
+ ``S3Connection.lookup`` method, which will either return a valid bucket
+ or ``None``.
+
+ :type bucket_name: string
+ :param bucket_name: The name of the bucket
+
+ :type headers: dict
+ :param headers: Additional headers to pass along with the request to
+ AWS.
+
+ :type validate: boolean
+ :param validate: If ``True``, it will try to fetch all keys within the
+ given bucket. (Default: ``True``)
+ """
+ bucket = self.bucket_class(self, bucket_name)
+ if validate:
+ bucket.get_all_keys(headers, maxkeys=0)
+ return bucket
diff --git a/boto/mws/response.py b/boto/mws/response.py
index 064f5d7f..0960e46e 100644
--- a/boto/mws/response.py
+++ b/boto/mws/response.py
@@ -609,7 +609,7 @@ class ProductCategory(ResponseElement):
class GetProductCategoriesResult(ResponseElement):
- Self = Element(ProductCategory)
+ Self = ElementList(ProductCategory)
class GetProductCategoriesForSKUResult(GetProductCategoriesResult):
diff --git a/boto/rds/__init__.py b/boto/rds/__init__.py
index c577a1bb..edf6237a 100644
--- a/boto/rds/__init__.py
+++ b/boto/rds/__init__.py
@@ -32,6 +32,8 @@ from boto.rds.regioninfo import RDSRegionInfo
from boto.rds.dbsubnetgroup import DBSubnetGroup
from boto.rds.vpcsecuritygroupmembership import VPCSecurityGroupMembership
from boto.regioninfo import get_regions
+from boto.rds.logfile import LogFile
+
def regions():
"""
@@ -1074,6 +1076,27 @@ class RDSConnection(AWSQueryConnection):
return self.get_list('DescribeDBSnapshots', params,
[('DBSnapshot', DBSnapshot)])
+ def get_all_logs(self, dbinstance_id=None):
+ """
+ Get all log files
+
+ :type instance_id: str
+ :param instance_id: The identifier of a DBInstance. If provided,
+ only the :class:`boto.rds.logfile.LogFile` related
+ to that instance will be returned. If not
+ provided, all logfiles will be returned.
+
+ :rtype: list
+ :return: A list of :class:`boto.rds.logfile.LogFile`
+ """
+ params = {}
+ if dbinstance_id:
+ params['DBInstanceIdentifier'] = dbinstance_id
+ params['MaxRecords'] = 26
+
+ return self.get_list('DescribeDBLogFiles', params,
+ [('DescribeDBLogFilesDetails',LogFile)])
+
def create_dbsnapshot(self, snapshot_id, dbinstance_id):
"""
Create a new DB snapshot.
diff --git a/boto/rds/logfile.py b/boto/rds/logfile.py
new file mode 100644
index 00000000..176d37f2
--- /dev/null
+++ b/boto/rds/logfile.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/
+# Copyright (c) 2014 Jumping Qu http://newrice.blogspot.com/
+#
+# 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.
+
+class LogFile(object):
+
+ def __init__(self, connection=None):
+ self.connection = connection
+ self.size = None
+ self.log_filename = None
+ self.last_written = None
+
+ def __repr__(self):
+ #return '(%s, %s, %s)' % (self.logfilename, self.size, self.lastwritten)
+ return '%s' % (self.log_filename)
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'LastWritten':
+ self.last_written = value
+ elif name == 'LogFileName':
+ self.log_filename = value
+ elif name == 'Size':
+ self.size = value
+ else:
+ setattr(self, name, value)
diff --git a/boto/route53/connection.py b/boto/route53/connection.py
index f9ba5bea..f0ac6573 100644
--- a/boto/route53/connection.py
+++ b/boto/route53/connection.py
@@ -54,10 +54,10 @@ class Route53Connection(AWSAuthConnection):
DefaultHost = 'route53.amazonaws.com'
"""The default Route53 API endpoint to connect to."""
- Version = '2012-02-29'
+ Version = '2013-04-01'
"""Route53 API version."""
- XMLNameSpace = 'https://route53.amazonaws.com/doc/2012-02-29/'
+ XMLNameSpace = 'https://route53.amazonaws.com/doc/2013-04-01/'
"""XML schema for this Route53 API version."""
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
diff --git a/boto/route53/record.py b/boto/route53/record.py
index ab436db9..81b707b3 100644
--- a/boto/route53/record.py
+++ b/boto/route53/record.py
@@ -35,7 +35,7 @@ class ResourceRecordSets(ResultSet):
"""
ChangeResourceRecordSetsBody = """<?xml version="1.0" encoding="UTF-8"?>
- <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2012-02-29/">
+ <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<ChangeBatch>
<Comment>%(comment)s</Comment>
<Changes>%(changes)s</Changes>
@@ -71,7 +71,7 @@ class ResourceRecordSets(ResultSet):
Add a change request to the set.
:type action: str
- :param action: The action to perform ('CREATE'|'DELETE')
+ :param action: The action to perform ('CREATE'|'DELETE'|'UPSERT')
:type name: str
:param name: The name of the domain you want to perform the action on.
diff --git a/boto/route53/zone.py b/boto/route53/zone.py
index 75cefd48..bb6907da 100644
--- a/boto/route53/zone.py
+++ b/boto/route53/zone.py
@@ -34,8 +34,8 @@ class Zone(object):
"""
A Route53 Zone.
- :ivar Route53Connection route53connection
- :ivar str Id: The ID of the hosted zone.
+ :ivar route53connection: A :class:`boto.route53.connection.Route53Connection` connection
+ :ivar id: The ID of the hosted zone
"""
def __init__(self, route53connection, zone_dict):
self.route53connection = route53connection
diff --git a/boto/s3/connection.py b/boto/s3/connection.py
index a84c701d..d6b3b52f 100644
--- a/boto/s3/connection.py
+++ b/boto/s3/connection.py
@@ -439,6 +439,23 @@ class S3Connection(AWSAuthConnection):
``S3Connection.lookup`` method, which will either return a valid bucket
or ``None``.
+ If ``validate=False`` is passed, no request is made to the service (no
+ charge/communication delay). This is only safe to do if you are **sure**
+ the bucket exists.
+
+ If the default ``validate=True`` is passed, a request is made to the
+ service to ensure the bucket exists. Prior to Boto v2.25.0, this fetched
+ a list of keys (but with a max limit set to ``0``, always returning an empty
+ list) in the bucket (& included better error messages), at an
+ increased expense. As of Boto v2.25.0, this now performs a HEAD request
+ (less expensive but worse error messages).
+
+ If you were relying on parsing the error message before, you should call
+ something like::
+
+ bucket = conn.get_bucket('<bucket_name>', validate=False)
+ bucket.get_all_keys(maxkeys=0)
+
:type bucket_name: string
:param bucket_name: The name of the bucket
@@ -447,13 +464,58 @@ class S3Connection(AWSAuthConnection):
AWS.
:type validate: boolean
- :param validate: If ``True``, it will try to fetch all keys within the
- given bucket. (Default: ``True``)
+ :param validate: If ``True``, it will try to verify the bucket exists
+ on the service-side. (Default: ``True``)
"""
- bucket = self.bucket_class(self, bucket_name)
if validate:
- bucket.get_all_keys(headers, maxkeys=0)
- return bucket
+ return self.head_bucket(bucket_name, headers=headers)
+ else:
+ return self.bucket_class(self, bucket_name)
+
+ def head_bucket(self, bucket_name, headers=None):
+ """
+ Determines if a bucket exists by name.
+
+ If the bucket does not exist, an ``S3ResponseError`` will be raised.
+
+ :type bucket_name: string
+ :param bucket_name: The name of the bucket
+
+ :type headers: dict
+ :param headers: Additional headers to pass along with the request to
+ AWS.
+
+ :returns: A <Bucket> object
+ """
+ response = self.make_request('HEAD', bucket_name, headers=headers)
+ body = response.read()
+ if response.status == 200:
+ return self.bucket_class(self, bucket_name)
+ elif response.status == 403:
+ # For backward-compatibility, we'll populate part of the exception
+ # with the most-common default.
+ err = self.provider.storage_response_error(
+ response.status,
+ response.reason,
+ body
+ )
+ err.error_code = 'AccessDenied'
+ err.error_message = 'Access Denied'
+ raise err
+ elif response.status == 404:
+ # For backward-compatibility, we'll populate part of the exception
+ # with the most-common default.
+ err = self.provider.storage_response_error(
+ response.status,
+ response.reason,
+ body
+ )
+ err.error_code = 'NoSuchBucket'
+ err.error_message = 'The specified bucket does not exist'
+ raise err
+ else:
+ raise self.provider.storage_response_error(
+ response.status, response.reason, body)
def lookup(self, bucket_name, validate=True, headers=None):
"""
diff --git a/boto/sqs/connection.py b/boto/sqs/connection.py
index 8444c84c..8fc69306 100644
--- a/boto/sqs/connection.py
+++ b/boto/sqs/connection.py
@@ -115,7 +115,7 @@ class SQSConnection(AWSQueryConnection):
Gets one or all attributes of a Queue
:type queue: A Queue object
- :param queue: The SQS queue to be deleted
+ :param queue: The SQS queue to get attributes for
:type attribute: str
:type attribute: The specific attribute requested. If not
diff --git a/docs/source/index.rst b/docs/source/index.rst
index b8d842f7..85afdb32 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -67,7 +67,7 @@ Currently Supported Services
* **Networking**
- * Route 53 -- (:doc:`API Reference <ref/route53>`)
+ * :doc:`Route 53 <route53_tut>` -- (:doc:`API Reference <ref/route53>`)
* :doc:`Virtual Private Cloud (VPC) <vpc_tut>` -- (:doc:`API Reference <ref/vpc>`)
* :doc:`Elastic Load Balancing (ELB) <elb_tut>` -- (:doc:`API Reference <ref/elb>`)
@@ -116,6 +116,8 @@ Release Notes
.. toctree::
:titlesonly:
+ releasenotes/v2.25.0
+ releasenotes/v2.24.0
releasenotes/v2.23.0
releasenotes/v2.22.1
releasenotes/v2.22.0
@@ -184,6 +186,7 @@ Release Notes
vpc_tut
elb_tut
s3_tut
+ route53_tut
boto_config_tut
documentation
contributing
diff --git a/docs/source/ref/route53.rst b/docs/source/ref/route53.rst
index 92771fcc..f3a03bab 100644
--- a/docs/source/ref/route53.rst
+++ b/docs/source/ref/route53.rst
@@ -13,7 +13,7 @@ boto.route53.connection
:undoc-members:
boto.route53.exception
--------------------
+----------------------
.. automodule:: boto.route53.exception
:members:
@@ -27,7 +27,7 @@ boto.route53.record
:undoc-members:
boto.route53.zone
-------------------------
+-----------------
.. automodule:: boto.route53.zone
:members:
diff --git a/docs/source/releasenotes/v2.25.0.rst b/docs/source/releasenotes/v2.25.0.rst
new file mode 100644
index 00000000..c925d2cf
--- /dev/null
+++ b/docs/source/releasenotes/v2.25.0.rst
@@ -0,0 +1,57 @@
+boto v2.25.0
+============
+
+:date: 2014/02/07
+
+This release includes Amazon Route53 service and documentation updates,
+preliminary log file support for Amazon Relational Database Service (RDS), as
+well as various other small fixes. Also included is an opt-in to use signature
+version 4 with Amazon EC2.
+
+**IMPORTANT** - This release also include a **SIGNIFICANT** underlying change
+to the Amazon S3 ``get_bucket`` method, to addresses the blog post by AppNeta_.
+We've altered the default behavior to now perform a ``HEAD`` on the bucket, in
+place of the old ``GET`` behavior (which would fetch a zero-length list of
+keys).
+
+This should reduce all users costs & should also be *mostly*
+backward-compatible. **HOWEVER**, if you were previously parsing the exception
+message from ``S3Connection.get_bucket``, you *will* have to change your code
+(see the S3 tutorial for details). ``HEAD`` does *not* return as detailed of
+error messages & while we've attempted to patch over as much of the differences
+as we can, there may still be edge-cases over the prior behavior.
+
+.. _AppNeta: http://www.appneta.com/blog/s3-list-get-bucket-default/
+
+
+Features
+--------
+
+* Add support for Route53 API version 2013-04-01 (:issue:`2080`, :sha:`600dcd0`)
+* Add option to opt-in for EC2 SigV4 (:issue:`2074`, :sha:`4d780bd`)
+* Add Autoscale feature to get all adjustment types (:issue:`2058`,
+ :issue:`1538`, :sha:`b9c7e15`)
+* Add Route53 unit tests (:issue:`2066`, :sha:`e859576`)
+* Add a basic Route53 tutorial (:issue:`2060`, :sha:`f0ad46b`)
+* Add Autoscale associated public IP to launch configuration (:issue:`2051`,
+ :issue:`2028`, :issue:`2029`, :sha:`c58bda6`)
+* Add option to pass VPC zone identifiers as a Python list (:issue:`2047`,
+ :issue:`1772`, :sha:`07ef9e1`)
+* Add RDS call to get all log files (:issue:`2040`, :issue:`1994`,
+ :sha:`925b8cb`)
+
+
+Bugfixes
+--------
+
+* Changed S3 ``get_bucket`` to use ``HEAD`` in place of ``GET``. (:issue:`2078`,
+ :issue:`2082`, :sha:`016be83`)
+* Fix EMR's describe_cluster_command. (:issue:`2034`, :sha:`1c5621e`)
+* Tutorial small code fix (:issue:`2072`, :sha:`38e7db1`)
+* Fix CloudFront string representation (:issue:`2069`, :sha:`885c397`)
+* Route53 doc cleanup (:issue:`2059`, :sha:`d2fc38e`)
+* Fix MWS parsing of GetProductCategoriesForASIN response. (:issue:`2024`,
+ :sha:`0af08ce`)
+* Fix SQS docs for get_queue_attributes (:issue:`2061`, :sha:`1cdc326`)
+* Don't insert a '?' in URLs unless there is a query string (:issue:`2042`,
+ :issue:`1943`, :sha:`c15ce60`)
diff --git a/docs/source/route53_tut.rst b/docs/source/route53_tut.rst
new file mode 100644
index 00000000..f24dfc28
--- /dev/null
+++ b/docs/source/route53_tut.rst
@@ -0,0 +1,87 @@
+.. _route53_tut.rst:
+
+===========================================
+An Introduction to boto's Route53 interface
+===========================================
+
+This tutorial focuses on the boto interface to Route53 from Amazon Web
+Services. This tutorial assumes that you have already downloaded and installed
+boto.
+
+Route53 is a Domain Name System (DNS) web service. It can be used to route
+requests to services running on AWS such as EC2 instances or load balancers, as
+well as to external services. Route53 also allows you to have automated checks
+to send requests where you require them.
+
+In this tutorial, we will be setting up our services for *example.com*.
+
+Creating a connection
+---------------------
+
+To start using Route53 you will need to create a connection to the service as
+normal:
+
+>>> import boto.route53
+>>> conn = boto.route53.connect_to_region('us-west-2')
+
+You will be using this conn object for the remainder of the tutorial to send
+commands to Route53.
+
+Working with domain names
+-------------------------
+
+You can manipulate domains through a zone object. For example, you can create a
+domain name:
+
+>>> zone = conn.create_zone("example.com.")
+
+Note that trailing dot on that domain name is significant. This is known as a
+fully qualified domain name (`FQDN <http://en.wikipedia.org/wiki/Fully_qualified_domain_name>`_).
+
+>>> zone
+<Zone:example.com.>
+
+You can also retrieve all your domain names:
+
+>>> conn.get_zones()
+[<Zone:example.com.>]
+
+Or you can retrieve a single domain:
+
+>>> conn.get_zone("example.com.")
+<Zone:example.com.>
+
+Finally, you can retrieve the list of nameservers that AWS has setup for this
+domain name as follows:
+
+>>> zone.get_nameservers()
+[u'ns-1000.awsdns-42.org.', u'ns-1001.awsdns-30.com.', u'ns-1002.awsdns-59.net.', u'ns-1003.awsdns-09.co.uk.']
+
+Once you have finished configuring your domain name, you will need to change
+your nameservers at your registrar to point to those nameservers for Route53 to
+work.
+
+Setting up dumb records
+-----------------------
+
+You can also add, update and delete records on a zone:
+
+>>> status = a.add_record("MX", "example.com.", "10 mail.isp.com")
+
+When you send a change request through, the status of the update will be
+*PENDING*:
+
+>>> status
+<Status:PENDING>
+
+You can call the API again and ask for the current status as follows:
+
+>>> status.update()
+'INSYNC'
+
+>>> status
+<Status:INSYNC>
+
+When the status has changed to *INSYNC*, the change has been propagated to
+remote servers
+
diff --git a/docs/source/s3_tut.rst b/docs/source/s3_tut.rst
index c87c4f73..fc5fd27a 100644
--- a/docs/source/s3_tut.rst
+++ b/docs/source/s3_tut.rst
@@ -158,7 +158,7 @@ exists within a bucket, you can skip the check for a key on the server.
>>> possible_key = b.get_key('mykey') # substitute your key name here
# Won't hit the API.
- >>> key_we_know_is_there = b.get_key(validate=False)
+ >>> key_we_know_is_there = b.get_key('mykey', validate=False)
Accessing A Bucket
@@ -175,6 +175,26 @@ override this behavior by passing ``validate=False``.::
>>> nonexistent = conn.get_bucket('i-dont-exist-at-all', validate=False)
+.. versionchanged:: 2.25.0
+.. warning::
+
+ If ``validate=False`` is passed, no request is made to the service (no
+ charge/communication delay). This is only safe to do if you are **sure**
+ the bucket exists.
+
+ If the default ``validate=True`` is passed, a request is made to the
+ service to ensure the bucket exists. Prior to Boto v2.25.0, this fetched
+ a list of keys (but with a max limit set to ``0``, always returning an empty
+ list) in the bucket (& included better error messages), at an
+ increased expense. As of Boto v2.25.0, this now performs a HEAD request
+ (less expensive but worse error messages).
+
+ If you were relying on parsing the error message before, you should call
+ something like::
+
+ bucket = conn.get_bucket('<bucket_name>', validate=False)
+ bucket.get_all_keys(maxkeys=0)
+
If the bucket does not exist, a ``S3ResponseError`` will commonly be thrown. If
you'd rather not deal with any exceptions, you can use the ``lookup`` method.::
@@ -184,6 +204,7 @@ you'd rather not deal with any exceptions, you can use the ``lookup`` method.::
...
No such bucket!
+
Deleting A Bucket
-----------------
diff --git a/tests/integration/mws/test.py b/tests/integration/mws/test.py
index 2e7c2b69..394fa0e3 100644
--- a/tests/integration/mws/test.py
+++ b/tests/integration/mws/test.py
@@ -71,8 +71,9 @@ class MWSTestCase(unittest.TestCase):
response = self.mws.get_product_categories_for_asin(
MarketplaceId=self.marketplace_id,
ASIN=asin)
- result = response._result
- self.assertTrue(int(result.Self.ProductCategoryId) == 21)
+ self.assertTrue(len(response._result.Self) == 2)
+ categoryids = [x.ProductCategoryId for x in response._result.Self]
+ self.assertSequenceEqual(categoryids, ['285856', '21'])
@unittest.skipUnless(simple and isolator, "skipping simple test")
def test_list_matching_products(self):
diff --git a/tests/unit/auth/test_sigv4.py b/tests/unit/auth/test_sigv4.py
index a35bfd68..a52cf44a 100644
--- a/tests/unit/auth/test_sigv4.py
+++ b/tests/unit/auth/test_sigv4.py
@@ -28,7 +28,9 @@ from tests.unit import unittest, MockServiceWithConfigTestCase
from boto.auth import HmacAuthV4Handler
from boto.auth import S3HmacAuthV4Handler
from boto.auth import detect_potential_s3sigv4
+from boto.auth import detect_potential_sigv4
from boto.connection import HTTPRequest
+from boto.regioninfo import RegionInfo
class TestSigV4Handler(unittest.TestCase):
@@ -41,6 +43,13 @@ class TestSigV4Handler(unittest.TestCase):
'/-/vaults/foo/archives', None, {},
{'x-amz-glacier-version': '2012-06-01'}, '')
+ def test_not_adding_empty_qs(self):
+ self.provider.security_token = None
+ auth = HmacAuthV4Handler('glacier.us-east-1.amazonaws.com', Mock(), self.provider)
+ req = copy.copy(self.request)
+ auth.add_auth(req)
+ self.assertEqual(req.path, '/-/vaults/foo/archives')
+
def test_inner_whitespace_is_collapsed(self):
auth = HmacAuthV4Handler('glacier.us-east-1.amazonaws.com',
Mock(), self.provider)
@@ -448,7 +457,19 @@ class FakeS3Connection(object):
pass
-class TestSigV4OptIn(MockServiceWithConfigTestCase):
+class FakeEC2Connection(object):
+ def __init__(self, *args, **kwargs):
+ self.region = kwargs.pop('region', None)
+
+ @detect_potential_sigv4
+ def _required_auth_capability(self):
+ return ['nope']
+
+ def _mexe(self, *args, **kwargs):
+ pass
+
+
+class TestS3SigV4OptIn(MockServiceWithConfigTestCase):
connection_class = FakeS3Connection
def test_sigv4_opt_out(self):
@@ -457,7 +478,7 @@ class TestSigV4OptIn(MockServiceWithConfigTestCase):
self.assertEqual(fake._required_auth_capability(), ['nope'])
def test_sigv4_non_optional(self):
- # Default is opt-out.
+ # Requires SigV4.
fake = FakeS3Connection(host='s3.cn-north-1.amazonaws.com.cn')
self.assertEqual(fake._required_auth_capability(), ['hmac-v4-s3'])
@@ -476,3 +497,44 @@ class TestSigV4OptIn(MockServiceWithConfigTestCase):
self.environ['S3_USE_SIGV4'] = True
fake = FakeS3Connection(host='s3.amazonaws.com')
self.assertEqual(fake._required_auth_capability(), ['hmac-v4-s3'])
+
+
+class TestSigV4OptIn(MockServiceWithConfigTestCase):
+ connection_class = FakeEC2Connection
+
+ def setUp(self):
+ super(TestSigV4OptIn, self).setUp()
+ self.standard_region = RegionInfo(
+ name='us-west-2',
+ endpoint='ec2.us-west-2.amazonaws.com'
+ )
+ self.sigv4_region = RegionInfo(
+ name='cn-north-1',
+ endpoint='ec2.cn-north-1.amazonaws.com.cn'
+ )
+
+ def test_sigv4_opt_out(self):
+ # Default is opt-out.
+ fake = FakeEC2Connection(region=self.standard_region)
+ self.assertEqual(fake._required_auth_capability(), ['nope'])
+
+ def test_sigv4_non_optional(self):
+ # Requires SigV4.
+ fake = FakeEC2Connection(region=self.sigv4_region)
+ self.assertEqual(fake._required_auth_capability(), ['hmac-v4'])
+
+ def test_sigv4_opt_in_config(self):
+ # Opt-in via the config.
+ self.config = {
+ 'ec2': {
+ 'use-sigv4': True,
+ },
+ }
+ fake = FakeEC2Connection(region=self.standard_region)
+ self.assertEqual(fake._required_auth_capability(), ['hmac-v4'])
+
+ def test_sigv4_opt_in_env(self):
+ # Opt-in via the ENV.
+ self.environ['EC2_USE_SIGV4'] = True
+ fake = FakeEC2Connection(region=self.standard_region)
+ self.assertEqual(fake._required_auth_capability(), ['hmac-v4'])
diff --git a/tests/unit/ec2/autoscale/test_group.py b/tests/unit/ec2/autoscale/test_group.py
index f154455d..ef106750 100644
--- a/tests/unit/ec2/autoscale/test_group.py
+++ b/tests/unit/ec2/autoscale/test_group.py
@@ -33,7 +33,7 @@ from boto.ec2.autoscale.tag import Tag
from boto.ec2.blockdevicemapping import EBSBlockDeviceType, BlockDeviceMapping
-from boto.ec2.autoscale import launchconfig
+from boto.ec2.autoscale import launchconfig, LaunchConfiguration
class TestAutoScaleGroup(AWSMockServiceTestCase):
connection_class = AutoScaleConnection
@@ -69,6 +69,42 @@ class TestAutoScaleGroup(AWSMockServiceTestCase):
'InstanceId': 'test-id',
}, ignore_params_values=['Version'])
+ def test_autoscaling_group_single_vpc_zone_identifier(self):
+ self.set_http_response(status_code=200)
+ autoscale = AutoScalingGroup(
+ name='foo',
+ vpc_zone_identifier='vpc_zone_1')
+ self.service_connection.create_auto_scaling_group(autoscale)
+ self.assert_request_parameters({
+ 'Action': 'CreateAutoScalingGroup',
+ 'AutoScalingGroupName': 'foo',
+ 'VPCZoneIdentifier': 'vpc_zone_1',
+ }, ignore_params_values=['MaxSize', 'MinSize', 'LaunchConfigurationName', 'Version'])
+
+ def test_autoscaling_group_vpc_zone_identifier_list(self):
+ self.set_http_response(status_code=200)
+ autoscale = AutoScalingGroup(
+ name='foo',
+ vpc_zone_identifier=['vpc_zone_1', 'vpc_zone_2'])
+ self.service_connection.create_auto_scaling_group(autoscale)
+ self.assert_request_parameters({
+ 'Action': 'CreateAutoScalingGroup',
+ 'AutoScalingGroupName': 'foo',
+ 'VPCZoneIdentifier': 'vpc_zone_1,vpc_zone_2',
+ }, ignore_params_values=['MaxSize', 'MinSize', 'LaunchConfigurationName', 'Version'])
+
+ def test_autoscaling_group_vpc_zone_identifier_multi(self):
+ self.set_http_response(status_code=200)
+ autoscale = AutoScalingGroup(
+ name='foo',
+ vpc_zone_identifier='vpc_zone_1,vpc_zone_2')
+ self.service_connection.create_auto_scaling_group(autoscale)
+ self.assert_request_parameters({
+ 'Action': 'CreateAutoScalingGroup',
+ 'AutoScalingGroupName': 'foo',
+ 'VPCZoneIdentifier': 'vpc_zone_1,vpc_zone_2',
+ }, ignore_params_values=['MaxSize', 'MinSize', 'LaunchConfigurationName', 'Version'])
+
class TestAutoScaleGroupHonorCooldown(AWSMockServiceTestCase):
connection_class = AutoScaleConnection
@@ -227,6 +263,73 @@ class TestDescribeTerminationPolicies(AWSMockServiceTestCase):
['ClosestToNextInstanceHour', 'Default',
'NewestInstance', 'OldestInstance', 'OldestLaunchConfiguration'])
+class TestLaunchConfigurationDescribe(AWSMockServiceTestCase):
+ connection_class = AutoScaleConnection
+
+ def default_body(self):
+ # This is a dummy response
+ return """
+ <DescribeLaunchConfigurationsResponse>
+ <DescribeLaunchConfigurationsResult>
+ <LaunchConfigurations>
+ <member>
+ <AssociatePublicIpAddress>true</AssociatePublicIpAddress>
+ <SecurityGroups/>
+ <CreatedTime>2013-01-21T23:04:42.200Z</CreatedTime>
+ <KernelId/>
+ <LaunchConfigurationName>my-test-lc</LaunchConfigurationName>
+ <UserData/>
+ <InstanceType>m1.small</InstanceType>
+ <LaunchConfigurationARN>arn:aws:autoscaling:us-east-1:803981987763:launchConfiguration:9dbbbf87-6141-428a-a409-0752edbe6cad:launchConfigurationName/my-test-lc</LaunchConfigurationARN>
+ <BlockDeviceMappings/>
+ <ImageId>ami-514ac838</ImageId>
+ <KeyName/>
+ <RamdiskId/>
+ <InstanceMonitoring>
+ <Enabled>true</Enabled>
+ </InstanceMonitoring>
+ <EbsOptimized>false</EbsOptimized>
+ </member>
+ </LaunchConfigurations>
+ </DescribeLaunchConfigurationsResult>
+ <ResponseMetadata>
+ <RequestId>d05a22f8-b690-11e2-bf8e-2113fEXAMPLE</RequestId>
+ </ResponseMetadata>
+ </DescribeLaunchConfigurationsResponse>
+ """
+
+ def test_get_all_launch_configurations(self):
+ self.set_http_response(status_code=200)
+
+ response = self.service_connection.get_all_launch_configurations()
+ self.assertTrue(isinstance(response, list))
+ self.assertEqual(len(response), 1)
+ self.assertTrue(isinstance(response[0], LaunchConfiguration))
+
+ self.assertEqual(response[0].associate_public_ip_address, True)
+ self.assertEqual(response[0].name, "my-test-lc")
+ self.assertEqual(response[0].instance_type, "m1.small")
+ self.assertEqual(response[0].launch_configuration_arn, "arn:aws:autoscaling:us-east-1:803981987763:launchConfiguration:9dbbbf87-6141-428a-a409-0752edbe6cad:launchConfigurationName/my-test-lc")
+ self.assertEqual(response[0].image_id, "ami-514ac838")
+ self.assertTrue(isinstance(response[0].instance_monitoring, launchconfig.InstanceMonitoring))
+ self.assertEqual(response[0].instance_monitoring.enabled, 'true')
+ self.assertEqual(response[0].ebs_optimized, False)
+
+ self.assert_request_parameters({
+ 'Action': 'DescribeLaunchConfigurations',
+ }, ignore_params_values=['Version'])
+
+ def test_get_all_configuration_limited(self):
+ self.set_http_response(status_code=200)
+
+ response = self.service_connection.get_all_launch_configurations(max_records=10, names=["my-test1", "my-test2"])
+ self.assert_request_parameters({
+ 'Action': 'DescribeLaunchConfigurations',
+ 'MaxRecords': 10,
+ 'LaunchConfigurationNames.member.1': 'my-test1',
+ 'LaunchConfigurationNames.member.2': 'my-test2'
+ }, ignore_params_values=['Version'])
+
class TestLaunchConfiguration(AWSMockServiceTestCase):
connection_class = AutoScaleConnection
@@ -544,6 +647,44 @@ class TestGetAccountLimits(AWSMockServiceTestCase):
self.assertEqual(limits.max_autoscaling_groups, 6)
self.assertEqual(limits.max_launch_configurations, 3)
+class TestGetAdjustmentTypes(AWSMockServiceTestCase):
+ connection_class = AutoScaleConnection
+
+ def setUp(self):
+ super(TestGetAdjustmentTypes, self).setUp()
+
+ def default_body(self):
+ return """
+ <DescribeAdjustmentTypesResponse xmlns="http://autoscaling.amazonaws.com/doc/201-01-01/">
+ <DescribeAdjustmentTypesResult>
+ <AdjustmentTypes>
+ <member>
+ <AdjustmentType>ChangeInCapacity</AdjustmentType>
+ </member>
+ <member>
+ <AdjustmentType>ExactCapacity</AdjustmentType>
+ </member>
+ <member>
+ <AdjustmentType>PercentChangeInCapacity</AdjustmentType>
+ </member>
+ </AdjustmentTypes>
+ </DescribeAdjustmentTypesResult>
+ <ResponseMetadata>
+ <RequestId>requestId</RequestId>
+ </ResponseMetadata>
+ </DescribeAdjustmentTypesResponse>
+ """
+ def test_autoscaling_adjustment_types(self):
+ self.set_http_response(status_code=200)
+ response = self.service_connection.get_all_adjustment_types()
+ self.assert_request_parameters({
+ 'Action': 'DescribeAdjustmentTypes'
+ }, ignore_params_values=['Version'])
+
+ self.assertTrue(isinstance(response, list))
+ self.assertEqual(response[0].adjustment_type, "ChangeInCapacity")
+ self.assertEqual(response[1].adjustment_type, "ExactCapacity")
+ self.assertEqual(response[2].adjustment_type, "PercentChangeInCapacity")
if __name__ == '__main__':
unittest.main()
diff --git a/tests/unit/emr/test_connection.py b/tests/unit/emr/test_connection.py
index 8116a121..9c46c7dd 100644
--- a/tests/unit/emr/test_connection.py
+++ b/tests/unit/emr/test_connection.py
@@ -24,10 +24,11 @@ from __future__ import with_statement
import boto.utils
from datetime import datetime
+from time import time
from tests.unit import AWSMockServiceTestCase
from boto.emr.connection import EmrConnection
-from boto.emr.emrobject import JobFlowStepList, StepSummaryList
+from boto.emr.emrobject import BootstrapAction, BootstrapActionList, ClusterStatus, ClusterSummaryList, ClusterSummary, ClusterTimeline, InstanceInfo, InstanceList, InstanceGroupInfo, InstanceGroup, InstanceGroupList, JobFlow, JobFlowStepList, Step, StepSummaryList
# These tests are just checking the basic structure of
# the Elastic MapReduce code, by picking a few calls
@@ -38,7 +39,49 @@ class TestListClusters(AWSMockServiceTestCase):
connection_class = EmrConnection
def default_body(self):
- return """<ListClustersOutput><Clusters></Clusters></ListClustersOutput>"""
+ return """
+<ListClustersResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31">
+ <ListClustersResult>
+ <Clusters>
+ <member>
+ <Id>j-aaaaaaaaaaaa</Id>
+ <Status>
+ <StateChangeReason>
+ <Message>Terminated by user request</Message>
+ <Code>USER_REQUEST</Code>
+ </StateChangeReason>
+ <State>TERMINATED</State>
+ <Timeline>
+ <CreationDateTime>2014-01-24T01:21:21Z</CreationDateTime>
+ <ReadyDateTime>2014-01-24T01:25:26Z</ReadyDateTime>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ </Timeline>
+ </Status>
+ <Name>analytics test</Name>
+ </member>
+ <member>
+ <Id>j-aaaaaaaaaaaab</Id>
+ <Status>
+ <StateChangeReason>
+ <Message>Terminated by user request</Message>
+ <Code>USER_REQUEST</Code>
+ </StateChangeReason>
+ <State>TERMINATED</State>
+ <Timeline>
+ <CreationDateTime>2014-01-21T02:53:08Z</CreationDateTime>
+ <ReadyDateTime>2014-01-21T02:56:40Z</ReadyDateTime>
+ <EndDateTime>2014-01-21T03:40:22Z</EndDateTime>
+ </Timeline>
+ </Status>
+ <Name>test job</Name>
+ </member>
+ </Clusters>
+ </ListClustersResult>
+ <ResponseMetadata>
+ <RequestId>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</RequestId>
+ </ResponseMetadata>
+</ListClustersResponse>
+ """
def test_list_clusters(self):
self.set_http_response(status_code=200)
@@ -49,6 +92,21 @@ class TestListClusters(AWSMockServiceTestCase):
'Version': '2009-03-31',
})
+ self.assertTrue(isinstance(response, ClusterSummaryList))
+
+ self.assertEqual(len(response.clusters), 2)
+ self.assertTrue(isinstance(response.clusters[0], ClusterSummary))
+ self.assertEqual(response.clusters[0].name, 'analytics test')
+
+ self.assertTrue(isinstance(response.clusters[0].status, ClusterStatus))
+
+ self.assertTrue(isinstance(response.clusters[0].status.timeline, ClusterTimeline))
+
+ self.assertEqual(response.clusters[0].status.timeline.creationdatetime, '2014-01-24T01:21:21Z')
+ self.assertEqual(response.clusters[0].status.timeline.readydatetime, '2014-01-24T01:25:26Z')
+ self.assertEqual(response.clusters[0].status.timeline.enddatetime, '2014-01-24T02:19:46Z')
+
+
def test_list_clusters_created_before(self):
self.set_http_response(status_code=200)
@@ -92,7 +150,60 @@ class TestListInstanceGroups(AWSMockServiceTestCase):
connection_class = EmrConnection
def default_body(self):
- return """<ListInstanceGroupsOutput><InstanceGroups></InstanceGroups></ListInstanceGroupsOutput>"""
+ return """
+<ListInstanceGroupsResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31">
+ <ListInstanceGroupsResult>
+ <InstanceGroups>
+ <member>
+ <Id>ig-aaaaaaaaaaaaa</Id>
+ <InstanceType>m1.large</InstanceType>
+ <Market>ON_DEMAND</Market>
+ <Status>
+ <StateChangeReason>
+ <Message>Job flow terminated</Message>
+ <Code>CLUSTER_TERMINATED</Code>
+ </StateChangeReason>
+ <State>TERMINATED</State>
+ <Timeline>
+ <CreationDateTime>2014-01-24T01:21:21Z</CreationDateTime>
+ <ReadyDateTime>2014-01-24T01:25:08Z</ReadyDateTime>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ </Timeline>
+ </Status>
+ <Name>Master instance group</Name>
+ <RequestedInstanceCount>1</RequestedInstanceCount>
+ <RunningInstanceCount>0</RunningInstanceCount>
+ <InstanceGroupType>MASTER</InstanceGroupType>
+ </member>
+ <member>
+ <Id>ig-aaaaaaaaaaab</Id>
+ <InstanceType>m1.large</InstanceType>
+ <Market>ON_DEMAND</Market>
+ <Status>
+ <StateChangeReason>
+ <Message>Job flow terminated</Message>
+ <Code>CLUSTER_TERMINATED</Code>
+ </StateChangeReason>
+ <State>TERMINATED</State>
+ <Timeline>
+ <CreationDateTime>2014-01-24T01:21:21Z</CreationDateTime>
+ <ReadyDateTime>2014-01-24T01:25:26Z</ReadyDateTime>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ </Timeline>
+ </Status>
+ <Name>Core instance group</Name>
+ <RequestedInstanceCount>2</RequestedInstanceCount>
+ <RunningInstanceCount>0</RunningInstanceCount>
+ <InstanceGroupType>CORE</InstanceGroupType>
+ </member>
+ </InstanceGroups>
+ </ListInstanceGroupsResult>
+ <ResponseMetadata>
+ <RequestId>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</RequestId>
+ </ResponseMetadata>
+</ListInstanceGroupsResponse>
+
+"""
def test_list_instance_groups(self):
self.set_http_response(200)
@@ -108,11 +219,96 @@ class TestListInstanceGroups(AWSMockServiceTestCase):
'Version': '2009-03-31'
})
+ self.assertTrue(isinstance(response, InstanceGroupList))
+ self.assertEqual(len(response.instancegroups), 2)
+ self.assertTrue(isinstance(response.instancegroups[0], InstanceGroupInfo))
+ self.assertEqual(response.instancegroups[0].id, 'ig-aaaaaaaaaaaaa')
+ self.assertEqual(response.instancegroups[0].instancegrouptype, "MASTER")
+ self.assertEqual(response.instancegroups[0].instancetype, "m1.large")
+ self.assertEqual(response.instancegroups[0].market, "ON_DEMAND")
+ self.assertEqual(response.instancegroups[0].name, "Master instance group")
+ self.assertEqual(response.instancegroups[0].requestedinstancecount, '1')
+ self.assertEqual(response.instancegroups[0].runninginstancecount, '0')
+ self.assertTrue(isinstance(response.instancegroups[0].status, ClusterStatus))
+ self.assertEqual(response.instancegroups[0].status.state, 'TERMINATED')
+ # status.statechangereason is not parsed into an object
+ #self.assertEqual(response.instancegroups[0].status.statechangereason.code, 'CLUSTER_TERMINATED')
+
class TestListInstances(AWSMockServiceTestCase):
connection_class = EmrConnection
def default_body(self):
- return """<ListInstancesOutput><Instances></Instances></ListInstancesOutput>"""
+ return """
+<ListInstancesResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31">
+ <ListInstancesResult>
+ <Instances>
+ <member>
+ <Id>ci-123456789abc</Id>
+ <Status>
+ <StateChangeReason>
+ <Message>Cluster was terminated.</Message>
+ <Code>CLUSTER_TERMINATED</Code>
+ </StateChangeReason>
+ <State>TERMINATED</State>
+ <Timeline>
+ <CreationDateTime>2014-01-24T01:21:26Z</CreationDateTime>
+ <ReadyDateTime>2014-01-24T01:25:25Z</ReadyDateTime>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ </Timeline>
+ </Status>
+ <PrivateDnsName>ip-10-0-0-60.us-west-1.compute.internal</PrivateDnsName>
+ <PublicIpAddress>54.0.0.1</PublicIpAddress>
+ <PublicDnsName>ec2-54-0-0-1.us-west-1.compute.amazonaws.com</PublicDnsName>
+ <Ec2InstanceId>i-aaaaaaaa</Ec2InstanceId>
+ <PrivateIpAddress>10.0.0.60</PrivateIpAddress>
+ </member>
+ <member>
+ <Id>ci-123456789abd</Id>
+ <Status>
+ <StateChangeReason>
+ <Message>Cluster was terminated.</Message>
+ <Code>CLUSTER_TERMINATED</Code>
+ </StateChangeReason>
+ <State>TERMINATED</State>
+ <Timeline>
+ <CreationDateTime>2014-01-24T01:21:26Z</CreationDateTime>
+ <ReadyDateTime>2014-01-24T01:25:25Z</ReadyDateTime>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ </Timeline>
+ </Status>
+ <PrivateDnsName>ip-10-0-0-61.us-west-1.compute.internal</PrivateDnsName>
+ <PublicIpAddress>54.0.0.2</PublicIpAddress>
+ <PublicDnsName>ec2-54-0-0-2.us-west-1.compute.amazonaws.com</PublicDnsName>
+ <Ec2InstanceId>i-aaaaaaab</Ec2InstanceId>
+ <PrivateIpAddress>10.0.0.61</PrivateIpAddress>
+ </member>
+ <member>
+ <Id>ci-123456789abe3</Id>
+ <Status>
+ <StateChangeReason>
+ <Message>Cluster was terminated.</Message>
+ <Code>CLUSTER_TERMINATED</Code>
+ </StateChangeReason>
+ <State>TERMINATED</State>
+ <Timeline>
+ <CreationDateTime>2014-01-24T01:21:33Z</CreationDateTime>
+ <ReadyDateTime>2014-01-24T01:25:08Z</ReadyDateTime>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ </Timeline>
+ </Status>
+ <PrivateDnsName>ip-10-0-0-62.us-west-1.compute.internal</PrivateDnsName>
+ <PublicIpAddress>54.0.0.3</PublicIpAddress>
+ <PublicDnsName>ec2-54-0-0-3.us-west-1.compute.amazonaws.com</PublicDnsName>
+ <Ec2InstanceId>i-aaaaaaac</Ec2InstanceId>
+ <PrivateIpAddress>10.0.0.62</PrivateIpAddress>
+ </member>
+ </Instances>
+ </ListInstancesResult>
+ <ResponseMetadata>
+ <RequestId>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</RequestId>
+ </ResponseMetadata>
+</ListInstancesResponse>
+ """
def test_list_instances(self):
self.set_http_response(200)
@@ -121,6 +317,16 @@ class TestListInstances(AWSMockServiceTestCase):
self.service_connection.list_instances()
response = self.service_connection.list_instances(cluster_id='j-123')
+ self.assertTrue(isinstance(response, InstanceList))
+ self.assertEqual(len(response.instances), 3)
+ self.assertTrue(isinstance(response.instances[0], InstanceInfo))
+ self.assertEqual(response.instances[0].ec2instanceid, 'i-aaaaaaaa')
+ self.assertEqual(response.instances[0].id, 'ci-123456789abc')
+ self.assertEqual(response.instances[0].privatednsname , 'ip-10-0-0-60.us-west-1.compute.internal')
+ self.assertEqual(response.instances[0].privateipaddress , '10.0.0.60')
+ self.assertEqual(response.instances[0].publicdnsname , 'ec2-54-0-0-1.us-west-1.compute.amazonaws.com')
+ self.assertEqual(response.instances[0].publicipaddress , '54.0.0.1')
+
self.assert_request_parameters({
'Action': 'ListInstances',
@@ -223,7 +429,47 @@ class TestDescribeCluster(AWSMockServiceTestCase):
connection_class = EmrConnection
def default_body(self):
- return """<DescribeClusterOutput></DescribeClusterOutput>"""
+ return """
+<DescribeClusterResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31">
+ <DescribeClusterResult>
+ <Cluster>
+ <Id>j-aaaaaaaaa</Id>
+ <Tags/>
+ <Ec2InstanceAttributes>
+ <Ec2AvailabilityZone>us-west-1c</Ec2AvailabilityZone>
+ <Ec2KeyName>my_secret_key</Ec2KeyName>
+ </Ec2InstanceAttributes>
+ <RunningAmiVersion>2.4.2</RunningAmiVersion>
+ <VisibleToAllUsers>true</VisibleToAllUsers>
+ <Status>
+ <StateChangeReason>
+ <Message>Terminated by user request</Message>
+ <Code>USER_REQUEST</Code>
+ </StateChangeReason>
+ <State>TERMINATED</State>
+ <Timeline>
+ <CreationDateTime>2014-01-24T01:21:21Z</CreationDateTime>
+ <ReadyDateTime>2014-01-24T01:25:26Z</ReadyDateTime>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ </Timeline>
+ </Status>
+ <AutoTerminate>false</AutoTerminate>
+ <Name>test analytics</Name>
+ <RequestedAmiVersion>2.4.2</RequestedAmiVersion>
+ <Applications>
+ <member>
+ <Name>hadoop</Name>
+ <Version>1.0.3</Version>
+ </member>
+ </Applications>
+ <TerminationProtected>false</TerminationProtected>
+ </Cluster>
+ </DescribeClusterResult>
+ <ResponseMetadata>
+ <RequestId>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</RequestId>
+ </ResponseMetadata>
+</DescribeClusterResponse>
+ """
def test_describe_cluster(self):
self.set_http_response(200)
@@ -233,6 +479,20 @@ class TestDescribeCluster(AWSMockServiceTestCase):
response = self.service_connection.describe_cluster(cluster_id='j-123')
+ self.assertTrue(isinstance(response, Cluster))
+ self.assertEqual(response.id, 'j-aaaaaaaaa')
+ self.assertEqual(response.runningamiversion, '2.4.2')
+ self.assertEqual(response.visibletoallusers, 'true')
+ self.assertEqual(response.autoterminate, 'false')
+ self.assertEqual(response.name, 'test analytics')
+ self.assertEqual(response.requestedamiversion, '2.4.2')
+ self.assertEqual(response.terminationprotected, 'false')
+ self.assertEqual(response.ec2instanceattributes.ec2availabilityzone, "us-west-1c")
+ self.assertEqual(response.ec2instanceattributes.ec2keyname, 'my_secret_key')
+ self.assertEqual(response.status.state, 'TERMINATED')
+ self.assertEqual(response.applications[0].name, 'hadoop')
+ self.assertEqual(response.applications[0].version, '1.0.3')
+
self.assert_request_parameters({
'Action': 'DescribeCluster',
'ClusterId': 'j-123',
@@ -413,3 +673,191 @@ class TestRemoveTag(AWSMockServiceTestCase):
'TagKeys.member.2': 'SecondKey',
'Version': '2009-03-31'
})
+
+class DescribeJobFlowsTestBase(AWSMockServiceTestCase):
+ connection_class = EmrConnection
+
+ def default_body(self):
+ return """
+<DescribeJobFlowsResponse xmlns="http://elasticmapreduce.amazonaws.com/doc/2009-03-31">
+ <DescribeJobFlowsResult>
+ <JobFlows>
+ <member>
+ <AmiVersion>2.4.2</AmiVersion>
+ <ExecutionStatusDetail>
+ <CreationDateTime>2014-01-24T01:21:21Z</CreationDateTime>
+ <LastStateChangeReason>Terminated by user request</LastStateChangeReason>
+ <StartDateTime>2014-01-24T01:25:26Z</StartDateTime>
+ <ReadyDateTime>2014-01-24T01:25:26Z</ReadyDateTime>
+ <State>TERMINATED</State>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ </ExecutionStatusDetail>
+ <BootstrapActions/>
+ <VisibleToAllUsers>true</VisibleToAllUsers>
+ <SupportedProducts/>
+ <Name>test analytics</Name>
+ <JobFlowId>j-aaaaaa</JobFlowId>
+ <Steps>
+ <member>
+ <ExecutionStatusDetail>
+ <CreationDateTime>2014-01-24T01:21:21Z</CreationDateTime>
+ <StartDateTime>2014-01-24T01:25:26Z</StartDateTime>
+ <State>COMPLETED</State>
+ <EndDateTime>2014-01-24T01:26:08Z</EndDateTime>
+ </ExecutionStatusDetail>
+ <StepConfig>
+ <HadoopJarStep>
+ <Args>
+ <member>s3://us-west-1.elasticmapreduce/libs/hive/hive-script</member>
+ <member>--base-path</member>
+ <member>s3://us-west-1.elasticmapreduce/libs/hive/</member>
+ <member>--install-hive</member>
+ <member>--hive-versions</member>
+ <member>0.11.0.1</member>
+ </Args>
+ <Jar>s3://us-west-1.elasticmapreduce/libs/script-runner/script-runner.jar</Jar>
+ <Properties/>
+ </HadoopJarStep>
+ <Name>Setup hive</Name>
+ <ActionOnFailure>TERMINATE_JOB_FLOW</ActionOnFailure>
+ </StepConfig>
+ </member>
+ </Steps>
+ <Instances>
+ <Placement>
+ <AvailabilityZone>us-west-1c</AvailabilityZone>
+ </Placement>
+ <MasterInstanceType>m1.large</MasterInstanceType>
+ <Ec2KeyName>my_key</Ec2KeyName>
+ <KeepJobFlowAliveWhenNoSteps>true</KeepJobFlowAliveWhenNoSteps>
+ <InstanceGroups>
+ <member>
+ <CreationDateTime>2014-01-24T01:21:21Z</CreationDateTime>
+ <InstanceRunningCount>0</InstanceRunningCount>
+ <StartDateTime>2014-01-24T01:23:56Z</StartDateTime>
+ <ReadyDateTime>2014-01-24T01:25:08Z</ReadyDateTime>
+ <State>ENDED</State>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ <InstanceRequestCount>1</InstanceRequestCount>
+ <InstanceType>m1.large</InstanceType>
+ <LastStateChangeReason>Job flow terminated</LastStateChangeReason>
+ <Market>ON_DEMAND</Market>
+ <InstanceGroupId>ig-aaaaaa</InstanceGroupId>
+ <InstanceRole>MASTER</InstanceRole>
+ <Name>Master instance group</Name>
+ </member>
+ <member>
+ <CreationDateTime>2014-01-24T01:21:21Z</CreationDateTime>
+ <InstanceRunningCount>0</InstanceRunningCount>
+ <StartDateTime>2014-01-24T01:25:26Z</StartDateTime>
+ <ReadyDateTime>2014-01-24T01:25:26Z</ReadyDateTime>
+ <State>ENDED</State>
+ <EndDateTime>2014-01-24T02:19:46Z</EndDateTime>
+ <InstanceRequestCount>2</InstanceRequestCount>
+ <InstanceType>m1.large</InstanceType>
+ <LastStateChangeReason>Job flow terminated</LastStateChangeReason>
+ <Market>ON_DEMAND</Market>
+ <InstanceGroupId>ig-aaaaab</InstanceGroupId>
+ <InstanceRole>CORE</InstanceRole>
+ <Name>Core instance group</Name>
+ </member>
+ </InstanceGroups>
+ <SlaveInstanceType>m1.large</SlaveInstanceType>
+ <MasterInstanceId>i-aaaaaa</MasterInstanceId>
+ <HadoopVersion>1.0.3</HadoopVersion>
+ <NormalizedInstanceHours>12</NormalizedInstanceHours>
+ <MasterPublicDnsName>ec2-184-0-0-1.us-west-1.compute.amazonaws.com</MasterPublicDnsName>
+ <InstanceCount>3</InstanceCount>
+ <TerminationProtected>false</TerminationProtected>
+ </Instances>
+ </member>
+ </JobFlows>
+ </DescribeJobFlowsResult>
+ <ResponseMetadata>
+ <RequestId>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</RequestId>
+ </ResponseMetadata>
+</DescribeJobFlowsResponse>
+ """
+
+class TestDescribeJobFlows(DescribeJobFlowsTestBase):
+
+ def test_describe_jobflows_response(self):
+ self.set_http_response(200)
+
+ response = self.service_connection.describe_jobflows()
+ self.assertTrue(isinstance(response, list))
+
+ jf = response[0]
+ self.assertTrue(isinstance(jf, JobFlow))
+ self.assertEqual(jf.amiversion, '2.4.2')
+ self.assertEqual(jf.visibletoallusers, 'true')
+ self.assertEqual(jf.name, 'test analytics')
+ self.assertEqual(jf.jobflowid, 'j-aaaaaa')
+ self.assertEqual(jf.ec2keyname, 'my_key')
+ self.assertEqual(jf.masterinstancetype, 'm1.large')
+ self.assertEqual(jf.availabilityzone, 'us-west-1c')
+ self.assertEqual(jf.keepjobflowalivewhennosteps, 'true')
+ self.assertEqual(jf.slaveinstancetype, 'm1.large')
+ self.assertEqual(jf.masterinstanceid, 'i-aaaaaa')
+ self.assertEqual(jf.hadoopversion, '1.0.3')
+ self.assertEqual(jf.normalizedinstancehours, '12')
+ self.assertEqual(jf.masterpublicdnsname, 'ec2-184-0-0-1.us-west-1.compute.amazonaws.com')
+ self.assertEqual(jf.instancecount, '3')
+ self.assertEqual(jf.terminationprotected, 'false')
+
+ self.assertTrue(isinstance(jf.steps, list))
+ step = jf.steps[0]
+ self.assertTrue(isinstance(step, Step))
+ self.assertEqual(step.jar, 's3://us-west-1.elasticmapreduce/libs/script-runner/script-runner.jar')
+ self.assertEqual(step.name, 'Setup hive')
+ self.assertEqual(step.actiononfailure, 'TERMINATE_JOB_FLOW')
+
+ self.assertTrue(isinstance(jf.instancegroups, list))
+ ig = jf.instancegroups[0]
+ self.assertTrue(isinstance(ig, InstanceGroup))
+ self.assertEqual(ig.creationdatetime, '2014-01-24T01:21:21Z')
+ self.assertEqual(ig.state, 'ENDED')
+ self.assertEqual(ig.instancerequestcount, '1')
+ self.assertEqual(ig.instancetype, 'm1.large')
+ self.assertEqual(ig.laststatechangereason, 'Job flow terminated')
+ self.assertEqual(ig.market, 'ON_DEMAND')
+ self.assertEqual(ig.instancegroupid, 'ig-aaaaaa')
+ self.assertEqual(ig.instancerole, 'MASTER')
+ self.assertEqual(ig.name, 'Master instance group')
+
+ def test_describe_jobflows_no_args(self):
+ self.set_http_response(200)
+
+ self.service_connection.describe_jobflows()
+
+ self.assert_request_parameters({
+ 'Action': 'DescribeJobFlows',
+ }, ignore_params_values=['Version'])
+
+ def test_describe_jobflows_filtered(self):
+ self.set_http_response(200)
+
+ now = datetime.now()
+ a_bit_before = datetime.fromtimestamp(time() - 1000)
+
+ self.service_connection.describe_jobflows(states=['WAITING', 'RUNNING'], jobflow_ids=['j-aaaaaa', 'j-aaaaab'], created_after=a_bit_before, created_before=now)
+ self.assert_request_parameters({
+ 'Action': 'DescribeJobFlows',
+ 'JobFlowIds.member.1': 'j-aaaaaa',
+ 'JobFlowIds.member.2': 'j-aaaaab',
+ 'JobFlowStates.member.1': 'WAITING',
+ 'JobFlowStates.member.2': 'RUNNING',
+ 'CreatedAfter': a_bit_before.strftime(boto.utils.ISO8601),
+ 'CreatedBefore': now.strftime(boto.utils.ISO8601),
+ }, ignore_params_values=['Version'])
+
+class TestDescribeJobFlow(DescribeJobFlowsTestBase):
+ def test_describe_jobflow(self):
+ self.set_http_response(200)
+
+ response = self.service_connection.describe_jobflow('j-aaaaaa')
+ self.assertTrue(isinstance(response, JobFlow))
+ self.assert_request_parameters({
+ 'Action': 'DescribeJobFlows',
+ 'JobFlowIds.member.1': 'j-aaaaaa',
+ }, ignore_params_values=['Version'])
diff --git a/tests/unit/glacier/test_concurrent.py b/tests/unit/glacier/test_concurrent.py
index b9f984e1..07515dd7 100644
--- a/tests/unit/glacier/test_concurrent.py
+++ b/tests/unit/glacier/test_concurrent.py
@@ -58,14 +58,12 @@ class TestConcurrentUploader(unittest.TestCase):
def setUp(self):
super(TestConcurrentUploader, self).setUp()
self.stat_patch = mock.patch('os.stat')
+ self.addCleanup(self.stat_patch.stop)
self.stat_mock = self.stat_patch.start()
# Give a default value for tests that don't care
# what the file size is.
self.stat_mock.return_value.st_size = 1024 * 1024 * 8
- def tearDown(self):
- self.stat_mock = self.stat_patch.start()
-
def test_calculate_required_part_size(self):
self.stat_mock.return_value.st_size = 1024 * 1024 * 8
uploader = ConcurrentUploader(mock.Mock(), 'vault_name')
diff --git a/tests/unit/rds/test_connection.py b/tests/unit/rds/test_connection.py
index 2dea3758..48afc9e0 100644
--- a/tests/unit/rds/test_connection.py
+++ b/tests/unit/rds/test_connection.py
@@ -28,7 +28,7 @@ from boto.ec2.securitygroup import SecurityGroup
from boto.rds import RDSConnection
from boto.rds.vpcsecuritygroupmembership import VPCSecurityGroupMembership
from boto.rds.parametergroup import ParameterGroup
-
+from boto.rds.logfile import LogFile
class TestRDSConnection(AWSMockServiceTestCase):
connection_class = RDSConnection
@@ -550,6 +550,85 @@ class TestRDSOptionGroups(AWSMockServiceTestCase):
self.assertEqual(options.engine_name, 'oracle-se1')
self.assertEqual(options.major_engine_version, '11.2')
+class TestRDSLogFile(AWSMockServiceTestCase):
+ connection_class = RDSConnection
+
+ def setUp(self):
+ super(TestRDSLogFile, self).setUp()
+
+ def default_body(self):
+ return """
+ <DescribeDBLogFilesResponse xmlns="http://rds.amazonaws.com/doc/2013-02-12/">
+ <DescribeDBLogFilesResult>
+ <DescribeDBLogFiles>
+ <DescribeDBLogFilesDetails>
+ <LastWritten>1364403600000</LastWritten>
+ <LogFileName>error/mysql-error-running.log</LogFileName>
+ <Size>0</Size>
+ </DescribeDBLogFilesDetails>
+ <DescribeDBLogFilesDetails>
+ <LastWritten>1364338800000</LastWritten>
+ <LogFileName>error/mysql-error-running.log.0</LogFileName>
+ <Size>0</Size>
+ </DescribeDBLogFilesDetails>
+ <DescribeDBLogFilesDetails>
+ <LastWritten>1364342400000</LastWritten>
+ <LogFileName>error/mysql-error-running.log.1</LogFileName>
+ <Size>0</Size>
+ </DescribeDBLogFilesDetails>
+ <DescribeDBLogFilesDetails>
+ <LastWritten>1364346000000</LastWritten>
+ <LogFileName>error/mysql-error-running.log.2</LogFileName>
+ <Size>0</Size>
+ </DescribeDBLogFilesDetails>
+ <DescribeDBLogFilesDetails>
+ <LastWritten>1364349600000</LastWritten>
+ <LogFileName>error/mysql-error-running.log.3</LogFileName>
+ <Size>0</Size>
+ </DescribeDBLogFilesDetails>
+ <DescribeDBLogFilesDetails>
+ <LastWritten>1364405700000</LastWritten>
+ <LogFileName>error/mysql-error.log</LogFileName>
+ <Size>0</Size>
+ </DescribeDBLogFilesDetails>
+ </DescribeDBLogFiles>
+ </DescribeDBLogFilesResult>
+ <ResponseMetadata>
+ <RequestId>d70fb3b3-9704-11e2-a0db-871552e0ef19</RequestId>
+ </ResponseMetadata>
+ </DescribeDBLogFilesResponse>
+ """
+
+ def test_get_all_logs(self):
+ self.set_http_response(status_code=200)
+ response = self.service_connection.get_all_logs()
+
+ self.assert_request_parameters({
+ 'Action': 'DescribeDBLogFiles',
+ 'MaxRecords': 26,
+ }, ignore_params_values=['Version'])
+
+ self.assertEqual(len(response), 6)
+ self.assertTrue(isinstance(response[0], LogFile))
+ self.assertEqual(response[0].log_filename, 'error/mysql-error-running.log')
+ self.assertEqual(response[0].last_written, '1364403600000')
+ self.assertEqual(response[0].size, '0')
+
+ def test_get_all_logs_single(self):
+ self.set_http_response(status_code=200)
+ response = self.service_connection.get_all_logs('db_instance_1')
+
+ self.assert_request_parameters({
+ 'Action': 'DescribeDBLogFiles',
+ 'DBInstanceIdentifier': 'db_instance_1',
+ 'MaxRecords': 26,
+ }, ignore_params_values=['Version'])
+
+ self.assertEqual(len(response), 6)
+ self.assertTrue(isinstance(response[0], LogFile))
+ self.assertEqual(response[0].log_filename, 'error/mysql-error-running.log')
+ self.assertEqual(response[0].last_written, '1364403600000')
+ self.assertEqual(response[0].size, '0')
class TestRDSOptionGroupOptions(AWSMockServiceTestCase):
connection_class = RDSConnection
diff --git a/tests/unit/route53/test_connection.py b/tests/unit/route53/test_connection.py
index 3141dd17..caa5f022 100644
--- a/tests/unit/route53/test_connection.py
+++ b/tests/unit/route53/test_connection.py
@@ -25,6 +25,8 @@ import mock
from boto.exception import BotoServerError
from boto.route53.connection import Route53Connection
from boto.route53.exception import DNSServerError
+from boto.route53.record import ResourceRecordSets, Record
+from boto.route53.zone import Zone
from tests.unit import unittest
from tests.unit import AWSMockServiceTestCase
@@ -82,3 +84,199 @@ class TestRoute53Connection(AWSMockServiceTestCase):
# Unpatch.
self.service_connection._retry_handler = orig_retry
+
+
+class TestCreateZoneRoute53(AWSMockServiceTestCase):
+ connection_class = Route53Connection
+
+ def setUp(self):
+ super(TestCreateZoneRoute53, self).setUp()
+
+ def default_body(self):
+ return """
+<CreateHostedZoneResponse xmlns="https://route53.amazonaws.com/doc/2012-02-29/">
+ <HostedZone>
+ <Id>/hostedzone/Z11111</Id>
+ <Name>example.com.</Name>
+ <CallerReference>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</CallerReference>
+ <Config>
+ <Comment></Comment>
+ </Config>
+ <ResourceRecordSetCount>2</ResourceRecordSetCount>
+ </HostedZone>
+ <ChangeInfo>
+ <Id>/change/C1111111111111</Id>
+ <Status>PENDING</Status>
+ <SubmittedAt>2014-02-02T10:19:29.928Z</SubmittedAt>
+ </ChangeInfo>
+ <DelegationSet>
+ <NameServers>
+ <NameServer>ns-100.awsdns-01.com</NameServer>
+ <NameServer>ns-1000.awsdns-01.co.uk</NameServer>
+ <NameServer>ns-1000.awsdns-01.org</NameServer>
+ <NameServer>ns-900.awsdns-01.net</NameServer>
+ </NameServers>
+ </DelegationSet>
+</CreateHostedZoneResponse>
+ """
+
+ def test_create_zone(self):
+ self.set_http_response(status_code=201)
+ response = self.service_connection.create_zone("example.com.")
+
+ self.assertTrue(isinstance(response, Zone))
+ self.assertEqual(response.id, "Z11111")
+ self.assertEqual(response.name, "example.com.")
+
+ def test_create_hosted_zone(self):
+ self.set_http_response(status_code=201)
+ response = self.service_connection.create_hosted_zone("example.com.", "my_ref", "this is a comment")
+
+ self.assertEqual(response['CreateHostedZoneResponse']['DelegationSet']['NameServers'],
+ ['ns-100.awsdns-01.com', 'ns-1000.awsdns-01.co.uk', 'ns-1000.awsdns-01.org', 'ns-900.awsdns-01.net'])
+
+
+class TestGetZoneRoute53(AWSMockServiceTestCase):
+ connection_class = Route53Connection
+
+ def setUp(self):
+ super(TestGetZoneRoute53, self).setUp()
+
+ def default_body(self):
+ return """
+<ListHostedZonesResponse xmlns="https://route53.amazonaws.com/doc/2012-02-29/">
+ <HostedZones>
+ <HostedZone>
+ <Id>/hostedzone/Z1111</Id>
+ <Name>example2.com.</Name>
+ <CallerReference>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</CallerReference>
+ <Config/>
+ <ResourceRecordSetCount>3</ResourceRecordSetCount>
+ </HostedZone>
+ <HostedZone>
+ <Id>/hostedzone/Z2222</Id>
+ <Name>example1.com.</Name>
+ <CallerReference>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeef</CallerReference>
+ <Config/>
+ <ResourceRecordSetCount>6</ResourceRecordSetCount>
+ </HostedZone>
+ <HostedZone>
+ <Id>/hostedzone/Z3333</Id>
+ <Name>example.com.</Name>
+ <CallerReference>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeg</CallerReference>
+ <Config/>
+ <ResourceRecordSetCount>6</ResourceRecordSetCount>
+ </HostedZone>
+ </HostedZones>
+ <IsTruncated>false</IsTruncated>
+ <MaxItems>100</MaxItems>
+</ListHostedZonesResponse>
+ """
+
+ def test_list_zones(self):
+ self.set_http_response(status_code=201)
+ response = self.service_connection.get_all_hosted_zones()
+
+ domains = ['example2.com.', 'example1.com.', 'example.com.']
+ print response['ListHostedZonesResponse']['HostedZones'][0]
+ for d in response['ListHostedZonesResponse']['HostedZones']:
+ print "Removing: %s" % d['Name']
+ domains.remove(d['Name'])
+
+ self.assertEqual(domains, [])
+
+ def test_get_zone(self):
+ self.set_http_response(status_code=201)
+ response = self.service_connection.get_zone('example.com.')
+
+ self.assertTrue(isinstance(response, Zone))
+ self.assertEqual(response.name, "example.com.")
+
+
+class TestGetHostedZoneRoute53(AWSMockServiceTestCase):
+ connection_class = Route53Connection
+
+ def setUp(self):
+ super(TestGetHostedZoneRoute53, self).setUp()
+
+ def default_body(self):
+ return """
+<GetHostedZoneResponse xmlns="https://route53.amazonaws.com/doc/2012-02-29/">
+ <HostedZone>
+ <Id>/hostedzone/Z1111</Id>
+ <Name>example.com.</Name>
+ <CallerReference>aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</CallerReference>
+ <Config/>
+ <ResourceRecordSetCount>3</ResourceRecordSetCount>
+ </HostedZone>
+ <DelegationSet>
+ <NameServers>
+ <NameServer>ns-1000.awsdns-40.org</NameServer>
+ <NameServer>ns-200.awsdns-30.com</NameServer>
+ <NameServer>ns-900.awsdns-50.net</NameServer>
+ <NameServer>ns-1000.awsdns-00.co.uk</NameServer>
+ </NameServers>
+ </DelegationSet>
+</GetHostedZoneResponse>
+"""
+
+ def test_list_zones(self):
+ self.set_http_response(status_code=201)
+ response = self.service_connection.get_hosted_zone("Z1111")
+
+ self.assertEqual(response['GetHostedZoneResponse']['HostedZone']['Id'], '/hostedzone/Z1111')
+ self.assertEqual(response['GetHostedZoneResponse']['HostedZone']['Name'], 'example.com.')
+ self.assertEqual(response['GetHostedZoneResponse']['DelegationSet']['NameServers'],
+ ['ns-1000.awsdns-40.org', 'ns-200.awsdns-30.com', 'ns-900.awsdns-50.net', 'ns-1000.awsdns-00.co.uk'])
+
+
+class TestGetAllRRSetsRoute53(AWSMockServiceTestCase):
+ connection_class = Route53Connection
+
+ def setUp(self):
+ super(TestGetAllRRSetsRoute53, self).setUp()
+
+ def default_body(self):
+ return """
+<ListResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2012-02-29/">
+ <ResourceRecordSets>
+ <ResourceRecordSet>
+ <Name>test.example.com.</Name>
+ <Type>A</Type>
+ <TTL>60</TTL>
+ <ResourceRecords>
+ <ResourceRecord>
+ <Value>10.0.0.1</Value>
+ </ResourceRecord>
+ </ResourceRecords>
+ </ResourceRecordSet>
+ <ResourceRecordSet>
+ <Name>www.example.com.</Name>
+ <Type>A</Type>
+ <TTL>60</TTL>
+ <ResourceRecords>
+ <ResourceRecord>
+ <Value>10.0.0.2</Value>
+ </ResourceRecord>
+ </ResourceRecords>
+ </ResourceRecordSet>
+ </ResourceRecordSets>
+ <IsTruncated>false</IsTruncated>
+ <MaxItems>100</MaxItems>
+</ListResourceRecordSetsResponse>
+ """
+
+ def test_get_all_rr_sets(self):
+ self.set_http_response(status_code=200)
+ response = self.service_connection.get_all_rrsets("Z1111", "A", "example.com.")
+
+ self.assertEqual(self.actual_request.path,
+ "/2013-04-01/hostedzone/Z1111/rrset?type=A&name=example.com.")
+
+ self.assertTrue(isinstance(response, ResourceRecordSets))
+ self.assertEqual(response.hosted_zone_id, "Z1111")
+ self.assertTrue(isinstance(response[0], Record))
+
+ self.assertTrue(response[0].name, "test.example.com.")
+ self.assertTrue(response[0].ttl, "60")
+ self.assertTrue(response[0].type, "A")
diff --git a/tests/unit/s3/test_bucket.py b/tests/unit/s3/test_bucket.py
index 2b36f254..bf638511 100644
--- a/tests/unit/s3/test_bucket.py
+++ b/tests/unit/s3/test_bucket.py
@@ -113,23 +113,23 @@ class TestS3Bucket(AWSMockServiceTestCase):
'initial=1&bar=%E2%98%83&max-keys=0&foo=true&some-other=thing'
)
- @patch.object(Bucket, 'get_all_keys')
- def test_bucket_copy_key_no_validate(self, mock_get_all_keys):
+ @patch.object(S3Connection, 'head_bucket')
+ def test_bucket_copy_key_no_validate(self, mock_head_bucket):
self.set_http_response(status_code=200)
bucket = self.service_connection.create_bucket('mybucket')
- self.assertFalse(mock_get_all_keys.called)
+ self.assertFalse(mock_head_bucket.called)
self.service_connection.get_bucket('mybucket', validate=True)
- self.assertTrue(mock_get_all_keys.called)
+ self.assertTrue(mock_head_bucket.called)
- mock_get_all_keys.reset_mock()
- self.assertFalse(mock_get_all_keys.called)
+ mock_head_bucket.reset_mock()
+ self.assertFalse(mock_head_bucket.called)
try:
bucket.copy_key('newkey', 'srcbucket', 'srckey', preserve_acl=True)
except:
# Will throw because of empty response.
pass
- self.assertFalse(mock_get_all_keys.called)
+ self.assertFalse(mock_head_bucket.called)
@patch.object(Bucket, '_get_all')
def test_bucket_encoding(self, mock_get_all):
diff --git a/tests/unit/s3/test_connection.py b/tests/unit/s3/test_connection.py
index f4a1d51d..ded110c4 100644
--- a/tests/unit/s3/test_connection.py
+++ b/tests/unit/s3/test_connection.py
@@ -20,12 +20,14 @@
# IN THE SOFTWARE.
#
import mock
+import time
from tests.unit import unittest
from tests.unit import AWSMockServiceTestCase
from tests.unit import MockServiceWithConfigTestCase
from boto.s3.connection import S3Connection, HostRequiredError
+from boto.s3.connection import S3ResponseError, Bucket
class TestSignatureAlteration(AWSMockServiceTestCase):
@@ -124,5 +126,53 @@ class TestUnicodeCallingFormat(AWSMockServiceTestCase):
self.service_connection.get_all_buckets()
+class TestHeadBucket(AWSMockServiceTestCase):
+ connection_class = S3Connection
+
+ def default_body(self):
+ # HEAD requests always have an empty body.
+ return ""
+
+ def test_head_bucket_success(self):
+ self.set_http_response(status_code=200)
+ buck = self.service_connection.head_bucket('my-test-bucket')
+ self.assertTrue(isinstance(buck, Bucket))
+ self.assertEqual(buck.name, 'my-test-bucket')
+
+ def test_head_bucket_forbidden(self):
+ self.set_http_response(status_code=403)
+
+ with self.assertRaises(S3ResponseError) as cm:
+ self.service_connection.head_bucket('cant-touch-this')
+
+ err = cm.exception
+ self.assertEqual(err.status, 403)
+ self.assertEqual(err.error_code, 'AccessDenied')
+ self.assertEqual(err.message, 'Access Denied')
+
+ def test_head_bucket_notfound(self):
+ self.set_http_response(status_code=404)
+
+ with self.assertRaises(S3ResponseError) as cm:
+ self.service_connection.head_bucket('totally-doesnt-exist')
+
+ err = cm.exception
+ self.assertEqual(err.status, 404)
+ self.assertEqual(err.error_code, 'NoSuchBucket')
+ self.assertEqual(err.message, 'The specified bucket does not exist')
+
+ def test_head_bucket_other(self):
+ self.set_http_response(status_code=405)
+
+ with self.assertRaises(S3ResponseError) as cm:
+ self.service_connection.head_bucket('you-broke-it')
+
+ err = cm.exception
+ self.assertEqual(err.status, 405)
+ # We don't have special-cases for this error status.
+ self.assertEqual(err.error_code, None)
+ self.assertEqual(err.message, '')
+
+
if __name__ == "__main__":
unittest.main()