Path: blob/develop/awscli/customizations/cloudformation/deployer.py
1567 views
# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.1#2# Licensed under the Apache License, Version 2.0 (the "License"). You3# may not use this file except in compliance with the License. A copy of4# the License is located at5#6# http://aws.amazon.com/apache2.0/7#8# or in the "license" file accompanying this file. This file is9# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF10# ANY KIND, either express or implied. See the License for the specific11# language governing permissions and limitations under the License.1213import sys14import time15import logging16import botocore17import collections1819from awscli.compat import get_current_datetime20from awscli.customizations.cloudformation import exceptions21from awscli.customizations.cloudformation.artifact_exporter import mktempfile, parse_s3_url222324LOG = logging.getLogger(__name__)2526ChangeSetResult = collections.namedtuple(27"ChangeSetResult", ["changeset_id", "changeset_type"])282930class Deployer(object):3132def __init__(self, cloudformation_client,33changeset_prefix="awscli-cloudformation-package-deploy-"):34self._client = cloudformation_client35self.changeset_prefix = changeset_prefix3637def has_stack(self, stack_name):38"""39Checks if a CloudFormation stack with given name exists4041:param stack_name: Name or ID of the stack42:return: True if stack exists. False otherwise43"""44try:45resp = self._client.describe_stacks(StackName=stack_name)46if len(resp["Stacks"]) != 1:47return False4849# When you run CreateChangeSet on a a stack that does not exist,50# CloudFormation will create a stack and set it's status51# REVIEW_IN_PROGRESS. However this stack is cannot be manipulated52# by "update" commands. Under this circumstances, we treat like53# this stack does not exist and call CreateChangeSet will54# ChangeSetType set to CREATE and not UPDATE.55stack = resp["Stacks"][0]56return stack["StackStatus"] != "REVIEW_IN_PROGRESS"5758except botocore.exceptions.ClientError as e:59# If a stack does not exist, describe_stacks will throw an60# exception. Unfortunately we don't have a better way than parsing61# the exception msg to understand the nature of this exception.62msg = str(e)6364if "Stack with id {0} does not exist".format(stack_name) in msg:65LOG.debug("Stack with id {0} does not exist".format(66stack_name))67return False68else:69# We don't know anything about this exception. Don't handle70LOG.debug("Unable to get stack details.", exc_info=e)71raise e7273def create_changeset(self, stack_name, cfn_template,74parameter_values, capabilities, role_arn,75notification_arns, s3_uploader, tags):76"""77Call Cloudformation to create a changeset and wait for it to complete7879:param stack_name: Name or ID of stack80:param cfn_template: CloudFormation template string81:param parameter_values: Template parameters object82:param capabilities: Array of capabilities passed to CloudFormation83:param tags: Array of tags passed to CloudFormation84:return:85"""8687now = get_current_datetime().isoformat()88description = "Created by AWS CLI at {0} UTC".format(now)8990# Each changeset will get a unique name based on time91changeset_name = self.changeset_prefix + str(int(time.time()))9293if not self.has_stack(stack_name):94changeset_type = "CREATE"95# When creating a new stack, UsePreviousValue=True is invalid.96# For such parameters, users should either override with new value,97# or set a Default value in template to successfully create a stack.98parameter_values = [x for x in parameter_values99if not x.get("UsePreviousValue", False)]100else:101changeset_type = "UPDATE"102# UsePreviousValue not valid if parameter is new103summary = self._client.get_template_summary(StackName=stack_name)104existing_parameters = [parameter['ParameterKey'] for parameter in \105summary['Parameters']]106parameter_values = [x for x in parameter_values107if not (x.get("UsePreviousValue", False) and \108x["ParameterKey"] not in existing_parameters)]109110kwargs = {111'ChangeSetName': changeset_name,112'StackName': stack_name,113'TemplateBody': cfn_template,114'ChangeSetType': changeset_type,115'Parameters': parameter_values,116'Capabilities': capabilities,117'Description': description,118'Tags': tags,119}120121# If an S3 uploader is available, use TemplateURL to deploy rather than122# TemplateBody. This is required for large templates.123if s3_uploader:124with mktempfile() as temporary_file:125temporary_file.write(kwargs.pop('TemplateBody'))126temporary_file.flush()127url = s3_uploader.upload_with_dedup(128temporary_file.name, "template")129# TemplateUrl property requires S3 URL to be in path-style format130parts = parse_s3_url(url, version_property="Version")131kwargs['TemplateURL'] = s3_uploader.to_path_style_s3_url(parts["Key"], parts.get("Version", None))132133# don't set these arguments if not specified to use existing values134if role_arn is not None:135kwargs['RoleARN'] = role_arn136if notification_arns is not None:137kwargs['NotificationARNs'] = notification_arns138try:139resp = self._client.create_change_set(**kwargs)140return ChangeSetResult(resp["Id"], changeset_type)141except Exception as ex:142LOG.debug("Unable to create changeset", exc_info=ex)143raise ex144145def wait_for_changeset(self, changeset_id, stack_name):146"""147Waits until the changeset creation completes148149:param changeset_id: ID or name of the changeset150:param stack_name: Stack name151:return: Latest status of the create-change-set operation152"""153sys.stdout.write("\nWaiting for changeset to be created..\n")154sys.stdout.flush()155156# Wait for changeset to be created157waiter = self._client.get_waiter("change_set_create_complete")158# Poll every 5 seconds. Changeset creation should be fast159waiter_config = {'Delay': 5}160try:161waiter.wait(ChangeSetName=changeset_id, StackName=stack_name,162WaiterConfig=waiter_config)163except botocore.exceptions.WaiterError as ex:164LOG.debug("Create changeset waiter exception", exc_info=ex)165166resp = ex.last_response167status = resp["Status"]168reason = resp["StatusReason"]169170if status == "FAILED" and \171"The submitted information didn't contain changes." in reason or \172"No updates are to be performed" in reason:173raise exceptions.ChangeEmptyError(stack_name=stack_name)174175raise RuntimeError("Failed to create the changeset: {0} "176"Status: {1}. Reason: {2}"177.format(ex, status, reason))178179def execute_changeset(self, changeset_id, stack_name,180disable_rollback=False):181"""182Calls CloudFormation to execute changeset183184:param changeset_id: ID of the changeset185:param stack_name: Name or ID of the stack186:param disable_rollback: Disable rollback of all resource changes187:return: Response from execute-change-set call188"""189return self._client.execute_change_set(190ChangeSetName=changeset_id,191StackName=stack_name,192DisableRollback=disable_rollback)193194def wait_for_execute(self, stack_name, changeset_type):195196sys.stdout.write("Waiting for stack create/update to complete\n")197sys.stdout.flush()198199# Pick the right waiter200if changeset_type == "CREATE":201waiter = self._client.get_waiter("stack_create_complete")202elif changeset_type == "UPDATE":203waiter = self._client.get_waiter("stack_update_complete")204else:205raise RuntimeError("Invalid changeset type {0}"206.format(changeset_type))207208# Poll every 30 seconds. Polling too frequently risks hitting rate limits209# on CloudFormation's DescribeStacks API210waiter_config = {211'Delay': 30,212'MaxAttempts': 120,213}214215try:216waiter.wait(StackName=stack_name, WaiterConfig=waiter_config)217except botocore.exceptions.WaiterError as ex:218LOG.debug("Execute changeset waiter exception", exc_info=ex)219220raise exceptions.DeployFailedError(stack_name=stack_name)221222def create_and_wait_for_changeset(self, stack_name, cfn_template,223parameter_values, capabilities, role_arn,224notification_arns, s3_uploader, tags):225226result = self.create_changeset(227stack_name, cfn_template, parameter_values, capabilities,228role_arn, notification_arns, s3_uploader, tags)229self.wait_for_changeset(result.changeset_id, stack_name)230231return result232233234