runbooks 1.1.3__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.
- runbooks/__init__.py +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
- runbooks/cfat/assessment/compliance.py +8 -8
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cfat/models.py +6 -2
- runbooks/cfat/tests/__init__.py +6 -1
- runbooks/cli/__init__.py +13 -0
- runbooks/cli/commands/cfat.py +274 -0
- runbooks/cli/commands/finops.py +1164 -0
- runbooks/cli/commands/inventory.py +379 -0
- runbooks/cli/commands/operate.py +239 -0
- runbooks/cli/commands/security.py +248 -0
- runbooks/cli/commands/validation.py +825 -0
- runbooks/cli/commands/vpc.py +310 -0
- runbooks/cli/registry.py +107 -0
- runbooks/cloudops/__init__.py +23 -30
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +549 -547
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +226 -227
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +179 -215
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +341 -0
- runbooks/common/aws_utils.py +75 -80
- runbooks/common/business_logic.py +127 -105
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
- runbooks/common/cross_account_manager.py +198 -205
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +235 -0
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +478 -495
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +176 -194
- runbooks/common/patterns.py +204 -0
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +248 -39
- runbooks/common/rich_utils.py +643 -92
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +29 -33
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +488 -622
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +40 -37
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +230 -292
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +338 -175
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1513 -482
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +25 -29
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +77 -78
- runbooks/finops/scenarios.py +1278 -439
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/tests/test_finops_dashboard.py +3 -3
- runbooks/finops/tests/test_reference_images_validation.py +2 -2
- runbooks/finops/tests/test_single_account_features.py +17 -17
- runbooks/finops/tests/validate_test_suite.py +1 -1
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +263 -269
- runbooks/finops/vpc_cleanup_exporter.py +191 -146
- runbooks/finops/vpc_cleanup_optimizer.py +593 -575
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/hitl/enhanced_workflow_engine.py +1 -1
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/README.md +3 -3
- runbooks/inventory/Tests/common_test_data.py +30 -30
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +28 -11
- runbooks/inventory/collectors/aws_networking.py +111 -101
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/discovery.md +2 -2
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/find_ec2_security_groups.py +1 -1
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +56 -52
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +733 -696
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +3 -3
- runbooks/main.py +152 -9147
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/metrics/dora_metrics_engine.py +2 -2
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/mcp_integration.py +1 -1
- runbooks/operate/networking_cost_heatmap.py +33 -10
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/operate/vpc_operations.py +648 -618
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +71 -67
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +91 -65
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +49 -44
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/integration_test_enterprise_security.py +5 -3
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/run_script.py +1 -1
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/mcp_reliability_engine.py +6 -6
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +51 -48
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +754 -708
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +190 -162
- runbooks/vpc/mcp_no_eni_validator.py +681 -640
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1302 -1129
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -956
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.3.dist-info/METADATA +0 -799
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.3.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
|
-
|
37
|
-
|
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
|
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
|
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
|
-
|
114
|
-
|
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
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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(
|
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(
|
260
|
+
|
261
|
+
for address in response.get("Addresses", []):
|
231
262
|
# Extract tags
|
232
|
-
tags = {tag[
|
233
|
-
|
263
|
+
tags = {tag["Key"]: tag["Value"] for tag in address.get("Tags", [])}
|
264
|
+
|
234
265
|
# Determine attachment status
|
235
|
-
is_attached =
|
236
|
-
|
237
|
-
elastic_ips.append(
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
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(
|
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(
|
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(
|
276
|
-
|
312
|
+
instance_found = len(response.get("Reservations", [])) > 0
|
313
|
+
|
277
314
|
if instance_found:
|
278
|
-
instance = response[
|
279
|
-
elastic_ip.is_attached = instance[
|
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(
|
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(
|
351
|
+
route53_client = self.session.client("route53")
|
308
352
|
hosted_zones = route53_client.list_hosted_zones()
|
309
|
-
|
310
|
-
for zone in hosted_zones.get(
|
353
|
+
|
354
|
+
for zone in hosted_zones.get("HostedZones", []):
|
311
355
|
try:
|
312
|
-
records = route53_client.list_resource_record_sets(
|
313
|
-
|
314
|
-
)
|
315
|
-
|
316
|
-
|
317
|
-
|
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(
|
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(
|
336
|
-
if elastic_ip.public_ip in lb.get(
|
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(
|
354
|
-
|
355
|
-
|
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(
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
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(
|
432
|
-
|
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
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
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(
|
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: {
|
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(
|
483
|
-
|
484
|
-
|
485
|
-
|
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
|
-
|
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
|
-
|
516
|
-
|
517
|
-
|
518
|
-
}.get(result.
|
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(
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
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
|
-
|
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
|
-
|
619
|
+
|
620
|
+
with open(output_file, "w", newline="") as f:
|
587
621
|
writer = csv.writer(f)
|
588
|
-
writer.writerow(
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
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
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
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,
|
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(
|
615
|
-
|
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(
|
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(
|
632
|
-
@click.option(
|
633
|
-
@click.option(
|
634
|
-
@click.option(
|
635
|
-
|
636
|
-
|
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
|
-
|
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 !=
|
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(
|
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__ ==
|
679
|
-
elastic_ip_optimizer()
|
738
|
+
if __name__ == "__main__":
|
739
|
+
elastic_ip_optimizer()
|