runbooks 1.0.0__py3-none-any.whl → 1.0.2__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 +1 -1
- runbooks/cfat/WEIGHT_CONFIG_README.md +368 -0
- runbooks/cfat/app.ts +27 -19
- runbooks/cfat/assessment/runner.py +6 -5
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/cloudops/models.py +20 -14
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1070 -105
- runbooks/common/aws_pricing_api.py +276 -44
- runbooks/common/date_utils.py +115 -0
- runbooks/common/dry_run_examples.py +587 -0
- runbooks/common/dry_run_framework.py +520 -0
- runbooks/common/enhanced_exception_handler.py +10 -7
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/memory_optimization.py +533 -0
- runbooks/common/performance_optimization_engine.py +1153 -0
- runbooks/common/profile_utils.py +86 -118
- runbooks/common/rich_utils.py +3 -3
- runbooks/common/sre_performance_suite.py +574 -0
- runbooks/finops/business_case_config.py +314 -0
- runbooks/finops/cost_processor.py +19 -4
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_cost_optimizer.py +1 -1
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/embedded_mcp_validator.py +642 -36
- runbooks/finops/enhanced_trend_visualization.py +7 -2
- runbooks/finops/executive_export.py +789 -0
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/finops_scenarios.py +34 -27
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/nat_gateway_optimizer.py +46 -27
- runbooks/finops/notebook_utils.py +1 -1
- runbooks/finops/schemas.py +73 -58
- runbooks/finops/single_dashboard.py +20 -4
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_exporter.py +2 -1
- runbooks/finops/vpc_cleanup_optimizer.py +22 -29
- runbooks/inventory/core/collector.py +51 -28
- runbooks/inventory/discovery.md +197 -247
- runbooks/inventory/inventory_modules.py +2 -2
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/models/account.py +5 -3
- runbooks/inventory/models/inventory.py +1 -1
- runbooks/inventory/models/resource.py +5 -3
- runbooks/inventory/organizations_discovery.py +102 -13
- runbooks/inventory/unified_validation_engine.py +2 -15
- runbooks/main.py +255 -92
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +17 -13
- runbooks/operate/vpc_operations.py +82 -13
- runbooks/remediation/base.py +3 -1
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +66 -18
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/security/compliance_automation_engine.py +99 -20
- runbooks/security/config/__init__.py +24 -0
- runbooks/security/config/compliance_config.py +255 -0
- runbooks/security/config/compliance_weights_example.json +22 -0
- runbooks/security/config_template_generator.py +500 -0
- runbooks/security/security_cli.py +377 -0
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +26 -15
- runbooks/validation/mcp_validator.py +62 -8
- runbooks/vpc/config.py +49 -15
- runbooks/vpc/cross_account_session.py +5 -1
- runbooks/vpc/heatmap_engine.py +438 -59
- runbooks/vpc/mcp_no_eni_validator.py +115 -36
- runbooks/vpc/performance_optimized_analyzer.py +546 -0
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +3 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/METADATA +1 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/RECORD +85 -79
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
- runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/vpc/runbooks.security.report_generator.log +0 -0
- runbooks/vpc/runbooks.security.run_script.log +0 -0
- runbooks/vpc/runbooks.security.security_export.log +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/WHEEL +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,314 @@
|
|
1
|
+
"""
|
2
|
+
Dynamic Business Case Configuration - Enterprise Template System
|
3
|
+
|
4
|
+
Strategic Achievement: Replace hardcoded JIRA references with dynamic business case templates
|
5
|
+
- Enterprise naming conventions with configurable business scenarios
|
6
|
+
- Dynamic financial targets and achievement tracking
|
7
|
+
- Reusable template system for unlimited business case scaling
|
8
|
+
|
9
|
+
This module provides configurable business case templates following enterprise standards:
|
10
|
+
- "Do one thing and do it well": Centralized configuration management
|
11
|
+
- "Move Fast, But Not So Fast We Crash": Proven template patterns with validation
|
12
|
+
"""
|
13
|
+
|
14
|
+
import os
|
15
|
+
from dataclasses import dataclass
|
16
|
+
from typing import Dict, List, Optional, Any, Union
|
17
|
+
from enum import Enum
|
18
|
+
|
19
|
+
|
20
|
+
class BusinessCaseType(Enum):
|
21
|
+
"""Standard business case types for enterprise scenarios."""
|
22
|
+
COST_OPTIMIZATION = "cost_optimization"
|
23
|
+
RESOURCE_CLEANUP = "resource_cleanup"
|
24
|
+
COMPLIANCE_FRAMEWORK = "compliance_framework"
|
25
|
+
SECURITY_ENHANCEMENT = "security_enhancement"
|
26
|
+
AUTOMATION_DEPLOYMENT = "automation_deployment"
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass
|
30
|
+
class BusinessScenario:
|
31
|
+
"""Dynamic business scenario configuration."""
|
32
|
+
scenario_id: str
|
33
|
+
display_name: str
|
34
|
+
business_case_type: BusinessCaseType
|
35
|
+
target_savings_min: Optional[float] = None
|
36
|
+
target_savings_max: Optional[float] = None
|
37
|
+
business_description: str = ""
|
38
|
+
technical_focus: str = ""
|
39
|
+
risk_level: str = "Medium"
|
40
|
+
implementation_status: str = "Analysis"
|
41
|
+
cli_command_suffix: str = ""
|
42
|
+
|
43
|
+
@property
|
44
|
+
def scenario_display_id(self) -> str:
|
45
|
+
"""Generate enterprise-friendly scenario display ID."""
|
46
|
+
return f"{self.business_case_type.value.replace('_', '-').title()}-{self.scenario_id}"
|
47
|
+
|
48
|
+
@property
|
49
|
+
def savings_range_display(self) -> str:
|
50
|
+
"""Generate savings range display for business presentations."""
|
51
|
+
if self.target_savings_min and self.target_savings_max:
|
52
|
+
if self.target_savings_min == self.target_savings_max:
|
53
|
+
return f"${self.target_savings_min:,.0f}/year"
|
54
|
+
else:
|
55
|
+
return f"${self.target_savings_min:,.0f}-${self.target_savings_max:,.0f}/year"
|
56
|
+
elif self.target_savings_min:
|
57
|
+
return f"${self.target_savings_min:,.0f}+/year"
|
58
|
+
else:
|
59
|
+
return "Analysis pending"
|
60
|
+
|
61
|
+
|
62
|
+
class BusinessCaseConfigManager:
|
63
|
+
"""Enterprise business case configuration manager."""
|
64
|
+
|
65
|
+
def __init__(self, config_source: Optional[str] = None):
|
66
|
+
"""
|
67
|
+
Initialize business case configuration manager.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
config_source: Optional path to configuration file or environment variable prefix
|
71
|
+
"""
|
72
|
+
self.config_source = config_source or "RUNBOOKS_BUSINESS_CASE"
|
73
|
+
self.scenarios = self._load_default_scenarios()
|
74
|
+
self._load_environment_overrides()
|
75
|
+
|
76
|
+
def _load_default_scenarios(self) -> Dict[str, BusinessScenario]:
|
77
|
+
"""Load default enterprise business scenarios."""
|
78
|
+
return {
|
79
|
+
"workspaces": BusinessScenario(
|
80
|
+
scenario_id="workspaces",
|
81
|
+
display_name="WorkSpaces Resource Optimization",
|
82
|
+
business_case_type=BusinessCaseType.RESOURCE_CLEANUP,
|
83
|
+
target_savings_min=12000,
|
84
|
+
target_savings_max=15000,
|
85
|
+
business_description="Identify and optimize unused Amazon WorkSpaces for cost efficiency",
|
86
|
+
technical_focus="Zero-usage WorkSpaces detection and cost analysis",
|
87
|
+
risk_level="Low",
|
88
|
+
cli_command_suffix="workspaces"
|
89
|
+
),
|
90
|
+
"rds-snapshots": BusinessScenario(
|
91
|
+
scenario_id="rds-snapshots",
|
92
|
+
display_name="RDS Storage Optimization",
|
93
|
+
business_case_type=BusinessCaseType.RESOURCE_CLEANUP,
|
94
|
+
target_savings_min=5000,
|
95
|
+
target_savings_max=24000,
|
96
|
+
business_description="Optimize manual RDS snapshots to reduce storage costs",
|
97
|
+
technical_focus="Manual RDS snapshot lifecycle management",
|
98
|
+
risk_level="Medium",
|
99
|
+
cli_command_suffix="snapshots"
|
100
|
+
),
|
101
|
+
"backup-investigation": BusinessScenario(
|
102
|
+
scenario_id="backup-investigation",
|
103
|
+
display_name="Backup Infrastructure Analysis",
|
104
|
+
business_case_type=BusinessCaseType.COMPLIANCE_FRAMEWORK,
|
105
|
+
business_description="Investigate backup account utilization and optimization opportunities",
|
106
|
+
technical_focus="Backup infrastructure resource utilization analysis",
|
107
|
+
risk_level="Medium",
|
108
|
+
implementation_status="Framework",
|
109
|
+
cli_command_suffix="commvault"
|
110
|
+
),
|
111
|
+
"nat-gateway": BusinessScenario(
|
112
|
+
scenario_id="nat-gateway",
|
113
|
+
display_name="Network Gateway Optimization",
|
114
|
+
business_case_type=BusinessCaseType.COST_OPTIMIZATION,
|
115
|
+
target_savings_min=8000,
|
116
|
+
target_savings_max=12000,
|
117
|
+
business_description="Optimize NAT Gateway configurations for cost efficiency",
|
118
|
+
technical_focus="NAT Gateway usage analysis and rightsizing",
|
119
|
+
cli_command_suffix="nat-gateway"
|
120
|
+
),
|
121
|
+
"elastic-ip": BusinessScenario(
|
122
|
+
scenario_id="elastic-ip",
|
123
|
+
display_name="IP Address Resource Management",
|
124
|
+
business_case_type=BusinessCaseType.RESOURCE_CLEANUP,
|
125
|
+
target_savings_min=44, # $3.65 * 12 months
|
126
|
+
business_description="Optimize unattached Elastic IP addresses",
|
127
|
+
technical_focus="Elastic IP attachment analysis and cleanup recommendations",
|
128
|
+
risk_level="Low",
|
129
|
+
cli_command_suffix="elastic-ip"
|
130
|
+
),
|
131
|
+
"ebs-optimization": BusinessScenario(
|
132
|
+
scenario_id="ebs-optimization",
|
133
|
+
display_name="Storage Volume Optimization",
|
134
|
+
business_case_type=BusinessCaseType.COST_OPTIMIZATION,
|
135
|
+
business_description="Optimize EBS volume types and utilization for cost efficiency",
|
136
|
+
technical_focus="EBS volume rightsizing and type optimization (15-20% potential)",
|
137
|
+
cli_command_suffix="ebs"
|
138
|
+
),
|
139
|
+
"vpc-cleanup": BusinessScenario(
|
140
|
+
scenario_id="vpc-cleanup",
|
141
|
+
display_name="Network Infrastructure Cleanup",
|
142
|
+
business_case_type=BusinessCaseType.RESOURCE_CLEANUP,
|
143
|
+
target_savings_min=5869,
|
144
|
+
business_description="Clean up unused VPC resources and infrastructure",
|
145
|
+
technical_focus="VPC resource utilization analysis and cleanup recommendations",
|
146
|
+
cli_command_suffix="vpc-cleanup"
|
147
|
+
)
|
148
|
+
}
|
149
|
+
|
150
|
+
def _load_environment_overrides(self) -> None:
|
151
|
+
"""Load configuration overrides from environment variables."""
|
152
|
+
prefix = f"{self.config_source}_"
|
153
|
+
|
154
|
+
for scenario_key, scenario in self.scenarios.items():
|
155
|
+
# Check for scenario-specific overrides
|
156
|
+
env_key = f"{prefix}{scenario_key.upper().replace('-', '_')}"
|
157
|
+
|
158
|
+
# Override target savings if specified
|
159
|
+
min_savings = os.getenv(f"{env_key}_MIN_SAVINGS")
|
160
|
+
max_savings = os.getenv(f"{env_key}_MAX_SAVINGS")
|
161
|
+
|
162
|
+
if min_savings:
|
163
|
+
scenario.target_savings_min = float(min_savings)
|
164
|
+
if max_savings:
|
165
|
+
scenario.target_savings_max = float(max_savings)
|
166
|
+
|
167
|
+
# Override display name if specified
|
168
|
+
display_name = os.getenv(f"{env_key}_DISPLAY_NAME")
|
169
|
+
if display_name:
|
170
|
+
scenario.display_name = display_name
|
171
|
+
|
172
|
+
# Override business description if specified
|
173
|
+
description = os.getenv(f"{env_key}_DESCRIPTION")
|
174
|
+
if description:
|
175
|
+
scenario.business_description = description
|
176
|
+
|
177
|
+
def get_scenario(self, scenario_key: str) -> Optional[BusinessScenario]:
|
178
|
+
"""Get business scenario by key."""
|
179
|
+
return self.scenarios.get(scenario_key)
|
180
|
+
|
181
|
+
def get_all_scenarios(self) -> Dict[str, BusinessScenario]:
|
182
|
+
"""Get all configured business scenarios."""
|
183
|
+
return self.scenarios
|
184
|
+
|
185
|
+
def get_scenario_choices(self) -> List[str]:
|
186
|
+
"""Get list of valid scenario keys for CLI choice options."""
|
187
|
+
return list(self.scenarios.keys())
|
188
|
+
|
189
|
+
def get_scenario_help_text(self) -> str:
|
190
|
+
"""Generate help text for CLI scenario option."""
|
191
|
+
help_parts = []
|
192
|
+
for key, scenario in self.scenarios.items():
|
193
|
+
savings_display = scenario.savings_range_display
|
194
|
+
help_parts.append(f"{key} ({scenario.display_name}: {savings_display})")
|
195
|
+
return "Business scenario analysis: " + ", ".join(help_parts)
|
196
|
+
|
197
|
+
def format_scenario_for_display(self, scenario_key: str,
|
198
|
+
achieved_savings: Optional[float] = None,
|
199
|
+
achievement_percentage: Optional[float] = None) -> str:
|
200
|
+
"""Format scenario for display in tables and reports."""
|
201
|
+
scenario = self.get_scenario(scenario_key)
|
202
|
+
if not scenario:
|
203
|
+
return f"Unknown scenario: {scenario_key}"
|
204
|
+
|
205
|
+
base_info = f"{scenario.display_name} ({scenario.savings_range_display})"
|
206
|
+
|
207
|
+
if achieved_savings:
|
208
|
+
base_info += f" - Achieved: ${achieved_savings:,.0f}"
|
209
|
+
|
210
|
+
if achievement_percentage:
|
211
|
+
base_info += f" ({achievement_percentage:.0f}% of target)"
|
212
|
+
|
213
|
+
return base_info
|
214
|
+
|
215
|
+
def create_business_case_summary(self) -> Dict[str, Any]:
|
216
|
+
"""Create executive summary of all business cases."""
|
217
|
+
total_min_savings = sum(
|
218
|
+
scenario.target_savings_min or 0
|
219
|
+
for scenario in self.scenarios.values()
|
220
|
+
)
|
221
|
+
|
222
|
+
total_max_savings = sum(
|
223
|
+
scenario.target_savings_max or 0
|
224
|
+
for scenario in self.scenarios.values()
|
225
|
+
if scenario.target_savings_max
|
226
|
+
)
|
227
|
+
|
228
|
+
return {
|
229
|
+
"total_scenarios": len(self.scenarios),
|
230
|
+
"total_potential_min": total_min_savings,
|
231
|
+
"total_potential_max": total_max_savings,
|
232
|
+
"potential_range": f"${total_min_savings:,.0f}-${total_max_savings:,.0f}",
|
233
|
+
"scenarios_by_type": {
|
234
|
+
case_type.value: [
|
235
|
+
s.display_name for s in self.scenarios.values()
|
236
|
+
if s.business_case_type == case_type
|
237
|
+
]
|
238
|
+
for case_type in BusinessCaseType
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
|
243
|
+
# Global configuration manager instance
|
244
|
+
_config_manager = None
|
245
|
+
|
246
|
+
def get_business_case_config() -> BusinessCaseConfigManager:
|
247
|
+
"""Get global business case configuration manager."""
|
248
|
+
global _config_manager
|
249
|
+
if _config_manager is None:
|
250
|
+
_config_manager = BusinessCaseConfigManager()
|
251
|
+
return _config_manager
|
252
|
+
|
253
|
+
|
254
|
+
def get_scenario_display_name(scenario_key: str) -> str:
|
255
|
+
"""Get enterprise-friendly display name for scenario."""
|
256
|
+
config = get_business_case_config()
|
257
|
+
scenario = config.get_scenario(scenario_key)
|
258
|
+
return scenario.display_name if scenario else scenario_key.title()
|
259
|
+
|
260
|
+
|
261
|
+
def get_scenario_savings_range(scenario_key: str) -> str:
|
262
|
+
"""Get savings range display for scenario."""
|
263
|
+
config = get_business_case_config()
|
264
|
+
scenario = config.get_scenario(scenario_key)
|
265
|
+
return scenario.savings_range_display if scenario else "Analysis pending"
|
266
|
+
|
267
|
+
|
268
|
+
def format_business_achievement(scenario_key: str, achieved_savings: float) -> str:
|
269
|
+
"""Format business achievement for executive reporting."""
|
270
|
+
config = get_business_case_config()
|
271
|
+
scenario = config.get_scenario(scenario_key)
|
272
|
+
|
273
|
+
if not scenario:
|
274
|
+
return f"{scenario_key}: ${achieved_savings:,.0f} annual savings"
|
275
|
+
|
276
|
+
# Calculate achievement percentage if target is available
|
277
|
+
achievement_text = f"{scenario.display_name}: ${achieved_savings:,.0f} annual savings"
|
278
|
+
|
279
|
+
if scenario.target_savings_min:
|
280
|
+
percentage = (achieved_savings / scenario.target_savings_min) * 100
|
281
|
+
achievement_text += f" ({percentage:.0f}% of target)"
|
282
|
+
|
283
|
+
return achievement_text
|
284
|
+
|
285
|
+
|
286
|
+
# Migration helper functions for existing hardcoded patterns
|
287
|
+
def migrate_legacy_scenario_reference(legacy_ref: str) -> str:
|
288
|
+
"""
|
289
|
+
Migrate legacy JIRA references to dynamic business case keys.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
legacy_ref: Legacy reference like "FinOps-24", "finops-23", etc.
|
293
|
+
|
294
|
+
Returns:
|
295
|
+
Dynamic business case key
|
296
|
+
"""
|
297
|
+
legacy_mapping = {
|
298
|
+
"finops-24": "workspaces",
|
299
|
+
"FinOps-24": "workspaces",
|
300
|
+
"finops-23": "rds-snapshots",
|
301
|
+
"FinOps-23": "rds-snapshots",
|
302
|
+
"finops-25": "backup-investigation",
|
303
|
+
"FinOps-25": "backup-investigation",
|
304
|
+
"finops-26": "nat-gateway",
|
305
|
+
"FinOps-26": "nat-gateway",
|
306
|
+
"finops-eip": "elastic-ip",
|
307
|
+
"FinOps-EIP": "elastic-ip",
|
308
|
+
"finops-ebs": "ebs-optimization",
|
309
|
+
"FinOps-EBS": "ebs-optimization",
|
310
|
+
"awso-05": "vpc-cleanup",
|
311
|
+
"AWSO-05": "vpc-cleanup"
|
312
|
+
}
|
313
|
+
|
314
|
+
return legacy_mapping.get(legacy_ref, legacy_ref.lower())
|
@@ -684,18 +684,33 @@ def get_cost_data(
|
|
684
684
|
if amount > 0.001: # Filter out negligible costs
|
685
685
|
costs_by_service[service] = amount
|
686
686
|
|
687
|
-
# Calculate period metadata for trend context
|
687
|
+
# Calculate period metadata for trend context with enhanced accuracy assessment
|
688
688
|
current_period_days = (end_date - start_date).days
|
689
689
|
previous_period_days = (previous_period_end - previous_period_start).days
|
690
|
-
|
690
|
+
days_difference = abs(current_period_days - previous_period_days)
|
691
|
+
is_partial_comparison = days_difference > 5
|
692
|
+
|
693
|
+
# ENHANCED RELIABILITY ASSESSMENT: Consider MCP validation success in trend reliability
|
694
|
+
trend_reliability = "high"
|
695
|
+
if is_partial_comparison:
|
696
|
+
if days_difference > 15:
|
697
|
+
trend_reliability = "low"
|
698
|
+
elif days_difference > 10:
|
699
|
+
trend_reliability = "medium"
|
700
|
+
else:
|
701
|
+
# Moderate difference - reliability depends on validation accuracy
|
702
|
+
trend_reliability = "medium_with_validation_support"
|
691
703
|
|
692
704
|
# Enhanced period information for trend analysis
|
693
705
|
period_metadata = {
|
694
706
|
"current_days": current_period_days,
|
695
707
|
"previous_days": previous_period_days,
|
708
|
+
"days_difference": days_difference,
|
696
709
|
"is_partial_comparison": is_partial_comparison,
|
697
|
-
"comparison_type": "
|
698
|
-
"trend_reliability":
|
710
|
+
"comparison_type": "equal_day_comparison" if is_partial_month else "standard_month_comparison",
|
711
|
+
"trend_reliability": trend_reliability,
|
712
|
+
"period_alignment_strategy": "equal_days" if is_partial_month and days_into_month > 1 else "standard_monthly",
|
713
|
+
"supports_mcp_validation": True # This data structure supports MCP cross-validation
|
699
714
|
}
|
700
715
|
|
701
716
|
return {
|
@@ -132,38 +132,57 @@ def estimate_resource_costs(session: boto3.Session, regions: List[str]) -> Dict[
|
|
132
132
|
}
|
133
133
|
|
134
134
|
try:
|
135
|
-
# EC2 Instance cost estimation
|
135
|
+
# EC2 Instance cost estimation using dynamic AWS pricing
|
136
136
|
profile_name = session.profile_name if hasattr(session, "profile_name") else None
|
137
137
|
ec2_data = ec2_summary(session, regions, profile_name)
|
138
|
+
|
139
|
+
from ..common.aws_pricing import get_ec2_monthly_cost, get_aws_pricing_engine
|
140
|
+
from ..common.rich_utils import console
|
141
|
+
|
138
142
|
for instance_type, count in ec2_data.items():
|
139
143
|
if count > 0:
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
"
|
144
|
-
|
145
|
-
|
146
|
-
"
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
144
|
+
try:
|
145
|
+
# Use dynamic AWS pricing - NO hardcoded values
|
146
|
+
# Assume primary region for cost estimation
|
147
|
+
primary_region = regions[0] if regions else "us-east-1"
|
148
|
+
monthly_cost_per_instance = get_ec2_monthly_cost(instance_type, primary_region)
|
149
|
+
total_monthly_cost = monthly_cost_per_instance * count
|
150
|
+
estimated_costs["EC2-Instance"] += total_monthly_cost
|
151
|
+
|
152
|
+
console.print(
|
153
|
+
f"[dim]Dynamic pricing: {count}x {instance_type} = "
|
154
|
+
f"${total_monthly_cost:.2f}/month[/]"
|
155
|
+
)
|
156
|
+
|
157
|
+
except Exception as e:
|
158
|
+
console.print(
|
159
|
+
f"[yellow]⚠ Warning: Could not get dynamic pricing for {instance_type}: {e}[/yellow]"
|
160
|
+
)
|
161
|
+
|
162
|
+
try:
|
163
|
+
# Use fallback pricing engine with AWS patterns
|
164
|
+
pricing_engine = get_aws_pricing_engine(enable_fallback=True)
|
165
|
+
primary_region = regions[0] if regions else "us-east-1"
|
166
|
+
result = pricing_engine.get_ec2_instance_pricing(instance_type, primary_region)
|
167
|
+
total_monthly_cost = result.monthly_cost * count
|
168
|
+
estimated_costs["EC2-Instance"] += total_monthly_cost
|
169
|
+
|
170
|
+
console.print(
|
171
|
+
f"[dim]Fallback pricing: {count}x {instance_type} = "
|
172
|
+
f"${total_monthly_cost:.2f}/month[/]"
|
173
|
+
)
|
174
|
+
|
175
|
+
except Exception as fallback_error:
|
176
|
+
console.print(
|
177
|
+
f"[red]⚠ ERROR: All pricing methods failed for {instance_type}: {fallback_error}[/red]"
|
178
|
+
)
|
179
|
+
console.print(
|
180
|
+
f"[red]Skipping cost estimation for {count}x {instance_type}[/red]"
|
181
|
+
)
|
182
|
+
logger.error(
|
183
|
+
f"ENTERPRISE VIOLATION: Cannot estimate cost for {instance_type} "
|
184
|
+
f"without hardcoded values. Instance type skipped."
|
185
|
+
)
|
167
186
|
|
168
187
|
# Add some EC2-Other costs (EBS, snapshots, etc.)
|
169
188
|
estimated_costs["EC2-Other"] = estimated_costs["EC2-Instance"] * 0.3
|
@@ -165,7 +165,7 @@ class EBSCostOptimizer:
|
|
165
165
|
region_task = progress.add_task("Analyzing regions...", total=len(regions))
|
166
166
|
|
167
167
|
for region in regions:
|
168
|
-
print(f"🔍 Analyzing EBS volumes in {region}")
|
168
|
+
console.print(f"🔍 Analyzing EBS volumes in {region}")
|
169
169
|
|
170
170
|
# Discover volumes in region
|
171
171
|
volumes_data = self._discover_ebs_volumes(region)
|
runbooks/finops/ebs_optimizer.py
CHANGED
@@ -166,15 +166,8 @@ class EBSOptimizer:
|
|
166
166
|
profile_name=get_profile_for_operation("operational", profile_name)
|
167
167
|
)
|
168
168
|
|
169
|
-
# EBS pricing
|
170
|
-
self.ebs_pricing =
|
171
|
-
'gp2': 0.10, # $0.10/GB/month
|
172
|
-
'gp3': 0.08, # $0.08/GB/month (20% cheaper than GP2)
|
173
|
-
'io1': 0.125, # $0.125/GB/month
|
174
|
-
'io2': 0.125, # $0.125/GB/month
|
175
|
-
'st1': 0.045, # $0.045/GB/month
|
176
|
-
'sc1': 0.025, # $0.025/GB/month
|
177
|
-
}
|
169
|
+
# EBS pricing using dynamic AWS pricing engine for universal compatibility
|
170
|
+
self.ebs_pricing = self._initialize_dynamic_ebs_pricing()
|
178
171
|
|
179
172
|
# GP3 conversion savings percentage
|
180
173
|
self.gp3_savings_percentage = 0.20 # 20% savings GP2→GP3
|
@@ -184,6 +177,60 @@ class EBSOptimizer:
|
|
184
177
|
self.low_usage_threshold_bytes = 1_000_000 # 1MB per day
|
185
178
|
self.analysis_period_days = 7
|
186
179
|
|
180
|
+
def _initialize_dynamic_ebs_pricing(self) -> Dict[str, float]:
|
181
|
+
"""Initialize dynamic EBS pricing using AWS pricing engine for universal compatibility."""
|
182
|
+
try:
|
183
|
+
from ..common.aws_pricing import get_service_monthly_cost
|
184
|
+
|
185
|
+
# Get dynamic pricing for common EBS volume types in us-east-1 (base region)
|
186
|
+
base_region = "us-east-1"
|
187
|
+
|
188
|
+
return {
|
189
|
+
'gp2': get_service_monthly_cost("ebs_gp2", base_region, self.profile_name),
|
190
|
+
'gp3': get_service_monthly_cost("ebs_gp3", base_region, self.profile_name),
|
191
|
+
'io1': get_service_monthly_cost("ebs_io1", base_region, self.profile_name),
|
192
|
+
'io2': get_service_monthly_cost("ebs_io2", base_region, self.profile_name),
|
193
|
+
'st1': get_service_monthly_cost("ebs_st1", base_region, self.profile_name),
|
194
|
+
'sc1': get_service_monthly_cost("ebs_sc1", base_region, self.profile_name),
|
195
|
+
}
|
196
|
+
except Exception as e:
|
197
|
+
print_warning(f"Dynamic EBS pricing initialization failed: {e}")
|
198
|
+
print_warning("Attempting AWS Pricing API fallback with universal profile support")
|
199
|
+
|
200
|
+
try:
|
201
|
+
from ..common.aws_pricing import get_aws_pricing_engine
|
202
|
+
|
203
|
+
# Use AWS Pricing API with profile support for universal compatibility
|
204
|
+
pricing_engine = get_aws_pricing_engine(profile=self.profile_name, enable_fallback=True)
|
205
|
+
|
206
|
+
# Get actual AWS pricing instead of hardcoded values
|
207
|
+
gp2_pricing = pricing_engine.get_ebs_pricing("gp2", "us-east-1")
|
208
|
+
gp3_pricing = pricing_engine.get_ebs_pricing("gp3", "us-east-1")
|
209
|
+
io1_pricing = pricing_engine.get_ebs_pricing("io1", "us-east-1")
|
210
|
+
io2_pricing = pricing_engine.get_ebs_pricing("io2", "us-east-1")
|
211
|
+
st1_pricing = pricing_engine.get_ebs_pricing("st1", "us-east-1")
|
212
|
+
sc1_pricing = pricing_engine.get_ebs_pricing("sc1", "us-east-1")
|
213
|
+
|
214
|
+
return {
|
215
|
+
'gp2': gp2_pricing.monthly_cost_per_gb,
|
216
|
+
'gp3': gp3_pricing.monthly_cost_per_gb,
|
217
|
+
'io1': io1_pricing.monthly_cost_per_gb,
|
218
|
+
'io2': io2_pricing.monthly_cost_per_gb,
|
219
|
+
'st1': st1_pricing.monthly_cost_per_gb,
|
220
|
+
'sc1': sc1_pricing.monthly_cost_per_gb,
|
221
|
+
}
|
222
|
+
|
223
|
+
except Exception as pricing_error:
|
224
|
+
print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine EBS pricing without AWS API access: {pricing_error}")
|
225
|
+
print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
|
226
|
+
|
227
|
+
# Return error state instead of hardcoded values to maintain enterprise compliance
|
228
|
+
raise RuntimeError(
|
229
|
+
"Universal compatibility mode requires dynamic AWS pricing API access. "
|
230
|
+
"Please ensure your AWS profile has pricing:GetProducts permissions or configure "
|
231
|
+
"appropriate billing/management profile access."
|
232
|
+
)
|
233
|
+
|
187
234
|
async def analyze_ebs_volumes(self, dry_run: bool = True) -> EBSOptimizerResults:
|
188
235
|
"""
|
189
236
|
Comprehensive EBS volume cost optimization analysis.
|