runbooks 1.1.4__py3-none-any.whl → 1.1.5__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.
Files changed (228) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +138 -35
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +11 -0
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +63 -74
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +201 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/cloud_foundations_integration.py +144 -149
  113. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  114. runbooks/inventory/collectors/aws_networking.py +109 -99
  115. runbooks/inventory/collectors/base.py +4 -0
  116. runbooks/inventory/core/collector.py +495 -313
  117. runbooks/inventory/drift_detection_cli.py +69 -96
  118. runbooks/inventory/inventory_mcp_cli.py +48 -46
  119. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  120. runbooks/inventory/mcp_inventory_validator.py +549 -465
  121. runbooks/inventory/mcp_vpc_validator.py +359 -442
  122. runbooks/inventory/organizations_discovery.py +55 -51
  123. runbooks/inventory/rich_inventory_display.py +33 -32
  124. runbooks/inventory/unified_validation_engine.py +278 -251
  125. runbooks/inventory/vpc_analyzer.py +732 -695
  126. runbooks/inventory/vpc_architecture_validator.py +293 -348
  127. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  128. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  129. runbooks/main.py +49 -34
  130. runbooks/main_final.py +91 -60
  131. runbooks/main_minimal.py +22 -10
  132. runbooks/main_optimized.py +131 -100
  133. runbooks/main_ultra_minimal.py +7 -2
  134. runbooks/mcp/__init__.py +36 -0
  135. runbooks/mcp/integration.py +679 -0
  136. runbooks/monitoring/performance_monitor.py +9 -4
  137. runbooks/operate/dynamodb_operations.py +3 -1
  138. runbooks/operate/ec2_operations.py +145 -137
  139. runbooks/operate/iam_operations.py +146 -152
  140. runbooks/operate/networking_cost_heatmap.py +29 -8
  141. runbooks/operate/rds_operations.py +223 -254
  142. runbooks/operate/s3_operations.py +107 -118
  143. runbooks/operate/vpc_operations.py +646 -616
  144. runbooks/remediation/base.py +1 -1
  145. runbooks/remediation/commons.py +10 -7
  146. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  147. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  148. runbooks/remediation/multi_account.py +24 -21
  149. runbooks/remediation/rds_snapshot_list.py +86 -60
  150. runbooks/remediation/remediation_cli.py +92 -146
  151. runbooks/remediation/universal_account_discovery.py +83 -79
  152. runbooks/remediation/workspaces_list.py +46 -41
  153. runbooks/security/__init__.py +19 -0
  154. runbooks/security/assessment_runner.py +1150 -0
  155. runbooks/security/baseline_checker.py +812 -0
  156. runbooks/security/cloudops_automation_security_validator.py +509 -535
  157. runbooks/security/compliance_automation_engine.py +17 -17
  158. runbooks/security/config/__init__.py +2 -2
  159. runbooks/security/config/compliance_config.py +50 -50
  160. runbooks/security/config_template_generator.py +63 -76
  161. runbooks/security/enterprise_security_framework.py +1 -1
  162. runbooks/security/executive_security_dashboard.py +519 -508
  163. runbooks/security/multi_account_security_controls.py +959 -1210
  164. runbooks/security/real_time_security_monitor.py +422 -444
  165. runbooks/security/security_baseline_tester.py +1 -1
  166. runbooks/security/security_cli.py +143 -112
  167. runbooks/security/test_2way_validation.py +439 -0
  168. runbooks/security/two_way_validation_framework.py +852 -0
  169. runbooks/sre/production_monitoring_framework.py +167 -177
  170. runbooks/tdd/__init__.py +15 -0
  171. runbooks/tdd/cli.py +1071 -0
  172. runbooks/utils/__init__.py +14 -17
  173. runbooks/utils/logger.py +7 -2
  174. runbooks/utils/version_validator.py +50 -47
  175. runbooks/validation/__init__.py +6 -6
  176. runbooks/validation/cli.py +9 -3
  177. runbooks/validation/comprehensive_2way_validator.py +745 -704
  178. runbooks/validation/mcp_validator.py +906 -228
  179. runbooks/validation/terraform_citations_validator.py +104 -115
  180. runbooks/validation/terraform_drift_detector.py +447 -451
  181. runbooks/vpc/README.md +617 -0
  182. runbooks/vpc/__init__.py +8 -1
  183. runbooks/vpc/analyzer.py +577 -0
  184. runbooks/vpc/cleanup_wrapper.py +476 -413
  185. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  186. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  187. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  188. runbooks/vpc/config.py +92 -97
  189. runbooks/vpc/cost_engine.py +411 -148
  190. runbooks/vpc/cost_explorer_integration.py +553 -0
  191. runbooks/vpc/cross_account_session.py +101 -106
  192. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  193. runbooks/vpc/eni_gate_validator.py +961 -0
  194. runbooks/vpc/heatmap_engine.py +185 -160
  195. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  196. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  197. runbooks/vpc/networking_wrapper.py +15 -8
  198. runbooks/vpc/pdca_remediation_planner.py +528 -0
  199. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  200. runbooks/vpc/runbooks_adapter.py +1167 -241
  201. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  202. runbooks/vpc/test_data_loader.py +358 -0
  203. runbooks/vpc/tests/conftest.py +314 -4
  204. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  205. runbooks/vpc/tests/test_cost_engine.py +0 -2
  206. runbooks/vpc/topology_generator.py +326 -0
  207. runbooks/vpc/unified_scenarios.py +1297 -1124
  208. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  209. runbooks-1.1.5.dist-info/METADATA +328 -0
  210. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/RECORD +214 -193
  211. runbooks/finops/README.md +0 -414
  212. runbooks/finops/accuracy_cross_validator.py +0 -647
  213. runbooks/finops/business_cases.py +0 -950
  214. runbooks/finops/dashboard_router.py +0 -922
  215. runbooks/finops/ebs_optimizer.py +0 -973
  216. runbooks/finops/embedded_mcp_validator.py +0 -1629
  217. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  218. runbooks/finops/finops_dashboard.py +0 -584
  219. runbooks/finops/finops_scenarios.py +0 -1218
  220. runbooks/finops/legacy_migration.py +0 -730
  221. runbooks/finops/multi_dashboard.py +0 -1519
  222. runbooks/finops/single_dashboard.py +0 -1113
  223. runbooks/finops/unlimited_scenarios.py +0 -393
  224. runbooks-1.1.4.dist-info/METADATA +0 -800
  225. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  226. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  227. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  228. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -16,14 +16,7 @@ from rich.panel import Panel
16
16
  from rich.prompt import Confirm, Prompt
17
17
 
18
18
  from runbooks.common.profile_utils import get_profile_for_operation
19
- from runbooks.common.rich_utils import (
20
- console,
21
- print_header,
22
- print_success,
23
- print_error,
24
- print_warning,
25
- create_table
26
- )
19
+ from runbooks.common.rich_utils import console, print_header, print_success, print_error, print_warning, create_table
27
20
  from runbooks.common.mcp_integration import EnterpriseMCPIntegrator
28
21
  from .vpc_cleanup_integration import VPCCleanupFramework, VPCCleanupPhase, VPCCleanupRisk
29
22
  from .manager_interface import VPCManagerInterface
@@ -34,7 +27,7 @@ logger = logging.getLogger(__name__)
34
27
  class VPCCleanupCLI:
35
28
  """
36
29
  Enterprise VPC Cleanup CLI wrapper with safety controls and approval gates
37
-
30
+
38
31
  Provides comprehensive VPC cleanup capabilities integrated with the existing
39
32
  runbooks framework architecture and enterprise multi-account patterns.
40
33
  """
@@ -44,11 +37,11 @@ class VPCCleanupCLI:
44
37
  profile: Optional[str] = None,
45
38
  region: str = "us-east-1",
46
39
  safety_mode: bool = True,
47
- console: Optional[Console] = None
40
+ console: Optional[Console] = None,
48
41
  ):
