diff options
author | Daniel Lindsley <daniel@toastdriven.com> | 2014-02-07 13:37:10 -0800 |
---|---|---|
committer | Daniel Lindsley <daniel@toastdriven.com> | 2014-02-07 13:37:10 -0800 |
commit | 522d67f9ff32af03df4be73ac7b9541c3ab6e3c0 (patch) | |
tree | 8c7a2bf36dccf96dbc18f120bf04e71661ecca0c | |
parent | 4d4746e665071f8247902bb7c7cd1585e6213c3f (diff) | |
parent | d4fc260634212ead98a0944cb0bf2f509442a573 (diff) | |
download | boto-2.25.0.tar.gz |
Merge branch 'release-2.25.0'2.25.0
32 files changed, 1391 insertions, 67 deletions
@@ -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() |