|
| 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