49
42
  """
50
43
  Initialize VPC Cleanup CLI
51
-
44
+
52
45
  Args:
53
46
  profile: AWS profile for operations
54
47
  region: AWS region
@@ -59,45 +52,39 @@ class VPCCleanupCLI:
59
52
  self.region = region
60
53
  self.safety_mode = safety_mode
61
54
  self.console = console or Console()
62
-
55
+
63
56
  # Initialize cleanup framework
64
57
  self.cleanup_framework = VPCCleanupFramework(
65
- profile=profile,
66
- region=region,
67
- console=self.console,
68
- safety_mode=safety_mode
58
+ profile=profile, region=region, console=self.console, safety_mode=safety_mode
69
59
  )
70
-
60
+
71
61
  # Initialize manager interface for business reporting
72
62
  self.manager_interface = VPCManagerInterface(console=self.console)
73
-
63
+
74
64
  # Initialize MCP integrator for cross-validation
75
- self.mcp_integrator = EnterpriseMCPIntegrator(
76
- user_profile=profile,
77
- console_instance=self.console
78
- )
65
+ self.mcp_integrator = EnterpriseMCPIntegrator(user_profile=profile, console_instance=self.console)
79
66
 
80
67
  def analyze_vpc_cleanup_candidates(
81
68
  self,
82
69
  vpc_ids: Optional[List[str]] = None,
83
70
  account_profiles: Optional[List[str]] = None,
84
71
  export_results: bool = True,
85
- output_directory: str = "./exports/vpc_cleanup"
72
+ output_directory: str = "./exports/vpc_cleanup",
86
73
  ) -> Dict[str, Any]:
87
74
  """
88
75
  Analyze VPC cleanup candidates with comprehensive dependency analysis
89
-
76
+
90
77
  Args:
91
78
  vpc_ids: Specific VPC IDs to analyze
92
79
  account_profiles: Multiple account profiles for multi-account analysis
93
80
  export_results: Export analysis results to files
94
81
  output_directory: Directory for exported files
95
-
82
+
96
83
  Returns:
97
84
  Dictionary with analysis results and recommendations
98
85
  """
99
86
  print_header("VPC Cleanup Analysis", "Enterprise Framework")
100
-
87
+
101
88
  # Profile validation
102
89
  if account_profiles:
103
90
  validated_profiles = []
@@ -109,393 +96,366 @@ class VPCCleanupCLI:
109
96
  print_success(f"Profile validated: {profile_candidate}")
110
97
  except Exception as e:
111
98
  print_error(f"Profile validation failed: {profile_candidate} - {e}")
112
-
99
+
113
100
  if not validated_profiles:
114
101
  print_error("No valid profiles available for analysis")
115
102
  return {}
116
-
103
+
117
104
  account_profiles = validated_profiles
118
-
105
+
119
106
  # Perform analysis
120
107
  try:
121
108
  candidates = self.cleanup_framework.analyze_vpc_cleanup_candidates(
122
- vpc_ids=vpc_ids,
123
- account_profiles=account_profiles
109
+ vpc_ids=vpc_ids, account_profiles=account_profiles
124
110
  )
125
-
111
+
126
112
  if not candidates:
127
113
  print_warning("No VPC cleanup candidates found")
128
114
  return {}
129
-
115
+
130
116
  # Generate cleanup plan
131
117
  cleanup_plan = self.cleanup_framework.generate_cleanup_plan(candidates)
132
-
118
+
133
119
  # MCP Cross-Validation: Verify VPC data against real AWS APIs
134
120
  vpc_validation_data = {
135
- 'vpc_candidates': candidates,
136
- 'total_vpcs': len(candidates),
137
- 'regions': [self.region],
138
- 'profile': self.profile
121
+ "vpc_candidates": candidates,
122
+ "total_vpcs": len(candidates),
123
+ "regions": [self.region],
124
+ "profile": self.profile,
139
125
  }
140
-
126
+
141
127
  print_warning("Performing MCP cross-validation against AWS APIs...")
142
128
  try:
143
129
  # Cross-validate VPC discovery and dependencies
144
130
  import asyncio
145
- mcp_result = asyncio.run(
146
- self.mcp_integrator.validate_vpc_operations(vpc_validation_data)
147
- )
148
-
131
+
132
+ mcp_result = asyncio.run(self.mcp_integrator.validate_vpc_operations(vpc_validation_data))
133
+
149
134
  if mcp_result.success and mcp_result.consistency_score >= 99.5:
150
135
  actual_vpc_count = mcp_result.total_resources_validated
151
136
  consistency_score = mcp_result.consistency_score
152
-
137
+
153
138
  print_success(
154
139
  f"✅ MCP Validation: {consistency_score:.1f}% accuracy - "
155
140
  f"Found {actual_vpc_count} VPCs vs {len(candidates)} candidates"
156
141
  )
157
-
142
+
158
143
  # Add MCP validation results to cleanup plan
159
- cleanup_plan['mcp_validation'] = {
160
- 'validated': True,
161
- 'consistency_score': consistency_score,
162
- 'actual_vpc_count': actual_vpc_count,
163
- 'validation_timestamp': mcp_result.validation_timestamp
144
+ cleanup_plan["mcp_validation"] = {
145
+ "validated": True,
146
+ "consistency_score": consistency_score,
147
+ "actual_vpc_count": actual_vpc_count,
148
+ "validation_timestamp": mcp_result.validation_timestamp,
164
149
  }
165
150
  else:
166
151
  print_error(f"❌ MCP Validation failed: {mcp_result.consistency_score:.1f}% accuracy")
167
- cleanup_plan['mcp_validation'] = {
168
- 'validated': False,
169
- 'errors': mcp_result.error_details
170
- }
171
-
152
+ cleanup_plan["mcp_validation"] = {"validated": False, "errors": mcp_result.error_details}
153
+
172
154
  except Exception as e:
173
155
  print_error(f"MCP cross-validation error: {e}")
174
- cleanup_plan['mcp_validation'] = {'validated': False, 'error': str(e)}
175
-
156
+ cleanup_plan["mcp_validation"] = {"validated": False, "error": str(e)}
157
+
176
158
  # Display results
177
159
  self.cleanup_framework.display_cleanup_analysis(candidates)
178
-
160
+
179
161
  # Display executive summary
180
162
  self._display_executive_summary(cleanup_plan)
181
-
163
+
182
164
  # Export results if requested
183
165
  exported_files = {}
184
166
  if export_results:
185
167
  exported_files = self.cleanup_framework.export_cleanup_plan(
186
- output_directory=output_directory,
187
- include_dependencies=True
168
+ output_directory=output_directory, include_dependencies=True
188
169
  )
189
-
170
+
190
171
  return {
191
- 'candidates': candidates,
192
- 'cleanup_plan': cleanup_plan,
193
- 'exported_files': exported_files,
194
- 'analysis_summary': {
195
- 'total_vpcs': len(candidates),
196
- 'immediate_cleanup': len([c for c in candidates if c.cleanup_phase == VPCCleanupPhase.IMMEDIATE]),
197
- 'total_annual_savings': sum((c.annual_savings or 0.0) for c in candidates),
198
- 'safety_mode_enabled': self.safety_mode
199
- }
172
+ "candidates": candidates,
173
+ "cleanup_plan": cleanup_plan,
174
+ "exported_files": exported_files,
175
+ "analysis_summary": {
176
+ "total_vpcs": len(candidates),
177
+ "immediate_cleanup": len([c for c in candidates if c.cleanup_phase == VPCCleanupPhase.IMMEDIATE]),
178
+ "total_annual_savings": sum((c.annual_savings or 0.0) for c in candidates),
179
+ "safety_mode_enabled": self.safety_mode,
180
+ },
200
181
  }
201
-
182
+
202
183
  except Exception as e:
203
184
  print_error(f"VPC cleanup analysis failed: {e}")
204
185
  logger.error(f"VPC cleanup analysis error: {e}")
205
186
  return {}
206
187
 
207
188
  def execute_cleanup_phase(
208
- self,
209
- phase: str,
210
- vpc_ids: Optional[List[str]] = None,
211
- dry_run: bool = True,
212
- require_approval: bool = True
189
+ self, phase: str, vpc_ids: Optional[List[str]] = None, dry_run: bool = True, require_approval: bool = True
213
190
  ) -> Dict[str, Any]:
214
191
  """
215
192
  Execute VPC cleanup for a specific phase
216
-
193
+
217
194
  Args:
218
195
  phase: Cleanup phase to execute (immediate, investigation, governance, complex)
219
196
  vpc_ids: Specific VPC IDs to clean up
220
197
  dry_run: Execute in dry-run mode only
221
198
  require_approval: Require explicit user approval
222
-
199
+
223
200
  Returns:
224
201
  Dictionary with execution results
