OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/ |
| 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 |
| 22 import boto |
| 23 from boto.cloudformation.stack import Stack, StackSummary, StackEvent |
| 24 from boto.cloudformation.stack import StackResource, StackResourceSummary |
| 25 from boto.cloudformation.template import Template |
| 26 from boto.connection import AWSQueryConnection |
| 27 from boto.regioninfo import RegionInfo |
| 28 from boto.compat import json |
| 29 |
| 30 |
| 31 class CloudFormationConnection(AWSQueryConnection): |
| 32 |
| 33 """ |
| 34 A Connection to the CloudFormation Service. |
| 35 """ |
| 36 APIVersion = boto.config.get('Boto', 'cfn_version', '2010-05-15') |
| 37 DefaultRegionName = boto.config.get('Boto', 'cfn_region_name', 'us-east-1') |
| 38 DefaultRegionEndpoint = boto.config.get('Boto', 'cfn_region_endpoint', |
| 39 'cloudformation.us-east-1.amazonaws.
com') |
| 40 |
| 41 valid_states = ( |
| 42 'CREATE_IN_PROGRESS', 'CREATE_FAILED', 'CREATE_COMPLETE', |
| 43 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE', |
| 44 'DELETE_IN_PROGRESS', 'DELETE_FAILED', 'DELETE_COMPLETE', |
| 45 'UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', |
| 46 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_IN_PROGRESS', |
| 47 'UPDATE_ROLLBACK_FAILED', |
| 48 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', |
| 49 'UPDATE_ROLLBACK_COMPLETE') |
| 50 |
| 51 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, |
| 52 is_secure=True, port=None, proxy=None, proxy_port=None, |
| 53 proxy_user=None, proxy_pass=None, debug=0, |
| 54 https_connection_factory=None, region=None, path='/', |
| 55 converter=None, security_token=None, validate_certs=True): |
| 56 if not region: |
| 57 region = RegionInfo(self, self.DefaultRegionName, |
| 58 self.DefaultRegionEndpoint, CloudFormationConnection) |
| 59 self.region = region |
| 60 AWSQueryConnection.__init__(self, aws_access_key_id, |
| 61 aws_secret_access_key, |
| 62 is_secure, port, proxy, proxy_port, |
| 63 proxy_user, proxy_pass, |
| 64 self.region.endpoint, debug, |
| 65 https_connection_factory, path, |
| 66 security_token, |
| 67 validate_certs=validate_certs) |
| 68 |
| 69 def _required_auth_capability(self): |
| 70 return ['hmac-v4'] |
| 71 |
| 72 def encode_bool(self, v): |
| 73 v = bool(v) |
| 74 return {True: "true", False: "false"}[v] |
| 75 |
| 76 def _build_create_or_update_params(self, stack_name, template_body, |
| 77 template_url, parameters, |
| 78 notification_arns, disable_rollback, |
| 79 timeout_in_minutes, capabilities, tags): |
| 80 """ |
| 81 Helper that creates JSON parameters needed by a Stack Create or |
| 82 Stack Update call. |
| 83 |
| 84 :type stack_name: string |
| 85 :param stack_name: The name of the Stack, must be unique amoung running |
| 86 Stacks |
| 87 |
| 88 :type template_body: string |
| 89 :param template_body: The template body (JSON string) |
| 90 |
| 91 :type template_url: string |
| 92 :param template_url: An S3 URL of a stored template JSON document. If |
| 93 both the template_body and template_url are |
| 94 specified, the template_body takes precedence |
| 95 |
| 96 :type parameters: list of tuples |
| 97 :param parameters: A list of (key, value) pairs for template input |
| 98 parameters. |
| 99 |
| 100 :type notification_arns: list of strings |
| 101 :param notification_arns: A list of SNS topics to send Stack event |
| 102 notifications to. |
| 103 |
| 104 :type disable_rollback: bool |
| 105 :param disable_rollback: Indicates whether or not to rollback on |
| 106 failure. |
| 107 |
| 108 :type timeout_in_minutes: int |
| 109 :param timeout_in_minutes: Maximum amount of time to let the Stack |
| 110 spend creating itself. If this timeout is exceeded, |
| 111 the Stack will enter the CREATE_FAILED state. |
| 112 |
| 113 :type capabilities: list |
| 114 :param capabilities: The list of capabilities you want to allow in |
| 115 the stack. Currently, the only valid capability is |
| 116 'CAPABILITY_IAM'. |
| 117 |
| 118 :type tags: dict |
| 119 :param tags: A dictionary of (key, value) pairs of tags to |
| 120 associate with this stack. |
| 121 |
| 122 :rtype: dict |
| 123 :return: JSON parameters represented as a Python dict. |
| 124 """ |
| 125 params = {'ContentType': "JSON", 'StackName': stack_name, |
| 126 'DisableRollback': self.encode_bool(disable_rollback)} |
| 127 if template_body: |
| 128 params['TemplateBody'] = template_body |
| 129 if template_url: |
| 130 params['TemplateURL'] = template_url |
| 131 if template_body and template_url: |
| 132 boto.log.warning("If both TemplateBody and TemplateURL are" |
| 133 " specified, only TemplateBody will be honored by the API") |
| 134 if len(parameters) > 0: |
| 135 for i, (key, value) in enumerate(parameters): |
| 136 params['Parameters.member.%d.ParameterKey' % (i + 1)] = key |
| 137 params['Parameters.member.%d.ParameterValue' % (i + 1)] = value |
| 138 if capabilities: |
| 139 for i, value in enumerate(capabilities): |
| 140 params['Capabilities.member.%d' % (i + 1)] = value |
| 141 if tags: |
| 142 for i, (key, value) in enumerate(tags.items()): |
| 143 params['Tags.member.%d.Key' % (i + 1)] = key |
| 144 params['Tags.member.%d.Value' % (i + 1)] = value |
| 145 if len(notification_arns) > 0: |
| 146 self.build_list_params(params, notification_arns, |
| 147 "NotificationARNs.member") |
| 148 if timeout_in_minutes: |
| 149 params['TimeoutInMinutes'] = int(timeout_in_minutes) |
| 150 return params |
| 151 |
| 152 def create_stack(self, stack_name, template_body=None, template_url=None, |
| 153 parameters=[], notification_arns=[], disable_rollback=False, |
| 154 timeout_in_minutes=None, capabilities=None, tags=None): |
| 155 """ |
| 156 Creates a CloudFormation Stack as specified by the template. |
| 157 |
| 158 :type stack_name: string |
| 159 :param stack_name: The name of the Stack, must be unique amoung running |
| 160 Stacks |
| 161 |
| 162 :type template_body: string |
| 163 :param template_body: The template body (JSON string) |
| 164 |
| 165 :type template_url: string |
| 166 :param template_url: An S3 URL of a stored template JSON document. If |
| 167 both the template_body and template_url are |
| 168 specified, the template_body takes precedence |
| 169 |
| 170 :type parameters: list of tuples |
| 171 :param parameters: A list of (key, value) pairs for template input |
| 172 parameters. |
| 173 |
| 174 :type notification_arns: list of strings |
| 175 :param notification_arns: A list of SNS topics to send Stack event |
| 176 notifications to. |
| 177 |
| 178 :type disable_rollback: bool |
| 179 :param disable_rollback: Indicates whether or not to rollback on |
| 180 failure. |
| 181 |
| 182 :type timeout_in_minutes: int |
| 183 :param timeout_in_minutes: Maximum amount of time to let the Stack |
| 184 spend creating itself. If this timeout is exceeded, |
| 185 the Stack will enter the CREATE_FAILED state. |
| 186 |
| 187 :type capabilities: list |
| 188 :param capabilities: The list of capabilities you want to allow in |
| 189 the stack. Currently, the only valid capability is |
| 190 'CAPABILITY_IAM'. |
| 191 |
| 192 :type tags: dict |
| 193 :param tags: A dictionary of (key, value) pairs of tags to |
| 194 associate with this stack. |
| 195 |
| 196 :rtype: string |
| 197 :return: The unique Stack ID. |
| 198 """ |
| 199 params = self._build_create_or_update_params(stack_name, |
| 200 template_body, template_url, parameters, notification_arns, |
| 201 disable_rollback, timeout_in_minutes, capabilities, tags) |
| 202 response = self.make_request('CreateStack', params, '/', 'POST') |
| 203 body = response.read() |
| 204 if response.status == 200: |
| 205 body = json.loads(body) |
| 206 return body['CreateStackResponse']['CreateStackResult']['StackId'] |
| 207 else: |
| 208 boto.log.error('%s %s' % (response.status, response.reason)) |
| 209 boto.log.error('%s' % body) |
| 210 raise self.ResponseError(response.status, response.reason, body) |
| 211 |
| 212 def update_stack(self, stack_name, template_body=None, template_url=None, |
| 213 parameters=[], notification_arns=[], disable_rollback=False, |
| 214 timeout_in_minutes=None, capabilities=None, tags=None): |
| 215 """ |
| 216 Updates a CloudFormation Stack as specified by the template. |
| 217 |
| 218 :type stack_name: string |
| 219 :param stack_name: The name of the Stack, must be unique amoung running |
| 220 Stacks. |
| 221 |
| 222 :type template_body: string |
| 223 :param template_body: The template body (JSON string) |
| 224 |
| 225 :type template_url: string |
| 226 :param template_url: An S3 URL of a stored template JSON document. If |
| 227 both the template_body and template_url are |
| 228 specified, the template_body takes precedence. |
| 229 |
| 230 :type parameters: list of tuples |
| 231 :param parameters: A list of (key, value) pairs for template input |
| 232 parameters. |
| 233 |
| 234 :type notification_arns: list of strings |
| 235 :param notification_arns: A list of SNS topics to send Stack event |
| 236 notifications to. |
| 237 |
| 238 :type disable_rollback: bool |
| 239 :param disable_rollback: Indicates whether or not to rollback on |
| 240 failure. |
| 241 |
| 242 :type timeout_in_minutes: int |
| 243 :param timeout_in_minutes: Maximum amount of time to let the Stack |
| 244 spend creating itself. If this timeout is exceeded, |
| 245 the Stack will enter the CREATE_FAILED state |
| 246 |
| 247 :type capabilities: list |
| 248 :param capabilities: The list of capabilities you want to allow in |
| 249 the stack. Currently, the only valid capability is |
| 250 'CAPABILITY_IAM'. |
| 251 |
| 252 :type tags: dict |
| 253 :param tags: A dictionary of (key, value) pairs of tags to |
| 254 associate with this stack. |
| 255 |
| 256 :rtype: string |
| 257 :return: The unique Stack ID. |
| 258 """ |
| 259 params = self._build_create_or_update_params(stack_name, |
| 260 template_body, template_url, parameters, notification_arns, |
| 261 disable_rollback, timeout_in_minutes, capabilities, tags) |
| 262 response = self.make_request('UpdateStack', params, '/', 'POST') |
| 263 body = response.read() |
| 264 if response.status == 200: |
| 265 body = json.loads(body) |
| 266 return body['UpdateStackResponse']['UpdateStackResult']['StackId'] |
| 267 else: |
| 268 boto.log.error('%s %s' % (response.status, response.reason)) |
| 269 boto.log.error('%s' % body) |
| 270 raise self.ResponseError(response.status, response.reason, body) |
| 271 |
| 272 def delete_stack(self, stack_name_or_id): |
| 273 params = {'ContentType': "JSON", 'StackName': stack_name_or_id} |
| 274 # TODO: change this to get_status ? |
| 275 response = self.make_request('DeleteStack', params, '/', 'GET') |
| 276 body = response.read() |
| 277 if response.status == 200: |
| 278 return json.loads(body) |
| 279 else: |
| 280 boto.log.error('%s %s' % (response.status, response.reason)) |
| 281 boto.log.error('%s' % body) |
| 282 raise self.ResponseError(response.status, response.reason, body) |
| 283 |
| 284 def describe_stack_events(self, stack_name_or_id=None, next_token=None): |
| 285 params = {} |
| 286 if stack_name_or_id: |
| 287 params['StackName'] = stack_name_or_id |
| 288 if next_token: |
| 289 params['NextToken'] = next_token |
| 290 return self.get_list('DescribeStackEvents', params, [('member', |
| 291 StackEvent)]) |
| 292 |
| 293 def describe_stack_resource(self, stack_name_or_id, logical_resource_id): |
| 294 params = {'ContentType': "JSON", 'StackName': stack_name_or_id, |
| 295 'LogicalResourceId': logical_resource_id} |
| 296 response = self.make_request('DescribeStackResource', params, |
| 297 '/', 'GET') |
| 298 body = response.read() |
| 299 if response.status == 200: |
| 300 return json.loads(body) |
| 301 else: |
| 302 boto.log.error('%s %s' % (response.status, response.reason)) |
| 303 boto.log.error('%s' % body) |
| 304 raise self.ResponseError(response.status, response.reason, body) |
| 305 |
| 306 def describe_stack_resources(self, stack_name_or_id=None, |
| 307 logical_resource_id=None, |
| 308 physical_resource_id=None): |
| 309 params = {} |
| 310 if stack_name_or_id: |
| 311 params['StackName'] = stack_name_or_id |
| 312 if logical_resource_id: |
| 313 params['LogicalResourceId'] = logical_resource_id |
| 314 if physical_resource_id: |
| 315 params['PhysicalResourceId'] = physical_resource_id |
| 316 return self.get_list('DescribeStackResources', params, |
| 317 [('member', StackResource)]) |
| 318 |
| 319 def describe_stacks(self, stack_name_or_id=None): |
| 320 params = {} |
| 321 if stack_name_or_id: |
| 322 params['StackName'] = stack_name_or_id |
| 323 return self.get_list('DescribeStacks', params, [('member', Stack)]) |
| 324 |
| 325 def get_template(self, stack_name_or_id): |
| 326 params = {'ContentType': "JSON", 'StackName': stack_name_or_id} |
| 327 response = self.make_request('GetTemplate', params, '/', 'GET') |
| 328 body = response.read() |
| 329 if response.status == 200: |
| 330 return json.loads(body) |
| 331 else: |
| 332 boto.log.error('%s %s' % (response.status, response.reason)) |
| 333 boto.log.error('%s' % body) |
| 334 raise self.ResponseError(response.status, response.reason, body) |
| 335 |
| 336 def list_stack_resources(self, stack_name_or_id, next_token=None): |
| 337 params = {'StackName': stack_name_or_id} |
| 338 if next_token: |
| 339 params['NextToken'] = next_token |
| 340 return self.get_list('ListStackResources', params, |
| 341 [('member', StackResourceSummary)]) |
| 342 |
| 343 def list_stacks(self, stack_status_filters=[], next_token=None): |
| 344 params = {} |
| 345 if next_token: |
| 346 params['NextToken'] = next_token |
| 347 if len(stack_status_filters) > 0: |
| 348 self.build_list_params(params, stack_status_filters, |
| 349 "StackStatusFilter.member") |
| 350 |
| 351 return self.get_list('ListStacks', params, |
| 352 [('member', StackSummary)]) |
| 353 |
| 354 def validate_template(self, template_body=None, template_url=None): |
| 355 params = {} |
| 356 if template_body: |
| 357 params['TemplateBody'] = template_body |
| 358 if template_url: |
| 359 params['TemplateURL'] = template_url |
| 360 if template_body and template_url: |
| 361 boto.log.warning("If both TemplateBody and TemplateURL are" |
| 362 " specified, only TemplateBody will be honored by the API") |
| 363 return self.get_object('ValidateTemplate', params, Template, |
| 364 verb="POST") |
OLD | NEW |