Odyssey
migrations.py
1 #!/usr/bin/env python
2 '''Stack migration definitions
3 
4 The migration definitions are used to defined the specific functions
5 used for migrating stacks between specific versions.
6 
7 The `MIGRATIONS` dictionary contains the mappings between
8 `'%(from)s-%(to)s'` version strings and the specific function to
9 perform the migration. The specific migration functions should be
10 denoted as private to this module.
11 
12 '''
13 
14 
15 def migrate_083_084(stack, **kwargs):
16  try:
17  # Move meta variables to regular values
18  region = stack.region
19  cluster = stack.cluster
20  created = stack.created
21  creator = stack.creator
22  uuid = stack.uuid
23  # Remove meta variables
24  stack.unset('region', meta=True)
25  stack.unset('cluster', meta=True)
26  stack.unset('created', meta=True)
27  stack.unset('creator', meta=True)
28  stack.unset('uuid', meta=True)
29  stack.unset('stack_version', meta=True)
30  stack.unset('state', meta=True)
31  # Add old meta variables as non-meta
32  stack.set('region', region)
33  stack.set('cluster', cluster)
34  stack.set('created', created)
35  stack.set('creator', creator)
36  stack.set('uuid', uuid)
37  stack.set('stack_version', '0.8.4')
38  stack.set('state', 'active')
39  # Remove Resources Key, These are no longer used since 0.8.0.
40  stack.unset('resources')
41 
42  # Save
43  stack._save(changelog='Migrated stack from 0.8.3 to 0.8.4')
44  except Exception as ex:
45  if kwargs.get('verbose', False):
46  print(ex)
47  stack.update_state('corrupt')
48 
49 
50 def migrate_082_083(stack, **kwargs):
51  import os
52  from . import stack as stk
53 
54  try:
55  git_sha = os.environ['GIT_REV']
56 
57  environment_name = 'homer'
58 
59  stack.set('environment_name', environment_name)
60 
61  stk.upgrade_cfn_stack(stack, git_sha, **kwargs)
62 
63  stack.set('stack_version', '0.8.3', meta=True)
64  stack.update_state('active')
65  except Exception as ex:
66  if kwargs.get('verbose', False):
67  print(ex)
68  stack.update_state('corrupt')
69 
70 
71 def migrate_081_082(stack, **kwargs):
72  import os
73  from . import stack as stk
74 
75  try:
76  git_sha = os.environ['GIT_REV']
77 
78  environment = 'prod' if stack.name == 'prod' else 'dev'
79 
80  stack.set('environment', environment)
81 
82  stk.upgrade_cfn_stack(stack, git_sha, **kwargs)
83 
84  stack.set('stack_version', '0.8.2', meta=True)
85  stack.update_state('active')
86  except Exception as ex:
87  if kwargs.get('verbose', False):
88  print(ex)
89  stack.update_state('corrupt')
90 
91 
92 def migrate_080_081(stack, **kwargs):
93  import os
94  from . import stack as stk
95 
96  try:
97  git_sha = os.environ['GIT_REV']
98 
99  stk.upgrade_cfn_stack(stack, git_sha, **kwargs)
100 
101  stack.set('stack_version', '0.8.1', meta=True)
102  stack.update_state('active')
103  except Exception as ex:
104  if kwargs.get('verbose', False):
105  print(ex)
106  stack.update_state('corrupt')
107 
108 
109 def migrate_071_080(stack, **kwargs):
110 
111  import os
112  import shellish
113  import boto3
114  import uuid
115  from . import stack as stk
116  from . import cloudformation
117 
118  def describe_service(cluster, service, region):
119  client = boto3.client('ecs', region_name=region)
120  return client.describe_services(
121  cluster=cluster,
122  services=[service]
123  ).get('services', [None])[0]
124 
125  def remove_elb_target_group(region, arn=None):
126  elb_api = boto3.client('elbv2', region_name=region)
127  elb_api.delete_target_group(TargetGroupArn=arn)
128 
129  def remove_elb_listener(region, arn=None):
130  elb_api = boto3.client('elbv2', region_name=region)
131  elb_api.delete_listener(ListenerArn=arn)
132 
133  def remove_elb(region, arn=None):
134  elb_api = boto3.client('elbv2', region_name=region)
135  elb_api.delete_load_balancer(LoadBalancerArn=arn)
136 
137  def remove_ecs_service(region, cluster=None, name=None):
138  ecs_api = boto3.client('ecs', region_name=region)
139  ecs_api.update_service(cluster=cluster, service=name, desiredCount=0)
140  ecs_api.delete_service(cluster=cluster, service=name)
141 
142  def remove_task_definition(region, family=None):
143  ecs_api = boto3.client('ecs', region_name=region)
144  paginator = ecs_api.get_paginator('list_task_definitions')
145  for page in paginator.paginate(familyPrefix=family, status='ACTIVE'):
146  for arn in page['taskDefinitionArns']:
147  print("\tDeregistering: %s" % arn)
148  ecs_api.deregister_task_definition(taskDefinition=arn)
149 
150  def remove_dns(region, name=None, record=None, hosted_zone_id=None):
151  change = {
152  'Changes': [{
153  'Action': 'DELETE',
154  'ResourceRecordSet': record
155  }]
156  }
157  r53_api = boto3.client('route53')
158  r53_api.change_resource_record_sets(HostedZoneId=hosted_zone_id,
159  ChangeBatch=change)
160 
161  def create_cfn_stack(stack, **kwargs):
162 
163  domain = '.'.join(stack.dns[0].split('.')[1:])
164  hostnames = [x.split('.')[0] for x in stack.dns]
165  app_service = describe_service(
166  stack.cluster,
167  'odyssey-%s-app' % stack.name,
168  stack.region)
169  stack.set('app_service_count', app_service['desiredCount'])
170  stack.set('uuid', str(uuid.uuid4()), meta=True)
171  stack.set('version', stack.name)
172 
173  stk.create_stack_aws_resources(
174  stack,
175  region=stack.region,
176  ecs_cluster=stack.cluster,
177  elb_security_group='homer-elb-sg',
178  elb_internal_security_group='homer-int-elb-sg',
179  eni_security_group='homer-ecs-sg',
180  rds_config='default',
181  pub_subnet_prefix='homer-public',
182  priv_subnet_prefix='homer-private',
183  https_cert_hostname='*',
184  domain=domain,
185  hostnames=hostnames,
186  create_database=False,
187  **kwargs,
188  )
189 
190  def remove_resources(stack):
191  resource_removers = {
192  'elb': remove_elb,
193  'elb-target-group': remove_elb_target_group,
194  'elb-listener': remove_elb_listener,
195  'ecs-service': remove_ecs_service,
196  'ecs-task-definition': remove_task_definition,
197  'dns': remove_dns,
198  }
199 
200  try:
201  for x in reversed(stack.resources):
202  print('Removing: %s - %s' % (x['kind'], x['ident']))
203  try:
204  resource_removers[x['kind']](stack.region, **x['options'])
205  except Exception as e:
206  raise e from None
207  except BaseException as e:
208  shellish.vtmlprint("<red><b>CRITICAL ERROR: %s" % e)
209  stack.update_state('corrupt')
210  raise e
211 
212  if stack is None:
213  raise SystemExit('Stack not found: %s' % stack.name)
214 
215  try:
216  create_cfn_stack(stack, **{**kwargs, **{'IsMigration': 'True'}})
217 
218  cfn_stack = cloudformation.watch_stack_progress(stack, **kwargs)
219 
220  if kwargs.get('verbose', False):
221  print(cfn_stack)
222 
223  if cfn_stack is None or 'ROLLBACK' in cfn_stack['StackStatus']:
224  stack.update_state('corrupt')
225  raise SystemExit("Unable to create cloudformation resources")
226 
227  remove_resources(stack)
228 
229  git_sha = os.environ['GIT_REV']
230 
231  stk.upgrade_cfn_stack(stack, git_sha, **kwargs)
232 
233  stack.set('stack_version', '0.8.0', meta=True)
234  stack.update_state('active')
235 
236  except Exception as ex:
237  if kwargs.get('verbose', False):
238  print(ex)
239  stack.update_state('corrupt')
240 
241 
242 MIGRATIONS = {
243  '0.7.1-0.8.0': migrate_071_080,
244  '0.8.0-0.8.1': migrate_080_081,
245  '0.8.1-0.8.2': migrate_081_082,
246  '0.8.2-0.8.3': migrate_082_083,
247  '0.8.3-0.8.4': migrate_083_084,
248 }
249 
250 
251 def migrate_stack(stack, from_version, target_version, **kwargs):
252  '''Entry point for stack migration'''
253  migration_str = '%s-%s' % (from_version, target_version)
254  if migration_str not in MIGRATIONS:
255  raise SystemExit("No migration path found for %s" % migration_str)
256 
257  MIGRATIONS[migration_str](stack, **kwargs)
def migrate_stack(stack, from_version, target_version, **kwargs)
Definition: migrations.py:251