summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Saryerwinnie <js@jamesls.com>2013-04-30 18:15:59 -0700
committerJames Saryerwinnie <js@jamesls.com>2013-04-30 18:15:59 -0700
commita9e834ae4b85d787092e5b0d86b2ce8566d9a7a0 (patch)
treefce4433d21a30be749026293bcc04fdd2299e555
parent89f4947000587e12042e5b35c4557871b21137b9 (diff)
parent38f8bec07ae1e554f666da3337ccf2db65842034 (diff)
downloadboto-a9e834ae4b85d787092e5b0d86b2ce8566d9a7a0.tar.gz
Merge branch 'release-2.9.1'2.9.1
* release-2.9.1: Bumping version to 2.9.1 Support docs & release notes. Initial Support API addition. Change num_retries default for resumable download handler to be 6, to be consistent w/ num_retries elsewhere in the code Added a ``connect_redshift`` function for easier access to a ``RedShiftConnection``. Fixed the error type checking. Updated DynamoDB v2 to incorporate retries & checksums. Allow port override in boto config Fix typo bug in autoscale tutorial. Trying to make the docs around the count param a bit more clear. Add clarifying comment about using OrdinaryCallingFormat in storage_uri. Fixed missing raise introduced by 57a41897493e3b11fbbe47360a9d2f1033aae87d (fixes resumable download test failures) Change calling_format override in storage_uri to be gs-specific. Add eu-west-1 endpoint for Redshift. Fixing bogus docs regarding return value of import_key_pair. Added back get_upload_id(). Bumped the version in README. Add dev prefix back to version in dev branch
-rw-r--r--README.rst4
-rw-r--r--boto/__init__.py44
-rw-r--r--boto/connection.py4
-rw-r--r--boto/dynamodb/layer2.py6
-rw-r--r--boto/dynamodb/table.py6
-rw-r--r--boto/dynamodb2/exceptions.py4
-rw-r--r--boto/dynamodb2/layer1.py65
-rw-r--r--boto/ec2/connection.py6
-rw-r--r--boto/gs/resumable_upload_handler.py16
-rw-r--r--boto/provider.py9
-rw-r--r--boto/redshift/__init__.py4
-rw-r--r--boto/s3/key.py1
-rw-r--r--boto/s3/resumable_download_handler.py4
-rwxr-xr-xboto/storage_uri.py27
-rw-r--r--boto/support/__init__.py47
-rw-r--r--boto/support/exceptions.py34
-rw-r--r--boto/support/layer1.py529
-rw-r--r--docs/source/autoscale_tut.rst2
-rw-r--r--docs/source/index.rst14
-rw-r--r--docs/source/ref/support.rst26
-rw-r--r--docs/source/releasenotes/v2.9.1.rst48
-rw-r--r--docs/source/support_tut.rst151
-rw-r--r--tests/integration/support/__init__.py0
-rw-r--r--tests/integration/support/test_cert_verification.py35
-rw-r--r--tests/integration/support/test_layer1.py76
25 files changed, 1136 insertions, 26 deletions
diff --git a/README.rst b/README.rst
index e1b9fa37..e0fae9ad 100644
--- a/README.rst
+++ b/README.rst
@@ -1,8 +1,8 @@
####
boto
####
-boto 2.8.0
-31-Jan-2013
+boto 2.9.1
+30-Apr-2013
.. image:: https://secure.travis-ci.org/boto/boto.png?branch=develop
:target: https://secure.travis-ci.org/boto/boto
diff --git a/boto/__init__.py b/boto/__init__.py
index 1c114296..4bb18e72 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.9.0'
+__version__ = '2.9.1'
Version = __version__ # for backware compatibility
UserAgent = 'Boto/%s (%s)' % (__version__, sys.platform)
@@ -674,6 +674,48 @@ def connect_opsworks(aws_access_key_id=None,
**kwargs)
+def connect_redshift(aws_access_key_id=None,
+ aws_secret_access_key=None,
+ **kwargs):
+ """
+ :type aws_access_key_id: string
+ :param aws_access_key_id: Your AWS Access Key ID
+
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: Your AWS Secret Access Key
+
+ :rtype: :class:`boto.redshift.layer1.RedshiftConnection`
+ :return: A connection to Amazon's Redshift service
+ """
+ from boto.redshift.layer1 import RedshiftConnection
+ return RedshiftConnection(
+ aws_access_key_id=aws_access_key_id,
+ aws_secret_access_key=aws_secret_access_key,
+ **kwargs
+ )
+
+
+def connect_support(aws_access_key_id=None,
+ aws_secret_access_key=None,
+ **kwargs):
+ """
+ :type aws_access_key_id: string
+ :param aws_access_key_id: Your AWS Access Key ID
+
+ :type aws_secret_access_key: string
+ :param aws_secret_access_key: Your AWS Secret Access Key
+
+ :rtype: :class:`boto.support.layer1.SupportConnection`
+ :return: A connection to Amazon's Support service
+ """
+ from boto.support.layer1 import SupportConnection
+ return SupportConnection(
+ aws_access_key_id=aws_access_key_id,
+ aws_secret_access_key=aws_secret_access_key,
+ **kwargs
+ )
+
+
def storage_uri(uri_str, default_scheme='file', debug=0, validate=True,
bucket_storage_uri_class=BucketStorageUri,
suppress_consec_slashes=True, is_latest=False):
diff --git a/boto/connection.py b/boto/connection.py
index 4d214c0f..97e9c980 100644
--- a/boto/connection.py
+++ b/boto/connection.py
@@ -539,9 +539,11 @@ class AWSAuthConnection(object):
aws_secret_access_key,
security_token)
- # allow config file to override default host
+ # Allow config file to override default host and port.
if self.provider.host:
self.host = self.provider.host
+ if self.provider.port:
+ self.port = self.provider.port
self._pool = ConnectionPool()
self._connection = (self.server_name(), self.is_secure)
diff --git a/boto/dynamodb/layer2.py b/boto/dynamodb/layer2.py
index ec8cc51d..16fcdbbb 100644
--- a/boto/dynamodb/layer2.py
+++ b/boto/dynamodb/layer2.py
@@ -681,6 +681,9 @@ class Layer2(object):
:param count: If True, Amazon DynamoDB returns a total
number of items for the Query operation, even if the
operation has no matching items for the assigned filter.
+ If count is True, the actual items are not returned and
+ the count is accessible as the ``count`` attribute of
+ the returned object.
:type exclusive_start_key: list or tuple
:param exclusive_start_key: Primary key of the item from
@@ -769,6 +772,9 @@ class Layer2(object):
:param count: If True, Amazon DynamoDB returns a total
number of items for the Scan operation, even if the
operation has no matching items for the assigned filter.
+ If count is True, the actual items are not returned and
+ the count is accessible as the ``count`` attribute of
+ the returned object.
:type exclusive_start_key: list or tuple
:param exclusive_start_key: Primary key of the item from
diff --git a/boto/dynamodb/table.py b/boto/dynamodb/table.py
index b10ce04f..129b0795 100644
--- a/boto/dynamodb/table.py
+++ b/boto/dynamodb/table.py
@@ -435,6 +435,9 @@ class Table(object):
:param count: If True, Amazon DynamoDB returns a total
number of items for the Query operation, even if the
operation has no matching items for the assigned filter.
+ If count is True, the actual items are not returned and
+ the count is accessible as the ``count`` attribute of
+ the returned object.
:type item_class: Class
@@ -494,6 +497,9 @@ class Table(object):
:param count: If True, Amazon DynamoDB returns a total
number of items for the Scan operation, even if the
operation has no matching items for the assigned filter.
+ If count is True, the actual items are not returned and
+ the count is accessible as the ``count`` attribute of
+ the returned object.
:type exclusive_start_key: list or tuple
:param exclusive_start_key: Primary key of the item from
diff --git a/boto/dynamodb2/exceptions.py b/boto/dynamodb2/exceptions.py
index 9821e451..6e42f5c6 100644
--- a/boto/dynamodb2/exceptions.py
+++ b/boto/dynamodb2/exceptions.py
@@ -46,5 +46,9 @@ class InternalServerError(JSONResponseError):
pass
+class ValidationException(JSONResponseError):
+ pass
+
+
class ItemCollectionSizeLimitExceededException(JSONResponseError):
pass
diff --git a/boto/dynamodb2/layer1.py b/boto/dynamodb2/layer1.py
index 01f58045..1a627f8a 100644
--- a/boto/dynamodb2/layer1.py
+++ b/boto/dynamodb2/layer1.py
@@ -19,6 +19,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
+from binascii import crc32
import json
import boto
@@ -30,9 +31,10 @@ from boto.dynamodb2 import exceptions
class DynamoDBConnection(AWSQueryConnection):
"""
- Amazon DynamoDB **Overview**
- This is the Amazon DynamoDB API Reference. This guide provides
- descriptions and samples of the Amazon DynamoDB API.
+ Amazon DynamoDB is a fast, highly scalable, highly available,
+ cost-effective non-relational database service. Amazon DynamoDB
+ removes traditional scalability limitations on data storage while
+ maintaining low latency and predictable performance.
"""
APIVersion = "2012-08-10"
DefaultRegionName = "us-east-1"
@@ -49,17 +51,23 @@ class DynamoDBConnection(AWSQueryConnection):
"ResourceNotFoundException": exceptions.ResourceNotFoundException,
"InternalServerError": exceptions.InternalServerError,
"ItemCollectionSizeLimitExceededException": exceptions.ItemCollectionSizeLimitExceededException,
+ "ValidationException": exceptions.ValidationException,
}
+ NumberRetries = 10
+
def __init__(self, **kwargs):
region = kwargs.pop('region', None)
+ validate_checksums = kwargs.pop('validate_checksums', True)
if not region:
region = RegionInfo(self, self.DefaultRegionName,
self.DefaultRegionEndpoint)
kwargs['host'] = region.endpoint
AWSQueryConnection.__init__(self, **kwargs)
self.region = region
+ self._validate_checksums = boto.config.getbool(
+ 'DynamoDB', 'validate_checksums', validate_checksums)
def _required_auth_capability(self):
return ['hmac-v4']
@@ -1392,7 +1400,8 @@ class DynamoDBConnection(AWSQueryConnection):
method='POST', path='/', auth_path='/', params={},
headers=headers, data=body)
response = self._mexe(http_request, sender=None,
- override_num_retries=10)
+ override_num_retries=self.NumberRetries,
+ retry_handler=self._retry_handler)
response_body = response.read()
boto.log.debug(response_body)
if response.status == 200:
@@ -1405,3 +1414,51 @@ class DynamoDBConnection(AWSQueryConnection):
raise exception_class(response.status, response.reason,
body=json_body)
+ def _retry_handler(self, response, i, next_sleep):
+ status = None
+ if response.status == 400:
+ response_body = response.read()
+ boto.log.debug(response_body)
+ data = json.loads(response_body)
+ if 'ProvisionedThroughputExceededException' in data.get('__type'):
+ self.throughput_exceeded_events += 1
+ msg = "%s, retry attempt %s" % (
+ 'ProvisionedThroughputExceededException',
+ i
+ )
+ next_sleep = self._exponential_time(i)
+ i += 1
+ status = (msg, i, next_sleep)
+ if i == self.NumberRetries:
+ # If this was our last retry attempt, raise
+ # a specific error saying that the throughput
+ # was exceeded.
+ raise exceptions.ProvisionedThroughputExceededException(
+ response.status, response.reason, data)
+ elif 'ConditionalCheckFailedException' in data.get('__type'):
+ raise exceptions.ConditionalCheckFailedException(
+ response.status, response.reason, data)
+ elif 'ValidationException' in data.get('__type'):
+ raise exceptions.ValidationException(
+ response.status, response.reason, data)
+ else:
+ raise self.ResponseError(response.status, response.reason,
+ data)
+ expected_crc32 = response.getheader('x-amz-crc32')
+ if self._validate_checksums and expected_crc32 is not None:
+ boto.log.debug('Validating crc32 checksum for body: %s',
+ response.read())
+ actual_crc32 = crc32(response.read()) & 0xffffffff
+ expected_crc32 = int(expected_crc32)
+ if actual_crc32 != expected_crc32:
+ msg = ("The calculated checksum %s did not match the expected "
+ "checksum %s" % (actual_crc32, expected_crc32))
+ status = (msg, i + 1, self._exponential_time(i))
+ return status
+
+ def _exponential_time(self, i):
+ if i == 0:
+ next_sleep = 0
+ else:
+ next_sleep = 0.05 * (2 ** i)
+ return next_sleep
diff --git a/boto/ec2/connection.py b/boto/ec2/connection.py
index fb0cc304..48cf5344 100644
--- a/boto/ec2/connection.py
+++ b/boto/ec2/connection.py
@@ -2208,9 +2208,9 @@ class EC2Connection(AWSQueryConnection):
it to AWS.
:rtype: :class:`boto.ec2.keypair.KeyPair`
- :return: The newly created :class:`boto.ec2.keypair.KeyPair`.
- The material attribute of the new KeyPair object
- will contain the the unencrypted PEM encoded RSA private key.
+ :return: A :class:`boto.ec2.keypair.KeyPair` object representing
+ the newly imported key pair. This object will contain only
+ the key name and the fingerprint.
"""
public_key_material = base64.b64encode(public_key_material)
params = {'KeyName': key_name,
diff --git a/boto/gs/resumable_upload_handler.py b/boto/gs/resumable_upload_handler.py
index b2ec8e8c..57ae7548 100644
--- a/boto/gs/resumable_upload_handler.py
+++ b/boto/gs/resumable_upload_handler.py
@@ -161,6 +161,22 @@ class ResumableUploadHandler(object):
"""
return self.tracker_uri
+ def get_upload_id(self):
+ """
+ Returns the upload ID for the resumable upload, or None if the upload
+ has not yet started.
+ """
+ # We extract the upload_id from the tracker uri. We could retrieve the
+ # upload_id from the headers in the response but this only works for
+ # the case where we get the tracker uri from the service. In the case
+ # where we get the tracker from the tracking file we need to do this
+ # logic anyway.
+ delim = '?upload_id='
+ if self.tracker_uri and delim in self.tracker_uri:
+ return self.tracker_uri[self.tracker_uri.index(delim) + len(delim):]
+ else:
+ return None
+
def _remove_tracker_file(self):
if (self.tracker_file_name and
os.path.exists(self.tracker_file_name)):
diff --git a/boto/provider.py b/boto/provider.py
index 8a990ed1..457a87e7 100644
--- a/boto/provider.py
+++ b/boto/provider.py
@@ -117,7 +117,8 @@ class Provider(object):
'metadata-directive',
RESUMABLE_UPLOAD_HEADER_KEY: None,
SECURITY_TOKEN_HEADER_KEY: AWS_HEADER_PREFIX + 'security-token',
- SERVER_SIDE_ENCRYPTION_KEY: AWS_HEADER_PREFIX + 'server-side-encryption',
+ SERVER_SIDE_ENCRYPTION_KEY: AWS_HEADER_PREFIX +
+ 'server-side-encryption',
VERSION_ID_HEADER_KEY: AWS_HEADER_PREFIX + 'version-id',
STORAGE_CLASS_HEADER_KEY: AWS_HEADER_PREFIX + 'storage-class',
MFA_HEADER_KEY: AWS_HEADER_PREFIX + 'mfa',
@@ -166,6 +167,7 @@ class Provider(object):
def __init__(self, name, access_key=None, secret_key=None,
security_token=None):
self.host = None
+ self.port = None
self.access_key = access_key
self.secret_key = secret_key
self.security_token = security_token
@@ -176,10 +178,13 @@ class Provider(object):
self.get_credentials(access_key, secret_key)
self.configure_headers()
self.configure_errors()
- # allow config file to override default host
+ # Allow config file to override default host and port.
host_opt_name = '%s_host' % self.HostKeyMap[self.name]
if config.has_option('Credentials', host_opt_name):
self.host = config.get('Credentials', host_opt_name)
+ port_opt_name = '%s_port' % self.HostKeyMap[self.name]
+ if config.has_option('Credentials', port_opt_name):
+ self.port = config.getint('Credentials', port_opt_name)
def get_access_key(self):
if self._credentials_need_refresh():
diff --git a/boto/redshift/__init__.py b/boto/redshift/__init__.py
index 15601e78..68b7275a 100644
--- a/boto/redshift/__init__.py
+++ b/boto/redshift/__init__.py
@@ -39,6 +39,9 @@ def regions():
RegionInfo(name='us-west-2',
endpoint='redshift.us-west-2.amazonaws.com',
connection_cls=cls),
+ RegionInfo(name='eu-west-1',
+ endpoint='redshift.eu-west-1.amazonaws.com',
+ connection_cls=cls),
]
@@ -47,4 +50,3 @@ def connect_to_region(region_name, **kw_params):
if region.name == region_name:
return region.connect(**kw_params)
return None
-
diff --git a/boto/s3/key.py b/boto/s3/key.py
index fa9bc61f..2fceb64d 100644
--- a/boto/s3/key.py
+++ b/boto/s3/key.py
@@ -1419,6 +1419,7 @@ class Key(object):
if e.errno == errno.ENOSPC:
raise StorageDataError('Out of space for destination file '
'%s' % fp.name)
+ raise
if cb and (cb_count <= 1 or i > 0) and data_len > 0:
cb(data_len, cb_size)
for alg in digesters:
diff --git a/boto/s3/resumable_download_handler.py b/boto/s3/resumable_download_handler.py
index 06d179f0..4114b5b1 100644
--- a/boto/s3/resumable_download_handler.py
+++ b/boto/s3/resumable_download_handler.py
@@ -263,9 +263,9 @@ class ResumableDownloadHandler(object):
headers = {}
# Use num-retries from constructor if one was provided; else check
- # for a value specified in the boto config file; else default to 5.
+ # for a value specified in the boto config file; else default to 6.
if self.num_retries is None:
- self.num_retries = config.getint('Boto', 'num_retries', 5)
+ self.num_retries = config.getint('Boto', 'num_retries', 6)
progress_less_iterations = 0
while True: # Retry as long as we're making progress.
diff --git a/boto/storage_uri.py b/boto/storage_uri.py
index dbccc13c..9a6b2bfa 100755
--- a/boto/storage_uri.py
+++ b/boto/storage_uri.py
@@ -101,15 +101,7 @@ class StorageUri(object):
@return: A connection to storage service provider of the given URI.
"""
connection_args = dict(self.connection_args or ())
- # Use OrdinaryCallingFormat instead of boto-default
- # SubdomainCallingFormat because the latter changes the hostname
- # that's checked during cert validation for HTTPS connections,
- # which will fail cert validation (when cert validation is enabled).
- # Note: the following import can't be moved up to the start of
- # this file else it causes a config import failure when run from
- # the resumable upload/download tests.
- from boto.s3.connection import OrdinaryCallingFormat
- connection_args['calling_format'] = OrdinaryCallingFormat()
+
if (hasattr(self, 'suppress_consec_slashes') and
'suppress_consec_slashes' not in connection_args):
connection_args['suppress_consec_slashes'] = (
@@ -126,6 +118,23 @@ class StorageUri(object):
self.provider_pool[self.scheme] = self.connection
elif self.scheme == 'gs':
from boto.gs.connection import GSConnection
+ # Use OrdinaryCallingFormat instead of boto-default
+ # SubdomainCallingFormat because the latter changes the hostname
+ # that's checked during cert validation for HTTPS connections,
+ # which will fail cert validation (when cert validation is
+ # enabled).
+ #
+ # The same is not true for S3's HTTPS certificates. In fact,
+ # we don't want to do this for S3 because S3 requires the
+ # subdomain to match the location of the bucket. If the proper
+ # subdomain is not used, the server will return a 301 redirect
+ # with no Location header.
+ #
+ # Note: the following import can't be moved up to the
+ # start of this file else it causes a config import failure when
+ # run from the resumable upload/download tests.
+ from boto.s3.connection import OrdinaryCallingFormat
+ connection_args['calling_format'] = OrdinaryCallingFormat()
self.connection = GSConnection(access_key_id,
secret_access_key,
**connection_args)
diff --git a/boto/support/__init__.py b/boto/support/__init__.py
new file mode 100644
index 00000000..6d59b375
--- /dev/null
+++ b/boto/support/__init__.py
@@ -0,0 +1,47 @@
+# 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.
+#
+
+from boto.regioninfo import RegionInfo
+
+
+def regions():
+ """
+ Get all available regions for the Amazon Support service.
+
+ :rtype: list
+ :return: A list of :class:`boto.regioninfo.RegionInfo`
+ """
+ from boto.support.layer1 import SupportConnection
+ return [
+ RegionInfo(
+ name='us-east-1',
+ endpoint='support.us-east-1.amazonaws.com',
+ connection_cls=SupportConnection
+ ),
+ ]
+
+
+def connect_to_region(region_name, **kw_params):
+ for region in regions():
+ if region.name == region_name:
+ return region.connect(**kw_params)
+ return None
diff --git a/boto/support/exceptions.py b/boto/support/exceptions.py
new file mode 100644
index 00000000..f4e33d01
--- /dev/null
+++ b/boto/support/exceptions.py
@@ -0,0 +1,34 @@
+# 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.
+#
+from boto.exception import JSONResponseError
+
+
+class CaseIdNotFound(JSONResponseError):
+ pass
+
+
+class CaseCreationLimitExceeded(JSONResponseError):
+ pass
+
+
+class InternalServerError(JSONResponseError):
+ pass
diff --git a/boto/support/layer1.py b/boto/support/layer1.py
new file mode 100644
index 00000000..0f9471ba
--- /dev/null
+++ b/boto/support/layer1.py
@@ -0,0 +1,529 @@
+# 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 json
+import boto
+from boto.connection import AWSQueryConnection
+from boto.regioninfo import RegionInfo
+from boto.exception import JSONResponseError
+from boto.support import exceptions
+
+
+class SupportConnection(AWSQueryConnection):
+ """
+ AWS Support
+ The AWS Support API reference is intended for programmers who need
+ detailed information about the AWS Support actions and data types.
+ This service enables you to manage with your AWS Support cases
+ programmatically. It is built on the AWS Query API programming
+ model and provides HTTP methods that take parameters and return
+ results in JSON format.
+
+ The AWS Support service also exposes a set of `Trusted Advisor`_
+ features. You can retrieve a list of checks you can run on your
+ resources, specify checks to run and refresh, and check the status
+ of checks you have submitted.
+
+ The following list describes the AWS Support case management
+ actions:
+
+
+ + **Service names, issue categories, and available severity
+ levels. **The actions `DescribeServices`_ and
+ `DescribeSeverityLevels`_ enable you to obtain AWS service names,
+ service codes, service categories, and problem severity levels.
+ You use these values when you call the `CreateCase`_ action.
+ + **Case Creation, case details, and case resolution**. The
+ actions `CreateCase`_, `DescribeCases`_, and `ResolveCase`_ enable
+ you to create AWS Support cases, retrieve them, and resolve them.
+ + **Case communication**. The actions
+ `DescribeCaseCommunications`_ and `AddCommunicationToCase`_ enable
+ you to retrieve and add communication to AWS Support cases.
+
+
+ The following list describes the actions available from the AWS
+ Support service for Trusted Advisor:
+
+
+ + `DescribeTrustedAdviserChecks`_ returns the list of checks that you can run against your AWS
+ resources.
+ + Using the CheckId for a specific check returned by
+ DescribeTrustedAdviserChecks, you can call
+ `DescribeTrustedAdvisorCheckResult`_ and obtain a new result for the check you specified.
+ + Using `DescribeTrustedAdvisorCheckSummaries`_, you can get
+ summaries for a set of Trusted Advisor checks.
+ + `RefreshTrustedAdvisorCheck`_ enables you to request that
+ Trusted Advisor run the check again.
+ + ``_ gets statuses on the checks you are running.
+
+
+ For authentication of requests, the AWS Support uses `Signature
+ Version 4 Signing Process`_.
+
+ See the AWS Support Developer Guide for information about how to
+ use this service to manage create and manage your support cases,
+ and how to call Trusted Advisor for results of checks on your
+ resources.
+ """
+ APIVersion = "2012-12-15"
+ DefaultRegionName = "us-east-1"
+ DefaultRegionEndpoint = "support.us-east-1.amazonaws.com"
+ ServiceName = "Support"
+ TargetPrefix = "AWSSupport_20130415"
+ ResponseError = JSONResponseError
+
+ _faults = {
+ "CaseIdNotFound": exceptions.CaseIdNotFound,
+ "CaseCreationLimitExceeded": exceptions.CaseCreationLimitExceeded,
+ "InternalServerError": exceptions.InternalServerError,
+ }
+
+
+ def __init__(self, **kwargs):
+ region = kwargs.pop('region', None)
+ if not region:
+ region = RegionInfo(self, self.DefaultRegionName,
+ self.DefaultRegionEndpoint)
+ kwargs['host'] = region.endpoint
+ AWSQueryConnection.__init__(self, **kwargs)
+ self.region = region
+
+ def _required_auth_capability(self):
+ return ['hmac-v4']
+
+ def add_communication_to_case(self, communication_body, case_id=None,
+ cc_email_addresses=None):
+ """
+ This action adds additional customer communication to an AWS
+ Support case. You use the CaseId value to identify the case to
+ which you want to add communication. You can list a set of
+ email addresses to copy on the communication using the
+ CcEmailAddresses value. The CommunicationBody value contains
+ the text of the communication.
+
+ This action's response indicates the success or failure of the
+ request.
+
+ This action implements a subset of the behavior on the AWS
+ Support `Your Support Cases`_ web form.
+
+ :type case_id: string
+ :param case_id:
+
+ :type communication_body: string
+ :param communication_body:
+
+ :type cc_email_addresses: list
+ :param cc_email_addresses:
+
+ """
+ params = {'communicationBody': communication_body, }
+ if case_id is not None:
+ params['caseId'] = case_id
+ if cc_email_addresses is not None:
+ params['ccEmailAddresses'] = cc_email_addresses
+ return self.make_request(action='AddCommunicationToCase',
+ body=json.dumps(params))
+
+ def create_case(self, subject, service_code, category_code,
+ communication_body, severity_code=None,
+ cc_email_addresses=None, language=None, issue_type=None):
+ """
+ Creates a new case in the AWS Support Center. This action is
+ modeled on the behavior of the AWS Support Center `Open a new
+ case`_ page. Its parameters require you to specify the
+ following information:
+
+
+ #. **ServiceCode.** Represents a code for an AWS service. You
+ obtain the ServiceCode by calling `DescribeServices`_.
+ #. **CategoryCode**. Represents a category for the service
+ defined for the ServiceCode value. You also obtain the
+ cateogory code for a service by calling `DescribeServices`_.
+ Each AWS service defines its own set of category codes.
+ #. **SeverityCode**. Represents a value that specifies the
+ urgency of the case, and the time interval in which your
+ service level agreement specifies a response from AWS Support.
+ You obtain the SeverityCode by calling
+ `DescribeSeverityLevels`_.
+ #. **Subject**. Represents the **Subject** field on the AWS
+ Support Center `Open a new case`_ page.
+ #. **CommunicationBody**. Represents the **Description** field
+ on the AWS Support Center `Open a new case`_ page.
+ #. **Language**. Specifies the human language in which AWS
+ Support handles the case. The API currently supports English
+ and Japanese.
+ #. **CcEmailAddresses**. Represents the AWS Support Center
+ **CC** field on the `Open a new case`_ page. You can list
+ email addresses to be copied on any correspondence about the
+ case. The account that opens the case is already identified by
+ passing the AWS Credentials in the HTTP POST method or in a
+ method or function call from one of the programming languages
+ supported by an `AWS SDK`_.
+
+
+ The AWS Support API does not currently support the ability to
+ add attachments to cases. You can, however, call
+ `AddCommunicationToCase`_ to add information to an open case.
+
+ A successful `CreateCase`_ request returns an AWS Support case
+ number. Case numbers are used by `DescribeCases`_ request to
+ retrieve existing AWS Support support cases.
+
+ :type subject: string
+ :param subject:
+
+ :type service_code: string
+ :param service_code:
+
+ :type severity_code: string
+ :param severity_code:
+
+ :type category_code: string
+ :param category_code:
+
+ :type communication_body: string
+ :param communication_body:
+
+ :type cc_email_addresses: list
+ :param cc_email_addresses:
+
+ :type language: string
+ :param language:
+
+ :type issue_type: string
+ :param issue_type:
+
+ """
+ params = {
+ 'subject': subject,
+ 'serviceCode': service_code,
+ 'categoryCode': category_code,
+ 'communicationBody': communication_body,
+ }
+ if severity_code is not None:
+ params['severityCode'] = severity_code
+ if cc_email_addresses is not None:
+ params['ccEmailAddresses'] = cc_email_addresses
+ if language is not None:
+ params['language'] = language
+ if issue_type is not None:
+ params['issueType'] = issue_type
+ return self.make_request(action='CreateCase',
+ body=json.dumps(params))
+
+ def describe_cases(self, case_id_list=None, display_id=None,
+ after_time=None, before_time=None,
+ include_resolved_cases=None, next_token=None,
+ max_results=None, language=None):
+ """
+ This action returns a list of cases that you specify by
+ passing one or more CaseIds. In addition, you can filter the
+ cases by date by setting values for the AfterTime and
+ BeforeTime request parameters.
+ The response returns the following in JSON format:
+
+ #. One or more `CaseDetails`_ data types.
+ #. One or more NextToken objects, strings that specifies where
+ to paginate the returned records represented by CaseDetails .
+
+ :type case_id_list: list
+ :param case_id_list:
+
+ :type display_id: string
+ :param display_id:
+
+ :type after_time: string
+ :param after_time:
+
+ :type before_time: string
+ :param before_time:
+
+ :type include_resolved_cases: boolean
+ :param include_resolved_cases:
+
+ :type next_token: string
+ :param next_token:
+
+ :type max_results: integer
+ :param max_results:
+
+ :type language: string
+ :param language:
+
+ """
+ params = {}
+ if case_id_list is not None:
+ params['caseIdList'] = case_id_list
+ if display_id is not None:
+ params['displayId'] = display_id
+ if after_time is not None:
+ params['afterTime'] = after_time
+ if before_time is not None:
+ params['beforeTime'] = before_time
+ if include_resolved_cases is not None:
+ params['includeResolvedCases'] = include_resolved_cases
+ if next_token is not None:
+ params['nextToken'] = next_token
+ if max_results is not None:
+ params['maxResults'] = max_results
+ if language is not None:
+ params['language'] = language
+ return self.make_request(action='DescribeCases',
+ body=json.dumps(params))
+
+ def describe_communications(self, case_id, before_time=None,
+ after_time=None, next_token=None,
+ max_results=None):
+ """
+ This action returns communications regarding the support case.
+ You can use the AfterTime and BeforeTime parameters to filter
+ by date. The CaseId parameter enables you to identify a
+ specific case by its CaseId number.
+
+ The MaxResults and NextToken parameters enable you to control
+ the pagination of the result set. Set MaxResults to the number
+ of cases you want displayed on each page, and use NextToken to
+ specify the resumption of pagination.
+
+ :type case_id: string
+ :param case_id:
+
+ :type before_time: string
+ :param before_time:
+
+ :type after_time: string
+ :param after_time:
+
+ :type next_token: string
+ :param next_token:
+
+ :type max_results: integer
+ :param max_results:
+
+ """
+ params = {'caseId': case_id, }
+ if before_time is not None:
+ params['beforeTime'] = before_time
+ if after_time is not None:
+ params['afterTime'] = after_time
+ if next_token is not None:
+ params['nextToken'] = next_token
+ if max_results is not None:
+ params['maxResults'] = max_results
+ return self.make_request(action='DescribeCommunications',
+ body=json.dumps(params))
+
+ def describe_services(self, service_code_list=None, language=None):
+ """
+ Returns the current list of AWS services and a list of service
+ categories that applies to each one. You then use service
+ names and categories in your `CreateCase`_ requests. Each AWS
+ service has its own set of categories.
+
+ The service codes and category codes correspond to the values
+ that are displayed in the **Service** and **Category** drop-
+ down lists on the AWS Support Center `Open a new case`_ page.
+ The values in those fields, however, do not necessarily match
+ the service codes and categories returned by the
+ `DescribeServices` request. Always use the service codes and
+ categories obtained programmatically. This practice ensures
+ that you always have the most recent set of service and
+ category codes.
+
+ :type service_code_list: list
+ :param service_code_list:
+
+ :type language: string
+ :param language:
+
+ """
+ params = {}
+ if service_code_list is not None:
+ params['serviceCodeList'] = service_code_list
+ if language is not None:
+ params['language'] = language
+ return self.make_request(action='DescribeServices',
+ body=json.dumps(params))
+
+ def describe_severity_levels(self, language=None):
+ """
+ This action returns the list of severity levels that you can
+ assign to an AWS Support case. The severity level for a case
+ is also a field in the `CaseDetails`_ data type included in
+ any `CreateCase`_ request.
+
+ :type language: string
+ :param language:
+
+ """
+ params = {}
+ if language is not None:
+ params['language'] = language
+ return self.make_request(action='DescribeSeverityLevels',
+ body=json.dumps(params))
+
+ def resolve_case(self, case_id=None):
+ """
+ Takes a CaseId and returns the initial state of the case along
+ with the state of the case after the call to `ResolveCase`_
+ completed.
+
+ :type case_id: string
+ :param case_id:
+
+ """
+ params = {}
+ if case_id is not None:
+ params['caseId'] = case_id
+ return self.make_request(action='ResolveCase',
+ body=json.dumps(params))
+
+ def describe_trusted_advisor_check_refresh_statuses(self, check_ids):
+ """
+ Returns the status of all refresh requests Trusted Advisor
+ checks called using `RefreshTrustedAdvisorCheck`_.
+
+ :type check_ids: list
+ :param check_ids:
+
+ """
+ params = {'checkIds': check_ids, }
+ return self.make_request(action='DescribeTrustedAdvisorCheckRefreshStatuses',
+ body=json.dumps(params))
+
+ def describe_trusted_advisor_check_result(self, check_id, language=None):
+ """
+ This action responds with the results of a Trusted Advisor
+ check. Once you have obtained the list of available Trusted
+ Advisor checks by calling `DescribeTrustedAdvisorChecks`_, you
+ specify the CheckId for the check you want to retrieve from
+ AWS Support.
+
+ The response for this action contains a JSON-formatted
+ `TrustedAdvisorCheckResult`_ object
+ , which is a container for the following three objects:
+
+
+
+ #. `TrustedAdvisorCategorySpecificSummary`_
+ #. `TrustedAdvisorResourceDetail`_
+ #. `TrustedAdvisorResourcesSummary`_
+
+
+ In addition, the response contains the following fields:
+
+
+ #. **Status**. Overall status of the check.
+ #. **Timestamp**. Time at which Trusted Advisor last ran the
+ check.
+ #. **CheckId**. Unique identifier for the specific check
+ returned by the request.
+
+ :type check_id: string
+ :param check_id:
+
+ :type language: string
+ :param language:
+
+ """
+ params = {'checkId': check_id, }
+ if language is not None:
+ params['language'] = language
+ return self.make_request(action='DescribeTrustedAdvisorCheckResult',
+ body=json.dumps(params))
+
+ def describe_trusted_advisor_check_summaries(self, check_ids):
+ """
+ This action enables you to get the latest summaries for
+ Trusted Advisor checks that you specify in your request. You
+ submit the list of Trusted Advisor checks for which you want
+ summaries. You obtain these CheckIds by submitting a
+ `DescribeTrustedAdvisorChecks`_ request.
+
+ The response body contains an array of
+ `TrustedAdvisorCheckSummary`_ objects.
+
+ :type check_ids: list
+ :param check_ids:
+
+ """
+ params = {'checkIds': check_ids, }
+ return self.make_request(action='DescribeTrustedAdvisorCheckSummaries',
+ body=json.dumps(params))
+
+ def describe_trusted_advisor_checks(self, language):
+ """
+ This action enables you to get a list of the available Trusted
+ Advisor checks. You must specify a language code. English
+ ("en") and Japanese ("jp") are currently supported. The
+ response contains a list of `TrustedAdvisorCheckDescription`_
+ objects.
+
+ :type language: string
+ :param language:
+
+ """
+ params = {'language': language, }
+ return self.make_request(action='DescribeTrustedAdvisorChecks',
+ body=json.dumps(params))
+
+ def refresh_trusted_advisor_check(self, check_id):
+ """
+ This action enables you to query the service to request a
+ refresh for a specific Trusted Advisor check. Your request
+ body contains a CheckId for which you are querying. The
+ response body contains a `RefreshTrustedAdvisorCheckResult`_
+ object containing Status and TimeUntilNextRefresh fields.
+
+ :type check_id: string
+ :param check_id:
+
+ """
+ params = {'checkId': check_id, }
+ return self.make_request(action='RefreshTrustedAdvisorCheck',
+ body=json.dumps(params))
+
+ def make_request(self, action, body):
+ headers = {
+ 'X-Amz-Target': '%s.%s' % (self.TargetPrefix, action),
+ 'Host': self.region.endpoint,
+ 'Content-Type': 'application/x-amz-json-1.1',
+ 'Content-Length': str(len(body)),
+ }
+ http_request = self.build_base_http_request(
+ method='POST', path='/', auth_path='/', params={},
+ headers=headers, data=body)
+ response = self._mexe(http_request, sender=None,
+ override_num_retries=10)
+ response_body = response.read()
+ boto.log.debug(response_body)
+ if response.status == 200:
+ if response_body:
+ return json.loads(response_body)
+ else:
+ json_body = json.loads(response_body)
+ fault_name = json_body.get('__type', None)
+ exception_class = self._faults.get(fault_name, self.ResponseError)
+ raise exception_class(response.status, response.reason,
+ body=json_body)
+
diff --git a/docs/source/autoscale_tut.rst b/docs/source/autoscale_tut.rst
index 1c3a0a18..86fc529f 100644
--- a/docs/source/autoscale_tut.rst
+++ b/docs/source/autoscale_tut.rst
@@ -202,7 +202,7 @@ To retrieve the instances in your autoscale group:
>>> 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 i in reservations for i in r.instances]
+>>> instances = [i for r in reservations for i in r.instances]
To delete your autoscale group, we first need to shutdown all the
instances:
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 090de3b6..918f82be 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -85,6 +85,7 @@ Currently Supported Services
* **Other**
* Marketplace Web Services -- (:doc:`API Reference <ref/mws>`)
+ * :doc:`Support <support_tut>` -- (:doc:`API Reference <ref/support>`)
Additional Resources
--------------------
@@ -103,6 +104,17 @@ Additional Resources
.. _IRC channel: http://webchat.freenode.net/?channels=boto
.. _Follow Mitch on Twitter: http://twitter.com/garnaat
+
+Release Notes
+-------------
+
+.. toctree::
+ :titlesonly:
+ :glob:
+
+ releasenotes/*
+
+
.. toctree::
:hidden:
@@ -153,6 +165,8 @@ Additional Resources
ref/elastictranscoder
ref/redshift
ref/dynamodb2
+ support_tut
+ ref/support
Indices and tables
diff --git a/docs/source/ref/support.rst b/docs/source/ref/support.rst
new file mode 100644
index 00000000..d63d8094
--- /dev/null
+++ b/docs/source/ref/support.rst
@@ -0,0 +1,26 @@
+.. _ref-support:
+
+=======
+Support
+=======
+
+boto.support
+------------
+
+.. automodule:: boto.support
+ :members:
+ :undoc-members:
+
+boto.support.layer1
+-------------------
+
+.. automodule:: boto.support.layer1
+ :members:
+ :undoc-members:
+
+boto.support.exceptions
+-----------------------
+
+.. automodule:: boto.support.exceptions
+ :members:
+ :undoc-members:
diff --git a/docs/source/releasenotes/v2.9.1.rst b/docs/source/releasenotes/v2.9.1.rst
new file mode 100644
index 00000000..b292c0d2
--- /dev/null
+++ b/docs/source/releasenotes/v2.9.1.rst
@@ -0,0 +1,48 @@
+boto v2.9.1
+===========
+
+:date: 2013/04/30
+
+Primarily a bugfix release, this release also includes support for the new
+AWS Support API.
+
+
+Features
+--------
+
+* AWS Support API - A client was added to support the new AWS Support API. It
+ gives programmatic access to Support cases opened with AWS. A short example
+ might look like::
+
+ >>> from boto.support.layer1 import SupportConnection
+ >>> conn = SupportConnection()
+ >>> new_case = conn.create_case(
+ ... subject='Description of the issue',
+ ... service_code='amazon-cloudsearch',
+ ... category_code='performance',
+ ... communication_body="We're seeing some latency from one of our...",
+ ... severity_code='low'
+ ... )
+ >>> new_case['caseId']
+ u'case-...'
+
+ The :ref:`Support Tutorial <support_tut>` has more information on how to use
+ the new API. (SHA: <insert_here>)
+
+
+Bugfixes
+--------
+
+* The reintroduction of ``ResumableUploadHandler.get_upload_id`` that was
+ accidentally removed in a previous commit. (SHA: 758322)
+* Added ``OrdinaryCallingFormat`` to support Google Storage's certificate
+ verification. (SHA: 4ca83b)
+* Added the ``eu-west-1`` region for Redshift. (SHA: e98b95)
+* Added support for overriding the port any connection in ``boto`` uses.
+ (SHA: 08e893)
+* Added retry/checksumming support to the DynamoDB v2 client. (SHA: 969ae2)
+* Several documentation improvements/fixes:
+
+ * Incorrect docs on EC2's ``import_key_pair``. (SHA: 6ada7d)
+ * Clearer docs on the DynamoDB ``count`` parameter. (SHA: dfa456)
+ * Fixed a typo in the ``autoscale_tut``. (SHA: 6df1ae)
diff --git a/docs/source/support_tut.rst b/docs/source/support_tut.rst
new file mode 100644
index 00000000..c122e74e
--- /dev/null
+++ b/docs/source/support_tut.rst
@@ -0,0 +1,151 @@
+.. _support_tut:
+
+===========================================
+An Introduction to boto's Support interface
+===========================================
+
+This tutorial focuses on the boto interface to Amazon Web Services Support,
+allowing you to programmatically interact with cases created with Support.
+This tutorial assumes that you have already downloaded and installed ``boto``.
+
+Creating a Connection
+---------------------
+
+The first step in accessing Support is to create a connection
+to the service. There are two ways to do this in boto. The first is:
+
+>>> from boto.support.connection import SupportConnection
+>>> conn = SupportConnection('<aws access key>', '<aws secret key>')
+
+At this point the variable ``conn`` will point to a ``SupportConnection``
+object. In this example, the AWS access key and AWS secret key are passed in to
+the method explicitly. Alternatively, you can set the environment variables:
+
+AWS_ACCESS_KEY_ID - Your AWS Access Key ID \
+AWS_SECRET_ACCESS_KEY - Your AWS Secret Access Key
+
+and then call the constructor without any arguments, like this:
+
+>>> conn = SupportConnection()
+
+There is also a shortcut function in boto
+that makes it easy to create Support connections:
+
+>>> import boto.support
+>>> conn = boto.support.connect_to_region('us-west-2')
+
+In either case, ``conn`` points to a ``SupportConnection`` object which we will
+use throughout the remainder of this tutorial.
+
+
+Describing Existing Cases
+-------------------------
+
+If you have existing cases or want to fetch cases in the future, you'll
+use the ``SupportConnection.describe_cases`` method. For example::
+
+ >>> cases = conn.describe_cases()
+ >>> len(cases['cases'])
+ 1
+ >>> cases['cases'][0]['title']
+ 'A test case.'
+ >>> cases['cases'][0]['caseId']
+ 'case-...'
+
+You can also fetch a set of cases (or single case) by providing a
+``case_id_list`` parameter::
+
+ >>> cases = conn.describe_cases(case_id_list=['case-1'])
+ >>> len(cases['cases'])
+ 1
+ >>> cases['cases'][0]['title']
+ 'A test case.'
+ >>> cases['cases'][0]['caseId']
+ 'case-...'
+
+
+Describing Service Codes
+------------------------
+
+In order to create a new case, you'll need to fetch the service (& category)
+codes available to you. Fetching them is a simple call to::
+
+ >>> services = conn.describe_services()
+ >>> services['services'][0]['code']
+ 'amazon-cloudsearch'
+
+If you only care about certain services, you can pass a list of service codes::
+
+ >>> service_details = conn.describe_services(service_code_list=[
+ ... 'amazon-cloudsearch',
+ ... 'amazon-dynamodb',
+ ... ])
+
+
+Describing Severity Levels
+--------------------------
+
+In order to create a new case, you'll also need to fetch the severity levels
+available to you. Fetching them looks like::
+
+ >>> severities = conn.describe_severity_levels()
+ >>> severities['severityLevels'][0]['code']
+ 'low'
+
+
+Creating a Case
+---------------
+
+Upon creating a connection to Support, you can now work with existing Support
+cases, create new cases or resolve them. We'll start with creating a new case::
+
+ >>> new_case = conn.create_case(
+ ... subject='This is a test case.',
+ ... service_code='',
+ ... category_code='',
+ ... communication_body="",
+ ... severity_code='low'
+ ... )
+ >>> new_case['caseId']
+ 'case-...'
+
+For the ``service_code/category_code`` parameters, you'll need to do a
+``SupportConnection.describe_services`` call, then select the appropriate
+service code (& appropriate category code within that service) from the
+response.
+
+For the ``severity_code`` parameter, you'll need to do a
+``SupportConnection.describe_severity_levels`` call, then select the appropriate
+severity code from the response.
+
+
+Adding to a Case
+----------------
+
+Since the purpose of a support case involves back-and-forth communication,
+you can add additional communication to the case as well. Providing a response
+might look like::
+
+ >>> result = conn.add_communication_to_case(
+ ... communication_body="This is a followup. It's working now."
+ ... case_id='case-...'
+ ... )
+
+
+Fetching all Communications for a Case
+--------------------------------------
+
+Getting all communications for a given case looks like::
+
+ >>> communications = conn.describe_communications('case-...')
+
+
+Resolving a Case
+----------------
+
+Once a case is finished, you should mark it as resolved to close it out.
+Resolving a case looks like::
+
+ >>> closed = conn.resolve_case(case_id='case-...')
+ >>> closed['result']
+ True
diff --git a/tests/integration/support/__init__.py b/tests/integration/support/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/integration/support/__init__.py
diff --git a/tests/integration/support/test_cert_verification.py b/tests/integration/support/test_cert_verification.py
new file mode 100644
index 00000000..586cc71d
--- /dev/null
+++ b/tests/integration/support/test_cert_verification.py
@@ -0,0 +1,35 @@
+# 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.
+#
+
+from tests.unit import unittest
+import boto.support
+
+
+class CertVerificationTest(unittest.TestCase):
+
+ support = True
+ ssl = True
+
+ def test_certs(self):
+ for region in boto.support.regions():
+ c = region.connect()
+ c.describe_services()
diff --git a/tests/integration/support/test_layer1.py b/tests/integration/support/test_layer1.py
new file mode 100644
index 00000000..6b2b65d2
--- /dev/null
+++ b/tests/integration/support/test_layer1.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 unittest
+import time
+
+from boto.support.layer1 import SupportConnection
+from boto.support import exceptions
+
+
+class TestSupportLayer1Management(unittest.TestCase):
+ support = True
+
+ def setUp(self):
+ self.api = SupportConnection()
+ self.wait_time = 5
+
+ def test_as_much_as_possible_before_teardown(self):
+ cases = self.api.describe_cases()
+ preexisting_count = len(cases.get('cases', []))
+
+ services = self.api.describe_services()
+ self.assertTrue('services' in services)
+ service_codes = [serv['code'] for serv in services['services']]
+ self.assertTrue('amazon-cloudsearch' in service_codes)
+
+ severity = self.api.describe_severity_levels()
+ self.assertTrue('severityLevels' in severity)
+ severity_codes = [sev['code'] for sev in severity['severityLevels']]
+ self.assertTrue('low' in severity_codes)
+
+ case_1 = self.api.create_case(
+ subject='TEST: I am a test case.',
+ service_code='amazon-cloudsearch',
+ category_code='other',
+ communication_body="This is a test problem",
+ severity_code='low',
+ language='en'
+ )
+ time.sleep(self.wait_time)
+ case_id = case_1['caseId']
+
+ new_cases = self.api.describe_cases()
+ self.assertTrue(len(new_cases['cases']) > preexisting_count)
+
+ result = self.api.add_communication_to_case(
+ communication_body="This is a test solution.",
+ case_id=case_id
+ )
+ self.assertTrue(result.get('result', False))
+ time.sleep(self.wait_time)
+
+ final_cases = self.api.describe_cases(case_id_list=[case_id])
+ comms = final_cases['cases'][0]['recentCommunications']\
+ ['communications']
+ self.assertEqual(len(comms), 2)
+
+ close_result = self.api.resolve_case(case_id=case_id)