Skip to content

Commit 385c0e4

Browse files
authored
Merge pull request #233 from StewartW/enable-enterprise-support-on-creation
Supporting AWS Support Subscriptions
2 parents 2199bb7 + 651795c commit 385c0e4

6 files changed

Lines changed: 158 additions & 5 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.vscode
2+
.idea
23
.pyc
34
.zip
45
.DS_Store
@@ -134,4 +135,4 @@ venv.bak/
134135
dmypy.json
135136

136137
# Pyre type checker
137-
.pyre/
138+
.pyre/

src/lambda_codebase/initial_commit/bootstrap_repository/adf-accounts/readme.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ The OU name is the name of the direct parent of the account. If you want to move
1818
- Create and update account alias.
1919
- Account tagging.
2020
- Allow the account access to view its own billing.
21+
- Set up support subscriptions during account provisioning
2122

2223
### Currently not supported
2324

2425
- Updating account names
2526
- Updating account email addresses
2627
- Removing accounts
2728
- Handling root account credentials and MFA
29+
- Changing the support subscription of an account.
2830

2931
### Configuration Parameters
3032

@@ -33,6 +35,9 @@ The OU name is the name of the direct parent of the account. If you want to move
3335
- `email`: Email associated by the account, must be valid otherwise it is not possible to access as root user when needed
3436
- `delete_default_vpc`: `True|False` if Default VPCs need to be delete from all AWS Regions.
3537
- `allow_billing`: `True|False` if the account see its own costs within the organization.
38+
- `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.
39+
**NB: This is for activating enterprise support on account creation only. As a prerequisite your organization master account must already have enterprise support activated**
40+
3641
- `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
3742
- `tags`: list of tags associate to the account.
3843

@@ -47,6 +52,7 @@ accounts:
4752
4853
allow_billing: False
4954
delete_default_vpc: True
55+
support_level: enterprise
5056
alias: prod-company-1
5157
tags:
5258
- created_by: adf
@@ -62,6 +68,7 @@ accounts:
6268
6369
allow_billing: True
6470
delete_default_vpc: False
71+
support_level: basic
6572
alias: test-company-11
6673
tags:
6774
- created_by: adf

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/main.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import os
1010
from concurrent.futures import ThreadPoolExecutor
1111
import boto3
12-
from src import read_config_files, delete_default_vpc
12+
from src import read_config_files, delete_default_vpc, Support
1313
from organizations import Organizations
1414
from logger import configure_logger
1515
from parameter_store import ParameterStore
@@ -26,6 +26,7 @@ def main():
2626
return
2727
LOGGER.info(f"Found {len(accounts)} account(s) in configuration file(s).")
2828
organizations = Organizations(boto3)
29+
support = Support(boto3)
2930
all_accounts = organizations.get_accounts()
3031
parameter_store = ParameterStore(os.environ.get('AWS_REGION', 'us-east-1'), boto3)
3132
adf_role_name = parameter_store.fetch_parameter('cross_account_access_role')
@@ -34,23 +35,26 @@ def main():
3435
account_id = next(acc["Id"] for acc in all_accounts if acc["Name"] == account.full_name)
3536
except StopIteration: # If the account does not exist yet..
3637
account_id = None
37-
create_or_update_account(organizations, account, adf_role_name, account_id)
38+
create_or_update_account(organizations, support, account, adf_role_name, account_id)
3839

3940

40-
def create_or_update_account(org_session, account, adf_role_name, account_id=None):
41+
def create_or_update_account(org_session, support_session, account, adf_role_name, account_id=None):
4142
"""Creates or updates a single AWS account.
4243
:param org_session: Instance of Organization class
4344
:param account: Instance of Account class
4445
"""
4546
if not account_id:
4647
LOGGER.info(f'Creating new account {account.full_name}')
4748
account_id = org_session.create_account(account, adf_role_name)
49+
# This only runs on account creation at the moment.
50+
support_session.set_support_level_for_account(account, account_id)
51+
4852
sts = STS()
4953
role = sts.assume_cross_account_role(
5054
'arn:aws:iam::{0}:role/{1}'.format(
5155
account_id,
5256
adf_role_name
53-
), 'delete_default_vpc'
57+
), 'adf_account_provisioning'
5458
)
5559

5660
LOGGER.info(f'Ensuring account {account_id} (alias {account.alias}) is in OU {account.ou_path}')

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
from .configparser import read_config_files
88
from .vpc import delete_default_vpc
99
from .account import Account
10+
from .support import Support, SupportLevel

