1# Copyright 2013 Google Inc. 2# 3# Permission is hereby granted, free of charge, to any person obtaining a 4# copy of this software and associated documentation files (the 5# "Software"), to deal in the Software without restriction, including 6# without limitation the rights to use, copy, modify, merge, publish, dis- 7# tribute, sublicense, and/or sell copies of the Software, and to permit 8# persons to whom the Software is furnished to do so, subject to the fol- 9# lowing conditions: 10# 11# The above copyright notice and this permission notice shall be included 12# in all copies or substantial portions of the Software. 13# 14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- 16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20# IN THE SOFTWARE. 21 22from boto.exception import InvalidLifecycleConfigError 23 24# Relevant tags for the lifecycle configuration XML document. 25LIFECYCLE_CONFIG = 'LifecycleConfiguration' 26RULE = 'Rule' 27ACTION = 'Action' 28DELETE = 'Delete' 29CONDITION = 'Condition' 30AGE = 'Age' 31CREATED_BEFORE = 'CreatedBefore' 32NUM_NEWER_VERSIONS = 'NumberOfNewerVersions' 33IS_LIVE = 'IsLive' 34 35# List of all action elements. 36LEGAL_ACTIONS = [DELETE] 37# List of all action parameter elements. 38LEGAL_ACTION_PARAMS = [] 39# List of all condition elements. 40LEGAL_CONDITIONS = [AGE, CREATED_BEFORE, NUM_NEWER_VERSIONS, IS_LIVE] 41# Dictionary mapping actions to supported action parameters for each action. 42LEGAL_ACTION_ACTION_PARAMS = { 43 DELETE: [], 44} 45 46class Rule(object): 47 """ 48 A lifecycle rule for a bucket. 49 50 :ivar action: Action to be taken. 51 52 :ivar action_params: A dictionary of action specific parameters. Each item 53 in the dictionary represents the name and value of an action parameter. 54 55 :ivar conditions: A dictionary of conditions that specify when the action 56 should be taken. Each item in the dictionary represents the name and value 57 of a condition. 58 """ 59 60 def __init__(self, action=None, action_params=None, conditions=None): 61 self.action = action 62 self.action_params = action_params or {} 63 self.conditions = conditions or {} 64 65 # Name of the current enclosing tag (used to validate the schema). 66 self.current_tag = RULE 67 68 def validateStartTag(self, tag, parent): 69 """Verify parent of the start tag.""" 70 if self.current_tag != parent: 71 raise InvalidLifecycleConfigError( 72 'Invalid tag %s found inside %s tag' % (tag, self.current_tag)) 73 74 def validateEndTag(self, tag): 75 """Verify end tag against the start tag.""" 76 if tag != self.current_tag: 77 raise InvalidLifecycleConfigError( 78 'Mismatched start and end tags (%s/%s)' % 79 (self.current_tag, tag)) 80 81 def startElement(self, name, attrs, connection): 82 if name == ACTION: 83 self.validateStartTag(name, RULE) 84 elif name in LEGAL_ACTIONS: 85 self.validateStartTag(name, ACTION) 86 # Verify there is only one action tag in the rule. 87 if self.action is not None: 88 raise InvalidLifecycleConfigError( 89 'Only one action tag is allowed in each rule') 90 self.action = name 91 elif name in LEGAL_ACTION_PARAMS: 92 # Make sure this tag is found in an action tag. 93 if self.current_tag not in LEGAL_ACTIONS: 94 raise InvalidLifecycleConfigError( 95 'Tag %s found outside of action' % name) 96 # Make sure this tag is allowed for the current action tag. 97 if name not in LEGAL_ACTION_ACTION_PARAMS[self.action]: 98 raise InvalidLifecycleConfigError( 99 'Tag %s not allowed in action %s' % (name, self.action)) 100 elif name == CONDITION: 101 self.validateStartTag(name, RULE) 102 elif name in LEGAL_CONDITIONS: 103 self.validateStartTag(name, CONDITION) 104 # Verify there is no duplicate conditions. 105 if name in self.conditions: 106 raise InvalidLifecycleConfigError( 107 'Found duplicate conditions %s' % name) 108 else: 109 raise InvalidLifecycleConfigError('Unsupported tag ' + name) 110 self.current_tag = name 111 112 def endElement(self, name, value, connection): 113 self.validateEndTag(name) 114 if name == RULE: 115 # We have to validate the rule after it is fully populated because 116 # the action and condition elements could be in any order. 117 self.validate() 118 elif name == ACTION: 119 self.current_tag = RULE 120 elif name in LEGAL_ACTIONS: 121 self.current_tag = ACTION 122 elif name in LEGAL_ACTION_PARAMS: 123 self.current_tag = self.action 124 # Add the action parameter name and value to the dictionary. 125 self.action_params[name] = value.strip() 126 elif name == CONDITION: 127 self.current_tag = RULE 128 elif name in LEGAL_CONDITIONS: 129 self.current_tag = CONDITION 130 # Add the condition name and value to the dictionary. 131 self.conditions[name] = value.strip() 132 else: 133 raise InvalidLifecycleConfigError('Unsupported end tag ' + name) 134 135 def validate(self): 136 """Validate the rule.""" 137 if not self.action: 138 raise InvalidLifecycleConfigError( 139 'No action was specified in the rule') 140 if not self.conditions: 141 raise InvalidLifecycleConfigError( 142 'No condition was specified for action %s' % self.action) 143 144 def to_xml(self): 145 """Convert the rule into XML string representation.""" 146 s = '<' + RULE + '>' 147 s += '<' + ACTION + '>' 148 if self.action_params: 149 s += '<' + self.action + '>' 150 for param in LEGAL_ACTION_PARAMS: 151 if param in self.action_params: 152 s += ('<' + param + '>' + self.action_params[param] + '</' 153 + param + '>') 154 s += '</' + self.action + '>' 155 else: 156 s += '<' + self.action + '/>' 157 s += '</' + ACTION + '>' 158 s += '<' + CONDITION + '>' 159 for condition in LEGAL_CONDITIONS: 160 if condition in self.conditions: 161 s += ('<' + condition + '>' + self.conditions[condition] + '</' 162 + condition + '>') 163 s += '</' + CONDITION + '>' 164 s += '</' + RULE + '>' 165 return s 166 167class LifecycleConfig(list): 168 """ 169 A container of rules associated with a lifecycle configuration. 170 """ 171 172 def __init__(self): 173 # Track if root tag has been seen. 174 self.has_root_tag = False 175 176 def startElement(self, name, attrs, connection): 177 if name == LIFECYCLE_CONFIG: 178 if self.has_root_tag: 179 raise InvalidLifecycleConfigError( 180 'Only one root tag is allowed in the XML') 181 self.has_root_tag = True 182 elif name == RULE: 183 if not self.has_root_tag: 184 raise InvalidLifecycleConfigError('Invalid root tag ' + name) 185 rule = Rule() 186 self.append(rule) 187 return rule 188 else: 189 raise InvalidLifecycleConfigError('Unsupported tag ' + name) 190 191 def endElement(self, name, value, connection): 192 if name == LIFECYCLE_CONFIG: 193 pass 194 else: 195 raise InvalidLifecycleConfigError('Unsupported end tag ' + name) 196 197 def to_xml(self): 198 """Convert LifecycleConfig object into XML string representation.""" 199 s = '<?xml version="1.0" encoding="UTF-8"?>' 200 s += '<' + LIFECYCLE_CONFIG + '>' 201 for rule in self: 202 s += rule.to_xml() 203 s += '</' + LIFECYCLE_CONFIG + '>' 204 return s 205 206 def add_rule(self, action, action_params, conditions): 207 """ 208 Add a rule to this Lifecycle configuration. This only adds the rule to 209 the local copy. To install the new rule(s) on the bucket, you need to 210 pass this Lifecycle config object to the configure_lifecycle method of 211 the Bucket object. 212 213 :type action: str 214 :param action: Action to be taken. 215 216 :type action_params: dict 217 :param action_params: A dictionary of action specific parameters. Each 218 item in the dictionary represents the name and value of an action 219 parameter. 220 221 :type conditions: dict 222 :param conditions: A dictionary of conditions that specify when the 223 action should be taken. Each item in the dictionary represents the name 224 and value of a condition. 225 """ 226 rule = Rule(action, action_params, conditions) 227 self.append(rule) 228