diff options
-rw-r--r-- | README.rst | 6 | ||||
-rw-r--r-- | boto/__init__.py | 2 | ||||
-rw-r--r-- | boto/compat.py | 13 | ||||
-rw-r--r-- | boto/dynamodb2/table.py | 2 | ||||
-rw-r--r-- | boto/ec2/connection.py | 12 | ||||
-rw-r--r-- | boto/ec2/elb/__init__.py | 2 | ||||
-rw-r--r-- | boto/ec2/elb/loadbalancer.py | 1 | ||||
-rw-r--r-- | boto/ec2/snapshot.py | 4 | ||||
-rw-r--r-- | boto/ec2/volume.py | 4 | ||||
-rw-r--r-- | boto/endpoints.json | 4 | ||||
-rw-r--r-- | boto/opsworks/__init__.py | 2 | ||||
-rw-r--r-- | boto/opsworks/layer1.py | 2 | ||||
-rw-r--r-- | boto/provider.py | 73 | ||||
-rw-r--r-- | boto/pyami/config.py | 17 | ||||
-rw-r--r-- | boto/rds2/layer1.py | 2 | ||||
-rw-r--r-- | boto/regioninfo.py | 4 | ||||
-rw-r--r-- | docs/source/boto_config_tut.rst | 25 | ||||
-rw-r--r-- | docs/source/cloudwatch_tut.rst | 2 | ||||
-rw-r--r-- | docs/source/elb_tut.rst | 2 | ||||
-rw-r--r-- | docs/source/index.rst | 1 | ||||
-rw-r--r-- | docs/source/releasenotes/v2.29.0.rst | 25 | ||||
-rw-r--r-- | setup.cfg | 4 | ||||
-rw-r--r-- | tests/integration/opsworks/test_layer1.py | 16 | ||||
-rwxr-xr-x | tests/unit/ec2/test_connection.py | 154 | ||||
-rw-r--r-- | tests/unit/ec2/test_snapshot.py | 3 | ||||
-rw-r--r-- | tests/unit/ec2/test_volume.py | 10 | ||||
-rw-r--r-- | tests/unit/provider/test_provider.py | 131 |
27 files changed, 469 insertions, 54 deletions
@@ -1,15 +1,15 @@ #### boto #### -boto 2.28.0 +boto 2.29.0 -Released: 8-May-2014 +Released: 29-May-2014 .. image:: https://travis-ci.org/boto/boto.png?branch=develop :target: https://travis-ci.org/boto/boto .. image:: https://pypip.in/d/boto/badge.png - :target: https://crate.io/packages/boto/ + :target: https://pypi.python.org/pypi/boto/ ************ Introduction diff --git a/boto/__init__.py b/boto/__init__.py index 53464c33..95bbf334 100644 --- a/boto/__init__.py +++ b/boto/__init__.py @@ -37,7 +37,7 @@ import logging.config import urlparse from boto.exception import InvalidUriError -__version__ = '2.28.0' +__version__ = '2.29.0' Version = __version__ # for backware compatibility # http://bugs.python.org/issue7980 diff --git a/boto/compat.py b/boto/compat.py index 44fbc3b3..a13f58b4 100644 --- a/boto/compat.py +++ b/boto/compat.py @@ -19,6 +19,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # +import os + # This allows boto modules to say "from boto.compat import json". This is # preferred so that all modules don't have to repeat this idiom. @@ -26,3 +28,14 @@ try: import simplejson as json except ImportError: import json + + +# If running in Google App Engine there is no "user" and +# os.path.expanduser() will fail. Attempt to detect this case and use a +# no-op expanduser function in this case. +try: + os.path.expanduser('~') + expanduser = os.path.expanduser +except (AttributeError, ImportError): + # This is probably running on App Engine. + expanduser = (lambda x: x) diff --git a/boto/dynamodb2/table.py b/boto/dynamodb2/table.py index 6b142f6e..37833dd9 100644 --- a/boto/dynamodb2/table.py +++ b/boto/dynamodb2/table.py @@ -1270,7 +1270,7 @@ class Table(object): # We pass the keys to the constructor instead, so it can maintain it's # own internal state as to what keys have been processed. results = BatchGetResultSet(keys=keys, max_batch_get=self.max_batch_get) - results.to_call(self._batch_get, consistent=False) + results.to_call(self._batch_get, consistent=consistent) return results def _batch_get(self, keys, consistent=False): diff --git a/boto/ec2/connection.py b/boto/ec2/connection.py index 02c589a4..36db8aab 100644 --- a/boto/ec2/connection.py +++ b/boto/ec2/connection.py @@ -71,7 +71,7 @@ from boto.exception import EC2ResponseError class EC2Connection(AWSQueryConnection): - APIVersion = boto.config.get('Boto', 'ec2_version', '2013-10-15') + APIVersion = boto.config.get('Boto', 'ec2_version', '2014-05-01') DefaultRegionName = boto.config.get('Boto', 'ec2_region_name', 'us-east-1') DefaultRegionEndpoint = boto.config.get('Boto', 'ec2_region_endpoint', 'ec2.us-east-1.amazonaws.com') @@ -2246,8 +2246,8 @@ class EC2Connection(AWSQueryConnection): params['DryRun'] = 'true' return self.get_status('ModifyVolumeAttribute', params, verb='POST') - def create_volume(self, size, zone, snapshot=None, - volume_type=None, iops=None, dry_run=False): + def create_volume(self, size, zone, snapshot=None, volume_type=None, + iops=None, encrypted=False, dry_run=False): """ Create a new EBS Volume. @@ -2269,6 +2269,10 @@ class EC2Connection(AWSQueryConnection): :param iops: The provisioned IOPs you want to associate with this volume. (optional) + :type encrypted: bool + :param encrypted: Specifies whether the volume should be encrypted. + (optional) + :type dry_run: bool :param dry_run: Set to True if the operation should not actually run. @@ -2286,6 +2290,8 @@ class EC2Connection(AWSQueryConnection): params['VolumeType'] = volume_type if iops: params['Iops'] = str(iops) + if encrypted: + params['Encrypted'] = 'true' if dry_run: params['DryRun'] = 'true' return self.get_object('CreateVolume', params, Volume, verb='POST') diff --git a/boto/ec2/elb/__init__.py b/boto/ec2/elb/__init__.py index 9c82ce76..cdd88074 100644 --- a/boto/ec2/elb/__init__.py +++ b/boto/ec2/elb/__init__.py @@ -615,7 +615,7 @@ class ELBConnection(AWSQueryConnection): def create_lb_policy(self, lb_name, policy_name, policy_type, policy_attributes): """ - Creates a new policy that contais the necessary attributes depending on + Creates a new policy that contains the necessary attributes depending on the policy type. Policies are settings that are saved for your load balancer and that can be applied to the front-end listener, or the back-end application server. diff --git a/boto/ec2/elb/loadbalancer.py b/boto/ec2/elb/loadbalancer.py index f76feb15..3dbaf6b8 100644 --- a/boto/ec2/elb/loadbalancer.py +++ b/boto/ec2/elb/loadbalancer.py @@ -82,6 +82,7 @@ class LoadBalancer(object): check policy for this load balancer. :ivar boto.ec2.elb.policies.Policies policies: Cookie stickiness and other policies. + :ivar str name: The name of the Load Balancer. :ivar str dns_name: The external DNS name for the balancer. :ivar str created_time: A date+time string showing when the load balancer was created. diff --git a/boto/ec2/snapshot.py b/boto/ec2/snapshot.py index 6121d0c8..22f69ab2 100644 --- a/boto/ec2/snapshot.py +++ b/boto/ec2/snapshot.py @@ -41,6 +41,7 @@ class Snapshot(TaggedEC2Object): self.owner_alias = None self.volume_size = None self.description = None + self.encrypted = None def __repr__(self): return 'Snapshot:%s' % self.id @@ -65,6 +66,8 @@ class Snapshot(TaggedEC2Object): self.volume_size = value elif name == 'description': self.description = value + elif name == 'encrypted': + self.encrypted = (value.lower() == 'true') else: setattr(self, name, value) @@ -152,6 +155,7 @@ class Snapshot(TaggedEC2Object): self.id, volume_type, iops, + self.encrypted, dry_run=dry_run ) diff --git a/boto/ec2/volume.py b/boto/ec2/volume.py index 95121fa8..c40062b3 100644 --- a/boto/ec2/volume.py +++ b/boto/ec2/volume.py @@ -44,6 +44,7 @@ class Volume(TaggedEC2Object): :ivar type: The type of volume (standard or consistent-iops) :ivar iops: If this volume is of type consistent-iops, this is the number of IOPS provisioned (10-300). + :ivar encrypted: True if this volume is encrypted. """ def __init__(self, connection=None): @@ -57,6 +58,7 @@ class Volume(TaggedEC2Object): self.zone = None self.type = None self.iops = None + self.encrypted = None def __repr__(self): return 'Volume:%s' % self.id @@ -92,6 +94,8 @@ class Volume(TaggedEC2Object): self.type = value elif name == 'iops': self.iops = int(value) + elif name == 'encrypted': + self.encrypted = (value.lower() == 'true') else: setattr(self, name, value) diff --git a/boto/endpoints.json b/boto/endpoints.json index bf52525f..a4681e08 100644 --- a/boto/endpoints.json +++ b/boto/endpoints.json @@ -19,6 +19,7 @@ "eu-west-1": "cloudformation.eu-west-1.amazonaws.com", "sa-east-1": "cloudformation.sa-east-1.amazonaws.com", "us-east-1": "cloudformation.us-east-1.amazonaws.com", + "us-gov-west-1": "cloudformation.us-gov-west-1.amazonaws.com", "us-west-1": "cloudformation.us-west-1.amazonaws.com", "us-west-2": "cloudformation.us-west-2.amazonaws.com" }, @@ -40,7 +41,10 @@ "us-west-2": "cloudsearch.us-west-2.amazonaws.com" }, "cloudtrail": { + "ap-southeast-2": "cloudtrail.ap-southeast-2.amazonaws.com", + "eu-west-1": "cloudtrail.eu-west-1.amazonaws.com", "us-east-1": "cloudtrail.us-east-1.amazonaws.com", + "us-west-1": "cloudtrail.us-west-1.amazonaws.com", "us-west-2": "cloudtrail.us-west-2.amazonaws.com" }, "cloudwatch": { diff --git a/boto/opsworks/__init__.py b/boto/opsworks/__init__.py index 71bc7209..1ff5c0f6 100644 --- a/boto/opsworks/__init__.py +++ b/boto/opsworks/__init__.py @@ -25,7 +25,7 @@ from boto.regioninfo import RegionInfo, get_regions def regions(): """ - Get all available regions for the Amazon Kinesis service. + Get all available regions for the Amazon OpsWorks service. :rtype: list :return: A list of :class:`boto.regioninfo.RegionInfo` diff --git a/boto/opsworks/layer1.py b/boto/opsworks/layer1.py index 6e8d24ba..e2e75e6f 100644 --- a/boto/opsworks/layer1.py +++ b/boto/opsworks/layer1.py @@ -91,7 +91,7 @@ class OpsWorksConnection(AWSQueryConnection): def __init__(self, **kwargs): - region = kwargs.get('region') + region = kwargs.pop('region', None) if not region: region = RegionInfo(self, self.DefaultRegionName, self.DefaultRegionEndpoint) diff --git a/boto/provider.py b/boto/provider.py index 2febdc99..8b1a7df6 100644 --- a/boto/provider.py +++ b/boto/provider.py @@ -31,6 +31,8 @@ from datetime import datetime import boto from boto import config +from boto.compat import expanduser +from boto.pyami.config import Config from boto.gs.acl import ACL from boto.gs.acl import CannedACLStrings as CannedGSACLStrings from boto.s3.acl import CannedACLStrings as CannedS3ACLStrings @@ -66,13 +68,16 @@ STORAGE_PERMISSIONS_ERROR = 'StoragePermissionsError' STORAGE_RESPONSE_ERROR = 'StorageResponseError' +class ProfileNotFoundError(ValueError): pass + + class Provider(object): CredentialMap = { 'aws': ('aws_access_key_id', 'aws_secret_access_key', - 'aws_security_token'), + 'aws_security_token', 'aws_profile'), 'google': ('gs_access_key_id', 'gs_secret_access_key', - None), + None, None), } AclClassMap = { @@ -182,9 +187,17 @@ class Provider(object): self.acl_class = self.AclClassMap[self.name] self.canned_acls = self.CannedAclsMap[self.name] self._credential_expiry_time = None + + # Load shared credentials file if it exists + shared_path = os.path.join(expanduser('~'), '.aws', 'credentials') + self.shared_credentials = Config(do_load=False) + if os.path.exists(shared_path): + self.shared_credentials.load_from_path(shared_path) + self.get_credentials(access_key, secret_key, security_token, profile_name) self.configure_headers() self.configure_errors() + # 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): @@ -247,16 +260,39 @@ class Provider(object): def get_credentials(self, access_key=None, secret_key=None, security_token=None, profile_name=None): - access_key_name, secret_key_name, security_token_name = self.CredentialMap[self.name] + access_key_name, secret_key_name, security_token_name, \ + profile_name_name = self.CredentialMap[self.name] + + # Load profile from shared environment variable if it was not + # already passed in and the environment variable exists + if profile_name is None and profile_name_name.upper() in os.environ: + profile_name = os.environ[profile_name_name.upper()] + + shared = self.shared_credentials + if access_key is not None: self.access_key = access_key boto.log.debug("Using access key provided by client.") elif access_key_name.upper() in os.environ: self.access_key = os.environ[access_key_name.upper()] boto.log.debug("Using access key found in environment variable.") - elif config.has_option("profile %s" % profile_name, access_key_name): - self.access_key = config.get("profile %s" % profile_name, access_key_name) - boto.log.debug("Using access key found in config file: profile %s." % profile_name) + elif profile_name is not None: + if shared.has_option(profile_name, access_key_name): + self.access_key = shared.get(profile_name, access_key_name) + boto.log.debug("Using access key found in shared credential " + "file for profile %s." % profile_name) + elif config.has_option("profile %s" % profile_name, + access_key_name): + self.access_key = config.get("profile %s" % profile_name, + access_key_name) + boto.log.debug("Using access key found in config file: " + "profile %s." % profile_name) + else: + raise ProfileNotFoundError('Profile "%s" not found!' % + profile_name) + elif shared.has_option('default', access_key_name): + self.access_key = shared.get('default', access_key_name) + boto.log.debug("Using access key found in shared credential file.") elif config.has_option('Credentials', access_key_name): self.access_key = config.get('Credentials', access_key_name) boto.log.debug("Using access key found in config file.") @@ -267,9 +303,22 @@ class Provider(object): elif secret_key_name.upper() in os.environ: self.secret_key = os.environ[secret_key_name.upper()] boto.log.debug("Using secret key found in environment variable.") - elif config.has_option("profile %s" % profile_name, secret_key_name): - self.secret_key = config.get("profile %s" % profile_name, secret_key_name) - boto.log.debug("Using secret key found in config file: profile %s." % profile_name) + elif profile_name is not None: + if shared.has_option(profile_name, secret_key_name): + self.secret_key = shared.get(profile_name, secret_key_name) + boto.log.debug("Using secret key found in shared credential " + "file for profile %s." % profile_name) + elif config.has_option("profile %s" % profile_name, secret_key_name): + self.secret_key = config.get("profile %s" % profile_name, + secret_key_name) + boto.log.debug("Using secret key found in config file: " + "profile %s." % profile_name) + else: + raise ProfileNotFoundError('Profile "%s" not found!' % + profile_name) + elif shared.has_option('default', secret_key_name): + self.secret_key = shared.get('default', secret_key_name) + boto.log.debug("Using secret key found in shared credential file.") elif config.has_option('Credentials', secret_key_name): self.secret_key = config.get('Credentials', secret_key_name) boto.log.debug("Using secret key found in config file.") @@ -299,6 +348,12 @@ class Provider(object): self.security_token = os.environ[security_token_name.upper()] boto.log.debug("Using security token found in environment" " variable.") + elif shared.has_option(profile_name or 'default', + security_token_name): + self.security_token = shared.get(profile_name or 'default', + security_token_name) + boto.log.debug("Using security token found in shared " + "credential file.") elif config.has_option('Credentials', security_token_name): self.security_token = config.get('Credentials', security_token_name) diff --git a/boto/pyami/config.py b/boto/pyami/config.py index 6669cc05..4756f5ee 100644 --- a/boto/pyami/config.py +++ b/boto/pyami/config.py @@ -20,20 +20,15 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # -import StringIO, os, re -import warnings import ConfigParser +import os +import re +import StringIO +import warnings + import boto +from boto.compat import expanduser -# If running in Google App Engine there is no "user" and -# os.path.expanduser() will fail. Attempt to detect this case and use a -# no-op expanduser function in this case. -try: - os.path.expanduser('~') - expanduser = os.path.expanduser -except (AttributeError, ImportError): - # This is probably running on App Engine. - expanduser = (lambda x: x) # By default we use two locations for the boto configurations, # /etc/boto.cfg and ~/.boto (which works on Windows and Unix). diff --git a/boto/rds2/layer1.py b/boto/rds2/layer1.py index 1e2ba537..887708ba 100644 --- a/boto/rds2/layer1.py +++ b/boto/rds2/layer1.py @@ -1011,7 +1011,7 @@ class RDSConnection(AWSQueryConnection): :param subnet_ids: The EC2 Subnet IDs for the DB subnet group. :type tags: list - :param tags: A list of tags. + :param tags: A list of tags into tuples. """ params = { diff --git a/boto/regioninfo.py b/boto/regioninfo.py index 29ebb1e3..eee20bbf 100644 --- a/boto/regioninfo.py +++ b/boto/regioninfo.py @@ -87,8 +87,8 @@ def load_regions(): # Try the ENV var. If not, check the config file. if os.environ.get('BOTO_ENDPOINTS'): additional_path = os.environ['BOTO_ENDPOINTS'] - elif boto.config.get('boto', 'endpoints_path'): - additional_path = boto.config.get('boto', 'endpoints_path') + elif boto.config.get('Boto', 'endpoints_path'): + additional_path = boto.config.get('Boto', 'endpoints_path') # If there's a file provided, we'll load it & additively merge it into # the endpoints. diff --git a/docs/source/boto_config_tut.rst b/docs/source/boto_config_tut.rst index a2917a0d..37c22f04 100644 --- a/docs/source/boto_config_tut.rst +++ b/docs/source/boto_config_tut.rst @@ -10,9 +10,9 @@ Introduction There is a growing list of configuration options for the boto library. Many of these options can be passed into the constructors for top-level objects such as connections. Some options, such as credentials, can also be read from -environment variables (e.g. ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY``). -It is also possible to manage these options in a central place through the use -of boto config files. +environment variables (e.g. ``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``, +``AWS_SECURITY_TOKEN`` and ``AWS_PROFILE``). It is also possible to manage +these options in a central place through the use of boto config files. Details ------- @@ -24,6 +24,7 @@ and in the following order: * /etc/boto.cfg - for site-wide settings that all users on this machine will use * ~/.boto - for user-specific settings +* ~/.aws/credentials - for credentials shared between SDKs In Windows, create a text file that has any name (e.g. boto.config). It's recommended that you put this file in your user folder. Then set @@ -58,6 +59,8 @@ boto requests. The order of precedence for authentication credentials is: * Credentials passed into the Connection class constructor. * Credentials specified by environment variables +* Credentials specified as named profiles in the shared credential file. +* Credentials specified by default in the shared credential file. * Credentials specified as named profiles in the config file. * Credentials specified by default in the config file. @@ -85,6 +88,22 @@ when you instantiate your connection. If you specify a profile that does not exist in the configuration, the keys used under the ``[Credentials]`` heading will be applied by default. +The shared credentials file in ``~/.aws/credentials`` uses a slightly +different format. For example:: + + [default] + aws_access_key_id = <your default access key> + aws_secret_access_key = <your default secret key> + + [name_goes_here] + aws_access_key_id = <access key for this profile> + aws_secret_access_key = <secret key for this profile> + + [another_profile] + aws_access_key_id = <access key for this profile> + aws_secret_access_key = <secret key for this profile> + aws_security_token = <optional security token for this profile> + For greater security, the secret key can be stored in a keyring and retrieved via the keyring package. To use a keyring, use ``keyring``, rather than ``aws_secret_access_key``:: diff --git a/docs/source/cloudwatch_tut.rst b/docs/source/cloudwatch_tut.rst index c9302092..37263a8d 100644 --- a/docs/source/cloudwatch_tut.rst +++ b/docs/source/cloudwatch_tut.rst @@ -76,7 +76,7 @@ that we are interested in. For this example, let's say we want the data for the previous hour::
>>> import datetime
- >>> end = datetime.datetime.now()
+ >>> end = datetime.datetime.utcnow()
>>> start = end - datetime.timedelta(hours=1)
We also need to supply the Statistic that we want reported and
diff --git a/docs/source/elb_tut.rst b/docs/source/elb_tut.rst index 0cff8ac8..2b25e74d 100644 --- a/docs/source/elb_tut.rst +++ b/docs/source/elb_tut.rst @@ -74,7 +74,7 @@ Alternatively, edit your boto.cfg with the default ELB endpoint to use:: Getting Existing Load Balancers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To retrieve any exiting load balancers: +To retrieve any existing load balancers: >>> conn.get_all_load_balancers() [LoadBalancer:load-balancer-prod, LoadBalancer:load-balancer-staging] diff --git a/docs/source/index.rst b/docs/source/index.rst index 2eed7c2a..4b805a6d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -119,6 +119,7 @@ Release Notes .. toctree:: :titlesonly: + releasenotes/v2.29.0 releasenotes/v2.28.0 releasenotes/v2.27.0 releasenotes/v2.26.1 diff --git a/docs/source/releasenotes/v2.29.0.rst b/docs/source/releasenotes/v2.29.0.rst new file mode 100644 index 00000000..5e58781a --- /dev/null +++ b/docs/source/releasenotes/v2.29.0.rst @@ -0,0 +1,25 @@ +boto v2.29.0 +============ + +:date: 2014/05/29 + +This release adds support for the AWS shared credentials file, adds support for Amazon Elastic Block Store (EBS) encryption, and contains a handful of fixes for Amazon EC2, AWS CloudFormation, AWS CloudWatch, AWS CloudTrail, Amazon DynamoDB and Amazon Relational Database Service (RDS). It also includes fixes for Python wheel support. + +A bug has been fixed such that a new exception is thrown when a profile name is explicitly passed either via code (``profile="foo"``) or an environment variable (``AWS_PROFILE=foo``) and that profile does not exist in any configuration file. Previously this was silently ignored, and the default credentials would be used without informing the user. + +Changes +------- +* Added support for shared credentials file. (:issue:`2292`, :sha:`d5ed49f`) +* Added support for EBS encryption. (:issue:`2282`, :sha:`d85a449`) +* Added GovCloud CloudFormation endpoint. (:issue:`2297`, :sha:`0f75fb9`) +* Added new CloudTrail endpoints to endpoints.json. (:issue:`2269`, :sha:`1168580`) +* Added 'name' param to documentation of ELB LoadBalancer. (:issue:`2291`, :sha:`86e1174`) +* Fix typo in ELB docs. (:issue:`2294`, :sha:`37aaa0f`) +* Fix typo in ELB tutorial. (:issue:`2290`, :sha:`40a758a`) +* Fix OpsWorks ``connect_to_region`` exception. (:issue:`2288`, :sha:`26729c7`) +* Fix timezones in CloudWatch date range example. (:issue:`2285`, :sha:`138a6d0`) +* Fix description of param tags into ``rds2.create_db_subnet_group``. (:issue:`2279`, :sha:`dc1037f`) +* Fix the incorrect name of a test case. (:issue:`2273`, :sha:`ee195a1`) +* Fix "consistent" argument to ``boto.dynamodb2.table.Table.batch_get``. (:issue:`2272`, :sha:`c432b09`) +* Update the wheel to be python 2 compatible only. (:issue:`2286`, :sha:`6ad0b75`) +* Crate.io is no longer a package index. (:issue:`2289`, :sha:`7f23de0`) @@ -1,2 +1,2 @@ -[wheel] -universal = 1 +[bdist_wheel] +python-tag = py2 diff --git a/tests/integration/opsworks/test_layer1.py b/tests/integration/opsworks/test_layer1.py index a2503952..944b5202 100644 --- a/tests/integration/opsworks/test_layer1.py +++ b/tests/integration/opsworks/test_layer1.py @@ -20,13 +20,15 @@ # IN THE SOFTWARE. # import unittest -import time from boto.exception import JSONResponseError +from boto.opsworks import connect_to_region, regions, RegionInfo from boto.opsworks.layer1 import OpsWorksConnection class TestOpsWorksConnection(unittest.TestCase): + opsworks = True + def setUp(self): self.api = OpsWorksConnection() @@ -38,3 +40,15 @@ class TestOpsWorksConnection(unittest.TestCase): with self.assertRaises(JSONResponseError): self.api.create_stack('testbotostack', 'us-east-1', 'badarn', 'badarn2') + + +class TestOpsWorksHelpers(unittest.TestCase): + opsworks = True + + def test_regions(self): + response = regions() + self.assertIsInstance(response[0], RegionInfo) + + def test_connect_to_region(self): + connection = connect_to_region('us-east-1') + self.assertIsInstance(connection, OpsWorksConnection) diff --git a/tests/unit/ec2/test_connection.py b/tests/unit/ec2/test_connection.py index d34c6046..deeb673d 100755 --- a/tests/unit/ec2/test_connection.py +++ b/tests/unit/ec2/test_connection.py @@ -2,7 +2,7 @@ import httplib from datetime import datetime, timedelta -from mock import MagicMock, Mock, patch +from mock import MagicMock, Mock from tests.unit import unittest from tests.unit import AWSMockServiceTestCase @@ -1467,5 +1467,157 @@ class TestAssociateAddressFail(TestEC2ConnectionBase): self.assertEqual(False, result) +class TestDescribeVolumes(TestEC2ConnectionBase): + def default_body(self): + return """ + <DescribeVolumesResponse xmlns="http://ec2.amazonaws.com/doc/2014-02-01/"> + <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> + <volumeSet> + <item> + <volumeId>vol-1a2b3c4d</volumeId> + <size>80</size> + <snapshotId/> + <availabilityZone>us-east-1a</availabilityZone> + <status>in-use</status> + <createTime>YYYY-MM-DDTHH:MM:SS.SSSZ</createTime> + <attachmentSet> + <item> + <volumeId>vol-1a2b3c4d</volumeId> + <instanceId>i-1a2b3c4d</instanceId> + <device>/dev/sdh</device> + <status>attached</status> + <attachTime>YYYY-MM-DDTHH:MM:SS.SSSZ</attachTime> + <deleteOnTermination>false</deleteOnTermination> + </item> + </attachmentSet> + <volumeType>standard</volumeType> + <encrypted>true</encrypted> + </item> + <item> + <volumeId>vol-5e6f7a8b</volumeId> + <size>80</size> + <snapshotId/> + <availabilityZone>us-east-1a</availabilityZone> + <status>in-use</status> + <createTime>YYYY-MM-DDTHH:MM:SS.SSSZ</createTime> + <attachmentSet> + <item> + <volumeId>vol-5e6f7a8b</volumeId> + <instanceId>i-5e6f7a8b</instanceId> + <device>/dev/sdz</device> + <status>attached</status> + <attachTime>YYYY-MM-DDTHH:MM:SS.SSSZ</attachTime> + <deleteOnTermination>false</deleteOnTermination> + </item> + </attachmentSet> + <volumeType>standard</volumeType> + <encrypted>false</encrypted> + </item> + </volumeSet> + </DescribeVolumesResponse> + """ + + def test_get_all_volumes(self): + self.set_http_response(status_code=200) + result = self.ec2.get_all_volumes(volume_ids=['vol-1a2b3c4d', 'vol-5e6f7a8b']) + self.assert_request_parameters({ + 'Action': 'DescribeVolumes', + 'VolumeId.1': 'vol-1a2b3c4d', + 'VolumeId.2': 'vol-5e6f7a8b'}, + ignore_params_values=['AWSAccessKeyId', 'SignatureMethod', + 'SignatureVersion', 'Timestamp', + 'Version']) + self.assertEqual(len(result), 2) + self.assertEqual(result[0].id, 'vol-1a2b3c4d') + self.assertTrue(result[0].encrypted) + self.assertEqual(result[1].id, 'vol-5e6f7a8b') + self.assertFalse(result[1].encrypted) + + +class TestDescribeSnapshots(TestEC2ConnectionBase): + def default_body(self): + return """ + <DescribeSnapshotsResponse xmlns="http://ec2.amazonaws.com/doc/2014-02-01/"> + <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> + <snapshotSet> + <item> + <snapshotId>snap-1a2b3c4d</snapshotId> + <volumeId>vol-1a2b3c4d</volumeId> + <status>pending</status> + <startTime>YYYY-MM-DDTHH:MM:SS.SSSZ</startTime> + <progress>80%</progress> + <ownerId>111122223333</ownerId> + <volumeSize>15</volumeSize> + <description>Daily Backup</description> + <tagSet/> + <encrypted>true</encrypted> + </item> + </snapshotSet> + <snapshotSet> + <item> + <snapshotId>snap-5e6f7a8b</snapshotId> + <volumeId>vol-5e6f7a8b</volumeId> + <status>completed</status> + <startTime>YYYY-MM-DDTHH:MM:SS.SSSZ</startTime> + <progress>100%</progress> + <ownerId>111122223333</ownerId> + <volumeSize>15</volumeSize> + <description>Daily Backup</description> + <tagSet/> + <encrypted>false</encrypted> + </item> + </snapshotSet> + </DescribeSnapshotsResponse> + """ + + def test_get_all_snapshots(self): + self.set_http_response(status_code=200) + result = self.ec2.get_all_snapshots(snapshot_ids=['snap-1a2b3c4d', 'snap-5e6f7a8b']) + self.assert_request_parameters({ + 'Action': 'DescribeSnapshots', + 'SnapshotId.1': 'snap-1a2b3c4d', + 'SnapshotId.2': 'snap-5e6f7a8b'}, + ignore_params_values=['AWSAccessKeyId', 'SignatureMethod', + 'SignatureVersion', 'Timestamp', + 'Version']) + self.assertEqual(len(result), 2) + self.assertEqual(result[0].id, 'snap-1a2b3c4d') + self.assertTrue(result[0].encrypted) + self.assertEqual(result[1].id, 'snap-5e6f7a8b') + self.assertFalse(result[1].encrypted) + + +class TestCreateVolume(TestEC2ConnectionBase): + def default_body(self): + return """ + <CreateVolumeResponse xmlns="http://ec2.amazonaws.com/doc/2014-05-01/"> + <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId> + <volumeId>vol-1a2b3c4d</volumeId> + <size>80</size> + <snapshotId/> + <availabilityZone>us-east-1a</availabilityZone> + <status>creating</status> + <createTime>YYYY-MM-DDTHH:MM:SS.000Z</createTime> + <volumeType>standard</volumeType> + <encrypted>true</encrypted> + </CreateVolumeResponse> + """ + + def test_create_volume(self): + self.set_http_response(status_code=200) + result = self.ec2.create_volume(80, 'us-east-1e', snapshot='snap-1a2b3c4d', + encrypted=True) + self.assert_request_parameters({ + 'Action': 'CreateVolume', + 'AvailabilityZone': 'us-east-1e', + 'Size': 80, + 'SnapshotId': 'snap-1a2b3c4d', + 'Encrypted': 'true'}, + ignore_params_values=['AWSAccessKeyId', 'SignatureMethod', + 'SignatureVersion', 'Timestamp', + 'Version']) + self.assertEqual(result.id, 'vol-1a2b3c4d') + self.assertTrue(result.encrypted) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/ec2/test_snapshot.py b/tests/unit/ec2/test_snapshot.py index 55dc4659..ba11f03d 100644 --- a/tests/unit/ec2/test_snapshot.py +++ b/tests/unit/ec2/test_snapshot.py @@ -28,12 +28,13 @@ class TestDescribeSnapshots(AWSMockServiceTestCase): <value>demo_db_14_backup</value> </item> </tagSet> + <encrypted>false</encrypted> </item> </snapshotSet> </DescribeSnapshotsResponse> """ - def test_cancel_spot_instance_requests(self): + def test_describe_snapshots(self): self.set_http_response(status_code=200) response = self.service_connection.get_all_snapshots(['snap-1a2b3c4d', 'snap-9f8e7d6c'], owner=['self', '111122223333'], diff --git a/tests/unit/ec2/test_volume.py b/tests/unit/ec2/test_volume.py index 7acc6181..81c98318 100644 --- a/tests/unit/ec2/test_volume.py +++ b/tests/unit/ec2/test_volume.py @@ -78,10 +78,11 @@ class VolumeTests(unittest.TestCase): retval = volume.startElement("not tagSet or attachmentSet", None, None) self.assertEqual(retval, None) - def check_that_attribute_has_been_set(self, name, value, attribute): + def check_that_attribute_has_been_set(self, name, value, attribute, obj_value=None): volume = Volume() volume.endElement(name, value, None) - self.assertEqual(getattr(volume, attribute), value) + expected_value = obj_value if obj_value is not None else value + self.assertEqual(getattr(volume, attribute), expected_value) def test_endElement_sets_correct_attributes_with_values(self): for arguments in [("volumeId", "some value", "id"), @@ -90,8 +91,9 @@ class VolumeTests(unittest.TestCase): ("size", 5, "size"), ("snapshotId", 1, "snapshot_id"), ("availabilityZone", "some zone", "zone"), - ("someName", "some value", "someName")]: - self.check_that_attribute_has_been_set(arguments[0], arguments[1], arguments[2]) + ("someName", "some value", "someName"), + ("encrypted", "true", "encrypted", True)]: + self.check_that_attribute_has_been_set(*arguments) def test_endElement_with_name_status_and_empty_string_value_doesnt_set_status(self): volume = Volume() diff --git a/tests/unit/provider/test_provider.py b/tests/unit/provider/test_provider.py index ece21215..d8949ca2 100644 --- a/tests/unit/provider/test_provider.py +++ b/tests/unit/provider/test_provider.py @@ -24,17 +24,25 @@ class TestProvider(unittest.TestCase): def setUp(self): self.environ = {} self.config = {} + self.shared_config = {} self.metadata_patch = mock.patch('boto.utils.get_instance_metadata') self.config_patch = mock.patch('boto.provider.config.get', self.get_config) self.has_config_patch = mock.patch('boto.provider.config.has_option', self.has_config) + self.config_object_patch = mock.patch.object( + provider.Config, 'get', self.get_shared_config) + self.has_config_object_patch = mock.patch.object( + provider.Config, 'has_option', self.has_shared_config) self.environ_patch = mock.patch('os.environ', self.environ) self.get_instance_metadata = self.metadata_patch.start() + self.get_instance_metadata.return_value = None self.config_patch.start() self.has_config_patch.start() + self.config_object_patch.start() + self.has_config_object_patch.start() self.environ_patch.start() @@ -42,6 +50,8 @@ class TestProvider(unittest.TestCase): self.metadata_patch.stop() self.config_patch.stop() self.has_config_patch.stop() + self.config_object_patch.stop() + self.has_config_object_patch.stop() self.environ_patch.stop() def has_config(self, section_name, key): @@ -57,6 +67,19 @@ class TestProvider(unittest.TestCase): except KeyError: return None + def has_shared_config(self, section_name, key): + try: + self.shared_config[section_name][key] + return True + except KeyError: + return False + + def get_shared_config(self, section_name, key): + try: + return self.shared_config[section_name][key] + except KeyError: + return None + def test_passed_in_values_are_used(self): p = provider.Provider('aws', 'access_key', 'secret_key', 'security_token') self.assertEqual(p.access_key, 'access_key') @@ -99,9 +122,23 @@ class TestProvider(unittest.TestCase): q = provider.Provider('aws', profile_name='dev') self.assertEqual(q.access_key, 'dev_access_key') self.assertEqual(q.secret_key, 'dev_secret_key') - r = provider.Provider('aws', profile_name='doesntexist') - self.assertEqual(r.access_key, 'default_access_key') - self.assertEqual(r.secret_key, 'default_secret_key') + + def test_config_missing_profile(self): + # None of these default profiles should be loaded! + self.shared_config = { + 'default': { + 'aws_access_key_id': 'shared_access_key', + 'aws_secret_access_key': 'shared_secret_key', + } + } + self.config = { + 'Credentials': { + 'aws_access_key_id': 'default_access_key', + 'aws_secret_access_key': 'default_secret_key' + } + } + with self.assertRaises(provider.ProfileNotFoundError): + provider.Provider('aws', profile_name='doesntexist') def test_config_values_are_used(self): self.config = { @@ -164,9 +201,27 @@ class TestProvider(unittest.TestCase): self.assertEqual(p.secret_key, 'secret_key') self.assertEqual(p.security_token, None) - def test_env_vars_beat_config_values(self): + def test_env_vars_beat_shared_creds_values(self): self.environ['AWS_ACCESS_KEY_ID'] = 'env_access_key' self.environ['AWS_SECRET_ACCESS_KEY'] = 'env_secret_key' + self.shared_config = { + 'default': { + 'aws_access_key_id': 'shared_access_key', + 'aws_secret_access_key': 'shared_secret_key', + } + } + p = provider.Provider('aws') + self.assertEqual(p.access_key, 'env_access_key') + self.assertEqual(p.secret_key, 'env_secret_key') + self.assertIsNone(p.security_token) + + def test_shared_creds_beat_config_values(self): + self.shared_config = { + 'default': { + 'aws_access_key_id': 'shared_access_key', + 'aws_secret_access_key': 'shared_secret_key', + } + } self.config = { 'Credentials': { 'aws_access_key_id': 'cfg_access_key', @@ -174,14 +229,70 @@ class TestProvider(unittest.TestCase): } } p = provider.Provider('aws') - self.assertEqual(p.access_key, 'env_access_key') - self.assertEqual(p.secret_key, 'env_secret_key') + self.assertEqual(p.access_key, 'shared_access_key') + self.assertEqual(p.secret_key, 'shared_secret_key') + self.assertIsNone(p.security_token) + + def test_shared_creds_profile_beats_defaults(self): + self.shared_config = { + 'default': { + 'aws_access_key_id': 'shared_access_key', + 'aws_secret_access_key': 'shared_secret_key', + }, + 'foo': { + 'aws_access_key_id': 'foo_access_key', + 'aws_secret_access_key': 'foo_secret_key', + } + } + p = provider.Provider('aws', profile_name='foo') + self.assertEqual(p.access_key, 'foo_access_key') + self.assertEqual(p.secret_key, 'foo_secret_key') + self.assertIsNone(p.security_token) + + def test_env_profile_loads_profile(self): + self.environ['AWS_PROFILE'] = 'foo' + self.shared_config = { + 'default': { + 'aws_access_key_id': 'shared_access_key', + 'aws_secret_access_key': 'shared_secret_key', + }, + 'foo': { + 'aws_access_key_id': 'shared_access_key_foo', + 'aws_secret_access_key': 'shared_secret_key_foo', + } + } + self.config = { + 'profile foo': { + 'aws_access_key_id': 'cfg_access_key_foo', + 'aws_secret_access_key': 'cfg_secret_key_foo', + }, + 'Credentials': { + 'aws_access_key_id': 'cfg_access_key', + 'aws_secret_access_key': 'cfg_secret_key', + } + } + p = provider.Provider('aws') + self.assertEqual(p.access_key, 'shared_access_key_foo') + self.assertEqual(p.secret_key, 'shared_secret_key_foo') + self.assertIsNone(p.security_token) + + self.shared_config = {} + p = provider.Provider('aws') + self.assertEqual(p.access_key, 'cfg_access_key_foo') + self.assertEqual(p.secret_key, 'cfg_secret_key_foo') self.assertIsNone(p.security_token) def test_env_vars_security_token_beats_config_values(self): self.environ['AWS_ACCESS_KEY_ID'] = 'env_access_key' self.environ['AWS_SECRET_ACCESS_KEY'] = 'env_secret_key' self.environ['AWS_SECURITY_TOKEN'] = 'env_security_token' + self.shared_config = { + 'default': { + 'aws_access_key_id': 'shared_access_key', + 'aws_secret_access_key': 'shared_secret_key', + 'aws_security_token': 'shared_security_token', + } + } self.config = { 'Credentials': { 'aws_access_key_id': 'cfg_access_key', @@ -194,6 +305,14 @@ class TestProvider(unittest.TestCase): self.assertEqual(p.secret_key, 'env_secret_key') self.assertEqual(p.security_token, 'env_security_token') + self.environ.clear() + p = provider.Provider('aws') + self.assertEqual(p.security_token, 'shared_security_token') + + self.shared_config.clear() + p = provider.Provider('aws') + self.assertEqual(p.security_token, 'cfg_security_token') + def test_metadata_server_credentials(self): self.get_instance_metadata.return_value = INSTANCE_CONFIG p = provider.Provider('aws') |