def reset_provisioned_product_owner(f): puppet_account_id = config.get_puppet_account_id() current_account_id = puppet_account_id manifest = manifest_utils.load(f, puppet_account_id) task_defs = manifest_utils_for_launches.generate_launch_tasks( manifest, puppet_account_id, False, False ) tasks_to_run = [] for task in task_defs: task_status = task.get("status") if task_status == constants.PROVISIONED: tasks_to_run.append( provisioning_tasks.ResetProvisionedProductOwnerTask( launch_name=task.get("launch_name"), account_id=task.get("account_id"), region=task.get("region"), ) ) cache_invalidator = str(datetime.now()) runner.run_tasks( puppet_account_id, current_account_id, tasks_to_run, 10, cache_invalidator=cache_invalidator, on_complete_url=None, )
def reset_provisioned_product_owner(f): puppet_account_id = config.get_puppet_account_id() current_account_id = puppet_account_id manifest = manifest_utils.load(f, puppet_account_id) os.environ["SCT_CACHE_INVALIDATOR"] = str(datetime.now()) task_defs = manifest_utils_for_launches.generate_launch_tasks( manifest, puppet_account_id, False, False) tasks_to_run = [] for task in task_defs: task_status = task.get("status") if task_status == constants.PROVISIONED: tasks_to_run.append( launch_tasks.ResetProvisionedProductOwnerTask( launch_name=task.get("launch_name"), account_id=task.get("account_id"), region=task.get("region"), )) runner.run_tasks( puppet_account_id, current_account_id, tasks_to_run, 10, execution_mode="hub", on_complete_url=None, )
def graph(f): current_account_id = puppet_account_id = config.get_puppet_account_id() tasks_to_run = generate_tasks(f, puppet_account_id, current_account_id) lines = [] nodes = [] for task in tasks_to_run: nodes += graph_nodes(task) what = task.requires() if isinstance(what, puppet_tasks.PuppetTask): lines.append(f'"{task.node_id}" -> "{what.node_id}"') elif isinstance(what, list): for item in what: lines += graph_lines(task, item) elif isinstance(what, dict): for item in what.values(): lines += graph_lines(task, item) else: raise Exception(f"unknown {type(what)}") # nodes.append(task.graph_node()) # lines += task.get_graph_lines() click.echo("digraph G {\n") click.echo("node [shape=record fontname=Arial];") for node in nodes: click.echo(f"{node};") for line in lines: click.echo(f'{line} [label="depends on"];') click.echo("}")
def output_location(self): puppet_account_id = config.get_puppet_account_id() path = f"output/{self.uid}.{self.output_suffix}" if config.is_caching_enabled(puppet_account_id): return f"s3://sc-puppet-caching-bucket-{config.get_puppet_account_id()}-{config.get_home_region(puppet_account_id)}/{path}" else: return path
def expand(f, single_account, parameter_override_file, parameter_override_forced): params = dict(single_account=single_account) if parameter_override_forced or misc_commands.is_a_parameter_override_execution( ): overrides = dict(**yaml.safe_load(parameter_override_file.read())) if overrides.get("subset"): subset = overrides.get("subset") overrides = dict( section=subset.get("section"), item=subset.get("name"), include_dependencies=subset.get("include_dependencies"), include_reverse_dependencies=subset.get( "include_reverse_dependencies"), ) params.update( dict( single_account=overrides.get("single_account"), subset=overrides, )) click.echo(f"Overridden parameters {params}") puppet_account_id = config.get_puppet_account_id() manifest_commands.expand(f, puppet_account_id, **params) if config.get_should_explode_manifest(puppet_account_id): manifest_commands.explode(f)
def bootstrap( with_manual_approvals, puppet_code_pipeline_role_permission_boundary, source_role_permissions_boundary, puppet_generate_role_permission_boundary, puppet_deploy_role_permission_boundary, puppet_provisioning_role_permissions_boundary, cloud_formation_deploy_role_permissions_boundary, deploy_environment_compute_type, deploy_num_workers, source_provider, repository_name, branch_name, owner, repo, branch, poll_for_source_changes, webhook_secret, ): puppet_account_id = config.get_puppet_account_id() if source_provider == "CodeCommit": core.bootstrap( with_manual_approvals, puppet_account_id, puppet_code_pipeline_role_permission_boundary, source_role_permissions_boundary, puppet_generate_role_permission_boundary, puppet_deploy_role_permission_boundary, puppet_provisioning_role_permissions_boundary, cloud_formation_deploy_role_permissions_boundary, deploy_environment_compute_type, deploy_num_workers, source_provider, None, repository_name, branch_name, poll_for_source_changes, webhook_secret, ) elif source_provider == "GitHub": core.bootstrap( with_manual_approvals, puppet_account_id, puppet_code_pipeline_role_permission_boundary, source_role_permissions_boundary, puppet_generate_role_permission_boundary, puppet_deploy_role_permission_boundary, puppet_provisioning_role_permissions_boundary, cloud_formation_deploy_role_permissions_boundary, deploy_environment_compute_type, deploy_num_workers, source_provider, owner, repo, branch, poll_for_source_changes, webhook_secret, ) else: raise Exception(f"Unsupported source provider: {source_provider}")
def bootstrap_spokes_in_ou(ou_path_or_id, role_name, iam_role_arns, permission_boundary): org_iam_role_arn = config.get_org_iam_role_arn() puppet_account_id = config.get_puppet_account_id() if org_iam_role_arn is None: click.echo('No org role set - not expanding') else: click.echo('Expanding using role: {}'.format(org_iam_role_arn)) with betterboto_client.CrossAccountClientContextManager( 'organizations', org_iam_role_arn, 'org-iam-role') as client: tasks = [] if ou_path_or_id.startswith('/'): ou_id = client.convert_path_to_ou(ou_path_or_id) else: ou_id = ou_path_or_id logging.info(f"ou_id is {ou_id}") response = client.list_children_nested(ParentId=ou_id, ChildType='ACCOUNT') for spoke in response: tasks.append( management_tasks.BootstrapSpokeAsTask( puppet_account_id=puppet_account_id, account_id=spoke.get('Id'), iam_role_arns=iam_role_arns, role_name=role_name, permission_boundary=permission_boundary, )) runner.run_tasks_for_bootstrap_spokes_in_ou(tasks)
def bootstrap_spokes_in_ou(ou_path_or_id, role_name, iam_role_arns, permission_boundary, num_workers=10): puppet_account_id = config.get_puppet_account_id() org_iam_role_arn = config.get_org_iam_role_arn(puppet_account_id) if org_iam_role_arn is None: click.echo("No org role set - not expanding") else: click.echo("Expanding using role: {}".format(org_iam_role_arn)) with betterboto_client.CrossAccountClientContextManager( "organizations", org_iam_role_arn, "org-iam-role") as client: tasks = [] if ou_path_or_id.startswith("/"): ou_id = client.convert_path_to_ou(ou_path_or_id) else: ou_id = ou_path_or_id logging.info(f"ou_id is {ou_id}") response = client.list_children_nested(ParentId=ou_id, ChildType="ACCOUNT") for spoke in response: tasks.append( management_tasks.BootstrapSpokeAsTask( puppet_account_id=puppet_account_id, account_id=spoke.get("Id"), iam_role_arns=iam_role_arns, role_name=role_name, permission_boundary=permission_boundary, )) runner.run_tasks_for_bootstrap_spokes_in_ou(tasks, num_workers)
def generate_tasks(f, single_account=None, is_dry_run=False): puppet_account_id = config.get_puppet_account_id() manifest = manifest_utils.load(f) should_use_sns = config.get_should_use_sns(os.environ.get("AWS_DEFAULT_REGION")) should_use_product_plans = config.get_should_use_product_plans(os.environ.get("AWS_DEFAULT_REGION")) tasks_to_run = manifest_utils_for_launches.generate_launch_tasks( manifest, puppet_account_id, should_use_sns, should_use_product_plans, include_expanded_from=False, single_account=single_account, is_dry_run=is_dry_run, ) logger.info("Finished generating provisioning tasks") if not is_dry_run: logger.info("Generating sharing tasks") spoke_local_portfolios_tasks = manifest_utils_for_spoke_local_portfolios.generate_spoke_local_portfolios_tasks( manifest, puppet_account_id, should_use_sns, should_use_product_plans, include_expanded_from=False, single_account=single_account, is_dry_run=is_dry_run, ) tasks_to_run += spoke_local_portfolios_tasks logger.info("Finished generating sharing tasks") logger.info("Finished generating all tasks") return tasks_to_run
def expand(f, single_account): click.echo("Expanding") puppet_account_id = config.get_puppet_account_id() manifest = manifest_utils.load(f, puppet_account_id) org_iam_role_arn = config.get_org_iam_role_arn(puppet_account_id) if org_iam_role_arn is None: click.echo("No org role set - not expanding") new_manifest = manifest else: click.echo("Expanding using role: {}".format(org_iam_role_arn)) with betterboto_client.CrossAccountClientContextManager( "organizations", org_iam_role_arn, "org-iam-role" ) as client: new_manifest = manifest_utils.expand_manifest(manifest, client) click.echo("Expanded") if single_account: click.echo(f"Filtering for single account: {single_account}") for account in new_manifest.get("accounts", []): if account.get("account_id") == single_account: click.echo(f"Found single account: {single_account}") new_manifest["accounts"] = [account] break click.echo("Filtered") new_name = f.name.replace(".yaml", "-expanded.yaml") logger.info("Writing new manifest: {}".format(new_name)) with open(new_name, "w") as output: output.write(yaml.safe_dump(new_manifest, default_flow_style=False))
def step_impl(context, account_type, version): if account_type == "puppet": sdk.bootstrap(False) elif account_type == "spoke": sdk.bootstrap_spoke( config.get_puppet_account_id(), "arn:aws:iam::aws:policy/AdministratorAccess" )
def output(self): should_use_s3_target_if_caching_is_on = ( "cache_invalidator" not in self.params_for_results_display().keys()) if should_use_s3_target_if_caching_is_on and config.is_caching_enabled( config.get_puppet_account_id()): return s3.S3Target(self.output_location, format=format.UTF8) else: return luigi.LocalTarget(self.output_location)
def on_task_processing_time(task, duration): print_stats() record_event("processing_time", task, {"duration": duration}) task_params = dict(**task.param_kwargs) task_params.update(task.params_for_results_display()) with betterboto_client.CrossAccountClientContextManager( "cloudwatch", config.get_puppet_role_arn(config.get_puppet_account_id()), "cloudwatch-puppethub", ) as cloudwatch: dimensions = [dict( Name="task_type", Value=task.__class__.__name__, )] for note_worthy in [ "launch_name", "region", "account_id", "puppet_account_id", "sharing_mode", "portfolio", "product", "version", "execution", ]: if task_params.get(note_worthy): dimensions.append( dict(Name=note_worthy, Value=task_params.get(note_worthy))) cloudwatch.put_metric_data( Namespace= f"ServiceCatalogTools/Puppet/v1/ProcessingTime/{task.__class__.__name__}", MetricData=[ dict( MetricName=task.__class__.__name__, Dimensions=dimensions, Value=duration, Unit="Seconds", ), ], ) cloudwatch.put_metric_data( Namespace=f"ServiceCatalogTools/Puppet/v1/ProcessingTime/Tasks", MetricData=[ dict( MetricName="Tasks", Dimensions=[ dict(Name="TaskType", Value=task.__class__.__name__) ], Value=duration, Unit="Seconds", ), ], )
def list_launches(expanded_manifest, format): current_account_id = puppet_account_id = config.get_puppet_account_id() deploy_commands.deploy( expanded_manifest, puppet_account_id, current_account_id, single_account=None, is_dry_run=True, is_list_launches=format, )
def dry_run(f, single_account, puppet_account_id): if puppet_account_id is None: puppet_account_id = config.get_puppet_account_id() executor_account_id = config.get_current_account_id() deploy_commands.deploy( f, executor_account_id, puppet_account_id, single_account=single_account, is_dry_run=True, )
def expand(f, single_account, parameter_override_file, parameter_override_forced): params = dict(single_account=single_account) if parameter_override_forced or misc_commands.is_a_parameter_override_execution(): overrides = dict(**yaml.safe_load(parameter_override_file.read())) params.update(overrides) click.echo(f"Overridden parameters {params}") puppet_account_id = config.get_puppet_account_id() manifest_commands.expand(f, puppet_account_id, **params) if config.get_should_explode_manifest(puppet_account_id): manifest_commands.explode(f)
def output_location(self): puppet_account_id = config.get_puppet_account_id() path = f"output/{self.uid}.{self.output_suffix}" should_use_s3_target_if_caching_is_on = ( "cache_invalidator" not in self.params_for_results_display().keys()) if should_use_s3_target_if_caching_is_on and config.is_caching_enabled( puppet_account_id): return f"s3://sc-puppet-caching-bucket-{config.get_puppet_account_id()}-{config.get_home_region(puppet_account_id)}/{path}" else: return path
def step_impl(context, account_type, version, role): if account_type == "puppet": args = { "with_manual_approvals": False } args[roles.get(role)] = constants.BOUNDARY_ARN_TO_USE sdk.bootstrap(**args) elif account_type == "spoke": sdk.bootstrap_spoke( config.get_puppet_account_id(), "arn:aws:iam::aws:policy/AdministratorAccess" )
def test_get_puppet_account_id(mocked_betterboto_client): # setup from servicecatalog_puppet import config as sut expected_result = "some_fake_arn" mocked_response = {"Account": expected_result} mocked_betterboto_client.return_value.__enter__.return_value.get_caller_identity.return_value = ( mocked_response) # exercise actual_result = sut.get_puppet_account_id() # verify assert actual_result == expected_result
def graph(f): current_account_id = puppet_account_id = config.get_puppet_account_id() tasks_to_run = generate_tasks(f, puppet_account_id, current_account_id) lines = [] nodes = [] for task in tasks_to_run: nodes.append(task.graph_node()) lines += task.get_graph_lines() click.echo("digraph G {\n") click.echo("node [shape=record fontname=Arial];") for node in nodes: click.echo(f"{node};") for line in lines: click.echo(f'{line} [label="depends on"];') click.echo("}")
def explode(f): logger.info("Exploding") puppet_account_id = config.get_puppet_account_id() original_name = f.name expanded_output = f.name.replace(".yaml", "-expanded.yaml") expanded_manifest = manifest_utils.load( open(expanded_output, "r"), puppet_account_id ) expanded_manifest = manifest_utils.Manifest(expanded_manifest) exploded = manifest_utils.explode(expanded_manifest) logger.info(f"found {len(exploded)} graphs") count = 0 for mani in exploded: with open(original_name.replace(".yaml", f"-exploded-{count}.yaml"), "w") as f: f.write(yaml.safe_dump(json.loads(json.dumps(mani)))) count += 1
def deploy( f, single_account, num_workers, execution_mode, puppet_account_id, home_region, regions, should_collect_cloudformation_events, should_forward_events_to_eventbridge, should_forward_failures_to_opscenter, on_complete_url, ): click.echo( f"running in partition: {config.get_partition()} as {config.get_puppet_role_path()}{config.get_puppet_role_name()}" ) if puppet_account_id is None: puppet_account_id = config.get_puppet_account_id() executor_account_id = config.get_current_account_id() if executor_account_id != puppet_account_id: click.echo("Not running in puppet account: writing config") with open("config.yaml", "w") as conf: conf.write( yaml.safe_dump( dict( home_region=home_region, regions=regions.split(","), should_collect_cloudformation_events= should_collect_cloudformation_events, should_forward_events_to_eventbridge= should_forward_events_to_eventbridge, should_forward_failures_to_opscenter= should_forward_failures_to_opscenter, ))) core.deploy( f, puppet_account_id, executor_account_id, single_account=single_account, num_workers=num_workers, execution_mode=execution_mode, on_complete_url=on_complete_url, )
def validate(f): logger.info("Validating {}".format(f.name)) manifest = manifest_utils.load(f, config.get_puppet_account_id()) schema = yamale.make_schema( asset_helpers.resolve_from_site_packages("schema.yaml")) data = yamale.make_data(content=yaml.safe_dump(manifest)) yamale.validate(schema, data, strict=False) tags_defined_by_accounts = {} for account in manifest.get("accounts"): for tag in account.get("tags", []): tags_defined_by_accounts[tag] = True for collection_type in constants.ALL_SECTION_NAMES: collection_to_check = manifest.get(collection_type, {}) for collection_name, collection_item in collection_to_check.items(): for deploy_to in collection_item.get("deploy_to", {}).get("tags", []): tag_to_check = deploy_to.get("tag") if tags_defined_by_accounts.get(tag_to_check) is None: print( f"{collection_type}.{collection_name} uses tag {tag_to_check} in deploy_to that does not exist" ) for depends_on in collection_item.get("depends_on", []): if isinstance(depends_on, str): if manifest.get( constants.LAUNCHES).get(depends_on) is None: print( f"{collection_type}.{collection_name} uses {depends_on} in depends_on that does not exist" ) else: tt = constants.SECTION_SINGULAR_TO_PLURAL.get( depends_on.get("type", constants.LAUNCH)) dd = depends_on.get("name") if manifest.get(tt).get(dd) is None: print( f"{collection_type}.{collection_name} uses {depends_on} in depends_on that does not exist" ) click.echo("Finished validating: {}".format(f.name)) click.echo("Finished validating: OK")
def reset_provisioned_product_owner(f): puppet_account_id = config.get_puppet_account_id() manifest = manifest_utils.load(f) task_defs = manifest_utils.convert_manifest_into_task_defs_for_launches( manifest, puppet_account_id, False, False) tasks_to_run = [] for task in task_defs: task_status = task.get('status') if task_status == constants.PROVISIONED: tasks_to_run.append( provisioning_tasks.ResetProvisionedProductOwnerTask( launch_name=task.get('launch_name'), account_id=task.get('account_id'), region=task.get('region'), )) runner.run_tasks(tasks_to_run, 10)
def expand(f, single_account, subset=None): click.echo("Expanding") puppet_account_id = config.get_puppet_account_id() manifest = manifest_utils.load(f, puppet_account_id) org_iam_role_arn = config.get_org_iam_role_arn(puppet_account_id) if org_iam_role_arn is None: click.echo("No org role set - not expanding") new_manifest = manifest else: click.echo("Expanding using role: {}".format(org_iam_role_arn)) with betterboto_client.CrossAccountClientContextManager( "organizations", org_iam_role_arn, "org-iam-role") as client: new_manifest = manifest_utils.expand_manifest(manifest, client) click.echo("Expanded") if single_account: click.echo(f"Filtering for single account: {single_account}") for account in new_manifest.get("accounts", []): if str(account.get("account_id")) == str(single_account): click.echo(f"Found single account: {single_account}") new_manifest["accounts"] = [account] break click.echo("Filtered") new_manifest = manifest_utils.rewrite_depends_on(new_manifest) if subset: click.echo(f"Filtering for subset: {subset}") new_manifest = manifest_utils.isolate( manifest_utils.Manifest(new_manifest), subset) new_manifest = json.loads(json.dumps(new_manifest)) if new_manifest.get(constants.LAMBDA_INVOCATIONS) is None: new_manifest[constants.LAMBDA_INVOCATIONS] = dict() new_name = f.name.replace(".yaml", "-expanded.yaml") logger.info("Writing new manifest: {}".format(new_name)) with open(new_name, "w") as output: output.write(yaml.safe_dump(new_manifest, default_flow_style=False))
def bootstrap_branch( branch_name, with_manual_approvals, puppet_code_pipeline_role_permission_boundary, source_role_permissions_boundary, puppet_generate_role_permission_boundary, puppet_deploy_role_permission_boundary, puppet_provisioning_role_permissions_boundary, cloud_formation_deploy_role_permissions_boundary, ): puppet_account_id = config.get_puppet_account_id() core.bootstrap_branch( branch_name, puppet_account_id, with_manual_approvals, puppet_code_pipeline_role_permission_boundary, source_role_permissions_boundary, puppet_generate_role_permission_boundary, puppet_deploy_role_permission_boundary, puppet_provisioning_role_permissions_boundary, cloud_formation_deploy_role_permissions_boundary, )
def generate_shares(f): logger.info('Starting to generate shares for: {}'.format(f.name)) tasks_to_run = [] puppet_account_id = config.get_puppet_account_id() manifest = manifest_utils.load(f) task_defs = manifest_utils.convert_manifest_into_task_defs_for_launches( manifest, puppet_account_id, False, False, include_expanded_from=True) for task in task_defs: tasks_to_run.append( portfoliomanagement_tasks.CreateShareForAccountLaunchRegion( puppet_account_id=puppet_account_id, account_id=task.get('account_id'), region=task.get('region'), portfolio=task.get('portfolio'), expanded_from=task.get('expanded_from'), organization=task.get('organization'), )) spoke_local_portfolios_tasks = manifest_utils.convert_manifest_into_task_defs_for_spoke_local_portfolios( manifest, puppet_account_id, False, tasks_to_run) for task in spoke_local_portfolios_tasks: if isinstance(task, portfoliomanagement_tasks.CreateSpokeLocalPortfolioTask): param_kwargs = task.param_kwargs tasks_to_run.append( portfoliomanagement_tasks.CreateShareForAccountLaunchRegion( puppet_account_id=puppet_account_id, account_id=param_kwargs.get('account_id'), region=param_kwargs.get('region'), portfolio=param_kwargs.get('portfolio'), expanded_from=param_kwargs.get('expanded_from'), organization=param_kwargs.get('organization'), )) runner.run_tasks_for_generate_shares(tasks_to_run)
def bootstrap( with_manual_approvals, puppet_code_pipeline_role_permission_boundary, source_role_permissions_boundary, puppet_generate_role_permission_boundary, puppet_deploy_role_permission_boundary, puppet_provisioning_role_permissions_boundary, cloud_formation_deploy_role_permissions_boundary, deploy_environment_compute_type, deploy_num_workers, ): puppet_account_id = config.get_puppet_account_id() core.bootstrap( with_manual_approvals, puppet_account_id, puppet_code_pipeline_role_permission_boundary, source_role_permissions_boundary, puppet_generate_role_permission_boundary, puppet_deploy_role_permission_boundary, puppet_provisioning_role_permissions_boundary, cloud_formation_deploy_role_permissions_boundary, deploy_environment_compute_type, deploy_num_workers, )
def generate_tasks(f, single_account=None, dry_run=False): puppet_account_id = config.get_puppet_account_id() manifest = manifest_utils.load(f) tasks_to_run = [] should_use_sns = config.get_should_use_sns( os.environ.get("AWS_DEFAULT_REGION")) should_use_product_plans = config.get_should_use_product_plans( os.environ.get("AWS_DEFAULT_REGION")) task_defs = manifest_utils.convert_manifest_into_task_defs_for_launches( manifest, puppet_account_id, should_use_sns, should_use_product_plans) for task in task_defs: if single_account is not None: if task.get('account_id') != single_account: continue task_status = task.get('status') del task['status'] if task_status == constants.PROVISIONED: task['should_use_sns'] = should_use_sns if dry_run: tasks_to_run.append( provisioning_tasks.ProvisionProductDryRunTask(**task)) else: tasks_to_run.append( provisioning_tasks.ProvisionProductTask(**task)) elif task_status == constants.TERMINATED: for attribute in constants.DISALLOWED_ATTRIBUTES_FOR_TERMINATED_LAUNCHES: logger.info( f"checking {task.get('launch_name')} for disallowed attributes" ) attribute_value = task.get(attribute) if attribute_value is not None: if isinstance(attribute_value, list): if len(attribute_value) != 0: raise Exception( f"Launch {task.get('launch_name')} has disallowed attribute: {attribute}" ) elif isinstance(attribute_value, dict): if len(attribute_value.keys()) != 0: raise Exception( f"Launch {task.get('launch_name')} has disallowed attribute: {attribute}" ) else: raise Exception( f"Launch {task.get('launch_name')} has disallowed attribute: {attribute}" ) del task['launch_parameters'] del task['manifest_parameters'] del task['account_parameters'] del task['should_use_sns'] del task['requested_priority'] del task['should_use_product_plans'] del task['pre_actions'] del task['post_actions'] if dry_run: tasks_to_run.append( provisioning_tasks.TerminateProductDryRunTask(**task)) else: tasks_to_run.append( provisioning_tasks.TerminateProductTask(**task)) else: raise Exception(f"Unsupported status of {task_status}") if not dry_run: spoke_local_portfolios_tasks = manifest_utils.convert_manifest_into_task_defs_for_spoke_local_portfolios( manifest, puppet_account_id, should_use_sns, tasks_to_run) for spoke_local_portfolios_task in spoke_local_portfolios_tasks: if single_account is not None: param_kwargs = spoke_local_portfolios_task.param_kwargs logger.info(f"EPF:: {param_kwargs}") if param_kwargs.get('account_id', 'not_an_account_id') != single_account: continue tasks_to_run.append(spoke_local_portfolios_task) return tasks_to_run
def release_spoke(puppet_account_id): if puppet_account_id is None: puppet_account_id = config.get_puppet_account_id() spoke_management_commands.release_spoke(puppet_account_id)