diff options
62 files changed, 1395 insertions, 112 deletions
@@ -1,9 +1,9 @@ #### boto #### -boto 2.10.0 +boto 2.11.0 -Released: 13-August-2013 +Released: 29-August-2013 .. image:: https://travis-ci.org/boto/boto.png?branch=develop :target: https://travis-ci.org/boto/boto diff --git a/bin/elbadmin b/bin/elbadmin index 87dd2b14..fc954f02 100755 --- a/bin/elbadmin +++ b/bin/elbadmin @@ -118,9 +118,8 @@ def get(elb, name): instances = [state.instance_id for state in instance_health] names = {} - for r in ec2.get_all_instances(instances): - for i in r.instances: - names[i.id] = i.tags.get('Name', '') + for i in ec2.get_only_instances(instances): + names[i.id] = i.tags.get('Name', '') name_column_width = max([4] + [len(v) for k,v in names.iteritems()]) + 2 diff --git a/bin/instance_events b/bin/instance_events index b36a4809..a851df66 100755 --- a/bin/instance_events +++ b/bin/instance_events @@ -51,7 +51,7 @@ def list(region, headers, order, completed): ec2 = boto.connect_ec2(region=region) - reservations = ec2.get_all_instances() + reservations = ec2.get_all_reservations() instanceinfo = {} events = {} diff --git a/bin/list_instances b/bin/list_instances index a8de4ada..8cb743c0 100755 --- a/bin/list_instances +++ b/bin/list_instances @@ -76,7 +76,7 @@ def main(): print format_string % headers print "-" * len(format_string % headers) - for r in ec2.get_all_instances(filters=filters): + for r in ec2.get_all_reservations(filters=filters): groups = [g.name for g in r.groups] for i in r.instances: i.groups = ','.join(groups) @@ -37,7 +37,9 @@ try: multipart_capable = True usage_flag_multipart_capable = """ [--multipart]""" usage_string_multipart_capable = """ - multipart - Upload files as multiple parts. This needs filechunkio.""" + multipart - Upload files as multiple parts. This needs filechunkio. + Requires ListBucket, ListMultipartUploadParts, + ListBucketMultipartUploads and PutObject permissions.""" except ImportError as err: multipart_capable = False usage_flag_multipart_capable = "" @@ -313,7 +315,7 @@ def main(): c = boto.connect_s3(aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key) c.debug = debug - b = c.get_bucket(bucket_name) + b = c.get_bucket(bucket_name, validate=False) existing_keys_to_check_against = [] files_to_check_for_upload = [] diff --git a/boto/__init__.py b/boto/__init__.py index a276c424..0f16009c 100644 --- a/boto/__init__.py +++ b/boto/__init__.py @@ -36,7 +36,7 @@ import logging.config import urlparse from boto.exception import InvalidUriError -__version__ = '2.10.0' +__version__ = '2.11.0' Version = __version__ # for backware compatibility UserAgent = 'Boto/%s (%s)' % (__version__, sys.platform) diff --git a/boto/auth.py b/boto/auth.py index 02de5e1e..f9426d56 100644 --- a/boto/auth.py +++ b/boto/auth.py @@ -431,6 +431,8 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys): parts = http_request.host.split('.') if self.region_name is not None: region_name = self.region_name + elif parts[1] == 'us-gov': + region_name = 'us-gov-west-1' else: if len(parts) == 3: region_name = 'us-east-1' @@ -510,6 +512,45 @@ class HmacAuthV4Handler(AuthHandler, HmacKeys): req.headers['Authorization'] = ','.join(l) +class QueryAuthHandler(AuthHandler): + """ + Provides pure query construction (no actual signing). + + Mostly useful for STS' ``assume_role_with_web_identity``. + + Does **NOT** escape query string values! + """ + + capability = ['pure-query'] + + def _escape_value(self, value): + # Would normally be ``return urllib.quote(value)``. + return value + + def _build_query_string(self, params): + keys = params.keys() + keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) + pairs = [] + for key in keys: + val = boto.utils.get_utf8_value(params[key]) + pairs.append(key + '=' + self._escape_value(val)) + return '&'.join(pairs) + + def add_auth(self, http_request, **kwargs): + headers = http_request.headers + params = http_request.params + qs = self._build_query_string( + http_request.params + ) + boto.log.debug('query_string: %s' % qs) + headers['Content-Type'] = 'application/json; charset=UTF-8' + http_request.body = '' + # if this is a retried request, the qs from the previous try will + # already be there, we need to get rid of that and rebuild it + http_request.path = http_request.path.split('?')[0] + http_request.path = http_request.path + '?' + qs + + class QuerySignatureHelper(HmacKeys): """ Helper for Query signature based Auth handler. diff --git a/boto/cloudformation/stack.py b/boto/cloudformation/stack.py index 2ee78022..c173de66 100644..100755 --- a/boto/cloudformation/stack.py +++ b/boto/cloudformation/stack.py @@ -295,7 +295,7 @@ class StackResource(object): class StackResourceSummary(object): def __init__(self, connection=None): self.connection = connection - self.last_updated_timestamp = None + self.last_updated_time = None self.logical_resource_id = None self.physical_resource_id = None self.resource_status = None @@ -306,14 +306,14 @@ class StackResourceSummary(object): return None def endElement(self, name, value, connection): - if name == "LastUpdatedTimestamp": + if name == "LastUpdatedTime": try: - self.last_updated_timestamp = datetime.strptime( + self.last_updated_time = datetime.strptime( value, '%Y-%m-%dT%H:%M:%SZ' ) except ValueError: - self.last_updated_timestamp = datetime.strptime( + self.last_updated_time = datetime.strptime( value, '%Y-%m-%dT%H:%M:%S.%fZ' ) diff --git a/boto/dynamodb/__init__.py b/boto/dynamodb/__init__.py index 12204361..46199732 100644 --- a/boto/dynamodb/__init__.py +++ b/boto/dynamodb/__init__.py @@ -35,6 +35,9 @@ def regions(): return [RegionInfo(name='us-east-1', endpoint='dynamodb.us-east-1.amazonaws.com', connection_cls=boto.dynamodb.layer2.Layer2), + RegionInfo(name='us-gov-west-1', + endpoint='dynamodb.us-gov-west-1.amazonaws.com', + connection_cls=boto.dynamodb.layer2.Layer2), RegionInfo(name='us-west-1', endpoint='dynamodb.us-west-1.amazonaws.com', connection_cls=boto.dynamodb.layer2.Layer2), diff --git a/boto/dynamodb2/__init__.py b/boto/dynamodb2/__init__.py index 2c56afa6..837f5620 100644 --- a/boto/dynamodb2/__init__.py +++ b/boto/dynamodb2/__init__.py @@ -35,6 +35,9 @@ def regions(): return [RegionInfo(name='us-east-1', endpoint='dynamodb.us-east-1.amazonaws.com', connection_cls=DynamoDBConnection), + RegionInfo(name='us-gov-west-1', + endpoint='dynamodb.us-gov-west-1.amazonaws.com', + connection_cls=DynamoDBConnection), RegionInfo(name='us-west-1', endpoint='dynamodb.us-west-1.amazonaws.com', connection_cls=DynamoDBConnection), diff --git a/boto/dynamodb2/table.py b/boto/dynamodb2/table.py index a78e3931..d552e4af 100644 --- a/boto/dynamodb2/table.py +++ b/boto/dynamodb2/table.py @@ -57,7 +57,7 @@ class Table(object): >>> conn = Table('users') # The full, minimum-extra-calls case. - >>> from boto.dynamodb2.layer1 import DynamoDBConnection + >>> from boto import dynamodb2 >>> users = Table('users', schema=[ ... HashKey('username'), ... RangeKey('date_joined', data_type=NUMBER) @@ -69,11 +69,10 @@ class Table(object): ... RangeKey('date_joined') ... ]), ... ], - ... connection=DynamoDBConnection( - ... aws_access_key_id='key', - ... aws_secret_access_key='key', - ... region='us-west-2' - ... )) + ... connection=dynamodb2.connect_to_region('us-west-2', + ... aws_access_key_id='key', + ... aws_secret_access_key='key', + ... )) """ self.table_name = table_name @@ -133,7 +132,7 @@ class Table(object): Example:: - >>> users = Table.create_table('users', schema=[ + >>> users = Table.create('users', schema=[ ... HashKey('username'), ... RangeKey('date_joined', data_type=NUMBER) ... ], throughput={ diff --git a/boto/ec2/__init__.py b/boto/ec2/__init__.py index 9cc14bec..4220c92e 100644 --- a/boto/ec2/__init__.py +++ b/boto/ec2/__init__.py @@ -29,6 +29,7 @@ from boto.regioninfo import RegionInfo RegionData = { 'us-east-1': 'ec2.us-east-1.amazonaws.com', + 'us-gov-west-1': 'ec2.us-gov-west-1.amazonaws.com', 'us-west-1': 'ec2.us-west-1.amazonaws.com', 'us-west-2': 'ec2.us-west-2.amazonaws.com', 'sa-east-1': 'ec2.sa-east-1.amazonaws.com', diff --git a/boto/ec2/autoscale/__init__.py b/boto/ec2/autoscale/__init__.py index 7ea4c9bd..f82ce9ec 100644 --- a/boto/ec2/autoscale/__init__.py +++ b/boto/ec2/autoscale/__init__.py @@ -47,6 +47,7 @@ from boto.ec2.autoscale.tag import Tag RegionData = { 'us-east-1': 'autoscaling.us-east-1.amazonaws.com', + 'us-gov-west-1': 'autoscaling.us-gov-west-1.amazonaws.com', 'us-west-1': 'autoscaling.us-west-1.amazonaws.com', 'us-west-2': 'autoscaling.us-west-2.amazonaws.com', 'sa-east-1': 'autoscaling.sa-east-1.amazonaws.com', diff --git a/boto/ec2/cloudwatch/__init__.py b/boto/ec2/cloudwatch/__init__.py index dd7b6811..82c529e4 100644 --- a/boto/ec2/cloudwatch/__init__.py +++ b/boto/ec2/cloudwatch/__init__.py @@ -33,6 +33,7 @@ import boto RegionData = { 'us-east-1': 'monitoring.us-east-1.amazonaws.com', + 'us-gov-west-1': 'monitoring.us-gov-west-1.amazonaws.com', 'us-west-1': 'monitoring.us-west-1.amazonaws.com', 'us-west-2': 'monitoring.us-west-2.amazonaws.com', 'sa-east-1': 'monitoring.sa-east-1.amazonaws.com', diff --git a/boto/ec2/connection.py b/boto/ec2/connection.py index f458552f..38eae9a0 100644 --- a/boto/ec2/connection.py +++ b/boto/ec2/connection.py @@ -436,6 +436,40 @@ class EC2Connection(AWSQueryConnection): def get_all_instances(self, instance_ids=None, filters=None): """ + Retrieve all the instance reservations associated with your account. + + .. note:: + This method's current behavior is deprecated in favor of + :meth:`get_all_reservations`. A future major release will change + :meth:`get_all_instances` to return a list of + :class:`boto.ec2.instance.Instance` objects as its name suggests. + To obtain that behavior today, use :meth:`get_only_instances`. + + :type instance_ids: list + :param instance_ids: A list of strings of instance IDs + + :type filters: dict + :param filters: Optional filters that can be used to limit the + results returned. Filters are provided in the form of a + dictionary consisting of filter names as the key and + filter values as the value. The set of allowable filter + names/values is dependent on the request being performed. + Check the EC2 API guide for details. + + :rtype: list + :return: A list of :class:`boto.ec2.instance.Reservation` + + """ + warnings.warn(('The current get_all_instances implementation will be ' + 'replaced with get_all_reservations.'), + PendingDeprecationWarning) + return self.get_all_reservations(instance_ids=instance_ids, + filters=filters) + + def get_only_instances(self, instance_ids=None, filters=None): + # A future release should rename this method to get_all_instances + # and make get_only_instances an alias for that. + """ Retrieve all the instances associated with your account. :type instance_ids: list @@ -450,6 +484,29 @@ class EC2Connection(AWSQueryConnection): Check the EC2 API guide for details. :rtype: list + :return: A list of :class:`boto.ec2.instance.Instance` + """ + reservations = self.get_all_reservations(instance_ids=instance_ids, + filters=filters) + return [instance for reservation in reservations + for instance in reservation.instances] + + def get_all_reservations(self, instance_ids=None, filters=None): + """ + Retrieve all the instance reservations associated with your account. + + :type instance_ids: list + :param instance_ids: A list of strings of instance IDs + + :type filters: dict + :param filters: Optional filters that can be used to limit the + results returned. Filters are provided in the form of a + dictionary consisting of filter names as the key and + filter values as the value. The set of allowable filter + names/values is dependent on the request being performed. + Check the EC2 API guide for details. + + :rtype: list :return: A list of :class:`boto.ec2.instance.Reservation` """ params = {} @@ -1957,7 +2014,7 @@ class EC2Connection(AWSQueryConnection): return snapshot.id def trim_snapshots(self, hourly_backups=8, daily_backups=7, - weekly_backups=4): + weekly_backups=4, monthly_backups=True): """ Trim excess snapshots, based on when they were taken. More current snapshots are retained, with the number retained decreasing as you @@ -1975,7 +2032,7 @@ class EC2Connection(AWSQueryConnection): snapshots taken in each of the last seven days, the first snapshots taken in the last 4 weeks (counting Midnight Sunday morning as the start of the week), and the first snapshot from the first - Sunday of each month forever. + day of each month forever. :type hourly_backups: int :param hourly_backups: How many recent hourly backups should be saved. @@ -1985,6 +2042,9 @@ class EC2Connection(AWSQueryConnection): :type weekly_backups: int :param weekly_backups: How many recent weekly backups should be saved. + + :type monthly_backups: int + :param monthly_backups: How many monthly backups should be saved. Use True for no limit. """ # This function first builds up an ordered list of target times @@ -2019,10 +2079,14 @@ class EC2Connection(AWSQueryConnection): target_backup_times.append(last_sunday - timedelta(weeks = week)) one_day = timedelta(days = 1) - while start_of_month > oldest_snapshot_date: + monthly_snapshots_added = 0 + while (start_of_month > oldest_snapshot_date and + (monthly_backups is True or + monthly_snapshots_added < monthly_backups)): # append the start of the month to the list of # snapshot dates to save: target_backup_times.append(start_of_month) + monthly_snapshots_added += 1 # there's no timedelta setting for one month, so instead: # decrement the day by one, so we go to the final day of # the previous month... diff --git a/boto/ec2/elb/__init__.py b/boto/ec2/elb/__init__.py index a190ab79..49100a43 100644 --- a/boto/ec2/elb/__init__.py +++ b/boto/ec2/elb/__init__.py @@ -36,6 +36,7 @@ import boto RegionData = { 'us-east-1': 'elasticloadbalancing.us-east-1.amazonaws.com', + 'us-gov-west-1': 'elasticloadbalancing.us-gov-west-1.amazonaws.com', 'us-west-1': 'elasticloadbalancing.us-west-1.amazonaws.com', 'us-west-2': 'elasticloadbalancing.us-west-2.amazonaws.com', 'sa-east-1': 'elasticloadbalancing.sa-east-1.amazonaws.com', diff --git a/boto/ec2/instance.py b/boto/ec2/instance.py index 5be701f0..e0137705 100644 --- a/boto/ec2/instance.py +++ b/boto/ec2/instance.py @@ -418,7 +418,7 @@ class Instance(TaggedEC2Object): raise a ValueError exception if no data is returned from EC2. """ - rs = self.connection.get_all_instances([self.id]) + rs = self.connection.get_all_reservations([self.id]) if len(rs) > 0: r = rs[0] for i in r.instances: diff --git a/boto/ec2/networkinterface.py b/boto/ec2/networkinterface.py index fca93d4c..6ffc79af 100644 --- a/boto/ec2/networkinterface.py +++ b/boto/ec2/networkinterface.py @@ -229,6 +229,9 @@ class NetworkInterfaceCollection(list): if ip_addr.primary is not None: params[query_param_key_prefix + '.Primary'] = \ 'true' if ip_addr.primary else 'false' + if spec.associate_public_ip_address is not None: + params[full_prefix + 'AssociatePublicIpAddress'] = \ + 'true' if spec.associate_public_ip_address else 'false' class NetworkInterfaceSpecification(object): @@ -236,7 +239,8 @@ class NetworkInterfaceSpecification(object): subnet_id=None, description=None, private_ip_address=None, groups=None, delete_on_termination=None, private_ip_addresses=None, - secondary_private_ip_address_count=None): + secondary_private_ip_address_count=None, + associate_public_ip_address=None): self.network_interface_id = network_interface_id self.device_index = device_index self.subnet_id = subnet_id @@ -247,3 +251,4 @@ class NetworkInterfaceSpecification(object): self.private_ip_addresses = private_ip_addresses self.secondary_private_ip_address_count = \ secondary_private_ip_address_count + self.associate_public_ip_address = associate_public_ip_address diff --git a/boto/ec2/securitygroup.py b/boto/ec2/securitygroup.py index 1b3c0ade..731c2390 100644 --- a/boto/ec2/securitygroup.py +++ b/boto/ec2/securitygroup.py @@ -26,6 +26,7 @@ Represents an EC2 Security Group from boto.ec2.ec2object import TaggedEC2Object from boto.exception import BotoClientError + class SecurityGroup(TaggedEC2Object): def __init__(self, connection=None, owner_id=None, @@ -73,7 +74,7 @@ class SecurityGroup(TaggedEC2Object): self.status = True else: raise Exception( - 'Unexpected value of status %s for group %s'%( + 'Unexpected value of status %s for group %s' % ( value, self.name ) @@ -268,16 +269,19 @@ class SecurityGroup(TaggedEC2Object): :rtype: list of :class:`boto.ec2.instance.Instance` :return: A list of Instance objects """ - # It would be more efficient to do this with filters now - # but not all services that implement EC2 API support filters. - instances = [] - rs = self.connection.get_all_instances() - for reservation in rs: - uses_group = [g.name for g in reservation.groups if g.name == self.name] - if uses_group: - instances.extend(reservation.instances) + rs = [] + if self.vpc_id: + rs.extend(self.connection.get_all_reservations( + filters={'instance.group-id': self.id} + )) + else: + rs.extend(self.connection.get_all_reservations( + filters={'group-id': self.id} + )) + instances = [i for r in rs for i in r.instances] return instances + class IPPermissionsList(list): def startElement(self, name, attrs, connection): @@ -289,6 +293,7 @@ class IPPermissionsList(list): def endElement(self, name, value, connection): pass + class IPPermissions(object): def __init__(self, parent=None): @@ -327,6 +332,7 @@ class IPPermissions(object): self.grants.append(grant) return grant + class GroupOrCIDR(object): def __init__(self, parent=None): diff --git a/boto/gs/key.py b/boto/gs/key.py index 41ad0569..7da1b3dc 100644 --- a/boto/gs/key.py +++ b/boto/gs/key.py @@ -119,6 +119,14 @@ class Key(S3Key): self.component_count = int(value) elif key == 'x-goog-generation': self.generation = value + # Use x-goog-stored-content-encoding and + # x-goog-stored-content-length to indicate original content length + # and encoding, which are transcoding-invariant (so are preferable + # over using content-encoding and size headers). + elif key == 'x-goog-stored-content-encoding': + self.content_encoding = value + elif key == 'x-goog-stored-content-length': + self.size = int(value) def open_read(self, headers=None, query_args='', override_num_retries=None, response_headers=None): diff --git a/boto/iam/__init__.py b/boto/iam/__init__.py index 71cf7177..f0444ac1 100644 --- a/boto/iam/__init__.py +++ b/boto/iam/__init__.py @@ -52,6 +52,9 @@ def regions(): """ return [IAMRegionInfo(name='universal', endpoint='iam.amazonaws.com', + connection_cls=IAMConnection), + IAMRegionInfo(name='us-gov-west-1', + endpoint='iam.us-gov.amazonaws.com', connection_cls=IAMConnection) ] diff --git a/boto/iam/connection.py b/boto/iam/connection.py index adacc8fb..f6fa6338 100644 --- a/boto/iam/connection.py +++ b/boto/iam/connection.py @@ -1004,7 +1004,10 @@ class IAMConnection(AWSQueryConnection): if not alias: raise Exception('No alias associated with this account. Please use iam.create_account_alias() first.') - return "https://%s.signin.aws.amazon.com/console/%s" % (alias, service) + if self.host == 'iam.us-gov.amazonaws.com': + return "https://%s.signin.amazonaws-us-gov.com/console/%s" % (alias, service) + else: + return "https://%s.signin.aws.amazon.com/console/%s" % (alias, service) def get_account_summary(self): """ diff --git a/boto/manage/server.py b/boto/manage/server.py index 2a2b1f16..3acc4b2f 100644 --- a/boto/manage/server.py +++ b/boto/manage/server.py @@ -353,7 +353,7 @@ class Server(Model): for region in regions: ec2 = region.connect() try: - rs = ec2.get_all_instances([instance_id]) + rs = ec2.get_all_reservations([instance_id]) except: rs = [] if len(rs) == 1: @@ -377,7 +377,7 @@ class Server(Model): regions = boto.ec2.regions() for region in regions: ec2 = region.connect() - rs = ec2.get_all_instances() + rs = ec2.get_all_reservations() for reservation in rs: for instance in reservation.instances: try: @@ -413,7 +413,7 @@ class Server(Model): self.ec2 = region.connect() if self.instance_id and not self._instance: try: - rs = self.ec2.get_all_instances([self.instance_id]) + rs = self.ec2.get_all_reservations([self.instance_id]) if len(rs) >= 1: for instance in rs[0].instances: if instance.id == self.instance_id: diff --git a/boto/mashups/server.py b/boto/mashups/server.py index 6cea106c..aa564471 100644 --- a/boto/mashups/server.py +++ b/boto/mashups/server.py @@ -114,7 +114,7 @@ class Server(Model): if not self._instance: if self.instance_id: try: - rs = self.ec2.get_all_instances([self.instance_id]) + rs = self.ec2.get_all_reservations([self.instance_id]) except: return None if len(rs) > 0: diff --git a/boto/pyami/installers/ubuntu/ebs.py b/boto/pyami/installers/ubuntu/ebs.py index a52549b0..3e5b5c28 100644 --- a/boto/pyami/installers/ubuntu/ebs.py +++ b/boto/pyami/installers/ubuntu/ebs.py @@ -122,7 +122,7 @@ class EBSInstaller(Installer): while volume.update() != 'available': boto.log.info('Volume %s not yet available. Current status = %s.' % (volume.id, volume.status)) time.sleep(5) - instance = ec2.get_all_instances([self.instance_id])[0].instances[0] + instance = ec2.get_only_instances([self.instance_id])[0] attempt_attach = True while attempt_attach: try: diff --git a/boto/rds/__init__.py b/boto/rds/__init__.py index c81b8e28..751c5d51 100644 --- a/boto/rds/__init__.py +++ b/boto/rds/__init__.py @@ -30,7 +30,7 @@ from boto.rds.dbsnapshot import DBSnapshot from boto.rds.event import Event from boto.rds.regioninfo import RDSRegionInfo from boto.rds.dbsubnetgroup import DBSubnetGroup - +from boto.rds.vpcsecuritygroupmembership import VPCSecurityGroupMembership def regions(): """ @@ -41,6 +41,8 @@ def regions(): """ return [RDSRegionInfo(name='us-east-1', endpoint='rds.amazonaws.com'), + RDSRegionInfo(name='us-gov-west-1', + endpoint='rds.us-gov-west-1.amazonaws.com'), RDSRegionInfo(name='eu-west-1', endpoint='rds.eu-west-1.amazonaws.com'), RDSRegionInfo(name='us-west-1', @@ -165,6 +167,7 @@ class RDSConnection(AWSQueryConnection): license_model = None, option_group_name = None, iops=None, + vpc_security_groups=None, ): # API version: 2012-09-17 # Parameter notes: @@ -363,6 +366,10 @@ class RDSConnection(AWSQueryConnection): If you specify a value, it must be at least 1000 IOPS and you must allocate 100 GB of storage. + :type vpc_security_groups: list of str or a VPCSecurityGroupMembership object + :param vpc_security_groups: List of VPC security group ids or a list of + VPCSecurityGroupMembership objects this DBInstance should be a member of + :rtype: :class:`boto.rds.dbinstance.DBInstance` :return: The new db instance. """ @@ -390,6 +397,7 @@ class RDSConnection(AWSQueryConnection): # port => Port # preferred_backup_window => PreferredBackupWindow # preferred_maintenance_window => PreferredMaintenanceWindow + # vpc_security_groups => VpcSecurityGroupIds.member.N params = { 'AllocatedStorage': allocated_storage, 'AutoMinorVersionUpgrade': str(auto_minor_version_upgrade).lower() if auto_minor_version_upgrade else None, @@ -424,6 +432,15 @@ class RDSConnection(AWSQueryConnection): l.append(group) self.build_list_params(params, l, 'DBSecurityGroups.member') + if vpc_security_groups: + l = [] + for vpc_grp in vpc_security_groups: + if isinstance(vpc_grp, VPCSecurityGroupMembership): + l.append(vpc_grp.vpc_group) + else: + l.append(vpc_grp) + self.build_list_params(params, l, 'VpcSecurityGroupIds.member') + # Remove any params set to None for k, v in params.items(): if not v: del(params[k]) @@ -507,7 +524,9 @@ class RDSConnection(AWSQueryConnection): preferred_backup_window=None, multi_az=False, apply_immediately=False, - iops=None): + iops=None, + vpc_security_groups=None, + ): """ Modify an existing DBInstance. @@ -583,6 +602,10 @@ class RDSConnection(AWSQueryConnection): If you specify a value, it must be at least 1000 IOPS and you must allocate 100 GB of storage. + :type vpc_security_groups: list of str or a VPCSecurityGroupMembership object + :param vpc_security_groups: List of VPC security group ids or a + VPCSecurityGroupMembership object this DBInstance should be a member of + :rtype: :class:`boto.rds.dbinstance.DBInstance` :return: The modified db instance. """ @@ -599,6 +622,14 @@ class RDSConnection(AWSQueryConnection): else: l.append(group) self.build_list_params(params, l, 'DBSecurityGroups.member') + if vpc_security_groups: + l = [] + for vpc_grp in vpc_security_groups: + if isinstance(vpc_grp, VPCSecurityGroupMembership): + l.append(vpc_grp.vpc_group) + else: + l.append(vpc_grp) + self.build_list_params(params, l, 'VpcSecurityGroupIds.member') if preferred_maintenance_window: params['PreferredMaintenanceWindow'] = preferred_maintenance_window if master_password: @@ -743,10 +774,10 @@ class RDSConnection(AWSQueryConnection): :param engine: Name of database engine. :type description: string - :param description: The description of the new security group + :param description: The description of the new dbparameter group - :rtype: :class:`boto.rds.dbsecuritygroup.DBSecurityGroup` - :return: The newly created DBSecurityGroup + :rtype: :class:`boto.rds.parametergroup.ParameterGroup` + :return: The newly created ParameterGroup """ params = {'DBParameterGroupName': name, 'DBParameterGroupFamily': engine, @@ -755,10 +786,10 @@ class RDSConnection(AWSQueryConnection): def modify_parameter_group(self, name, parameters=None): """ - Modify a parameter group for your account. + Modify a ParameterGroup for your account. :type name: string - :param name: The name of the new parameter group + :param name: The name of the new ParameterGroup :type parameters: list of :class:`boto.rds.parametergroup.Parameter` :param parameters: The new parameters @@ -798,10 +829,10 @@ class RDSConnection(AWSQueryConnection): def delete_parameter_group(self, name): """ - Delete a DBSecurityGroup from your account. + Delete a ParameterGroup from your account. :type key_name: string - :param key_name: The name of the DBSecurityGroup to delete + :param key_name: The name of the ParameterGroup to delete """ params = {'DBParameterGroupName': name} return self.get_status('DeleteDBParameterGroup', params) @@ -1094,7 +1125,8 @@ class RDSConnection(AWSQueryConnection): restore_time=None, dbinstance_class=None, port=None, - availability_zone=None): + availability_zone=None, + db_subnet_group_name=None): """ Create a new DBInstance from a point in time. @@ -1127,6 +1159,11 @@ class RDSConnection(AWSQueryConnection): :param availability_zone: Name of the availability zone to place DBInstance into. + :type db_subnet_group_name: str + :param db_subnet_group_name: A DB Subnet Group to associate with this DB Instance. + If there is no DB Subnet Group, then it is a non-VPC DB + instance. + :rtype: :class:`boto.rds.dbinstance.DBInstance` :return: The newly created DBInstance """ @@ -1142,6 +1179,8 @@ class RDSConnection(AWSQueryConnection): params['Port'] = port if availability_zone: params['AvailabilityZone'] = availability_zone + if db_subnet_group_name is not None: + params['DBSubnetGroupName'] = db_subnet_group_name return self.get_object('RestoreDBInstanceToPointInTime', params, DBInstance) diff --git a/boto/rds/dbinstance.py b/boto/rds/dbinstance.py index e6b51b76..043052ea 100644 --- a/boto/rds/dbinstance.py +++ b/boto/rds/dbinstance.py @@ -22,6 +22,7 @@ from boto.rds.dbsecuritygroup import DBSecurityGroup from boto.rds.parametergroup import ParameterGroup from boto.rds.statusinfo import StatusInfo +from boto.rds.vpcsecuritygroupmembership import VPCSecurityGroupMembership from boto.resultset import ResultSet @@ -65,6 +66,9 @@ class DBInstance(object): Multi-AZ deployment. :ivar iops: The current number of provisioned IOPS for the DB Instance. Can be None if this is a standard instance. + :ivar vpc_security_groups: List of VPC Security Group Membership elements + containing only VpcSecurityGroupMembership.VpcSecurityGroupId and + VpcSecurityGroupMembership.Status subelements. :ivar pending_modified_values: Specifies that changes to the DB Instance are pending. This element is only included when changes are pending. Specific changes are identified by subelements. @@ -94,6 +98,7 @@ class DBInstance(object): self.latest_restorable_time = None self.multi_az = False self.iops = None + self.vpc_security_groups = None self.pending_modified_values = None self._in_endpoint = False self._port = None @@ -114,6 +119,10 @@ class DBInstance(object): self.security_groups = ResultSet([('DBSecurityGroup', DBSecurityGroup)]) return self.security_groups + elif name == 'VpcSecurityGroups': + self.vpc_security_groups = ResultSet([('VpcSecurityGroupMembership', + VPCSecurityGroupMembership)]) + return self.vpc_security_groups elif name == 'PendingModifiedValues': self.pending_modified_values = PendingModifiedValues() return self.pending_modified_values @@ -264,6 +273,7 @@ class DBInstance(object): preferred_backup_window=None, multi_az=False, iops=None, + vpc_security_groups=None, apply_immediately=False): """ Modify this DBInstance. @@ -335,6 +345,10 @@ class DBInstance(object): If you specify a value, it must be at least 1000 IOPS and you must allocate 100 GB of storage. + :type vpc_security_groups: list + :param vpc_security_groups: List of VPCSecurityGroupMembership + that this DBInstance is a memberof. + :rtype: :class:`boto.rds.dbinstance.DBInstance` :return: The modified db instance. """ @@ -349,7 +363,8 @@ class DBInstance(object): preferred_backup_window, multi_az, apply_immediately, - iops) + iops, + vpc_security_groups) class PendingModifiedValues(dict): diff --git a/boto/rds/vpcsecuritygroupmembership.py b/boto/rds/vpcsecuritygroupmembership.py new file mode 100644 index 00000000..e0092e9c --- /dev/null +++ b/boto/rds/vpcsecuritygroupmembership.py @@ -0,0 +1,85 @@ +# Copyright (c) 2013 Anthony Tonns http://www.corsis.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. + +""" +Represents a VPCSecurityGroupMembership +""" + + +class VPCSecurityGroupMembership(object): + """ + Represents VPC Security Group that this RDS database is a member of + + Properties reference available from the AWS documentation at + http://docs.aws.amazon.com/AmazonRDS/latest/APIReference/\ + API_VpcSecurityGroupMembership.html + + Example:: + pri = "sg-abcdefgh" + sec = "sg-hgfedcba" + + # Create with list of str + db = c.create_dbinstance(... vpc_security_groups=[pri], ... ) + + # Modify with list of str + db.modify(... vpc_security_groups=[pri,sec], ... ) + + # Create with objects + memberships = [] + membership = VPCSecurityGroupMembership() + membership.vpc_group = pri + memberships.append(membership) + + db = c.create_dbinstance(... vpc_security_groups=memberships, ... ) + + # Modify with objects + memberships = d.vpc_security_groups + membership = VPCSecurityGroupMembership() + membership.vpc_group = sec + memberships.append(membership) + + db.modify(... vpc_security_groups=memberships, ... ) + + :ivar connection: :py:class:`boto.rds.RDSConnection` associated with the + current object + :ivar vpc_group: This id of the VPC security group + :ivar status: Status of the VPC security group membership + <boto.ec2.securitygroup.SecurityGroup>` objects that this RDS Instance + is a member of + """ + def __init__(self, connection=None, status=None, vpc_group=None): + self.connection = connection + self.status = status + self.vpc_group = vpc_group + + def __repr__(self): + return 'VPCSecurityGroupMembership:%s' % self.vpc_group + + def startElement(self, name, attrs, connection): + pass + + def endElement(self, name, value, connection): + if name == 'VpcSecurityGroupId': + self.vpc_group = value + elif name == 'Status': + self.status = value + else: + setattr(self, name, value) diff --git a/boto/route53/record.py b/boto/route53/record.py index 3fe75abb..d26ca119 100644 --- a/boto/route53/record.py +++ b/boto/route53/record.py @@ -161,6 +161,7 @@ class ResourceRecordSets(ResultSet): def __iter__(self): """Override the next function to support paging""" results = ResultSet.__iter__(self) + truncated = self.is_truncated while results: for obj in results: yield obj @@ -169,6 +170,8 @@ class ResourceRecordSets(ResultSet): results = self.connection.get_all_rrsets(self.hosted_zone_id, name=self.next_record_name, type=self.next_record_type) else: results = None + self.is_truncated = truncated + diff --git a/boto/s3/__init__.py b/boto/s3/__init__.py index 30d610d2..f7237157 100644 --- a/boto/s3/__init__.py +++ b/boto/s3/__init__.py @@ -53,6 +53,9 @@ def regions(): return [S3RegionInfo(name='us-east-1', endpoint='s3.amazonaws.com', connection_cls=S3Connection), + S3RegionInfo(name='us-gov-west-1', + endpoint='s3-us-gov-west-1.amazonaws.com', + connection_cls=S3Connection), S3RegionInfo(name='us-west-1', endpoint='s3-us-west-1.amazonaws.com', connection_cls=S3Connection), diff --git a/boto/sns/__init__.py b/boto/sns/__init__.py index 565317a1..4ed0539a 100644 --- a/boto/sns/__init__.py +++ b/boto/sns/__init__.py @@ -39,6 +39,9 @@ def regions(): RegionInfo(name='eu-west-1', endpoint='sns.eu-west-1.amazonaws.com', connection_cls=SNSConnection), + RegionInfo(name='us-gov-west-1', + endpoint='sns.us-gov-west-1.amazonaws.com', + connection_cls=SNSConnection), RegionInfo(name='us-west-1', endpoint='sns.us-west-1.amazonaws.com', connection_cls=SNSConnection), diff --git a/boto/sns/connection.py b/boto/sns/connection.py index cd97a731..c2c23f54 100644 --- a/boto/sns/connection.py +++ b/boto/sns/connection.py @@ -71,6 +71,33 @@ class SNSConnection(AWSQueryConnection): security_token=security_token, validate_certs=validate_certs) + def _build_dict_as_list_params(self, params, dictionary, name): + """ + Serialize a parameter 'name' which value is a 'dictionary' into a list of parameters. + + See: http://docs.aws.amazon.com/sns/latest/api/API_SetPlatformApplicationAttributes.html + For example:: + + dictionary = {'PlatformPrincipal': 'foo', 'PlatformCredential': 'bar'} + name = 'Attributes' + + would result in params dict being populated with: + Attributes.entry.1.key = PlatformPrincipal + Attributes.entry.1.value = foo + Attributes.entry.2.key = PlatformCredential + Attributes.entry.2.value = bar + + :param params: the resulting parameters will be added to this dict + :param dictionary: dict - value of the serialized parameter + :param name: name of the serialized parameter + """ + items = sorted(dictionary.items(), key=lambda x:x[0]) + for kv, index in zip(items, range(1, len(items)+1)): + key, value = kv + prefix = '%s.entry.%s' % (name, index) + params['%s.key' % prefix] = key + params['%s.value' % prefix] = value + def _required_auth_capability(self): return ['hmac-v4'] @@ -182,8 +209,8 @@ class SNSConnection(AWSQueryConnection): params = {'TopicArn': topic} return self._make_request('DeleteTopic', params, '/', 'GET') - def publish(self, topic=None, message=None, subject=None, - target_arn=None): + def publish(self, topic=None, message=None, subject=None, target_arn=None, + message_structure=None): """ Get properties of a Topic @@ -195,12 +222,20 @@ class SNSConnection(AWSQueryConnection): Messages must be UTF-8 encoded strings and be at most 4KB in size. + :type message_structure: string + :param message_structure: Optional parameter. If left as ``None``, + plain text will be sent. If set to ``json``, + your message should be a JSON string that + matches the structure described at + http://docs.aws.amazon.com/sns/latest/dg/PublishTopic.html#sns-message-formatting-by-protocol + :type subject: string :param subject: Optional parameter to be used as the "Subject" line of the email notifications. :type target_arn: string - :param target_arn: + :param target_arn: Optional parameter for either TopicArn or + EndpointArn, but not both. """ if message is None: @@ -215,6 +250,8 @@ class SNSConnection(AWSQueryConnection): params['TopicArn'] = topic if target_arn is not None: params['TargetArn'] = target_arn + if message_structure is not None: + params['MessageStructure'] = message_structure return self._make_request('Publish', params) def subscribe(self, topic, protocol, endpoint): @@ -406,7 +443,7 @@ class SNSConnection(AWSQueryConnection): if platform is not None: params['Platform'] = platform if attributes is not None: - params['Attributes'] = attributes + self._build_dict_as_list_params(params, attributes, 'Attributes') return self._make_request(action='CreatePlatformApplication', params=params) @@ -453,7 +490,7 @@ class SNSConnection(AWSQueryConnection): if platform_application_arn is not None: params['PlatformApplicationArn'] = platform_application_arn if attributes is not None: - params['Attributes'] = attributes + self._build_dict_as_list_params(params, attributes, 'Attributes') return self._make_request(action='SetPlatformApplicationAttributes', params=params) @@ -601,7 +638,7 @@ class SNSConnection(AWSQueryConnection): if custom_user_data is not None: params['CustomUserData'] = custom_user_data if attributes is not None: - params['Attributes'] = attributes + self._build_dict_as_list_params(params, attributes, 'Attributes') return self._make_request(action='CreatePlatformEndpoint', params=params) @@ -654,7 +691,7 @@ class SNSConnection(AWSQueryConnection): if endpoint_arn is not None: params['EndpointArn'] = endpoint_arn if attributes is not None: - params['Attributes'] = attributes + self._build_dict_as_list_params(params, attributes, 'Attributes') return self._make_request(action='SetEndpointAttributes', params=params) diff --git a/boto/sqs/__init__.py b/boto/sqs/__init__.py index b59a4572..973b8ba5 100644 --- a/boto/sqs/__init__.py +++ b/boto/sqs/__init__.py @@ -32,6 +32,8 @@ def regions(): """ return [SQSRegionInfo(name='us-east-1', endpoint='queue.amazonaws.com'), + SQSRegionInfo(name='us-gov-west-1', + endpoint='sqs.us-gov-west-1.amazonaws.com'), SQSRegionInfo(name='eu-west-1', endpoint='eu-west-1.queue.amazonaws.com'), SQSRegionInfo(name='us-west-1', diff --git a/boto/sts/__init__.py b/boto/sts/__init__.py index 05fd74e5..0b7a8de2 100644 --- a/boto/sts/__init__.py +++ b/boto/sts/__init__.py @@ -33,7 +33,11 @@ def regions(): """ return [RegionInfo(name='us-east-1', endpoint='sts.amazonaws.com', + connection_cls=STSConnection), + RegionInfo(name='us-gov-west-1', + endpoint='sts.us-gov-west-1.amazonaws.com', connection_cls=STSConnection) + ] diff --git a/boto/sts/connection.py b/boto/sts/connection.py index bdf21859..5f488e26 100644 --- a/boto/sts/connection.py +++ b/boto/sts/connection.py @@ -69,12 +69,13 @@ class STSConnection(AWSQueryConnection): is_secure=True, port=None, proxy=None, proxy_port=None, proxy_user=None, proxy_pass=None, debug=0, https_connection_factory=None, region=None, path='/', - converter=None, validate_certs=True): + converter=None, validate_certs=True, anon=False): if not region: region = RegionInfo(self, self.DefaultRegionName, self.DefaultRegionEndpoint, connection_cls=STSConnection) self.region = region + self.anon = anon self._mutex = threading.Semaphore() AWSQueryConnection.__init__(self, aws_access_key_id, aws_secret_access_key, @@ -85,7 +86,10 @@ class STSConnection(AWSQueryConnection): validate_certs=validate_certs) def _required_auth_capability(self): - return ['sign-v2'] + if self.anon: + return ['pure-query'] + else: + return ['sign-v2'] def _check_token_cache(self, token_key, duration=None, window_seconds=60): token = _session_token_cache.get(token_key, None) diff --git a/boto/sts/credentials.py b/boto/sts/credentials.py index a28d1067..21828db7 100644 --- a/boto/sts/credentials.py +++ b/boto/sts/credentials.py @@ -42,6 +42,7 @@ class Credentials(object): self.secret_key = None self.session_token = None self.expiration = None + self.request_id = None @classmethod def from_json(cls, json_doc): @@ -138,6 +139,7 @@ class Credentials(object): delta = ts - now return delta.total_seconds() <= 0 + class FederationToken(object): """ :ivar credentials: A Credentials object containing the credentials. @@ -153,6 +155,7 @@ class FederationToken(object): self.federated_user_arn = None self.federated_user_id = None self.packed_policy_size = None + self.request_id = None def startElement(self, name, attrs, connection): if name == 'Credentials': diff --git a/boto/swf/__init__.py b/boto/swf/__init__.py index 5eab6bc0..3594444d 100644 --- a/boto/swf/__init__.py +++ b/boto/swf/__init__.py @@ -27,6 +27,7 @@ import boto.swf.layer1 REGION_ENDPOINTS = { 'us-east-1': 'swf.us-east-1.amazonaws.com', + 'us-gov-west-1': 'swf.us-gov-west-1.amazonaws.com', 'us-west-1': 'swf.us-west-1.amazonaws.com', 'us-west-2': 'swf.us-west-2.amazonaws.com', 'sa-east-1': 'swf.sa-east-1.amazonaws.com', diff --git a/boto/swf/layer1.py b/boto/swf/layer1.py index 8e1af903..264016bd 100644 --- a/boto/swf/layer1.py +++ b/boto/swf/layer1.py @@ -85,7 +85,7 @@ class Layer1(AWSAuthConnection): debug, session_token) def _required_auth_capability(self): - return ['hmac-v3-http'] + return ['hmac-v4'] @classmethod def _normalize_request_dict(cls, data): @@ -112,7 +112,7 @@ class Layer1(AWSAuthConnection): :type data: dict :param data: Specifies request parameters associated with the action. - """ + """ self._normalize_request_dict(data) json_input = json.dumps(data) return self.make_request(action, json_input, object_hook) @@ -175,7 +175,7 @@ class Layer1(AWSAuthConnection): :raises: UnknownResourceFault, SWFOperationNotPermittedError """ return self.json_request('PollForActivityTask', { - 'domain': domain, + 'domain': domain, 'taskList': {'name': task_list}, 'identity': identity, }) @@ -243,7 +243,7 @@ class Layer1(AWSAuthConnection): 'taskToken': task_token, 'details': details, }) - + def record_activity_task_heartbeat(self, task_token, details=None): """ Used by activity workers to report to the service that the @@ -317,7 +317,7 @@ class Layer1(AWSAuthConnection): :raises: UnknownResourceFault, SWFOperationNotPermittedError """ return self.json_request('PollForDecisionTask', { - 'domain': domain, + 'domain': domain, 'taskList': {'name': task_list}, 'identity': identity, 'maximumPageSize': maximum_page_size, @@ -351,7 +351,7 @@ class Layer1(AWSAuthConnection): return self.json_request('RespondDecisionTaskCompleted', { 'taskToken': task_token, 'decisions': decisions, - 'executionContext': execution_context, + 'executionContext': execution_context, }) def request_cancel_workflow_execution(self, domain, workflow_id, @@ -378,7 +378,7 @@ class Layer1(AWSAuthConnection): :raises: UnknownResourceFault, SWFOperationNotPermittedError """ return self.json_request('RequestCancelWorkflowExecution', { - 'domain': domain, + 'domain': domain, 'workflowId': workflow_id, 'runId': run_id, }) @@ -465,7 +465,7 @@ class Layer1(AWSAuthConnection): SWFOperationNotPermittedError, DefaultUndefinedFault """ return self.json_request('StartWorkflowExecution', { - 'domain': domain, + 'domain': domain, 'workflowId': workflow_id, 'workflowType': {'name': workflow_name, 'version': workflow_version}, @@ -509,7 +509,7 @@ class Layer1(AWSAuthConnection): :raises: UnknownResourceFault, SWFOperationNotPermittedError """ return self.json_request('SignalWorkflowExecution', { - 'domain': domain, + 'domain': domain, 'signalName': signal_name, 'workflowId': workflow_id, 'input': input, @@ -567,7 +567,7 @@ class Layer1(AWSAuthConnection): :raises: UnknownResourceFault, SWFOperationNotPermittedError """ return self.json_request('TerminateWorkflowExecution', { - 'domain': domain, + 'domain': domain, 'workflowId': workflow_id, 'childPolicy': child_policy, 'details': details, @@ -682,7 +682,7 @@ class Layer1(AWSAuthConnection): 'activityType': {'name': activity_name, 'version': activity_version} }) - + ## Workflow Management def register_workflow_type(self, domain, name, version, @@ -756,8 +756,8 @@ class Layer1(AWSAuthConnection): UnknownResourceFault, SWFOperationNotPermittedError """ return self.json_request('RegisterWorkflowType', { - 'domain': domain, - 'name': name, + 'domain': domain, + 'name': name, 'version': version, 'defaultTaskList': {'name': task_list}, 'defaultChildPolicy': default_child_policy, @@ -765,7 +765,7 @@ class Layer1(AWSAuthConnection): 'defaultTaskStartToCloseTimeout': default_task_start_to_close_timeout, 'description': description, }) - + def deprecate_workflow_type(self, domain, workflow_name, workflow_version): """ Deprecates the specified workflow type. After a workflow type @@ -905,7 +905,7 @@ class Layer1(AWSAuthConnection): 'nextPageToken': next_page_token, 'reverseOrder': reverse_order, }) - + def describe_activity_type(self, domain, activity_name, activity_version): """ Returns information about the specified activity type. This @@ -975,7 +975,7 @@ class Layer1(AWSAuthConnection): :raises: SWFOperationNotPermittedError, UnknownResourceFault """ return self.json_request('ListWorkflowTypes', { - 'domain': domain, + 'domain': domain, 'name': name, 'registrationStatus': registration_status, 'maximumPageSize': maximum_page_size, @@ -1031,7 +1031,7 @@ class Layer1(AWSAuthConnection): """ return self.json_request('DescribeWorkflowExecution', { 'domain': domain, - 'execution': {'runId': run_id, + 'execution': {'runId': run_id, 'workflowId': workflow_id}, }) @@ -1080,13 +1080,13 @@ class Layer1(AWSAuthConnection): """ return self.json_request('GetWorkflowExecutionHistory', { 'domain': domain, - 'execution': {'runId': run_id, + 'execution': {'runId': run_id, 'workflowId': workflow_id}, 'maximumPageSize': maximum_page_size, 'nextPageToken': next_page_token, 'reverseOrder': reverse_order, }) - + def count_open_workflow_executions(self, domain, latest_date, oldest_date, tag=None, workflow_id=None, @@ -1454,7 +1454,7 @@ class Layer1(AWSAuthConnection): 'nextPageToken': next_page_token, 'reverseOrder': reverse_order, }) - + def describe_domain(self, name): """ Returns information about the specified domain including @@ -1486,7 +1486,7 @@ class Layer1(AWSAuthConnection): :raises: UnknownResourceFault, SWFOperationNotPermittedError """ return self.json_request('CountPendingDecisionTasks', { - 'domain': domain, + 'domain': domain, 'taskList': {'name': task_list} }) @@ -1507,6 +1507,6 @@ class Layer1(AWSAuthConnection): :raises: UnknownResourceFault, SWFOperationNotPermittedError """ return self.json_request('CountPendingActivityTasks', { - 'domain': domain, + 'domain': domain, 'taskList': {'name': task_list} }) diff --git a/boto/vpc/__init__.py b/boto/vpc/__init__.py index e529b6f3..0e5eb3fb 100644 --- a/boto/vpc/__init__.py +++ b/boto/vpc/__init__.py @@ -52,6 +52,10 @@ def regions(**kw_params): endpoint=RegionData[region_name], connection_cls=VPCConnection) regions.append(region) + regions.append(RegionInfo(name='us-gov-west-1', + endpoint=RegionData[region_name], + connection_cls=VPCConnection) + ) return regions diff --git a/docs/source/autoscale_tut.rst b/docs/source/autoscale_tut.rst index 86fc529f..d1eaf3f9 100644 --- a/docs/source/autoscale_tut.rst +++ b/docs/source/autoscale_tut.rst @@ -201,8 +201,7 @@ To retrieve the instances in your autoscale group: >>> ec2 = boto.ec2.connect_to_region('us-west-2) >>> conn.get_all_groups(names=['my_group'])[0] >>> instance_ids = [i.instance_id for i in group.instances] ->>> reservations = ec2.get_all_instances(instance_ids) ->>> instances = [i for r in reservations for i in r.instances] +>>> instances = ec2.get_only_instances(instance_ids) To delete your autoscale group, we first need to shutdown all the instances: diff --git a/docs/source/dynamodb2_tut.rst b/docs/source/dynamodb2_tut.rst index b6e98118..3e37675c 100644 --- a/docs/source/dynamodb2_tut.rst +++ b/docs/source/dynamodb2_tut.rst @@ -73,8 +73,8 @@ Simple example:: A full example:: + >>> import boto.dynamodb2 >>> from boto.dynamodb2.fields import HashKey, RangeKey, KeysOnlyIndex, AllIndex - >>> from boto.dynamodb2.layer1 import DynamoDBConnection >>> from boto.dynamodb2.table import Table >>> from boto.dynamodb2.types import NUMBER @@ -90,11 +90,7 @@ A full example:: ... ]) ... ], ... # If you need to specify custom parameters like keys or region info... - ... connection=DynamoDBConnection( - ... aws_access_key_id='key', - ... aws_secret_access_key='key', - ... region='us-west-2' - ... )) + ... connection= boto.dynamodb2.connect_to_region('us-east-1')) Using an Existing Table diff --git a/docs/source/ec2_tut.rst b/docs/source/ec2_tut.rst index d9ffe38c..6e179262 100644 --- a/docs/source/ec2_tut.rst +++ b/docs/source/ec2_tut.rst @@ -88,7 +88,7 @@ Checking What Instances Are Running ----------------------------------- You can also get information on your currently running instances:: - >>> reservations = conn.get_all_instances() + >>> reservations = conn.get_all_reservations() >>> reservations [Reservation:r-00000000] diff --git a/docs/source/releasenotes/v2.11.0.rst b/docs/source/releasenotes/v2.11.0.rst new file mode 100644 index 00000000..267d4a15 --- /dev/null +++ b/docs/source/releasenotes/v2.11.0.rst @@ -0,0 +1,62 @@ +boto v2.11.0 +============ + +:date: 2013/08/29 + +This release adds Public IP address support for VPCs created by EC2. It also +makes the GovCloud region available for all services. Finally, this release +also fixes a number of bugs. + + +Features +-------- + +* Added Public IP address support within VPCs created by EC2. (:sha:`be132d1`) +* All services can now easily use GovCloud. (:issue:`1651`, :sha:`542a301`, + :sha:`3c56121`, :sha:`9167d89`) +* Added ``db_subnet_group`` to + ``RDSConnection.restore_dbinstance_from_point_in_time``. (:issue:`1640`, + :sha:`06592b9`) +* Added ``monthly_backups`` to EC2's ``trim_snapshots``. (:issue:`1688`, + :sha:`a2ad606`, :sha:`2998c11`, :sha:`e32d033`) +* Added ``get_all_reservations`` & ``get_only_instances`` methods to EC2. + (:issue:`1572`, :sha:`ffc6cc0`) + + +Bugfixes +-------- + +* Fixed the parsing of CloudFormation's ``LastUpdatedTime``. (:issue:`1667`, + :sha:` 70f363a`) +* Fixed STS' ``assume_role_with_web_identity`` to work correctly. + (:issue:`1671`, :sha:`ed1f403`, :sha:`ca794d5`, :sha:`ed7e563`, + :sha:`859762d`) +* Fixed how VPC security group filtering is done in EC2. (:issue:`1665`, + :issue:`1677`, :sha:`be00956`, :sha:`5e85dd1`, :sha:`e63aae8`) +* Fixed fetching more than 100 records with ``ResourceRecordSet``. + (:issue:`1647`, :issue:`1648`, :issue:`1680`, :sha:`b64dd4f`, :sha:`276df7e`, + :sha:`e57cab0`, :sha:`e62a58b`, :sha:`4c81bea`, :sha:`a3c635b`) +* Fixed how VPC Security Groups are referred to when working with RDS. + (:issue:`1602`, :issue:`1683`, :issue:`1685`, :issue:`1694`, :sha:`012aa0c`, + :sha:`d5c6dfa`, :sha:`7841230`, :sha:`0a90627`, :sha:`ed4fd8c`, + :sha:`61d394b`, :sha:`ebe84c9`, :sha:`a6b0f7e`) +* Google Storage ``Key`` now uses transcoding-invariant headers where possible. + (:sha:`d36eac3`) +* Doing non-multipart uploads when using ``s3put`` no longer requires having + the ``ListBucket`` permission. (:issue:`1642`, :issue:`1693`, :sha:`f35e914`) +* Fixed the serialization of ``attributes`` in a variety of SNS methods. + (:issue:`1686`, :sha:`4afb3dd`, :sha:`a58af54`) +* Fixed SNS to be better behaved when constructing an mobile push notification. + (:issue:`1692`, :sha:`62fdf34`) +* Moved SWF to SigV4. (:sha:`ef7d255`) +* Several documentation improvements/fixes: + + * Updated the DynamoDB v2 docs to correct how the connection is built. + (:issue:`1662`, :sha:`047962d`) + * Fixed a typo in the DynamoDB v2 docstring for ``Table.create``. + (:sha:`be00956`) + * Fixed a typo in the DynamoDB v2 docstring for ``Table`` for custom + connections. (:issue:`1681`, :sha:`6a53020`) + * Fixed incorrect parameter names for ``DBParameterGroup`` in RDS. + (:issue:`1682`, :sha:`0d46aed`) + * Fixed a typo in the SQS tutorial. (:issue:`1684`, :sha:`38b7889`) diff --git a/docs/source/sqs_tut.rst b/docs/source/sqs_tut.rst index d4d69c98..72ccca1d 100644 --- a/docs/source/sqs_tut.rst +++ b/docs/source/sqs_tut.rst @@ -229,7 +229,7 @@ to count the number of messages in a queue: >>> q.count() 10 -This can be handy but is command as well as the other two utility methods +This can be handy but this command as well as the other two utility methods I'll describe in a minute are inefficient and should be used with caution on queues with lots of messages (e.g. many hundreds or more). Similarly, you can clear (delete) all messages in a queue with: diff --git a/tests/integration/ec2/test_cert_verification.py b/tests/integration/ec2/test_cert_verification.py index d2428fa0..67880f76 100644 --- a/tests/integration/ec2/test_cert_verification.py +++ b/tests/integration/ec2/test_cert_verification.py @@ -37,4 +37,4 @@ class CertVerificationTest(unittest.TestCase): def test_certs(self): for region in boto.ec2.regions(): c = region.connect() - c.get_all_instances() + c.get_all_reservations() diff --git a/tests/integration/ec2/vpc/test_connection.py b/tests/integration/ec2/vpc/test_connection.py index 59c07343..45fd82f4 100644 --- a/tests/integration/ec2/vpc/test_connection.py +++ b/tests/integration/ec2/vpc/test_connection.py @@ -69,7 +69,7 @@ class TestVPCConnection(unittest.TestCase): time.sleep(10) instance = reservation.instances[0] self.addCleanup(self.terminate_instance, instance) - retrieved = self.api.get_all_instances(instance_ids=[instance.id]) + retrieved = self.api.get_all_reservations(instance_ids=[instance.id]) self.assertEqual(len(retrieved), 1) retrieved_instances = retrieved[0].instances self.assertEqual(len(retrieved_instances), 1) diff --git a/tests/integration/route53/test_resourcerecordsets.py b/tests/integration/route53/test_resourcerecordsets.py index c6baadfc..9c8f3b22 100644 --- a/tests/integration/route53/test_resourcerecordsets.py +++ b/tests/integration/route53/test_resourcerecordsets.py @@ -23,7 +23,6 @@ import unittest from boto.route53.connection import Route53Connection from boto.route53.record import ResourceRecordSets -from boto.exception import TooManyRecordsException class TestRoute53ResourceRecordSets(unittest.TestCase): @@ -48,6 +47,53 @@ class TestRoute53ResourceRecordSets(unittest.TestCase): deleted.add_value('192.168.0.25') rrs.commit() + def test_record_count(self): + rrs = ResourceRecordSets(self.conn, self.zone.id) + hosts = 101 + + for hostid in range(hosts): + rec = "test" + str(hostid) + ".example.com" + created = rrs.add_change("CREATE", rec, "A") + ip = '192.168.0.' + str(hostid) + created.add_value(ip) + + # Max 100 changes per commit + if (hostid + 1) % 100 == 0: + rrs.commit() + rrs = ResourceRecordSets(self.conn, self.zone.id) + + rrs.commit() + + all_records = self.conn.get_all_rrsets(self.zone.id) + + # First time around was always fine + i = 0 + for rset in all_records: + i += 1 + + # Second time was a failure + i = 0 + for rset in all_records: + i += 1 + + # Cleanup indivual records + rrs = ResourceRecordSets(self.conn, self.zone.id) + for hostid in range(hosts): + rec = "test" + str(hostid) + ".example.com" + deleted = rrs.add_change("DELETE", rec, "A") + ip = '192.168.0.' + str(hostid) + deleted.add_value(ip) + + # Max 100 changes per commit + if (hostid + 1) % 100 == 0: + rrs.commit() + rrs = ResourceRecordSets(self.conn, self.zone.id) + + rrs.commit() + + # 2nd count should match the number of hosts plus NS/SOA records + records = hosts + 2 + self.assertEqual(i, records) if __name__ == '__main__': unittest.main() diff --git a/tests/integration/sqs/test_connection.py b/tests/integration/sqs/test_connection.py index 9b2ab59a..237ae7e2 100644 --- a/tests/integration/sqs/test_connection.py +++ b/tests/integration/sqs/test_connection.py @@ -239,3 +239,44 @@ class SQSConnectionTest(unittest.TestCase): # Wait long enough for SQS to finally remove the queues. time.sleep(90) self.assertEqual(len(conn.get_all_queues()), initial_count) + + def test_get_messages_attributes(self): + conn = SQSConnection() + current_timestamp = int(time.time()) + queue_name = 'test%d' % int(time.time()) + test = conn.create_queue(queue_name) + self.addCleanup(conn.delete_queue, test) + time.sleep(65) + + # Put a message in the queue. + m1 = Message() + m1.set_body('This is a test message.') + test.write(m1) + self.assertEqual(test.count(), 1) + + # Check all attributes. + msgs = test.get_messages( + num_messages=1, + attributes='All' + ) + for msg in msgs: + self.assertEqual(msg.attributes['ApproximateReceiveCount'], '1') + first_rec = msg.attributes['ApproximateFirstReceiveTimestamp'] + first_rec = int(first_rec) / 1000 + self.assertTrue(first_rec >= current_timestamp) + + # Put another message in the queue. + m2 = Message() + m2.set_body('This is another test message.') + test.write(m2) + self.assertEqual(test.count(), 1) + + # Check a specific attribute. + msgs = test.get_messages( + num_messages=1, + attributes='ApproximateReceiveCount' + ) + for msg in msgs: + self.assertEqual(msg.attributes['ApproximateReceiveCount'], '1') + with self.assertRaises(KeyError): + msg.attributes['ApproximateFirstReceiveTimestamp'] diff --git a/tests/integration/sts/test_session_token.py b/tests/integration/sts/test_session_token.py index 3d548b9f..d47071d9 100644 --- a/tests/integration/sts/test_session_token.py +++ b/tests/integration/sts/test_session_token.py @@ -67,13 +67,15 @@ class SessionTokenTest (unittest.TestCase): print '--- tests completed ---' def test_assume_role_with_web_identity(self): - c = STSConnection() + c = STSConnection(anon=True) + arn = 'arn:aws:iam::000240903217:role/FederatedWebIdentityRole' + wit = 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' try: creds = c.assume_role_with_web_identity( - 'arn:aws:s3:::my_corporate_bucket/*', - 'guestuser', - 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9', + role_arn=arn, + role_session_name='guestuser', + web_identity_token=wit, provider_id='www.amazon.com', ) except BotoServerError as err: diff --git a/tests/unit/auth/test_query.py b/tests/unit/auth/test_query.py new file mode 100644 index 00000000..fa5882c9 --- /dev/null +++ b/tests/unit/auth/test_query.py @@ -0,0 +1,76 @@ +# Copyright (c) 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, dis- +# tribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the fol- +# lowing conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- +# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +import copy +from mock import Mock +from tests.unit import unittest + +from boto.auth import QueryAuthHandler +from boto.connection import HTTPRequest + + +class TestQueryAuthHandler(unittest.TestCase): + def setUp(self): + self.provider = Mock() + self.provider.access_key = 'access_key' + self.provider.secret_key = 'secret_key' + self.request = HTTPRequest( + method='GET', + protocol='https', + host='sts.amazonaws.com', + port=443, + path='/', + auth_path=None, + params={ + 'Action': 'AssumeRoleWithWebIdentity', + 'Version': '2011-06-15', + 'RoleSessionName': 'web-identity-federation', + 'ProviderId': '2012-06-01', + 'WebIdentityToken': 'Atza|IQEBLjAsAhRkcxQ', + }, + headers={}, + body='' + ) + + def test_escape_value(self): + auth = QueryAuthHandler('sts.amazonaws.com', + Mock(), self.provider) + # This should **NOT** get escaped. + value = auth._escape_value('Atza|IQEBLjAsAhRkcxQ') + self.assertEqual(value, 'Atza|IQEBLjAsAhRkcxQ') + + def test_build_query_string(self): + auth = QueryAuthHandler('sts.amazonaws.com', + Mock(), self.provider) + query_string = auth._build_query_string(self.request.params) + self.assertEqual(query_string, 'Action=AssumeRoleWithWebIdentity' + \ + '&ProviderId=2012-06-01&RoleSessionName=web-identity-federation' + \ + '&Version=2011-06-15&WebIdentityToken=Atza|IQEBLjAsAhRkcxQ') + + def test_add_auth(self): + auth = QueryAuthHandler('sts.amazonaws.com', + Mock(), self.provider) + req = copy.copy(self.request) + auth.add_auth(req) + self.assertEqual(req.path, + '/?Action=AssumeRoleWithWebIdentity' + \ + '&ProviderId=2012-06-01&RoleSessionName=web-identity-federation' + \ + '&Version=2011-06-15&WebIdentityToken=Atza|IQEBLjAsAhRkcxQ') diff --git a/tests/unit/auth/test_sigv4.py b/tests/unit/auth/test_sigv4.py index 64a72ab3..406dac0c 100644 --- a/tests/unit/auth/test_sigv4.py +++ b/tests/unit/auth/test_sigv4.py @@ -99,6 +99,60 @@ class TestSigV4Handler(unittest.TestCase): canonical_uri = auth.canonical_uri(request) self.assertEqual(canonical_uri, '/x/x.html') + def test_credential_scope(self): + # test the AWS standard regions IAM endpoint + auth = HmacAuthV4Handler('iam.amazonaws.com', + Mock(), self.provider) + request = HTTPRequest( + 'POST', 'https', 'iam.amazonaws.com', 443, + '/', '/', + {'Action': 'ListAccountAliases', 'Version': '2010-05-08'}, + { + 'Content-Length': '44', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Amz-Date': '20130808T013210Z' + }, + 'Action=ListAccountAliases&Version=2010-05-08') + credential_scope = auth.credential_scope(request) + region_name = credential_scope.split('/')[1] + self.assertEqual(region_name, 'us-east-1') + + # test the AWS GovCloud region IAM endpoint + auth = HmacAuthV4Handler('iam.us-gov.amazonaws.com', + Mock(), self.provider) + request = HTTPRequest( + 'POST', 'https', 'iam.us-gov.amazonaws.com', 443, + '/', '/', + {'Action': 'ListAccountAliases', 'Version': '2010-05-08'}, + { + 'Content-Length': '44', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Amz-Date': '20130808T013210Z' + }, + 'Action=ListAccountAliases&Version=2010-05-08') + credential_scope = auth.credential_scope(request) + region_name = credential_scope.split('/')[1] + self.assertEqual(region_name, 'us-gov-west-1') + + # iam.us-west-1.amazonaws.com does not exist however this + # covers the remaining region_name control structure for a + # different region name + auth = HmacAuthV4Handler('iam.us-west-1.amazonaws.com', + Mock(), self.provider) + request = HTTPRequest( + 'POST', 'https', 'iam.us-west-1.amazonaws.com', 443, + '/', '/', + {'Action': 'ListAccountAliases', 'Version': '2010-05-08'}, + { + 'Content-Length': '44', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Amz-Date': '20130808T013210Z' + }, + 'Action=ListAccountAliases&Version=2010-05-08') + credential_scope = auth.credential_scope(request) + region_name = credential_scope.split('/')[1] + self.assertEqual(region_name, 'us-west-1') + def test_headers_to_sign(self): auth = HmacAuthV4Handler('glacier.us-east-1.amazonaws.com', Mock(), self.provider) diff --git a/tests/unit/cloudformation/test_connection.py b/tests/unit/cloudformation/test_connection.py index 7b1fbc59..766e6f1c 100644..100755 --- a/tests/unit/cloudformation/test_connection.py +++ b/tests/unit/cloudformation/test_connection.py @@ -462,14 +462,14 @@ class TestCloudFormationListStackResources(CloudFormationConnectionBase): <member> <ResourceStatus>CREATE_COMPLETE</ResourceStatus> <LogicalResourceId>SampleDB</LogicalResourceId> - <LastUpdatedTimestamp>2011-06-21T20:25:57Z</LastUpdatedTimestamp> + <LastUpdatedTime>2011-06-21T20:25:57Z</LastUpdatedTime> <PhysicalResourceId>My-db-ycx</PhysicalResourceId> <ResourceType>AWS::RDS::DBInstance</ResourceType> </member> <member> <ResourceStatus>CREATE_COMPLETE</ResourceStatus> <LogicalResourceId>CPUAlarmHigh</LogicalResourceId> - <LastUpdatedTimestamp>2011-06-21T20:29:23Z</LastUpdatedTimestamp> + <LastUpdatedTime>2011-06-21T20:29:23Z</LastUpdatedTime> <PhysicalResourceId>MyStack-CPUH-PF</PhysicalResourceId> <ResourceType>AWS::CloudWatch::Alarm</ResourceType> </member> @@ -486,7 +486,7 @@ class TestCloudFormationListStackResources(CloudFormationConnectionBase): resources = self.service_connection.list_stack_resources('MyStack', next_token='next_token') self.assertEqual(len(resources), 2) - self.assertEqual(resources[0].last_updated_timestamp, + self.assertEqual(resources[0].last_updated_time, datetime(2011, 6, 21, 20, 25, 57)) self.assertEqual(resources[0].logical_resource_id, 'SampleDB') self.assertEqual(resources[0].physical_resource_id, 'My-db-ycx') @@ -494,7 +494,7 @@ class TestCloudFormationListStackResources(CloudFormationConnectionBase): self.assertEqual(resources[0].resource_status_reason, None) self.assertEqual(resources[0].resource_type, 'AWS::RDS::DBInstance') - self.assertEqual(resources[1].last_updated_timestamp, + self.assertEqual(resources[1].last_updated_time, datetime(2011, 6, 21, 20, 29, 23)) self.assertEqual(resources[1].logical_resource_id, 'CPUAlarmHigh') self.assertEqual(resources[1].physical_resource_id, 'MyStack-CPUH-PF') diff --git a/tests/unit/cloudformation/test_stack.py b/tests/unit/cloudformation/test_stack.py index f42fee2a..c3bc9438 100644..100755 --- a/tests/unit/cloudformation/test_stack.py +++ b/tests/unit/cloudformation/test_stack.py @@ -116,14 +116,14 @@ LIST_STACK_RESOURCES_XML = r""" <member> <ResourceStatus>CREATE_COMPLETE</ResourceStatus> <LogicalResourceId>DBSecurityGroup</LogicalResourceId> - <LastUpdatedTimestamp>2011-06-21T20:15:58Z</LastUpdatedTimestamp> + <LastUpdatedTime>2011-06-21T20:15:58Z</LastUpdatedTime> <PhysicalResourceId>gmarcteststack-dbsecuritygroup-1s5m0ez5lkk6w</PhysicalResourceId> <ResourceType>AWS::RDS::DBSecurityGroup</ResourceType> </member> <member> <ResourceStatus>CREATE_COMPLETE</ResourceStatus> <LogicalResourceId>SampleDB</LogicalResourceId> - <LastUpdatedTimestamp>2011-06-21T20:25:57.875643Z</LastUpdatedTimestamp> + <LastUpdatedTime>2011-06-21T20:25:57.875643Z</LastUpdatedTime> <PhysicalResourceId>MyStack-sampledb-ycwhk1v830lx</PhysicalResourceId> <ResourceType>AWS::RDS::DBInstance</ResourceType> </member> @@ -207,12 +207,12 @@ class TestStackParse(unittest.TestCase): ]) h = boto.handler.XmlHandler(rs, None) xml.sax.parseString(LIST_STACK_RESOURCES_XML, h) - timestamp_1 = rs[0].last_updated_timestamp + timestamp_1 = rs[0].last_updated_time self.assertEqual( timestamp_1, datetime.datetime(2011, 6, 21, 20, 15, 58) ) - timestamp_2 = rs[1].last_updated_timestamp + timestamp_2 = rs[1].last_updated_time self.assertEqual( timestamp_2, datetime.datetime(2011, 6, 21, 20, 25, 57, 875643) diff --git a/tests/unit/ec2/test_connection.py b/tests/unit/ec2/test_connection.py index c73138f5..43b44db3 100644 --- a/tests/unit/ec2/test_connection.py +++ b/tests/unit/ec2/test_connection.py @@ -1,7 +1,8 @@ #!/usr/bin/env python import httplib -from mock import Mock +from datetime import datetime, timedelta +from mock import MagicMock, Mock, patch from tests.unit import unittest from tests.unit import AWSMockServiceTestCase @@ -9,6 +10,7 @@ import boto.ec2 from boto.regioninfo import RegionInfo from boto.ec2.connection import EC2Connection +from boto.ec2.snapshot import Snapshot class TestEC2ConnectionBase(AWSMockServiceTestCase): @@ -785,5 +787,96 @@ class TestConnectToRegion(unittest.TestCase): self.assertEqual(None, self.ec2) +class TestTrimSnapshots(TestEC2ConnectionBase): + """ + Test snapshot trimming functionality by ensuring that expected calls + are made when given a known set of volume snapshots. + """ + def _get_snapshots(self): + """ + Generate a list of fake snapshots with names and dates. + """ + snaps = [] + + # Generate some dates offset by days, weeks, months + now = datetime.now() + dates = [ + now, + now - timedelta(days=1), + now - timedelta(days=2), + now - timedelta(days=7), + now - timedelta(days=14), + datetime(now.year, now.month, 1) - timedelta(days=30), + datetime(now.year, now.month, 1) - timedelta(days=60), + datetime(now.year, now.month, 1) - timedelta(days=90) + ] + + for date in dates: + # Create a fake snapshot for each date + snap = Snapshot(self.ec2) + snap.tags['Name'] = 'foo' + # Times are expected to be ISO8601 strings + snap.start_time = date.strftime('%Y-%m-%dT%H:%M:%S.000Z') + snaps.append(snap) + + return snaps + + def test_trim_defaults(self): + """ + Test trimming snapshots with the default arguments, which should + keep all monthly backups forever. The result of this test should + be that nothing is deleted. + """ + # Setup mocks + orig = { + 'get_all_snapshots': self.ec2.get_all_snapshots, + 'delete_snapshot': self.ec2.delete_snapshot + } + + snaps = self._get_snapshots() + + self.ec2.get_all_snapshots = MagicMock(return_value=snaps) + self.ec2.delete_snapshot = MagicMock() + + # Call the tested method + self.ec2.trim_snapshots() + + # Assertions + self.assertEqual(True, self.ec2.get_all_snapshots.called) + self.assertEqual(False, self.ec2.delete_snapshot.called) + + # Restore + self.ec2.get_all_snapshots = orig['get_all_snapshots'] + self.ec2.delete_snapshot = orig['delete_snapshot'] + + def test_trim_months(self): + """ + Test trimming monthly snapshots and ensure that older months + get deleted properly. The result of this test should be that + the two oldest snapshots get deleted. + """ + # Setup mocks + orig = { + 'get_all_snapshots': self.ec2.get_all_snapshots, + 'delete_snapshot': self.ec2.delete_snapshot + } + + snaps = self._get_snapshots() + + self.ec2.get_all_snapshots = MagicMock(return_value=snaps) + self.ec2.delete_snapshot = MagicMock() + + # Call the tested method + self.ec2.trim_snapshots(monthly_backups=1) + + # Assertions + self.assertEqual(True, self.ec2.get_all_snapshots.called) + self.assertEqual(2, self.ec2.delete_snapshot.call_count) + + # Restore + self.ec2.get_all_snapshots = orig['get_all_snapshots'] + self.ec2.delete_snapshot = orig['delete_snapshot'] + + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/ec2/test_instance.py b/tests/unit/ec2/test_instance.py index c48ef114..6ee0f2f2 100644 --- a/tests/unit/ec2/test_instance.py +++ b/tests/unit/ec2/test_instance.py @@ -216,7 +216,7 @@ class TestDescribeInstances(AWSMockServiceTestCase): def test_multiple_private_ip_addresses(self): self.set_http_response(status_code=200) - api_response = self.service_connection.get_all_instances() + api_response = self.service_connection.get_all_reservations() self.assertEqual(len(api_response), 1) instances = api_response[0].instances diff --git a/tests/unit/ec2/test_networkinterface.py b/tests/unit/ec2/test_networkinterface.py index b23f6c36..4674823d 100644 --- a/tests/unit/ec2/test_networkinterface.py +++ b/tests/unit/ec2/test_networkinterface.py @@ -54,7 +54,8 @@ class TestNetworkInterfaceCollection(unittest.TestCase): groups=['group_id1', 'group_id2'], private_ip_address='10.0.1.54', delete_on_termination=False, private_ip_addresses=[self.private_ip_address3, - self.private_ip_address4]) + self.private_ip_address4], + associate_public_ip_address=True) def test_param_serialization(self): collection = NetworkInterfaceCollection(self.network_interfaces_spec1, @@ -78,6 +79,7 @@ class TestNetworkInterfaceCollection(unittest.TestCase): 'NetworkInterface.2.DeleteOnTermination': 'false', 'NetworkInterface.2.PrivateIpAddress': '10.0.1.54', 'NetworkInterface.2.SubnetId': 'subnet_id2', + 'NetworkInterface.2.AssociatePublicIpAddress': 'true', 'NetworkInterface.2.SecurityGroupId.1': 'group_id1', 'NetworkInterface.2.SecurityGroupId.2': 'group_id2', 'NetworkInterface.2.PrivateIpAddresses.1.Primary': 'false', @@ -89,7 +91,6 @@ class TestNetworkInterfaceCollection(unittest.TestCase): }) def test_add_prefix_to_serialization(self): - return collection = NetworkInterfaceCollection(self.network_interfaces_spec1, self.network_interfaces_spec2) params = {} @@ -121,6 +122,7 @@ class TestNetworkInterfaceCollection(unittest.TestCase): 'LaunchSpecification.NetworkInterface.2.PrivateIpAddress': '10.0.1.54', 'LaunchSpecification.NetworkInterface.2.SubnetId': 'subnet_id2', + 'LaunchSpecification.NetworkInterface.2.AssociatePublicIpAddress': 'true', 'LaunchSpecification.NetworkInterface.2.SecurityGroupId.1': 'group_id1', 'LaunchSpecification.NetworkInterface.2.SecurityGroupId.2': diff --git a/tests/unit/ec2/test_securitygroup.py b/tests/unit/ec2/test_securitygroup.py new file mode 100644 index 00000000..2876ffff --- /dev/null +++ b/tests/unit/ec2/test_securitygroup.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python + +from tests.unit import unittest +from tests.unit import AWSMockServiceTestCase + +import mock + +from boto.ec2.connection import EC2Connection + +DESCRIBE_SECURITY_GROUP = r"""<?xml version="1.0" encoding="UTF-8"?> +<DescribeSecurityGroupsResponse xmlns="http://ec2.amazonaws.com/doc/2013-06-15/"> + <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> + <securityGroupInfo> + <item> + <ownerId>111122223333</ownerId> + <groupId>sg-1a2b3c4d</groupId> + <groupName>WebServers</groupName> + <groupDescription>Web Servers</groupDescription> + <vpcId/> + <ipPermissions> + <item> + <ipProtocol>tcp</ipProtocol> + <fromPort>80</fromPort> + <toPort>80</toPort> + <groups/> + <ipRanges> + <item> + <cidrIp>0.0.0.0/0</cidrIp> + </item> + </ipRanges> + </item> + </ipPermissions> + <ipPermissionsEgress/> + </item> + <item> + <ownerId>111122223333</ownerId> + <groupId>sg-2a2b3c4d</groupId> + <groupName>RangedPortsBySource</groupName> + <groupDescription>Group A</groupDescription> + <ipPermissions> + <item> + <ipProtocol>tcp</ipProtocol> + <fromPort>6000</fromPort> + <toPort>7000</toPort> + <groups> + <item> + <userId>111122223333</userId> + <groupId>sg-3a2b3c4d</groupId> + <groupName>Group B</groupName> + </item> + </groups> + <ipRanges/> + </item> + </ipPermissions> + <ipPermissionsEgress/> + </item> + </securityGroupInfo> +</DescribeSecurityGroupsResponse>""" + +DESCRIBE_INSTANCES = r"""<?xml version="1.0" encoding="UTF-8"?> +<DescribeInstancesResponse xmlns="http://ec2.amazonaws.com/doc/2012-10-01/"> + <requestId>c6132c74-b524-4884-87f5-0f4bde4a9760</requestId> + <reservationSet> + <item> + <reservationId>r-72ef4a0a</reservationId> + <ownerId>184906166255</ownerId> + <groupSet/> + <instancesSet> + <item> + <instanceId>i-instance</instanceId> + <imageId>ami-1624987f</imageId> + <instanceState> + <code>16</code> + <name>running</name> + </instanceState> + <privateDnsName/> + <dnsName/> + <reason/> + <keyName>mykeypair</keyName> + <amiLaunchIndex>0</amiLaunchIndex> + <productCodes/> + <instanceType>m1.small</instanceType> + <launchTime>2012-12-14T23:48:37.000Z</launchTime> + <placement> + <availabilityZone>us-east-1d</availabilityZone> + <groupName/> + <tenancy>default</tenancy> + </placement> + <kernelId>aki-88aa75e1</kernelId> + <monitoring> + <state>disabled</state> + </monitoring> + <subnetId>subnet-0dc60667</subnetId> + <vpcId>vpc-id</vpcId> + <privateIpAddress>10.0.0.67</privateIpAddress> + <sourceDestCheck>true</sourceDestCheck> + <groupSet> + <item> + <groupId>sg-1a2b3c4d</groupId> + <groupName>WebServerSG</groupName> + </item> + </groupSet> + <architecture>x86_64</architecture> + <rootDeviceType>ebs</rootDeviceType> + <rootDeviceName>/dev/sda1</rootDeviceName> + <blockDeviceMapping> + <item> + <deviceName>/dev/sda1</deviceName> + <ebs> + <volumeId>vol-id</volumeId> + <status>attached</status> + <attachTime>2012-12-14T23:48:43.000Z</attachTime> + <deleteOnTermination>true</deleteOnTermination> + </ebs> + </item> + </blockDeviceMapping> + <virtualizationType>paravirtual</virtualizationType> + <clientToken>foo</clientToken> + <tagSet> + <item> + <key>Name</key> + <value/> + </item> + </tagSet> + <hypervisor>xen</hypervisor> + <networkInterfaceSet> + <item> + <networkInterfaceId>eni-id</networkInterfaceId> + <subnetId>subnet-id</subnetId> + <vpcId>vpc-id</vpcId> + <description>Primary network interface</description> + <ownerId>ownerid</ownerId> + <status>in-use</status> + <privateIpAddress>10.0.0.67</privateIpAddress> + <sourceDestCheck>true</sourceDestCheck> + <groupSet> + <item> + <groupId>sg-id</groupId> + <groupName>WebServerSG</groupName> + </item> + </groupSet> + <attachment> + <attachmentId>eni-attach-id</attachmentId> + <deviceIndex>0</deviceIndex> + <status>attached</status> + <attachTime>2012-12-14T23:48:37.000Z</attachTime> + <deleteOnTermination>true</deleteOnTermination> + </attachment> + <privateIpAddressesSet> + <item> + <privateIpAddress>10.0.0.67</privateIpAddress> + <primary>true</primary> + </item> + <item> + <privateIpAddress>10.0.0.54</privateIpAddress> + <primary>false</primary> + </item> + <item> + <privateIpAddress>10.0.0.55</privateIpAddress> + <primary>false</primary> + </item> + </privateIpAddressesSet> + </item> + </networkInterfaceSet> + <ebsOptimized>false</ebsOptimized> + </item> + </instancesSet> + </item> + </reservationSet> +</DescribeInstancesResponse> +""" + +class TestDescribeSecurityGroups(AWSMockServiceTestCase): + connection_class = EC2Connection + + def test_get_instances(self): + self.set_http_response(status_code=200, body=DESCRIBE_SECURITY_GROUP) + groups = self.service_connection.get_all_security_groups() + + self.set_http_response(status_code=200, body=DESCRIBE_INSTANCES) + instances = groups[0].instances() + + self.assertEqual(1, len(instances)) + self.assertEqual(groups[0].id, instances[0].groups[0].id) diff --git a/tests/unit/rds/test_connection.py b/tests/unit/rds/test_connection.py index d4a24cc2..b8d012d1 100644 --- a/tests/unit/rds/test_connection.py +++ b/tests/unit/rds/test_connection.py @@ -24,7 +24,9 @@ from tests.unit import unittest from tests.unit import AWSMockServiceTestCase +from boto.ec2.securitygroup import SecurityGroup from boto.rds import RDSConnection +from boto.rds.vpcsecuritygroupmembership import VPCSecurityGroupMembership from boto.rds.parametergroup import ParameterGroup @@ -73,6 +75,12 @@ class TestRDSConnection(AWSMockServiceTestCase): <DBSecurityGroupName>default</DBSecurityGroupName> </DBSecurityGroup> </DBSecurityGroups> + <VpcSecurityGroups> + <VpcSecurityGroupMembership> + <VpcSecurityGroupId>sg-1</VpcSecurityGroupId> + <Status>active</Status> + </VpcSecurityGroupMembership> + </VpcSecurityGroups> <DBName>mydb2</DBName> <AutoMinorVersionUpgrade>true</AutoMinorVersionUpgrade> <InstanceCreateTime>2012-10-03T22:01:51.047Z</InstanceCreateTime> @@ -137,6 +145,8 @@ class TestRDSConnection(AWSMockServiceTestCase): self.assertEqual(db.status_infos[0].normal, True) self.assertEqual(db.status_infos[0].status, 'replicating') self.assertEqual(db.status_infos[0].status_type, 'read replication') + self.assertEqual(db.vpc_security_groups[0].status, 'active') + self.assertEqual(db.vpc_security_groups[0].vpc_group, 'sg-1') class TestRDSCCreateDBInstance(AWSMockServiceTestCase): @@ -292,12 +302,169 @@ class TestRDSCCreateDBInstance(AWSMockServiceTestCase): self.assertEqual(db.multi_az, False) self.assertEqual(db.pending_modified_values, {'MasterUserPassword': '****'}) + self.assertEqual(db.parameter_group.name, + 'default.mysql5.1') + self.assertEqual(db.parameter_group.description, None) + self.assertEqual(db.parameter_group.engine, None) + + +class TestRDSConnectionRestoreDBInstanceFromPointInTime(AWSMockServiceTestCase): + connection_class = RDSConnection + + def setUp(self): + super(TestRDSConnectionRestoreDBInstanceFromPointInTime, self).setUp() + + def default_body(self): + return """ + <RestoreDBInstanceToPointInTimeResponse xmlns="http://rds.amazonaws.com/doc/2013-05-15/"> + <RestoreDBInstanceToPointInTimeResult> + <DBInstance> + <ReadReplicaDBInstanceIdentifiers/> + <Engine>mysql</Engine> + <PendingModifiedValues/> + <BackupRetentionPeriod>1</BackupRetentionPeriod> + <MultiAZ>false</MultiAZ> + <LicenseModel>general-public-license</LicenseModel> + <DBInstanceStatus>creating</DBInstanceStatus> + <EngineVersion>5.1.50</EngineVersion> + <DBInstanceIdentifier>restored-db</DBInstanceIdentifier> + <DBParameterGroups> + <DBParameterGroup> + <ParameterApplyStatus>in-sync</ParameterApplyStatus> + <DBParameterGroupName>default.mysql5.1</DBParameterGroupName> + </DBParameterGroup> + </DBParameterGroups> + <DBSecurityGroups> + <DBSecurityGroup> + <Status>active</Status> + <DBSecurityGroupName>default</DBSecurityGroupName> + </DBSecurityGroup> + </DBSecurityGroups> + <PreferredBackupWindow>00:00-00:30</PreferredBackupWindow> + <AutoMinorVersionUpgrade>true</AutoMinorVersionUpgrade> + <PreferredMaintenanceWindow>sat:07:30-sat:08:00</PreferredMaintenanceWindow> + <AllocatedStorage>10</AllocatedStorage> + <DBInstanceClass>db.m1.large</DBInstanceClass> + <MasterUsername>master</MasterUsername> + </DBInstance> + </RestoreDBInstanceToPointInTimeResult> + <ResponseMetadata> + <RequestId>1ef546bc-850b-11e0-90aa-eb648410240d</RequestId> + </ResponseMetadata> + </RestoreDBInstanceToPointInTimeResponse> + """ + + def test_restore_dbinstance_from_point_in_time(self): + self.set_http_response(status_code=200) + db = self.service_connection.restore_dbinstance_from_point_in_time( + 'simcoprod01', + 'restored-db', + True) + + self.assert_request_parameters({ + 'Action': 'RestoreDBInstanceToPointInTime', + 'SourceDBInstanceIdentifier': 'simcoprod01', + 'TargetDBInstanceIdentifier': 'restored-db', + 'UseLatestRestorableTime': 'true', + }, ignore_params_values=['Version']) + + self.assertEqual(db.id, 'restored-db') + self.assertEqual(db.engine, 'mysql') + self.assertEqual(db.status, 'creating') + self.assertEqual(db.allocated_storage, 10) + self.assertEqual(db.instance_class, 'db.m1.large') + self.assertEqual(db.master_username, 'master') + self.assertEqual(db.multi_az, False) self.assertEqual(db.parameter_group.name, 'default.mysql5.1') self.assertEqual(db.parameter_group.description, None) self.assertEqual(db.parameter_group.engine, None) + def test_restore_dbinstance_from_point_in_time__db_subnet_group_name(self): + self.set_http_response(status_code=200) + db = self.service_connection.restore_dbinstance_from_point_in_time( + 'simcoprod01', + 'restored-db', + True, + db_subnet_group_name='dbsubnetgroup') + + self.assert_request_parameters({ + 'Action': 'RestoreDBInstanceToPointInTime', + 'SourceDBInstanceIdentifier': 'simcoprod01', + 'TargetDBInstanceIdentifier': 'restored-db', + 'UseLatestRestorableTime': 'true', + 'DBSubnetGroupName': 'dbsubnetgroup', + }, ignore_params_values=['Version']) + + def test_create_db_instance_vpc_sg_str(self): + self.set_http_response(status_code=200) + vpc_security_groups = [ + VPCSecurityGroupMembership(self.service_connection, 'active', 'sg-1'), + VPCSecurityGroupMembership(self.service_connection, None, 'sg-2')] + + db = self.service_connection.create_dbinstance( + 'SimCoProd01', + 10, + 'db.m1.large', + 'master', + 'Password01', + param_group='default.mysql5.1', + db_subnet_group_name='dbSubnetgroup01', + vpc_security_groups=vpc_security_groups) + + self.assert_request_parameters({ + 'Action': 'CreateDBInstance', + 'AllocatedStorage': 10, + 'AutoMinorVersionUpgrade': 'true', + 'DBInstanceClass': 'db.m1.large', + 'DBInstanceIdentifier': 'SimCoProd01', + 'DBParameterGroupName': 'default.mysql5.1', + 'DBSubnetGroupName': 'dbSubnetgroup01', + 'Engine': 'MySQL5.1', + 'MasterUsername': 'master', + 'MasterUserPassword': 'Password01', + 'Port': 3306, + 'VpcSecurityGroupIds.member.1': 'sg-1', + 'VpcSecurityGroupIds.member.2': 'sg-2' + }, ignore_params_values=['Version']) + + def test_create_db_instance_vpc_sg_obj(self): + self.set_http_response(status_code=200) + + sg1 = SecurityGroup(name='sg-1') + sg2 = SecurityGroup(name='sg-2') + + vpc_security_groups = [ + VPCSecurityGroupMembership(self.service_connection, 'active', sg1.name), + VPCSecurityGroupMembership(self.service_connection, None, sg2.name)] + + db = self.service_connection.create_dbinstance( + 'SimCoProd01', + 10, + 'db.m1.large', + 'master', + 'Password01', + param_group='default.mysql5.1', + db_subnet_group_name='dbSubnetgroup01', + vpc_security_groups=vpc_security_groups) + + self.assert_request_parameters({ + 'Action': 'CreateDBInstance', + 'AllocatedStorage': 10, + 'AutoMinorVersionUpgrade': 'true', + 'DBInstanceClass': 'db.m1.large', + 'DBInstanceIdentifier': 'SimCoProd01', + 'DBParameterGroupName': 'default.mysql5.1', + 'DBSubnetGroupName': 'dbSubnetgroup01', + 'Engine': 'MySQL5.1', + 'MasterUsername': 'master', + 'MasterUserPassword': 'Password01', + 'Port': 3306, + 'VpcSecurityGroupIds.member.1': 'sg-1', + 'VpcSecurityGroupIds.member.2': 'sg-2' + }, ignore_params_values=['Version']) + class TestRDSOptionGroups(AWSMockServiceTestCase): connection_class = RDSConnection diff --git a/tests/unit/sns/test_connection.py b/tests/unit/sns/test_connection.py index 7aec7dbe..3a474c3c 100644 --- a/tests/unit/sns/test_connection.py +++ b/tests/unit/sns/test_connection.py @@ -127,12 +127,104 @@ class TestSNSConnection(AWSMockServiceTestCase): 'Message': 'message', }, ignore_params_values=['Version', 'ContentType']) + def test_create_platform_application(self): + self.set_http_response(status_code=200) + + self.service_connection.create_platform_application( + name='MyApp', + platform='APNS', + attributes={ + 'PlatformPrincipal': 'a ssl certificate', + 'PlatformCredential': 'a private key' + } + ) + self.assert_request_parameters({ + 'Action': 'CreatePlatformApplication', + 'Name': 'MyApp', + 'Platform': 'APNS', + 'Attributes.entry.1.key': 'PlatformCredential', + 'Attributes.entry.1.value': 'a private key', + 'Attributes.entry.2.key': 'PlatformPrincipal', + 'Attributes.entry.2.value': 'a ssl certificate', + }, ignore_params_values=['Version', 'ContentType']) + + def test_set_platform_application_attributes(self): + self.set_http_response(status_code=200) + + self.service_connection.set_platform_application_attributes( + platform_application_arn='arn:myapp', + attributes={'PlatformPrincipal': 'a ssl certificate', + 'PlatformCredential': 'a private key'}) + self.assert_request_parameters({ + 'Action': 'SetPlatformApplicationAttributes', + 'PlatformApplicationArn': 'arn:myapp', + 'Attributes.entry.1.key': 'PlatformCredential', + 'Attributes.entry.1.value': 'a private key', + 'Attributes.entry.2.key': 'PlatformPrincipal', + 'Attributes.entry.2.value': 'a ssl certificate', + }, ignore_params_values=['Version', 'ContentType']) + + def test_create_platform_endpoint(self): + self.set_http_response(status_code=200) + + self.service_connection.create_platform_endpoint( + platform_application_arn='arn:myapp', + token='abcde12345', + custom_user_data='john', + attributes={'Enabled': False}) + self.assert_request_parameters({ + 'Action': 'CreatePlatformEndpoint', + 'PlatformApplicationArn': 'arn:myapp', + 'Token': 'abcde12345', + 'CustomUserData': 'john', + 'Attributes.entry.1.key': 'Enabled', + 'Attributes.entry.1.value': False, + }, ignore_params_values=['Version', 'ContentType']) + + def test_set_endpoint_attributes(self): + self.set_http_response(status_code=200) + + self.service_connection.set_endpoint_attributes( + endpoint_arn='arn:myendpoint', + attributes={'CustomUserData': 'john', + 'Enabled': False}) + self.assert_request_parameters({ + 'Action': 'SetEndpointAttributes', + 'EndpointArn': 'arn:myendpoint', + 'Attributes.entry.1.key': 'CustomUserData', + 'Attributes.entry.1.value': 'john', + 'Attributes.entry.2.key': 'Enabled', + 'Attributes.entry.2.value': False, + }, ignore_params_values=['Version', 'ContentType']) + def test_message_is_required(self): self.set_http_response(status_code=200) with self.assertRaises(TypeError): self.service_connection.publish(topic='topic', subject='subject') + def test_publish_with_json(self): + self.set_http_response(status_code=200) + + self.service_connection.publish( + message=json.dumps({ + 'default': 'Ignored.', + 'GCM': { + 'data': 'goes here', + } + }), + message_structure='json', + subject='subject', + target_arn='target_arn' + ) + self.assert_request_parameters({ + 'Action': 'Publish', + 'TargetArn': 'target_arn', + 'Subject': 'subject', + 'Message': '{"default": "Ignored.", "GCM": {"data": "goes here"}}', + 'MessageStructure': 'json', + }, ignore_params_values=['Version', 'ContentType']) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/sts/__init__.py b/tests/unit/sts/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tests/unit/sts/__init__.py diff --git a/tests/unit/sts/test_connection.py b/tests/unit/sts/test_connection.py index f874cafd..de0ab261 100644 --- a/tests/unit/sts/test_connection.py +++ b/tests/unit/sts/test_connection.py @@ -70,5 +70,93 @@ class TestSTSConnection(AWSMockServiceTestCase): self.assertEqual(response.user.assume_role_id, 'roleid:myrolesession') +class TestSTSWebIdentityConnection(AWSMockServiceTestCase): + connection_class = STSConnection + + def setUp(self): + super(TestSTSWebIdentityConnection, self).setUp() + + def default_body(self): + return """ +<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/"> + <AssumeRoleWithWebIdentityResult> + <SubjectFromWebIdentityToken> + amzn1.account.AF6RHO7KZU5XRVQJGXK6HB56KR2A + </SubjectFromWebIdentityToken> + <AssumedRoleUser> + <Arn> + arn:aws:sts::000240903217:assumed-role/FederatedWebIdentityRole/app1 + </Arn> + <AssumedRoleId> + AROACLKWSDQRAOFQC3IDI:app1 + </AssumedRoleId> + </AssumedRoleUser> + <Credentials> + <SessionToken> + AQoDYXdzEE0a8ANXXXXXXXXNO1ewxE5TijQyp+IPfnyowF + </SessionToken> + <SecretAccessKey> + secretkey + </SecretAccessKey> + <Expiration> + 2013-05-14T23:00:23Z + </Expiration> + <AccessKeyId> + accesskey + </AccessKeyId> + </Credentials> + </AssumeRoleWithWebIdentityResult> + <ResponseMetadata> + <RequestId>ad4156e9-bce1-11e2-82e6-6b6ef249e618</RequestId> + </ResponseMetadata> +</AssumeRoleWithWebIdentityResponse> + """ + + def test_assume_role_with_web_identity(self): + arn = 'arn:aws:iam::000240903217:role/FederatedWebIdentityRole' + wit = 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9' + + self.set_http_response(status_code=200) + response = self.service_connection.assume_role_with_web_identity( + role_arn=arn, + role_session_name='guestuser', + web_identity_token=wit, + provider_id='www.amazon.com', + ) + self.assert_request_parameters({ + 'RoleSessionName': 'guestuser', + 'AWSAccessKeyId': 'aws_access_key_id', + 'RoleArn': arn, + 'WebIdentityToken': wit, + 'ProviderId': 'www.amazon.com', + 'Action': 'AssumeRoleWithWebIdentity' + }, ignore_params_values=[ + 'SignatureMethod', + 'Timestamp', + 'SignatureVersion', + 'Version', + ]) + self.assertEqual( + response.credentials.access_key.strip(), + 'accesskey' + ) + self.assertEqual( + response.credentials.secret_key.strip(), + 'secretkey' + ) + self.assertEqual( + response.credentials.session_token.strip(), + 'AQoDYXdzEE0a8ANXXXXXXXXNO1ewxE5TijQyp+IPfnyowF' + ) + self.assertEqual( + response.user.arn.strip(), + 'arn:aws:sts::000240903217:assumed-role/FederatedWebIdentityRole/app1' + ) + self.assertEqual( + response.user.assume_role_id.strip(), + 'AROACLKWSDQRAOFQC3IDI:app1' + ) + + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/sts/test_credentials.py b/tests/unit/sts/test_credentials.py new file mode 100644 index 00000000..27a16ca7 --- /dev/null +++ b/tests/unit/sts/test_credentials.py @@ -0,0 +1,38 @@ +import unittest + +from boto.sts.credentials import Credentials + + +class STSCredentialsTest(unittest.TestCase): + sts = True + + def setUp(self): + super(STSCredentialsTest, self).setUp() + self.creds = Credentials() + + def test_to_dict(self): + # This would fail miserably if ``Credentials.request_id`` hadn't been + # explicitly set (no default). + # Default. + self.assertEqual(self.creds.to_dict(), { + 'access_key': None, + 'expiration': None, + 'request_id': None, + 'secret_key': None, + 'session_token': None + }) + + # Override. + creds = Credentials() + creds.access_key = 'something' + creds.secret_key = 'crypto' + creds.session_token = 'this' + creds.expiration = 'way' + creds.request_id = 'comes' + self.assertEqual(creds.to_dict(), { + 'access_key': 'something', + 'expiration': 'way', + 'request_id': 'comes', + 'secret_key': 'crypto', + 'session_token': 'this' + }) |