diff options
author | James Saryerwinnie <js@jamesls.com> | 2013-04-30 18:15:59 -0700 |
---|---|---|
committer | James Saryerwinnie <js@jamesls.com> | 2013-04-30 18:15:59 -0700 |
commit | a9e834ae4b85d787092e5b0d86b2ce8566d9a7a0 (patch) | |
tree | fce4433d21a30be749026293bcc04fdd2299e555 | |
parent | 89f4947000587e12042e5b35c4557871b21137b9 (diff) | |
parent | 38f8bec07ae1e554f666da3337ccf2db65842034 (diff) | |
download | boto-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.rst | 4 | ||||
-rw-r--r-- | boto/__init__.py | 44 | ||||
-rw-r--r-- | boto/connection.py | 4 | ||||
-rw-r--r-- | boto/dynamodb/layer2.py | 6 | ||||
-rw-r--r-- | boto/dynamodb/table.py | 6 | ||||
-rw-r--r-- | boto/dynamodb2/exceptions.py | 4 | ||||
-rw-r--r-- | boto/dynamodb2/layer1.py | 65 | ||||
-rw-r--r-- | boto/ec2/connection.py | 6 | ||||
-rw-r--r-- | boto/gs/resumable_upload_handler.py | 16 | ||||
-rw-r--r-- | boto/provider.py | 9 | ||||
-rw-r--r-- | boto/redshift/__init__.py | 4 | ||||
-rw-r--r-- | boto/s3/key.py | 1 | ||||
-rw-r--r-- | boto/s3/resumable_download_handler.py | 4 | ||||
-rwxr-xr-x | boto/storage_uri.py | 27 | ||||
-rw-r--r-- | boto/support/__init__.py | 47 | ||||
-rw-r--r-- | boto/support/exceptions.py | 34 | ||||
-rw-r--r-- | boto/support/layer1.py | 529 | ||||
-rw-r--r-- | docs/source/autoscale_tut.rst | 2 | ||||
-rw-r--r-- | docs/source/index.rst | 14 | ||||
-rw-r--r-- | docs/source/ref/support.rst | 26 | ||||
-rw-r--r-- | docs/source/releasenotes/v2.9.1.rst | 48 | ||||
-rw-r--r-- | docs/source/support_tut.rst | 151 | ||||
-rw-r--r-- | tests/integration/support/__init__.py | 0 | ||||
-rw-r--r-- | tests/integration/support/test_cert_verification.py | 35 | ||||
-rw-r--r-- | tests/integration/support/test_layer1.py | 76 |
25 files changed, 1136 insertions, 26 deletions
@@ -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) |