def converge(self, dry_run_report, provisioner_overrides=None): ''' Executes the proposed actions in the provided dry run report. Returns a report of actions taken. ''' report = OrderedDict() if 'actions' not in dry_run_report: return report report['actions'] = dry_run_report['actions'] report['changes'] = OrderedDict() if provisioner_overrides: for name, value in provisioner_overrides.items(): self.provisioner[name] = value session_builder = self._get_session_builder() org_service = OrganizationService(session_builder=session_builder) if report['actions'].get('organizations', {}).get('organization', {}).get('action', None) == 'create': self._create_organization(report=report, org_service=org_service) logger.info('Executing dry run to identify changes needed to converge the newly created organization.') new_dry_run_report = self.dry_run() if 'actions' in new_dry_run_report: report['actions'] = OrderedDict({**report['actions'], **new_dry_run_report['actions']}) self.updated_model = copy.deepcopy(self.aws_model) self._upsert_policies(report=report, org_service=org_service) self._upsert_root_policy_targets(report=report, org_service=org_service) self._upsert_accounts(report=report, org_service=org_service) self._upsert_orgunits(report=report, org_service=org_service) self._update_account_associations(report=report, org_service=org_service) self._delete_orgunits(report=report, org_service=org_service) self._delete_policies(report=report, org_service=org_service) return report
def create_accounts(self, organization, accounts): ''' Creates one or more accounts in the Organization. As account creation is asynchronous, it waits until each account finishes creating, then returns a changes dict for the report. ''' changes = {} for account in accounts: creation_statuses = {} logger.info('Creating account %s', account) create_account_params = { 'Email': organization.accounts[account]['owner'], 'AccountName': organization.accounts[account]['name'] } create_res = self.client.create_account(**create_account_params) creation_statuses[account] = create_res['CreateAccountStatus'] self._wait_on_account_creation(creation_statuses) for account_name, status in creation_statuses.items(): if status['State'] == 'SUCCEEDED': change = 'created' elif status['State'] == 'FAILED': change = 'failed' else: change = 'unknown' changes[account_name] = {"change": change} if status == 'failed': changes[account_name]['reason'] = status['FailureReason'] return changes
def _create_organization(self, org_model): logger.info('Creating organization.') organization_parameters = {"FeatureSet": org_model.featureset} self.client.create_organization(**organization_parameters) root_parent_id = self._get_root_parent_id(org_model.root_account_id) logger.info('Enabling Service Control Policy policy type.') self.client.enable_policy_type(RootId=root_parent_id, PolicyType='SERVICE_CONTROL_POLICY')
def _get_config_loader_for_version(version): if version in _CONFIG_VERSIONS_TO_LOADERS: logger.info('Using config loader for config version %s', version) loader = _CONFIG_VERSIONS_TO_LOADERS[version]() else: #pylint: disable=line-too-long logger.info('No config loader found for specified config version, or config version not specified. Using default.') loader = _CONFIG_VERSIONS_TO_LOADERS['default']() return loader
def _update_orgunit(self, org_model, orgunit_name): logger.info('Updating orgunit %s', orgunit_name) orgunit_parameters = { 'OrganizationalUnitId': org_model.updated_model.orgunits[orgunit_name]['id'], 'Name': org_model.orgunits[orgunit_name]['name'] } update_res = self.client.update_organizational_unit( **orgunit_parameters) return update_res['OrganizationalUnit']['Id']
def _update_policy(self, org_model, policy_name): logger.info('Updating policy %s', policy_name) content_json = json.dumps( org_model.policies[policy_name]['document']['content']) policy_parameters = { 'Content': content_json, 'Description': org_model.policies[policy_name]['description'], 'Name': org_model.policies[policy_name]['name'], 'PolicyId': org_model.updated_model.policies[policy_name]['id'] } update_res = self.client.update_policy(**policy_parameters) return update_res['Policy']['PolicySummary']['Id']
def _create_policy(self, org_model, policy_name): logger.info('Creating policy %s', policy_name) content_json = json.dumps( org_model.policies[policy_name]['document']['content']) policy_parameters = { 'Content': content_json, 'Description': org_model.policies[policy_name]['description'], 'Name': org_model.policies[policy_name]['name'], 'Type': 'SERVICE_CONTROL_POLICY' } create_res = self.client.create_policy(**policy_parameters) return create_res['Policy']['PolicySummary']['Id']
def delete_policy(self, organization, policy_name): ''' Deletes a Service Control Policy from the organization and returns a changes dict for the converge report. The caller is responsible for ensuring that all policy attachments have already been removed to ensure successful deletion. ''' logger.info('Deleting policy %s', policy_name) policy_id = organization.aws_model.policies[policy_name]['id'] self.client.delete_policy(PolicyId=policy_id) return {'change': 'deleted', 'id': policy_id}
def run(): ''' Parses arguments and executes from the command line. ''' args = _parse_args() provisioner_overrides = {} if args.profile: provisioner_overrides['profile'] = args.profile log_handling.enable_console_logging(level=args.log_level) org_model = config_loader.load_organization_from_yaml_file( args.config_file) dry_run_report = org_model.dry_run( provisioner_overrides=provisioner_overrides) dry_run_report[ 'configured_organization'] = config_loader.dump_organization_to_config( dry_run_report['configured_organization']) dry_run_report[ 'actual_organization'] = config_loader.dump_organization_to_config( dry_run_report['actual_organization']) dry_run_report_yaml = helpers.ordered_yaml_dump(dry_run_report) logger.info('Dry run report follows.') logger.info(dry_run_report_yaml) if args.dry_run_report_file: helpers.write_report(report=dry_run_report_yaml, output_file=args.dry_run_report_file) if args.converge: converge_report = org_model.converge( dry_run_report=dry_run_report, provisioner_overrides=provisioner_overrides) converge_report_yaml = helpers.ordered_yaml_dump(converge_report) logger.info('Converge report follows.') logger.info(converge_report_yaml) if args.converge_report_file: helpers.write_report(report=converge_report_yaml, output_file=args.converge_report_file)
def _rebuild_orgunits(self, org_service): logger.info("Rebuilding the OrganizationalUnit hierarchy.") for orgunit in self.aws_model.orgunits.values(): self._remove_accounts_from_orgunit(orgunit_name=orgunit['name'], org_service=org_service) self._remove_orgunits(org_service=org_service) self._create_orgunits(org_service=org_service) self.updated_model.reload_orgunits(org_service=org_service) self.updated_model.reload_policies(org_service=org_service) for orgunit in self.orgunits: org_service.update_orgunit_policies(organization=self, orgunit_name=orgunit) for account in self.orgunits[orgunit].get('accounts', []): if not account in self.updated_model.accounts: continue org_service.move_account(organization=self, account_name=account, parent_name=orgunit)
def _wait_on_account_creation(self, creation_statuses): logger.info('Waiting on account creation to complete.') waiting_accounts = creation_statuses.keys() while waiting_accounts: # Wait 10 seconds between checking account statuses time.sleep(10) for account in waiting_accounts: request_id = creation_statuses[account]['Id'] creation_statuses[ account] = self.client.describe_create_account_status( CreateAccountRequestId=request_id )['CreateAccountStatus'] waiting_accounts = [ account for account in waiting_accounts if creation_statuses[account]['State'] == 'IN_PROGRESS' ]
def dry_run(self, provisioner_overrides=None): ''' Loads a corresponding model from AWS and generates a report of a comparison against it and actions that would be taken to converge the AWS resources to match this model. ''' if provisioner_overrides: for name, value in provisioner_overrides.items(): self.provisioner[name] = value report = OrderedDict() self.raise_if_invalid() logger.info("Loading model from existing AWS resources.") self.initialize_aws_model() aws_model_problems = self.aws_model.validate() if aws_model_problems: report['aws_model_problems'] = aws_model_problems report = self.compare_against_aws_model(report=report) return report
def load_orgunits(self, organization): ''' Loads information about OrganizationalUnits into an initialized AWS Organization model from AWS. The caller is responsible for already having loaded accounts into the Organization model so that account names can be loaded into organizations from account IDs. It doesn't return anything useful as it's modifying the provided Organization model directly. ''' logger.debug("Organization model ids_to_children follows.") logger.debug(helpers.pretty_format(organization.ids_to_children)) for orgunit_id in organization.ids_to_children[ organization.root_parent_id]['orgunits']: self._load_orgunit(org_model=organization, orgunit_id=orgunit_id) self._add_orgunit_children_to_parents(org_model=organization)
def load_accounts(self, organization): ''' Loads existing accounts into organization.accounts. It also creates organization.account_ids_to_names with a mapping of account Id values to account Name values, for use by load_orgunits in loading the names of child accounts. ''' paginator = self.client.get_paginator('list_accounts') for page in paginator.paginate(): logger.debug("list_accounts page response follows") logger.debug(helpers.pretty_format(page)) for account in page['Accounts']: account_model = { "name": account["Name"], "owner": account["Email"], "id": str(account["Id"]), "regions": [] } organization.accounts[account["Name"]] = account_model organization.account_ids_to_names[ account["Id"]] = account["Name"]
def create_orgunit(self, org_model, orgunit_name, parent_name): ''' Creates the specified orgunit from the configured Organization model. The caller is responsible for ensuring that the Organization is in the appropriate state for the Organization to be created (e.g., parent Orgunits have already been created. ''' logger.info('Creating orgunit %s', orgunit_name) if parent_name == 'root': parent_id = org_model.updated_model.root_parent_id else: parent_id = org_model.updated_model.orgunits[parent_name]['id'] orgunit_parameters = { # Set the parent ID to the root ID, we'll update it later. 'ParentId': parent_id, 'Name': org_model.orgunits[orgunit_name]['name'] } create_res = self.client.create_organizational_unit( **orgunit_parameters) return create_res['OrganizationalUnit']['Id']
def update_entity_policy_attachments(self, org_model, target_id, old_policies, new_policies): ''' Updates the policies associations for the target entity ''' for policy in new_policies: if policy not in old_policies: logger.info('Adding policy association for %s to target %s', policy, target_id) policy_id = org_model.updated_model.policies[policy]['id'] try: self.client.attach_policy(PolicyId=policy_id, TargetId=target_id) except botocore.exceptions.ClientError as err: if 'DuplicatePolicyAttachmentException' in str(err): logger.warning( 'Policy %s is already attached to target %s', policy, target_id) else: raise for policy in old_policies: if policy not in new_policies: logger.info('Removing policy association for %s to target %s', policy, target_id) policy_id = org_model.updated_model.policies[policy]['id'] self.client.detach_policy(PolicyId=policy_id, TargetId=target_id)
def load_organization_from_config(self, config): ''' Creates a new organization from the provided configuration datastructure. Returns the created organization. ''' organization = Organization(root_account_id=str(config['root'])) organization.source = "config" organization.raw_config = config if 'root_policies' in config: organization.root_policies = config['root_policies'] else: organization.root_policies = self._default_root_policies if 'featureset' in config: organization.featureset = config['featureset'] else: logger.info('featureset parameter not present on organization, assuming "ALL"') organization.featureset = self._default_featureset if 'accounts' in config: organization.accounts = self._load_accounts(config['accounts']) if 'policies' in config: organization.policies = self._load_policies(config['policies']) if 'orgunits' in config: organization.orgunits = self._load_orgunits(config['orgunits']) return organization
def move_account(self, organization, account_name, parent_name): ''' Reassociates an account with a new parent and returns a changes dict for the converge report. No change will be made if the account is currently associated with the specified parent. ''' if parent_name == 'root': dest_parent_id = organization.aws_model.root_parent_id else: dest_parent_id = organization.updated_model.orgunits[parent_name][ 'id'] account_id = organization.updated_model.accounts[account_name]['id'] list_parents_res = self.client.list_parents(ChildId=account_id) source_parent_id = list_parents_res['Parents'][0]['Id'] if dest_parent_id != source_parent_id: logger.info('Associating account %s with parent %s', account_name, parent_name) self.client.move_account(AccountId=account_id, SourceParentId=source_parent_id, DestinationParentId=dest_parent_id) return {"changes": "reassociated", "parent": dest_parent_id} return {}
def delete_orgunit(self, organization, orgunit_name): ''' Deletes an OrganizationalUnit from the organization and returns a changes dict for the converge report. The caller is responsible for ensuring that the Orgunit to be deleted has no child Accounts or Orgunits to ensure successful deletion. ''' logger.info('Deleting orgunit %s', orgunit_name) orgunit_id = organization.aws_model.orgunits[orgunit_name]['id'] try: self.client.delete_organizational_unit( OrganizationalUnitId=orgunit_id) return {'change': 'deleted', 'id': orgunit_id} except botocore.exceptions.ClientError as err: logger.info('Orgunit %s already deleted.', orgunit_name) logger.info(str(err)) return None
def load_organization(self, organization): ''' Initializes an Organization model with information about the Organization and its OrganizationUnit and Account hierarchy. It doesn't return anything useful as it's modifying the provided Organization model directly. ''' try: describe_org_response = self.client.describe_organization() if not describe_org_response['Organization']: organization.exists = False logger.debug("describe_organization response follows.") logger.debug(helpers.pretty_format(describe_org_response)) except botocore.exceptions.ClientError: logger.info( "Got an error trying to describe the organization, assuming organization does not exist." ) organization.exists = False if organization.exists: self._set_organization_attributes( org_model=organization, describe_org_response=describe_org_response['Organization'])
def _remove_accounts_from_orgunit(self, orgunit_name, org_service): logger.info("Moving accounts associated with %s to the organization root temporarily", orgunit_name) for account in self.updated_model.orgunits[orgunit_name].get('accounts', []): org_service.move_account(organization=self, account_name=account, parent_name='root')
def _set_org_ids_to_children(self, org_model, parent): logger.debug("Enumerating children for parent %s", parent) orgunit_children = {"Children": []} paginator = self.client.get_paginator('list_children') list_orgunit_children_params = { "ParentId": parent, "ChildType": "ORGANIZATIONAL_UNIT" } logger.debug("list_children orgunit call params follow") logger.debug(helpers.pretty_format(list_orgunit_children_params)) for page in paginator.paginate(**list_orgunit_children_params): logger.debug("Page response follows.") logger.debug(helpers.pretty_format(page)) if page['Children']: orgunit_children['Children'] += page['Children'] account_children = {"Children": []} list_account_children_params = { "ParentId": parent, "ChildType": "ACCOUNT" } logger.debug("list_children account call params follow") logger.debug(helpers.pretty_format(list_account_children_params)) for page in paginator.paginate(**list_account_children_params): logger.debug("Page response follows.") logger.debug(helpers.pretty_format(page)) if page['Children']: account_children['Children'] += page['Children'] logger.debug("orgunit children response follows:") logger.debug(helpers.pretty_format(orgunit_children)) logger.debug("account children response follows:") logger.debug(helpers.pretty_format(account_children)) if not parent in org_model.ids_to_children: org_model.ids_to_children[parent] = { "accounts": [], "orgunits": [] } if account_children['Children']: for account in account_children['Children']: org_model.ids_to_children[parent]['accounts'].append( account['Id']) if orgunit_children['Children']: for orgunit in orgunit_children['Children']: org_model.ids_to_children[parent]['orgunits'].append( orgunit['Id']) self._set_org_ids_to_children(org_model=org_model, parent=orgunit['Id'])