runbooks 0.9.8__py3-none-any.whl → 0.9.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
runbooks/__init__.py CHANGED
@@ -61,7 +61,7 @@ s3_ops = S3Operations()
61
61
 
62
62
  # Centralized Version Management - Single Source of Truth
63
63
  # All modules MUST import __version__ from this location
64
- __version__ = "0.9.7"
64
+ __version__ = "0.9.9"
65
65
 
66
66
  # Fallback for legacy importlib.metadata usage during transition
67
67
  try:
@@ -132,6 +132,7 @@ def print_banner() -> None:
132
132
 
133
133
  def create_table(
134
134
  title: Optional[str] = None,
135
+ caption: Optional[str] = None,
135
136
  columns: List[Dict[str, Any]] = None,
136
137
  show_header: bool = True,
137
138
  show_footer: bool = False,
@@ -143,6 +144,7 @@ def create_table(
143
144
 
144
145
  Args:
145
146
  title: Table title
147
+ caption: Table caption (displayed below the table)
146
148
  columns: List of column definitions [{"name": "Col1", "style": "cyan", "justify": "left"}]
147
149
  show_header: Show header row
148
150
  show_footer: Show footer row
@@ -154,6 +156,7 @@ def create_table(
154
156
  """
155
157
  table = Table(
156
158
  title=title,
159
+ caption=caption,
157
160
  show_header=show_header,
158
161
  show_footer=show_footer,
159
162
  box=box_style,
@@ -423,6 +423,232 @@ class MarkdownExporter:
423
423
  highest = max(profiles, key=lambda p: p.get("potential_savings", 0))
424
424
  return highest.get("profile_name", "Unknown")[:20]
425
425
 
426
+ def format_vpc_cleanup_table(self, vpc_candidates: List[Any]) -> str:
427
+ """
428
+ Format VPC cleanup candidates into 15-column markdown table.
429
+
430
+ Args:
431
+ vpc_candidates: List of VPCCandidate objects from vpc.unified_scenarios
432
+
433
+ Returns:
434
+ Markdown formatted table string with VPC cleanup analysis
435
+ """
436
+ if not vpc_candidates:
437
+ return "⚠️ No VPC candidates to format"
438
+
439
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
440
+
441
+ # Build table header and separator
442
+ headers = [
443
+ "Account_ID", "VPC_ID", "VPC_Name", "CIDR_Block", "Overlapping",
444
+ "Is_Default", "ENI_Count", "Tags", "Flow_Logs", "TGW/Peering",
445
+ "LBs_Present", "IaC", "Timeline", "Decision", "Owners/Approvals", "Notes"
446
+ ]
447
+
448
+ markdown_lines = [
449
+ "# VPC Cleanup Analysis Report",
450
+ "",
451
+ f"**Generated**: {timestamp}",
452
+ f"**Total VPC Candidates**: {len(vpc_candidates)}",
453
+ f"**Analysis Source**: CloudOps Runbooks VPC Module v0.9.9",
454
+ "",
455
+ "## VPC Cleanup Decision Table",
456
+ "",
457
+ "| " + " | ".join(headers) + " |",
458
+ "| " + " | ".join(["---" for _ in headers]) + " |"
459
+ ]
460
+
461
+ # Process each VPC candidate
462
+ for candidate in vpc_candidates:
463
+ # Extract data with safe attribute access and formatting
464
+ account_id = getattr(candidate, 'account_id', 'Unknown')
465
+ vpc_id = getattr(candidate, 'vpc_id', 'Unknown')
466
+ vpc_name = getattr(candidate, 'vpc_name', '') or 'Unnamed'
467
+ cidr_block = getattr(candidate, 'cidr_block', 'Unknown')
468
+
469
+ # Handle overlapping logic - may need to calculate from CIDR analysis
470
+ overlapping = "Yes" if getattr(candidate, 'overlapping', False) else "No"
471
+
472
+ # Format boolean indicators with emoji
473
+ is_default = "⚠️ Yes" if getattr(candidate, 'is_default', False) else "✅ No"
474
+ flow_logs = "✅ Yes" if getattr(candidate, 'flow_logs_enabled', False) else "❌ No"
475
+ tgw_peering = "✅ Yes" if getattr(candidate, 'tgw_peering_attached', False) else "❌ No"
476
+ load_balancers = "✅ Yes" if getattr(candidate, 'load_balancers_present', False) else "❌ No"
477
+ iac_managed = "✅ Yes" if getattr(candidate, 'iac_managed', False) else "❌ No"
478
+
479
+ # ENI Count handling
480
+ eni_count = getattr(candidate, 'eni_count', 0)
481
+
482
+ # Tags formatting - prioritize important tags with enhanced display
483
+ tags = getattr(candidate, 'tags', {}) or {}
484
+ relevant_tags = []
485
+ if tags:
486
+ # Priority order for business-relevant tags
487
+ priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team', 'CostCenter', 'Application']
488
+ for key in priority_keys:
489
+ if key in tags and tags[key] and len(relevant_tags) < 3: # Increased limit for better visibility
490
+ relevant_tags.append(f"{key}:{tags[key]}")
491
+
492
+ # Add other important tags if space available
493
+ for key, value in tags.items():
494
+ if key not in priority_keys and value and len(relevant_tags) < 3:
495
+ relevant_tags.append(f"{key}:{value}")
496
+
497
+ # Enhanced display logic for tags
498
+ if relevant_tags:
499
+ tags_display = "; ".join(relevant_tags)
500
+ if len(tags_display) > 35: # Slightly increased width for better readability
501
+ tags_display = tags_display[:32] + "..."
502
+ elif tags:
503
+ # If tags exist but none were priority, show count
504
+ tags_display = f"({len(tags)} tags)"
505
+ else:
506
+ # No tags at all
507
+ tags_display = "No tags"
508
+
509
+ # Timeline and Decision
510
+ timeline = getattr(candidate, 'cleanup_timeline', '') or getattr(candidate, 'implementation_timeline', 'Unknown')
511
+
512
+ # Decision handling - check for different decision attribute names
513
+ decision_attr = getattr(candidate, 'decision', None)
514
+ if decision_attr:
515
+ if hasattr(decision_attr, 'value'):
516
+ decision = decision_attr.value
517
+ else:
518
+ decision = str(decision_attr)
519
+ else:
520
+ # Fallback decision logic based on risk/dependencies
521
+ decision = getattr(candidate, 'cleanup_bucket', 'Unknown')
522
+
523
+ # Owners/Approvals - Enhanced extraction from tags if not populated
524
+ owners = getattr(candidate, 'owners_approvals', []) or getattr(candidate, 'stakeholders', [])
525
+
526
+ # If no owners found via attributes, try to extract from tags directly
527
+ if not owners and tags:
528
+ owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
529
+ for key in owner_keys:
530
+ if key in tags and tags[key]:
531
+ if 'business' in key.lower() or 'manager' in tags[key].lower():
532
+ owners.append(f"{tags[key]} (Business)")
533
+ elif 'technical' in key.lower() or any(tech in tags[key].lower() for tech in ['ops', 'devops', 'engineering']):
534
+ owners.append(f"{tags[key]} (Technical)")
535
+ else:
536
+ owners.append(tags[key])
537
+ break # Take first found owner to avoid clutter
538
+
539
+ if owners:
540
+ owners_display = "; ".join(owners)
541
+ if len(owners_display) > 30: # Increased width for better display
542
+ owners_display = owners_display[:27] + "..."
543
+ else:
544
+ # Enhanced "unknown" display based on VPC characteristics
545
+ if getattr(candidate, 'is_default', False):
546
+ owners_display = "System Default"
547
+ elif getattr(candidate, 'iac_detected', False):
548
+ owners_display = "IaC Managed"
549
+ else:
550
+ owners_display = "No owner tags"
551
+
552
+ # Notes - combination of risk assessment and business impact
553
+ notes_parts = []
554
+ risk_level = getattr(candidate, 'risk_level', None)
555
+ if risk_level:
556
+ risk_val = risk_level.value if hasattr(risk_level, 'value') else str(risk_level)
557
+ notes_parts.append(f"Risk:{risk_val}")
558
+
559
+ business_impact = getattr(candidate, 'business_impact', '')
560
+ if business_impact:
561
+ notes_parts.append(business_impact[:15]) # Truncate
562
+
563
+ notes = "; ".join(notes_parts) if notes_parts else getattr(candidate, 'notes', 'No notes')
564
+ if len(notes) > 30: # Truncate for table formatting
565
+ notes = notes[:27] + "..."
566
+
567
+ # Create table row - escape pipes for markdown compatibility
568
+ row_data = [
569
+ account_id, vpc_id, vpc_name, cidr_block, overlapping,
570
+ is_default, str(eni_count), tags_display, flow_logs, tgw_peering,
571
+ load_balancers, iac_managed, timeline, decision, owners_display, notes
572
+ ]
573
+
574
+ # Escape pipes and format row
575
+ escaped_data = [str(cell).replace("|", "\\|") for cell in row_data]
576
+ markdown_lines.append("| " + " | ".join(escaped_data) + " |")
577
+
578
+ # Add summary statistics
579
+ total_vpcs = len(vpc_candidates)
580
+ default_vpcs = sum(1 for c in vpc_candidates if getattr(c, 'is_default', False))
581
+ flow_logs_enabled = sum(1 for c in vpc_candidates if getattr(c, 'flow_logs_enabled', False))
582
+ iac_managed_count = sum(1 for c in vpc_candidates if getattr(c, 'iac_managed', False))
583
+ zero_eni_vpcs = sum(1 for c in vpc_candidates if getattr(c, 'eni_count', 1) == 0)
584
+
585
+ markdown_lines.extend([
586
+ "",
587
+ "## Analysis Summary",
588
+ "",
589
+ f"- **Total VPCs Analyzed**: {total_vpcs}",
590
+ f"- **Default VPCs**: {default_vpcs} ({(default_vpcs/total_vpcs*100):.1f}%)",
591
+ f"- **Flow Logs Enabled**: {flow_logs_enabled} ({(flow_logs_enabled/total_vpcs*100):.1f}%)",
592
+ f"- **IaC Managed**: {iac_managed_count} ({(iac_managed_count/total_vpcs*100):.1f}%)",
593
+ f"- **Zero ENI Attachments**: {zero_eni_vpcs} ({(zero_eni_vpcs/total_vpcs*100):.1f}%)",
594
+ "",
595
+ "## Cleanup Recommendations",
596
+ "",
597
+ "1. **Priority 1**: VPCs with zero ENI attachments and no dependencies",
598
+ "2. **Priority 2**: Default VPCs with no active resources",
599
+ "3. **Priority 3**: Non-IaC managed VPCs requiring manual cleanup",
600
+ "4. **Review Required**: VPCs with unclear ownership or business impact",
601
+ "",
602
+ "---",
603
+ f"*Generated by CloudOps Runbooks VPC Module v0.9.9 at {timestamp}*"
604
+ ])
605
+
606
+ return "\n".join(markdown_lines)
607
+
608
+ def export_vpc_analysis_to_file(self, vpc_candidates: List[Any], filename: str = None, output_dir: str = "./exports") -> str:
609
+ """
610
+ Export VPC analysis to markdown file with intelligent naming.
611
+
612
+ Args:
613
+ vpc_candidates: List of VPC candidates from analysis
614
+ filename: Base filename (optional, auto-generated if not provided)
615
+ output_dir: Output directory path
616
+
617
+ Returns:
618
+ Path to exported file
619
+ """
620
+ if not filename:
621
+ timestamp = datetime.now().strftime("%Y-%m-%d")
622
+ filename = f"vpc-cleanup-analysis-{timestamp}.md"
623
+
624
+ # Ensure .md extension
625
+ if not filename.endswith('.md'):
626
+ filename = f"{filename}.md"
627
+
628
+ # Create output directory
629
+ output_path = Path(output_dir)
630
+ output_path.mkdir(parents=True, exist_ok=True)
631
+
632
+ # Generate markdown content
633
+ markdown_content = self.format_vpc_cleanup_table(vpc_candidates)
634
+
635
+ # Write to file
636
+ filepath = output_path / filename
637
+
638
+ print_info(f"📝 Exporting VPC analysis to: {filename}")
639
+
640
+ try:
641
+ with open(filepath, "w", encoding="utf-8") as f:
642
+ f.write(markdown_content)
643
+
644
+ print_success(f"✅ VPC analysis exported: {filepath}")
645
+ print_info(f"🔗 Ready for executive review or documentation systems")
646
+ return str(filepath)
647
+
648
+ except Exception as e:
649
+ print_warning(f"❌ Failed to export VPC analysis: {e}")
650
+ return ""
651
+
426
652
 
427
653
  def export_finops_to_markdown(
428
654
  profile_data: Union[Dict[str, Any], List[Dict[str, Any]]],
@@ -11,6 +11,8 @@ from typing import Any, Dict, List, Optional
11
11
 
12
12
  import boto3
13
13
 
14
+ from ..common.rich_utils import console
15
+
14
16
 
15
17
  @dataclass
16
18
  class CostSavingsOpportunity:
@@ -638,7 +638,7 @@ class SingleAccountDashboard:
638
638
  style="dim",
639
639
  )
640
640
 
641
- console.print(table)
641
+ rich_console.print(table)
642
642
 
643
643
  # Summary panel (using filtered services for consistent analysis)
644
644
  total_current = sum(filtered_current_services.values())
@@ -681,7 +681,7 @@ class SingleAccountDashboard:
681
681
  • Services Analyzed: {len(all_services)}{period_info}
682
682
  """
683
683
 
684
- console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
684
+ rich_console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
685
685
 
686
686
  def _export_service_analysis(
687
687
  self, args: argparse.Namespace, cost_data: Dict[str, Any], service_costs: List[str], account_id: str
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ VPC Cleanup Exporter Module - Enterprise VPC Cleanup Result Export
4
+
5
+ This module provides export functionality for VPC cleanup analysis results,
6
+ leveraging the existing markdown_exporter infrastructure with VPC-specific formatting.
7
+
8
+ Author: CloudOps Runbooks Team
9
+ Version: 0.9.9
10
+ """
11
+
12
+ import csv
13
+ import json
14
+ import os
15
+ from datetime import datetime
16
+ from typing import Any, Dict, List
17
+
18
+ from .markdown_exporter import MarkdownExporter
19
+
20
+
21
+ def _format_tags_for_display(tags_dict: Dict[str, str]) -> str:
22
+ """Format tags for display with priority order."""
23
+ if not tags_dict:
24
+ return "No tags"
25
+
26
+ priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team', 'CostCenter']
27
+ relevant_tags = []
28
+
29
+ for key in priority_keys:
30
+ if key in tags_dict and tags_dict[key]:
31
+ relevant_tags.append(f"{key}:{tags_dict[key]}")
32
+
33
+ # Add other important tags
34
+ for key, value in tags_dict.items():
35
+ if key not in priority_keys and value and len(relevant_tags) < 5:
36
+ relevant_tags.append(f"{key}:{value}")
37
+
38
+ return "; ".join(relevant_tags) if relevant_tags else f"({len(tags_dict)} tags)"
39
+
40
+
41
+ def export_vpc_cleanup_results(vpc_result: Any, export_formats: List[str], output_dir: str = "./") -> Dict[str, str]:
42
+ """
43
+ Export VPC cleanup results in multiple formats.
44
+
45
+ Args:
46
+ vpc_result: VPC cleanup analysis result object
47
+ export_formats: List of formats to export (markdown, csv, json, pdf)
48
+ output_dir: Directory to save exported files
49
+
50
+ Returns:
51
+ Dict mapping format to exported filename
52
+ """
53
+ results = {}
54
+
55
+ # Extract VPC candidates from result - use correct attribute name
56
+ vpc_candidates = getattr(vpc_result, 'cleanup_candidates', [])
57
+ if not vpc_candidates:
58
+ # Fallback to other possible attribute names
59
+ vpc_candidates = getattr(vpc_result, 'vpc_candidates', [])
60
+
61
+ if 'markdown' in export_formats:
62
+ try:
63
+ exporter = MarkdownExporter()
64
+ markdown_filename = exporter.export_vpc_analysis_to_file(
65
+ vpc_candidates,
66
+ filename="vpc-cleanup-candidates.md",
67
+ output_dir=output_dir
68
+ )
69
+ results['markdown'] = markdown_filename
70
+ except Exception as e:
71
+ print(f"Warning: Markdown export failed: {e}")
72
+ results['markdown'] = None
73
+
74
+ # Real implementations for other formats
75
+ if 'csv' in export_formats:
76
+ try:
77
+ csv_filename = _export_vpc_candidates_csv(vpc_candidates, output_dir)
78
+ results['csv'] = csv_filename
79
+ except Exception as e:
80
+ print(f"Warning: CSV export failed: {e}")
81
+ results['csv'] = None
82
+
83
+ if 'json' in export_formats:
84
+ try:
85
+ json_filename = _export_vpc_candidates_json(vpc_candidates, output_dir)
86
+ results['json'] = json_filename
87
+ except Exception as e:
88
+ print(f"Warning: JSON export failed: {e}")
89
+ results['json'] = None
90
+
91
+ if 'pdf' in export_formats:
92
+ try:
93
+ pdf_filename = _export_vpc_candidates_pdf(vpc_candidates, output_dir)
94
+ results['pdf'] = pdf_filename
95
+ except Exception as e:
96
+ print(f"Warning: PDF export failed: {e}")
97
+ results['pdf'] = None
98
+
99
+ return results
100
+
101
+
102
+ def _export_vpc_candidates_csv(vpc_candidates: List[Any], output_dir: str) -> str:
103
+ """Export VPC candidates to CSV format with all 15 columns."""
104
+ filename = os.path.join(output_dir, "vpc-cleanup-candidates.csv")
105
+
106
+ # 15-column headers for comprehensive VPC analysis
107
+ headers = [
108
+ "Account_ID", "VPC_ID", "VPC_Name", "CIDR_Block", "Overlapping",
109
+ "Is_Default", "ENI_Count", "Tags", "Flow_Logs", "TGW/Peering",
110
+ "LBs_Present", "IaC", "Timeline", "Decision", "Owners/Approvals", "Notes"
111
+ ]
112
+
113
+ with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
114
+ writer = csv.writer(csvfile)
115
+ writer.writerow(headers)
116
+
117
+ for candidate in vpc_candidates:
118
+ # Extract data with enhanced tag and owner handling
119
+ tags_dict = getattr(candidate, 'tags', {}) or {}
120
+
121
+ # Enhanced tag display - prioritize important tags
122
+ if tags_dict:
123
+ priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team']
124
+ relevant_tags = []
125
+ for key in priority_keys:
126
+ if key in tags_dict and tags_dict[key]:
127
+ relevant_tags.append(f"{key}:{tags_dict[key]}")
128
+
129
+ # Add other important tags
130
+ for key, value in tags_dict.items():
131
+ if key not in priority_keys and value and len(relevant_tags) < 5:
132
+ relevant_tags.append(f"{key}:{value}")
133
+
134
+ tags_str = "; ".join(relevant_tags)
135
+ else:
136
+ tags_str = "No tags"
137
+
138
+ load_balancers = getattr(candidate, 'load_balancers', []) or []
139
+ lbs_present = "Yes" if load_balancers else "No"
140
+
141
+ # Enhanced owner extraction
142
+ owners = getattr(candidate, 'owners_approvals', []) or []
143
+
144
+ # If no owners found via attributes, extract from tags directly
145
+ if not owners and tags_dict:
146
+ owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
147
+ for key in owner_keys:
148
+ if key in tags_dict and tags_dict[key]:
149
+ if 'business' in key.lower() or 'manager' in tags_dict[key].lower():
150
+ owners.append(f"{tags_dict[key]} (Business)")
151
+ elif 'technical' in key.lower() or any(tech in tags_dict[key].lower() for tech in ['ops', 'devops', 'engineering']):
152
+ owners.append(f"{tags_dict[key]} (Technical)")
153
+ else:
154
+ owners.append(tags_dict[key])
155
+
156
+ if owners:
157
+ owners_str = "; ".join(owners)
158
+ else:
159
+ # Enhanced fallback for CSV
160
+ if getattr(candidate, 'is_default', False):
161
+ owners_str = "System Default VPC"
162
+ elif getattr(candidate, 'iac_detected', False):
163
+ owners_str = "IaC Managed"
164
+ else:
165
+ owners_str = "No owner tags found"
166
+
167
+ row = [
168
+ getattr(candidate, 'account_id', 'Unknown'),
169
+ getattr(candidate, 'vpc_id', ''),
170
+ getattr(candidate, 'vpc_name', 'Unnamed'),
171
+ getattr(candidate, 'cidr_block', ''),
172
+ "No", # Overlapping analysis would need CIDR comparison
173
+ "Yes" if getattr(candidate, 'is_default', False) else "No",
174
+ getattr(candidate, 'dependency_analysis', {}).eni_count if hasattr(candidate, 'dependency_analysis') else 0,
175
+ tags_str,
176
+ "Yes" if getattr(candidate, 'flow_logs_enabled', False) else "No",
177
+ "No", # TGW/Peering analysis placeholder
178
+ lbs_present,
179
+ "Yes" if getattr(candidate, 'iac_detected', False) else "No",
180
+ "Unknown", # Timeline analysis placeholder
181
+ getattr(candidate, 'cleanup_recommendation', 'unknown'),
182
+ owners_str,
183
+ "Generated by CloudOps Runbooks VPC Module"
184
+ ]
185
+ writer.writerow(row)
186
+
187
+ return filename
188
+
189
+
190
+ def _export_vpc_candidates_json(vpc_candidates: List[Any], output_dir: str) -> str:
191
+ """Export VPC candidates to JSON format with full data structure."""
192
+ filename = os.path.join(output_dir, "vpc-cleanup-candidates.json")
193
+
194
+ # Convert candidates to serializable format
195
+ candidates_data = []
196
+ for candidate in vpc_candidates:
197
+ candidate_dict = {
198
+ "account_id": getattr(candidate, 'account_id', 'Unknown'),
199
+ "vpc_id": getattr(candidate, 'vpc_id', ''),
200
+ "vpc_name": getattr(candidate, 'vpc_name', 'Unnamed'),
201
+ "cidr_block": getattr(candidate, 'cidr_block', ''),
202
+ "region": getattr(candidate, 'region', 'unknown'),
203
+ "is_default": getattr(candidate, 'is_default', False),
204
+ "state": getattr(candidate, 'state', 'unknown'),
205
+ "tags": getattr(candidate, 'tags', {}) or {},
206
+ "tags_summary": _format_tags_for_display(getattr(candidate, 'tags', {}) or {}),
207
+ "flow_logs_enabled": getattr(candidate, 'flow_logs_enabled', False),
208
+ "load_balancers": getattr(candidate, 'load_balancers', []) or [],
209
+ "iac_detected": getattr(candidate, 'iac_detected', False),
210
+ "owners_approvals": getattr(candidate, 'owners_approvals', []) or [],
211
+ "cleanup_bucket": getattr(candidate, 'cleanup_bucket', 'unknown'),
212
+ "cleanup_recommendation": getattr(candidate, 'cleanup_recommendation', 'unknown'),
213
+ "risk_assessment": getattr(candidate, 'risk_assessment', 'unknown'),
214
+ "business_impact": getattr(candidate, 'business_impact', 'unknown')
215
+ }
216
+
217
+ # Add dependency analysis if available
218
+ if hasattr(candidate, 'dependency_analysis') and candidate.dependency_analysis:
219
+ dep_analysis = candidate.dependency_analysis
220
+ candidate_dict["dependency_analysis"] = {
221
+ "eni_count": getattr(dep_analysis, 'eni_count', 0),
222
+ "route_tables": getattr(dep_analysis, 'route_tables', []),
223
+ "security_groups": getattr(dep_analysis, 'security_groups', []),
224
+ "internet_gateways": getattr(dep_analysis, 'internet_gateways', []),
225
+ "nat_gateways": getattr(dep_analysis, 'nat_gateways', []),
226
+ "vpc_endpoints": getattr(dep_analysis, 'vpc_endpoints', []),
227
+ "peering_connections": getattr(dep_analysis, 'peering_connections', []),
228
+ "dependency_risk_level": getattr(dep_analysis, 'dependency_risk_level', 'unknown')
229
+ }
230
+
231
+ candidates_data.append(candidate_dict)
232
+
233
+ # Create export metadata
234
+ export_data = {
235
+ "metadata": {
236
+ "export_timestamp": datetime.now().isoformat(),
237
+ "total_candidates": len(candidates_data),
238
+ "generator": "CloudOps Runbooks VPC Module v0.9.9"
239
+ },
240
+ "vpc_candidates": candidates_data
241
+ }
242
+
243
+ with open(filename, 'w', encoding='utf-8') as jsonfile:
244
+ json.dump(export_data, jsonfile, indent=2, ensure_ascii=False)
245
+
246
+ return filename
247
+
248
+
249
+ def _export_vpc_candidates_pdf(vpc_candidates: List[Any], output_dir: str) -> str:
250
+ """Export VPC candidates to PDF format for executive presentation."""
251
+ filename = os.path.join(output_dir, "vpc-cleanup-candidates.pdf")
252
+
253
+ try:
254
+ # Try to use reportlab for PDF generation
255
+ from reportlab.lib import colors
256
+ from reportlab.lib.pagesizes import letter, A4
257
+ from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
258
+ from reportlab.lib.styles import getSampleStyleSheet
259
+
260
+ doc = SimpleDocTemplate(filename, pagesize=A4)
261
+ styles = getSampleStyleSheet()
262
+ story = []
263
+
264
+ # Title
265
+ title = Paragraph("VPC Cleanup Analysis Report", styles['Title'])
266
+ story.append(title)
267
+ story.append(Spacer(1, 20))
268
+
269
+ # Summary
270
+ summary_text = f"""
271
+ <b>Generated:</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}<br/>
272
+ <b>Total VPC Candidates:</b> {len(vpc_candidates)}<br/>
273
+ <b>Analysis Source:</b> CloudOps Runbooks VPC Module v0.9.9
274
+ """
275
+ summary = Paragraph(summary_text, styles['Normal'])
276
+ story.append(summary)
277
+ story.append(Spacer(1, 20))
278
+
279
+ # Create table data
280
+ table_data = [
281
+ ["Account ID", "VPC ID", "VPC Name", "CIDR", "Default", "ENI Count", "Decision"]
282
+ ]
283
+
284
+ for candidate in vpc_candidates:
285
+ row = [
286
+ str(getattr(candidate, 'account_id', 'Unknown'))[:15], # Truncate for PDF width
287
+ str(getattr(candidate, 'vpc_id', ''))[:20],
288
+ str(getattr(candidate, 'vpc_name', 'Unnamed'))[:15],
289
+ str(getattr(candidate, 'cidr_block', ''))[:15],
290
+ "Yes" if getattr(candidate, 'is_default', False) else "No",
291
+ str(getattr(candidate, 'dependency_analysis', {}).eni_count if hasattr(candidate, 'dependency_analysis') else 0),
292
+ str(getattr(candidate, 'cleanup_recommendation', 'unknown'))[:10]
293
+ ]
294
+ table_data.append(row)
295
+
296
+ # Create table
297
+ table = Table(table_data)
298
+ table.setStyle(TableStyle([
299
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
300
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
301
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
302
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
303
+ ('FONTSIZE', (0, 0), (-1, 0), 10),
304
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
305
+ ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
306
+ ('FONTSIZE', (0, 1), (-1, -1), 8),
307
+ ('GRID', (0, 0), (-1, -1), 1, colors.black)
308
+ ]))
309
+
310
+ story.append(table)
311
+ doc.build(story)
312
+
313
+ except ImportError:
314
+ # Fallback: create a simple text-based PDF placeholder
315
+ with open(filename, 'w', encoding='utf-8') as f:
316
+ f.write("VPC Cleanup Analysis Report (PDF)\n")
317
+ f.write("=" * 40 + "\n\n")
318
+ f.write(f"Generated: {datetime.now().isoformat()}\n")
319
+ f.write(f"Total VPC Candidates: {len(vpc_candidates)}\n\n")
320
+
321
+ for i, candidate in enumerate(vpc_candidates, 1):
322
+ f.write(f"{i}. VPC {getattr(candidate, 'vpc_id', 'Unknown')}\n")
323
+ f.write(f" Account: {getattr(candidate, 'account_id', 'Unknown')}\n")
324
+ f.write(f" CIDR: {getattr(candidate, 'cidr_block', 'Unknown')}\n")
325
+ f.write(f" ENI Count: {getattr(candidate, 'dependency_analysis', {}).eni_count if hasattr(candidate, 'dependency_analysis') else 0}\n")
326
+ f.write(f" Decision: {getattr(candidate, 'cleanup_recommendation', 'unknown')}\n\n")
327
+
328
+ return filename