225
202
  """
226
203
  print_header(f"VPC Cleanup Execution - {phase.title()} Phase", "Enterprise Safety Controls")
227
-
204
+
228
205
  if not self.cleanup_framework.cleanup_candidates:
229
206
  print_error("No VPC candidates available. Run analysis first.")
230
207
  return {}
231
-
208
+
232
209
  # Map phase string to enum
233
210
  phase_mapping = {
234
- 'immediate': VPCCleanupPhase.IMMEDIATE,
235
- 'investigation': VPCCleanupPhase.INVESTIGATION,
236
- 'governance': VPCCleanupPhase.GOVERNANCE,
237
- 'complex': VPCCleanupPhase.COMPLEX
211
+ "immediate": VPCCleanupPhase.IMMEDIATE,
212
+ "investigation": VPCCleanupPhase.INVESTIGATION,
213
+ "governance": VPCCleanupPhase.GOVERNANCE,
214
+ "complex": VPCCleanupPhase.COMPLEX,
238
215
  }
239
-
216
+
240
217
  cleanup_phase = phase_mapping.get(phase.lower())
241
218
  if not cleanup_phase:
242
219
  print_error(f"Invalid cleanup phase: {phase}")
243
220
  return {}
244
-
221
+
245
222
  # Filter candidates for this phase
246
- phase_candidates = [
247
- c for c in self.cleanup_framework.cleanup_candidates
248
- if c.cleanup_phase == cleanup_phase
249
- ]
250
-
223
+ phase_candidates = [c for c in self.cleanup_framework.cleanup_candidates if c.cleanup_phase == cleanup_phase]
224
+
251
225
  if vpc_ids:
252
226
  phase_candidates = [c for c in phase_candidates if c.vpc_id in vpc_ids]
253
-
227
+
254
228
  if not phase_candidates:
255
229
  print_warning(f"No VPC candidates found for {phase} phase")
256
230
  return {}
257
-
231
+
258
232
  # Safety checks
259
233
  if self.safety_mode and not dry_run:
260
234
  print_warning("Safety mode is enabled. Forced dry-run execution.")
261
235
  dry_run = True
262
-
236
+
263
237
  # Display execution plan
264
238
  self._display_execution_plan(phase_candidates, dry_run)
265
-
239
+
266
240
  # Require approval for non-dry-run execution
267
241
  if not dry_run and require_approval:
268
242
  approval_message = (
269
243
  f"You are about to execute VPC cleanup for {len(phase_candidates)} VPCs.\n"
270
244
  f"This action cannot be undone. Are you sure you want to proceed?"
271
245
  )
272
-
246
+
273
247
  if not Confirm.ask(approval_message, default=False):
274
248
  print_warning("VPC cleanup execution cancelled by user")
275
- return {'status': 'cancelled', 'reason': 'user_cancellation'}
276
-
249
+ return {"status": "cancelled", "reason": "user_cancellation"}
250
+
277
251
  # Execute cleanup (currently dry-run only for safety)
278
252
  execution_results = {
279
- 'phase': phase,
280
- 'vpc_count': len(phase_candidates),
281
- 'dry_run': True, # Force dry-run for safety
282
- 'execution_plan': [],
283
- 'warnings': [],
284
- 'recommendations': []
253
+ "phase": phase,
254
+ "vpc_count": len(phase_candidates),
255
+ "dry_run": True, # Force dry-run for safety
256
+ "execution_plan": [],
257
+ "warnings": [],
258
+ "recommendations": [],
285
259
  }
286
-
260
+
287
261
  for candidate in phase_candidates:
288
262
  vpc_plan = self._generate_vpc_deletion_plan(candidate)
289
- execution_results['execution_plan'].append(vpc_plan)
290
-
263
+ execution_results["execution_plan"].append(vpc_plan)
264
+
291
265
  # Safety warnings
292
266
  if (candidate.blocking_dependencies or 0) > 0:
293
- execution_results['warnings'].append(
267
+ execution_results["warnings"].append(
294
268
  f"VPC {candidate.vpc_id} has {candidate.blocking_dependencies or 0} blocking dependencies"
295
269
  )
296
-
270
+
297
271
  if candidate.is_default:
298
- execution_results['warnings'].append(
272
+ execution_results["warnings"].append(
299
273
  f"VPC {candidate.vpc_id} is a default VPC - requires platform approval"
300
274
  )
301
-
275
+
302
276
  # Generate recommendations
303
- execution_results['recommendations'] = self._generate_execution_recommendations(phase_candidates)
304
-
277
+ execution_results["recommendations"] = self._generate_execution_recommendations(phase_candidates)
278
+
305
279
  print_success(f"VPC cleanup plan generated for {len(phase_candidates)} VPCs")
306
-
280
+
307
281
  if dry_run:
308
282
  print_warning("Dry-run mode: No actual VPC deletions performed")
309
-
283
+
310
284
  return execution_results
311
285
 
312
286
  def generate_business_report(
313
- self,
314
- include_executive_summary: bool = True,
315
- export_formats: Optional[List[str]] = None
287
+ self, include_executive_summary: bool = True, export_formats: Optional[List[str]] = None
316
288
  ) -> Dict[str, Any]:
317
289
  """
318
290
  Generate business-focused VPC cleanup report
319
-
291
+
320
292
  Args:
321
293
  include_executive_summary: Include executive summary
322
294
  export_formats: Export formats (json, csv, html)
323
-
295
+
324
296
  Returns:
325
297
  Dictionary with business report and export information
326
298
  """
327
299
  print_header("VPC Cleanup Business Report", "Executive Dashboard")
328
-
300
+
329
301
  if not self.cleanup_framework.cleanup_candidates:
330
302
  print_error("No VPC analysis data available. Run analysis first.")
331
303
  return {}
332
-
304
+
333
305
  if not export_formats:
334
- export_formats = ['json', 'csv']
335
-
306
+ export_formats = ["json", "csv"]
307
+
336
308
  try:
337
309
  # Configure manager interface for business reporting
338
310
  self.manager_interface.configure_for_business_user(
339
311
  safety_mode=self.safety_mode,
340
312
  target_savings=30.0, # 30% cost reduction target
341
- approval_threshold=1000.0 # $1K approval threshold
313
+ approval_threshold=1000.0, # $1K approval threshold
342
314
  )
343
-
315
+
344
316
  # Convert technical analysis to business insights
345
317
  vpc_analysis_results = {
346
- 'vpc_candidates': self.cleanup_framework.cleanup_candidates,
347
- 'cleanup_plan': self.cleanup_framework.analysis_results
318
+ "vpc_candidates": self.cleanup_framework.cleanup_candidates,
319
+ "cleanup_plan": self.cleanup_framework.analysis_results,
348
320
  }
349
-
350
- business_analysis = self.manager_interface.analyze_cost_optimization_opportunity(
351
- vpc_analysis_results
352
- )
353
-
321
+
322
+ business_analysis = self.manager_interface.analyze_cost_optimization_opportunity(vpc_analysis_results)
323
+
354
324
  # Display business dashboard
355
325
  if include_executive_summary:
356
326
  self.manager_interface.display_business_dashboard()
357
-
327
+
358
328
  # Export business reports
359
329
  exported_files = self.manager_interface.export_manager_friendly_reports()
360
-
330
+
361
331
  return {
362
- 'business_analysis': business_analysis,
363
- 'recommendations': self.manager_interface.business_recommendations,
364
- 'executive_presentation': self.manager_interface.generate_executive_presentation(),
365
- 'exported_files': exported_files
332
+ "business_analysis": business_analysis,
333
+ "recommendations": self.manager_interface.business_recommendations,
334
+ "executive_presentation": self.manager_interface.generate_executive_presentation(),
335
+ "exported_files": exported_files,
366
336
  }
367
-
337
+
368
338
  except Exception as e:
369
339
  print_error(f"Business report generation failed: {e}")
370
340
  logger.error(f"Business report error: {e}")
371
341
  return {}
372
342
 
373
- def validate_vpc_cleanup_safety(
374
- self,
375
- vpc_id: str,
376
- account_profile: Optional[str] = None
377
- ) -> Dict[str, Any]:
343
+ def validate_vpc_cleanup_safety(self, vpc_id: str, account_profile: Optional[str] = None) -> Dict[str, Any]:
378
344
  """
379
345
  Validate VPC cleanup safety with comprehensive dependency checking
380
-
346
+
381
347
  Args:
382
348
  vpc_id: VPC ID to validate
383
349
  account_profile: AWS profile for the account containing the VPC
384
-
350
+
385
351
  Returns:
386
352
  Dictionary with safety validation results
