Odyssey
cloudformation.py
1 #!/usr/bin/env python
2 '''Hosting Cloudformation Library'''
3 
4 import json
5 import logging
6 import os
7 import sys
8 import time
9 import boto3
10 
11 STACK_TEMPLATE_DIR = '/cloudformation'
12 STACK_TEMPLATE = os.path.join(STACK_TEMPLATE_DIR, 'odyssey.cf.json')
13 LOG = logging.getLogger(__name__)
14 
15 
16 def __get_boto_client__(region):
17  return boto3.client('cloudformation', region_name=region)
18 
19 
20 def __get_stack_name__(cluster, region, name, uuid):
21  return '-'.join((cluster, region, 'odyssey', name, uuid))
22 
23 
24 def __get_changeset_name__(stack_name, git_sha):
25  return '-'.join((stack_name, git_sha))
26 
27 
28 def __read_template_body_as_json__():
29  with open(STACK_TEMPLATE, 'r') as fh:
30  return json.loads(fh.read())
31 
32 
33 def __aws_tags_translation__(tags):
34  return list({'Key': k, 'Value': v} for k, v in tags.items())
35 
36 
37 def __aws_params_translation__(params):
38  return list({'ParameterKey': k,
39  'ParameterValue': v} for k, v in params.items())
40 
41 
42 def __update_template_tasks_environments__(template, env):
43  tasks = [
44  'AppECSTask',
45  'CUManageECSTask',
46  'SchedECSTask',
47  'SSLRedirectECSTask',
48  ]
49  for task in tasks:
50  task_def = template['Resources'][task]['Properties']
51  cdefs = task_def['ContainerDefinitions']
52  for cdef in cdefs:
53  cenv = dict((x['Name'], x['Value']) for x in cdef['Environment'])
54  cenv.update(env)
55  cdef['Environment'] = [{
56  'Name': key,
57  'Value': value
58  } for key, value in cenv.items()]
59  return template
60 
61 
62 def __validate_template__(client, template_body):
63  return client.validate_template(
64  TemplateBody=template_body
65  )
66 
67 
68 def describe_stack(stack, **kwargs):
69  assert stack
70  stack_name = __get_stack_name__(stack.cluster,
71  stack.region,
72  stack.name,
73  stack.uuid)
74  client = __get_boto_client__(stack.region)
75  stack = client.describe_stacks(
76  StackName=stack_name)
77  return stack['Stacks'][-1]
78 
79 
80 def describe_changeset(stack, git_sha, **kwargs):
81  assert stack
82  stack_name = __get_stack_name__(stack.cluster,
83  stack.region,
84  stack.name,
85  stack.uuid)
86  changeset_name = __get_changeset_name__(stack_name, git_sha)
87  client = __get_boto_client__(stack.region)
88  changeset = client.describe_change_set(
89  ChangeSetName=changeset_name,
90  StackName=stack_name
91  )
92  return changeset
93 
94 
95 def watch_stack_progress(stack, **kwargs):
96  while True:
97  try:
98  cfn_stack = describe_stack(stack, **kwargs)
99  if cfn_stack['StackStatus'].endswith('_IN_PROGRESS'):
100  time.sleep(10)
101  print('.', file=sys.stderr, end='')
102  continue
103  else:
104  print('', file=sys.stderr)
105  return cfn_stack
106  except Exception as ex:
107  if kwargs.get('verbose', False):
108  print(ex)
109  return None
110 
111 
112 def watch_changeset_progress(stack, git_sha, **kwargs):
113  while True:
114  changeset = describe_changeset(stack, git_sha, **kwargs)
115  if changeset['Status'] in ['CREATE_PENDING', 'CREATE_IN_PROGRESS']:
116  print('.', file=sys.stderr, end='')
117  time.sleep(10)
118  continue
119  else:
120  print('', file=sys.stderr)
121  return changeset
122 
123 
124 def list_stack_events(stack, **kwargs):
125  assert stack
126  stack_name = __get_stack_name__(stack.cluster,
127  stack.region,
128  stack.name,
129  stack.uuid)
130  client = __get_boto_client__(stack.region)
131  events = client.describe_stack_events(
132  StackName=stack_name)
133  return events['StackEvents']
134 
135 
137  params,
138  tags=None,
139  **kwargs):
140  '''Create AWS Cloudformation Stack'''
141  if tags is None:
142  tags = {}
143  stack_name = __get_stack_name__(stack.cluster,
144  stack.region,
145  stack.name,
146  stack.uuid)
147  stack_template = __update_template_tasks_environments__(
148  __read_template_body_as_json__(),
149  stack.env
150  )
151  template_body = json.dumps(stack_template, indent=4)
152 
153  client = __get_boto_client__(stack.region)
154 
155  if kwargs.get('verbose', False):
156  print(__validate_template__(client, template_body), file=sys.stderr)
157 
158  response = client.create_stack(
159  StackName=stack_name,
160  Parameters=__aws_params_translation__(params),
161  TemplateBody=template_body,
162  Capabilities=['CAPABILITY_NAMED_IAM'],
163  Tags=__aws_tags_translation__(tags))
164  return response.get('StackId')
165 
166 
168  params,
169  git_sha,
170  tags=None,
171  **kwargs):
172  '''Create AWS Cloudformation Changeset'''
173  if tags is None:
174  tags = {}
175  client = __get_boto_client__(stack.region)
176  stack_name = __get_stack_name__(stack.cluster,
177  stack.region,
178  stack.name,
179  stack.uuid)
180  changeset_name = __get_changeset_name__(stack_name, git_sha)
181  stack_template = __update_template_tasks_environments__(
182  __read_template_body_as_json__(),
183  stack.env
184  )
185  template_body = json.dumps(stack_template, indent=2)
186 
187  if kwargs.get('verbose', False):
188  print(__validate_template__(client, template_body), file=sys.stderr)
189 
190  try:
191  response = client.create_change_set(
192  StackName=stack_name,
193  ChangeSetName=changeset_name,
194  TemplateBody=template_body,
195  Parameters=__aws_params_translation__(params),
196  Capabilities=['CAPABILITY_NAMED_IAM'],
197  Tags=__aws_tags_translation__(tags))
198  except Exception as ex:
199  print(ex, file=sys.stderr)
200  return False
201 
202  if kwargs.get('verbose', False):
203  print(response, file=sys.stderr)
204 
205  if response['ResponseMetadata']['HTTPStatusCode'] != 200:
206  return False
207  else:
208  return True
209 
210 
211 def execute_changeset(stack, git_sha, **kwargs):
212  assert stack
213  assert git_sha
214  wait_until_complete = kwargs.get('wait_until_complete', False)
215  stack_name = __get_stack_name__(stack.cluster,
216  stack.region,
217  stack.name,
218  stack.uuid)
219  changeset_name = __get_changeset_name__(stack_name, git_sha)
220  client = __get_boto_client__(stack.region)
221  client.execute_change_set(
222  StackName=stack_name,
223  ChangeSetName=changeset_name)
224  if wait_until_complete:
225  cfn_stack = watch_stack_progress(stack, **kwargs)
226  if cfn_stack['StackStatus'] == 'UPDATE_COMPLETE':
227  return True
228  else:
229  LOG.error(cfn_stack['StackStatusReason'])
230  return False
231  return True
232 
233 
234 def deploy_changeset(stack, git_sha, params, **kwargs):
235  if kwargs.get('verbose', False):
236  print('Creating Changeset...', file=sys.stderr)
237  create_changeset = create_cloudformation_changeset(
238  stack,
239  params,
240  git_sha,
241  **kwargs)
242  changeset = watch_changeset_progress(stack, git_sha, **kwargs)
243 
244  if not (create_changeset or changeset['STATUS'] == 'CREATE_COMPLETE'):
245  raise SystemExit("Changeset creation failed")
246 
247  if kwargs.get('verbose', False):
248  print('Executing Changeset...', file=sys.stderr)
249  return execute_changeset(stack, git_sha, **kwargs)
250 
251 
252 def delete_stack(stack, **kwargs):
253  '''Delete stack'''
254  assert stack
255  stack_name = __get_stack_name__(stack.cluster,
256  stack.region,
257  stack.name,
258  stack.uuid)
259  client = boto3.client('cloudformation', region_name=stack.region)
260  client.delete_stack(StackName=stack_name)
261  if kwargs.get('wait_until_complete', False):
262  watch_stack_progress(stack, **kwargs)
def create_cloudformation_changeset(stack, params, git_sha, tags=None, **kwargs)
def create_cloudformation_stack(stack, params, tags=None, **kwargs)
def delete_stack(stack, **kwargs)