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
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  Elastic IP Resource Efficiency Analyzer - Enterprise FinOps Analysis Platform
4
- Strategic Business Focus: Elastic IP resource efficiency optimization for Manager, Financial, and CTO stakeholders
4
+ Strategic Business Focus: Elastic IP resource efficiency optimization for Manager, Financial, and CTO stakeholders
5
5
 
6
6
  Strategic Achievement: Part of $132,720+ annual savings methodology (380-757% ROI achievement)
7
7
  Business Impact: $1.8M-$3.1M annual savings potential across enterprise accounts
8
8
  Technical Foundation: Enterprise-grade Elastic IP discovery and attachment validation
9
9
 
10
10
  This module provides comprehensive Elastic IP resource efficiency analysis following proven FinOps patterns:
11
- - Multi-region Elastic IP discovery across all AWS regions
11
+ - Multi-region Elastic IP discovery across all AWS regions
12
12
  - Instance attachment validation and DNS dependency checking
13
13
  - Cost savings calculation ($3.65/month per unattached EIP)
14
14
  - Safety analysis (ensure EIPs aren't referenced in DNS, load balancers, etc.)
@@ -16,7 +16,7 @@ This module provides comprehensive Elastic IP resource efficiency analysis follo
16
16
 
17
17
  Strategic Alignment:
18
18
  - "Do one thing and do it well": Elastic IP resource efficiency specialization
19
- - "Move Fast, But Not So Fast We Crash": Safety-first analysis approach
19
+ - "Move Fast, But Not So Fast We Crash": Safety-first analysis approach
20
20
  - Enterprise FAANG SDLC: Evidence-based optimization with audit trails
21
21
  - Universal $132K Cost Optimization Methodology: Manager scenarios prioritized over generic patterns
22
22
  """
@@ -32,19 +32,29 @@ import click
32
32
  from botocore.exceptions import ClientError, NoCredentialsError
33
33
  from pydantic import BaseModel, Field
34
34
 
35
+ from ..common.aws_pricing import calculate_annual_cost, get_service_monthly_cost
36
+ from ..common.profile_utils import get_profile_for_operation
35
37
  from ..common.rich_utils import (
36
- console, print_header, print_success, print_error, print_warning, print_info,
37
- create_table, create_progress_bar, format_cost, create_panel, STATUS_INDICATORS
38
+ STATUS_INDICATORS,
39
+ console,
40
+ create_panel,
41
+ create_progress_bar,
42
+ create_table,
43
+ format_cost,
44
+ print_error,
45
+ print_header,
46
+ print_info,
47
+ print_success,
48
+ print_warning,
38
49
  )
39
- from ..common.aws_pricing import get_service_monthly_cost, calculate_annual_cost
40
- from .embedded_mcp_validator import EmbeddedMCPValidator
41
- from ..common.profile_utils import get_profile_for_operation
50
+ from .mcp_validator import EmbeddedMCPValidator
42
51
 
43
52
  logger = logging.getLogger(__name__)
44
53
 
45
54
 
46
55
  class ElasticIPDetails(BaseModel):
47
56
  """Elastic IP details from EC2 API."""
57
+
48
58
  allocation_id: str
49
59
  public_ip: str
50
60
  region: str
@@ -60,6 +70,7 @@ class ElasticIPDetails(BaseModel):
60
70
 
61
71
  class ElasticIPOptimizationResult(BaseModel):
62
72
  """Elastic IP optimization analysis results."""
73
+
63
74
  allocation_id: str
64
75
  public_ip: str
65
76
  region: str
@@ -67,7 +78,7 @@ class ElasticIPOptimizationResult(BaseModel):
67
78
  is_attached: bool
68
79
  instance_id: Optional[str] = None
69
80
  monthly_cost: float = 0.0 # Calculated dynamically per region
70
- annual_cost: float = 0.0 # Calculated dynamically (monthly * 12)
81
+ annual_cost: float = 0.0 # Calculated dynamically (monthly * 12)
71
82
  optimization_recommendation: str = "retain" # retain, release
72
83
  risk_level: str = "low" # low, medium, high
73
84
  business_impact: str = "minimal"
@@ -79,6 +90,7 @@ class ElasticIPOptimizationResult(BaseModel):
79
90
 
80
91
  class ElasticIPOptimizerResults(BaseModel):
81
92
  """Complete Elastic IP optimization analysis results."""
93
+
82
94
  total_elastic_ips: int = 0
83
95
  attached_elastic_ips: int = 0
84
96
  unattached_elastic_ips: int = 0
@@ -96,7 +108,7 @@ class ElasticIPOptimizerResults(BaseModel):
96
108
  class ElasticIPOptimizer:
97
109
  """
98
110
  Elastic IP Resource Efficiency Analyzer - Enterprise FinOps Analysis Engine
99
-
111
+
100
112
  Following $132,720+ methodology with proven FinOps patterns targeting $1.8M-$3.1M annual savings:
101
113
  - Multi-region discovery and analysis across enterprise accounts
102
114
  - Instance attachment validation with safety controls
@@ -105,92 +117,111 @@ class ElasticIPOptimizer:
105
117
  - Evidence generation for Manager/Financial/CTO executive reporting
106
118
  - Business-focused naming for executive presentation readiness
107
119
  """
108
-
120
+
109
121
  def __init__(self, profile_name: Optional[str] = None, regions: Optional[List[str]] = None):
110
122
  """Initialize Elastic IP optimizer with enterprise profile support."""
111
123
  self.profile_name = profile_name
112
124
  self.regions = regions or [
113
- 'us-east-1', 'us-west-2', 'us-east-2', 'us-west-1',
114
- 'eu-west-1', 'eu-central-1', 'ap-southeast-1', 'ap-northeast-1'
125
+ "us-east-1",
126
+ "us-west-2",
127
+ "us-east-2",
128
+ "us-west-1",
129
+ "eu-west-1",
130
+ "eu-central-1",
131
+ "ap-southeast-1",
132
+ "ap-northeast-1",
115
133
  ]
116
-
134
+
117
135
  # Initialize AWS session with profile priority system
118
- self.session = boto3.Session(
119
- profile_name=get_profile_for_operation("operational", profile_name)
120
- )
121
-
136
+ self.session = boto3.Session(profile_name=get_profile_for_operation("operational", profile_name))
137
+
122
138
  # Dynamic Elastic IP pricing - Enterprise compliance (no hardcoded values)
123
139
  # Pricing will be calculated dynamically per region using AWS Pricing API
124
-
140
+
125
141
  # All AWS regions for comprehensive discovery
126
142
  self.all_regions = [
127
- 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
128
- 'af-south-1', 'ap-east-1', 'ap-south-1', 'ap-northeast-1',
129
- 'ap-northeast-2', 'ap-northeast-3', 'ap-southeast-1', 'ap-southeast-2',
130
- 'ca-central-1', 'eu-central-1', 'eu-west-1', 'eu-west-2',
131
- 'eu-west-3', 'eu-south-1', 'eu-north-1', 'me-south-1',
132
- 'sa-east-1'
143
+ "us-east-1",
144
+ "us-east-2",
145
+ "us-west-1",
146
+ "us-west-2",
147
+ "af-south-1",
148
+ "ap-east-1",
149
+ "ap-south-1",
150
+ "ap-northeast-1",
151
+ "ap-northeast-2",
152
+ "ap-northeast-3",
153
+ "ap-southeast-1",
154
+ "ap-southeast-2",
155
+ "ca-central-1",
156
+ "eu-central-1",
157
+ "eu-west-1",
158
+ "eu-west-2",
159
+ "eu-west-3",
160
+ "eu-south-1",
161
+ "eu-north-1",
162
+ "me-south-1",
163
+ "sa-east-1",
133
164
  ]
134
-
165
+
135
166
  async def analyze_elastic_ips(self, dry_run: bool = True) -> ElasticIPOptimizerResults:
136
167
  """
137
168
  Comprehensive Elastic IP cost optimization analysis.
138
-
169
+
139
170
  Args:
140
171
  dry_run: Safety mode - READ-ONLY analysis only
141
-
172
+
142
173
  Returns:
143
174
  Complete analysis results with optimization recommendations
144
175
  """
145
176
  print_header("Elastic IP Resource Efficiency Analyzer", "Enterprise FinOps Analysis Platform v1.0")
146
-
177
+
147
178
  if not dry_run:
148
179
  print_warning("⚠️ Dry-run disabled - This optimizer is READ-ONLY analysis only")
149
180
  print_info("All Elastic IP operations require manual execution after review")
150
-
181
+
151
182
  analysis_start_time = time.time()
152
-
183
+
153
184
  try:
154
185
  with create_progress_bar() as progress:
155
186
  # Step 1: Multi-region Elastic IP discovery
156
187
  discovery_task = progress.add_task("Discovering Elastic IPs...", total=len(self.regions))
157
188
  elastic_ips = await self._discover_elastic_ips_multi_region(progress, discovery_task)
158
-
189
+
159
190
  if not elastic_ips:
160
191
  print_warning("No Elastic IPs found in specified regions")
161
192
  return ElasticIPOptimizerResults(
162
193
  analyzed_regions=self.regions,
163
194
  analysis_timestamp=datetime.now(),
164
- execution_time_seconds=time.time() - analysis_start_time
195
+ execution_time_seconds=time.time() - analysis_start_time,
165
196
  )
166
-
167
- # Step 2: Attachment validation analysis
197
+
198
+ # Step 2: Attachment validation analysis
168
199
  attachment_task = progress.add_task("Validating attachments...", total=len(elastic_ips))
169
200
  validated_elastic_ips = await self._validate_attachments(elastic_ips, progress, attachment_task)
170
-
201
+
171
202
  # Step 3: DNS dependency analysis for safety
172
203
  dns_task = progress.add_task("Checking DNS dependencies...", total=len(elastic_ips))
173
204
  dns_dependencies = await self._analyze_dns_dependencies(validated_elastic_ips, progress, dns_task)
174
-
205
+
175
206
  # Step 4: Cost optimization analysis
176
207
  optimization_task = progress.add_task("Calculating optimization potential...", total=len(elastic_ips))
177
208
  optimization_results = await self._calculate_optimization_recommendations(
178
209
  validated_elastic_ips, dns_dependencies, progress, optimization_task
179
210
  )
180
-
211
+
181
212
  # Step 5: MCP validation
182
213
  validation_task = progress.add_task("MCP validation...", total=1)
183
214
  mcp_accuracy = await self._validate_with_mcp(optimization_results, progress, validation_task)
184
-
215
+
185
216
  # Compile comprehensive results
186
217
  attached_count = sum(1 for result in optimization_results if result.is_attached)
187
218
  unattached_count = len(optimization_results) - attached_count
188
-
219
+
189
220
  total_monthly_cost = sum(result.monthly_cost for result in optimization_results if not result.is_attached)
190
221
  total_annual_cost = total_monthly_cost * 12
191
222
  potential_monthly_savings = sum(result.potential_monthly_savings for result in optimization_results)
192
223
  potential_annual_savings = potential_monthly_savings * 12
193
-
224
+
194
225
  results = ElasticIPOptimizerResults(
195
226
  total_elastic_ips=len(elastic_ips),
196
227
  attached_elastic_ips=attached_count,
@@ -203,185 +234,195 @@ class ElasticIPOptimizer:
203
234
  potential_annual_savings=potential_annual_savings,
204
235
  execution_time_seconds=time.time() - analysis_start_time,
205
236
  mcp_validation_accuracy=mcp_accuracy,
206
- analysis_timestamp=datetime.now()
237
+ analysis_timestamp=datetime.now(),
207
238
  )
208
-
239
+
209
240
  # Display executive summary
210
241
  self._display_executive_summary(results)
211
-
242
+
212
243
  return results
213
-
244
+
214
245
  except Exception as e:
215
246
  print_error(f"Elastic IP optimization analysis failed: {e}")
216
247
  logger.error(f"Elastic IP analysis error: {e}", exc_info=True)
217
248
  raise
218
-
249
+
219
250
  async def _discover_elastic_ips_multi_region(self, progress, task_id) -> List[ElasticIPDetails]:
220
251
  """Discover Elastic IPs across multiple regions."""
221
252
  elastic_ips = []
222
-
253
+
223
254
  for region in self.regions:
224
255
  try:
225
- ec2_client = self.session.client('ec2', region_name=region)
226
-
256
+ ec2_client = self.session.client("ec2", region_name=region)
257
+
227
258
  # Get all Elastic IPs in region
228
259
  response = ec2_client.describe_addresses()
229
-
230
- for address in response.get('Addresses', []):
260
+
261
+ for address in response.get("Addresses", []):
231
262
  # Extract tags
232
- tags = {tag['Key']: tag['Value'] for tag in address.get('Tags', [])}
233
-
263
+ tags = {tag["Key"]: tag["Value"] for tag in address.get("Tags", [])}
264
+
234
265
  # Determine attachment status
235
- is_attached = 'AssociationId' in address
236
-
237
- elastic_ips.append(ElasticIPDetails(
238
- allocation_id=address['AllocationId'],
239
- public_ip=address['PublicIp'],
240
- region=region,
241
- domain=address.get('Domain', 'vpc'),
242
- instance_id=address.get('InstanceId'),
243
- association_id=address.get('AssociationId'),
244
- network_interface_id=address.get('NetworkInterfaceId'),
245
- network_interface_owner_id=address.get('NetworkInterfaceOwnerId'),
246
- private_ip_address=address.get('PrivateIpAddress'),
247
- tags=tags,
248
- is_attached=is_attached
249
- ))
250
-
251
- print_info(f"Region {region}: {len([eip for eip in elastic_ips if eip.region == region])} Elastic IPs discovered")
252
-
266
+ is_attached = "AssociationId" in address
267
+
268
+ elastic_ips.append(
269
+ ElasticIPDetails(
270
+ allocation_id=address["AllocationId"],
271
+ public_ip=address["PublicIp"],
272
+ region=region,
273
+ domain=address.get("Domain", "vpc"),
274
+ instance_id=address.get("InstanceId"),
275
+ association_id=address.get("AssociationId"),
276
+ network_interface_id=address.get("NetworkInterfaceId"),
277
+ network_interface_owner_id=address.get("NetworkInterfaceOwnerId"),
278
+ private_ip_address=address.get("PrivateIpAddress"),
279
+ tags=tags,
280
+ is_attached=is_attached,
281
+ )
282
+ )
283
+
284
+ print_info(
285
+ f"Region {region}: {len([eip for eip in elastic_ips if eip.region == region])} Elastic IPs discovered"
286
+ )
287
+
253
288
  except ClientError as e:
254
289
  print_warning(f"Region {region}: Access denied or region unavailable - {e.response['Error']['Code']}")
255
290
  except Exception as e:
256
291
  print_error(f"Region {region}: Discovery error - {str(e)}")
257
-
292
+
258
293
  progress.advance(task_id)
259
-
294
+
260
295
  return elastic_ips
261
-
262
- async def _validate_attachments(self, elastic_ips: List[ElasticIPDetails], progress, task_id) -> List[ElasticIPDetails]:
296
+
297
+ async def _validate_attachments(
298
+ self, elastic_ips: List[ElasticIPDetails], progress, task_id
299
+ ) -> List[ElasticIPDetails]:
263
300
  """Validate Elastic IP attachments and instance details."""
264
301
  validated_ips = []
265
-
302
+
266
303
  for elastic_ip in elastic_ips:
267
304
  try:
268
305
  # Additional validation for attached EIPs
269
306
  if elastic_ip.is_attached and elastic_ip.instance_id:
270
- ec2_client = self.session.client('ec2', region_name=elastic_ip.region)
271
-
307
+ ec2_client = self.session.client("ec2", region_name=elastic_ip.region)
308
+
272
309
  # Verify instance still exists and is running
273
310
  try:
274
311
  response = ec2_client.describe_instances(InstanceIds=[elastic_ip.instance_id])
275
- instance_found = len(response.get('Reservations', [])) > 0
276
-
312
+ instance_found = len(response.get("Reservations", [])) > 0
313
+
277
314
  if instance_found:
278
- instance = response['Reservations'][0]['Instances'][0]
279
- elastic_ip.is_attached = instance['State']['Name'] in ['running', 'stopped', 'stopping', 'starting']
315
+ instance = response["Reservations"][0]["Instances"][0]
316
+ elastic_ip.is_attached = instance["State"]["Name"] in [
317
+ "running",
318
+ "stopped",
319
+ "stopping",
320
+ "starting",
321
+ ]
280
322
  else:
281
323
  elastic_ip.is_attached = False
282
-
324
+
283
325
  except ClientError:
284
326
  # Instance not found - EIP is effectively unattached
285
327
  elastic_ip.is_attached = False
286
-
328
+
287
329
  validated_ips.append(elastic_ip)
288
-
330
+
289
331
  except Exception as e:
290
332
  print_warning(f"Validation failed for {elastic_ip.public_ip}: {str(e)}")
291
333
  validated_ips.append(elastic_ip) # Add with original status
292
-
334
+
293
335
  progress.advance(task_id)
294
-
336
+
295
337
  return validated_ips
296
-
297
- async def _analyze_dns_dependencies(self, elastic_ips: List[ElasticIPDetails], progress, task_id) -> Dict[str, List[str]]:
338
+
339
+ async def _analyze_dns_dependencies(
340
+ self, elastic_ips: List[ElasticIPDetails], progress, task_id
341
+ ) -> Dict[str, List[str]]:
298
342
  """Analyze potential DNS dependencies for Elastic IPs."""
299
343
  dns_dependencies = {}
300
-
344
+
301
345
  for elastic_ip in elastic_ips:
302
346
  try:
303
347
  dns_refs = []
304
-
348
+
305
349
  # Check Route 53 hosted zones for this IP
306
350
  try:
307
- route53_client = self.session.client('route53')
351
+ route53_client = self.session.client("route53")
308
352
  hosted_zones = route53_client.list_hosted_zones()
309
-
310
- for zone in hosted_zones.get('HostedZones', []):
353
+
354
+ for zone in hosted_zones.get("HostedZones", []):
311
355
  try:
312
- records = route53_client.list_resource_record_sets(
313
- HostedZoneId=zone['Id']
314
- )
315
-
316
- for record in records.get('ResourceRecordSets', []):
317
- if record['Type'] == 'A':
318
- for resource_record in record.get('ResourceRecords', []):
319
- if resource_record.get('Value') == elastic_ip.public_ip:
356
+ records = route53_client.list_resource_record_sets(HostedZoneId=zone["Id"])
357
+
358
+ for record in records.get("ResourceRecordSets", []):
359
+ if record["Type"] == "A":
360
+ for resource_record in record.get("ResourceRecords", []):
361
+ if resource_record.get("Value") == elastic_ip.public_ip:
320
362
  dns_refs.append(f"Route53: {record['Name']} -> {elastic_ip.public_ip}")
321
-
363
+
322
364
  except ClientError:
323
365
  # Zone not accessible or other error - continue
324
366
  pass
325
-
367
+
326
368
  except ClientError:
327
369
  # Route 53 not accessible - skip DNS check
328
370
  pass
329
-
371
+
330
372
  # Check Application Load Balancers (ALB)
331
373
  try:
332
- elbv2_client = self.session.client('elbv2', region_name=elastic_ip.region)
374
+ elbv2_client = self.session.client("elbv2", region_name=elastic_ip.region)
333
375
  load_balancers = elbv2_client.describe_load_balancers()
334
-
335
- for lb in load_balancers.get('LoadBalancers', []):
336
- if elastic_ip.public_ip in lb.get('CanonicalHostedZoneId', ''):
376
+
377
+ for lb in load_balancers.get("LoadBalancers", []):
378
+ if elastic_ip.public_ip in lb.get("CanonicalHostedZoneId", ""):
337
379
  dns_refs.append(f"ALB: {lb['LoadBalancerName']} references EIP")
338
-
380
+
339
381
  except ClientError:
340
382
  # ELB not accessible - skip check
341
383
  pass
342
-
384
+
343
385
  dns_dependencies[elastic_ip.allocation_id] = dns_refs
344
-
386
+
345
387
  except Exception as e:
346
388
  print_warning(f"DNS analysis failed for {elastic_ip.public_ip}: {str(e)}")
347
389
  dns_dependencies[elastic_ip.allocation_id] = []
348
-
390
+
349
391
  progress.advance(task_id)
350
-
392
+
351
393
  return dns_dependencies
352
-
353
- async def _calculate_optimization_recommendations(self,
354
- elastic_ips: List[ElasticIPDetails],
355
- dns_dependencies: Dict[str, List[str]],
356
- progress, task_id) -> List[ElasticIPOptimizationResult]:
394
+
395
+ async def _calculate_optimization_recommendations(
396
+ self, elastic_ips: List[ElasticIPDetails], dns_dependencies: Dict[str, List[str]], progress, task_id
397
+ ) -> List[ElasticIPOptimizationResult]:
357
398
  """Calculate optimization recommendations and potential savings."""
358
399
  optimization_results = []
359
-
400
+
360
401
  for elastic_ip in elastic_ips:
361
402
  try:
362
403
  dns_refs = dns_dependencies.get(elastic_ip.allocation_id, [])
363
-
404
+
364
405
  # Calculate current costs (only unattached EIPs are charged) - Dynamic pricing
365
406
  if elastic_ip.is_attached:
366
407
  monthly_cost = 0.0 # Attached EIPs are free
367
408
  else:
368
409
  monthly_cost = get_service_monthly_cost("elastic_ip", elastic_ip.region)
369
410
  annual_cost = calculate_annual_cost(monthly_cost)
370
-
411
+
371
412
  # Determine optimization recommendation
372
413
  recommendation = "retain" # Default: keep the Elastic IP
373
414
  risk_level = "low"
374
415
  business_impact = "minimal"
375
416
  potential_monthly_savings = 0.0
376
-
417
+
377
418
  # Safety checks
378
419
  safety_checks = {
379
420
  "is_unattached": not elastic_ip.is_attached,
380
421
  "no_dns_references": len(dns_refs) == 0,
381
422
  "no_instance_dependency": elastic_ip.instance_id is None,
382
- "safe_to_release": False
423
+ "safe_to_release": False,
383
424
  }
384
-
425
+
385
426
  if not elastic_ip.is_attached:
386
427
  if not dns_refs:
387
428
  # Unattached with no DNS references - safe to release
@@ -402,71 +443,74 @@ class ElasticIPOptimizer:
402
443
  risk_level = "low"
403
444
  business_impact = "none"
404
445
  potential_monthly_savings = 0.0
405
-
406
- optimization_results.append(ElasticIPOptimizationResult(
407
- allocation_id=elastic_ip.allocation_id,
408
- public_ip=elastic_ip.public_ip,
409
- region=elastic_ip.region,
410
- domain=elastic_ip.domain,
411
- is_attached=elastic_ip.is_attached,
412
- instance_id=elastic_ip.instance_id,
413
- monthly_cost=monthly_cost,
414
- annual_cost=annual_cost,
415
- optimization_recommendation=recommendation,
416
- risk_level=risk_level,
417
- business_impact=business_impact,
418
- potential_monthly_savings=potential_monthly_savings,
419
- potential_annual_savings=potential_monthly_savings * 12,
420
- safety_checks=safety_checks,
421
- dns_references=dns_refs
422
- ))
423
-
446
+
447
+ optimization_results.append(
448
+ ElasticIPOptimizationResult(
449
+ allocation_id=elastic_ip.allocation_id,
450
+ public_ip=elastic_ip.public_ip,
451
+ region=elastic_ip.region,
452
+ domain=elastic_ip.domain,
453
+ is_attached=elastic_ip.is_attached,
454
+ instance_id=elastic_ip.instance_id,
455
+ monthly_cost=monthly_cost,
456
+ annual_cost=annual_cost,
457
+ optimization_recommendation=recommendation,
458
+ risk_level=risk_level,
459
+ business_impact=business_impact,
460
+ potential_monthly_savings=potential_monthly_savings,
461
+ potential_annual_savings=potential_monthly_savings * 12,
462
+ safety_checks=safety_checks,
463
+ dns_references=dns_refs,
464
+ )
465
+ )
466
+
424
467
  except Exception as e:
425
468
  print_error(f"Optimization calculation failed for {elastic_ip.public_ip}: {str(e)}")
426
-
469
+
427
470
  progress.advance(task_id)
428
-
471
+
429
472
  return optimization_results
430
-
431
- async def _validate_with_mcp(self, optimization_results: List[ElasticIPOptimizationResult],
432
- progress, task_id) -> float:
473
+
474
+ async def _validate_with_mcp(
475
+ self, optimization_results: List[ElasticIPOptimizationResult], progress, task_id
476
+ ) -> float:
433
477
  """Validate optimization results with embedded MCP validator."""
434
478
  try:
435
479
  # Prepare validation data in FinOps format
436
480
  validation_data = {
437
- 'total_annual_cost': sum(result.annual_cost for result in optimization_results),
438
- 'potential_annual_savings': sum(result.potential_annual_savings for result in optimization_results),
439
- 'elastic_ips_analyzed': len(optimization_results),
440
- 'regions_analyzed': list(set(result.region for result in optimization_results)),
441
- 'analysis_timestamp': datetime.now().isoformat()
481
+ "total_annual_cost": sum(result.annual_cost for result in optimization_results),
482
+ "potential_annual_savings": sum(result.potential_annual_savings for result in optimization_results),
483
+ "elastic_ips_analyzed": len(optimization_results),
484
+ "regions_analyzed": list(set(result.region for result in optimization_results)),
485
+ "analysis_timestamp": datetime.now().isoformat(),
442
486
  }
443
-
487
+
444
488
  # Initialize MCP validator if profile is available
445
489
  if self.profile_name:
446
490
  mcp_validator = EmbeddedMCPValidator([self.profile_name])
447
491
  validation_results = await mcp_validator.validate_cost_data_async(validation_data)
448
- accuracy = validation_results.get('total_accuracy', 0.0)
449
-
492
+ accuracy = validation_results.get("total_accuracy", 0.0)
493
+
450
494
  if accuracy >= 99.5:
451
495
  print_success(f"MCP Validation: {accuracy:.1f}% accuracy achieved (target: ≥99.5%)")
452
496
  else:
453
497
  print_warning(f"MCP Validation: {accuracy:.1f}% accuracy (target: ≥99.5%)")
454
-
498
+
455
499
  progress.advance(task_id)
456
500
  return accuracy
457
501
  else:
458
502
  print_info("MCP validation skipped - no profile specified")
459
503
  progress.advance(task_id)
460
504
  return 0.0
461
-
505
+
462
506
  except Exception as e:
463
507
  print_warning(f"MCP validation failed: {str(e)}")
464
508
  progress.advance(task_id)
465
509
  return 0.0
466
-
510
+
467
511
  def _display_executive_summary(self, results: ElasticIPOptimizerResults) -> None:
468
512
  """Display executive summary with Rich CLI formatting."""
469
-
513
+
470
514
  # Executive Summary Panel
471
515
  summary_content = f"""
472
516
  💰 Total Annual Cost: {format_cost(results.total_annual_cost)}
@@ -474,22 +518,22 @@ class ElasticIPOptimizer:
474
518
  🎯 Elastic IPs Analyzed: {results.total_elastic_ips}
475
519
  📎 Attached EIPs: {results.attached_elastic_ips}
476
520
  🔓 Unattached EIPs: {results.unattached_elastic_ips}
477
- 🌍 Regions: {', '.join(results.analyzed_regions)}
521
+ 🌍 Regions: {", ".join(results.analyzed_regions)}
478
522
  ⚡ Analysis Time: {results.execution_time_seconds:.2f}s
479
523
  ✅ MCP Accuracy: {results.mcp_validation_accuracy:.1f}%
480
524
  """
481
-
482
- console.print(create_panel(
483
- summary_content.strip(),
484
- title="🏆 Elastic IP Resource Efficiency Analysis Summary",
485
- border_style="green"
486
- ))
487
-
488
- # Detailed Results Table
489
- table = create_table(
490
- title="Elastic IP Optimization Recommendations"
525
+
526
+ console.print(
527
+ create_panel(
528
+ summary_content.strip(),
529
+ title="🏆 Elastic IP Resource Efficiency Analysis Summary",
530
+ border_style="green",
531
+ )
491
532
  )
492
-
533
+
534
+ # Detailed Results Table
535
+ table = create_table(title="Elastic IP Optimization Recommendations")
536
+
493
537
  table.add_column("Elastic IP", style="cyan", no_wrap=True)
494
538
  table.add_column("Region", style="dim")
495
539
  table.add_column("Status", justify="center")
@@ -498,31 +542,21 @@ class ElasticIPOptimizer:
498
542
  table.add_column("Recommendation", justify="center")
499
543
  table.add_column("Risk Level", justify="center")
500
544
  table.add_column("DNS Refs", justify="center", style="dim")
501
-
545
+
502
546
  # Sort by potential savings (descending)
503
- sorted_results = sorted(
504
- results.optimization_results,
505
- key=lambda x: x.potential_annual_savings,
506
- reverse=True
507
- )
508
-
547
+ sorted_results = sorted(results.optimization_results, key=lambda x: x.potential_annual_savings, reverse=True)
548
+
509
549
  for result in sorted_results:
510
550
  # Status indicators
511
551
  status_indicator = "🔗 Attached" if result.is_attached else "🔓 Unattached"
512
-
552
+
513
553
  # Recommendation colors
514
- rec_color = {
515
- "release": "red",
516
- "investigate": "yellow",
517
- "retain": "green"
518
- }.get(result.optimization_recommendation, "white")
519
-
520
- risk_indicator = {
521
- "low": "🟢",
522
- "medium": "🟡",
523
- "high": "🔴"
524
- }.get(result.risk_level, "⚪")
525
-
554
+ rec_color = {"release": "red", "investigate": "yellow", "retain": "green"}.get(
555
+ result.optimization_recommendation, "white"
556
+ )
557
+
558
+ risk_indicator = {"low": "🟢", "medium": "🟡", "high": "🔴"}.get(result.risk_level, "")
559
+
526
560
  table.add_row(
527
561
  result.public_ip,
528
562
  result.region,
@@ -531,11 +565,11 @@ class ElasticIPOptimizer:
531
565
  format_cost(result.potential_annual_savings) if result.potential_annual_savings > 0 else "-",
532
566
  f"[{rec_color}]{result.optimization_recommendation.title()}[/]",
533
567
  f"{risk_indicator} {result.risk_level.title()}",
534
- str(len(result.dns_references))
568
+ str(len(result.dns_references)),
535
569
  )
536
-
570
+
537
571
  console.print(table)
538
-
572
+
539
573
  # Optimization Summary by Recommendation
540
574
  if results.optimization_results:
541
575
  recommendations_summary = {}
@@ -545,64 +579,84 @@ class ElasticIPOptimizer:
545
579
  recommendations_summary[rec] = {"count": 0, "savings": 0.0}
546
580
  recommendations_summary[rec]["count"] += 1
547
581
  recommendations_summary[rec]["savings"] += result.potential_annual_savings
548
-
582
+
549
583
  rec_content = []
550
584
  for rec, data in recommendations_summary.items():
551
- rec_content.append(f"• {rec.title()}: {data['count']} Elastic IPs ({format_cost(data['savings'])} potential savings)")
552
-
553
- console.print(create_panel(
554
- "\n".join(rec_content),
555
- title="📋 Recommendations Summary",
556
- border_style="blue"
557
- ))
558
-
559
- def export_results(self, results: ElasticIPOptimizerResults,
560
- output_file: Optional[str] = None,
561
- export_format: str = "json") -> str:
585
+ rec_content.append(
586
+ f"• {rec.title()}: {data['count']} Elastic IPs ({format_cost(data['savings'])} potential savings)"
587
+ )
588
+
589
+ console.print(create_panel("\n".join(rec_content), title="📋 Recommendations Summary", border_style="blue"))
590
+
591
+ def export_results(
592
+ self, results: ElasticIPOptimizerResults, output_file: Optional[str] = None, export_format: str = "json"
593
+ ) -> str:
562
594
  """
563
595
  Export optimization results to various formats.
564
-
596
+
565
597
  Args:
566
598
  results: Optimization analysis results
567
599
  output_file: Output file path (optional)
568
600
  export_format: Export format (json, csv, markdown)
569
-
601
+
570
602
  Returns:
571
603
  Path to exported file
572
604
  """
573
605
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
574
-
606
+
575
607
  if not output_file:
576
608
  output_file = f"elastic_ip_optimization_{timestamp}.{export_format}"
577
-
609
+
578
610
  try:
579
611
  if export_format.lower() == "json":
580
612
  import json
581
- with open(output_file, 'w') as f:
613
+
614
+ with open(output_file, "w") as f:
582
615
  json.dump(results.dict(), f, indent=2, default=str)
583
-
616
+
584
617
  elif export_format.lower() == "csv":
585
618
  import csv
586
- with open(output_file, 'w', newline='') as f:
619
+
620
+ with open(output_file, "w", newline="") as f:
587
621
  writer = csv.writer(f)
588
- writer.writerow([
589
- 'Allocation ID', 'Public IP', 'Region', 'Domain', 'Attached',
590
- 'Instance ID', 'Monthly Cost', 'Annual Cost',
591
- 'Potential Monthly Savings', 'Potential Annual Savings',
592
- 'Recommendation', 'Risk Level', 'DNS References'
593
- ])
622
+ writer.writerow(
623
+ [
624
+ "Allocation ID",
625
+ "Public IP",
626
+ "Region",
627
+ "Domain",
628
+ "Attached",
629
+ "Instance ID",
630
+ "Monthly Cost",
631
+ "Annual Cost",
632
+ "Potential Monthly Savings",
633
+ "Potential Annual Savings",
634
+ "Recommendation",
635
+ "Risk Level",
636
+ "DNS References",
637
+ ]
638
+ )
594
639
  for result in results.optimization_results:
595
- writer.writerow([
596
- result.allocation_id, result.public_ip, result.region,
597
- result.domain, result.is_attached, result.instance_id or '',
598
- f"${result.monthly_cost:.2f}", f"${result.annual_cost:.2f}",
599
- f"${result.potential_monthly_savings:.2f}", f"${result.potential_annual_savings:.2f}",
600
- result.optimization_recommendation, result.risk_level,
601
- len(result.dns_references)
602
- ])
603
-
640
+ writer.writerow(
641
+ [
642
+ result.allocation_id,
643
+ result.public_ip,
644
+ result.region,
645
+ result.domain,
646
+ result.is_attached,
647
+ result.instance_id or "",
648
+ f"${result.monthly_cost:.2f}",
649
+ f"${result.annual_cost:.2f}",
650
+ f"${result.potential_monthly_savings:.2f}",
651
+ f"${result.potential_annual_savings:.2f}",
652
+ result.optimization_recommendation,
653
+ result.risk_level,
654
+ len(result.dns_references),
655
+ ]
656
+ )
657
+
604
658
  elif export_format.lower() == "markdown":
605
- with open(output_file, 'w') as f:
659
+ with open(output_file, "w") as f:
606
660
  f.write(f"# Elastic IP Cost Optimization Report\n\n")
607
661
  f.write(f"**Analysis Date**: {results.analysis_timestamp}\n")
608
662
  f.write(f"**Total Elastic IPs**: {results.total_elastic_ips}\n")
@@ -611,16 +665,22 @@ class ElasticIPOptimizer:
611
665
  f.write(f"**Total Annual Cost**: ${results.total_annual_cost:.2f}\n")
612
666
  f.write(f"**Potential Annual Savings**: ${results.potential_annual_savings:.2f}\n\n")
613
667
  f.write(f"## Optimization Recommendations\n\n")
614
- f.write(f"| Public IP | Region | Status | Annual Cost | Potential Savings | Recommendation | Risk |\n")
615
- f.write(f"|-----------|--------|--------|-------------|-------------------|----------------|------|\n")
668
+ f.write(
669
+ f"| Public IP | Region | Status | Annual Cost | Potential Savings | Recommendation | Risk |\n"
670
+ )
671
+ f.write(
672
+ f"|-----------|--------|--------|-------------|-------------------|----------------|------|\n"
673
+ )
616
674
  for result in results.optimization_results:
617
675
  status = "Attached" if result.is_attached else "Unattached"
618
676
  f.write(f"| {result.public_ip} | {result.region} | {status} | ${result.annual_cost:.2f} | ")
619
- f.write(f"${result.potential_annual_savings:.2f} | {result.optimization_recommendation} | {result.risk_level} |\n")
620
-
677
+ f.write(
678
+ f"${result.potential_annual_savings:.2f} | {result.optimization_recommendation} | {result.risk_level} |\n"
679
+ )
680
+
621
681
  print_success(f"Results exported to: {output_file}")
622
682
  return output_file
623
-
683
+
624
684
  except Exception as e:
625
685
  print_error(f"Export failed: {str(e)}")
626
686
  raise
@@ -628,20 +688,21 @@ class ElasticIPOptimizer:
628
688
 
629
689
  # CLI Integration for enterprise runbooks commands
630
690
  @click.command()
631
- @click.option('--profile', help='AWS profile name (3-tier priority: User > Environment > Default)')
632
- @click.option('--regions', multiple=True, help='AWS regions to analyze (space-separated)')
633
- @click.option('--dry-run/--no-dry-run', default=True, help='Execute in dry-run mode (READ-ONLY analysis)')
634
- @click.option('--export-format', type=click.Choice(['json', 'csv', 'markdown']),
635
- default='json', help='Export format for results')
636
- @click.option('--output-file', help='Output file path for results export')
691
+ @click.option("--profile", help="AWS profile name (3-tier priority: User > Environment > Default)")
692
+ @click.option("--regions", multiple=True, help="AWS regions to analyze (space-separated)")
693
+ @click.option("--dry-run/--no-dry-run", default=True, help="Execute in dry-run mode (READ-ONLY analysis)")
694
+ @click.option(
695
+ "--export-format", type=click.Choice(["json", "csv", "markdown"]), default="json", help="Export format for results"
696
+ )
697
+ @click.option("--output-file", help="Output file path for results export")
637
698
  def elastic_ip_optimizer(profile, regions, dry_run, export_format, output_file):
638
699
  """
639
700
  Elastic IP Cost Optimizer - Enterprise Multi-Region Analysis
640
-
701
+
641
702
  Part of $132,720+ annual savings methodology targeting direct cost elimination.
642
-
703
+
643
704
  SAFETY: READ-ONLY analysis only - no resource modifications.
644
-
705
+
645
706
  Examples:
646
707
  runbooks finops elastic-ip --cleanup
647
708
  runbooks finops elastic-ip --profile my-profile --regions us-east-1 us-west-2
@@ -649,24 +710,23 @@ def elastic_ip_optimizer(profile, regions, dry_run, export_format, output_file):
649
710
  """
650
711
  try:
651
712
  # Initialize optimizer
652
- optimizer = ElasticIPOptimizer(
653
- profile_name=profile,
654
- regions=list(regions) if regions else None
655
- )
656
-
713
+ optimizer = ElasticIPOptimizer(profile_name=profile, regions=list(regions) if regions else None)
714
+
657
715
  # Execute analysis
658
716
  results = asyncio.run(optimizer.analyze_elastic_ips(dry_run=dry_run))
659
-
717
+
660
718
  # Export results if requested
661
- if output_file or export_format != 'json':
719
+ if output_file or export_format != "json":
662
720
  optimizer.export_results(results, output_file, export_format)
663
-
721
+
664
722
  # Display final success message
665
723
  if results.potential_annual_savings > 0:
666
- print_success(f"Analysis complete: {format_cost(results.potential_annual_savings)} potential annual savings identified")
724
+ print_success(
725
+ f"Analysis complete: {format_cost(results.potential_annual_savings)} potential annual savings identified"
726
+ )
667
727
  else:
668
728
  print_info("Analysis complete: All Elastic IPs are optimally configured")
669
-
729
+
670
730
  except KeyboardInterrupt:
671
731
  print_warning("Analysis interrupted by user")
672
732
  raise click.Abort()
@@ -675,5 +735,5 @@ def elastic_ip_optimizer(profile, regions, dry_run, export_format, output_file):
675
735
  raise click.Abort()
676
736
 
677
737
 
678
- if __name__ == '__main__':
679
- elastic_ip_optimizer()
738
+ if __name__ == "__main__":
739
+ elastic_ip_optimizer()