Permalink
Browse files

Supports getting and setting lifecycle configuration for buckets.

  • Loading branch information...
1 parent 7df9e77 commit 652fc817235f358e99d35ba7d8bca50d10a58466 @yzhihong yzhihong committed Apr 24, 2013
Showing with 340 additions and 0 deletions.
  1. +7 −0 boto/exception.py
  2. +42 −0 boto/gs/bucket.py
  3. +227 −0 boto/gs/lifecycle.py
  4. +15 −0 boto/storage_uri.py
  5. +49 −0 tests/integration/gs/test_basic.py
View
@@ -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,
View
@@ -30,6 +30,7 @@
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 @@
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 @@ def configure_versioning(self, enabled, headers=None):
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)
View
@@ -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)
View
@@ -770,6 +770,21 @@ def compose(self, components, content_type=None, headers=None):
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:
@@ -37,6 +37,7 @@
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 @@
'<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 @@ def test_cors_xml_storage_uri(self):
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)

0 comments on commit 652fc81

Please sign in to comment.