387
353
  """
388
354
  print_header(f"VPC Safety Validation", vpc_id)
389
-
355
+
390
356
  # Find the VPC candidate
391
357
  vpc_candidate = None
392
358
  for candidate in self.cleanup_framework.cleanup_candidates:
393
359
  if candidate.vpc_id == vpc_id:
394
360
  vpc_candidate = candidate
395
361
  break
396
-
362
+
397
363
  if not vpc_candidate:
398
364
  # Run targeted analysis for this VPC
399
365
  profile_to_use = account_profile or self.profile
400
-
366
+
401
367
  temp_framework = VPCCleanupFramework(
402
- profile=profile_to_use,
403
- region=self.region,
404
- console=self.console,
405
- safety_mode=True
368
+ profile=profile_to_use, region=self.region, console=self.console, safety_mode=True
406
369
  )
407
-
370
+
408
371
  candidates = temp_framework.analyze_vpc_cleanup_candidates(vpc_ids=[vpc_id])
409
-
372
+
410
373
  if candidates:
411
374
  vpc_candidate = candidates[0]
412
375
  else:
413
376
  print_error(f"VPC {vpc_id} not found or inaccessible")
414
377
  return {}
415
-
378
+
416
379
  # Perform safety validation
417
380
  safety_results = {
418
- 'vpc_id': vpc_id,
419
- 'safety_score': 'SAFE',
420
- 'blocking_dependencies': vpc_candidate.blocking_dependencies or 0,
421
- 'risk_level': vpc_candidate.risk_level.value,
422
- 'safety_checks': [],
423
- 'warnings': [],
424
- 'approval_required': vpc_candidate.approval_required
381
+ "vpc_id": vpc_id,
382
+ "safety_score": "SAFE",
383
+ "blocking_dependencies": vpc_candidate.blocking_dependencies or 0,
384
+ "risk_level": vpc_candidate.risk_level.value,
385
+ "safety_checks": [],
386
+ "warnings": [],
387
+ "approval_required": vpc_candidate.approval_required,
425
388
  }
426
-
389
+
427
390
  # ENI check (most critical)
428
391
  if vpc_candidate.eni_count > 0:
429
- safety_results['safety_checks'].append({
430
- 'check': 'ENI Count',
431
- 'status': 'FAIL',
432
- 'details': f"{vpc_candidate.eni_count} network interfaces found",
433
- 'blocking': True
434
- })
435
- safety_results['safety_score'] = 'UNSAFE'
392
+ safety_results["safety_checks"].append(
393
+ {
394
+ "check": "ENI Count",
395
+ "status": "FAIL",
396
+ "details": f"{vpc_candidate.eni_count} network interfaces found",
397
+ "blocking": True,
398
+ }
399
+ )
400
+ safety_results["safety_score"] = "UNSAFE"
436
401
  else:
437
- safety_results['safety_checks'].append({
438
- 'check': 'ENI Count',
439
- 'status': 'PASS',
440
- 'details': 'No active network interfaces',
441
- 'blocking': False
442
- })
443
-
402
+ safety_results["safety_checks"].append(
403
+ {"check": "ENI Count", "status": "PASS", "details": "No active network interfaces", "blocking": False}
404
+ )
405
+
444
406
  # Dependency checks
445
407
  internal_deps = len([d for d in vpc_candidate.dependencies if d.dependency_level == 1])
446
408
  external_deps = len([d for d in vpc_candidate.dependencies if d.dependency_level == 2])
447
409
  control_deps = len([d for d in vpc_candidate.dependencies if d.dependency_level == 3])
448
-
449
- safety_results['safety_checks'].extend([
450
- {
451
- 'check': 'Internal Dependencies',
452
- 'status': 'WARN' if internal_deps > 0 else 'PASS',
453
- 'details': f"{internal_deps} internal dependencies (NAT, Endpoints, etc.)",
454
- 'blocking': internal_deps > 0
455
- },
456
- {
457
- 'check': 'External Dependencies',
458
- 'status': 'WARN' if external_deps > 0 else 'PASS',
459
- 'details': f"{external_deps} external dependencies (TGW, Peering, etc.)",
460
- 'blocking': external_deps > 0
461
- },
462
- {
463
- 'check': 'Control Plane Dependencies',
464
- 'status': 'WARN' if control_deps > 0 else 'PASS',
465
- 'details': f"{control_deps} control plane dependencies",
466
- 'blocking': control_deps > 0
467
- }
468
- ])
469
-
410
+
411
+ safety_results["safety_checks"].extend(
412
+ [
413
+ {
414
+ "check": "Internal Dependencies",
415
+ "status": "WARN" if internal_deps > 0 else "PASS",
416
+ "details": f"{internal_deps} internal dependencies (NAT, Endpoints, etc.)",
417
+ "blocking": internal_deps > 0,
418
+ },
419
+ {
420
+ "check": "External Dependencies",
421
+ "status": "WARN" if external_deps > 0 else "PASS",
422
+ "details": f"{external_deps} external dependencies (TGW, Peering, etc.)",
423
+ "blocking": external_deps > 0,
424
+ },
425
+ {
426
+ "check": "Control Plane Dependencies",
427
+ "status": "WARN" if control_deps > 0 else "PASS",
428
+ "details": f"{control_deps} control plane dependencies",
429
+ "blocking": control_deps > 0,
430
+ },
431
+ ]
432
+ )
433
+
470
434
  # Update safety score based on blocking dependencies
471
- blocking_checks = len([c for c in safety_results['safety_checks'] if c['blocking']])
435
+ blocking_checks = len([c for c in safety_results["safety_checks"] if c["blocking"]])
472
436
  if blocking_checks > 0:
473
- safety_results['safety_score'] = 'UNSAFE'
474
-
437
+ safety_results["safety_score"] = "UNSAFE"
438
+
475
439
  # IaC management check
476
440
  if vpc_candidate.iac_managed:
477
- safety_results['warnings'].append(
478
- f"VPC is managed by Infrastructure as Code: {vpc_candidate.iac_source}"
479
- )
480
-
441
+ safety_results["warnings"].append(f"VPC is managed by Infrastructure as Code: {vpc_candidate.iac_source}")
442
+
481
443
  # Default VPC check
482
444
  if vpc_candidate.is_default:
483
- safety_results['warnings'].append(
484
- "VPC is a default VPC - requires platform team approval"
485
- )
486
-
445
+ safety_results["warnings"].append("VPC is a default VPC - requires platform team approval")
446
+
487
447
  # Display results
488
448
  self._display_safety_validation(safety_results)
489
-
449
+
490
450
  return safety_results
491
451
 
492
452
  def _display_executive_summary(self, cleanup_plan: Dict[str, Any]) -> None:
493
453
  """Display executive summary of cleanup plan"""
494
454
  if not cleanup_plan:
495
455
  return
496
-
497
- exec_summary = cleanup_plan.get('executive_summary', {})
498
-
456
+
457
+ exec_summary = cleanup_plan.get("executive_summary", {})
458
+
499
459
  summary_text = (
500
460
  f"[bold blue]📊 EXECUTIVE SUMMARY[/bold blue]\n\n"
501
461
  f"Total VPCs Analyzed: [yellow]{cleanup_plan['metadata']['total_vpcs_analyzed']}[/yellow]\n"
@@ -507,13 +467,13 @@ class VPCCleanupCLI:
507
467
  f"Total Annual Savings: [bold green]${(cleanup_plan['metadata']['total_annual_savings'] or 0.0):,.2f}[/bold green]\n"
508
468
  f"Business Case Strength: [cyan]{exec_summary.get('business_case_strength', 'Unknown')}[/cyan]"
509
469
  )
510
-
470
+
511
471
  self.console.print(Panel(summary_text, title="Executive Summary", style="white", width=80))
512
472
 
513
473
  def _display_execution_plan(self, candidates: List, dry_run: bool) -> None:
514
474
  """Display VPC cleanup execution plan"""
515
475
  mode_text = "[yellow]DRY RUN MODE[/yellow]" if dry_run else "[red]LIVE EXECUTION MODE[/red]"
516
-
476
+
517
477
  plan_text = (
518
478
  f"[bold blue]🚀 EXECUTION PLAN[/bold blue]\n\n"
519
479
  f"Mode: {mode_text}\n"
@@ -522,7 +482,7 @@ class VPCCleanupCLI:
522
482
  f"High Risk VPCs: [red]{len([c for c in candidates if c.risk_level == VPCCleanupRisk.HIGH])}[/red]\n"
523
483
  f"Default VPCs: [magenta]{len([c for c in candidates if c.is_default])}[/magenta]"
524
484
  )
525
-
485
+
526
486
  self.console.print(Panel(plan_text, title="Execution Plan", style="yellow" if dry_run else "red", width=80))
527
487
 
528
488
  def _display_safety_validation(self, safety_results: Dict[str, Any]) -> None:
@@ -534,122 +494,216 @@ class VPCCleanupCLI:
534
494
  {"header": "Check", "style": "cyan"},
535
495
  {"header": "Status", "style": "green"},
536
496
  {"header": "Details", "style": "white"},
537
- {"header": "Blocking", "style": "red"}
538
- ]
497
+ {"header": "Blocking", "style": "red"},
498
+ ],
539
499
  )
540
-
541
- for check in safety_results['safety_checks']:
500
+
501
+ for check in safety_results["safety_checks"]:
542
502
  status_color = {
543
- 'PASS': '[green]✅ PASS[/green]',
544
- 'WARN': '[yellow]⚠️ WARN[/yellow]',
545
- 'FAIL': '[red]❌ FAIL[/red]'
546
- }.get(check['status'], check['status'])
547
-
548
- blocking_indicator = "🔴 YES" if check['blocking'] else "✅ NO"
549
-
550
- table.add_row(
551
- check['check'],
552
- status_color,
553
- check['details'],
554
- blocking_indicator
555
- )
556
-
503
+ "PASS": "[green]✅ PASS[/green]",
504
+ "WARN": "[yellow]⚠️ WARN[/yellow]",
505
+ "FAIL": "[red]❌ FAIL[/red]",
506
+ }.get(check["status"], check["status"])
507
+
508
+ blocking_indicator = "🔴 YES" if check["blocking"] else "✅ NO"
509
+
510
+ table.add_row(check["check"], status_color, check["details"], blocking_indicator)
511
+
557
512
  self.console.print(table)
558
-
513
+
559
514
  # Overall safety assessment
560
- safety_color = "green" if safety_results['safety_score'] == 'SAFE' else "red"
515
+ safety_color = "green" if safety_results["safety_score"] == "SAFE" else "red"
561
516
  assessment_text = (
562
517
  f"[bold {safety_color}]Overall Safety: {safety_results['safety_score']}[/bold {safety_color}]\n"
563
518
  f"Risk Level: [magenta]{safety_results['risk_level']}[/magenta]\n"
564
519
  f"Approval Required: [yellow]{'YES' if safety_results['approval_required'] else 'NO'}[/yellow]"
565
520
  )
566
-
521
+
567
522
  self.console.print(Panel(assessment_text, title="Safety Assessment", style=safety_color, width=60))
568
-
523
+
569
524
  # Display warnings
570
- if safety_results['warnings']:
571
- warnings_text = "\n".join([f"⚠️ {warning}" for warning in safety_results['warnings']])
525
+ if safety_results["warnings"]:
526
+ warnings_text = "\n".join([f"⚠️ {warning}" for warning in safety_results["warnings"]])
572
527
  self.console.print(Panel(warnings_text, title="Important Warnings", style="yellow", width=80))
573
528
 
574
529
  def _generate_vpc_deletion_plan(self, candidate) -> Dict[str, Any]:
575
530
  """Generate detailed VPC deletion plan"""
576
531
  deletion_steps = []
577
-
532
+
578
533
  # Sort dependencies by deletion order
579
534
  sorted_deps = sorted(candidate.dependencies, key=lambda x: x.deletion_order)
580
-
535
+
581
536
  for i, dep in enumerate(sorted_deps, 1):
582
- deletion_steps.append({
583
- 'step': i,
584
- 'action': f"Delete {dep.resource_type}",
585
- 'resource_id': dep.resource_id,
586
- 'api_method': dep.api_method,
587
- 'description': dep.description,
588
- 'dependency_level': dep.dependency_level
589
- })
590
-
537
+ deletion_steps.append(
538
+ {
539
+ "step": i,
540
+ "action": f"Delete {dep.resource_type}",
541
+ "resource_id": dep.resource_id,
542
+ "api_method": dep.api_method,
543
+ "description": dep.description,
544
+ "dependency_level": dep.dependency_level,
545
+ }
546
+ )
547
+
591
548
  # Final VPC deletion step
592
- deletion_steps.append({
593
- 'step': len(deletion_steps) + 1,
594
- 'action': 'Delete VPC',
595
- 'resource_id': candidate.vpc_id,
596
- 'api_method': 'delete_vpc',
597
- 'description': 'Final VPC deletion',
598
- 'dependency_level': 0
599
- })
600
-
549
+ deletion_steps.append(
550
+ {
551
+ "step": len(deletion_steps) + 1,
552
+ "action": "Delete VPC",
553
+ "resource_id": candidate.vpc_id,
554
+ "api_method": "delete_vpc",
555
+ "description": "Final VPC deletion",
556
+ "dependency_level": 0,
557
+ }
558
+ )
559
+
601
560
  return {
602
- 'vpc_id': candidate.vpc_id,
603
- 'vpc_name': candidate.vpc_name,
604
- 'risk_level': candidate.risk_level.value,
605
- 'total_steps': len(deletion_steps),
606
- 'estimated_time': f"{len(deletion_steps) * 2} minutes",
607
- 'deletion_steps': deletion_steps
561
+ "vpc_id": candidate.vpc_id,
562
+ "vpc_name": candidate.vpc_name,
563
+ "risk_level": candidate.risk_level.value,
564
+ "total_steps": len(deletion_steps),
565
+ "estimated_time": f"{len(deletion_steps) * 2} minutes",
566
+ "deletion_steps": deletion_steps,
608
567
  }
609
568
 
610
569
  def _generate_execution_recommendations(self, candidates: List) -> List[str]:
611
570
  """Generate execution recommendations"""
612
571
  recommendations = []
613
-
572
+
614
573
  # Phase-specific recommendations
615
574
  immediate_count = len([c for c in candidates if c.cleanup_phase == VPCCleanupPhase.IMMEDIATE])
616
575
  high_risk_count = len([c for c in candidates if c.risk_level == VPCCleanupRisk.HIGH])
617
576
  default_vpc_count = len([c for c in candidates if c.is_default])
618
577
  iac_managed_count = len([c for c in candidates if c.iac_managed])
619
-
578
+
620
579
  if immediate_count > 0:
621
- recommendations.append(
622
- f"Execute {immediate_count} immediate cleanup candidates first for quick wins"
623
- )
624
-
580
+ recommendations.append(f"Execute {immediate_count} immediate cleanup candidates first for quick wins")
581
+
625
582
  if high_risk_count > 0:
626
- recommendations.append(
627
- f"Review {high_risk_count} high-risk VPCs with stakeholders before execution"
628
- )
629
-
583
+ recommendations.append(f"Review {high_risk_count} high-risk VPCs with stakeholders before execution")
584
+
630
585
  if default_vpc_count > 0:
631
- recommendations.append(
632
- f"Obtain platform team approval for {default_vpc_count} default VPC deletions"
633
- )
634
-
586
+ recommendations.append(f"Obtain platform team approval for {default_vpc_count} default VPC deletions")
587
+
635
588
  if iac_managed_count > 0:
636
- recommendations.append(
637
- f"Update Infrastructure as Code for {iac_managed_count} IaC-managed VPCs"
638
- )
639
-
589
+ recommendations.append(f"Update Infrastructure as Code for {iac_managed_count} IaC-managed VPCs")
590
+
640
591
  # General recommendations
641
- recommendations.extend([
642
- "Execute VPC cleanup in phases to minimize blast radius",
643
- "Validate each deletion step before proceeding to next",
644
- "Maintain comprehensive audit trail of all deletion activities",
645
- "Schedule cleanup during maintenance windows to minimize impact"
646
- ])
647
-
592
+ recommendations.extend(
593
+ [
594
+ "Execute VPC cleanup in phases to minimize blast radius",
595
+ "Validate each deletion step before proceeding to next",
596
+ "Maintain comprehensive audit trail of all deletion activities",
597
+ "Schedule cleanup during maintenance windows to minimize impact",
598
+ ]
599
+ )
600
+
648
601
  return recommendations
649
602
 
650
603
 
604
+ def display_config_campaign_results(results: Dict[str, Any]) -> None:
605
+ """
606
+ Display config-driven campaign results using Rich CLI formatting (NEW FUNCTION)
607
+
608
+ This function formats and displays the results from VPCCleanupFramework.analyze_from_config()
609
+ with enterprise-grade Rich CLI presentation for campaign analysis visibility.
610
+
611
+ Args:
612
+ results: Campaign analysis results from analyze_from_config()
613
+ """
614
+ from runbooks.common.rich_utils import (
615
+ console,
616
+ create_table,
617
+ print_header,
618
+ print_success,
619
+ print_warning,
620
+ print_info,
621
+ create_panel,
622
+ )
623
+
624
+ # Extract campaign metadata
625
+ campaign_meta = results.get("campaign_metadata", {})
626
+ campaign_id = campaign_meta.get("campaign_id", "Unknown")
627
+ campaign_name = campaign_meta.get("campaign_name", "VPC Cleanup Campaign")
628
+ aws_profile = campaign_meta.get("aws_billing_profile", "default")
629
+ execution_date = campaign_meta.get("execution_date", "N/A")
630
+
631
+ # Display campaign header
632
+ print_header(f"Campaign {campaign_id}: {campaign_name}", "Config-Driven Analysis")
633
+
634
+ # Campaign metadata panel
635
+ metadata_content = (
636
+ f"[bold cyan]Campaign Metadata[/bold cyan]\n\n"
637
+ f"[white]Campaign ID:[/white] [bright_yellow]{campaign_id}[/bright_yellow]\n"
638
+ f"[white]AWS Billing Profile:[/white] [bright_blue]{aws_profile}[/bright_blue]\n"
639
+ f"[white]Execution Date:[/white] [dim]{execution_date}[/dim]\n"
640
+ f"[white]Description:[/white] {campaign_meta.get('description', 'N/A')}"
641
+ )
642
+
643
+ console.print(create_panel(metadata_content, title="Campaign Information", border_style="cyan"))
644
+ console.print()
645
+
646
+ # VPC results table
647
+ vpc_results = results.get("vpc_results", [])
648
+
649
+ if vpc_results:
650
+ table = create_table(
651
+ title=f"VPC Campaign Results - {len(vpc_results)} VPCs Analyzed",
652
+ columns=[
653
+ {"name": "VPC ID", "style": "bright_cyan"},
654
+ {"name": "Account", "style": "bright_blue"},
655
+ {"name": "Region", "style": "yellow"},
656
+ {"name": "Deletion Date", "style": "dim"},
657
+ {"name": "Monthly Savings", "style": "bright_green", "justify": "right"},
658
+ {"name": "Annual Savings", "style": "bright_green bold", "justify": "right"},
659
+ {"name": "Confidence", "style": "cyan"},
660
+ ],
661
+ )
662
+
663
+ for vpc_result in vpc_results:
664
+ table.add_row(
665
+ vpc_result.get("vpc_id", "N/A"),
666
+ vpc_result.get("account_id", "N/A"),
667
+ vpc_result.get("region", "N/A"),
668
+ vpc_result.get("deletion_date", "N/A"),
669
+ f"${vpc_result.get('monthly_savings', 0.0):,.2f}",
670
+ f"${vpc_result.get('annual_savings', 0.0):,.2f}",
671
+ vpc_result.get("confidence_level", "MEDIUM"),
672
+ )
673
+
674
+ console.print(table)
675
+ console.print()
676
+ else:
677
+ print_warning("No VPC results found in campaign analysis")
678
+ console.print()
679
+
680
+ # Total savings summary panel
681
+ total_savings = results.get("total_savings", {})
682
+ monthly_total = total_savings.get("monthly", 0.0)
683
+ annual_total = total_savings.get("annual", 0.0)
684
+
685
+ savings_content = (
686
+ f"[bold green]💰 Total Realized Savings[/bold green]\n\n"
687
+ f"[white]Monthly Savings:[/white] [bright_green]${monthly_total:,.2f}[/bright_green]\n"
688
+ f"[white]Annual Savings:[/white] [bright_green bold]${annual_total:,.2f}[/bright_green bold]\n"
689
+ f"[white]VPCs Analyzed:[/white] [bright_yellow]{len(vpc_results)}[/bright_yellow]"
690
+ )
691
+
692
+ console.print(create_panel(savings_content, title="Financial Impact", border_style="bright_green"))
693
+ console.print()
694
+
695
+ # Campaign success message
696
+ if annual_total > 0:
697
+ print_success(
698
+ f"Campaign {campaign_id} analysis complete: ${annual_total:,.2f}/year realized savings from {len(vpc_results)} VPCs"
699
+ )
700
+ else:
701
+ print_info(f"Campaign {campaign_id} analysis complete - See results above")
702
+
703
+
651
704
  # CLI Command Functions for integration with runbooks CLI
652
705
 
706
+
653
707
  def analyze_cleanup_candidates(
654
708
  profile: Optional[str] = None,
655
709
  vpc_ids: Optional[List[str]] = None,
@@ -657,11 +711,12 @@ def analyze_cleanup_candidates(
657
711
  region: str = "us-east-1",
658
712
  export_results: bool = True,
659
713
  account_limit: Optional[int] = None,
660
- region_limit: Optional[int] = None
714
+ region_limit: Optional[int] = None,
715
+ config: Optional[str] = None,
661
716
  ) -> Dict[str, Any]:
662
717
  """
