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.
- 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/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +138 -35
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- 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 +177 -213
- 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 +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- 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 +476 -493
- 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 +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +201 -141
- 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 +26 -30
- 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 +484 -618
- 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 +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- 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 +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- 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 +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/drift_detection_cli.py +69 -96
- 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 +55 -51
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +732 -695
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- 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/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/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- 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/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- 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/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 +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- 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 +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- 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 +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/RECORD +214 -193
- 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 -973
- 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.4.dist-info/METADATA +0 -800
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,7 @@ Technical Foundation: Enterprise-grade RI analysis across EC2, RDS, ElastiCache,
|
|
9
9
|
|
10
10
|
This module provides comprehensive Reserved Instance optimization analysis following proven FinOps patterns:
|
11
11
|
- Multi-service resource analysis (EC2, RDS, ElastiCache, Redshift, OpenSearch)
|
12
|
-
- Historical usage pattern analysis for RI sizing recommendations
|
12
|
+
- Historical usage pattern analysis for RI sizing recommendations
|
13
13
|
- Financial modeling with break-even analysis and ROI calculations
|
14
14
|
- Coverage optimization across different RI terms and payment options
|
15
15
|
- Cross-account RI sharing strategy for enterprise organizations
|
@@ -25,43 +25,55 @@ Strategic Alignment:
|
|
25
25
|
import asyncio
|
26
26
|
import logging
|
27
27
|
import time
|
28
|
-
from datetime import datetime, timedelta
|
29
|
-
from typing import Any, Dict, List, Optional, Tuple
|
30
28
|
from dataclasses import dataclass
|
29
|
+
from datetime import datetime, timedelta
|
31
30
|
from enum import Enum
|
31
|
+
from typing import Any, Dict, List, Optional, Tuple
|
32
32
|
|
33
33
|
import boto3
|
34
34
|
import click
|
35
35
|
from botocore.exceptions import ClientError, NoCredentialsError
|
36
36
|
from pydantic import BaseModel, Field
|
37
37
|
|
38
|
+
from ..common.profile_utils import get_profile_for_operation
|
38
39
|
from ..common.rich_utils import (
|
39
|
-
|
40
|
-
|
40
|
+
STATUS_INDICATORS,
|
41
|
+
console,
|
42
|
+
create_panel,
|
43
|
+
create_progress_bar,
|
44
|
+
create_table,
|
45
|
+
format_cost,
|
46
|
+
print_error,
|
47
|
+
print_header,
|
48
|
+
print_info,
|
49
|
+
print_success,
|
50
|
+
print_warning,
|
41
51
|
)
|
42
|
-
from .
|
43
|
-
from ..common.profile_utils import get_profile_for_operation
|
52
|
+
from .mcp_validator import EmbeddedMCPValidator
|
44
53
|
|
45
54
|
logger = logging.getLogger(__name__)
|
46
55
|
|
47
56
|
|
48
57
|
class RIService(str, Enum):
|
49
58
|
"""AWS services that support Reserved Instances."""
|
59
|
+
|
50
60
|
EC2 = "ec2"
|
51
61
|
RDS = "rds"
|
52
62
|
ELASTICACHE = "elasticache"
|
53
63
|
REDSHIFT = "redshift"
|
54
64
|
OPENSEARCH = "opensearch"
|
55
|
-
|
65
|
+
|
56
66
|
|
57
67
|
class RITerm(str, Enum):
|
58
68
|
"""Reserved Instance term lengths."""
|
69
|
+
|
59
70
|
ONE_YEAR = "1yr"
|
60
71
|
THREE_YEAR = "3yr"
|
61
72
|
|
62
73
|
|
63
74
|
class RIPaymentOption(str, Enum):
|
64
75
|
"""Reserved Instance payment options."""
|
76
|
+
|
65
77
|
NO_UPFRONT = "no_upfront"
|
66
78
|
PARTIAL_UPFRONT = "partial_upfront"
|
67
79
|
ALL_UPFRONT = "all_upfront"
|
@@ -69,27 +81,28 @@ class RIPaymentOption(str, Enum):
|
|
69
81
|
|
70
82
|
class ResourceUsagePattern(BaseModel):
|
71
83
|
"""Resource usage pattern analysis for RI recommendations."""
|
84
|
+
|
72
85
|
resource_id: str
|
73
86
|
resource_type: str # instance_type, db_instance_class, node_type, etc.
|
74
87
|
service: RIService
|
75
88
|
region: str
|
76
89
|
availability_zone: Optional[str] = None
|
77
|
-
|
90
|
+
|
78
91
|
# Usage statistics over analysis period
|
79
92
|
total_hours_running: float = 0.0
|
80
93
|
average_daily_hours: float = 0.0
|
81
94
|
usage_consistency_score: float = 0.0 # 0-1 consistency score
|
82
95
|
seasonal_variation: float = 0.0 # 0-1 seasonal variation
|
83
|
-
|
96
|
+
|
84
97
|
# Current pricing
|
85
98
|
on_demand_hourly_rate: float = 0.0
|
86
99
|
current_monthly_cost: float = 0.0
|
87
100
|
current_annual_cost: float = 0.0
|
88
|
-
|
101
|
+
|
89
102
|
# RI Suitability scoring
|
90
103
|
ri_suitability_score: float = 0.0 # 0-100 RI recommendation score
|
91
104
|
minimum_usage_threshold: float = 0.7 # 70% usage required for RI recommendation
|
92
|
-
|
105
|
+
|
93
106
|
analysis_period_days: int = 90
|
94
107
|
platform: Optional[str] = None # windows, linux for EC2
|
95
108
|
engine: Optional[str] = None # mysql, postgres for RDS
|
@@ -98,35 +111,36 @@ class ResourceUsagePattern(BaseModel):
|
|
98
111
|
|
99
112
|
class RIRecommendation(BaseModel):
|
100
113
|
"""Reserved Instance purchase recommendation."""
|
114
|
+
|
101
115
|
resource_type: str
|
102
116
|
service: RIService
|
103
117
|
region: str
|
104
118
|
availability_zone: Optional[str] = None
|
105
119
|
platform: Optional[str] = None
|
106
|
-
|
120
|
+
|
107
121
|
# Recommendation details
|
108
122
|
recommended_quantity: int = 1
|
109
123
|
ri_term: RITerm = RITerm.ONE_YEAR
|
110
124
|
payment_option: RIPaymentOption = RIPaymentOption.PARTIAL_UPFRONT
|
111
|
-
|
125
|
+
|
112
126
|
# Financial analysis
|
113
127
|
ri_upfront_cost: float = 0.0
|
114
128
|
ri_hourly_rate: float = 0.0
|
115
129
|
ri_effective_hourly_rate: float = 0.0 # Including upfront amortized
|
116
130
|
on_demand_hourly_rate: float = 0.0
|
117
|
-
|
131
|
+
|
118
132
|
# Savings analysis
|
119
133
|
break_even_months: float = 0.0
|
120
134
|
first_year_savings: float = 0.0
|
121
135
|
total_term_savings: float = 0.0
|
122
136
|
annual_savings: float = 0.0
|
123
137
|
roi_percentage: float = 0.0
|
124
|
-
|
138
|
+
|
125
139
|
# Risk assessment
|
126
140
|
utilization_confidence: float = 0.0 # 0-1 confidence in utilization
|
127
141
|
risk_level: str = "low" # low, medium, high
|
128
142
|
flexibility_impact: str = "minimal" # minimal, moderate, significant
|
129
|
-
|
143
|
+
|
130
144
|
# Supporting resources
|
131
145
|
covered_resources: List[str] = Field(default_factory=list)
|
132
146
|
usage_justification: str = ""
|
@@ -134,30 +148,31 @@ class RIRecommendation(BaseModel):
|
|
134
148
|
|
135
149
|
class RIOptimizerResults(BaseModel):
|
136
150
|
"""Complete Reserved Instance optimization analysis results."""
|
151
|
+
|
137
152
|
analyzed_services: List[RIService] = Field(default_factory=list)
|
138
153
|
analyzed_regions: List[str] = Field(default_factory=list)
|
139
|
-
|
154
|
+
|
140
155
|
# Resource analysis summary
|
141
156
|
total_resources_analyzed: int = 0
|
142
157
|
ri_suitable_resources: int = 0
|
143
158
|
current_ri_coverage: float = 0.0 # % of resources already covered by RIs
|
144
|
-
|
159
|
+
|
145
160
|
# Financial summary
|
146
161
|
total_current_on_demand_cost: float = 0.0
|
147
162
|
total_potential_ri_cost: float = 0.0
|
148
163
|
total_annual_savings: float = 0.0
|
149
164
|
total_upfront_investment: float = 0.0
|
150
165
|
portfolio_roi: float = 0.0
|
151
|
-
|
166
|
+
|
152
167
|
# Recommendations
|
153
168
|
ri_recommendations: List[RIRecommendation] = Field(default_factory=list)
|
154
|
-
|
169
|
+
|
155
170
|
# Service breakdown
|
156
171
|
ec2_recommendations: List[RIRecommendation] = Field(default_factory=list)
|
157
172
|
rds_recommendations: List[RIRecommendation] = Field(default_factory=list)
|
158
173
|
elasticache_recommendations: List[RIRecommendation] = Field(default_factory=list)
|
159
174
|
redshift_recommendations: List[RIRecommendation] = Field(default_factory=list)
|
160
|
-
|
175
|
+
|
161
176
|
execution_time_seconds: float = 0.0
|
162
177
|
mcp_validation_accuracy: float = 0.0
|
163
178
|
analysis_timestamp: datetime = Field(default_factory=datetime.now)
|
@@ -166,7 +181,7 @@ class RIOptimizerResults(BaseModel):
|
|
166
181
|
class ReservationOptimizer:
|
167
182
|
"""
|
168
183
|
Reserved Instance Optimization Platform - Enterprise FinOps RI Strategy Engine
|
169
|
-
|
184
|
+
|
170
185
|
Following $132,720+ methodology with proven FinOps patterns targeting $3.2M-$17M annual savings:
|
171
186
|
- Multi-service resource discovery and usage analysis
|
172
187
|
- Historical usage pattern analysis for accurate RI sizing
|
@@ -176,119 +191,128 @@ class ReservationOptimizer:
|
|
176
191
|
- Evidence generation for Manager/Financial/CTO executive reporting
|
177
192
|
- Business-focused RI procurement strategy for enterprise budgeting
|
178
193
|
"""
|
179
|
-
|
194
|
+
|
180
195
|
def __init__(self, profile_name: Optional[str] = None, regions: Optional[List[str]] = None):
|
181
196
|
"""Initialize RI optimizer with enterprise profile support."""
|
182
197
|
self.profile_name = profile_name
|
183
|
-
self.regions = regions or [
|
184
|
-
|
198
|
+
self.regions = regions or ["us-east-1", "us-west-2", "eu-west-1"]
|
199
|
+
|
185
200
|
# Initialize AWS session with profile priority system
|
186
|
-
self.session = boto3.Session(
|
187
|
-
|
188
|
-
)
|
189
|
-
|
201
|
+
self.session = boto3.Session(profile_name=get_profile_for_operation("operational", profile_name))
|
202
|
+
|
190
203
|
# RI analysis parameters
|
191
204
|
self.analysis_period_days = 90 # 3 months usage analysis
|
192
205
|
self.minimum_usage_threshold = 0.75 # 75% usage required for RI recommendation
|
193
206
|
self.break_even_target_months = 10 # Target break-even within 10 months
|
194
|
-
|
207
|
+
|
195
208
|
# Service-specific pricing configurations (approximate 2024 rates)
|
196
209
|
self.service_pricing = {
|
197
210
|
RIService.EC2: {
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
211
|
+
"m5.large": {"on_demand": 0.096, "ri_1yr_partial": {"upfront": 550, "hourly": 0.055}},
|
212
|
+
"m5.xlarge": {"on_demand": 0.192, "ri_1yr_partial": {"upfront": 1100, "hourly": 0.11}},
|
213
|
+
"m5.2xlarge": {"on_demand": 0.384, "ri_1yr_partial": {"upfront": 2200, "hourly": 0.22}},
|
214
|
+
"c5.large": {"on_demand": 0.085, "ri_1yr_partial": {"upfront": 500, "hourly": 0.048}},
|
215
|
+
"c5.xlarge": {"on_demand": 0.17, "ri_1yr_partial": {"upfront": 1000, "hourly": 0.096}},
|
216
|
+
"r5.large": {"on_demand": 0.126, "ri_1yr_partial": {"upfront": 720, "hourly": 0.072}},
|
217
|
+
"r5.xlarge": {"on_demand": 0.252, "ri_1yr_partial": {"upfront": 1440, "hourly": 0.144}},
|
205
218
|
},
|
206
219
|
RIService.RDS: {
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
220
|
+
"db.t3.medium": {"on_demand": 0.068, "ri_1yr_partial": {"upfront": 390, "hourly": 0.038}},
|
221
|
+
"db.m5.large": {"on_demand": 0.192, "ri_1yr_partial": {"upfront": 1100, "hourly": 0.11}},
|
222
|
+
"db.m5.xlarge": {"on_demand": 0.384, "ri_1yr_partial": {"upfront": 2200, "hourly": 0.22}},
|
223
|
+
"db.r5.large": {"on_demand": 0.24, "ri_1yr_partial": {"upfront": 1370, "hourly": 0.135}},
|
224
|
+
"db.r5.xlarge": {"on_demand": 0.48, "ri_1yr_partial": {"upfront": 2740, "hourly": 0.27}},
|
212
225
|
},
|
213
226
|
RIService.ELASTICACHE: {
|
214
|
-
|
215
|
-
|
216
|
-
}
|
227
|
+
"cache.m5.large": {"on_demand": 0.136, "ri_1yr_partial": {"upfront": 780, "hourly": 0.077}},
|
228
|
+
"cache.r5.large": {"on_demand": 0.188, "ri_1yr_partial": {"upfront": 1075, "hourly": 0.106}},
|
229
|
+
},
|
217
230
|
}
|
218
|
-
|
219
|
-
async def analyze_reservation_opportunities(
|
231
|
+
|
232
|
+
async def analyze_reservation_opportunities(
|
233
|
+
self, services: List[RIService] = None, dry_run: bool = True
|
234
|
+
) -> RIOptimizerResults:
|
220
235
|
"""
|
221
236
|
Comprehensive Reserved Instance optimization analysis across AWS services.
|
222
|
-
|
237
|
+
|
223
238
|
Args:
|
224
239
|
services: List of AWS services to analyze (None = all supported services)
|
225
240
|
dry_run: Safety mode - READ-ONLY analysis only
|
226
|
-
|
241
|
+
|
227
242
|
Returns:
|
228
243
|
Complete analysis results with RI recommendations and financial modeling
|
229
244
|
"""
|
230
245
|
print_header("Reserved Instance Optimization Platform", "Enterprise Multi-Service RI Strategy Engine v1.0")
|
231
|
-
|
246
|
+
|
232
247
|
if not dry_run:
|
233
248
|
print_warning("⚠️ Dry-run disabled - This optimizer is READ-ONLY analysis only")
|
234
249
|
print_info("All RI procurement decisions require manual execution after review")
|
235
|
-
|
250
|
+
|
236
251
|
analysis_start_time = time.time()
|
237
252
|
services_to_analyze = services or [RIService.EC2, RIService.RDS, RIService.ELASTICACHE]
|
238
|
-
|
253
|
+
|
239
254
|
try:
|
240
255
|
with create_progress_bar() as progress:
|
241
256
|
# Step 1: Multi-service resource discovery
|
242
|
-
discovery_task = progress.add_task(
|
243
|
-
|
244
|
-
|
245
|
-
|
257
|
+
discovery_task = progress.add_task(
|
258
|
+
"Discovering resources across services...", total=len(services_to_analyze) * len(self.regions)
|
259
|
+
)
|
260
|
+
usage_patterns = await self._discover_resources_multi_service(
|
261
|
+
services_to_analyze, progress, discovery_task
|
262
|
+
)
|
263
|
+
|
246
264
|
if not usage_patterns:
|
247
265
|
print_warning("No suitable resources found for RI analysis")
|
248
266
|
return RIOptimizerResults(
|
249
267
|
analyzed_services=services_to_analyze,
|
250
268
|
analyzed_regions=self.regions,
|
251
269
|
analysis_timestamp=datetime.now(),
|
252
|
-
execution_time_seconds=time.time() - analysis_start_time
|
270
|
+
execution_time_seconds=time.time() - analysis_start_time,
|
253
271
|
)
|
254
|
-
|
272
|
+
|
255
273
|
# Step 2: Usage pattern analysis
|
256
274
|
usage_task = progress.add_task("Analyzing usage patterns...", total=len(usage_patterns))
|
257
275
|
analyzed_patterns = await self._analyze_usage_patterns(usage_patterns, progress, usage_task)
|
258
|
-
|
276
|
+
|
259
277
|
# Step 3: RI suitability assessment
|
260
278
|
suitability_task = progress.add_task("Assessing RI suitability...", total=len(analyzed_patterns))
|
261
279
|
suitable_resources = await self._assess_ri_suitability(analyzed_patterns, progress, suitability_task)
|
262
|
-
|
280
|
+
|
263
281
|
# Step 4: Financial modeling and recommendations
|
264
282
|
modeling_task = progress.add_task("Financial modeling...", total=len(suitable_resources))
|
265
283
|
recommendations = await self._generate_ri_recommendations(suitable_resources, progress, modeling_task)
|
266
|
-
|
284
|
+
|
267
285
|
# Step 5: Portfolio optimization
|
268
286
|
optimization_task = progress.add_task("Optimizing RI portfolio...", total=1)
|
269
|
-
optimized_recommendations = await self._optimize_ri_portfolio(
|
270
|
-
|
287
|
+
optimized_recommendations = await self._optimize_ri_portfolio(
|
288
|
+
recommendations, progress, optimization_task
|
289
|
+
)
|
290
|
+
|
271
291
|
# Step 6: MCP validation
|
272
292
|
validation_task = progress.add_task("MCP validation...", total=1)
|
273
293
|
mcp_accuracy = await self._validate_with_mcp(optimized_recommendations, progress, validation_task)
|
274
|
-
|
294
|
+
|
275
295
|
# Compile comprehensive results
|
276
|
-
results = self._compile_results(
|
277
|
-
|
296
|
+
results = self._compile_results(
|
297
|
+
usage_patterns, optimized_recommendations, mcp_accuracy, analysis_start_time, services_to_analyze
|
298
|
+
)
|
299
|
+
|
278
300
|
# Display executive summary
|
279
301
|
self._display_executive_summary(results)
|
280
|
-
|
302
|
+
|
281
303
|
return results
|
282
|
-
|
304
|
+
|
283
305
|
except Exception as e:
|
284
306
|
print_error(f"Reserved Instance optimization analysis failed: {e}")
|
285
307
|
logger.error(f"RI analysis error: {e}", exc_info=True)
|
286
308
|
raise
|
287
|
-
|
288
|
-
async def _discover_resources_multi_service(
|
309
|
+
|
310
|
+
async def _discover_resources_multi_service(
|
311
|
+
self, services: List[RIService], progress, task_id
|
312
|
+
) -> List[ResourceUsagePattern]:
|
289
313
|
"""Discover resources across multiple AWS services for RI analysis."""
|
290
314
|
usage_patterns = []
|
291
|
-
|
315
|
+
|
292
316
|
for service in services:
|
293
317
|
for region in self.regions:
|
294
318
|
try:
|
@@ -304,272 +328,294 @@ class ReservationOptimizer:
|
|
304
328
|
elif service == RIService.REDSHIFT:
|
305
329
|
patterns = await self._discover_redshift_resources(region)
|
306
330
|
usage_patterns.extend(patterns)
|
307
|
-
|
308
|
-
print_info(
|
309
|
-
|
331
|
+
|
332
|
+
print_info(
|
333
|
+
f"Service {service.value} in {region}: {len([p for p in usage_patterns if p.region == region and p.service == service])} resources discovered"
|
334
|
+
)
|
335
|
+
|
310
336
|
except ClientError as e:
|
311
337
|
print_warning(f"Service {service.value} in {region}: Access denied - {e.response['Error']['Code']}")
|
312
338
|
except Exception as e:
|
313
339
|
print_error(f"Service {service.value} in {region}: Discovery error - {str(e)}")
|
314
|
-
|
340
|
+
|
315
341
|
progress.advance(task_id)
|
316
|
-
|
342
|
+
|
317
343
|
return usage_patterns
|
318
|
-
|
344
|
+
|
319
345
|
async def _discover_ec2_resources(self, region: str) -> List[ResourceUsagePattern]:
|
320
346
|
"""Discover EC2 instances for RI analysis."""
|
321
347
|
patterns = []
|
322
|
-
|
348
|
+
|
323
349
|
try:
|
324
|
-
ec2_client = self.session.client(
|
325
|
-
|
326
|
-
paginator = ec2_client.get_paginator(
|
350
|
+
ec2_client = self.session.client("ec2", region_name=region)
|
351
|
+
|
352
|
+
paginator = ec2_client.get_paginator("describe_instances")
|
327
353
|
page_iterator = paginator.paginate()
|
328
|
-
|
354
|
+
|
329
355
|
for page in page_iterator:
|
330
|
-
for reservation in page.get(
|
331
|
-
for instance in reservation.get(
|
356
|
+
for reservation in page.get("Reservations", []):
|
357
|
+
for instance in reservation.get("Instances", []):
|
332
358
|
# Skip terminated instances
|
333
|
-
if instance.get(
|
359
|
+
if instance.get("State", {}).get("Name") == "terminated":
|
334
360
|
continue
|
335
|
-
|
361
|
+
|
336
362
|
# Extract tags
|
337
|
-
tags = {tag[
|
338
|
-
|
363
|
+
tags = {tag["Key"]: tag["Value"] for tag in instance.get("Tags", [])}
|
364
|
+
|
339
365
|
# Get pricing information
|
340
|
-
instance_type = instance[
|
366
|
+
instance_type = instance["InstanceType"]
|
341
367
|
pricing = self.service_pricing.get(RIService.EC2, {}).get(instance_type, {})
|
342
|
-
on_demand_rate = pricing.get(
|
343
|
-
|
344
|
-
patterns.append(
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
368
|
+
on_demand_rate = pricing.get("on_demand", 0.1) # Default fallback
|
369
|
+
|
370
|
+
patterns.append(
|
371
|
+
ResourceUsagePattern(
|
372
|
+
resource_id=instance["InstanceId"],
|
373
|
+
resource_type=instance_type,
|
374
|
+
service=RIService.EC2,
|
375
|
+
region=region,
|
376
|
+
availability_zone=instance["Placement"]["AvailabilityZone"],
|
377
|
+
on_demand_hourly_rate=on_demand_rate,
|
378
|
+
platform=instance.get("Platform", "linux"),
|
379
|
+
tags=tags,
|
380
|
+
analysis_period_days=self.analysis_period_days,
|
381
|
+
)
|
382
|
+
)
|
383
|
+
|
356
384
|
except Exception as e:
|
357
385
|
logger.warning(f"EC2 discovery failed in {region}: {e}")
|
358
|
-
|
386
|
+
|
359
387
|
return patterns
|
360
|
-
|
388
|
+
|
361
389
|
async def _discover_rds_resources(self, region: str) -> List[ResourceUsagePattern]:
|
362
390
|
"""Discover RDS instances for RI analysis."""
|
363
391
|
patterns = []
|
364
|
-
|
392
|
+
|
365
393
|
try:
|
366
|
-
rds_client = self.session.client(
|
367
|
-
|
368
|
-
paginator = rds_client.get_paginator(
|
394
|
+
rds_client = self.session.client("rds", region_name=region)
|
395
|
+
|
396
|
+
paginator = rds_client.get_paginator("describe_db_instances")
|
369
397
|
page_iterator = paginator.paginate()
|
370
|
-
|
398
|
+
|
371
399
|
for page in page_iterator:
|
372
|
-
for db_instance in page.get(
|
400
|
+
for db_instance in page.get("DBInstances", []):
|
373
401
|
# Skip instances that are not running/available
|
374
|
-
if db_instance.get(
|
402
|
+
if db_instance.get("DBInstanceStatus") not in ["available", "storage-optimization"]:
|
375
403
|
continue
|
376
|
-
|
404
|
+
|
377
405
|
# Get pricing information
|
378
|
-
instance_class = db_instance[
|
406
|
+
instance_class = db_instance["DBInstanceClass"]
|
379
407
|
pricing = self.service_pricing.get(RIService.RDS, {}).get(instance_class, {})
|
380
|
-
on_demand_rate = pricing.get(
|
381
|
-
|
382
|
-
patterns.append(
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
408
|
+
on_demand_rate = pricing.get("on_demand", 0.2) # Default fallback
|
409
|
+
|
410
|
+
patterns.append(
|
411
|
+
ResourceUsagePattern(
|
412
|
+
resource_id=db_instance["DBInstanceIdentifier"],
|
413
|
+
resource_type=instance_class,
|
414
|
+
service=RIService.RDS,
|
415
|
+
region=region,
|
416
|
+
availability_zone=db_instance.get("AvailabilityZone"),
|
417
|
+
on_demand_hourly_rate=on_demand_rate,
|
418
|
+
engine=db_instance.get("Engine"),
|
419
|
+
analysis_period_days=self.analysis_period_days,
|
420
|
+
)
|
421
|
+
)
|
422
|
+
|
393
423
|
except Exception as e:
|
394
424
|
logger.warning(f"RDS discovery failed in {region}: {e}")
|
395
|
-
|
425
|
+
|
396
426
|
return patterns
|
397
|
-
|
427
|
+
|
398
428
|
async def _discover_elasticache_resources(self, region: str) -> List[ResourceUsagePattern]:
|
399
429
|
"""Discover ElastiCache clusters for RI analysis."""
|
400
430
|
patterns = []
|
401
|
-
|
431
|
+
|
402
432
|
try:
|
403
|
-
elasticache_client = self.session.client(
|
404
|
-
|
433
|
+
elasticache_client = self.session.client("elasticache", region_name=region)
|
434
|
+
|
405
435
|
# Discover Redis clusters
|
406
436
|
response = elasticache_client.describe_cache_clusters()
|
407
|
-
for cluster in response.get(
|
408
|
-
if cluster.get(
|
437
|
+
for cluster in response.get("CacheClusters", []):
|
438
|
+
if cluster.get("CacheClusterStatus") != "available":
|
409
439
|
continue
|
410
|
-
|
411
|
-
node_type = cluster.get(
|
440
|
+
|
441
|
+
node_type = cluster.get("CacheNodeType")
|
412
442
|
pricing = self.service_pricing.get(RIService.ELASTICACHE, {}).get(node_type, {})
|
413
|
-
on_demand_rate = pricing.get(
|
414
|
-
|
415
|
-
patterns.append(
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
443
|
+
on_demand_rate = pricing.get("on_demand", 0.15) # Default fallback
|
444
|
+
|
445
|
+
patterns.append(
|
446
|
+
ResourceUsagePattern(
|
447
|
+
resource_id=cluster["CacheClusterId"],
|
448
|
+
resource_type=node_type,
|
449
|
+
service=RIService.ELASTICACHE,
|
450
|
+
region=region,
|
451
|
+
on_demand_hourly_rate=on_demand_rate,
|
452
|
+
engine=cluster.get("Engine"),
|
453
|
+
analysis_period_days=self.analysis_period_days,
|
454
|
+
)
|
455
|
+
)
|
456
|
+
|
425
457
|
except Exception as e:
|
426
458
|
logger.warning(f"ElastiCache discovery failed in {region}: {e}")
|
427
|
-
|
459
|
+
|
428
460
|
return patterns
|
429
|
-
|
461
|
+
|
430
462
|
async def _discover_redshift_resources(self, region: str) -> List[ResourceUsagePattern]:
|
431
463
|
"""Discover Redshift clusters for RI analysis."""
|
432
464
|
patterns = []
|
433
|
-
|
465
|
+
|
434
466
|
try:
|
435
|
-
redshift_client = self.session.client(
|
436
|
-
|
467
|
+
redshift_client = self.session.client("redshift", region_name=region)
|
468
|
+
|
437
469
|
response = redshift_client.describe_clusters()
|
438
|
-
for cluster in response.get(
|
439
|
-
if cluster.get(
|
470
|
+
for cluster in response.get("Clusters", []):
|
471
|
+
if cluster.get("ClusterStatus") != "available":
|
440
472
|
continue
|
441
|
-
|
442
|
-
node_type = cluster.get(
|
473
|
+
|
474
|
+
node_type = cluster.get("NodeType")
|
443
475
|
# Redshift pricing is more complex, using simplified estimate
|
444
476
|
on_demand_rate = 0.25 # Approximate rate per node per hour
|
445
|
-
|
446
|
-
patterns.append(
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
477
|
+
|
478
|
+
patterns.append(
|
479
|
+
ResourceUsagePattern(
|
480
|
+
resource_id=cluster["ClusterIdentifier"],
|
481
|
+
resource_type=node_type,
|
482
|
+
service=RIService.REDSHIFT,
|
483
|
+
region=region,
|
484
|
+
on_demand_hourly_rate=on_demand_rate,
|
485
|
+
analysis_period_days=self.analysis_period_days,
|
486
|
+
)
|
487
|
+
)
|
488
|
+
|
455
489
|
except Exception as e:
|
456
490
|
logger.warning(f"Redshift discovery failed in {region}: {e}")
|
457
|
-
|
491
|
+
|
458
492
|
return patterns
|
459
|
-
|
460
|
-
async def _analyze_usage_patterns(
|
493
|
+
|
494
|
+
async def _analyze_usage_patterns(
|
495
|
+
self, patterns: List[ResourceUsagePattern], progress, task_id
|
496
|
+
) -> List[ResourceUsagePattern]:
|
461
497
|
"""Analyze resource usage patterns via CloudWatch metrics."""
|
462
498
|
analyzed_patterns = []
|
463
499
|
end_time = datetime.utcnow()
|
464
500
|
start_time = end_time - timedelta(days=self.analysis_period_days)
|
465
|
-
|
501
|
+
|
466
502
|
for pattern in patterns:
|
467
503
|
try:
|
468
|
-
cloudwatch = self.session.client(
|
469
|
-
|
504
|
+
cloudwatch = self.session.client("cloudwatch", region_name=pattern.region)
|
505
|
+
|
470
506
|
# Get utilization metrics based on service type
|
471
507
|
if pattern.service == RIService.EC2:
|
472
|
-
cpu_utilization = await self._get_ec2_utilization(
|
508
|
+
cpu_utilization = await self._get_ec2_utilization(
|
509
|
+
cloudwatch, pattern.resource_id, start_time, end_time
|
510
|
+
)
|
473
511
|
usage_hours = self._calculate_usage_hours(cpu_utilization, self.analysis_period_days)
|
474
512
|
elif pattern.service == RIService.RDS:
|
475
|
-
cpu_utilization = await self._get_rds_utilization(
|
513
|
+
cpu_utilization = await self._get_rds_utilization(
|
514
|
+
cloudwatch, pattern.resource_id, start_time, end_time
|
515
|
+
)
|
476
516
|
usage_hours = self._calculate_usage_hours(cpu_utilization, self.analysis_period_days)
|
477
517
|
else:
|
478
518
|
# For other services, assume consistent usage pattern
|
479
519
|
usage_hours = self.analysis_period_days * 24 * 0.8 # 80% uptime assumption
|
480
|
-
|
520
|
+
|
481
521
|
# Calculate usage statistics
|
482
522
|
total_possible_hours = self.analysis_period_days * 24
|
483
523
|
usage_percentage = usage_hours / total_possible_hours if total_possible_hours > 0 else 0
|
484
|
-
|
524
|
+
|
485
525
|
# Update pattern with usage analysis
|
486
526
|
pattern.total_hours_running = usage_hours
|
487
527
|
pattern.average_daily_hours = usage_hours / self.analysis_period_days
|
488
528
|
pattern.usage_consistency_score = min(1.0, usage_percentage)
|
489
|
-
pattern.current_monthly_cost =
|
529
|
+
pattern.current_monthly_cost = (
|
530
|
+
pattern.on_demand_hourly_rate * (usage_hours / self.analysis_period_days) * 30.44 * 24
|
531
|
+
)
|
490
532
|
pattern.current_annual_cost = pattern.current_monthly_cost * 12
|
491
|
-
|
533
|
+
|
492
534
|
# Calculate RI suitability score
|
493
535
|
pattern.ri_suitability_score = self._calculate_ri_suitability_score(pattern)
|
494
|
-
|
536
|
+
|
495
537
|
analyzed_patterns.append(pattern)
|
496
|
-
|
538
|
+
|
497
539
|
except Exception as e:
|
498
540
|
print_warning(f"Usage analysis failed for {pattern.resource_id}: {str(e)}")
|
499
541
|
# Keep pattern with default values
|
500
542
|
pattern.usage_consistency_score = 0.5
|
501
543
|
pattern.ri_suitability_score = 40.0
|
502
544
|
analyzed_patterns.append(pattern)
|
503
|
-
|
545
|
+
|
504
546
|
progress.advance(task_id)
|
505
|
-
|
547
|
+
|
506
548
|
return analyzed_patterns
|
507
|
-
|
508
|
-
async def _get_ec2_utilization(
|
549
|
+
|
550
|
+
async def _get_ec2_utilization(
|
551
|
+
self, cloudwatch, instance_id: str, start_time: datetime, end_time: datetime
|
552
|
+
) -> List[float]:
|
509
553
|
"""Get EC2 instance CPU utilization from CloudWatch."""
|
510
554
|
try:
|
511
555
|
response = cloudwatch.get_metric_statistics(
|
512
|
-
Namespace=
|
513
|
-
MetricName=
|
514
|
-
Dimensions=[{
|
556
|
+
Namespace="AWS/EC2",
|
557
|
+
MetricName="CPUUtilization",
|
558
|
+
Dimensions=[{"Name": "InstanceId", "Value": instance_id}],
|
515
559
|
StartTime=start_time,
|
516
560
|
EndTime=end_time,
|
517
561
|
Period=86400, # Daily data points
|
518
|
-
Statistics=[
|
562
|
+
Statistics=["Average"],
|
519
563
|
)
|
520
|
-
|
521
|
-
return [point[
|
522
|
-
|
564
|
+
|
565
|
+
return [point["Average"] for point in response.get("Datapoints", [])]
|
566
|
+
|
523
567
|
except Exception as e:
|
524
568
|
logger.warning(f"CloudWatch CPU metrics unavailable for EC2 {instance_id}: {e}")
|
525
569
|
return []
|
526
|
-
|
527
|
-
async def _get_rds_utilization(
|
570
|
+
|
571
|
+
async def _get_rds_utilization(
|
572
|
+
self, cloudwatch, db_identifier: str, start_time: datetime, end_time: datetime
|
573
|
+
) -> List[float]:
|
528
574
|
"""Get RDS instance CPU utilization from CloudWatch."""
|
529
575
|
try:
|
530
576
|
response = cloudwatch.get_metric_statistics(
|
531
|
-
Namespace=
|
532
|
-
MetricName=
|
533
|
-
Dimensions=[{
|
577
|
+
Namespace="AWS/RDS",
|
578
|
+
MetricName="CPUUtilization",
|
579
|
+
Dimensions=[{"Name": "DBInstanceIdentifier", "Value": db_identifier}],
|
534
580
|
StartTime=start_time,
|
535
581
|
EndTime=end_time,
|
536
582
|
Period=86400, # Daily data points
|
537
|
-
Statistics=[
|
583
|
+
Statistics=["Average"],
|
538
584
|
)
|
539
|
-
|
540
|
-
return [point[
|
541
|
-
|
585
|
+
|
586
|
+
return [point["Average"] for point in response.get("Datapoints", [])]
|
587
|
+
|
542
588
|
except Exception as e:
|
543
589
|
logger.warning(f"CloudWatch CPU metrics unavailable for RDS {db_identifier}: {e}")
|
544
590
|
return []
|
545
|
-
|
591
|
+
|
546
592
|
def _calculate_usage_hours(self, utilization_data: List[float], analysis_days: int) -> float:
|
547
593
|
"""Calculate actual usage hours based on utilization data."""
|
548
594
|
if not utilization_data:
|
549
595
|
# No metrics available, assume moderate usage
|
550
596
|
return analysis_days * 24 * 0.7 # 70% uptime assumption
|
551
|
-
|
597
|
+
|
552
598
|
# Assume instance is "in use" if CPU > 5%
|
553
599
|
active_days = sum(1 for cpu in utilization_data if cpu > 5.0)
|
554
600
|
total_hours = active_days * 24 # Assume full day usage when active
|
555
|
-
|
601
|
+
|
556
602
|
return min(total_hours, analysis_days * 24)
|
557
|
-
|
603
|
+
|
558
604
|
def _calculate_ri_suitability_score(self, pattern: ResourceUsagePattern) -> float:
|
559
605
|
"""Calculate RI suitability score (0-100) for resource."""
|
560
606
|
score = 0.0
|
561
|
-
|
607
|
+
|
562
608
|
# Usage consistency (50% weight)
|
563
609
|
score += pattern.usage_consistency_score * 50
|
564
|
-
|
610
|
+
|
565
611
|
# Resource type stability (25% weight)
|
566
|
-
if pattern.tags.get(
|
612
|
+
if pattern.tags.get("Environment") in ["production", "prod"]:
|
567
613
|
score += 25
|
568
|
-
elif pattern.tags.get(
|
614
|
+
elif pattern.tags.get("Environment") in ["staging", "test"]:
|
569
615
|
score += 10
|
570
616
|
else:
|
571
617
|
score += 15 # Unknown environment
|
572
|
-
|
618
|
+
|
573
619
|
# Cost impact (25% weight)
|
574
620
|
if pattern.current_annual_cost > 5000: # High cost resources
|
575
621
|
score += 25
|
@@ -577,110 +623,126 @@ class ReservationOptimizer:
|
|
577
623
|
score += 20
|
578
624
|
else:
|
579
625
|
score += 10
|
580
|
-
|
626
|
+
|
581
627
|
return min(100.0, score)
|
582
|
-
|
583
|
-
async def _assess_ri_suitability(
|
628
|
+
|
629
|
+
async def _assess_ri_suitability(
|
630
|
+
self, patterns: List[ResourceUsagePattern], progress, task_id
|
631
|
+
) -> List[ResourceUsagePattern]:
|
584
632
|
"""Assess which resources are suitable for Reserved Instance purchase."""
|
585
633
|
suitable_resources = []
|
586
|
-
|
634
|
+
|
587
635
|
for pattern in patterns:
|
588
636
|
try:
|
589
637
|
# Check if resource meets RI suitability criteria
|
590
|
-
if (
|
591
|
-
pattern.
|
638
|
+
if (
|
639
|
+
pattern.ri_suitability_score >= 60.0
|
640
|
+
and pattern.usage_consistency_score >= self.minimum_usage_threshold
|
641
|
+
):
|
592
642
|
suitable_resources.append(pattern)
|
593
|
-
|
643
|
+
|
594
644
|
except Exception as e:
|
595
645
|
logger.warning(f"RI suitability assessment failed for {pattern.resource_id}: {e}")
|
596
|
-
|
646
|
+
|
597
647
|
progress.advance(task_id)
|
598
|
-
|
648
|
+
|
599
649
|
return suitable_resources
|
600
|
-
|
601
|
-
async def _generate_ri_recommendations(
|
650
|
+
|
651
|
+
async def _generate_ri_recommendations(
|
652
|
+
self, suitable_resources: List[ResourceUsagePattern], progress, task_id
|
653
|
+
) -> List[RIRecommendation]:
|
602
654
|
"""Generate Reserved Instance purchase recommendations with financial modeling."""
|
603
655
|
recommendations = []
|
604
|
-
|
656
|
+
|
605
657
|
for resource in suitable_resources:
|
606
658
|
try:
|
607
659
|
# Get RI pricing for resource type
|
608
660
|
service_pricing = self.service_pricing.get(resource.service, {})
|
609
661
|
type_pricing = service_pricing.get(resource.resource_type, {})
|
610
|
-
ri_pricing = type_pricing.get(
|
611
|
-
|
662
|
+
ri_pricing = type_pricing.get("ri_1yr_partial", {})
|
663
|
+
|
612
664
|
if not ri_pricing:
|
613
665
|
progress.advance(task_id)
|
614
666
|
continue
|
615
|
-
|
667
|
+
|
616
668
|
# Calculate financial model
|
617
|
-
upfront_cost = ri_pricing.get(
|
618
|
-
ri_hourly_rate = ri_pricing.get(
|
619
|
-
|
669
|
+
upfront_cost = ri_pricing.get("upfront", 0)
|
670
|
+
ri_hourly_rate = ri_pricing.get("hourly", resource.on_demand_hourly_rate * 0.6)
|
671
|
+
|
620
672
|
# Effective hourly rate including amortized upfront
|
621
673
|
effective_hourly_rate = ri_hourly_rate + (upfront_cost / (365.25 * 24))
|
622
|
-
|
674
|
+
|
623
675
|
# Savings calculations based on actual usage
|
624
676
|
annual_usage_hours = resource.average_daily_hours * 365.25
|
625
677
|
on_demand_annual_cost = resource.on_demand_hourly_rate * annual_usage_hours
|
626
678
|
ri_annual_cost = upfront_cost + (ri_hourly_rate * annual_usage_hours)
|
627
679
|
annual_savings = on_demand_annual_cost - ri_annual_cost
|
628
|
-
|
680
|
+
|
629
681
|
# Break-even analysis
|
630
682
|
monthly_savings = annual_savings / 12
|
631
683
|
break_even_months = upfront_cost / monthly_savings if monthly_savings > 0 else 999
|
632
|
-
|
684
|
+
|
633
685
|
# ROI calculation
|
634
|
-
roi_percentage = (
|
635
|
-
|
686
|
+
roi_percentage = (
|
687
|
+
(annual_savings / (upfront_cost + ri_hourly_rate * annual_usage_hours)) * 100
|
688
|
+
if upfront_cost > 0
|
689
|
+
else 0
|
690
|
+
)
|
691
|
+
|
636
692
|
# Risk assessment
|
637
693
|
utilization_confidence = resource.usage_consistency_score
|
638
|
-
risk_level =
|
639
|
-
|
694
|
+
risk_level = (
|
695
|
+
"low" if utilization_confidence > 0.8 else ("medium" if utilization_confidence > 0.6 else "high")
|
696
|
+
)
|
697
|
+
|
640
698
|
# Only recommend if financially beneficial
|
641
699
|
if annual_savings > 0 and break_even_months <= self.break_even_target_months:
|
642
|
-
recommendations.append(
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
700
|
+
recommendations.append(
|
701
|
+
RIRecommendation(
|
702
|
+
resource_type=resource.resource_type,
|
703
|
+
service=resource.service,
|
704
|
+
region=resource.region,
|
705
|
+
availability_zone=resource.availability_zone,
|
706
|
+
platform=resource.platform,
|
707
|
+
recommended_quantity=1,
|
708
|
+
ri_term=RITerm.ONE_YEAR,
|
709
|
+
payment_option=RIPaymentOption.PARTIAL_UPFRONT,
|
710
|
+
ri_upfront_cost=upfront_cost,
|
711
|
+
ri_hourly_rate=ri_hourly_rate,
|
712
|
+
ri_effective_hourly_rate=effective_hourly_rate,
|
713
|
+
on_demand_hourly_rate=resource.on_demand_hourly_rate,
|
714
|
+
break_even_months=break_even_months,
|
715
|
+
first_year_savings=annual_savings,
|
716
|
+
total_term_savings=annual_savings, # 1-year term
|
717
|
+
annual_savings=annual_savings,
|
718
|
+
roi_percentage=roi_percentage,
|
719
|
+
utilization_confidence=utilization_confidence,
|
720
|
+
risk_level=risk_level,
|
721
|
+
flexibility_impact="minimal",
|
722
|
+
covered_resources=[resource.resource_id],
|
723
|
+
usage_justification=f"Resource shows {resource.usage_consistency_score * 100:.1f}% consistent usage over {resource.analysis_period_days} days",
|
724
|
+
)
|
725
|
+
)
|
726
|
+
|
667
727
|
except Exception as e:
|
668
728
|
logger.warning(f"RI recommendation generation failed for {resource.resource_id}: {e}")
|
669
|
-
|
729
|
+
|
670
730
|
progress.advance(task_id)
|
671
|
-
|
731
|
+
|
672
732
|
return recommendations
|
673
|
-
|
674
|
-
async def _optimize_ri_portfolio(
|
733
|
+
|
734
|
+
async def _optimize_ri_portfolio(
|
735
|
+
self, recommendations: List[RIRecommendation], progress, task_id
|
736
|
+
) -> List[RIRecommendation]:
|
675
737
|
"""Optimize RI portfolio for maximum value and minimum risk."""
|
676
738
|
try:
|
677
739
|
# Sort recommendations by ROI and risk level
|
678
740
|
optimized = sorted(recommendations, key=lambda x: (x.roi_percentage, -x.break_even_months), reverse=True)
|
679
|
-
|
741
|
+
|
680
742
|
# Apply portfolio constraints (simplified)
|
681
743
|
budget_limit = 1_000_000 # $1M annual RI budget limit
|
682
744
|
current_investment = 0
|
683
|
-
|
745
|
+
|
684
746
|
final_recommendations = []
|
685
747
|
for recommendation in optimized:
|
686
748
|
if current_investment + recommendation.ri_upfront_cost <= budget_limit:
|
@@ -688,71 +750,75 @@ class ReservationOptimizer:
|
|
688
750
|
current_investment += recommendation.ri_upfront_cost
|
689
751
|
else:
|
690
752
|
break
|
691
|
-
|
753
|
+
|
692
754
|
progress.advance(task_id)
|
693
755
|
return final_recommendations
|
694
|
-
|
756
|
+
|
695
757
|
except Exception as e:
|
696
758
|
logger.warning(f"RI portfolio optimization failed: {e}")
|
697
759
|
progress.advance(task_id)
|
698
760
|
return recommendations
|
699
|
-
|
761
|
+
|
700
762
|
async def _validate_with_mcp(self, recommendations: List[RIRecommendation], progress, task_id) -> float:
|
701
763
|
"""Validate RI recommendations with embedded MCP validator."""
|
702
764
|
try:
|
703
765
|
# Prepare validation data in FinOps format
|
704
766
|
validation_data = {
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
767
|
+
"total_upfront_investment": sum(rec.ri_upfront_cost for rec in recommendations),
|
768
|
+
"total_annual_savings": sum(rec.annual_savings for rec in recommendations),
|
769
|
+
"recommendations_count": len(recommendations),
|
770
|
+
"services_analyzed": list(set(rec.service.value for rec in recommendations)),
|
771
|
+
"analysis_timestamp": datetime.now().isoformat(),
|
710
772
|
}
|
711
|
-
|
773
|
+
|
712
774
|
# Initialize MCP validator if profile is available
|
713
775
|
if self.profile_name:
|
714
776
|
mcp_validator = EmbeddedMCPValidator([self.profile_name])
|
715
777
|
validation_results = await mcp_validator.validate_cost_data_async(validation_data)
|
716
|
-
accuracy = validation_results.get(
|
717
|
-
|
778
|
+
accuracy = validation_results.get("total_accuracy", 0.0)
|
779
|
+
|
718
780
|
if accuracy >= 99.5:
|
719
781
|
print_success(f"MCP Validation: {accuracy:.1f}% accuracy achieved (target: ≥99.5%)")
|
720
782
|
else:
|
721
783
|
print_warning(f"MCP Validation: {accuracy:.1f}% accuracy (target: ≥99.5%)")
|
722
|
-
|
784
|
+
|
723
785
|
progress.advance(task_id)
|
724
786
|
return accuracy
|
725
787
|
else:
|
726
788
|
print_info("MCP validation skipped - no profile specified")
|
727
789
|
progress.advance(task_id)
|
728
790
|
return 0.0
|
729
|
-
|
791
|
+
|
730
792
|
except Exception as e:
|
731
793
|
print_warning(f"MCP validation failed: {str(e)}")
|
732
794
|
progress.advance(task_id)
|
733
795
|
return 0.0
|
734
|
-
|
735
|
-
def _compile_results(
|
736
|
-
|
737
|
-
|
738
|
-
|
796
|
+
|
797
|
+
def _compile_results(
|
798
|
+
self,
|
799
|
+
usage_patterns: List[ResourceUsagePattern],
|
800
|
+
recommendations: List[RIRecommendation],
|
801
|
+
mcp_accuracy: float,
|
802
|
+
analysis_start_time: float,
|
803
|
+
services_analyzed: List[RIService],
|
804
|
+
) -> RIOptimizerResults:
|
739
805
|
"""Compile comprehensive RI optimization results."""
|
740
|
-
|
806
|
+
|
741
807
|
# Categorize recommendations by service
|
742
808
|
ec2_recommendations = [r for r in recommendations if r.service == RIService.EC2]
|
743
809
|
rds_recommendations = [r for r in recommendations if r.service == RIService.RDS]
|
744
810
|
elasticache_recommendations = [r for r in recommendations if r.service == RIService.ELASTICACHE]
|
745
811
|
redshift_recommendations = [r for r in recommendations if r.service == RIService.REDSHIFT]
|
746
|
-
|
812
|
+
|
747
813
|
# Calculate financial summary
|
748
814
|
total_upfront_investment = sum(rec.ri_upfront_cost for rec in recommendations)
|
749
815
|
total_annual_savings = sum(rec.annual_savings for rec in recommendations)
|
750
816
|
total_current_on_demand_cost = sum(pattern.current_annual_cost for pattern in usage_patterns)
|
751
817
|
total_potential_ri_cost = total_current_on_demand_cost - total_annual_savings
|
752
|
-
|
818
|
+
|
753
819
|
# Calculate portfolio ROI
|
754
820
|
portfolio_roi = (total_annual_savings / total_upfront_investment * 100) if total_upfront_investment > 0 else 0
|
755
|
-
|
821
|
+
|
756
822
|
return RIOptimizerResults(
|
757
823
|
analyzed_services=services_analyzed,
|
758
824
|
analyzed_regions=self.regions,
|
@@ -771,12 +837,12 @@ class ReservationOptimizer:
|
|
771
837
|
redshift_recommendations=redshift_recommendations,
|
772
838
|
execution_time_seconds=time.time() - analysis_start_time,
|
773
839
|
mcp_validation_accuracy=mcp_accuracy,
|
774
|
-
analysis_timestamp=datetime.now()
|
840
|
+
analysis_timestamp=datetime.now(),
|
775
841
|
)
|
776
|
-
|
842
|
+
|
777
843
|
def _display_executive_summary(self, results: RIOptimizerResults) -> None:
|
778
844
|
"""Display executive summary with Rich CLI formatting."""
|
779
|
-
|
845
|
+
|
780
846
|
# Executive Summary Panel
|
781
847
|
summary_content = f"""
|
782
848
|
💼 Reserved Instance Portfolio Analysis
|
@@ -794,23 +860,21 @@ class ReservationOptimizer:
|
|
794
860
|
• ElastiCache: {len(results.elasticache_recommendations)} recommendations
|
795
861
|
• Redshift: {len(results.redshift_recommendations)} recommendations
|
796
862
|
|
797
|
-
🌍 Regions: {
|
863
|
+
🌍 Regions: {", ".join(results.analyzed_regions)}
|
798
864
|
⚡ Analysis Time: {results.execution_time_seconds:.2f}s
|
799
865
|
✅ MCP Accuracy: {results.mcp_validation_accuracy:.1f}%
|
800
866
|
"""
|
801
|
-
|
802
|
-
console.print(
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
)
|
807
|
-
|
867
|
+
|
868
|
+
console.print(
|
869
|
+
create_panel(
|
870
|
+
summary_content.strip(), title="🏆 Reserved Instance Portfolio Executive Summary", border_style="green"
|
871
|
+
)
|
872
|
+
)
|
873
|
+
|
808
874
|
# RI Recommendations Table
|
809
875
|
if results.ri_recommendations:
|
810
|
-
table = create_table(
|
811
|
-
|
812
|
-
)
|
813
|
-
|
876
|
+
table = create_table(title="Reserved Instance Purchase Recommendations")
|
877
|
+
|
814
878
|
table.add_column("Service", style="cyan", no_wrap=True)
|
815
879
|
table.add_column("Resource Type", style="dim")
|
816
880
|
table.add_column("Region", justify="center")
|
@@ -819,24 +883,16 @@ class ReservationOptimizer:
|
|
819
883
|
table.add_column("Break-even", justify="center")
|
820
884
|
table.add_column("ROI", justify="right", style="blue")
|
821
885
|
table.add_column("Risk", justify="center")
|
822
|
-
|
886
|
+
|
823
887
|
# Sort by annual savings (descending)
|
824
|
-
sorted_recommendations = sorted(
|
825
|
-
|
826
|
-
key=lambda x: x.annual_savings,
|
827
|
-
reverse=True
|
828
|
-
)
|
829
|
-
|
888
|
+
sorted_recommendations = sorted(results.ri_recommendations, key=lambda x: x.annual_savings, reverse=True)
|
889
|
+
|
830
890
|
# Show top 20 recommendations
|
831
891
|
display_recommendations = sorted_recommendations[:20]
|
832
|
-
|
892
|
+
|
833
893
|
for rec in display_recommendations:
|
834
|
-
risk_indicator = {
|
835
|
-
|
836
|
-
"medium": "🟡",
|
837
|
-
"high": "🔴"
|
838
|
-
}.get(rec.risk_level, "⚪")
|
839
|
-
|
894
|
+
risk_indicator = {"low": "🟢", "medium": "🟡", "high": "🔴"}.get(rec.risk_level, "⚪")
|
895
|
+
|
840
896
|
table.add_row(
|
841
897
|
rec.service.value.upper(),
|
842
898
|
rec.resource_type,
|
@@ -845,17 +901,23 @@ class ReservationOptimizer:
|
|
845
901
|
format_cost(rec.annual_savings),
|
846
902
|
f"{rec.break_even_months:.1f} mo",
|
847
903
|
f"{rec.roi_percentage:.1f}%",
|
848
|
-
f"{risk_indicator} {rec.risk_level}"
|
904
|
+
f"{risk_indicator} {rec.risk_level}",
|
849
905
|
)
|
850
|
-
|
906
|
+
|
851
907
|
if len(sorted_recommendations) > 20:
|
852
908
|
table.add_row(
|
853
|
-
"...",
|
854
|
-
|
909
|
+
"...",
|
910
|
+
"...",
|
911
|
+
"...",
|
912
|
+
"...",
|
913
|
+
"...",
|
914
|
+
"...",
|
915
|
+
"...",
|
916
|
+
f"[dim]+{len(sorted_recommendations) - 20} more recommendations[/]",
|
855
917
|
)
|
856
|
-
|
918
|
+
|
857
919
|
console.print(table)
|
858
|
-
|
920
|
+
|
859
921
|
# Financial Summary Panel
|
860
922
|
financial_content = f"""
|
861
923
|
💰 RI Investment Portfolio Summary:
|
@@ -869,40 +931,42 @@ class ReservationOptimizer:
|
|
869
931
|
🔄 Cost Transformation:
|
870
932
|
• From: {format_cost(results.total_current_on_demand_cost)} On-Demand
|
871
933
|
• To: {format_cost(results.total_potential_ri_cost)} Reserved Instance
|
872
|
-
• Savings: {format_cost(results.total_annual_savings)} ({(results.total_annual_savings/results.total_current_on_demand_cost*100):.1f}% reduction)
|
934
|
+
• Savings: {format_cost(results.total_annual_savings)} ({(results.total_annual_savings / results.total_current_on_demand_cost * 100):.1f}% reduction)
|
873
935
|
"""
|
874
|
-
|
875
|
-
console.print(
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
)
|
936
|
+
|
937
|
+
console.print(
|
938
|
+
create_panel(
|
939
|
+
financial_content.strip(), title="💼 RI Procurement Financial Analysis", border_style="blue"
|
940
|
+
)
|
941
|
+
)
|
880
942
|
|
881
943
|
|
882
944
|
# CLI Integration for enterprise runbooks commands
|
883
945
|
@click.command()
|
884
|
-
@click.option(
|
885
|
-
@click.option(
|
886
|
-
@click.option(
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
946
|
+
@click.option("--profile", help="AWS profile name (3-tier priority: User > Environment > Default)")
|
947
|
+
@click.option("--regions", multiple=True, help="AWS regions to analyze (space-separated)")
|
948
|
+
@click.option(
|
949
|
+
"--services",
|
950
|
+
multiple=True,
|
951
|
+
type=click.Choice(["ec2", "rds", "elasticache", "redshift"]),
|
952
|
+
help="AWS services to analyze for RI opportunities",
|
953
|
+
)
|
954
|
+
@click.option("--dry-run/--no-dry-run", default=True, help="Execute in dry-run mode (READ-ONLY analysis)")
|
955
|
+
@click.option("--usage-threshold-days", type=int, default=90, help="Usage analysis period in days")
|
892
956
|
def reservation_optimizer(profile, regions, services, dry_run, usage_threshold_days):
|
893
957
|
"""
|
894
958
|
Reserved Instance Optimizer - Enterprise Multi-Service RI Strategy
|
895
|
-
|
959
|
+
|
896
960
|
Comprehensive RI analysis and procurement recommendations:
|
897
961
|
• Multi-service RI analysis (EC2, RDS, ElastiCache, Redshift)
|
898
962
|
• Historical usage pattern analysis with financial modeling
|
899
963
|
• Break-even analysis and ROI calculations for RI procurement
|
900
964
|
• Portfolio optimization with risk assessment and budget constraints
|
901
|
-
|
965
|
+
|
902
966
|
Part of $132,720+ annual savings methodology targeting $3.2M-$17M RI optimization.
|
903
|
-
|
967
|
+
|
904
968
|
SAFETY: READ-ONLY analysis only - no actual RI purchases.
|
905
|
-
|
969
|
+
|
906
970
|
Examples:
|
907
971
|
runbooks finops reservation --analyze
|
908
972
|
runbooks finops reservation --services ec2 rds --regions us-east-1 us-west-2
|
@@ -913,37 +977,37 @@ def reservation_optimizer(profile, regions, services, dry_run, usage_threshold_d
|
|
913
977
|
service_enums = []
|
914
978
|
if services:
|
915
979
|
service_map = {
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
980
|
+
"ec2": RIService.EC2,
|
981
|
+
"rds": RIService.RDS,
|
982
|
+
"elasticache": RIService.ELASTICACHE,
|
983
|
+
"redshift": RIService.REDSHIFT,
|
920
984
|
}
|
921
985
|
service_enums = [service_map[s] for s in services]
|
922
|
-
|
986
|
+
|
923
987
|
# Initialize optimizer
|
924
|
-
optimizer = ReservationOptimizer(
|
925
|
-
|
926
|
-
regions=list(regions) if regions else None
|
927
|
-
)
|
928
|
-
|
988
|
+
optimizer = ReservationOptimizer(profile_name=profile, regions=list(regions) if regions else None)
|
989
|
+
|
929
990
|
# Override analysis period if specified
|
930
991
|
if usage_threshold_days != 90:
|
931
992
|
optimizer.analysis_period_days = usage_threshold_days
|
932
|
-
|
993
|
+
|
933
994
|
# Execute comprehensive analysis
|
934
|
-
results = asyncio.run(
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
995
|
+
results = asyncio.run(
|
996
|
+
optimizer.analyze_reservation_opportunities(
|
997
|
+
services=service_enums if service_enums else None, dry_run=dry_run
|
998
|
+
)
|
999
|
+
)
|
1000
|
+
|
939
1001
|
# Display final success message
|
940
1002
|
if results.total_annual_savings > 0:
|
941
1003
|
print_success(f"Analysis complete: {format_cost(results.total_annual_savings)} potential annual savings")
|
942
|
-
print_info(
|
1004
|
+
print_info(
|
1005
|
+
f"Required investment: {format_cost(results.total_upfront_investment)} ({results.portfolio_roi:.1f}% ROI)"
|
1006
|
+
)
|
943
1007
|
print_info(f"Services analyzed: {', '.join([s.value.upper() for s in results.analyzed_services])}")
|
944
1008
|
else:
|
945
1009
|
print_info("Analysis complete: No cost-effective RI opportunities identified")
|
946
|
-
|
1010
|
+
|
947
1011
|
except KeyboardInterrupt:
|
948
1012
|
print_warning("Analysis interrupted by user")
|
949
1013
|
raise click.Abort()
|
@@ -952,5 +1016,5 @@ def reservation_optimizer(profile, regions, services, dry_run, usage_threshold_d
|
|
952
1016
|
raise click.Abort()
|
953
1017
|
|
954
1018
|
|
955
|
-
if __name__ ==
|
956
|
-
reservation_optimizer()
|
1019
|
+
if __name__ == "__main__":
|
1020
|
+
reservation_optimizer()
|