-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathsupport_cases_aggregator.py
executable file
·220 lines (187 loc) · 7.68 KB
/
support_cases_aggregator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"""
Function that manages persistence and retrieval of AWS Support
service case information
"""
import logging
import os
from datetime import datetime, timedelta
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError, BotoCoreError
from aws_common_utils_layer import (
get_session_with_arn,
get_session,
clear_empty_strings,
set_logging_level,
)
set_logging_level()
DEFAULT_CASE_LOOKBACK_DAYS = 60
# Provide IAM role that is assumed in the member account
# where the support case was opened
ORG_SUPPORT_VIEWER_ROLE = "GetSupportInfoRole"
def list_account_ids():
"""
Default requires permission to invoke organizations:ListAccounts API.
DEFAULTS TO CALLING organizations:ListAccounts WITH CURRENT ROLE
If CloudFormation stack is deployed in non-master AWS Organizations
account, must assume role in that master AWS Organizations account.
See README for details.
"""
accounts = []
assumed_role_arn = os.environ.get("ORG_MASTER_ACCOUNT_VIEWER_ROLE")
if assumed_role_arn:
session = get_session_with_arn(
role_arn=assumed_role_arn, session_name="listAccountIds", base_session=None
)
else:
session = boto3.session.Session() # get local session
try:
client = session.client(
"organizations", config=Config(retries={"max_attempts": 8})
)
paginator = client.get_paginator("list_accounts")
response_iterator = paginator.paginate()
for page in response_iterator:
accounts.extend(page.get("Accounts", []))
except (BotoCoreError, ClientError) as e:
if e.response["Error"]["Code"] == "AccessDeniedException":
logging.error(e)
logging.error(
"Could not call organizations:ListAccounts. "
"Current account is likely not "
"the AWS Organizations master account. "
"See README for more details on setup. "
"Returning empty list by default."
)
return []
return [str(account_info.get("Id", "")) for account_info in accounts]
def get_all_existing_cases(recent_cases_only):
"""
This function is called on every cron interval
to reload the support cases DynamoDB table.
"""
account_ids = list_account_ids()
dynamodb_session = boto3.session.Session()
dynamodb = dynamodb_session.resource("dynamodb")
support_cases_table = dynamodb.Table(os.environ.get("SUPPORT_CASES_TABLE_NAME"))
for account_id in account_ids:
session = get_session(account_id, ORG_SUPPORT_VIEWER_ROLE, "get_support_info")
client = session.client("support")
if recent_cases_only:
update_recent_cases(support_cases_table, account_id, client)
else:
update_all_cases(support_cases_table, account_id, client)
def update_recent_cases(
support_cases_table, account_id, client, days=DEFAULT_CASE_LOOKBACK_DAYS
):
"""
Only retrieve updates within last X days to avoid unnecessary duplication
"""
kwargs = {
"includeResolvedCases": True,
"maxResults": 100,
"afterTime": (datetime.now() - timedelta(days=days)).isoformat(),
}
update_cases_helper(support_cases_table, account_id, client, kwargs)
def update_all_cases(support_cases_table, account_id, client):
"""
For a manual update of every case.
"""
kwargs = {"includeResolvedCases": True, "maxResults": 100}
update_cases_helper(support_cases_table, account_id, client, kwargs)
def update_cases_helper(support_cases_table, account_id, client, kwargs):
"""
:param support_cases_table: DDB table name
:param account_id: account id
:param client: DDB session object
:param kwargs: pagination params
:return: None
"""
try:
case_response = client.describe_cases(**kwargs)
except (ClientError, BotoCoreError) as e:
if e.response["Error"]["Code"] == "SubscriptionRequiredException":
logging.error("Failed subscription for account %s; ignoring", account_id)
return
raise e
for case in case_response.get("cases"):
# WARNING: recentCommunications is only the last 5 communications.
if case.get("recentCommunications", {}).get("nextToken"):
del case["recentCommunications"]["nextToken"]
# put updated info into table
case["AccountId"] = account_id
support_cases_table.put_item(Item=case)
if "nextToken" in case_response:
kwargs["nextToken"] = case_response["nextToken"]
update_cases_helper(support_cases_table, account_id, client, kwargs)
def lambda_handler(event, context):
"""
:param event:
Event will be either a CloudWatch Event triggered periodically,
whose source is aws.events, or event will be the payload from
aws_cloudtrail_process.py in the following format:
{
"AccountId": account_id string,
"CaseIds": [caseid1 string, caseid2 string, etc.]
}
:param context:
see https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html
:return: None
"""
logging.debug(context)
logging.debug(event)
if event.get("source") == "aws.events" or "ManualUpdate" in event:
logging.info("Invocation to ensure support info is up-to-date")
# Note: ensure list_account_ids() works and has proper permissions
return get_all_existing_cases(recent_cases_only=True)
account_id = event.get("AccountId")
# Note: Case Id format is
# case-ACCOUNT_NUMBER-<alphanumeric>-YEAR-<other_alphanumeric>
# The 10-digit "Case Id" viewed from the
# Support console is the Display Id of the case.
case_ids = event.get("CaseIds")
# assume role
member_account_session = get_session(
account_id, ORG_SUPPORT_VIEWER_ROLE, "get_support_info"
)
# support client has difficult in regions that aren't us-east-1 strangely
client = member_account_session.client("support", region_name="us-east-1")
# Use current role
dynamodb_session = boto3.session.Session()
dynamodb = dynamodb_session.resource("dynamodb")
support_cases_table = dynamodb.Table(os.environ.get("SUPPORT_CASES_TABLE_NAME"))
with support_cases_table.batch_writer() as support_table_batch:
for case_id in case_ids:
# get support info
try:
case_response = client.describe_cases(
caseIdList=[case_id], includeResolvedCases=True
)
except ClientError as e:
logging.error("error on %s", case_id)
if e.response["Error"]["Code"] == "SubscriptionRequiredException":
logging.error(
"Failed subscription for account %s, "
"need Enterprise Support; ignoring",
account_id,
)
support_cases_table.put_item(
Item={
"caseId": case_id,
"status": "** N/A; " "Must Enable Enterprise Support **",
}
)
continue
raise e
except Exception as e:
logging.error("error on %s", case_id)
raise e
case = case_response.get("cases")[0]
# WARNING: recentCommunications is only the last 5 communications.
if case.get("recentCommunications", {}).get("nextToken"):
del case["recentCommunications"]["nextToken"]
clear_empty_strings(case)
# put updated info into table
case["AccountId"] = account_id
support_table_batch.put_item(Item=case)
return True