663
718
  CLI function to analyze VPC cleanup candidates
664
-
719
+
665
720
  Args:
666
721
  profile: AWS profile for analysis
667
722
  vpc_ids: Specific VPC IDs to analyze
@@ -670,275 +725,291 @@ def analyze_cleanup_candidates(
670
725
  export_results: Export results to files
671
726
  account_limit: Limit number of accounts to process for faster testing
672
727
  region_limit: Limit number of regions to scan per account
673
-
728
+ config: Path to YAML campaign configuration file (NEW)
729
+
674
730
  Returns:
675
731
  Dictionary with analysis results
676
732
  """
677
733
  # Determine profile to use
678
734
  operational_profile = get_profile_for_operation("operational", profile)
679
-
735
+
736
+ # NEW: Config-driven campaign analysis
737
+ if config:
738
+ cleanup_framework = VPCCleanupFramework(profile=operational_profile, region=region, safety_mode=True)
739
+
740
+ results = cleanup_framework.analyze_from_config(config)
741
+ display_config_campaign_results(results)
742
+ return results
743
+
680
744
  # Initialize CLI wrapper
681
745
  cleanup_cli = VPCCleanupCLI(
682
746
  profile=operational_profile,
683
747
  region=region,
684
- safety_mode=True # Always enable safety mode
748
+ safety_mode=True, # Always enable safety mode
685
749
  )
686
-
750
+
687
751
  # Handle multi-account analysis
688
752
  account_profiles = None
689
753
  if all_accounts:
690
754
  # Use Organizations API to discover all accounts
691
755
  console.print("[blue]🔍 Discovering organization accounts for multi-account VPC analysis...[/blue]")
692
-
756
+
693
757
  try:
694
758
  # Import Organizations discovery functionality from FinOps module
695
759
  from runbooks.finops.aws_client import get_organization_accounts
696
760
  from runbooks.common.profile_utils import create_operational_session, create_management_session
697
761
  from runbooks.vpc.cross_account_session import convert_accounts_to_sessions
698
-
762
+
699
763
  # Check for cached Organizations data first (performance optimization)
700
-
764
+
701
765
  # Use CENTRALISED_OPS_PROFILE if available for operational accounts
702
766
  import os
703
- centralised_ops_profile = os.getenv('CENTRALISED_OPS_PROFILE')
767
+
768
+ centralised_ops_profile = os.getenv("CENTRALISED_OPS_PROFILE")
704
769
  if centralised_ops_profile:
705
770
  console.print(f"[green]✅ Using CENTRALISED_OPS_PROFILE: {centralised_ops_profile}[/green]")
706
771
  from .mcp_no_eni_validator import _get_cached_organizations_data, _cache_organizations_data
707
-
772
+
708
773
  org_accounts = _get_cached_organizations_data()
709
-
774
+
710
775
  if not org_accounts:
711
776
  # Create management session for Organizations discovery (needs Organizations permissions)
712
- session = create_management_session(profile=operational_profile)
713
-
777
+ session = create_management_session(profile_name=operational_profile)
778
+
714
779
  # Discover all organization accounts
715
780
  org_accounts = get_organization_accounts(session, operational_profile)
716
-
781
+
717
782
  # Cache the results for future use (prevents duplicate calls)
718
783
  if org_accounts:
719
784
  _cache_organizations_data(org_accounts)
720
-
785
+
721
786
  if org_accounts:
722
787
  # Apply account limit for performance optimization before session creation
723
788
  if account_limit and account_limit < len(org_accounts):
724
789
  console.print(f"[yellow]🎯 Performance mode: limiting to first {account_limit} accounts[/yellow]")
725
790
  org_accounts = org_accounts[:account_limit]
726
-
791
+
727
792
  # Convert accounts to cross-account sessions using STS AssumeRole
728
793
  account_sessions, account_metadata = convert_accounts_to_sessions(org_accounts, operational_profile)
729
-
794
+
730
795
  console.print(f"[green]✅ Discovered {len(org_accounts)} organization accounts[/green]")
731
- console.print(f"[cyan]📋 Created {len(account_sessions)} cross-account sessions for VPC analysis[/cyan]")
732
-
796
+ console.print(
797
+ f"[cyan]📋 Created {len(account_sessions)} cross-account sessions for VPC analysis[/cyan]"
798
+ )
799
+
733
800
  # Log account discovery for transparency
734
801
  active_count = len([acc for acc in org_accounts if acc.get("status") == "ACTIVE"])
735
802
  inactive_count = len(org_accounts) - active_count
736
- console.print(f"[dim]Organization scope: {active_count} active, {inactive_count} inactive accounts[/dim]")
737
-
803
+ console.print(
804
+ f"[dim]Organization scope: {active_count} active, {inactive_count} inactive accounts[/dim]"
805
+ )
806
+
738
807
  # Detect STS AssumeRole failures and switch to multi-profile discovery
739
808
  if len(account_sessions) == 0 and len(org_accounts) > 0:
740
- console.print(f"[red]❌ STS AssumeRole failed for all {len(org_accounts)} accounts - cross-account access denied[/red]")
741
- console.print("[yellow]💡 Enhancing to multi-profile discovery for comprehensive VPC scanning[/yellow]")
742
-
809
+ console.print(
810
+ f"[red] STS AssumeRole failed for all {len(org_accounts)} accounts - cross-account access denied[/red]"
811
+ )
812
+ console.print(
813
+ "[yellow]💡 Enhancing to multi-profile discovery for comprehensive VPC scanning[/yellow]"
814
+ )
815
+
743
816
  # Enhanced multi-profile discovery pattern (KISS & DRY)
744
817
  console.print("[blue]🔍 Discovering VPC profiles from available AWS configurations...[/blue]")
745
818
  account_profiles = _discover_vpc_profiles_from_available_aws_profiles(operational_profile)
746
-
819
+
747
820
  if account_profiles and len(account_profiles) > 1:
748
- console.print(f"[green]✅ Enhanced discovery found {len(account_profiles)} profiles with VPC access[/green]")
821
+ console.print(
822
+ f"[green]✅ Enhanced discovery found {len(account_profiles)} profiles with VPC access[/green]"
823
+ )
749
824
  else:
750
825
  console.print("[yellow]⚠️ Enhanced discovery fallback to single profile[/yellow]")
751
826
  else:
752
827
  # Store sessions for VPC discovery instead of profiles
753
828
  account_profiles = account_sessions # Pass sessions instead of profile strings
754
-
829
+
755
830
  else:
756
831
  console.print("[yellow]⚠️ No organization accounts found, falling back to single profile[/yellow]")
757
832
  account_profiles = [operational_profile] if operational_profile else None
758
-
833
+
759
834
  except ImportError as e:
760
835
  console.print(f"[red]❌ Organizations discovery unavailable: {e}[/red]")
761
836
  console.print("[yellow]💡 Falling back to single profile analysis[/yellow]")
762
837
  account_profiles = [operational_profile] if operational_profile else None
763
-
838
+
764
839
  except Exception as e:
765
840
  console.print(f"[red]❌ Organizations discovery failed: {e}[/red]")
766
841
  console.print("[yellow]💡 Enhancing to multi-profile discovery for comprehensive VPC scanning[/yellow]")
767
-
842
+
768
843
  # Enhanced multi-profile discovery pattern (KISS & DRY)
769
844
  console.print("[blue]🔍 Discovering VPC profiles from available AWS configurations...[/blue]")
770
845
  account_profiles = _discover_vpc_profiles_from_available_aws_profiles(operational_profile)
771
-
846
+
772
847
  if account_profiles and len(account_profiles) > 1:
773
- console.print(f"[green]✅ Enhanced discovery found {len(account_profiles)} profiles with VPC access[/green]")
848
+ console.print(
849
+ f"[green]✅ Enhanced discovery found {len(account_profiles)} profiles with VPC access[/green]"
850
+ )
774
851
  else:
775
852
  console.print("[yellow]⚠️ Enhanced discovery fallback to single profile[/yellow]")
776
-
853
+
777
854
  return cleanup_cli.analyze_vpc_cleanup_candidates(
778
- vpc_ids=vpc_ids,
779
- account_profiles=account_profiles,
780
- export_results=export_results
855
+ vpc_ids=vpc_ids, account_profiles=account_profiles, export_results=export_results
781
856
  )
782
857
 
783
858
 
784
- def validate_cleanup_safety(
785
- vpc_id: str,
786
- profile: Optional[str] = None,
787
- region: str = "us-east-1"
788
- ) -> Dict[str, Any]:
859
+ def validate_cleanup_safety(vpc_id: str, profile: Optional[str] = None, region: str = "us-east-1") -> Dict[str, Any]:
789
860
  """
790
861
  CLI function to validate VPC cleanup safety
791
-
862
+
792
863
  Args:
793
864
  vpc_id: VPC ID to validate
794
865
  profile: AWS profile
795
866
  region: AWS region
796
-
867
+
797
868
  Returns:
798
869
  Dictionary with safety validation results
799
870
  """
800
871
  operational_profile = get_profile_for_operation("operational", profile)
801
-
802
- cleanup_cli = VPCCleanupCLI(
803
- profile=operational_profile,
804
- region=region,
805
- safety_mode=True
806
- )
807
-
808
- return cleanup_cli.validate_vpc_cleanup_safety(
809
- vpc_id=vpc_id,
810
- account_profile=operational_profile
811
- )
872
+
873
+ cleanup_cli = VPCCleanupCLI(profile=operational_profile, region=region, safety_mode=True)
874
+
875
+ return cleanup_cli.validate_vpc_cleanup_safety(vpc_id=vpc_id, account_profile=operational_profile)
812
876
 
813
877
 
814
878
  def _discover_vpc_profiles_from_available_aws_profiles(primary_profile: str) -> List[str]:
815
879
  """
816
880
  Enhanced multi-profile discovery for comprehensive VPC scanning across all available AWS profiles.
817
-
881
+
818
882
  KISS & DRY approach: Use boto3's available_profiles to discover VPCs across Landing Zone
819
883
  when Organizations API cross-account role assumption fails.
820
-
884
+
821
885
  Args:
822
886
  primary_profile: Primary operational profile to include
823
-
887
+
824
888
  Returns:
825
889
  List of validated AWS profile names for VPC discovery
826
890
  """
827
891
  import boto3
828
892
  from rich.progress import Progress, TaskID
829
-
893
+
830
894
  console.print("[blue]🔍 Discovering VPC profiles from available AWS configurations...[/blue]")
831
-
895
+
832
896
  # Get all available AWS profiles
833
897
  try:
834
898
  session = boto3.Session()
835
899
  available_profiles = session.available_profiles
836
-
900
+
837
901
  if not available_profiles:
838
902
  console.print("[yellow]⚠️ No AWS profiles found in configuration[/yellow]")
839
903
  return [primary_profile] if primary_profile else []
840
-
904
+
841
905
  console.print(f"[cyan]📋 Found {len(available_profiles)} AWS profiles in configuration[/cyan]")
842
-
906
+
843
907
  # Enhanced multi-region discovery for comprehensive Landing Zone coverage
844
908
  # Based on user's confirmed NO-ENI VPCs in: us-east-1, us-west-2, ap-southeast-2
845
909
  regions_to_check = [
846
- 'us-east-1', # Primary US region - user confirmed VPCs here
847
- 'us-west-2', # Secondary US region - user confirmed VPCs here
848
- 'ap-southeast-2', # APAC region - user confirmed VPCs here
849
- 'eu-west-1', # Europe primary
850
- 'ca-central-1', # Canada
851
- 'ap-northeast-1', # Tokyo (common enterprise region)
910
+ "us-east-1", # Primary US region - user confirmed VPCs here
911
+ "us-west-2", # Secondary US region - user confirmed VPCs here
912
+ "ap-southeast-2", # APAC region - user confirmed VPCs here
913
+ "eu-west-1", # Europe primary
914
+ "ca-central-1", # Canada
915
+ "ap-northeast-1", # Tokyo (common enterprise region)
852
916
  ]
853
-
917
+
854
918
  # Validate profiles by attempting to create sessions and check VPC access
855
919
  validated_profiles = []
856
920
  profile_vpc_details = {}
857
-
921
+
858
922
  with Progress() as progress:
859
923
  profile_task = progress.add_task("🔍 Validating profiles for VPC access...", total=len(available_profiles))
860
-
924
+
861
925
  for profile_name in available_profiles:
862
926
  try:
863
927
  # Skip obvious non-VPC profiles but be less restrictive
864
- if 'billing' in profile_name.lower() and 'readonly' in profile_name.lower():
928
+ if "billing" in profile_name.lower() and "readonly" in profile_name.lower():
865
929
  console.print(f"[dim]⏭️ Skipping {profile_name} (billing-only profile)[/dim]")
866
930
  progress.advance(profile_task)
867
931
  continue
868
-
932
+
869
933
  # Create test session
870
934
  test_session = boto3.Session(profile_name=profile_name)
871
935
  total_vpcs = 0
872
936
  regions_with_vpcs = []
873
-
937
+
874
938
  # Check multiple regions for VPCs (Landing Zone accounts may have VPCs in different regions)
875
939
  for region in regions_to_check:
876
940
  try:
877
- ec2_client = test_session.client('ec2', region_name=region)
941
+ ec2_client = test_session.client("ec2", region_name=region)
878
942
  vpc_response = ec2_client.describe_vpcs(MaxResults=10) # Check more VPCs per region
879
- region_vpc_count = len(vpc_response.get('Vpcs', []))
880
-
943
+ region_vpc_count = len(vpc_response.get("Vpcs", []))
944
+
881
945
  if region_vpc_count > 0:
882
946
  total_vpcs += region_vpc_count
883
947
  regions_with_vpcs.append(f"{region}:{region_vpc_count}")
884
-
948
+
885
949
  except Exception as region_error:
886
950
  # Log region-specific errors but don't fail the whole profile
887
951
  if "UnauthorizedOperation" not in str(region_error):
888
952
  console.print(f"[dim]⚠️ {profile_name} in {region}: {str(region_error)[:30]}...[/dim]")
889
953
  continue
890
-
954
+
891
955
  # Add profile if it has VPCs in any region OR if it's the primary profile
892
956
  if total_vpcs > 0:
893
957
  validated_profiles.append(profile_name)
894
- profile_vpc_details[profile_name] = {
895
- 'total_vpcs': total_vpcs,
896
- 'regions': regions_with_vpcs
897
- }
898
- console.print(f"[green]✅ {profile_name}: {total_vpcs} VPCs across {len(regions_with_vpcs)} regions[/green]")
958
+ profile_vpc_details[profile_name] = {"total_vpcs": total_vpcs, "regions": regions_with_vpcs}
959
+ console.print(
960
+ f"[green]✅ {profile_name}: {total_vpcs} VPCs across {len(regions_with_vpcs)} regions[/green]"
961
+ )
899
962
  elif profile_name == primary_profile:
900
963
  # Always include primary profile even if no VPCs found
901
964
  validated_profiles.append(profile_name)
902
- console.print(f"[yellow]🔑 {profile_name}: Primary profile (included despite no VPCs found)[/yellow]")
965
+ console.print(
966
+ f"[yellow]🔑 {profile_name}: Primary profile (included despite no VPCs found)[/yellow]"
967
+ )
903
968
  else:
904
969
  console.print(f"[dim]⚪ {profile_name}: No VPCs found in {len(regions_to_check)} regions[/dim]")
905
-
970
+
906
971
  except Exception as e:
907
972
  console.print(f"[dim]❌ {profile_name}: Access failed ({str(e)[:50]}...)[/dim]")
908
-
973
+
909
974
  progress.advance(profile_task)
910
-
975
+
911
976
  # Ensure primary profile is included if it was validated
912
977
  if primary_profile and primary_profile not in validated_profiles:
913
978
  try:
914
979
  # Test primary profile separately
915
980
  test_session = boto3.Session(profile_name=primary_profile)
916
- ec2_client = test_session.client('ec2', region_name='us-east-1')
981
+ ec2_client = test_session.client("ec2", region_name="us-east-1")
917
982
  ec2_client.describe_vpcs(MaxResults=1)
918
983
  validated_profiles.insert(0, primary_profile) # Add at front
919
984
  console.print(f"[green]✅ Primary profile {primary_profile} added[/green]")
920
985
  except Exception:
921
986
  console.print(f"[yellow]⚠️ Primary profile {primary_profile} validation failed[/yellow]")
922
-
987
+
923
988
  # Enhanced VPC discovery summary
924
- total_vpcs_found = sum(details.get('total_vpcs', 0) for details in profile_vpc_details.values())
989
+ total_vpcs_found = sum(details.get("total_vpcs", 0) for details in profile_vpc_details.values())
925
990
  console.print(f"[bold green]🎯 VPC Discovery Ready: {len(validated_profiles)} validated profiles[/bold green]")
926
-
991
+
927
992
  if total_vpcs_found > 0:
928
- console.print(f"[bold cyan]📊 Total VPCs discovered: {total_vpcs_found} across {len(profile_vpc_details)} accounts[/bold cyan]")
929
-
993
+ console.print(
994
+ f"[bold cyan]📊 Total VPCs discovered: {total_vpcs_found} across {len(profile_vpc_details)} accounts[/bold cyan]"
995
+ )
996
+
930
997
  # Show detailed breakdown for profiles with VPCs
931
998
  for profile, details in profile_vpc_details.items():
932
- if details['total_vpcs'] > 0:
933
- regions_str = ', '.join(details['regions'])
999
+ if details["total_vpcs"] > 0:
1000
+ regions_str = ", ".join(details["regions"])
934
1001
  console.print(f"[dim] • {profile}: {regions_str}[/dim]")
935
1002
  else:
936
- console.print(f"[yellow]⚠️ No VPCs found across {len(validated_profiles)} profiles - Landing Zone accounts may be empty[/yellow]")
937
-
938
- console.print(f"[dim]Profiles: {', '.join(validated_profiles[:3])}{'...' if len(validated_profiles) > 3 else ''}[/dim]")
939
-
1003
+ console.print(
1004
+ f"[yellow]⚠️ No VPCs found across {len(validated_profiles)} profiles - Landing Zone accounts may be empty[/yellow]"
1005
+ )
1006
+
1007
+ console.print(
1008
+ f"[dim]Profiles: {', '.join(validated_profiles[:3])}{'...' if len(validated_profiles) > 3 else ''}[/dim]"
1009
+ )
1010
+
940
1011
  return validated_profiles
941
-
1012
+
942
1013
  except Exception as e:
943
1014
  console.print(f"[red]❌ Profile discovery failed: {e}[/red]")
944
1015
  console.print(f"[yellow]💡 Falling back to primary profile: {primary_profile}[/yellow]")
@@ -946,33 +1017,25 @@ def _discover_vpc_profiles_from_available_aws_profiles(primary_profile: str) ->
946
1017
 
947
1018
 
948
1019
  def generate_business_report(
949
- profile: Optional[str] = None,
950
- region: str = "us-east-1",
951
- export_formats: Optional[List[str]] = None
1020
+ profile: Optional[str] = None, region: str = "us-east-1", export_formats: Optional[List[str]] = None
952
1021
  ) -> Dict[str, Any]:
953
1022
  """
954
1023
  CLI function to generate business VPC cleanup report
955
-
1024
+
956
1025
  Args:
957
1026
  profile: AWS profile
958
1027
  region: AWS region
959
1028
  export_formats: Export formats
960
-
1029
+
961
1030
  Returns:
962
1031
  Dictionary with business report
963
1032
  """
964
1033
  operational_profile = get_profile_for_operation("operational", profile)
965
-
966
- cleanup_cli = VPCCleanupCLI(
967
- profile=operational_profile,
968
- region=region,
969
- safety_mode=True
970
- )
971
-
1034
+
1035
+ cleanup_cli = VPCCleanupCLI(profile=operational_profile, region=region, safety_mode=True)
1036
+
972
1037
  # First run analysis if no candidates exist
973
1038
  if not cleanup_cli.cleanup_framework.cleanup_candidates:
974
1039
  cleanup_cli.analyze_vpc_cleanup_candidates()
975
-
976
- return cleanup_cli.generate_business_report(
977
- export_formats=export_formats or ['json', 'csv']
978
- )
1040
+
1041
+ return cleanup_cli.generate_business_report(export_formats=export_formats or ["json", "csv"])