summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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'
+ })