diff --git a/.gitignore b/.gitignore index 1917cf5f0..9fca97731 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode +.idea .pyc .zip .DS_Store @@ -134,4 +135,4 @@ venv.bak/ dmypy.json # Pyre type checker -.pyre/ \ No newline at end of file +.pyre/ diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-accounts/readme.md b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-accounts/readme.md index b14c2e006..7f9e5d298 100644 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-accounts/readme.md +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-accounts/readme.md @@ -18,6 +18,7 @@ The OU name is the name of the direct parent of the account. If you want to move - Create and update account alias. - Account tagging. - Allow the account access to view its own billing. +- Set up support subscriptions during account provisioning ### Currently not supported @@ -25,6 +26,7 @@ The OU name is the name of the direct parent of the account. If you want to move - Updating account email addresses - Removing accounts - Handling root account credentials and MFA +- Changing the support subscription of an account. ### Configuration Parameters @@ -33,6 +35,9 @@ The OU name is the name of the direct parent of the account. If you want to move - `email`: Email associated by the account, must be valid otherwise it is not possible to access as root user when needed - `delete_default_vpc`: `True|False` if Default VPCs need to be delete from all AWS Regions. - `allow_billing`: `True|False` if the account see its own costs within the organization. +- `support_level`: `basic|enterprise` ADF will raise a ticket to add the account to an existing AWS support subscription when an account is created. Currently only supports basic or enterprise. + **NB: This is for activating enterprise support on account creation only. As a prerequisite your organization master account must already have enterprise support activated** + - `alias`: AWS account alias. Must be unique globally otherwise cannot be created. Check [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/console_account-alias.html) for further details. If the account alias is not created or already exists, in the Federation login page, no alias will be presented - `tags`: list of tags associate to the account. @@ -47,6 +52,7 @@ accounts: email: prod-team-1@company.com allow_billing: False delete_default_vpc: True + support_level: enterprise alias: prod-company-1 tags: - created_by: adf @@ -62,6 +68,7 @@ accounts: email: test-team-1@company.com allow_billing: True delete_default_vpc: False + support_level: basic alias: test-company-11 tags: - created_by: adf diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/main.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/main.py index d1d4850b9..7cf0466f6 100755 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/main.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/main.py @@ -9,7 +9,7 @@ import os from concurrent.futures import ThreadPoolExecutor import boto3 -from src import read_config_files, delete_default_vpc +from src import read_config_files, delete_default_vpc, Support from organizations import Organizations from logger import configure_logger from parameter_store import ParameterStore @@ -26,6 +26,7 @@ def main(): return LOGGER.info(f"Found {len(accounts)} account(s) in configuration file(s).") organizations = Organizations(boto3) + support = Support(boto3) all_accounts = organizations.get_accounts() parameter_store = ParameterStore(os.environ.get('AWS_REGION', 'us-east-1'), boto3) adf_role_name = parameter_store.fetch_parameter('cross_account_access_role') @@ -34,10 +35,10 @@ def main(): account_id = next(acc["Id"] for acc in all_accounts if acc["Name"] == account.full_name) except StopIteration: # If the account does not exist yet.. account_id = None - create_or_update_account(organizations, account, adf_role_name, account_id) + create_or_update_account(organizations, support, account, adf_role_name, account_id) -def create_or_update_account(org_session, account, adf_role_name, account_id=None): +def create_or_update_account(org_session, support_session, account, adf_role_name, account_id=None): """Creates or updates a single AWS account. :param org_session: Instance of Organization class :param account: Instance of Account class @@ -45,12 +46,15 @@ def create_or_update_account(org_session, account, adf_role_name, account_id=Non if not account_id: LOGGER.info(f'Creating new account {account.full_name}') account_id = org_session.create_account(account, adf_role_name) + # This only runs on account creation at the moment. + support_session.set_support_level_for_account(account, account_id) + sts = STS() role = sts.assume_cross_account_role( 'arn:aws:iam::{0}:role/{1}'.format( account_id, adf_role_name - ), 'delete_default_vpc' + ), 'adf_account_provisioning' ) LOGGER.info(f'Ensuring account {account_id} (alias {account.alias}) is in OU {account.ou_path}') diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/__init__.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/__init__.py index 95ca44064..8f1a9c905 100755 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/__init__.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/__init__.py @@ -7,3 +7,4 @@ from .configparser import read_config_files from .vpc import delete_default_vpc from .account import Account +from .support import Support, SupportLevel diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/account.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/account.py index 75eb0fdda..a94fedba1 100755 --- a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/account.py +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/account.py @@ -17,6 +17,7 @@ def __init__( delete_default_vpc=False, allow_direct_move_between_ou=False, allow_billing=True, + support_level='basic', tags=None ): self.full_name = full_name @@ -26,6 +27,7 @@ def __init__( self.allow_direct_move_between_ou = allow_direct_move_between_ou self.allow_billing = allow_billing self.alias = alias + self.support_level = support_level if tags is None: self.tags = {} @@ -51,6 +53,9 @@ def load_from_config(cls, config): allow_billing=config.get( "allow_billing", True), + support_level=config.get( + "support_level", + 'basic'), tags=config.get( "tags", {})) diff --git a/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/support.py b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/support.py new file mode 100644 index 000000000..7e98cb2d4 --- /dev/null +++ b/src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/support.py @@ -0,0 +1,135 @@ +"""Support module used throughout the ADF +""" +from enum import Enum +from botocore.config import Config +from botocore.exceptions import ClientError, BotoCoreError +from logger import configure_logger +from .account import Account + + +LOGGER = configure_logger(__name__) + + +class SupportLevel(Enum): + BASIC = "basic" + DEVELOPER = "developer" + BUSINESS = "business" + ENTERPRISE = "enterprise" + + +class Support: # pylint: disable=R0904 + """Class used for accessing AWS Support API + """ + _config = Config(retries=dict(max_attempts=30)) + + def __init__(self, role): + self.client = role.client("support", region_name='us-east-1', config=Support._config) + + def get_support_level(self) -> SupportLevel: + """ + Gets the AWS Support Level of the current Account + based on the Role passed in during the init of the Support class. + + :returns: + SupportLevels Enum defining the level of AWS support. + + :raises: + ClientError + BotoCoreError + + """ + try: + severity_levels = self.client.get_severity_levels()['severityLevels'] + available_support_codes = [level['code'] for level in severity_levels] + + # See: https://aws.amazon.com/premiumsupport/plans/ for insights into the interpretation of + # the available support codes. + + if 'critical' in available_support_codes: # Business Critical System Down Severity + return SupportLevel.ENTERPRISE + if 'urgent' in available_support_codes: # Production System Down Severity + return SupportLevel.BUSINESS + if 'low' in available_support_codes: # System Impaired Severity + return SupportLevel.DEVELOPER + + return SupportLevel.BASIC + + except (ClientError, BotoCoreError) as e: + if e.response["Error"]["Code"] == "SubscriptionRequiredException": + LOGGER.info('Enterprise Support is not enabled') + return SupportLevel.BASIC + raise + + def set_support_level_for_account(self, account: Account, account_id: str, current_level: SupportLevel = SupportLevel.BASIC): + """ + Sets the support level for the account. If the current_value is the same as the value in the instance + of the account Class it will not create a new ticket. + + Currently only supports "basic|enterprise" tiers. + + :param account: Instance of Account class + :param account_id: AWS Account ID of the account that will have support configured for it. + :param current_level: SupportLevel value that represents the current support tier of the account (Default: Basic) + :return: Void + :raises: ValueError if account.support_level is not a valid/supported SupportLevel. + """ + desired_level = SupportLevel(account.support_level) + + if desired_level is current_level: + LOGGER.info(f'Account {account.full_name} ({account_id}) already has {desired_level.value} support enabled.') + + elif desired_level is SupportLevel.ENTERPRISE: + LOGGER.info(f'Enabling {desired_level.value} for Account {account.full_name} ({account_id})') + self._enable_support_for_account(account, account_id, desired_level) + + else: + LOGGER.error(f'Invalid support tier configured: {desired_level.value}. ' + f'Currently only "{SupportLevel.BASIC.value}" or "{SupportLevel.ENTERPRISE.value}" ' + 'are accepted.', exc_info=True) + raise ValueError(f'Invalid Support Tier Value: {desired_level.value}') + + def _enable_support_for_account(self, account: Account, account_id, desired_level: SupportLevel): + """ + Raises a support ticket in the organization root account, enabling support for the account specified + by account_id. + + :param account: Instance of Account class + :param account_id: AWS Account ID, of the account that will have support configured + :param desired_level: Desired Support Level + :return: Void + :raises: ClientError, BotoCoreError. + """ + try: + cc_email = account.email + subject = f'[ADF] Enable {desired_level.value} Support for account: {account_id}' + body = ( + f'Hello, \n' + f'Can {desired_level.value} support be enabled on Account: {account_id} ({account.email}) \n' + 'Thank you!\n' + '(This ticket was raised automatically via ADF)' + + ) + LOGGER.info(f'Creating AWS Support ticket. {desired_level.value} Support for Account ' + f'{account.full_name}({account_id})') + + response = self.client.create_case( + subject=subject, + serviceCode='account-management', + severityCode='low', + categoryCode='billing', + communicationBody=body, + ccEmailAddresses=[ + cc_email, + ], + language='en', + ) + + LOGGER.info(f'AWS Support ticket: {response["caseId"]} ' + f'has been created. {desired_level.value} Support has ' + f'been requested on Account {account.full_name} ({account_id}). ' + f'{account.email} has been CCd') + + except (ClientError, BotoCoreError): + LOGGER.error(f'Failed to enable {desired_level.value} support for account: ' + f'{account.full_name} ({account.alias}): {account_id}', exc_info=True) + raise