diff --git a/local-app/python-tools/cross-organization/purge_check_s3_buckets.py.new b/local-app/python-tools/cross-organization/purge_check_s3_buckets.py.new index eadf2a8b..9ec30dbd 100755 --- a/local-app/python-tools/cross-organization/purge_check_s3_buckets.py.new +++ b/local-app/python-tools/cross-organization/purge_check_s3_buckets.py.new @@ -11,6 +11,10 @@ import getpass import smtplib from datetime import datetime, timezone from email.message import EmailMessage +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email import encoders from collections import defaultdict from concurrent.futures import ThreadPoolExecutor @@ -21,27 +25,42 @@ from rich.table import Table from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn, SpinnerColumn # --- VERSIONING --- -__version__ = "2.7.0" +__version__ = "2.8.0" console = Console() -def dispatch_progress_email(recipient_email, subject_str, body_str): - """Dispatches a plain-text status tracking email via localhost SMTP.""" +def dispatch_progress_email(recipient_email, subject_str, body_str, attachment_paths=None): + """Dispatches a plain-text or multipart status tracking email via localhost SMTP.""" try: sender_user = getpass.getuser() sender_host = "localhost" sender_address = f"{sender_user}@{sender_host}" - msg = EmailMessage() - msg.set_content(body_str) - msg["Subject"] = subject_str - msg["From"] = sender_address - msg["To"] = recipient_email + if attachment_paths: + msg = MIMEMultipart() + msg["Subject"] = subject_str + msg["From"] = sender_address + msg["To"] = recipient_email + msg.attach(MIMEText(body_str, "plain")) + + for path in attachment_paths: + if os.path.exists(path): + part = MIMEBase("application", "octet-stream") + with open(path, "rb") as f: + part.set_payload(f.read()) + encoders.encode_base64(part) + part.add_header("Content-Disposition", f'attachment; filename="{os.path.basename(path)}"') + msg.attach(part) + else: + msg = EmailMessage() + msg.set_content(body_str) + msg["Subject"] = subject_str + msg["From"] = sender_address + msg["To"] = recipient_email with smtplib.SMTP("localhost", 25) as server: server.send_message(msg) except Exception as email_err: - # Fail gracefully to keep terminal execution threads uninterrupted console.print(f"[bold yellow]Warning: Notification pipeline failed to dispatch email: {email_err}[/bold yellow]") def build_assumed_role_session(target_account_id, role_name, region_name, profile=None): @@ -288,7 +307,6 @@ def main(): active_lane_allocations = {i: None for i in range(args.workers)} futures_map = {} - # Milestone notification variables setup completed_counter = 0 next_email_milestone_pct = 10 @@ -330,7 +348,6 @@ def main(): completed_futures = [f for f in futures_map if f.done()] for fut in completed_futures: meta = futures_map.pop(fut) - assigned_lane = next(k for k, v in active_lane_allocations.items() if v == fut) active_lane_allocations[assigned_lane] = None @@ -351,7 +368,6 @@ def main(): overall_progress.update(macro_task_id, advance=1) lane_progress.update(lane_task_ids[assigned_lane], bucket_name="IDLE", completed=0, current_test="IDLE", total_tests="0") - # --- REFRESH EMAIL NOTIFICATION PIPELINE SNAPSHOT CHECKS --- if args.notification and completed_counter < total_targets_count: current_macro_pct = int((completed_counter / total_targets_count) * 100) if current_macro_pct >= next_email_milestone_pct: @@ -376,20 +392,6 @@ def main(): global_end_timestamp = datetime.now(timezone.utc).strftime(time_format)[:-3] + "Z" global_total_elapsed = round(global_end_epoch - global_start_epoch, 3) - # --- DISPATCH COMPLETION EMAIL SNAPSHOT REPORT --- - if args.notification: - sub_msg = f"purge progress 100% bucket {total_targets_count} of {total_targets_count}" - body_msg = ( - f"AWS ORGANIZATIONAL S3 METRICS STORAGE PURGE LIFECYCLE PROGRESS REPORT\n" - f"======================================================================\n" - f"Current Status : CONCLUSION (All Operations Executed Safely)\n" - f"Progress Percentage : 100%\n" - f"Total Buckets Managed : {total_targets_count} of {total_targets_count}\n" - f"Total Elapsed Duration: {global_total_elapsed:.2f} seconds\n" - f"Dry Run Active Status : {dry_run}\n" - ) - dispatch_progress_email(args.notification, sub_msg, body_msg) - if args.output != "." and not os.path.exists(args.output): os.makedirs(args.output, exist_ok=True) @@ -431,6 +433,41 @@ def main(): with open(json_out_path, "w", encoding="utf-8") as jf: json.dump(json_telemetry_payload, jf, indent=2) + # --- GENERATE SUMMARY TEXT TABLE FOR EMAIL BODY --- + table_header = f"| {'Account ID':<12} | {'Account Name':<20} | {'Bucket Name':<35} | {'Region':<12} | {detected_size_unit_label:<12} | {'Objects':<10} | {'Elapsed (s)':<12} | {'Errors':<6} |\n" + table_divider = f"|{'-'*14}|{'-'*22}|{'-'*37}|{'-'*14}|{'-'*14}|{'-'*12}|{'-'*14}|{'-'*8}|\n" + table_body = "" + + for r in operation_tracking_results: + acct_id_trunc = r["account_id"] + acct_name_trunc = r["account_name"][:18] + ".." if len(r["account_name"]) > 20 else r["account_name"] + bucket_trunc = r["bucket_name"][:32] + ".." if len(r["bucket_name"]) > 35 else r["bucket_name"] + region_str = r["region"] + size_str = f"{r['source_size']:.4f}" + objects_str = f"{r['object_count']:,}" + elapsed_str = f"{r['elapsed_time_seconds']:.3f}" + errors_str = str(r["error_count"]) + + table_body += f"| {acct_id_trunc:<12} | {acct_name_trunc:<20} | {bucket_trunc:<35} | {region_str:<12} | {size_str:<12} | {objects_str:<10} | {elapsed_str:<12} | {errors_str:<6} |\n" + + ascii_summary_table = table_header + table_divider + table_body + + # --- DISPATCH COMPLETION EMAIL SNAPSHOT REPORT WITH LOG ATTACHMENTS --- + if args.notification: + sub_msg = f"purge progress 100% bucket {total_targets_count} of {total_targets_count}" + body_msg = ( + f"AWS ORGANIZATIONAL S3 METRICS STORAGE PURGE LIFECYCLE PROGRESS REPORT\n" + f"======================================================================\n" + f"Current Status : CONCLUSION (All Operations Executed Safely)\n" + f"Progress Percentage : 100%\n" + f"Total Buckets Managed : {total_targets_count} of {total_targets_count}\n" + f"Total Elapsed Duration: {global_total_elapsed:.2f} seconds\n" + f"Dry Run Active Status : {dry_run}\n\n" + f"EXECUTION SUMMARY TABLE:\n" + f"{ascii_summary_table}" + ) + dispatch_progress_email(args.notification, sub_msg, body_msg, attachment_paths=[csv_out_path, json_out_path]) + console.print(f"\n[bold blue]======================================================================[/bold blue]") console.print(f"[*] PURGE SEQUENCE LIFECYCLE CONCLUDED | Total Run Time: {global_total_elapsed:.2f}s") console.print(f"[bold blue]======================================================================[/bold blue]")