Skip to content

Commit

Permalink
Merge pull request #233 from StewartW/enable-enterprise-support-on-cr…
Browse files Browse the repository at this point in the history
…eation

Supporting AWS Support Subscriptions
  • Loading branch information
sbkok authored Sep 10, 2020
2 parents 2199bb7 + 651795c commit 385c0e4
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.vscode
.idea
.pyc
.zip
.DS_Store
Expand Down Expand Up @@ -134,4 +135,4 @@ venv.bak/
dmypy.json

# Pyre type checker
.pyre/
.pyre/
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ 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

- Updating account names
- Updating account email addresses
- Removing accounts
- Handling root account credentials and MFA
- Changing the support subscription of an account.

### Configuration Parameters

Expand All @@ -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.

Expand All @@ -47,6 +52,7 @@ accounts:
email: [email protected]
allow_billing: False
delete_default_vpc: True
support_level: enterprise
alias: prod-company-1
tags:
- created_by: adf
Expand All @@ -62,6 +68,7 @@ accounts:
email: [email protected]
allow_billing: True
delete_default_vpc: False
support_level: basic
alias: test-company-11
tags:
- created_by: adf
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand All @@ -34,23 +35,26 @@ 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
"""
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}')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {}
Expand All @@ -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",
{}))
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 385c0e4

Please sign in to comment.