diff --git a/assets/css/reporting.css b/assets/css/reporting.css new file mode 100644 index 000000000..f18be789f --- /dev/null +++ b/assets/css/reporting.css @@ -0,0 +1,68 @@ +/* CSS for html report generated during dry run. */ +body{ + font-family: sans-serif; + } + h1,h3{ + margin-left: 15px; + } + table, th, td { + border-collapse: collapse; + margin-left: 0; + } + + #cloud_table { + border-collapse: collapse; + margin: 25px 0; + font: 0.9em sans-serif; + min-width: 800px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); + border-radius: 5px 5px 0 0; + overflow: hidden; + } + + #cloud_table thead tr { + background-color: #009879; + color: #ffffff; + text-align: center; + font-weight: bold; + } + + #cloud_table th, #cloud_table td { + padding: 12px 15px; + } + + #cloud_table th:not(:last-child), #cloud_table td:not(:last-child) { + border-right: 0.1px solid black; + } + + #cloud_table tbody tr { + border-bottom: 1px solid #dddddd; + color: #488b8b; + font-weight: bold; + } + + #cloud_table tbody tr:nth-of-type(odd) { + background-color: #f3f3f3; + } + + #cloud_table tbody tr:last-of-type { + border-bottom: 2px solid #009879; + } + + #cloud_table tbody td { + text-align: left; + } + + ul { + margin: 0; + padding: 0 0 0 20px; + list-style-type: circle; + } + + ul li { + margin-top: 10px; + margin-bottom: 10px; + } + #cloud_table td { + font-size: 0.8em; + } \ No newline at end of file diff --git a/cloudwash/providers/aws.py b/cloudwash/providers/aws.py index 77ac5b6ce..37a59516e 100644 --- a/cloudwash/providers/aws.py +++ b/cloudwash/providers/aws.py @@ -11,6 +11,7 @@ def cleanup(**kwargs): is_dry_run = kwargs["dry_run"] data = ['VMS', 'NICS', 'DISCS', 'PIPS', 'RESOURCES', 'STACKS'] regions = settings.aws.auth.regions + dry_data['PROVIDER'] = "AWS" if "all" in regions: with compute_client("aws", aws_region="us-west-2") as client: regions = client.list_regions() diff --git a/cloudwash/providers/azure.py b/cloudwash/providers/azure.py index 55b4824bc..f05ce75c5 100644 --- a/cloudwash/providers/azure.py +++ b/cloudwash/providers/azure.py @@ -42,7 +42,7 @@ def cleanup(**kwargs): data = ['VMS', 'NICS', 'DISCS', 'IMAGES', 'PIPS', 'RESOURCES'] regions = settings.azure.auth.regions groups = settings.azure.auth.resource_groups - + dry_data['PROVIDER'] = "AZURE" if "all" in regions: # non-existent RG can be chosen for query # as it's never accessed and is only stored within wrapper diff --git a/cloudwash/providers/gce.py b/cloudwash/providers/gce.py index 7e3f257f3..1db171a61 100644 --- a/cloudwash/providers/gce.py +++ b/cloudwash/providers/gce.py @@ -10,7 +10,7 @@ def cleanup(**kwargs): is_dry_run = kwargs["dry_run"] - + dry_data['PROVIDER'] = "GCE" with compute_client("gce") as gce_client: if kwargs["vms"] or kwargs["_all"]: allvms = gce_client.list_vms(zones=gce_zones()) diff --git a/cloudwash/providers/vmware.py b/cloudwash/providers/vmware.py index 7fedd3d62..7eea10299 100644 --- a/cloudwash/providers/vmware.py +++ b/cloudwash/providers/vmware.py @@ -9,7 +9,7 @@ def cleanup(**kwargs): is_dry_run = kwargs["dry_run"] - + dry_data['PROVIDER'] = "VMWARE" with compute_client("vmware") as client: if kwargs["vms"] or kwargs["_all"]: allvms = client.list_vms() diff --git a/cloudwash/utils.py b/cloudwash/utils.py index e450b9919..ca716fd24 100644 --- a/cloudwash/utils.py +++ b/cloudwash/utils.py @@ -6,6 +6,10 @@ from cloudwash.logger import logger +import dominate + +from dominate.tags import * + _vms_dict = {"VMS": {"delete": [], "stop": [], "skip": []}} dry_data = { "NICS": {"delete": []}, @@ -14,10 +18,10 @@ "RESOURCES": {"delete": []}, "STACKS": {"delete": []}, "IMAGES": {"delete": []}, + "PROVIDER": "" } dry_data.update(_vms_dict) - def echo_dry(dry_data=None) -> None: """Prints and Logs the per resource cleanup data on STDOUT and logfile @@ -25,47 +29,58 @@ def echo_dry(dry_data=None) -> None: it follows the format of module scoped `dry_data` variable in this module """ logger.info("\n=========== DRY SUMMARY ============\n") - deletable_vms = dry_data["VMS"]["delete"] - stopable_vms = dry_data["VMS"]["stop"] - skipped_vms = dry_data["VMS"]["skip"] - deletable_discs = dry_data["DISCS"]["delete"] - deletable_nics = dry_data["NICS"]["delete"] - deletable_images = dry_data["IMAGES"]["delete"] - deletable_pips = dry_data["PIPS"]["delete"] if "PIPS" in dry_data else None - deletable_resources = dry_data["RESOURCES"]["delete"] - deletable_stacks = dry_data["STACKS"]["delete"] if "STACKS" in dry_data else None - if deletable_vms or stopable_vms or skipped_vms: - logger.info( - f"VMs:\n\tDeletable: {deletable_vms}\n\tStoppable: {stopable_vms}\n\t" - f"Skip: {skipped_vms}" - ) - if deletable_discs: - logger.info(f"DISCs:\n\tDeletable: {deletable_discs}") - if deletable_nics: - logger.info(f"NICs:\n\tDeletable: {deletable_nics}") - if deletable_images: - logger.info(f"IMAGES:\n\tDeletable: {deletable_images}") - if deletable_pips: - logger.info(f"PIPs:\n\tDeletable: {deletable_pips}") - if deletable_resources: - logger.info(f"RESOURCEs:\n\tDeletable: {deletable_resources}") - if deletable_stacks: - logger.info(f"STACKs:\n\tDeletable: {deletable_stacks}") - if not any( - [ - deletable_vms, - stopable_vms, - deletable_discs, - deletable_nics, - deletable_pips, - deletable_resources, - deletable_stacks, - deletable_images, - ] - ): - logger.info("\nNo resources are eligible for cleanup!") - logger.info("\n====================================\n") + resource_data = { + "provider": dry_data.get('PROVIDER'), + "deletable_vms": dry_data["VMS"]["delete"], + "stopable_vms": dry_data["VMS"]["stop"], + "skipped_vms": dry_data["VMS"]["skip"], + "deletable_discs": dry_data["DISCS"]["delete"], + "deletable_nics": dry_data["NICS"]["delete"], + "deletable_images": dry_data["IMAGES"]["delete"], + "deletable_pips": dry_data["PIPS"]["delete"] if "PIPS" in dry_data else None, + "deletable_resources": dry_data["RESOURCES"]["delete"], + "deletable_stacks": dry_data["STACKS"]["delete"] if "STACKS" in dry_data else None + } + if any(value for key, value in resource_data.items() if key != 'provider'): + logger.info("Resources eligible for cleanup:") + for key, value in resource_data.items(): + if value and key!="provider": + logger.info(f"{key.replace('_', ' ').title()}:\n\t{key.split('_')[0].title()}: {value}") + + logger.info("\n====================================\n") + + create_html(**resource_data) + else: + logger.info("\nNo resources are eligible for cleanup!\n") + +def create_html(**kwargs): + doc = dominate.document(title="Cloud resources page") + + with doc.head: + with open('assets/css/reporting.css','r') as css: + style(css.read()) + with doc: + with div(cls='cloud_box'): + h1('CLOUDWASH REPORT') + h3(f"{kwargs.get('provider')} RESOURCES") + with table(id='cloud_table'): + with thead(): + with tr(): + for table_head in kwargs.keys(): + if kwargs[table_head] and table_head!="provider": + th(table_head.replace("_"," ").title()) + with tbody(): + for key,values in kwargs.items(): + if key!="provider" and values: + if isinstance(values,list): + with td(): + with ul(): + [li(resource_name) for resource_name in values] + else: + td(values) + with open('cleanup_resource.html','w') as file: + file.write(doc.render()) def total_running_time(vm_obj) -> namedtuple: """Calculates the VMs total running time diff --git a/setup.cfg b/setup.cfg index a47c204fd..7e01c8312 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,7 @@ install_requires = dynaconf click wget + dominate packages = find: [options.extras_require]