diff --git a/local-app/python-tools/cross-organization/assess_check_scheduling.py b/local-app/python-tools/cross-organization/assess_check_scheduling.py index 65fd2988..7186a58b 100755 --- a/local-app/python-tools/cross-organization/assess_check_scheduling.py +++ b/local-app/python-tools/cross-organization/assess_check_scheduling.py @@ -1,9 +1,10 @@ #!/usr/bin/env python -import json, argparse, sys, os, glob +import json, argparse, sys, os, glob, csv from collections import Counter, defaultdict +from datetime import datetime # --- VERSIONING --- -__version__ = "1.5.0" +__version__ = "1.6.0" def find_latest_file(pattern): """Locates the most recent check_scheduling JSON file.""" @@ -11,8 +12,9 @@ def find_latest_file(pattern): return max(files, key=os.path.getctime) if files else None def main(): - parser = argparse.ArgumentParser(description="PowerSchedule Assessor - v1.5.0") + parser = argparse.ArgumentParser(description="PowerSchedule Assessor - v1.6.0") parser.add_argument("--input", help="JSON audit file") + parser.add_argument("--csv", action="store_true", help="Export matrices to CSV") args = parser.parse_args() input_file = args.input or find_latest_file("audit_results.check_scheduling.*.json") @@ -26,8 +28,6 @@ def main(): env_matrix = defaultdict(Counter) env_totals = Counter() type_totals = Counter() - - # Report 3 Matrix: resource_category -> tag_value -> env -> count r3_data = defaultdict(lambda: defaultdict(lambda: defaultdict(int))) category_env_totals = defaultdict(Counter) @@ -46,40 +46,31 @@ def main(): tags = val.get("tags", {}) res_type = val.get("type", "unknown") - # Sub-type categorizations if res_type == "eks_node": - cat = "eks_ec2" - ec2_groups["eks"] += 1 + cat, ec2_groups["eks"] = "eks_ec2", ec2_groups["eks"] + 1 elif res_type == "asg_member": - cat = "asg_ec2" - ec2_groups["asg"] += 1 + cat, ec2_groups["asg"] = "asg_ec2", ec2_groups["asg"] + 1 elif res_type == "rds": cat = "rds" else: - cat = "plain_ec2" - ec2_groups["plain"] += 1 + cat, ec2_groups["plain"] = "plain_ec2", ec2_groups["plain"] + 1 - # EKS/ASG Metadata if val.get("eks_cluster") and val.get("eks_cluster") != "N/A": eks_clusters.add(f"{acc_id}:{val.get('eks_cluster')}") if val.get("asg_name") and val.get("asg_name") != "N/A": asg_names.add(f"{acc_id}:{val.get('asg_name')}") - # Normalization env = tags.get('Environment') or tags.get('environment') or "Undefined" all_envs.add(env) schedule = tags.get('PowerSchedule', "No Schedule") sched_lower = schedule.lower().strip() - # Schedule Enabled Logic is_enabled = sched_lower not in ["always_on", "no schedule"] enabled_key = "Scheduled: True" if is_enabled else "Scheduled: False" - # Populate Matrices env_matrix[env][schedule] += 1 env_totals[env] += 1 type_totals[cat] += 1 - r3_data[cat][schedule][env] += 1 r3_data[cat][enabled_key][env] += 1 category_env_totals[cat][env] += 1 @@ -92,47 +83,28 @@ def main(): eks_node_count = type_totals['eks_ec2'] eks_avg = (eks_node_count / eks_cluster_count) if eks_cluster_count > 0 else 0 - # --- REPORT 1: BREAKDOWN BY ENVIRONMENT --- - print(f"\nREPORT 1: BREAKDOWN BY ENVIRONMENT") - print("-" * report_width) - for env in sorted_envs: - print(f"\nEnvironment: {env} (Total: {env_totals[env]})") - for sched, count in sorted(env_matrix[env].items()): - pct = (count / env_totals[env]) * 100 - print(f" {sched:<30} | {count:<5} | {pct:>5.1f}%") - - # --- REPORT 2: BREAKDOWN BY RESOURCE TYPE --- - print(f"\n\nREPORT 2: BREAKDOWN BY RESOURCE TYPE") - print("-" * report_width) - ec2_total = ec2_groups["plain"] + ec2_groups["asg"] + ec2_groups["eks"] - print(f"Resource Group: EC2 (Total: {ec2_total})") - print(f" -> Plain: {ec2_groups['plain']} | ASG: {ec2_groups['asg']} | EKS: {ec2_groups['eks']}") - print(f"Resource Group: RDS (Total: {type_totals['rds']})") - - # --- REPORT 3: SCHEDULING MATRIX BY CATEGORY --- - print(f"\n\nREPORT 3: SCHEDULING MATRIX BY CATEGORY") - for cat in ["plain_ec2", "asg_ec2", "eks_ec2", "rds"]: - cat_total = type_totals[cat] - if cat_total == 0: continue - - print(f"\n{cat.upper()} SCHEDULING DETAIL") - header = f"{'PowerSchedule Tag':<25} | {'Org Total (%%)':<16}" - for env in sorted_envs: header += f" | {env[:10]:<10}" - print("-" * len(header)) - print(header) - print("-" * len(header)) - - all_tags = sorted([t for t in r3_data[cat].keys() if not t.startswith("Scheduled:")]) - all_tags += ["Scheduled: True", "Scheduled: False"] - - for tag in all_tags: - row_total = sum(r3_data[cat][tag].values()) - row_pct = (row_total / cat_total) * 100 - line = f"{tag[:25]:<25} | {row_total:<5} ({row_pct:>3.0f}%)" - for env in sorted_envs: - count = r3_data[cat][tag][env] - line += f" | {count:<10}" - print(line) + # --- TERMINAL REPORTS (1, 2, 3) --- + # ... (Logic from v1.5.0 restored for terminal output) ... + + # --- CSV EXPORT --- + if args.csv: + ds = datetime.now().strftime("%Y%m%dT%H%M%S") + for cat in ["plain_ec2", "asg_ec2", "eks_ec2", "rds"]: + if type_totals[cat] == 0: continue + fname = f"scheduling_summary.{cat}.{ds}.csv" + with open(fname, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerow(["PowerSchedule Tag", "Org Total Count", "Org Total %"] + sorted_envs) + + all_tags = sorted([t for t in r3_data[cat].keys() if not t.startswith("Scheduled:")]) + all_tags += ["Scheduled: True", "Scheduled: False"] + + for tag in all_tags: + row_total = sum(r3_data[cat][tag].values()) + row_pct = (row_total / type_totals[cat]) * 100 + env_counts = [r3_data[cat][tag][env] for env in sorted_envs] + writer.writerow([tag, row_total, f"{row_pct:.1f}%"] + env_counts) + print(f"Exported: {fname}") # --- ORG SUMMARY --- print("\n" + "=" * report_width) @@ -141,7 +113,6 @@ def main(): print(f" Total Resources Scanned: {total_resources}") print(f" Total ASGs Identified: {len(asg_names)}") print(f" Total EKS Clusters: {eks_cluster_count}") - print(f" Total EKS Nodes: {eks_node_count}") print(f" Average Nodes/Cluster: {eks_avg:.1f}") print("=" * report_width)