forked from stelligent/empty-stack
-
Notifications
You must be signed in to change notification settings - Fork 0
/
empty_stack.py
201 lines (185 loc) · 7.02 KB
/
empty_stack.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"""
empty_stack.py creates an empty cloudformation stack, then updates the stack.
Creating the stack without infrastructure allows rollbacks that don't require
the stack to be deleted on an error.
Example usage:
python3 empty_stack.py --name main_vpc --template vpc.yaml --parameters params.json
"""
import logging
import sys
import boto3
import optparse
def setup_logging(log_stream=sys.stdout, log_level=logging.INFO):
"""Sets up logging."""
logging.basicConfig(level=options.log)
log = logging.getLogger(__name__)
out_hdlr = logging.StreamHandler(log_stream)
out_hdlr.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
# out_hdlr.setLevel(logging.INFO)
log.addHandler(out_hdlr)
log.setLevel(log_level)
return log
def stack_exists(stack_name, token):
"""Determines if CloudFormation stack exists with given name."""
if token:
response = cfn.list_stacks(NextToken=token)
else:
response = cfn.list_stacks()
if 'StackSummaries' in response:
stacks = response['StackSummaries']
for stack in stacks:
if stack['StackName'] == stack_name:
if stack['StackStatus'] != 'DELETE_COMPLETE':
logger.info('Found existing stack with name: %s', stack_name)
return True
if 'NextToken' in response:
return stack_exists(stack_name, response['NextToken'])
return False
def create_empty_stack(stack_name, cfn):
"""Create CloudFormation stack with no infrastructure."""
json = """
{
'AWSTemplateFormatVersion' : '2010-09-09',
'Conditions' : {
'HasNot': { 'Fn::Equals' : [ 'a', 'b' ] }
},
'Resources' : {
'NullResource' : {
'Type' : 'Custom::NullResource',
'Condition' : 'HasNot'
}
}
}
"""
cfn.create_stack(
StackName=stack_name,
TemplateBody=json
)
waiter = cfn.get_waiter('stack_create_complete')
waiter.wait(
StackName=stack_name,
WaiterConfig={
'Delay': 3,
'MaxAttempts': 100
}
)
logger.info(' Successfully created stack: %s', stack_name)
return 0
def update_stack(stack_name, template, parameters, iam, cfn):
"""Update existing CloudFormation stack."""
iam_capabilities = []
if iam:
iam_capabilities = ['CAPABILITY_NAMED_IAM']
try:
cfn.update_stack(
StackName=stack_name,
TemplateBody=template,
Parameters=parameters,
Capabilities=iam_capabilities
)
waiter = cfn.get_waiter('stack_update_complete')
waiter.wait(
StackName=stack_name,
WaiterConfig={
'Delay': 10,
'MaxAttempts': 180
}
)
except Exception as e:
a = str(e).split(':')
end = len(a)
if 'No updates' in a[end -1]:
logger.info(stack_name+ " : " + a[end -1])
else:
logger.error(e)
sys.exit(1)
logger.info(' Successfully updated stack: %s', stack_name)
return 0
def update_stack_url(stack_name, templateURL, parameters, iam, cfn):
"""Update existing CloudFormation stack."""
iam_capabilities = []
if iam:
iam_capabilities = ['CAPABILITY_NAMED_IAM']
try:
cfn.update_stack(
StackName=stack_name,
TemplateURL=templateURL,
Parameters=parameters,
Capabilities=iam_capabilities
)
waiter = cfn.get_waiter('stack_update_complete')
waiter.wait(
StackName=stack_name,
WaiterConfig={
'Delay': 10,
'MaxAttempts': 180
}
)
except Exception as e:
a = str(e).split(':')
end = len(a)
if 'No updates' in a[end -1]:
logger.info(stack_name+ " : " + a[end -1])
else:
logger.error(e)
sys.exit(1)
logger.info('Successfully updated stack: %s', stack_name)
return 0
def parse_params(params_file):
"""Parse parameters into list. If no params passed in, return empty list."""
params = []
if params_file:
import json
logger.info('Parsing parameters from file: %s', params_file)
with open(params_file) as file:
read_data = file.read()
params = json.loads(read_data)
return params
def cfn_conn(region):
cfn = boto3.client('cloudformation', region_name=region)
return cfn
if __name__ == '__main__':
global options,cft
argv = None
parser = optparse.OptionParser(usage="%prog", version="%prog 1.0.0")
parser.add_option("-n", "--name", dest="stack_name", type="string", help="Name of stack to update.")
parser.add_option("-t", "--template", dest="template", default=False,type="string", help="Cloudformation template file location.")
parser.add_option("-u", "--templateURL", dest="templateURL", type="string", help="Cloudformation template url location.")
parser.add_option("-p", "--parameters", dest="params_file", type="string", help="Parameter file to use with the CloudFormation template.")
parser.add_option("-i", "--iam", dest="iam", default=False, help="Add this flag to use iam capabilites.")
parser.add_option("-r", "--region", dest="region", default="us-west-2", type="string", help="set aws region")
parser.add_option("-l", "--log", dest="log", type="string",
default='WARNING', help="Set log level DEBUG,INFO,WARNING")
if argv is None:
argv = sys.argv
(options, args) = parser.parse_args(args=argv[1:])
logger = setup_logging(log_level=options.log)
stack_name = options.stack_name
template = options.template
templateURL = options.templateURL
params_file = options.params_file
iam = options.iam
region = options.region
def cli(stack_name, template, templateURL, params_file, iam, cfn):
"""Command Line Interface logic"""
if not stack_exists(stack_name, None):
logger.info(' Creating stack with name: %s', stack_name)
create_empty_stack(stack_name, cfn)
params = parse_params(params_file)
if template:
logger.info(' Reading template from file: %s', stack_name)
with open(template) as file:
read_data = file.read()
if len(read_data) <= 51200:
logger.info('Updating stack: %s', stack_name)
update_stack(stack_name, read_data, params, iam, cfn)
else:
logger.error(" CFN Template file size is to large to use as a local file, please upload to s3 and use --templateURL instead")
elif templateURL != None:
logger.info(' Using templateUrl from URL: %s', templateURL)
update_stack_url(stack_name, templateURL, params, iam, cfn)
else:
logger.error(' No Cloudformation templates selected!')
# pylint: disable=no-value-for-parameter
cfn = cfn_conn(region)
sys.exit(cli(stack_name, template, templateURL, params_file, iam, cfn))