CloudBender/cloudbender/cli.py

215 lines
6.2 KiB
Python
Raw Normal View History

2018-11-22 18:31:59 +00:00
import os
import click
import functools
2018-12-10 17:58:54 +00:00
from concurrent.futures import ThreadPoolExecutor, as_completed
2018-11-22 18:31:59 +00:00
from . import __version__
from .core import CloudBender
from .utils import setup_logging
import logging
logger = logging.getLogger(__name__)
2018-11-22 18:31:59 +00:00
@click.group()
@click.version_option(version=__version__, prog_name="CloudBender")
@click.option("--debug", is_flag=True, help="Turn on debug logging.")
@click.option("--dir", "directory", help="Specify cloudbender project directory.")
@click.pass_context
def cli(ctx, debug, directory):
setup_logging(debug)
2018-11-22 18:31:59 +00:00
# Read global config
cb = CloudBender(directory if directory else os.getcwd())
cb.read_config()
cb.dump_config()
2019-02-08 10:51:44 +00:00
ctx.obj = cb
2018-11-22 18:31:59 +00:00
@click.command()
@click.argument("stack_names", nargs=-1)
2018-11-22 18:31:59 +00:00
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
2019-02-08 10:51:44 +00:00
@click.pass_obj
def render(cb, stack_names, multi):
2018-11-22 18:31:59 +00:00
""" Renders template and its parameters """
2019-02-08 10:51:44 +00:00
stacks = _find_stacks(cb, stack_names, multi)
_render(stacks)
2018-11-22 18:31:59 +00:00
@click.command()
@click.argument("stack_names", nargs=-1)
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
@click.pass_obj
def sync(cb, stack_names, multi):
""" Renders template and provisions it right away """
stacks = _find_stacks(cb, stack_names, multi)
_render(stacks)
_provision(cb, stacks)
2018-11-22 18:31:59 +00:00
@click.command()
@click.argument("stack_names", nargs=-1)
2018-11-22 18:31:59 +00:00
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
2019-02-08 10:51:44 +00:00
@click.pass_obj
def validate(cb, stack_names, multi):
2019-01-30 13:00:06 +00:00
""" Validates already rendered templates using cfn-lint """
2019-02-08 10:51:44 +00:00
stacks = _find_stacks(cb, stack_names, multi)
2018-11-22 18:31:59 +00:00
for s in stacks:
s.validate()
2019-01-30 13:00:06 +00:00
@click.command()
@click.argument("stack_name")
@click.argument("change_set_name")
2019-02-08 10:51:44 +00:00
@click.pass_obj
def create_change_set(cb, stack_name, change_set_name):
2019-01-30 13:00:06 +00:00
""" Creates a change set for an existing stack """
2019-02-08 10:51:44 +00:00
stacks = _find_stacks(cb, [stack_name])
2019-01-30 13:00:06 +00:00
for s in stacks:
s.create_change_set(change_set_name)
2018-11-22 18:31:59 +00:00
@click.command()
@click.argument("stack_names", nargs=-1)
2018-11-22 18:31:59 +00:00
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
2019-02-08 10:51:44 +00:00
@click.pass_obj
def provision(cb, stack_names, multi):
2018-11-22 18:31:59 +00:00
""" Creates or updates stacks or stack groups """
2019-02-08 10:51:44 +00:00
stacks = _find_stacks(cb, stack_names, multi)
_provision(cb, stacks)
2018-11-22 18:31:59 +00:00
@click.command()
@click.argument("stack_names", nargs=-1)
2018-11-22 18:31:59 +00:00
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
2019-02-08 10:51:44 +00:00
@click.pass_obj
def delete(cb, stack_names, multi):
2018-11-22 18:31:59 +00:00
""" Deletes stacks or stack groups """
2019-02-08 10:51:44 +00:00
stacks = _find_stacks(cb, stack_names, multi)
2018-11-22 18:31:59 +00:00
# Reverse steps
2019-02-08 10:51:44 +00:00
steps = [s for s in sort_stacks(cb, stacks)]
2018-11-22 18:31:59 +00:00
delete_steps = steps[::-1]
for step in delete_steps:
if step:
with ThreadPoolExecutor(max_workers=len(step)) as group:
futures = []
for stack in step:
if stack.multi_delete:
futures.append(group.submit(stack.delete))
2018-12-10 17:58:54 +00:00
for future in as_completed(futures):
future.result()
2018-11-22 18:31:59 +00:00
@click.command()
2019-02-08 10:51:44 +00:00
@click.pass_obj
def clean(cb):
2018-11-22 18:31:59 +00:00
""" Deletes all previously rendered files locally """
cb.clean()
2019-02-08 10:51:44 +00:00
def sort_stacks(cb, stacks):
2018-11-22 18:31:59 +00:00
""" Sort stacks by dependencies """
data = {}
for s in stacks:
# To resolve dependencies we have to read each template
s.read_template_file()
2018-11-22 18:31:59 +00:00
deps = []
for d in s.dependencies:
# For now we assume deps are artifacts so we prepend them with our local profile and region to match stack.id
for dep_stack in cb.filter_stacks({'region': s.region, 'profile': s.profile, 'provides': d}):
deps.append(dep_stack.id)
# also look for global services
for dep_stack in cb.filter_stacks({'region': 'global', 'profile': s.profile, 'provides': d}):
deps.append(dep_stack.id)
2018-12-10 17:58:54 +00:00
2018-11-22 18:31:59 +00:00
data[s.id] = set(deps)
logger.debug("Stack {} depends on {}".format(s.id, deps))
2018-11-22 18:31:59 +00:00
# Ignore self dependencies
2018-11-22 18:31:59 +00:00
for k, v in data.items():
v.discard(k)
2018-11-22 18:31:59 +00:00
extra_items_in_deps = functools.reduce(set.union, data.values()) - set(data.keys())
data.update({item: set() for item in extra_items_in_deps})
2018-11-22 18:31:59 +00:00
while True:
ordered = set(item for item, dep in data.items() if not dep)
2018-11-22 18:31:59 +00:00
if not ordered:
break
# return list of stack objects rather than just names
result = []
for o in ordered:
for s in stacks:
if s.id == o:
result.append(s)
2018-11-22 18:31:59 +00:00
yield result
data = {item: (dep - ordered) for item, dep in data.items()
2018-11-22 18:31:59 +00:00
if item not in ordered}
assert not data, "A cyclic dependency exists amongst %r" % data
2019-02-08 10:51:44 +00:00
def _find_stacks(cb, stack_names, multi=False):
""" search stacks by name """
2018-11-22 18:31:59 +00:00
stacks = []
for s in stack_names:
stacks = stacks + cb.resolve_stacks(s)
2018-11-22 18:31:59 +00:00
if not multi and len(stacks) > 1:
logger.error('Found more than one stack matching name ({}). Please set --multi if that is what you want.'.format(', '.join(stack_names)))
2018-11-22 18:31:59 +00:00
raise click.Abort()
if not stacks:
logger.error('Cannot find stack matching: {}'.format(', '.join(stack_names)))
2018-11-22 18:31:59 +00:00
raise click.Abort()
return stacks
def _render(stacks):
""" Utility function to reuse code between tasks """
for s in stacks:
s.render()
s.write_template_file()
def _provision(cb, stacks):
""" Utility function to reuse code between tasks """
for step in sort_stacks(cb, stacks):
if step:
with ThreadPoolExecutor(max_workers=len(step)) as group:
futures = []
for stack in step:
status = stack.get_status()
if not status:
futures.append(group.submit(stack.create))
else:
futures.append(group.submit(stack.update))
for future in as_completed(futures):
future.result()
2018-11-22 18:31:59 +00:00
cli.add_command(render)
cli.add_command(sync)
2018-11-22 18:31:59 +00:00
cli.add_command(validate)
cli.add_command(provision)
cli.add_command(delete)
cli.add_command(clean)
2019-01-30 13:00:06 +00:00
cli.add_command(create_change_set)
if __name__ == '__main__':
cli(obj={})