Skip to content

Commit eab7ddc

Browse files
authored
Add cfn test (#595)
1 parent bb52964 commit eab7ddc

File tree

1 file changed

+330
-0
lines changed

1 file changed

+330
-0
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
""" Test of Dashboard creation with the right permission.
2+
3+
Test CID creation via CFN with the correct roles
4+
5+
Personas:
6+
- Admin: a person with full admin access
7+
- Finops who needs to deploy CID via CFN
8+
9+
Procedure:
10+
1. Admin creates a role and adds policies to the role
11+
2. Finops using this role creates CUR and deploys CID
12+
3. Verify dashboard exists
13+
3. Delete all in reverse order
14+
15+
This must be executed with admin priveleges.
16+
"""
17+
import os
18+
import json
19+
import time
20+
import logging
21+
22+
import boto3
23+
24+
logger = logging.getLogger(__name__)
25+
26+
# Console Colors
27+
HEADER = '\033[95m'
28+
BLUE = '\033[94m'
29+
CYAN = '\033[96m'
30+
GREEN = '\033[92m'
31+
WARNING = '\033[93m'
32+
RED = '\033[91m'
33+
END = '\033[0m'
34+
BOLD = '\033[1m'
35+
UNDERLINE = '\033[4m'
36+
37+
account_id = boto3.client('sts').get_caller_identity()['Account']
38+
39+
40+
def delete_bucket(name): # move to tools
41+
"""delete all content and the bucket"""
42+
s3r = boto3.resource('s3')
43+
try:
44+
s3r.Bucket(name).object_versions.delete()
45+
except Exception: # nosec B110 pylint: disable=broad-exception-caught
46+
pass
47+
s3c = boto3.client('s3')
48+
try:
49+
s3c.delete_bucket(Bucket=name)
50+
except s3c.exceptions.NoSuchBucket:
51+
pass
52+
53+
def upload_to_s3(filename): # move to tools
54+
"""upload file object to a temporary bucket and return a public url"""
55+
path = os.path.basename(filename)
56+
s3c = boto3.client('s3')
57+
bucket = f'{account_id}-cid-tests-deleteme'
58+
try:
59+
s3c.create_bucket(Bucket=bucket)
60+
except s3c.exceptions.BucketAlreadyExists:
61+
pass
62+
s3c.upload_file(filename, bucket, path)
63+
return f'https://{bucket}.s3.amazonaws.com/{path}'
64+
65+
def format_event(stack, event): # move to tools
66+
"""format event line"""
67+
line = '\t'.join([
68+
event['Timestamp'].strftime("%H:%M:%S"),
69+
stack,
70+
event['LogicalResourceId'],
71+
event['ResourceStatus'],
72+
event.get('ResourceStatusReason',''),
73+
])
74+
color = END
75+
if '_COMPLETE' in line:
76+
color = GREEN
77+
elif '_FAILED' in line or 'failed to create' in line:
78+
color = RED
79+
return f'{color}{line}{END}'
80+
81+
82+
def watch_stacks(cloudformation, stacks=None): # move to tools
83+
""" watch stacks while they are IN_PROGRESS and/or until they are deleted"""
84+
stacks = stacks or []
85+
last_update = {stack: None for stack in stacks}
86+
in_progress = True
87+
while stacks and in_progress:
88+
time.sleep(5)
89+
in_progress = False
90+
for stack in stacks[:]:
91+
# Check events
92+
events = []
93+
try:
94+
events = cloudformation.describe_stack_events(StackName=stack)['StackEvents']
95+
except cloudformation.exceptions.ClientError as exc:
96+
if 'does not exist' in exc.response['Error']['Message']:
97+
stacks.remove(stack)
98+
logger.info(exc.response['Error']['Message'])
99+
for event in events:
100+
if last_update.get(stack) and last_update.get(stack) >= event['Timestamp']:
101+
continue
102+
logger.info(format_event(stack, event))
103+
last_update[stack] = event['Timestamp']
104+
try:
105+
# Check stack status
106+
current_stack = cloudformation.describe_stacks(StackName=stack)['Stacks'][0]
107+
if 'IN_PROGRESS' in current_stack['StackStatus']:
108+
in_progress = True
109+
# Check nested stacks and add them to tracking
110+
for res in cloudformation.list_stack_resources(StackName=stack)['StackResourceSummaries']:
111+
if res['ResourceType'] == 'AWS::CloudFormation::Stack':
112+
name = res['PhysicalResourceId'].split('/')[-2]
113+
if name not in stacks:
114+
stacks.append(name)
115+
except: # nosec B110 using in tests; pylint: disable=bare-except
116+
pass
117+
118+
def get_qs_user(): # move to tools
119+
""" get any valid qs user """
120+
qs_ = boto3.client('quicksight')
121+
users = qs_.list_users(AwsAccountId=account_id, Namespace='default')['UserList']
122+
assert users, 'No QS users, pleas craete one.' # nosec B101:assert_used
123+
return users[0]['UserName']
124+
125+
def timeit(method): # move to tools
126+
"""timing decorator"""
127+
def timed(*args, **kwargs):
128+
start = time.time()
129+
result = method(*args, **kwargs)
130+
end = time.time()
131+
print(f'{method.__name__} timing: {(end - start) / 60} min')
132+
return result
133+
return timed
134+
135+
136+
def create_finops_role():
137+
""" Create a finops role and grant needed permissions. """
138+
admin_cfn = boto3.client('cloudformation')
139+
admin_iam = boto3.client('iam')
140+
141+
logger.info('As admin creating role for Finops')
142+
role_arn = admin_iam.create_role(
143+
Path='/',
144+
RoleName='TestFinopsRole',
145+
AssumeRolePolicyDocument=json.dumps({
146+
"Version": "2012-10-17",
147+
"Statement": [
148+
{
149+
"Effect": "Allow",
150+
"Principal": {"AWS": f"arn:aws:iam::{account_id}:root" },
151+
"Action": "sts:AssumeRole",
152+
}
153+
]
154+
}),
155+
Description='string'
156+
)['Role']['Arn']
157+
admin_iam.put_role_policy(
158+
RoleName='TestFinopsRole',
159+
PolicyName='finops-access-to-bucket-with-cfn',
160+
PolicyDocument=json.dumps({
161+
"Version": "2012-10-17",
162+
"Statement": [
163+
{
164+
"Sid": "ReadTestBucket",
165+
"Effect": "Allow",
166+
"Action": [
167+
"s3:List*",
168+
"s3:Get*"
169+
],
170+
"Resource": [
171+
f"arn:aws:s3:::{account_id}-cid-tests-deleteme",
172+
f"arn:aws:s3:::{account_id}-cid-tests-deleteme/*"
173+
]
174+
},
175+
]
176+
}),
177+
)
178+
logger.info('Role Created %s', role_arn)
179+
180+
logger.info('As admin creating permissions for Finops')
181+
admin_cfn.create_stack(
182+
StackName="cid-admin",
183+
TemplateURL=upload_to_s3('cfn-templates/cid-admin-policies.yaml'),
184+
Parameters=[
185+
{"ParameterKey": 'RoleName', "ParameterValue":'TestFinopsRole'},
186+
{"ParameterKey": 'QuickSightManagement', "ParameterValue":'no'},
187+
{"ParameterKey": 'QuickSightAdmin', "ParameterValue":'no'},
188+
{"ParameterKey": 'CloudIntelligenceDashboardsCFNManagement', "ParameterValue":'yes'},
189+
{"ParameterKey": 'CURDestination', "ParameterValue":'yes'},
190+
{"ParameterKey": 'CURReplication', "ParameterValue":'no'},
191+
],
192+
Capabilities=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
193+
)
194+
watch_stacks(admin_cfn, ["cid-admin"])
195+
logger.info('Stack created')
196+
197+
198+
def create_cid_as_finops():
199+
"""creates cid with finops role"""
200+
admin_cfn = boto3.client('cloudformation')
201+
202+
logger.info('Logging is as Finops person')
203+
credentials = boto3.client('sts').assume_role(
204+
RoleArn=f"arn:aws:iam::{account_id}:role/TestFinopsRole" ,
205+
RoleSessionName="FinopsPerson"
206+
)['Credentials']
207+
finops_session = boto3.session.Session(
208+
aws_access_key_id=credentials['AccessKeyId'],
209+
aws_secret_access_key=credentials['SecretAccessKey'],
210+
aws_session_token=credentials['SessionToken']
211+
)
212+
logger.info('Finops Session created')
213+
214+
logger.info('As Fionps Creating CUR')
215+
finops_cfn = finops_session.client('cloudformation')
216+
finops_cfn.create_stack(
217+
StackName="CID-CUR-Destination",
218+
TemplateURL=upload_to_s3('cfn-templates/cur-aggregation.yaml'),
219+
Parameters=[
220+
{"ParameterKey": 'DestinationAccountId', "ParameterValue": account_id},
221+
{"ParameterKey": 'ResourcePrefix', "ParameterValue": 'cid'},
222+
{"ParameterKey": 'CreateCUR', "ParameterValue": 'True'},
223+
{"ParameterKey": 'SourceAccountIds', "ParameterValue": ''},
224+
],
225+
Capabilities=['CAPABILITY_IAM'],
226+
)
227+
watch_stacks(admin_cfn, ["CID-CUR-Destination"])
228+
logger.info('Stack created')
229+
230+
logger.info('As Finops Creating Dashboards')
231+
finops_cfn.create_stack(
232+
StackName="Cloud-Intelligence-Dashboards",
233+
TemplateURL=upload_to_s3('cfn-templates/cid-cfn.yml'),
234+
Parameters=[
235+
{"ParameterKey": 'PrerequisitesQuickSight', "ParameterValue": 'yes'},
236+
{"ParameterKey": 'PrerequisitesQuickSightPermissions', "ParameterValue": 'yes'},
237+
{"ParameterKey": 'QuickSightUser', "ParameterValue": get_qs_user()},
238+
{"ParameterKey": 'DeployCUDOSDashboard', "ParameterValue": 'yes'},
239+
],
240+
Capabilities=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
241+
)
242+
watch_stacks(admin_cfn, ["Cloud-Intelligence-Dashboards"])
243+
logger.info('Stack created')
244+
245+
def test_dashboard_exists():
246+
"""check that dashboard exists"""
247+
dash = boto3.client('quicksight').describe_dashboard(
248+
AwsAccountId=account_id,
249+
DashboardId='cudos'
250+
)['Dashboard']
251+
logger.info("Dashboard exists with status = %s", dash['Version']['Status'])
252+
253+
def test_dataset_scheduled():
254+
"""check that dataset and schedule exist"""
255+
schedules = boto3.client('quicksight').list_refresh_schedules(AwsAccountId=account_id, DataSetId='d01a936f-2b8f-49dd-8f95-d9c7130c5e46')['RefreshSchedules']
256+
if not schedules:
257+
raise Exception('Schedules not set') #pylint: disable=broad-exception-raised
258+
259+
def teardown():
260+
"""Cleanup the test"""
261+
admin_cfn = boto3.client('cloudformation')
262+
admin_iam = boto3.client('iam')
263+
264+
logger.info('Logging is as Finops person')
265+
credentials = boto3.client('sts').assume_role(
266+
RoleArn=f"arn:aws:iam::{account_id}:role/TestFinopsRole" ,
267+
RoleSessionName="FinopsPerson"
268+
)['Credentials']
269+
finops_session = boto3.session.Session(
270+
aws_access_key_id=credentials['AccessKeyId'],
271+
aws_secret_access_key=credentials['SecretAccessKey'],
272+
aws_session_token=credentials['SessionToken']
273+
)
274+
logger.info('Finops Session created')
275+
276+
finops_cfn = finops_session.client('cloudformation')
277+
278+
logger.info("Deleting bucket")
279+
delete_bucket(f'cid-{account_id}-shared') # Cannot be done by CFN
280+
logger.info("Deleting Dasbhoards stack")
281+
try:
282+
finops_cfn.delete_stack(StackName="Cloud-Intelligence-Dashboards")
283+
except Exception as exc: # pylint: disable=broad-exception-caught
284+
logger.info(exc)
285+
logger.info("Deleting CUR stack")
286+
try:
287+
finops_cfn.delete_stack(StackName="CID-CUR-Destination")
288+
except Exception as exc: # pylint: disable=broad-exception-caught
289+
logger.info(exc)
290+
logger.info("Deleting Admin stack")
291+
watch_stacks(admin_cfn, ["Cloud-Intelligence-Dashboards", "CID-CUR-Destination"])
292+
293+
try:
294+
admin_cfn.delete_stack(StackName="cid-admin")
295+
except Exception as exc: # pylint: disable=broad-exception-caught
296+
logger.info(exc)
297+
logger.info("Waiting for all deletions to complete")
298+
watch_stacks(admin_cfn, ["cid-admin"])
299+
300+
logger.info("Delete role")
301+
try:
302+
for policy in admin_iam.list_role_policies(RoleName='TestFinopsRole')['PolicyNames']:
303+
admin_iam.delete_role_policy(RoleName='TestFinopsRole', PolicyName=policy)
304+
admin_iam.delete_role(RoleName='TestFinopsRole')
305+
except Exception as exc: # pylint: disable=broad-exception-caught
306+
logger.info(exc)
307+
308+
logger.info("Cleanup tmp bucket")
309+
delete_bucket(f'{account_id}-cid-tests-deleteme')
310+
logger.info("Teardown done")
311+
312+
313+
@timeit
314+
def main():
315+
""" main """
316+
try:
317+
teardown() #Try to remove previous attempt
318+
create_finops_role()
319+
create_cid_as_finops()
320+
test_dashboard_exists()
321+
test_dataset_scheduled()
322+
finally:
323+
for index in range(10):
324+
print(f'Press Ctrl+C if you want to avoid teardown: {9-index}\a') # beeep
325+
time.sleep(1)
326+
teardown()
327+
328+
if __name__ == '__main__':
329+
logging.basicConfig(level=logging.INFO)
330+
main()

0 commit comments

Comments
 (0)