summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst4
-rwxr-xr-xbin/elbadmin15
-rw-r--r--boto/__init__.py2
-rw-r--r--boto/cloudformation/connection.py6
-rw-r--r--boto/ec2/elb/__init__.py117
-rw-r--r--boto/ec2/elb/listener.py12
-rw-r--r--boto/exception.py7
-rw-r--r--boto/gs/bucket.py42
-rwxr-xr-xboto/gs/lifecycle.py227
-rw-r--r--boto/rds/__init__.py137
-rw-r--r--boto/rds/dbinstance.py9
-rw-r--r--boto/rds/optiongroup.py404
-rw-r--r--boto/rds/statusinfo.py54
-rw-r--r--boto/route53/record.py6
-rw-r--r--boto/sqs/message.py2
-rwxr-xr-xboto/storage_uri.py15
-rw-r--r--boto/sts/connection.py55
-rw-r--r--boto/sts/credentials.py19
-rw-r--r--docs/source/cloudfront_tut.rst3
-rw-r--r--docs/source/index.rst1
-rw-r--r--docs/source/releasenotes/v2.9.8.rst35
-rw-r--r--tests/integration/ec2/elb/test_connection.py27
-rw-r--r--tests/integration/gs/test_basic.py49
-rw-r--r--tests/integration/route53/test_resourcerecordsets.py53
-rw-r--r--tests/integration/s3/mock_storage_service.py16
-rw-r--r--tests/integration/sts/test_session_token.py9
-rw-r--r--tests/unit/cloudformation/test_connection.py15
-rw-r--r--tests/unit/ec2/elb/test_listener.py101
-rw-r--r--tests/unit/rds/test_connection.py110
29 files changed, 1508 insertions, 44 deletions
diff --git a/README.rst b/README.rst
index 13c67d8f..cfeebef7 100644
--- a/README.rst
+++ b/README.rst
@@ -1,9 +1,9 @@
####
boto
####
-boto 2.9.7
+boto 2.9.8
-Released: 08-July-2013
+Released: 18-July-2013
.. image:: https://travis-ci.org/boto/boto.png?branch=develop
:target: https://travis-ci.org/boto/boto
diff --git a/bin/elbadmin b/bin/elbadmin
index e0aaf9d9..87dd2b14 100755
--- a/bin/elbadmin
+++ b/bin/elbadmin
@@ -108,7 +108,11 @@ def get(elb, name):
print
# Make map of all instance Id's to Name tags
- ec2 = boto.connect_ec2()
+ if not options.region:
+ ec2 = boto.connect_ec2()
+ else:
+ import boto.ec2.elb
+ ec2 = boto.ec2.connect_to_region(options.region)
instance_health = b.get_instance_health()
instances = [state.instance_id for state in instance_health]
@@ -236,6 +240,9 @@ if __name__ == "__main__":
parser.add_option("-l", "--listener",
help="Specify Listener in,out,proto",
action="append", default=[], dest="listeners")
+ parser.add_option("-r", "--region",
+ help="Region to connect to",
+ action="store", dest="region")
(options, args) = parser.parse_args()
@@ -243,7 +250,11 @@ if __name__ == "__main__":
parser.print_help()
sys.exit(1)
- elb = boto.connect_elb()
+ if not options.region:
+ elb = boto.connect_elb()
+ else:
+ import boto.ec2.elb
+ elb = boto.ec2.elb.connect_to_region(options.region)
print "%s" % (elb.region.endpoint)
diff --git a/boto/__init__.py b/boto/__init__.py
index 033d24b8..20c9000d 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.7'
+__version__ = '2.9.8'
Version = __version__ # for backware compatibility
UserAgent = 'Boto/%s (%s)' % (__version__, sys.platform)
diff --git a/boto/cloudformation/connection.py b/boto/cloudformation/connection.py
index 84c7680e..5970225f 100644
--- a/boto/cloudformation/connection.py
+++ b/boto/cloudformation/connection.py
@@ -362,3 +362,9 @@ class CloudFormationConnection(AWSQueryConnection):
" specified, only TemplateBody will be honored by the API")
return self.get_object('ValidateTemplate', params, Template,
verb="POST")
+
+ def cancel_update_stack(self, stack_name_or_id=None):
+ params = {}
+ if stack_name_or_id:
+ params['StackName'] = stack_name_or_id
+ return self.get_status('CancelUpdateStack', params)
diff --git a/boto/ec2/elb/__init__.py b/boto/ec2/elb/__init__.py
index c5e71b9f..ed9aaeaa 100644
--- a/boto/ec2/elb/__init__.py
+++ b/boto/ec2/elb/__init__.py
@@ -137,8 +137,8 @@ class ELBConnection(AWSQueryConnection):
return self.get_list('DescribeLoadBalancers', params,
[('member', LoadBalancer)])
- def create_load_balancer(self, name, zones, listeners, subnets=None,
- security_groups=None, scheme='internet-facing'):
+ def create_load_balancer(self, name, zones, listeners=None, subnets=None,
+ security_groups=None, scheme='internet-facing', complex_listeners=None):
"""
Create a new load balancer for your account. By default the load
balancer will be created in EC2. To create a load balancer inside a
@@ -157,7 +157,7 @@ class ELBConnection(AWSQueryConnection):
(LoadBalancerPortNumber, InstancePortNumber, Protocol,
[SSLCertificateId]) where LoadBalancerPortNumber and
InstancePortNumber are integer values between 1 and 65535,
- Protocol is a string containing either 'TCP', 'HTTP' or
+ Protocol is a string containing either 'TCP', 'SSL', HTTP', or
'HTTPS'; SSLCertificateID is the ARN of a AWS AIM
certificate, and must be specified when doing HTTPS.
@@ -182,20 +182,54 @@ class ELBConnection(AWSQueryConnection):
This option is only available for LoadBalancers attached
to an Amazon VPC.
+ :type complex_listeners: List of tuples
+ :param complex_listeners: Each tuple contains four or five values,
+ (LoadBalancerPortNumber, InstancePortNumber, Protocol, InstanceProtocol,
+ SSLCertificateId).
+
+ Where;
+ - LoadBalancerPortNumber and InstancePortNumber are integer
+ values between 1 and 65535.
+ - Protocol and InstanceProtocol is a string containing either 'TCP',
+ 'SSL', 'HTTP', or 'HTTPS'
+ - SSLCertificateId is the ARN of an SSL certificate loaded into
+ AWS IAM
+
:rtype: :class:`boto.ec2.elb.loadbalancer.LoadBalancer`
:return: The newly created
:class:`boto.ec2.elb.loadbalancer.LoadBalancer`
"""
+ if not listeners and not complex_listeners:
+ # Must specify one of the two options
+ return None
+
params = {'LoadBalancerName': name,
'Scheme': scheme}
- for index, listener in enumerate(listeners):
- i = index + 1
- protocol = listener[2].upper()
- params['Listeners.member.%d.LoadBalancerPort' % i] = listener[0]
- params['Listeners.member.%d.InstancePort' % i] = listener[1]
- params['Listeners.member.%d.Protocol' % i] = listener[2]
- if protocol == 'HTTPS' or protocol == 'SSL':
- params['Listeners.member.%d.SSLCertificateId' % i] = listener[3]
+
+ # Handle legacy listeners
+ if listeners:
+ for index, listener in enumerate(listeners):
+ i = index + 1
+ protocol = listener[2].upper()
+ params['Listeners.member.%d.LoadBalancerPort' % i] = listener[0]
+ params['Listeners.member.%d.InstancePort' % i] = listener[1]
+ params['Listeners.member.%d.Protocol' % i] = listener[2]
+ if protocol == 'HTTPS' or protocol == 'SSL':
+ params['Listeners.member.%d.SSLCertificateId' % i] = listener[3]
+
+ # Handle the full listeners
+ if complex_listeners:
+ for index, listener in enumerate(complex_listeners):
+ i = index + 1
+ protocol = listener[2].upper()
+ InstanceProtocol = listener[3].upper()
+ params['Listeners.member.%d.LoadBalancerPort' % i] = listener[0]
+ params['Listeners.member.%d.InstancePort' % i] = listener[1]
+ params['Listeners.member.%d.Protocol' % i] = listener[2]
+ params['Listeners.member.%d.InstanceProtocol' % i] = listener[3]
+ if protocol == 'HTTPS' or protocol == 'SSL':
+ params['Listeners.member.%d.SSLCertificateId' % i] = listener[4]
+
if zones:
self.build_list_params(params, zones, 'AvailabilityZones.member.%d')
@@ -215,7 +249,7 @@ class ELBConnection(AWSQueryConnection):
load_balancer.security_groups = security_groups
return load_balancer
- def create_load_balancer_listeners(self, name, listeners):
+ def create_load_balancer_listeners(self, name, listeners=None, complex_listeners=None):
"""
Creates a Listener (or group of listeners) for an existing
Load Balancer
@@ -224,26 +258,59 @@ class ELBConnection(AWSQueryConnection):
:param name: The name of the load balancer to create the listeners for
:type listeners: List of tuples
- :param listeners: Each tuple contains three values,
+ :param listeners: Each tuple contains three or four values,
(LoadBalancerPortNumber, InstancePortNumber, Protocol,
[SSLCertificateId]) where LoadBalancerPortNumber and
InstancePortNumber are integer values between 1 and 65535,
- Protocol is a string containing either 'TCP', 'HTTP',
- 'HTTPS', or 'SSL'; SSLCertificateID is the ARN of a AWS
- AIM certificate, and must be specified when doing HTTPS or
- SSL.
+ Protocol is a string containing either 'TCP', 'SSL', HTTP', or
+ 'HTTPS'; SSLCertificateID is the ARN of a AWS AIM
+ certificate, and must be specified when doing HTTPS.
+
+ :type complex_listeners: List of tuples
+ :param complex_listeners: Each tuple contains four or five values,
+ (LoadBalancerPortNumber, InstancePortNumber, Protocol, InstanceProtocol,
+ SSLCertificateId).
+
+ Where;
+ - LoadBalancerPortNumber and InstancePortNumber are integer
+ values between 1 and 65535.
+ - Protocol and InstanceProtocol is a string containing either 'TCP',
+ 'SSL', 'HTTP', or 'HTTPS'
+ - SSLCertificateId is the ARN of an SSL certificate loaded into
+ AWS IAM
:return: The status of the request
"""
+ if not listeners and not complex_listeners:
+ # Must specify one of the two options
+ return None
+
params = {'LoadBalancerName': name}
- for index, listener in enumerate(listeners):
- i = index + 1
- protocol = listener[2].upper()
- params['Listeners.member.%d.LoadBalancerPort' % i] = listener[0]
- params['Listeners.member.%d.InstancePort' % i] = listener[1]
- params['Listeners.member.%d.Protocol' % i] = listener[2]
- if protocol == 'HTTPS' or protocol == 'SSL':
- params['Listeners.member.%d.SSLCertificateId' % i] = listener[3]
+
+ # Handle the simple listeners
+ if listeners:
+ for index, listener in enumerate(listeners):
+ i = index + 1
+ protocol = listener[2].upper()
+ params['Listeners.member.%d.LoadBalancerPort' % i] = listener[0]
+ params['Listeners.member.%d.InstancePort' % i] = listener[1]
+ params['Listeners.member.%d.Protocol' % i] = listener[2]
+ if protocol == 'HTTPS' or protocol == 'SSL':
+ params['Listeners.member.%d.SSLCertificateId' % i] = listener[3]
+
+ # Handle the full listeners
+ if complex_listeners:
+ for index, listener in enumerate(complex_listeners):
+ i = index + 1
+ protocol = listener[2].upper()
+ InstanceProtocol = listener[3].upper()
+ params['Listeners.member.%d.LoadBalancerPort' % i] = listener[0]
+ params['Listeners.member.%d.InstancePort' % i] = listener[1]
+ params['Listeners.member.%d.Protocol' % i] = listener[2]
+ params['Listeners.member.%d.InstanceProtocol' % i] = listener[3]
+ if protocol == 'HTTPS' or protocol == 'SSL':
+ params['Listeners.member.%d.SSLCertificateId' % i] = listener[4]
+
return self.get_status('CreateLoadBalancerListeners', params)
def delete_load_balancer(self, name):
diff --git a/boto/ec2/elb/listener.py b/boto/ec2/elb/listener.py
index a50b02cd..cf26c41c 100644
--- a/boto/ec2/elb/listener.py
+++ b/boto/ec2/elb/listener.py
@@ -30,16 +30,19 @@ class Listener(object):
"""
def __init__(self, load_balancer=None, load_balancer_port=0,
- instance_port=0, protocol='', ssl_certificate_id=None):
+ instance_port=0, protocol='', ssl_certificate_id=None, instance_protocol=None):
self.load_balancer = load_balancer
self.load_balancer_port = load_balancer_port
self.instance_port = instance_port
self.protocol = protocol
+ self.instance_protocol = instance_protocol
self.ssl_certificate_id = ssl_certificate_id
self.policy_names = ListElement()
def __repr__(self):
r = "(%d, %d, '%s'" % (self.load_balancer_port, self.instance_port, self.protocol)
+ if self.instance_protocol:
+ r += ", '%s'" % self.instance_protocol
if self.ssl_certificate_id:
r += ', %s' % (self.ssl_certificate_id)
r += ')'
@@ -55,6 +58,8 @@ class Listener(object):
self.load_balancer_port = int(value)
elif name == 'InstancePort':
self.instance_port = int(value)
+ elif name == 'InstanceProtocol':
+ self.instance_protocol = value
elif name == 'Protocol':
self.protocol = value
elif name == 'SSLCertificateId':
@@ -65,6 +70,9 @@ class Listener(object):
def get_tuple(self):
return self.load_balancer_port, self.instance_port, self.protocol
+ def get_complex_tuple(self):
+ return self.load_balancer_port, self.instance_port, self.protocol, self.instance_protocol
+
def __getitem__(self, key):
if key == 0:
return self.load_balancer_port
@@ -72,4 +80,6 @@ class Listener(object):
return self.instance_port
if key == 2:
return self.protocol
+ if key == 4:
+ return self.instance_protocol
raise KeyError
diff --git a/boto/exception.py b/boto/exception.py
index 64618368..0c871b37 100644
--- a/boto/exception.py
+++ b/boto/exception.py
@@ -409,6 +409,13 @@ class NoAuthHandlerFound(Exception):
"""Is raised when no auth handlers were found ready to authenticate."""
pass
+class InvalidLifecycleConfigError(Exception):
+ """Exception raised when GCS lifecycle configuration XML is invalid."""
+
+ def __init__(self, message):
+ Exception.__init__(self, message)
+ self.message = message
+
# Enum class for resumable upload failure disposition.
class ResumableTransferDisposition(object):
# START_OVER means an attempt to resume an existing transfer failed,
diff --git a/boto/gs/bucket.py b/boto/gs/bucket.py
index 96c2bdc3..a8ced49a 100644
--- a/boto/gs/bucket.py
+++ b/boto/gs/bucket.py
@@ -30,6 +30,7 @@ from boto.gs.acl import ACL, CannedACLStrings
from boto.gs.acl import SupportedPermissions as GSPermissions
from boto.gs.bucketlistresultset import VersionedBucketListResultSet
from boto.gs.cors import Cors
+from boto.gs.lifecycle import LifecycleConfig
from boto.gs.key import Key as GSKey
from boto.s3.acl import Policy
from boto.s3.bucket import Bucket as S3Bucket
@@ -39,6 +40,7 @@ from boto.utils import get_utf8_value
DEF_OBJ_ACL = 'defaultObjectAcl'
STANDARD_ACL = 'acl'
CORS_ARG = 'cors'
+LIFECYCLE_ARG = 'lifecycle'
class Bucket(S3Bucket):
"""Represents a Google Cloud Storage bucket."""
@@ -918,3 +920,43 @@ class Bucket(S3Bucket):
else:
req_body = self.VersioningBody % ('Suspended')
self.set_subresource('versioning', req_body, headers=headers)
+
+ def get_lifecycle_config(self, headers=None):
+ """
+ Returns the current lifecycle configuration on the bucket.
+
+ :rtype: :class:`boto.gs.lifecycle.LifecycleConfig`
+ :returns: A LifecycleConfig object that describes all current
+ lifecycle rules in effect for the bucket.
+ """
+ response = self.connection.make_request('GET', self.name,
+ query_args=LIFECYCLE_ARG, headers=headers)
+ body = response.read()
+ boto.log.debug(body)
+ if response.status == 200:
+ lifecycle_config = LifecycleConfig()
+ h = handler.XmlHandler(lifecycle_config, self)
+ xml.sax.parseString(body, h)
+ return lifecycle_config
+ else:
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
+
+ def configure_lifecycle(self, lifecycle_config, headers=None):
+ """
+ Configure lifecycle for this bucket.
+
+ :type lifecycle_config: :class:`boto.gs.lifecycle.LifecycleConfig`
+ :param lifecycle_config: The lifecycle configuration you want
+ to configure for this bucket.
+ """
+ xml = lifecycle_config.to_xml()
+ response = self.connection.make_request(
+ 'PUT', get_utf8_value(self.name), data=get_utf8_value(xml),
+ query_args=LIFECYCLE_ARG, headers=headers)
+ body = response.read()
+ if response.status == 200:
+ return True
+ else:
+ raise self.connection.provider.storage_response_error(
+ response.status, response.reason, body)
diff --git a/boto/gs/lifecycle.py b/boto/gs/lifecycle.py
new file mode 100755
index 00000000..65f7d65d
--- /dev/null
+++ b/boto/gs/lifecycle.py
@@ -0,0 +1,227 @@
+# Copyright 2013 Google Inc.
+#
+# 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 InvalidLifecycleConfigError
+
+# Relevant tags for the lifecycle configuration XML document.
+LIFECYCLE_CONFIG = 'LifecycleConfiguration'
+RULE = 'Rule'
+ACTION = 'Action'
+DELETE = 'Delete'
+CONDITION = 'Condition'
+AGE = 'Age'
+CREATED_BEFORE = 'CreatedBefore'
+NUM_NEWER_VERSIONS = 'NumberOfNewerVersions'
+IS_LIVE = 'IsLive'
+
+# List of all action elements.
+LEGAL_ACTIONS = [DELETE]
+# List of all action parameter elements.
+LEGAL_ACTION_PARAMS = []
+# List of all condition elements.
+LEGAL_CONDITIONS = [AGE, CREATED_BEFORE, NUM_NEWER_VERSIONS, IS_LIVE]
+# Dictionary mapping actions to supported action parameters for each action.
+LEGAL_ACTION_ACTION_PARAMS = {
+ DELETE: [],
+}
+
+class Rule(object):
+ """
+ A lifecycle rule for a bucket.
+
+ :ivar action: Action to be taken.
+
+ :ivar action_params: A dictionary of action specific parameters. Each item
+ in the dictionary represents the name and value of an action parameter.
+
+ :ivar conditions: A dictionary of conditions that specify when the action
+ should be taken. Each item in the dictionary represents the name and value
+ of a condition.
+ """
+
+ def __init__(self, action=None, action_params=None, conditions=None):
+ self.action = action
+ self.action_params = action_params or {}
+ self.conditions = conditions or {}
+
+ # Name of the current enclosing tag (used to validate the schema).
+ self.current_tag = RULE
+
+ def validateStartTag(self, tag, parent):
+ """Verify parent of the start tag."""
+ if self.current_tag != parent:
+ raise InvalidLifecycleConfigError(
+ 'Invalid tag %s found inside %s tag' % (tag, self.current_tag))
+
+ def validateEndTag(self, tag):
+ """Verify end tag against the start tag."""
+ if tag != self.current_tag:
+ raise InvalidLifecycleConfigError(
+ 'Mismatched start and end tags (%s/%s)' %
+ (self.current_tag, tag))
+
+ def startElement(self, name, attrs, connection):
+ if name == ACTION:
+ self.validateStartTag(name, RULE)
+ elif name in LEGAL_ACTIONS:
+ self.validateStartTag(name, ACTION)
+ # Verify there is only one action tag in the rule.
+ if self.action is not None:
+ raise InvalidLifecycleConfigError(
+ 'Only one action tag is allowed in each rule')
+ self.action = name
+ elif name in LEGAL_ACTION_PARAMS:
+ # Make sure this tag is found in an action tag.
+ if self.current_tag not in LEGAL_ACTIONS:
+ raise InvalidLifecycleConfigError(
+ 'Tag %s found outside of action' % name)
+ # Make sure this tag is allowed for the current action tag.
+ if name not in LEGAL_ACTION_ACTION_PARAMS[self.action]:
+ raise InvalidLifecycleConfigError(
+ 'Tag %s not allowed in action %s' % (name, self.action))
+ elif name == CONDITION:
+ self.validateStartTag(name, RULE)
+ elif name in LEGAL_CONDITIONS:
+ self.validateStartTag(name, CONDITION)
+ # Verify there is no duplicate conditions.
+ if name in self.conditions:
+ raise InvalidLifecycleConfigError(
+ 'Found duplicate conditions %s' % name)
+ else:
+ raise InvalidLifecycleConfigError('Unsupported tag ' + name)
+ self.current_tag = name
+
+ def endElement(self, name, value, connection):
+ self.validateEndTag(name)
+ if name == RULE:
+ # We have to validate the rule after it is fully populated because
+ # the action and condition elements could be in any order.
+ self.validate()
+ elif name == ACTION:
+ self.current_tag = RULE
+ elif name in LEGAL_ACTIONS:
+ self.current_tag = ACTION
+ elif name in LEGAL_ACTION_PARAMS:
+ self.current_tag = self.action
+ # Add the action parameter name and value to the dictionary.
+ self.action_params[name] = value.strip()
+ elif name == CONDITION:
+ self.current_tag = RULE
+ elif name in LEGAL_CONDITIONS:
+ self.current_tag = CONDITION
+ # Add the condition name and value to the dictionary.
+ self.conditions[name] = value.strip()
+ else:
+ raise InvalidLifecycleConfigError('Unsupported end tag ' + name)
+
+ def validate(self):
+ """Validate the rule."""
+ if not self.action:
+ raise InvalidLifecycleConfigError(
+ 'No action was specified in the rule')
+ if not self.conditions:
+ raise InvalidLifecycleConfigError(
+ 'No condition was specified for action %s' % self.action)
+
+ def to_xml(self):
+ """Convert the rule into XML string representation."""
+ s = '<' + RULE + '>'
+ s += '<' + ACTION + '>'
+ if self.action_params:
+ s += '<' + self.action + '>'
+ for param in LEGAL_ACTION_PARAMS:
+ if param in self.action_params:
+ s += ('<' + param + '>' + self.action_params[param] + '</'
+ + param + '>')
+ s += '</' + self.action + '>'
+ else:
+ s += '<' + self.action + '/>'
+ s += '</' + ACTION + '>'
+ s += '<' + CONDITION + '>'
+ for condition in LEGAL_CONDITIONS:
+ if condition in self.conditions:
+ s += ('<' + condition + '>' + self.conditions[condition] + '</'
+ + condition + '>')
+ s += '</' + CONDITION + '>'
+ s += '</' + RULE + '>'
+ return s
+
+class LifecycleConfig(list):
+ """
+ A container of rules associated with a lifecycle configuration.
+ """
+
+ def __init__(self):
+ # Track if root tag has been seen.
+ self.has_root_tag = False
+
+ def startElement(self, name, attrs, connection):
+ if name == LIFECYCLE_CONFIG:
+ if self.has_root_tag:
+ raise InvalidLifecycleConfigError(
+ 'Only one root tag is allowed in the XML')
+ self.has_root_tag = True
+ elif name == RULE:
+ if not self.has_root_tag:
+ raise InvalidLifecycleConfigError('Invalid root tag ' + name)
+ rule = Rule()
+ self.append(rule)
+ return rule
+ else:
+ raise InvalidLifecycleConfigError('Unsupported tag ' + name)
+
+ def endElement(self, name, value, connection):
+ if name == LIFECYCLE_CONFIG:
+ pass
+ else:
+ raise InvalidLifecycleConfigError('Unsupported end tag ' + name)
+
+ def to_xml(self):
+ """Convert LifecycleConfig object into XML string representation."""
+ s = '<?xml version="1.0" encoding="UTF-8"?>'
+ s += '<' + LIFECYCLE_CONFIG + '>'
+ for rule in self:
+ s += rule.to_xml()
+ s += '</' + LIFECYCLE_CONFIG + '>'
+ return s
+
+ def add_rule(self, action, action_params, conditions):
+ """
+ Add a rule to this Lifecycle configuration. This only adds the rule to
+ the local copy. To install the new rule(s) on the bucket, you need to
+ pass this Lifecycle config object to the configure_lifecycle method of
+ the Bucket object.
+
+ :type action: str
+ :param action: Action to be taken.
+
+ :type action_params: dict
+ :param action_params: A dictionary of action specific parameters. Each
+ item in the dictionary represents the name and value of an action
+ parameter.
+
+ :type conditions: dict
+ :param conditions: A dictionary of conditions that specify when the
+ action should be taken. Each item in the dictionary represents the name
+ and value of a condition.
+ """
+ rule = Rule(action, action_params, conditions)
+ self.append(rule)
diff --git a/boto/rds/__init__.py b/boto/rds/__init__.py
index 5a4305f2..ec3305cf 100644
--- a/boto/rds/__init__.py
+++ b/boto/rds/__init__.py
@@ -24,6 +24,7 @@ import urllib
from boto.connection import AWSQueryConnection
from boto.rds.dbinstance import DBInstance
from boto.rds.dbsecuritygroup import DBSecurityGroup
+from boto.rds.optiongroup import OptionGroup, OptionGroupOption
from boto.rds.parametergroup import ParameterGroup
from boto.rds.dbsnapshot import DBSnapshot
from boto.rds.event import Event
@@ -82,7 +83,7 @@ class RDSConnection(AWSQueryConnection):
DefaultRegionName = 'us-east-1'
DefaultRegionEndpoint = 'rds.amazonaws.com'
- APIVersion = '2012-09-17'
+ APIVersion = '2013-05-15'
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
is_secure=True, port=None, proxy=None, proxy_port=None,
@@ -1197,3 +1198,137 @@ class RDSConnection(AWSQueryConnection):
if marker:
params['Marker'] = marker
return self.get_list('DescribeEvents', params, [('Event', Event)])
+
+ def create_option_group(self, name, engine_name, major_engine_version,
+ description=None):
+ """
+ Create a new option group for your account.
+ This will create the option group within the region you
+ are currently connected to.
+
+ :type name: string
+ :param name: The name of the new option group
+
+ :type engine_name: string
+ :param engine_name: Specifies the name of the engine that this option
+ group should be associated with.
+
+ :type major_engine_version: string
+ :param major_engine_version: Specifies the major version of the engine
+ that this option group should be
+ associated with.
+
+ :type description: string
+ :param description: The description of the new option group
+
+ :rtype: :class:`boto.rds.optiongroup.OptionGroup`
+ :return: The newly created OptionGroup
+ """
+ params = {
+ 'OptionGroupName': name,
+ 'EngineName': engine_name,
+ 'MajorEngineVersion': major_engine_version,
+ 'OptionGroupDescription': description,
+ }
+ group = self.get_object('CreateOptionGroup', params, OptionGroup)
+ group.name = name
+ group.engine_name = engine_name
+ group.major_engine_version = major_engine_version
+ group.description = description
+ return group
+
+ def delete_option_group(self, name):
+ """
+ Delete an OptionGroup from your account.
+
+ :type key_name: string
+ :param key_name: The name of the OptionGroup to delete
+ """
+ params = {'OptionGroupName': name}
+ return self.get_status('DeleteOptionGroup', params)
+
+ def describe_option_groups(self, name=None, engine_name=None,
+ major_engine_version=None, max_records=100,
+ marker=None):
+ """
+ Describes the available option groups.
+
+ :type name: str
+ :param name: The name of the option group to describe. Cannot be
+ supplied together with engine_name or major_engine_version.
+
+ :type engine_name: str
+ :param engine_name: Filters the list of option groups to only include
+ groups associated with a specific database engine.
+
+ :type major_engine_version: datetime
+ :param major_engine_version: Filters the list of option groups to only
+ include groups associated with a specific
+ database engine version. If specified, then
+ engine_name must also be specified.
+
+ :type max_records: int
+ :param max_records: The maximum number of records to be returned.
+ If more results are available, a MoreToken will
+ be returned in the response that can be used to
+ retrieve additional records. Default is 100.
+
+ :type marker: str
+ :param marker: The marker provided by a previous request.
+
+ :rtype: list
+ :return: A list of class:`boto.rds.optiongroup.OptionGroup`
+ """
+ params = {}
+ if name:
+ params['OptionGroupName'] = name
+ elif engine_name and major_engine_version:
+ params['EngineName'] = engine_name
+ params['MajorEngineVersion'] = major_engine_version
+ if max_records:
+ params['MaxRecords'] = int(max_records)
+ if marker:
+ params['Marker'] = marker
+ return self.get_list('DescribeOptionGroups', params, [
+ ('OptionGroup', OptionGroup)
+ ])
+
+ def describe_option_group_options(self, engine_name=None,
+ major_engine_version=None, max_records=100,
+ marker=None):
+ """
+ Describes the available option group options.
+
+ :type engine_name: str
+ :param engine_name: Filters the list of option groups to only include
+ groups associated with a specific database engine.
+
+ :type major_engine_version: datetime
+ :param major_engine_version: Filters the list of option groups to only
+ include groups associated with a specific
+ database engine version. If specified, then
+ engine_name must also be specified.
+
+ :type max_records: int
+ :param max_records: The maximum number of records to be returned.
+ If more results are available, a MoreToken will
+ be returned in the response that can be used to
+ retrieve additional records. Default is 100.
+
+ :type marker: str
+ :param marker: The marker provided by a previous request.
+
+ :rtype: list
+ :return: A list of class:`boto.rds.optiongroup.Option`
+ """
+ params = {}
+ if engine_name and major_engine_version:
+ params['EngineName'] = engine_name
+ params['MajorEngineVersion'] = major_engine_version
+ if max_records:
+ params['MaxRecords'] = int(max_records)
+ if marker:
+ params['Marker'] = marker
+ return self.get_list('DescribeOptionGroupOptions', params, [
+ ('OptionGroupOptions', OptionGroupOption)
+ ]) \ No newline at end of file
diff --git a/boto/rds/dbinstance.py b/boto/rds/dbinstance.py
index 7002791c..e6b51b76 100644
--- a/boto/rds/dbinstance.py
+++ b/boto/rds/dbinstance.py
@@ -21,6 +21,7 @@
from boto.rds.dbsecuritygroup import DBSecurityGroup
from boto.rds.parametergroup import ParameterGroup
+from boto.rds.statusinfo import StatusInfo
from boto.resultset import ResultSet
@@ -69,6 +70,8 @@ class DBInstance(object):
are pending. Specific changes are identified by subelements.
:ivar read_replica_dbinstance_identifiers: List of read replicas
associated with this DB instance.
+ :ivar status_infos: The status of a Read Replica. If the instance is not a
+ for a read replica, this will be blank.
"""
def __init__(self, connection=None, id=None):
@@ -95,6 +98,7 @@ class DBInstance(object):
self._in_endpoint = False
self._port = None
self._address = None
+ self.status_infos = None
def __repr__(self):
return 'DBInstance:%s' % self.id
@@ -117,6 +121,11 @@ class DBInstance(object):
self.read_replica_dbinstance_identifiers = \
ReadReplicaDBInstanceIdentifiers()
return self.read_replica_dbinstance_identifiers
+ elif name == 'StatusInfos':
+ self.status_infos = ResultSet([
+ ('DBInstanceStatusInfo', StatusInfo)
+ ])
+ return self.status_infos
return None
def endElement(self, name, value, connection):
diff --git a/boto/rds/optiongroup.py b/boto/rds/optiongroup.py
new file mode 100644
index 00000000..8968b6ca
--- /dev/null
+++ b/boto/rds/optiongroup.py
@@ -0,0 +1,404 @@
+# 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.
+
+"""
+Represents an OptionGroup
+"""
+
+from boto.rds.dbsecuritygroup import DBSecurityGroup
+from boto.resultset import ResultSet
+
+
+class OptionGroup(object):
+ """
+ Represents an RDS option group
+
+ Properties reference available from the AWS documentation at
+ http://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_OptionGroup.html
+
+ :ivar connection: :py:class:`boto.rds.RDSConnection` associated with the
+ current object
+ :ivar name: Name of the option group
+ :ivar description: The description of the option group
+ :ivar engine_name: The name of the database engine to use
+ :ivar major_engine_version: The major version number of the engine to use
+ :ivar allow_both_vpc_and_nonvpc: Indicates whether this option group can be
+ applied to both VPC and non-VPC instances.
+ The value ``True`` indicates the option
+ group can be applied to both VPC and
+ non-VPC instances.
+ :ivar vpc_id: If AllowsVpcAndNonVpcInstanceMemberships is 'false', this
+ field is blank. If AllowsVpcAndNonVpcInstanceMemberships is
+ ``True`` and this field is blank, then this option group can
+ be applied to both VPC and non-VPC instances. If this field
+ contains a value, then this option group can only be applied
+ to instances that are in the VPC indicated by this field.
+ :ivar options: The list of :py:class:`boto.rds.optiongroup.Option` objects
+ associated with the group
+ """
+ def __init__(self, connection=None, name=None, engine_name=None,
+ major_engine_version=None, description=None,
+ allow_both_vpc_and_nonvpc=False, vpc_id=None):
+ self.name = name
+ self.engine_name = engine_name
+ self.major_engine_version = major_engine_version
+ self.description = description
+ self.allow_both_vpc_and_nonvpc = allow_both_vpc_and_nonvpc
+ self.vpc_id = vpc_id
+ self.options = []
+
+ def __repr__(self):
+ return 'OptionGroup:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ if name == 'Options':
+ self.options = ResultSet([
+ ('Options', Option)
+ ])
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'OptionGroupName':
+ self.name = value
+ elif name == 'EngineName':
+ self.engine_name = value
+ elif name == 'MajorEngineVersion':
+ self.major_engine_version = value
+ elif name == 'OptionGroupDescription':
+ self.description = value
+ elif name == 'AllowsVpcAndNonVpcInstanceMemberships':
+ if value.lower() == 'true':
+ self.allow_both_vpc_and_nonvpc = True
+ else:
+ self.allow_both_vpc_and_nonvpc = False
+ elif name == 'VpcId':
+ self.vpc_id = value
+ else:
+ setattr(self, name, value)
+
+ def delete(self):
+ return self.connection.delete_option_group(self.name)
+
+
+class Option(object):
+ """
+ Describes a Option for use in an OptionGroup
+
+ :ivar name: The name of the option
+ :ivar description: The description of the option.
+ :ivar permanent: Indicate if this option is permanent.
+ :ivar persistent: Indicate if this option is persistent.
+ :ivar port: If required, the port configured for this option to use.
+ :ivar settings: The option settings for this option.
+ :ivar db_security_groups: If the option requires access to a port, then
+ this DB Security Group allows access to the port.
+ :ivar vpc_security_groups: If the option requires access to a port, then
+ this VPC Security Group allows access to the
+ port.
+ """
+ def __init__(self, name=None, description=None, permanent=False,
+ persistent=False, port=None, settings=None,
+ db_security_groups=None, vpc_security_groups=None):
+ self.name = name
+ self.description = description
+ self.permanent = permanent
+ self.persistent = persistent
+ self.port = port
+ self.settings = settings
+ self.db_security_groups = db_security_groups
+ self.vpc_security_groups = vpc_security_groups
+
+ if self.settings is None:
+ self.settings = []
+
+ if self.db_security_groups is None:
+ self.db_security_groups = []
+
+ if self.vpc_security_groups is None:
+ self.vpc_security_groups = []
+
+ def __repr__(self):
+ return 'Option:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ if name == 'OptionSettings':
+ self.settings = ResultSet([
+ ('OptionSettings', OptionSetting)
+ ])
+ elif name == 'DBSecurityGroupMemberships':
+ self.db_security_groups = ResultSet([
+ ('DBSecurityGroupMemberships', DBSecurityGroup)
+ ])
+ elif name == 'VpcSecurityGroupMemberships':
+ self.vpc_security_groups = ResultSet([
+ ('VpcSecurityGroupMemberships', VpcSecurityGroup)
+ ])
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'OptionName':
+ self.name = value
+ elif name == 'OptionDescription':
+ self.description = value
+ elif name == 'Permanent':
+ if value.lower() == 'true':
+ self.permenant = True
+ else:
+ self.permenant = False
+ elif name == 'Persistent':
+ if value.lower() == 'true':
+ self.persistent = True
+ else:
+ self.persistent = False
+ elif name == 'Port':
+ self.port = int(value)
+ else:
+ setattr(self, name, value)
+
+
+class OptionSetting(object):
+ """
+ Describes a OptionSetting for use in an Option
+
+ :ivar name: The name of the option that has settings that you can set.
+ :ivar description: The description of the option setting.
+ :ivar value: The current value of the option setting.
+ :ivar default_value: The default value of the option setting.
+ :ivar allowed_values: The allowed values of the option setting.
+ :ivar data_type: The data type of the option setting.
+ :ivar apply_type: The DB engine specific parameter type.
+ :ivar is_modifiable: A Boolean value that, when true, indicates the option
+ setting can be modified from the default.
+ :ivar is_collection: Indicates if the option setting is part of a
+ collection.
+ """
+
+ def __init__(self, name=None, description=None, value=None,
+ default_value=False, allowed_values=None, data_type=None,
+ apply_type=None, is_modifiable=False, is_collection=False):
+ self.name = name
+ self.description = description
+ self.value = value
+ self.default_value = default_value
+ self.allowed_values = allowed_values
+ self.data_type = data_type
+ self.apply_type = apply_type
+ self.is_modifiable = is_modifiable
+ self.is_collection = is_collection
+
+ def __repr__(self):
+ return 'OptionSetting:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Name':
+ self.name = value
+ elif name == 'Description':
+ self.description = value
+ elif name == 'Value':
+ self.value = value
+ elif name == 'DefaultValue':
+ self.default_value = value
+ elif name == 'AllowedValues':
+ self.allowed_values = value
+ elif name == 'DataType':
+ self.data_type = value
+ elif name == 'ApplyType':
+ self.apply_type = value
+ elif name == 'IsModifiable':
+ if value.lower() == 'true':
+ self.is_modifiable = True
+ else:
+ self.is_modifiable = False
+ elif name == 'IsCollection':
+ if value.lower() == 'true':
+ self.is_collection = True
+ else:
+ self.is_collection = False
+ else:
+ setattr(self, name, value)
+
+
+class VpcSecurityGroup(object):
+ """
+ Describes a VPC security group for use in a OptionGroup
+ """
+ def __init__(self, vpc_id=None, status=None):
+ self.vpc_id = vpc_id
+ self.status = status
+
+ def __repr__(self):
+ return 'VpcSecurityGroup:%s' % self.vpc_id
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'VpcSecurityGroupId':
+ self.vpc_id = value
+ elif name == 'Status':
+ self.status = value
+ else:
+ setattr(self, name, value)
+
+
+class OptionGroupOption(object):
+ """
+ Describes a OptionGroupOption for use in an OptionGroup
+
+ :ivar name: The name of the option
+ :ivar description: The description of the option.
+ :ivar engine_name: Engine name that this option can be applied to.
+ :ivar major_engine_version: Indicates the major engine version that the
+ option is available for.
+ :ivar min_minor_engine_version: The minimum required engine version for the
+ option to be applied.
+ :ivar permanent: Indicate if this option is permanent.
+ :ivar persistent: Indicate if this option is persistent.
+ :ivar port_required: Specifies whether the option requires a port.
+ :ivar default_port: If the option requires a port, specifies the default
+ port for the option.
+ :ivar settings: The option settings for this option.
+ :ivar depends_on: List of all options that are prerequisites for this
+ option.
+ """
+ def __init__(self, name=None, description=None, engine_name=None,
+ major_engine_version=None, min_minor_engine_version=None,
+ permanent=False, persistent=False, port_required=False,
+ default_port=None, settings=None, depends_on=None):
+ self.name = name
+ self.description = description
+ self.engine_name = engine_name
+ self.major_engine_version = major_engine_version
+ self.min_minor_engine_version = min_minor_engine_version
+ self.permanent = permanent
+ self.persistent = persistent
+ self.port_required = port_required
+ self.default_port = default_port
+ self.settings = settings
+ self.depends_on = depends_on
+
+ if self.settings is None:
+ self.settings = []
+
+ if self.depends_on is None:
+ self.depends_on = []
+
+ def __repr__(self):
+ return 'OptionGroupOption:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ if name == 'OptionGroupOptionSettings':
+ self.settings = ResultSet([
+ ('OptionGroupOptionSettings', OptionGroupOptionSetting)
+ ])
+ elif name == 'OptionsDependedOn':
+ self.depends_on = []
+ else:
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'Name':
+ self.name = value
+ elif name == 'Description':
+ self.description = value
+ elif name == 'EngineName':
+ self.engine_name = value
+ elif name == 'MajorEngineVersion':
+ self.major_engine_version = value
+ elif name == 'MinimumRequiredMinorEngineVersion':
+ self.min_minor_engine_version = value
+ elif name == 'Permanent':
+ if value.lower() == 'true':
+ self.permenant = True
+ else:
+ self.permenant = False
+ elif name == 'Persistent':
+ if value.lower() == 'true':
+ self.persistent = True
+ else:
+ self.persistent = False
+ elif name == 'PortRequired':
+ if value.lower() == 'true':
+ self.port_required = True
+ else:
+ self.port_required = False
+ elif name == 'DefaultPort':
+ self.default_port = int(value)
+ else:
+ setattr(self, name, value)
+
+
+class OptionGroupOptionSetting(object):
+ """
+ Describes a OptionGroupOptionSetting for use in an OptionGroupOption.
+
+ :ivar name: The name of the option that has settings that you can set.
+ :ivar description: The description of the option setting.
+ :ivar value: The current value of the option setting.
+ :ivar default_value: The default value of the option setting.
+ :ivar allowed_values: The allowed values of the option setting.
+ :ivar data_type: The data type of the option setting.
+ :ivar apply_type: The DB engine specific parameter type.
+ :ivar is_modifiable: A Boolean value that, when true, indicates the option
+ setting can be modified from the default.
+ :ivar is_collection: Indicates if the option setting is part of a
+ collection.
+ """
+
+ def __init__(self, name=None, description=None, default_value=False,
+ allowed_values=None, apply_type=None, is_modifiable=False):
+ self.name = name
+ self.description = description
+ self.default_value = default_value
+ self.allowed_values = allowed_values
+ self.apply_type = apply_type
+ self.is_modifiable = is_modifiable
+
+ def __repr__(self):
+ return 'OptionGroupOptionSetting:%s' % self.name
+
+ def startElement(self, name, attrs, connection):
+ return None
+
+ def endElement(self, name, value, connection):
+ if name == 'SettingName':
+ self.name = value
+ elif name == 'SettingDescription':
+ self.description = value
+ elif name == 'DefaultValue':
+ self.default_value = value
+ elif name == 'AllowedValues':
+ self.allowed_values = value
+ elif name == 'ApplyType':
+ self.apply_type = value
+ elif name == 'IsModifiable':
+ if value.lower() == 'true':
+ self.is_modifiable = True
+ else:
+ self.is_modifiable = False
+ else:
+ setattr(self, name, value)
diff --git a/boto/rds/statusinfo.py b/boto/rds/statusinfo.py
new file mode 100644
index 00000000..d4ff9b08
--- /dev/null
+++ b/boto/rds/statusinfo.py
@@ -0,0 +1,54 @@
+# 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.
+#
+
+class StatusInfo(object):
+ """
+ Describes a status message.
+ """
+
+ def __init__(self, status_type=None, normal=None, status=None, message=None):
+ self.status_type = status_type
+ self.normal = normal
+ self.status = status
+ self.message = message
+
+ def __repr__(self):
+ return 'StatusInfo:%s' % self.message
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'StatusType':
+ self.status_type = value
+ elif name == 'Normal':
+ if value.lower() == 'true':
+ self.normal = True
+ else:
+ self.normal = False
+ elif name == 'Status':
+ self.status = value
+ elif name == 'Message':
+ self.message = value
+ else:
+ setattr(self, name, value)
diff --git a/boto/route53/record.py b/boto/route53/record.py
index 643af2a1..3fe75abb 100644
--- a/boto/route53/record.py
+++ b/boto/route53/record.py
@@ -244,20 +244,24 @@ class Record(object):
else:
# Use resource record(s)
records = ""
+
for r in self.resource_records:
records += self.ResourceRecordBody % r
+
body = self.ResourceRecordsBody % {
"ttl": self.ttl,
"records": records,
}
+
weight = ""
+
if self.identifier != None and self.weight != None:
weight = self.WRRBody % {"identifier": self.identifier, "weight":
self.weight}
elif self.identifier != None and self.region != None:
weight = self.RRRBody % {"identifier": self.identifier, "region":
self.region}
-
+
params = {
"name": self.name,
"type": self.type,
diff --git a/boto/sqs/message.py b/boto/sqs/message.py
index c7319903..43efee38 100644
--- a/boto/sqs/message.py
+++ b/boto/sqs/message.py
@@ -144,7 +144,7 @@ class Message(RawMessage):
encodes/decodes the message body using Base64 encoding to avoid any
illegal characters in the message body. See:
- http://developer.amazonwebservices.com/connect/thread.jspa?messageID=49680%EC%88%90
+ https://forums.aws.amazon.com/thread.jspa?threadID=13067
for details on why this is a good idea. The encode/decode is meant to
be transparent to the end-user.
diff --git a/boto/storage_uri.py b/boto/storage_uri.py
index f046fb15..40fee473 100755
--- a/boto/storage_uri.py
+++ b/boto/storage_uri.py
@@ -770,6 +770,21 @@ class BucketStorageUri(StorageUri):
self._build_uri_strings()
return self
+ def get_lifecycle_config(self, validate=False, headers=None):
+ """Returns a bucket's lifecycle configuration."""
+ self._check_bucket_uri('get_lifecycle_config')
+ bucket = self.get_bucket(validate, headers)
+ lifecycle_config = bucket.get_lifecycle_config(headers)
+ self.check_response(lifecycle_config, 'lifecycle', self.uri)
+ return lifecycle_config
+
+ def configure_lifecycle(self, lifecycle_config, validate=False,
+ headers=None):
+ """Sets or updates a bucket's lifecycle configuration."""
+ self._check_bucket_uri('configure_lifecycle')
+ bucket = self.get_bucket(validate, headers)
+ bucket.configure_lifecycle(lifecycle_config, headers)
+
def exists(self, headers=None):
"""Returns True if the object exists or False if it doesn't"""
if not self.object_name:
diff --git a/boto/sts/connection.py b/boto/sts/connection.py
index 5a1fdf2c..bdf21859 100644
--- a/boto/sts/connection.py
+++ b/boto/sts/connection.py
@@ -1,5 +1,6 @@
# Copyright (c) 2011 Mitch Garnaat http://garnaat.org/
# Copyright (c) 2011, Eucalyptus Systems, Inc.
+# 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
@@ -23,6 +24,7 @@
from boto.connection import AWSQueryConnection
from boto.regioninfo import RegionInfo
from credentials import Credentials, FederationToken, AssumedRole
+from credentials import DecodeAuthorizationMessage
import boto
import boto.utils
import datetime
@@ -437,3 +439,56 @@ class STSConnection(AWSQueryConnection):
AssumedRole,
verb='POST'
)
+
+ def decode_authorization_message(self, encoded_message):
+ """
+ Decodes additional information about the authorization status
+ of a request from an encoded message returned in response to
+ an AWS request.
+
+ For example, if a user is not authorized to perform an action
+ that he or she has requested, the request returns a
+ `Client.UnauthorizedOperation` response (an HTTP 403
+ response). Some AWS actions additionally return an encoded
+ message that can provide details about this authorization
+ failure.
+ Only certain AWS actions return an encoded authorization
+ message. The documentation for an individual action indicates
+ whether that action returns an encoded message in addition to
+ returning an HTTP code.
+ The message is encoded because the details of the
+ authorization status can constitute privileged information
+ that the user who requested the action should not see. To
+ decode an authorization status message, a user must be granted
+ permissions via an IAM policy to request the
+ `DecodeAuthorizationMessage` (
+ `sts:DecodeAuthorizationMessage`) action.
+
+ The decoded message includes the following type of
+ information:
+
+
+ + Whether the request was denied due to an explicit deny or
+ due to the absence of an explicit allow. For more information,
+ see `Determining Whether a Request is Allowed or Denied`_ in
+ Using IAM .
+ + The principal who made the request.
+ + The requested action.
+ + The requested resource.
+ + The values of condition keys in the context of the user's
+ request.
+
+ :type encoded_message: string
+ :param encoded_message: The encoded message that was returned with the
+ response.
+
+ """
+ params = {
+ 'EncodedMessage': encoded_message,
+ }
+ return self.get_object(
+ 'DecodeAuthorizationMessage',
+ params,
+ DecodeAuthorizationMessage,
+ verb='POST'
+ )
diff --git a/boto/sts/credentials.py b/boto/sts/credentials.py
index 33fe4ee7..a28d1067 100644
--- a/boto/sts/credentials.py
+++ b/boto/sts/credentials.py
@@ -213,3 +213,22 @@ class User(object):
self.arn = value
elif name == 'AssumedRoleId':
self.assume_role_id = value
+
+
+class DecodeAuthorizationMessage(object):
+ """
+ :ivar request_id: The request ID.
+ :ivar decoded_message: The decoded authorization message (may be JSON).
+ """
+ def __init__(self, request_id=None, decoded_message=None):
+ self.request_id = request_id
+ self.decoded_message = decoded_message
+
+ def startElement(self, name, attrs, connection):
+ pass
+
+ def endElement(self, name, value, connection):
+ if name == 'requestId':
+ self.request_id = value
+ elif name == 'DecodedMessage':
+ self.decoded_message = value
diff --git a/docs/source/cloudfront_tut.rst b/docs/source/cloudfront_tut.rst
index 7f41ff15..cd33056e 100644
--- a/docs/source/cloudfront_tut.rst
+++ b/docs/source/cloudfront_tut.rst
@@ -31,7 +31,8 @@ Working with CloudFront Distributions
-------------------------------------
Create a new :class:`boto.cloudfront.distribution.Distribution`::
- >>> distro = c.create_distribution(origin='mybucket.s3.amazonaws.com', enabled=False, comment='My new Distribution')
+ >>> origin = boto.cloudfront.origin.S3Origin('mybucket.s3.amazonaws.com')
+ >>> distro = c.create_distribution(origin=origin, enabled=False, comment='My new Distribution')
>>> d.domain_name
u'd2oxf3980lnb8l.cloudfront.net'
>>> d.id
diff --git a/docs/source/index.rst b/docs/source/index.rst
index dedc0d69..f720cf1b 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -111,6 +111,7 @@ Release Notes
.. toctree::
:titlesonly:
+ releasenotes/v2.9.8
releasenotes/v2.9.7
releasenotes/v2.9.6
releasenotes/v2.9.5
diff --git a/docs/source/releasenotes/v2.9.8.rst b/docs/source/releasenotes/v2.9.8.rst
new file mode 100644
index 00000000..36432813
--- /dev/null
+++ b/docs/source/releasenotes/v2.9.8.rst
@@ -0,0 +1,35 @@
+boto v2.9.8
+===========
+
+:date: 2013/07/18
+
+This release is adds new methods in AWS Security Token Service (STS), AWS
+CloudFormation, updates AWS Relational Database Service (RDS) & Google Storage.
+It also has several bugfixes & documentation improvements.
+
+
+Features
+--------
+
+* Added support for the ``DecodeAuthorizationMessage`` in STS (:sha:`1ada5ac`).
+* Added support for creating/deleting/describing ``OptionGroup``s in RDS.
+ (:sha:`d629228` & :sha:`d059a3b`)
+* Added ``CancelUpdateStack`` to CloudFormation. (:issue:`1476`, :sha:`5bae130`)
+* Added support for getting/setting lifecycle configurations on GS buckets.
+ (:issue:`1604`, :sha:`652fc81`)
+
+
+Bugfixes
+--------
+
+* Added region support to ``bin/elbadmin``. (:issue:`1586`,
+ :sha:`2ffbc60`)
+* Changed the mock storage to use case-insensitive headers. (:issue:`1594`,
+ :sha:`71849cb`)
+* Added ``complex_listeners`` to ELB. (:issue:`1048`, :sha:`b782ce2`)
+* Added tests for Route53's ``ResourceRecordSets``. (:sha:`fad5bde`)
+* Several documentation improvements/fixes:
+
+ * Updated CloudFront docs. (:issue:`1546`, :sha:`a811197`)
+ * Updated the URL explaining the use of base64 in SQS messages.
+ (:issue:`1596`, :sha:`00de3a2`)
diff --git a/tests/integration/ec2/elb/test_connection.py b/tests/integration/ec2/elb/test_connection.py
index 618d0ce9..1661899b 100644
--- a/tests/integration/ec2/elb/test_connection.py
+++ b/tests/integration/ec2/elb/test_connection.py
@@ -30,14 +30,14 @@ from boto.ec2.elb import ELBConnection
class ELBConnectionTest(unittest.TestCase):
ec2 = True
- def setup(self):
+ def setUp(self):
"""Creates a named load balancer that can be safely
deleted at the end of each test"""
self.conn = ELBConnection()
self.name = 'elb-boto-unit-test'
self.availability_zones = ['us-east-1a']
self.listeners = [(80, 8000, 'HTTP')]
- self.balancer = self.conn.create_load_balancer(name, availability_zones, listeners)
+ self.balancer = self.conn.create_load_balancer(self.name, self.availability_zones, self.listeners)
def tearDown(self):
""" Deletes the test load balancer after every test.
@@ -80,7 +80,7 @@ class ELBConnectionTest(unittest.TestCase):
def test_delete_load_balancer_listeners(self):
mod_listeners = [(80, 8000, 'HTTP'), (443, 8001, 'HTTP')]
- mod_name = self.name + "_mod"
+ mod_name = self.name + "-mod"
self.mod_balancer = self.conn.create_load_balancer(mod_name,\
self.availability_zones, mod_listeners)
@@ -118,5 +118,26 @@ class ELBConnectionTest(unittest.TestCase):
# Policy names should be checked here once they are supported
# in the Listener object.
+ def test_create_load_balancer_complex_listeners(self):
+ complex_listeners = [
+ (8080, 80, 'HTTP', 'HTTP'),
+ (2525, 25, 'TCP', 'TCP'),
+ ]
+
+ self.conn.create_load_balancer_listeners(
+ self.name,
+ complex_listeners=complex_listeners
+ )
+
+ balancers = self.conn.get_all_load_balancers(
+ load_balancer_names=[self.name]
+ )
+ self.assertEqual([lb.name for lb in balancers], [self.name])
+ self.assertEqual(
+ sorted(l.get_complex_tuple() for l in balancers[0].listeners),
+ # We need an extra 'HTTP' here over what ``self.listeners`` uses.
+ sorted([(80, 8000, 'HTTP', 'HTTP')] + complex_listeners)
+ )
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/integration/gs/test_basic.py b/tests/integration/gs/test_basic.py
index 9ac60b91..ffc890ff 100644
--- a/tests/integration/gs/test_basic.py
+++ b/tests/integration/gs/test_basic.py
@@ -37,6 +37,7 @@ from boto import handler
from boto import storage_uri
from boto.gs.acl import ACL
from boto.gs.cors import Cors
+from boto.gs.lifecycle import LifecycleConfig
from tests.integration.gs.testcase import GSTestCase
@@ -49,6 +50,21 @@ CORS_DOC = ('<CorsConfig><Cors><Origins><Origin>origin1.example.com'
'<ResponseHeader>bar</ResponseHeader></ResponseHeaders>'
'</Cors></CorsConfig>')
+LIFECYCLE_EMPTY = ('<?xml version="1.0" encoding="UTF-8"?>'
+ '<LifecycleConfiguration></LifecycleConfiguration>')
+LIFECYCLE_DOC = ('<?xml version="1.0" encoding="UTF-8"?>'
+ '<LifecycleConfiguration><Rule>'
+ '<Action><Delete/></Action>'
+ '<Condition><Age>365</Age>'
+ '<CreatedBefore>2013-01-15</CreatedBefore>'
+ '<NumberOfNewerVersions>3</NumberOfNewerVersions>'
+ '<IsLive>true</IsLive></Condition>'
+ '</Rule></LifecycleConfiguration>')
+LIFECYCLE_CONDITIONS = {'Age': '365',
+ 'CreatedBefore': '2013-01-15',
+ 'NumberOfNewerVersions': '3',
+ 'IsLive': 'true'}
+
# Regexp for matching project-private default object ACL.
PROJECT_PRIVATE_RE = ('\s*<AccessControlList>\s*<Entries>\s*<Entry>'
'\s*<Scope type="GroupById"><ID>[0-9a-fA-F]+</ID></Scope>'
@@ -377,3 +393,36 @@ class GSBasicTest(GSTestCase):
uri.set_cors(cors_obj)
cors = re.sub(r'\s', '', uri.get_cors().to_xml())
self.assertEqual(cors, CORS_DOC)
+
+ def test_lifecycle_config_bucket(self):
+ """Test setting and getting of lifecycle config on Bucket."""
+ # create a new bucket
+ bucket = self._MakeBucket()
+ bucket_name = bucket.name
+ # now call get_bucket to see if it's really there
+ bucket = self._GetConnection().get_bucket(bucket_name)
+ # get lifecycle config and make sure it's empty
+ xml = bucket.get_lifecycle_config().to_xml()
+ self.assertEqual(xml, LIFECYCLE_EMPTY)
+ # set lifecycle config
+ lifecycle_config = LifecycleConfig()
+ lifecycle_config.add_rule('Delete', None, LIFECYCLE_CONDITIONS)
+ bucket.configure_lifecycle(lifecycle_config)
+ xml = bucket.get_lifecycle_config().to_xml()
+ self.assertEqual(xml, LIFECYCLE_DOC)
+
+ def test_lifecycle_config_storage_uri(self):
+ """Test setting and getting of lifecycle config with storage_uri."""
+ # create a new bucket
+ bucket = self._MakeBucket()
+ bucket_name = bucket.name
+ uri = storage_uri('gs://' + bucket_name)
+ # get lifecycle config and make sure it's empty
+ xml = uri.get_lifecycle_config().to_xml()
+ self.assertEqual(xml, LIFECYCLE_EMPTY)
+ # set lifecycle config
+ lifecycle_config = LifecycleConfig()
+ lifecycle_config.add_rule('Delete', None, LIFECYCLE_CONDITIONS)
+ uri.configure_lifecycle(lifecycle_config)
+ xml = uri.get_lifecycle_config().to_xml()
+ self.assertEqual(xml, LIFECYCLE_DOC)
diff --git a/tests/integration/route53/test_resourcerecordsets.py b/tests/integration/route53/test_resourcerecordsets.py
new file mode 100644
index 00000000..c6baadfc
--- /dev/null
+++ b/tests/integration/route53/test_resourcerecordsets.py
@@ -0,0 +1,53 @@
+# 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
+from boto.route53.connection import Route53Connection
+from boto.route53.record import ResourceRecordSets
+from boto.exception import TooManyRecordsException
+
+
+class TestRoute53ResourceRecordSets(unittest.TestCase):
+ def setUp(self):
+ super(TestRoute53ResourceRecordSets, self).setUp()
+ self.conn = Route53Connection()
+ self.zone = self.conn.create_zone('example.com')
+
+ def tearDown(self):
+ self.zone.delete()
+ super(TestRoute53ResourceRecordSets, self).tearDown()
+
+ def test_add_change(self):
+ rrs = ResourceRecordSets(self.conn, self.zone.id)
+
+ created = rrs.add_change("CREATE", "vpn.example.com.", "A")
+ created.add_value('192.168.0.25')
+ rrs.commit()
+
+ rrs = ResourceRecordSets(self.conn, self.zone.id)
+ deleted = rrs.add_change('DELETE', "vpn.example.com.", "A")
+ deleted.add_value('192.168.0.25')
+ rrs.commit()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/integration/s3/mock_storage_service.py b/tests/integration/s3/mock_storage_service.py
index 507695bf..15a72adf 100644
--- a/tests/integration/s3/mock_storage_service.py
+++ b/tests/integration/s3/mock_storage_service.py
@@ -32,6 +32,8 @@ import base64
import re
from boto.utils import compute_md5
+from boto.utils import find_matching_headers
+from boto.utils import merge_headers_by_name
from boto.s3.prefix import Prefix
try:
@@ -99,12 +101,14 @@ class MockKey(object):
def _handle_headers(self, headers):
if not headers:
return
- if 'Content-Encoding' in headers:
- self.content_encoding = headers['Content-Encoding']
- if 'Content-Type' in headers:
- self.content_type = headers['Content-Type']
- if 'Content-Language' in headers:
- self.content_language = headers['Content-Language']
+ if find_matching_headers('Content-Encoding', headers):
+ self.content_encoding = merge_headers_by_name('Content-Encoding',
+ headers)
+ if find_matching_headers('Content-Type', headers):
+ self.content_type = merge_headers_by_name('Content-Type', headers)
+ if find_matching_headers('Content-Language', headers):
+ self.content_language = merge_headers_by_name('Content-Language',
+ headers)
# Simplistic partial implementation for headers: Just supports range GETs
# of flavor 'Range: bytes=xyz-'.
diff --git a/tests/integration/sts/test_session_token.py b/tests/integration/sts/test_session_token.py
index 35d42ca8..3d548b9f 100644
--- a/tests/integration/sts/test_session_token.py
+++ b/tests/integration/sts/test_session_token.py
@@ -79,3 +79,12 @@ class SessionTokenTest (unittest.TestCase):
except BotoServerError as err:
self.assertEqual(err.status, 403)
self.assertTrue('Not authorized' in err.body)
+
+ def test_decode_authorization_message(self):
+ c = STSConnection()
+
+ try:
+ creds = c.decode_authorization_message('b94d27b9934')
+ except BotoServerError as err:
+ self.assertEqual(err.status, 400)
+ self.assertTrue('Invalid token' in err.body)
diff --git a/tests/unit/cloudformation/test_connection.py b/tests/unit/cloudformation/test_connection.py
index d7f86c70..6890f152 100644
--- a/tests/unit/cloudformation/test_connection.py
+++ b/tests/unit/cloudformation/test_connection.py
@@ -601,5 +601,20 @@ class TestCloudFormationValidateTemplate(CloudFormationConnectionBase):
})
+class TestCloudFormationCancelUpdateStack(CloudFormationConnectionBase):
+ def default_body(self):
+ return """<CancelUpdateStackResult/>"""
+
+ def test_cancel_update_stack(self):
+ self.set_http_response(status_code=200)
+ api_response = self.service_connection.cancel_update_stack('stack_name')
+ self.assertEqual(api_response, True)
+ self.assert_request_parameters({
+ 'Action': 'CancelUpdateStack',
+ 'StackName': 'stack_name',
+ 'Version': '2010-05-15',
+ })
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/unit/ec2/elb/test_listener.py b/tests/unit/ec2/elb/test_listener.py
new file mode 100644
index 00000000..ff0f693c
--- /dev/null
+++ b/tests/unit/ec2/elb/test_listener.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python
+import xml.sax
+from tests.unit import unittest
+
+import boto.resultset
+from boto.ec2.elb.loadbalancer import LoadBalancer
+
+
+LISTENERS_RESPONSE = r"""<?xml version="1.0" encoding="UTF-8"?>
+<DescribeLoadBalancersResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
+ <DescribeLoadBalancersResult>
+ <LoadBalancerDescriptions>
+ <member>
+ <SecurityGroups/>
+ <CreatedTime>2013-07-09T19:18:00.520Z</CreatedTime>
+ <LoadBalancerName>elb-boto-unit-test</LoadBalancerName>
+ <HealthCheck>
+ <Interval>30</Interval>
+ <Target>TCP:8000</Target>
+ <HealthyThreshold>10</HealthyThreshold>
+ <Timeout>5</Timeout>
+ <UnhealthyThreshold>2</UnhealthyThreshold>
+ </HealthCheck>
+ <ListenerDescriptions>
+ <member>
+ <PolicyNames/>
+ <Listener>
+ <Protocol>HTTP</Protocol>
+ <LoadBalancerPort>80</LoadBalancerPort>
+ <InstanceProtocol>HTTP</InstanceProtocol>
+ <InstancePort>8000</InstancePort>
+ </Listener>
+ </member>
+ <member>
+ <PolicyNames/>
+ <Listener>
+ <Protocol>HTTP</Protocol>
+ <LoadBalancerPort>8080</LoadBalancerPort>
+ <InstanceProtocol>HTTP</InstanceProtocol>
+ <InstancePort>80</InstancePort>
+ </Listener>
+ </member>
+ <member>
+ <PolicyNames/>
+ <Listener>
+ <Protocol>TCP</Protocol>
+ <LoadBalancerPort>2525</LoadBalancerPort>
+ <InstanceProtocol>TCP</InstanceProtocol>
+ <InstancePort>25</InstancePort>
+ </Listener>
+ </member>
+ </ListenerDescriptions>
+ <Instances/>
+ <Policies>
+ <AppCookieStickinessPolicies/>
+ <OtherPolicies/>
+ <LBCookieStickinessPolicies/>
+ </Policies>
+ <AvailabilityZones>
+ <member>us-east-1a</member>
+ </AvailabilityZones>
+ <CanonicalHostedZoneName>elb-boto-unit-test-408121642.us-east-1.elb.amazonaws.com</CanonicalHostedZoneName>
+ <CanonicalHostedZoneNameID>Z3DZXE0Q79N41H</CanonicalHostedZoneNameID>
+ <Scheme>internet-facing</Scheme>
+ <SourceSecurityGroup>
+ <OwnerAlias>amazon-elb</OwnerAlias>
+ <GroupName>amazon-elb-sg</GroupName>
+ </SourceSecurityGroup>
+ <DNSName>elb-boto-unit-test-408121642.us-east-1.elb.amazonaws.com</DNSName>
+ <BackendServerDescriptions/>
+ <Subnets/>
+ </member>
+ </LoadBalancerDescriptions>
+ </DescribeLoadBalancersResult>
+ <ResponseMetadata>
+ <RequestId>5763d932-e8cc-11e2-a940-11136cceffb8</RequestId>
+ </ResponseMetadata>
+</DescribeLoadBalancersResponse>
+"""
+
+
+class TestListenerResponseParsing(unittest.TestCase):
+ def test_parse_complex(self):
+ rs = boto.resultset.ResultSet([
+ ('member', LoadBalancer)
+ ])
+ h = boto.handler.XmlHandler(rs, None)
+ xml.sax.parseString(LISTENERS_RESPONSE, h)
+ listeners = rs[0].listeners
+ self.assertEqual(
+ sorted([l.get_complex_tuple() for l in listeners]),
+ [
+ (80, 8000, 'HTTP', 'HTTP'),
+ (2525, 25, 'TCP', 'TCP'),
+ (8080, 80, 'HTTP', 'HTTP'),
+ ]
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/unit/rds/test_connection.py b/tests/unit/rds/test_connection.py
index ff3000fc..7eef7415 100644
--- a/tests/unit/rds/test_connection.py
+++ b/tests/unit/rds/test_connection.py
@@ -78,6 +78,14 @@ class TestRDSConnection(AWSMockServiceTestCase):
<AllocatedStorage>200</AllocatedStorage>
<DBInstanceClass>db.m1.large</DBInstanceClass>
<MasterUsername>awsuser</MasterUsername>
+ <StatusInfos>
+ <DBInstanceStatusInfo>
+ <Message></Message>
+ <Normal>true</Normal>
+ <Status>replicating</Status>
+ <StatusType>read replication</StatusType>
+ </DBInstanceStatusInfo>
+ </StatusInfos>
</DBInstance>
</DBInstances>
</DescribeDBInstancesResult>
@@ -123,6 +131,108 @@ class TestRDSConnection(AWSMockServiceTestCase):
self.assertEqual(db.security_group.description, None)
self.assertEqual(db.security_group.ec2_groups, [])
self.assertEqual(db.security_group.ip_ranges, [])
+ self.assertEqual(len(db.status_infos), 1)
+ self.assertEqual(db.status_infos[0].message, '')
+ self.assertEqual(db.status_infos[0].normal, True)
+ self.assertEqual(db.status_infos[0].status, 'replicating')
+ self.assertEqual(db.status_infos[0].status_type, 'read replication')
+
+
+class TestRDSOptionGroups(AWSMockServiceTestCase):
+ connection_class = RDSConnection
+
+ def setUp(self):
+ super(TestRDSOptionGroups, self).setUp()
+
+ def default_body(self):
+ return """
+ <DescribeOptionGroupsResponse xmlns="http://rds.amazonaws.com/doc/2013-05-15/">
+ <DescribeOptionGroupsResult>
+ <OptionGroupsList>
+ <OptionGroup>
+ <MajorEngineVersion>11.2</MajorEngineVersion>
+ <OptionGroupName>myoptiongroup</OptionGroupName>
+ <EngineName>oracle-se1</EngineName>
+ <OptionGroupDescription>Test option group</OptionGroupDescription>
+ <Options/>
+ </OptionGroup>
+ <OptionGroup>
+ <MajorEngineVersion>11.2</MajorEngineVersion>
+ <OptionGroupName>default:oracle-se1-11-2</OptionGroupName>
+ <EngineName>oracle-se1</EngineName>
+ <OptionGroupDescription>Default Option Group.</OptionGroupDescription>
+ <Options/>
+ </OptionGroup>
+ </OptionGroupsList>
+ </DescribeOptionGroupsResult>
+ <ResponseMetadata>
+ <RequestId>e4b234d9-84d5-11e1-87a6-71059839a52b</RequestId>
+ </ResponseMetadata>
+ </DescribeOptionGroupsResponse>
+ """
+
+ def test_describe_option_groups(self):
+ self.set_http_response(status_code=200)
+ response = self.service_connection.describe_option_groups()
+ self.assertEqual(len(response), 2)
+ options = response[0]
+ self.assertEqual(options.name, 'myoptiongroup')
+ self.assertEqual(options.description, 'Test option group')
+ self.assertEqual(options.engine_name, 'oracle-se1')
+ self.assertEqual(options.major_engine_version, '11.2')
+ options = response[1]
+ self.assertEqual(options.name, 'default:oracle-se1-11-2')
+ self.assertEqual(options.description, 'Default Option Group.')
+ self.assertEqual(options.engine_name, 'oracle-se1')
+ self.assertEqual(options.major_engine_version, '11.2')
+
+
+class TestRDSOptionGroupOptions(AWSMockServiceTestCase):
+ connection_class = RDSConnection
+
+ def setUp(self):
+ super(TestRDSOptionGroupOptions, self).setUp()
+
+ def default_body(self):
+ return """
+ <DescribeOptionGroupOptionsResponse xmlns="http://rds.amazonaws.com/doc/2013-05-15/">
+ <DescribeOptionGroupOptionsResult>
+ <OptionGroupOptions>
+ <OptionGroupOption>
+ <MajorEngineVersion>11.2</MajorEngineVersion>
+ <PortRequired>true</PortRequired>
+ <OptionsDependedOn/>
+ <Description>Oracle Enterprise Manager</Description>
+ <DefaultPort>1158</DefaultPort>
+ <Name>OEM</Name>
+ <EngineName>oracle-se1</EngineName>
+ <MinimumRequiredMinorEngineVersion>0.2.v3</MinimumRequiredMinorEngineVersion>
+ <Persistent>false</Persistent>
+ <Permanent>false</Permanent>
+ </OptionGroupOption>
+ </OptionGroupOptions>
+ </DescribeOptionGroupOptionsResult>
+ <ResponseMetadata>
+ <RequestId>d9c8f6a1-84c7-11e1-a264-0b23c28bc344</RequestId>
+ </ResponseMetadata>
+ </DescribeOptionGroupOptionsResponse>
+ """
+
+ def test_describe_option_group_options(self):
+ self.set_http_response(status_code=200)
+ response = self.service_connection.describe_option_group_options()
+ self.assertEqual(len(response), 1)
+ options = response[0]
+ self.assertEqual(options.name, 'OEM')
+ self.assertEqual(options.description, 'Oracle Enterprise Manager')
+ self.assertEqual(options.engine_name, 'oracle-se1')
+ self.assertEqual(options.major_engine_version, '11.2')
+ self.assertEqual(options.min_minor_engine_version, '0.2.v3')
+ self.assertEqual(options.port_required, True)
+ self.assertEqual(options.default_port, 1158)
+ self.assertEqual(options.permanent, False)
+ self.assertEqual(options.persistent, False)
+ self.assertEqual(options.depends_on, [])
if __name__ == '__main__':