summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Lindsley <daniel@toastdriven.com>2013-08-29 12:37:35 -0700
committerDaniel Lindsley <daniel@toastdriven.com>2013-08-29 12:37:35 -0700
commitefa5d58c82dfb837f7fb26946f076bda29eee416 (patch)
treec047f085b5a70de1c5a951e5bdf3dfc981224eee
parent32761fac621dc30fe345a5b03c37a5e4c9829722 (diff)
parent6a7e0c71e9b94336e803a8a3f74bf957d17b5818 (diff)
downloadboto-efa5d58c82dfb837f7fb26946f076bda29eee416.tar.gz
Merge branch 'release-2.11.0'2.11.0
* release-2.11.0: (45 commits) Bumping version to 2.11.0 Added release notes for v2.11.0. Fixed SNS' ``publish`` to behave better when sending mobile push notifications. Example update and PEP8 fixes Add unit tests to dialt0ne's VPC implementation rds support for vpc security groups (rebased on boto 0.10.0 master) Revert "Merge pull request #1683 from danielgtaylor/rds-vpc-sg" sort dictionary parameters by keys. add unit tests for SNS fix serialization of parameter of type map Switched SWF over to SigV4. Start moving get_all_instances to get_all_reservations Make s3put non-multipart uploads not require ListBucket access, and document that multi-part uploads require ListBucket and ListMultipartUploadParts permissions. Fixes #1642. Add unit tests for trim_snapshots defeault behavior and test the new monthly backups feature add trim_snapshots option to limit number of monthly backups correct start of month in trim_snapshots docstring Use transcoding-invariant headers when available in gs.Key Fixed a small typo in the SQS tutorial. Add unit tests for vpc_security_groups when creating RDS database instances. Documentation fixes Rename vpc_security_groups_ids -> vpc_security_groups for consistency with security_groups option; Modify behavior to allow passing in SecurityGroup instances from EC2. ...
-rw-r--r--README.rst4
-rwxr-xr-xbin/elbadmin5
-rwxr-xr-xbin/instance_events2
-rwxr-xr-xbin/list_instances2
-rwxr-xr-xbin/s3put6
-rw-r--r--boto/__init__.py2
-rw-r--r--boto/auth.py41
-rwxr-xr-x[-rw-r--r--]boto/cloudformation/stack.py8
-rw-r--r--boto/dynamodb/__init__.py3
-rw-r--r--boto/dynamodb2/__init__.py3
-rw-r--r--boto/dynamodb2/table.py13
-rw-r--r--boto/ec2/__init__.py1
-rw-r--r--boto/ec2/autoscale/__init__.py1
-rw-r--r--boto/ec2/cloudwatch/__init__.py1
-rw-r--r--boto/ec2/connection.py70
-rw-r--r--boto/ec2/elb/__init__.py1
-rw-r--r--boto/ec2/instance.py2
-rw-r--r--boto/ec2/networkinterface.py7
-rw-r--r--boto/ec2/securitygroup.py24
-rw-r--r--boto/gs/key.py8
-rw-r--r--boto/iam/__init__.py3
-rw-r--r--boto/iam/connection.py5
-rw-r--r--boto/manage/server.py6
-rw-r--r--boto/mashups/server.py2
-rw-r--r--boto/pyami/installers/ubuntu/ebs.py2
-rw-r--r--boto/rds/__init__.py59
-rw-r--r--boto/rds/dbinstance.py17
-rw-r--r--boto/rds/vpcsecuritygroupmembership.py85
-rw-r--r--boto/route53/record.py3
-rw-r--r--boto/s3/__init__.py3
-rw-r--r--boto/sns/__init__.py3
-rw-r--r--boto/sns/connection.py51
-rw-r--r--boto/sqs/__init__.py2
-rw-r--r--boto/sts/__init__.py4
-rw-r--r--boto/sts/connection.py8
-rw-r--r--boto/sts/credentials.py3
-rw-r--r--boto/swf/__init__.py1
-rw-r--r--boto/swf/layer1.py44
-rw-r--r--boto/vpc/__init__.py4
-rw-r--r--docs/source/autoscale_tut.rst3
-rw-r--r--docs/source/dynamodb2_tut.rst8
-rw-r--r--docs/source/ec2_tut.rst2
-rw-r--r--docs/source/releasenotes/v2.11.0.rst62
-rw-r--r--docs/source/sqs_tut.rst2
-rw-r--r--tests/integration/ec2/test_cert_verification.py2
-rw-r--r--tests/integration/ec2/vpc/test_connection.py2
-rw-r--r--tests/integration/route53/test_resourcerecordsets.py48
-rw-r--r--tests/integration/sqs/test_connection.py41
-rw-r--r--tests/integration/sts/test_session_token.py10
-rw-r--r--tests/unit/auth/test_query.py76
-rw-r--r--tests/unit/auth/test_sigv4.py54
-rwxr-xr-x[-rw-r--r--]tests/unit/cloudformation/test_connection.py8
-rwxr-xr-x[-rw-r--r--]tests/unit/cloudformation/test_stack.py8
-rw-r--r--tests/unit/ec2/test_connection.py95
-rw-r--r--tests/unit/ec2/test_instance.py2
-rw-r--r--tests/unit/ec2/test_networkinterface.py6
-rw-r--r--tests/unit/ec2/test_securitygroup.py184
-rw-r--r--tests/unit/rds/test_connection.py167
-rw-r--r--tests/unit/sns/test_connection.py92
-rw-r--r--tests/unit/sts/__init__.py0
-rw-r--r--tests/unit/sts/test_connection.py88
-rw-r--r--tests/unit/sts/test_credentials.py38
62 files changed, 1395 insertions, 112 deletions
diff --git a/README.rst b/README.rst
index e4e0a870..854c0b58 100644
--- a/README.rst
+++ b/README.rst
@@ -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)
diff --git a/bin/s3put b/bin/s3put
index 627cf963..faf10ffd 100755
--- a/bin/s3put
+++ b/bin/s3put
@@ -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'
+ })