2 AWS stack provisioning. 17 from pwgen
import pwgen
18 from .
import s3, base, migrations, cloudformation, cumanage
19 from .
import REGISTRIES
20 from .
import get_client_oauth_data
23 RDS_CONFIG_BUCKET =
'homecu.odyssey.rds' 24 RDS_CONFIG_PATH_FMT =
'%(region)s/%(ecs_cluster)s/%(rds_config)s.json' 28 """ Manage entire odyssey stacks in AWS. """ 32 def setup_args(self, parser):
33 self.add_subcommand(ListCommand, default=
True)
34 self.add_subcommand(ShowCommand)
35 self.add_subcommand(MonitorCommand)
36 self.add_subcommand(SetEnvCommand)
37 self.add_subcommand(GetEnvCommand)
38 self.add_subcommand(SetStackStateCommand)
39 self.add_subcommand(ScaleCommand)
40 self.add_subcommand(StartMaintCommand)
41 self.add_subcommand(EndMaintCommand)
42 self.add_subcommand(MigrationCommand)
43 self.add_subcommand(CreateCommand)
44 self.add_subcommand(UpgradeCommand)
45 self.add_subcommand(RemoveCommand)
49 """ List Odyssey stacks used in AWS. """ 56 'partial_deploy':
'red',
63 '''List Command Initialization''' 85 def setup_args(self, parser):
87 self.add_argument(
'--verbose',
'-v', action=
'store_true',
88 help=
'Enable verbose output')
93 with shellish.Table(headers=headers, **table_options)
as t:
96 for summary
in self.
stacks.objects.all():
98 output.append([y
for (x, y)
in stack.items()
if x
in headers])
101 def _get_stack_data(self, stack):
102 now = base.localnow()
103 if stack.deploy.get(
'updated'):
104 delta = now - base.parse_ts(stack.deploy.get(
'updated'))
105 deployed =
'%s ago' % humanize.naturaldelta(delta)
108 style = self.
state_style.get(stack.state,
'reverse')
109 status =
'<%s>%s</%s>' % (style, stack.state, style)
110 if stack.env.get(
'MAINTENANCE_MODE') ==
'1':
111 status =
'%s (<red>MAINTENANCE</red>)' % status
112 return collections.OrderedDict((
113 (
'Stack', stack.name),
114 (
'Stack Version', stack.stack_version),
116 (
'Version', stack.version),
117 (
'Creator', stack.creator.split(
'@', 1)[0]),
118 (
'URL(s)',
' '.join(
'https://%s/' % x
for x
in stack.dns)),
119 (
'Region', stack.region),
120 (
'Created', humanize.naturaldate(base.parse_ts(stack.created))),
121 (
'Last Deploy', deployed)
126 """ Show full detail of a stack. """ 129 min_version =
'0.8.0' 131 allowed_states = {
'init',
'ready',
'active',
'partial_deploy',
'corrupt'}
133 def setup_args(self, parser):
139 raise SystemExit(
'Stack not found: %s' % args.stack)
142 t = shellish.Table(headers=[
'Field',
'Value'],
143 title=
'Stack: %s' % args.stack)
145 (
'Name', stack.name),
146 (
'State', stack.state),
147 (
'DNS',
', '.join(stack.dns)),
148 (
'Stack Version', stack.stack_version),
149 (
'Creator', stack.creator),
150 (
'Region', stack.region),
151 (
'Cluster', stack.cluster),
152 (
'Created', base.formatdatetime(base.parse_ts(stack.created))),
153 (
'Deploy Count', stack.deploy.get(
'count', 0)),
155 if hasattr(stack,
'db'):
156 t.print_row((
'Database',
'%s@%s:%d' % (stack.db[
'name'],
157 stack.db[
'host'], stack.db[
'port'])))
163 t = shellish.Table(headers=[
'Field',
'Value'],
164 title=
'Current Deployment')
166 (
'Updated', base.formatdatetime(base.parse_ts(
168 (
'Project', dep[
'project']),
169 (
'User/Machine', dep[
'build_ident']),
170 (
'GIT Rev', dep[
'git_rev']),
171 (
'GIT Branch', dep[
'git_branch']),
172 (
'GIT Repo', dep[
'git_repo']),
173 (
'GitHub',
'%s/commit/%s' % (
174 dep[
'git_repo'].rstrip(
'.git'), dep[
'git_rev'][:10])),
175 (
'Docker Version', dep[
'docker_version']),
180 cfn_stack = cloudformation.describe_stack(stack)
181 services = [x[
'OutputValue']
for x
in cfn_stack[
'Outputs']
182 if 'Service' in x[
'OutputKey']]
184 ecs_api = boto3.client(
'ecs', region_name=stack.region)
185 services = ecs_api.describe_services(
186 cluster=stack.cluster,
189 t = shellish.Table(headers=[
198 ], title=
"Container Services")
200 for service
in services:
201 for deploy
in service[
'deployments']:
203 service[
'serviceName'].rsplit(
'-')[-2],
205 deploy[
'taskDefinition'].rsplit(
'/', 1)[-1],
206 deploy[
'desiredCount'],
207 deploy[
'pendingCount'],
208 deploy[
'runningCount'],
209 humanize.naturaldelta(base.localnow() -
210 deploy[
'createdAt']),
211 humanize.naturaldelta(base.localnow() -
212 deploy[
'updatedAt']),
219 t = shellish.Table(headers=[
'Variable',
'Value'],
221 t.print(sorted(stack.env.items()))
226 "title":
'Change Log',
227 "headers": [
'Date',
'Change',
'Operator'],
229 lambda x: base.formatdatetime(base.parse_ts(x[
'ts'])),
234 with shellish.Table(**tbl_config)
as t:
235 t.print(reversed(stack.get_changelog()))
239 """ Set the number of application containers to run in the cluster. """ 242 allowed_states = {
'active',
'partial_deploy'}
245 def setup_args(self, parser):
246 self.add_argument(
'container_count', type=int, help=
'Set the service ' 247 '<i>desired</i> count to this value.')
253 raise SystemExit(
'Stack not found: %s' % args.stack)
255 stack.check_passphrase()
256 stack.set(
'app_service_count', args.container_count)
259 cs_result = cloudformation.deploy_changeset(
261 os.environ.get(
'GIT_REV'),
266 stack._save(
'Changed container count of `app` to %d' %
267 args.container_count)
269 stack._save(
'Scaling app container failed')
270 stack.update_stack(
'corrupt')
275 allowed_states = {
'ready',
'active',
'partial_deploy'}
277 def setup_args(self, parser):
280 def prerun(self, args):
284 raise SystemExit(
'Stack not found: %s' % args.stack)
286 stack.check_passphrase()
287 self.
ecs = boto3.client(
'ecs', region_name=stack.region)
291 """ Convert task definition environment section to a dict. """ 292 return dict((x[
'name'], x[
'value'])
for x
in task_env)
295 """ Convert task definition environment section to a dict. """ 299 }
for key, value
in env.items()]
302 """ Get cleaned up task definition suitable for 303 register_task_definition. """ 304 family =
'odyssey-%s-%s' % (self.
stack.name, ident)
305 result = self.
ecs.describe_task_definition(taskDefinition=family)
306 fulldef = result[
'taskDefinition']
310 "containerDefinitions",
312 "placementConstraints" 314 return dict((key, value)
for key, value
in fulldef.items()
318 """ Update app service by pointing it to the latest task def. """ 319 common_name =
'odyssey-%s-app' % self.
stack.name
320 self.
ecs.update_service(cluster=self.
stack.cluster,
322 taskDefinition=common_name)
324 def adj_sched_service_count(self, count):
325 sched_service =
'odyssey-%s-sched' % self.
stack.name
326 self.
ecs.update_service(cluster=self.
stack.cluster,
327 service=sched_service, desiredCount=count)
330 """ Patch the ENV for all containers in the app task def. """ 331 for cdef
in taskdef[
'containerDefinitions']:
335 del env[
'MAINTENANCE_MODE']
339 env[
'MAINTENANCE_MODE'] = value
344 """ Put the stack into maintenance mode. 346 <i>Note, the full meaning and extent of maintenance mode is not covered 349 <b>Action Summary:</b> 350 1. Disable public access to web container. 351 2. Shutdown scheduler service. 355 max_version =
'0.7.1' 358 if self.
stack.env.get(
'MAINTENANCE_MODE') ==
'1':
359 raise SystemExit(
"Already in maintenance mode")
362 self.
stack.env[
'MAINTENANCE_MODE'] =
'1' 363 self.
stack._save(
"Starting Maintenance")
367 self.
ecs.register_task_definition(**appdef)
372 """ Resume normal operation of stack currently in maintenance mode. """ 375 max_version =
'0.7.1' 377 def setup_args(self, parser):
381 if self.
stack.env.get(
'MAINTENANCE_MODE') !=
'1':
382 raise SystemExit(
"Maintenance mode is not active")
383 del self.
stack.env[
'MAINTENANCE_MODE']
384 self.
stack._save(
"Ending Maintenance")
388 self.
ecs.register_task_definition(**appdef)
393 """Migrate stack versions 395 Usage: hosting stack migrate {stack-name} --to {target-version} 397 All parameters are required. 399 Backwards migration isn't necessarily supported, however, it's not 400 expressly prohibited either. Such a migration would simply be a 406 min_version =
'0.8.0' 408 def setup_args(self, parser):
410 self.add_argument(
'--passphrase', env=
'ODYSSEY_STACK_PASSPHRASE',
411 help=
'To avoid being prompted for a passphrase on ' 412 'protected stacks, you can set it on the command ' 413 'line or via the env.')
414 self.add_argument(
'stack_version', help=
'Target version of migration')
415 self.add_argument(
'--verbose',
'-v', action=
'store_true',
416 help=
'Show verbose output')
418 def confirm_action(self, message):
419 confirm = input(message)
421 raise SystemExit(
'Aborted')
426 raise SystemExit(
'Stack not found: %s' % args.stack)
427 passphrase = stack.check_passphrase(args.passphrase)
431 stack.stack_version, args.stack_version))
432 migrations.migrate_stack(
436 verbose=args.verbose,
437 passphrase=passphrase,
442 """ Monitor a stack's event log. """ 445 min_version =
'0.8.0' 446 allowed_states = {
'init',
'ready',
'active',
'corrupt',
'partial_deploy'}
448 def setup_args(self, parser):
454 raise SystemExit(
'Stack not found: %s' % args.stack)
456 raise SystemExit(
'No deployments for: %s' % args.stack)
458 ecs_api = boto3.client(
'ecs', region_name=stack.region)
460 cfn_stack = cloudformation.describe_stack(stack)
461 service_keys = [x[
'OutputValue']
for x
in cfn_stack[
'Outputs']
462 if 'Service' in x[
'OutputKey']]
465 services = ecs_api.describe_services(
466 cluster=stack.cluster,
467 services=service_keys,
469 for service
in services:
470 for ev
in service[
'events']:
474 new_events.append((ev[
'createdAt'], ev[
'message']))
478 dt = x[0].strftime(
'%I:%M %p %Z')
479 shellish.vtmlprint(
"[<cyan>%s</cyan>] %s" % (dt, x[1]))
484 """ Set or clear an env variable in a stack configuration. 486 Note that the env var won't take effect until the next deploy. """ 489 allowed_states = {
'ready',
'active',
'corrupt',
'partial_deploy'}
491 def setup_args(self, parser):
493 self.add_argument(
'key', help=
'Env key. Eg. `MY_ENV_VAR`')
494 self.add_argument(
'value', nargs=
'?', help=
'Env value to set. Omit to ' 495 'clear an existing value.')
500 raise SystemExit(
'Stack not found: %s' % args.stack)
502 if args.value
is None:
503 if args.key
not in stack.env:
504 raise SystemExit(
"Not Found: %s" % args.key)
505 print(
"WARNING: Clearing env var:", args.key)
506 del stack.env[args.key]
509 print(
"WARNING: Setting empty env:", args.key)
510 stack.env[args.key] = args.value
511 stack.save(
'Set ENV variable: %s' % args.key)
515 """ Get the value of an env variable from a stack configuration. """ 518 allowed_states = {
'ready',
'active',
'corrupt',
'partial_deploy'}
520 def setup_args(self, parser):
522 self.add_argument(
'key', nargs=
'?', help=
'Env key. Eg. `MY_ENV_VAR`')
527 raise SystemExit(
'Stack not found: %s' % args.stack)
529 if args.key
and args.key
not in stack.env:
530 raise SystemExit(
"Not Found: %s" % args.key)
531 keys = [args.key]
if args.key
else stack.env.keys()
533 print(
'%s=%s' % (key, shlex.quote(stack.env[key])))
537 '''Override stack status''' 540 allowed_states = {
'init',
'ready',
'deploying',
'partial_deploy',
'corrupt'}
542 def setup_args(self, parser):
544 self.add_argument(
'state', help=
'State to set the stack to')
545 self.add_argument(
'--passphrase', env=
'ODYSSEY_STACK_PASSPHRASE',
546 help=
'To avoid being prompted for a passphrase on ' 547 'protected stacks, you can set it on the command ' 548 'line or via the env.')
552 new_state = args.state
554 raise SystemExit(
'Stack not found: %s' % args.stack)
555 if new_state
not in [
'active',
'test']:
556 raise SystemExit(
'Not a valid new state')
557 stack.check_passphrase(args.passphrase)
560 old_state = stack.state
562 if new_state == old_state:
563 raise SystemExit(
"Stack already at desired status")
565 stack.update_state(new_state)
566 stack._save(
'Updated state from %s to %s by %s' % (
569 os.environ[
'OPERATOR_IDENT']))
573 """ Create a new Odyssey stack in AWS <b>[use caution]</b>. """ 576 dns_compat = re.compile(
"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
578 def setup_args(self, parser):
580 self.add_argument(
'--verbose',
'-v',
581 action=
'store_true', default=
False)
582 self.add_argument(
'--region', default=
'us-east-2',
583 help=
'Region to host the stack.')
584 self.add_argument(
'--elb-security-group',
585 default=
'homecu-only-homer-elb-sg',
586 help=
'ELB security group name.')
587 self.add_argument(
'--eni-security-group', default=
'homer-ecs-sg',
588 help=
'ENI security group name.')
589 self.add_argument(
'--elb-int-security-group',
590 default=
'homer-int-elb-sg',
591 help=
'Internal ELB security group name. ' 592 'This security group is used for internal ' 593 'services of the stack, e.g., scheduler.')
594 self.add_argument(
'--pub-subnet-prefix', default=
'homer-public',
595 help=
'Naming prefix scheme for public subnets.')
596 self.add_argument(
'--priv-subnet-prefix', default=
'homer-private',
597 help=
'Naming prefix scheme for private subnets.')
598 self.add_argument(
'--domain', default=
'homecu.io', help=
'Default ' 599 'domain used for cert selection and DNS.')
600 self.add_argument(
'--hostnames', nargs=
'+', help=
'Override the ' 601 'hostname(s) registered for this stack. Use "" for ' 602 'a domain-only DNS entry. By default the stack name ' 604 self.add_argument(
'--https-cert-hostname', default=
'*',
605 help=
'ACM certificate hostname for HTTPS. Note, ' 606 'value is combined with `--domain`.')
607 self.add_argument(
'--ecs-cluster', default=
'homer',
608 help=
'ECS cluster to provision services on.')
609 self.add_argument(
'--rds-config', default=
'default',
610 help=
'Name of config file in S3 used for RDS ' 611 'configuration. The S3 URL is formed from the ' 612 'triplet (region, ecs-cluster, rds-config). E.g. ' 613 '`s3://homecu.odyssey.rds/us-west-2/default/' 615 self.add_argument(
'--env', nargs=
'+', metavar=
"KEY=VALUE",
616 help=
'Supplemental ECS task-definition environment ' 617 'key=value pair(s) for this deploy. Note this is ' 618 'applied to all task definitions for a stack.')
619 self.add_argument(
'--odyssey-version', default=
'latest',
620 help=
'Odyssey Tag or version to deploy')
621 self.add_argument(
'--environment', default=
'dev',
622 help=(
'Environment tag for stack, e.g., dev or prod' 623 '*Notice* This is not to be confused with the' 625 self.add_argument(
'--environment_name', default=
'homer',
626 help=
'Environment Name for the stack, e.g., homer')
629 """ Raise exception if stack name is not valid. """ 631 raise SystemExit(
'Name too long')
633 raise SystemExit(
'Name is not DNS compatible')
635 def create_passphrase(self):
637 first = getpass.getpass(
'Enter optional lock passphrase: ')
638 second = getpass.getpass(
'Re-enter optional lock passphrase: ')
640 shellish.vtmlprint(
"<b><red>Passphrases do not match")
643 shellish.vtmlprint(
"<b>WARNING: No passphrase will be " 644 "required for this stack.")
649 if self.
get_stack(args.stack)
is not None:
650 raise SystemExit(
'Stack already exists: %s' % args.stack)
651 if args.hostnames
is None:
652 args.hostnames = [args.stack]
655 phash = passphrase
and stack.gen_passphrase_hash(passphrase)
657 stack.set(
'_passphrase_hash', phash
or '', meta=
True)
658 stack.set(
"stack_version", base.VERSION)
659 stack.set(
"uuid", str(uuid.uuid4()))
660 stack.set(
"state",
'init')
661 stack.set(
"creator", os.environ[
'OPERATOR_IDENT'])
662 stack.set(
"region", args.region)
663 stack.set(
"cluster", args.ecs_cluster)
664 stack.set(
"created", base.localnow().isoformat())
665 stack.set(
"name", args.stack)
666 stack.set(
"dns", [
'%s.%s' % (x, args.domain)
for x
in args.hostnames])
667 env = dict(x.split(
'=')
for x
in args.env)
if args.env
else {}
668 stack.set(
"env", env)
669 stack.set(
"app_service_count", 1)
670 stack.set(
"deploy", {})
671 stack.set(
"changelog", [])
673 stack.set(
"version", args.odyssey_version)
674 stack.set(
'environment', args.environment)
675 stack.set(
'environment_name', args.environment_name)
676 stack._save(
'Created Stack')
681 ecs_cluster=args.ecs_cluster,
682 elb_security_group=args.elb_security_group,
683 elb_internal_security_group=args.elb_int_security_group,
684 eni_security_group=args.eni_security_group,
685 rds_config=args.rds_config,
686 pub_subnet_prefix=args.pub_subnet_prefix,
687 priv_subnet_prefix=args.priv_subnet_prefix,
688 https_cert_hostname=args.https_cert_hostname,
690 hostnames=args.hostnames,
691 verbose=args.verbose,
696 """Set the stack version and deploy CFN template""" 699 min_version =
'0.8.3' 700 allowed_states = {
'ready',
'active',
'partial_deploy'}
702 def setup_args(self, parser):
704 self.add_argument(
'--verbose',
'-v', action=
'store_true')
705 self.add_argument(
'--passphrase', env=
'ODYSSEY_STACK_PASSPHRASE',
706 help=
'To avoid being prompted for a passphrase on ' 707 'protected stack, you can set it on the command ' 708 'line or via the env.')
709 self.add_argument(
'new_version', action=
'store',
710 help=
'Version or tag to deploy')
711 self.add_argument(
'--run-db-migrations', action=
'store_true',
712 help=
'Run database migrations after upgrade. ' 713 'Notice, this implies `--check-exit-status`.')
714 self.add_argument(
'--check-exit-status', action=
'store_true',
715 help=
'Wait and report exit status of update')
721 raise SystemExit(
"Stack not found: %s" % args.stack)
722 passphrase = stack.check_passphrase(args.passphrase)
724 stack.update_state(
'deploying')
725 stack.set(
'version', args.new_version)
726 git_sha = os.environ[
'GIT_REV']
731 verbose=args.verbose,
732 wait_until_complete=args.check_exit_status,
733 run_db_migrations=args.run_db_migrations,
738 """ Remove an Odyssey stack from AWS <b>[use caution]</b>. """ 741 min_version =
"0.8.0" 742 allowed_states = {
'init',
'ready',
'active',
'partial_deploy'}
744 def setup_args(self, parser):
746 self.add_argument(
'--force',
'-f', action=
'store_true',
747 help=
'Force removal without confirmation.')
748 self.add_argument(
'--best-effort', action=
'store_true',
749 help=
'Attempt to remove corrupt stacks.')
750 self.add_argument(
'--drop-database', action=
'store_true',
752 help=
'Drops the database and user. Use CAUTION, ' 753 'This operation cannot be undone!')
754 self.add_argument(
'--check-exit-status', action=
'store_true',
755 help=
'Wait for completion of remove action')
757 def confirm_action(self, message):
758 confirm = input(message)
760 raise SystemExit(
'Aborted')
765 raise SystemExit(
'Stack not found: %s' % args.stack)
766 passphrase = stack.check_passphrase()
768 print(
"WARNING: Skipping requirements check!")
774 if args.drop_database:
776 "Are you sure? [yes|NO] ")
778 wait_until_complete = args.check_exit_status
780 stack.update_state(
'removing')
782 cloudformation.delete_stack(
784 wait_until_complete=wait_until_complete)
785 if args.drop_database
and stack.db:
786 print(
'Deleting database...')
787 self.
remove_db(stack.db.get(
'name',
None),
790 stack.db.get(
'rds_config',
None))
791 except Exception
as e:
793 shellish.vtmlprint(
"<red><b>IGNORING ERROR: %s" % e)
795 shellish.vtmlprint(
"<red><b>CRITICAL ERROR: %s" % e)
796 stack.update_state(
'corrupt')
799 stack.delete(passphrase)
802 def remove_db(self, db_ident, region, cluster, rds_config=None):
804 raise ValueError(
'No database identity was provided, aborting!')
806 rds_config =
'default' 807 db_config = get_rds_config(region, cluster, rds_config)
809 'DROP DATABASE IF EXISTS %s;' % db_ident,
810 'DROP USER IF EXISTS %s;' % db_ident,
812 submit_rsql_job(sql, db_config, region, cluster)
819 elb_internal_security_group,
828 '''Create required AWS resources for given stack''' 829 verbose = kwargs.get(
'verbose',
False)
830 db_config = get_rds_config(region,
834 def get_vpc_subnets(vpc, prefix):
836 return [x[
'Value']
for x
in tags
837 if x[
'Key'] ==
'Name'][0]
838 return [x
for x
in vpc.subnets.all()
839 if name_tag(x.tags).startswith(prefix)]
841 def get_sg(name, region):
842 ec2_api = boto3.client(
'ec2', region_name=region)
843 return ec2_api.describe_security_groups(Filters=[{
844 'Name':
'group-name',
846 }])[
'SecurityGroups'][0]
848 def get_https_cert(name, region):
849 acm_api = boto3.client(
'acm', region_name=region)
850 certs = acm_api.list_certificates()[
'CertificateSummaryList']
852 if x[
'DomainName'] == name:
855 def get_hosted_zone_id(domain):
856 r53_api = boto3.client(
'route53')
857 response = r53_api.list_hosted_zones_by_name(
861 if len(response.get(
'HostedZones', [])) == 0:
862 raise SystemExit(
"Unable to find hosted zone by name")
863 return response[
'HostedZones'][0][
'Id']
865 elb_sg = get_sg(elb_security_group, region)
866 elb_int_sg = get_sg(elb_internal_security_group, region)
867 eni_sg = get_sg(eni_security_group, region)
868 ec2_res = boto3.resource(
'ec2', region_name=region)
869 vpc = ec2_res.Vpc(elb_sg[
'VpcId'])
870 elb_subnets = get_vpc_subnets(vpc, pub_subnet_prefix)
872 raise SystemExit(
"No usable subnets found")
873 eni_subnets = get_vpc_subnets(vpc, priv_subnet_prefix)
875 raise SystemExit(
"No usable private subnets found")
877 cert_name =
'%s.%s' % (https_cert_hostname, domain)
878 cert = get_https_cert(cert_name, region)
880 hosted_zone_id = get_hosted_zone_id(domain)
883 raise SystemExit(
"No HTTPS cert found for: %s" % cert_name)
886 'vpc_id': vpc.vpc_id,
887 'certificate_arn': cert[
'CertificateArn'],
888 'elb_security_groups': [elb_sg[
'GroupId']],
889 'elb_internal_security_groups': [elb_int_sg[
'GroupId']],
890 'eni_security_groups': [eni_sg[
'GroupId']],
891 'private_subnet_ids': [x.id
for x
in eni_subnets],
892 'subnet_ids': [x.id
for x
in elb_subnets],
893 'hosted_zone_id': hosted_zone_id,
896 if kwargs.get(
'create_database',
True):
897 db_creds = create_db(stack, db_config)
899 "host": db_config[
'host'],
900 "port": db_config[
'port'],
901 "name": db_creds[
'db'],
902 "user": db_creds[
'user'],
903 "password": db_creds[
'password'],
904 "rds_config": rds_config,
909 stack_cf_id = cloudformation.create_cloudformation_stack(
914 if stack_cf_id
is None:
915 raise SystemExit(
"Unable to create Cloudformation Stack for Stack")
917 print(
"Created Cloudformation Stack: %s" % stack_cf_id)
919 stack.deploy = _deploy_dictionary_(stack)
921 stack.update_state(
'active')
924 def upgrade_cfn_stack(stack, git_sha, passphrase, **kwargs):
925 def deploy(stack, git_sha, **kwargs):
926 '''perform stack deployment''' 929 return cloudformation.deploy_changeset(
935 run_db_migrations = kwargs.get(
'run_db_migrations',
False)
936 wait_until_complete = (kwargs.get(
'wait_until_complete',
False)
or 938 verbose = kwargs.get(
'verbose',
False)
939 kws = {k: v
for k, v
in kwargs.items()
940 if k
not in [
'verbose',
941 'wait_until_complete']}
942 deploy_status = deploy(
946 wait_until_complete=wait_until_complete,
950 stack.deploy = _deploy_dictionary_(stack)
951 stack.update_state(
'active')
952 stack._save(
'Deployed %s from %s' % (
953 git_sha, os.environ[
'PROJECT_NAME']))
954 if run_db_migrations:
956 print(
'Submitting database migrations...')
957 cumanage.run_cumanage_task(stack,
959 wait_until_complete=
True,
960 passphrase=passphrase)
962 stack.update_state(
'partial_deploy')
963 stack._save(
'Deploy failure of %s from %s' % (
964 git_sha, os.environ[
'PROJECT_NAME']))
965 except Exception
as ex:
967 print(ex, file=sys.stderr)
968 stack.update_state(
'partial_deploy')
969 stack._save(
'Deploy failure of %s from %s' % (
970 git_sha, os.environ[
'PROJECT_NAME']))
971 raise SystemExit(
"Failed to deploy stack")
974 def get_rds_config(region, cluster, rds_config):
975 s3 = boto3.resource(
's3')
976 bucket = s3.Bucket(RDS_CONFIG_BUCKET)
977 config_obj = bucket.Object(
978 RDS_CONFIG_PATH_FMT % {
'region': region,
979 'ecs_cluster': cluster,
980 'rds_config': rds_config})
981 data = config_obj.get()
982 return json.loads(data[
'Body'].read().decode())
985 def create_db(stack, db_config):
986 stack_name = re.sub(
'[^a-z0-9]',
'', stack.name.lower())
987 user_uuid = str(stack.uuid).replace(
'-',
'_')
988 ident =
'odyssey_%s_%s' % (stack_name, user_uuid)
991 "CREATE USER %s WITH PASSWORD '%s';" % (ident, password),
992 "CREATE DATABASE %s;" % ident,
993 "REVOKE CONNECT ON DATABASE %s FROM PUBLIC;" % ident,
994 "GRANT ALL ON DATABASE %s TO %s;" % (ident, ident)
996 print(
'Creating RDS USER:', ident)
997 print(
'Creating RDS DB:', ident)
998 resp = submit_rsql_job(sql, db_config, stack.region, stack.cluster)
1000 assert not resp[
'failures'], resp[
'failures']
1004 "password": password
1008 def submit_rsql_job(sql, db_config, region, cluster, operator=None):
1010 operator = os.environ[
'OPERATOR_IDENT']
1011 client = boto3.client(
'ecs', region_name=region)
1012 resp = client.run_task(**{
1014 "taskDefinition":
'rds-psql',
1015 "startedBy": operator,
1017 "containerOverrides": [{
1021 "value": db_config[
'host']
1025 "value": str(db_config[
'port'])
1028 "name":
"PGPASSWORD",
1029 "value": db_config[
'password']
1033 "value": db_config[
'user']
1037 "command": [
' && '.join(
'psql -c "%s"' % x
for x
in sql)]
1041 assert not resp[
'failures'], resp[
'failures']
1046 '''Create necessary Params dictionary for cloudformation''' 1047 registry = REGISTRIES[stack.region]
1050 'VpcId': stack.aws[
'vpc_id'],
1051 'ECSCluster': stack.cluster,
1052 'StackName': stack.name,
1053 'StackVersion': stack.version,
1054 'StackDomain': stack.dns[0],
1055 'ContainerRegistry': registry,
1056 'StackDatabaseHost': stack.db[
'host'],
1057 'StackDatabasePort': str(stack.db[
'port']),
1058 'StackDatabaseName': stack.db[
'name'],
1059 'StackDatabaseUsername': stack.db[
'user'],
1060 'StackDatabasePassword': stack.db[
'password'],
1061 'Oauth2ClientId': oauth_client[0],
1062 'Oauth2ClientSecret': oauth_client[1],
1063 'AppServiceDesiredCount': str(stack.app_service_count),
1064 'CertificateArn': stack.aws[
'certificate_arn'],
1065 'HostedZoneId': stack.aws[
'hosted_zone_id'],
1066 'ELBSecurityGroupsIds':
','.join(stack.aws[
'elb_security_groups']),
1067 'InternalELBSecurityGroupIds':
','.join(
1068 stack.aws[
'elb_internal_security_groups']),
1069 'ENISecurityGroupIds':
','.join(stack.aws[
'eni_security_groups']),
1070 'PrivateSubnetIds':
','.join(stack.aws[
'private_subnet_ids']),
1071 'SubnetIds':
','.join(stack.aws[
'subnet_ids']),
1072 'IsMigration': kwargs.get(
'IsMigration',
'False'),
1073 'Environment': stack.environment,
1074 'EnvironmentName': stack.environment_name,
1078 def _deploy_dictionary_(stack):
1079 count = stack.deploy.get(
'count', 0)
1082 'updated': base.localnow().isoformat(),
1083 'project': os.environ[
'PROJECT_NAME'],
1084 'build_ident': os.environ[
'OPERATOR_IDENT'],
1085 'git_rev': os.environ[
'GIT_REV'],
1086 'git_branch': os.environ[
'GIT_BRANCH'],
1087 'git_repo': os.environ[
'GIT_REPO'],
1088 'docker_version': os.environ[
'DOCKER_VERSION'],
def confirm_action(self, message)
def confirm_action(self, message)
def get_task_definition(self, ident)
def create_cloudformation_stack_params(stack, **kwargs)
def _get_stack_data(self, stack)
def remove_db(self, db_ident, region, cluster, rds_config=None)
def check_name(self, name)
def task_env_to_dict(self, task_env)
def dict_to_task_env(self, env)
def restart_app_service(self)
def taskdef_env_override(self, taskdef, key, value=None)
def adj_sched_service_count(self, count)
def create_stack_aws_resources(stack, region, ecs_cluster, elb_security_group, elb_internal_security_group, eni_security_group, rds_config, pub_subnet_prefix, priv_subnet_prefix, https_cert_hostname, domain, hostnames, **kwargs)
def get_client_oauth_data(domain)
def add_stack_argument(self, *args, env=DEFAULT_STACK_ENV, help=None, metavar='STACK_NAME', **kwargs)
def get_stack(self, name)
def check_requirements(self, stack)
def create_passphrase(self)
def __init__(self, *args, **kwargs)