Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting AWS Support Subscriptions #233

Merged
merged 4 commits into from
Sep 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
sbkok marked this conversation as resolved.
Show resolved Hide resolved
)

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
sbkok marked this conversation as resolved.
Show resolved Hide resolved
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',
)
sbkok marked this conversation as resolved.
Show resolved Hide resolved

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