src/lambda_codebase/initial_commit/bootstrap_repository/adf-build/provisioner/src/account.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def __init__(
1717
delete_default_vpc=False,
1818
allow_direct_move_between_ou=False,
1919
allow_billing=True,
20+
support_level='basic',
2021
tags=None
2122
):
2223
self.full_name = full_name
@@ -26,6 +27,7 @@ def __init__(
2627
self.allow_direct_move_between_ou = allow_direct_move_between_ou
2728
self.allow_billing = allow_billing
2829
self.alias = alias
30+
self.support_level = support_level
2931

3032
if tags is None:
3133
self.tags = {}
@@ -51,6 +53,9 @@ def load_from_config(cls, config):
5153
allow_billing=config.get(
5254
"allow_billing",
5355
True),
56+
support_level=config.get(
57+
"support_level",
58+
'basic'),
5459
tags=config.get(
5560
"tags",
5661
{}))
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Support module used throughout the ADF
2+
"""
3+
from enum import Enum
4+
from botocore.config import Config
5+
from botocore.exceptions import ClientError, BotoCoreError
6+
from logger import configure_logger
7+
from .account import Account
8+
9+
10+
LOGGER = configure_logger(__name__)
11+
12+
13+
class SupportLevel(Enum):
14+
BASIC = "basic"
15+
DEVELOPER = "developer"
16+
BUSINESS = "business"
17+
ENTERPRISE = "enterprise"
18+
19+
20+
class Support: # pylint: disable=R0904
21+
"""Class used for accessing AWS Support API
22+
"""
23+
_config = Config(retries=dict(max_attempts=30))
24+
25+
def __init__(self, role):
26+
self.client = role.client("support", region_name='us-east-1', config=Support._config)
27+
28+
def get_support_level(self) -> SupportLevel:
29+
"""
30+
Gets the AWS Support Level of the current Account
31+
based on the Role passed in during the init of the Support class.
32+
33+
:returns:
34+
SupportLevels Enum defining the level of AWS support.
35+
36+
:raises:
37+
ClientError
38+
BotoCoreError
39+
40+
"""
41+
try:
42+
severity_levels = self.client.get_severity_levels()['severityLevels']
43+
available_support_codes = [level['code'] for level in severity_levels]
44+
45+
# See: https://aws.amazon.com/premiumsupport/plans/ for insights into the interpretation of
46+
# the available support codes.
47+
48+
if 'critical' in available_support_codes: # Business Critical System Down Severity
49+
return SupportLevel.ENTERPRISE
50+
if 'urgent' in available_support_codes: # Production System Down Severity
51+
return SupportLevel.BUSINESS
52+
if 'low' in available_support_codes: # System Impaired Severity
53+
return SupportLevel.DEVELOPER
54+
55+
return SupportLevel.BASIC
56+
57+
except (ClientError, BotoCoreError) as e:
58+
if e.response["Error"]["Code"] == "SubscriptionRequiredException":
59+
LOGGER.info('Enterprise Support is not enabled')
60+
return SupportLevel.BASIC
61+
raise
62+
63+
def set_support_level_for_account(self, account: Account, account_id: str, current_level: SupportLevel = SupportLevel.BASIC):
64+
"""
65+
Sets the support level for the account. If the current_value is the same as the value in the instance
66+
of the account Class it will not create a new ticket.
67+
68+
Currently only supports "basic|enterprise" tiers.
69+
70+
:param account: Instance of Account class
71+
:param account_id: AWS Account ID of the account that will have support configured for it.
72+
:param current_level: SupportLevel value that represents the current support tier of the account (Default: Basic)
73+
:return: Void
74+
:raises: ValueError if account.support_level is not a valid/supported SupportLevel.
75+
"""
76+
desired_level = SupportLevel(account.support_level)
77+
78+
if desired_level is current_level:
79+
LOGGER.info(f'Account {account.full_name} ({account_id}) already has {desired_level.value} support enabled.')
80+
81+
elif desired_level is SupportLevel.ENTERPRISE:
82+
LOGGER.info(f'Enabling {desired_level.value} for Account {account.full_name} ({account_id})')
83+
self._enable_support_for_account(account, account_id, desired_level)
84+
85+
else:
86+
LOGGER.error(f'Invalid support tier configured: {desired_level.value}. '
87+
f'Currently only "{SupportLevel.BASIC.value}" or "{SupportLevel.ENTERPRISE.value}" '
88+
'are accepted.', exc_info=True)
89+
raise ValueError(f'Invalid Support Tier Value: {desired_level.value}')
90+
91+
def _enable_support_for_account(self, account: Account, account_id, desired_level: SupportLevel):
92+
"""
93+
Raises a support ticket in the organization root account, enabling support for the account specified
94+
by account_id.
95+
96+
:param account: Instance of Account class
97+
:param account_id: AWS Account ID, of the account that will have support configured
98+
:param desired_level: Desired Support Level
99+
:return: Void
100+
:raises: ClientError, BotoCoreError.
101+
"""
102+
try:
103+
cc_email = account.email
104+
subject = f'[ADF] Enable {desired_level.value} Support for account: {account_id}'
105+
body = (
106+
f'Hello, \n'
107+
f'Can {desired_level.value} support be enabled on Account: {account_id} ({account.email}) \n'
108+
'Thank you!\n'
109+
'(This ticket was raised automatically via ADF)'
110+
111+
)
112+
LOGGER.info(f'Creating AWS Support ticket. {desired_level.value} Support for Account '
113+
f'{account.full_name}({account_id})')
114+
115+
response = self.client.create_case(
116+
subject=subject,
117+
serviceCode='account-management',
118+
severityCode='low',
119+
categoryCode='billing',
120+
communicationBody=body,
121+
ccEmailAddresses=[
122+
cc_email,
123+
],
124+
language='en',
125+
)
126+
127+
LOGGER.info(f'AWS Support ticket: {response["caseId"]} '
128+
f'has been created. {desired_level.value} Support has '
129+
f'been requested on Account {account.full_name} ({account_id}). '
130+
f'{account.email} has been CCd')
131+
132+
except (ClientError, BotoCoreError):
133+
LOGGER.error(f'Failed to enable {desired_level.value} support for account: '
134+
f'{account.full_name} ({account.alias}): {account_id}', exc_info=True)
135+
raise

0 commit comments

Comments
 (0)