summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst6
-rw-r--r--boto/__init__.py2
-rw-r--r--boto/compat.py13
-rw-r--r--boto/dynamodb2/table.py2
-rw-r--r--boto/ec2/connection.py12
-rw-r--r--boto/ec2/elb/__init__.py2
-rw-r--r--boto/ec2/elb/loadbalancer.py1
-rw-r--r--boto/ec2/snapshot.py4
-rw-r--r--boto/ec2/volume.py4
-rw-r--r--boto/endpoints.json4
-rw-r--r--boto/opsworks/__init__.py2
-rw-r--r--boto/opsworks/layer1.py2
-rw-r--r--boto/provider.py73
-rw-r--r--boto/pyami/config.py17
-rw-r--r--boto/rds2/layer1.py2
-rw-r--r--boto/regioninfo.py4
-rw-r--r--docs/source/boto_config_tut.rst25
-rw-r--r--docs/source/cloudwatch_tut.rst2
-rw-r--r--docs/source/elb_tut.rst2
-rw-r--r--docs/source/index.rst1
-rw-r--r--docs/source/releasenotes/v2.29.0.rst25
-rw-r--r--setup.cfg4
-rw-r--r--tests/integration/opsworks/test_layer1.py16
-rwxr-xr-xtests/unit/ec2/test_connection.py154
-rw-r--r--tests/unit/ec2/test_snapshot.py3
-rw-r--r--tests/unit/ec2/test_volume.py10
-rw-r--r--tests/unit/provider/test_provider.py131
27 files changed, 469 insertions, 54 deletions
diff --git a/README.rst b/README.rst
index 7efd7fd0..6bc38e28 100644
--- a/README.rst
+++ b/README.rst
@@ -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`)
diff --git a/setup.cfg b/setup.cfg
index 5e409001..bda6d79c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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')