Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/customizations/cloudformation/deployer.py
1567 views
1
# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
14
import sys
15
import time
16
import logging
17
import botocore
18
import collections
19
20
from awscli.compat import get_current_datetime
21
from awscli.customizations.cloudformation import exceptions
22
from awscli.customizations.cloudformation.artifact_exporter import mktempfile, parse_s3_url
23
24
25
LOG = logging.getLogger(__name__)
26
27
ChangeSetResult = collections.namedtuple(
28
"ChangeSetResult", ["changeset_id", "changeset_type"])
29
30
31
class Deployer(object):
32
33
def __init__(self, cloudformation_client,
34
changeset_prefix="awscli-cloudformation-package-deploy-"):
35
self._client = cloudformation_client
36
self.changeset_prefix = changeset_prefix
37
38
def has_stack(self, stack_name):
39
"""
40
Checks if a CloudFormation stack with given name exists
41
42
:param stack_name: Name or ID of the stack
43
:return: True if stack exists. False otherwise
44
"""
45
try:
46
resp = self._client.describe_stacks(StackName=stack_name)
47
if len(resp["Stacks"]) != 1:
48
return False
49
50
# When you run CreateChangeSet on a a stack that does not exist,
51
# CloudFormation will create a stack and set it's status
52
# REVIEW_IN_PROGRESS. However this stack is cannot be manipulated
53
# by "update" commands. Under this circumstances, we treat like
54
# this stack does not exist and call CreateChangeSet will
55
# ChangeSetType set to CREATE and not UPDATE.
56
stack = resp["Stacks"][0]
57
return stack["StackStatus"] != "REVIEW_IN_PROGRESS"
58
59
except botocore.exceptions.ClientError as e:
60
# If a stack does not exist, describe_stacks will throw an
61
# exception. Unfortunately we don't have a better way than parsing
62
# the exception msg to understand the nature of this exception.
63
msg = str(e)
64
65
if "Stack with id {0} does not exist".format(stack_name) in msg:
66
LOG.debug("Stack with id {0} does not exist".format(
67
stack_name))
68
return False
69
else:
70
# We don't know anything about this exception. Don't handle
71
LOG.debug("Unable to get stack details.", exc_info=e)
72
raise e
73
74
def create_changeset(self, stack_name, cfn_template,
75
parameter_values, capabilities, role_arn,
76
notification_arns, s3_uploader, tags):
77
"""
78
Call Cloudformation to create a changeset and wait for it to complete
79
80
:param stack_name: Name or ID of stack
81
:param cfn_template: CloudFormation template string
82
:param parameter_values: Template parameters object
83
:param capabilities: Array of capabilities passed to CloudFormation
84
:param tags: Array of tags passed to CloudFormation
85
:return:
86
"""
87
88
now = get_current_datetime().isoformat()
89
description = "Created by AWS CLI at {0} UTC".format(now)
90
91
# Each changeset will get a unique name based on time
92
changeset_name = self.changeset_prefix + str(int(time.time()))
93
94
if not self.has_stack(stack_name):
95
changeset_type = "CREATE"
96
# When creating a new stack, UsePreviousValue=True is invalid.
97
# For such parameters, users should either override with new value,
98
# or set a Default value in template to successfully create a stack.
99
parameter_values = [x for x in parameter_values
100
if not x.get("UsePreviousValue", False)]
101
else:
102
changeset_type = "UPDATE"
103
# UsePreviousValue not valid if parameter is new
104
summary = self._client.get_template_summary(StackName=stack_name)
105
existing_parameters = [parameter['ParameterKey'] for parameter in \
106
summary['Parameters']]
107
parameter_values = [x for x in parameter_values
108
if not (x.get("UsePreviousValue", False) and \
109
x["ParameterKey"] not in existing_parameters)]
110
111
kwargs = {
112
'ChangeSetName': changeset_name,
113
'StackName': stack_name,
114
'TemplateBody': cfn_template,
115
'ChangeSetType': changeset_type,
116
'Parameters': parameter_values,
117
'Capabilities': capabilities,
118
'Description': description,
119
'Tags': tags,
120
}
121
122
# If an S3 uploader is available, use TemplateURL to deploy rather than
123
# TemplateBody. This is required for large templates.
124
if s3_uploader:
125
with mktempfile() as temporary_file:
126
temporary_file.write(kwargs.pop('TemplateBody'))
127
temporary_file.flush()
128
url = s3_uploader.upload_with_dedup(
129
temporary_file.name, "template")
130
# TemplateUrl property requires S3 URL to be in path-style format
131
parts = parse_s3_url(url, version_property="Version")
132
kwargs['TemplateURL'] = s3_uploader.to_path_style_s3_url(parts["Key"], parts.get("Version", None))
133
134
# don't set these arguments if not specified to use existing values
135
if role_arn is not None:
136
kwargs['RoleARN'] = role_arn
137
if notification_arns is not None:
138
kwargs['NotificationARNs'] = notification_arns
139
try:
140
resp = self._client.create_change_set(**kwargs)
141
return ChangeSetResult(resp["Id"], changeset_type)
142
except Exception as ex:
143
LOG.debug("Unable to create changeset", exc_info=ex)
144
raise ex
145
146
def wait_for_changeset(self, changeset_id, stack_name):
147
"""
148
Waits until the changeset creation completes
149
150
:param changeset_id: ID or name of the changeset
151
:param stack_name: Stack name
152
:return: Latest status of the create-change-set operation
153
"""
154
sys.stdout.write("\nWaiting for changeset to be created..\n")
155
sys.stdout.flush()
156
157
# Wait for changeset to be created
158
waiter = self._client.get_waiter("change_set_create_complete")
159
# Poll every 5 seconds. Changeset creation should be fast
160
waiter_config = {'Delay': 5}
161
try:
162
waiter.wait(ChangeSetName=changeset_id, StackName=stack_name,
163
WaiterConfig=waiter_config)
164
except botocore.exceptions.WaiterError as ex:
165
LOG.debug("Create changeset waiter exception", exc_info=ex)
166
167
resp = ex.last_response
168
status = resp["Status"]
169
reason = resp["StatusReason"]
170
171
if status == "FAILED" and \
172
"The submitted information didn't contain changes." in reason or \
173
"No updates are to be performed" in reason:
174
raise exceptions.ChangeEmptyError(stack_name=stack_name)
175
176
raise RuntimeError("Failed to create the changeset: {0} "
177
"Status: {1}. Reason: {2}"
178
.format(ex, status, reason))
179
180
def execute_changeset(self, changeset_id, stack_name,
181
disable_rollback=False):
182
"""
183
Calls CloudFormation to execute changeset
184
185
:param changeset_id: ID of the changeset
186
:param stack_name: Name or ID of the stack
187
:param disable_rollback: Disable rollback of all resource changes
188
:return: Response from execute-change-set call
189
"""
190
return self._client.execute_change_set(
191
ChangeSetName=changeset_id,
192
StackName=stack_name,
193
DisableRollback=disable_rollback)
194
195
def wait_for_execute(self, stack_name, changeset_type):
196
197
sys.stdout.write("Waiting for stack create/update to complete\n")
198
sys.stdout.flush()
199
200
# Pick the right waiter
201
if changeset_type == "CREATE":
202
waiter = self._client.get_waiter("stack_create_complete")
203
elif changeset_type == "UPDATE":
204
waiter = self._client.get_waiter("stack_update_complete")
205
else:
206
raise RuntimeError("Invalid changeset type {0}"
207
.format(changeset_type))
208
209
# Poll every 30 seconds. Polling too frequently risks hitting rate limits
210
# on CloudFormation's DescribeStacks API
211
waiter_config = {
212
'Delay': 30,
213
'MaxAttempts': 120,
214
}
215
216
try:
217
waiter.wait(StackName=stack_name, WaiterConfig=waiter_config)
218
except botocore.exceptions.WaiterError as ex:
219
LOG.debug("Execute changeset waiter exception", exc_info=ex)
220
221
raise exceptions.DeployFailedError(stack_name=stack_name)
222
223
def create_and_wait_for_changeset(self, stack_name, cfn_template,
224
parameter_values, capabilities, role_arn,
225
notification_arns, s3_uploader, tags):
226
227
result = self.create_changeset(
228
stack_name, cfn_template, parameter_values, capabilities,
229
role_arn, notification_arns, s3_uploader, tags)
230
self.wait_for_changeset(result.changeset_id, stack_name)
231
232
return result
233
234