runbooks 1.0.1__py3-none-any.whl → 1.0.3__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/cloudops/models.py +20 -14
- runbooks/common/aws_pricing_api.py +276 -44
- runbooks/common/dry_run_examples.py +587 -0
- runbooks/common/dry_run_framework.py +520 -0
- runbooks/common/memory_optimization.py +533 -0
- runbooks/common/performance_optimization_engine.py +1153 -0
- runbooks/common/profile_utils.py +10 -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/ebs_cost_optimizer.py +1 -1
- runbooks/finops/embedded_mcp_validator.py +642 -36
- runbooks/finops/executive_export.py +789 -0
- runbooks/finops/finops_scenarios.py +34 -27
- runbooks/finops/notebook_utils.py +1 -1
- runbooks/finops/schemas.py +73 -58
- runbooks/finops/single_dashboard.py +20 -4
- runbooks/finops/vpc_cleanup_exporter.py +2 -1
- 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 +89 -5
- runbooks/main.py +182 -61
- runbooks/operate/vpc_operations.py +60 -31
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/vpc/config.py +17 -8
- runbooks/vpc/heatmap_engine.py +425 -53
- runbooks/vpc/performance_optimized_analyzer.py +546 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/METADATA +15 -15
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/RECORD +35 -27
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/WHEEL +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/top_level.txt +0 -0
runbooks/vpc/config.py
CHANGED
@@ -81,24 +81,33 @@ class AWSCostModel:
|
|
81
81
|
|
82
82
|
@staticmethod
|
83
83
|
def _get_nat_gateway_hourly() -> float:
|
84
|
-
"""Get NAT Gateway hourly cost from AWS Pricing API with
|
84
|
+
"""Get NAT Gateway hourly cost from AWS Pricing API with enhanced enterprise fallback."""
|
85
85
|
if AWS_PRICING_AVAILABLE:
|
86
86
|
try:
|
87
|
-
|
88
|
-
|
89
|
-
|
87
|
+
# Use enhanced pricing API with regional fallback and graceful degradation
|
88
|
+
current_region = os.getenv('AWS_DEFAULT_REGION', 'us-east-1')
|
89
|
+
monthly_cost = pricing_api.get_nat_gateway_monthly_cost(current_region)
|
90
|
+
return monthly_cost / (24 * 30)
|
91
|
+
except Exception as e:
|
92
|
+
print(f"⚠️ NAT Gateway pricing API fallback: {e}")
|
93
|
+
|
90
94
|
# Universal compatibility: standard AWS pricing when API unavailable
|
95
|
+
print("ℹ️ Using universal compatibility NAT Gateway rate")
|
91
96
|
return 0.045 # AWS standard NAT Gateway hourly rate
|
92
97
|
|
93
98
|
@staticmethod
|
94
99
|
def _get_nat_gateway_monthly() -> float:
|
95
|
-
"""Get NAT Gateway monthly cost from AWS Pricing API with
|
100
|
+
"""Get NAT Gateway monthly cost from AWS Pricing API with enhanced enterprise fallback."""
|
96
101
|
if AWS_PRICING_AVAILABLE:
|
97
102
|
try:
|
98
|
-
|
99
|
-
|
100
|
-
|
103
|
+
# Use enhanced pricing API with regional fallback and graceful degradation
|
104
|
+
current_region = os.getenv('AWS_DEFAULT_REGION', 'us-east-1')
|
105
|
+
return pricing_api.get_nat_gateway_monthly_cost(current_region)
|
106
|
+
except Exception as e:
|
107
|
+
print(f"⚠️ NAT Gateway monthly pricing API fallback: {e}")
|
108
|
+
|
101
109
|
# Universal compatibility: calculate from hourly rate
|
110
|
+
print("ℹ️ Calculating monthly cost from universal compatibility hourly rate")
|
102
111
|
return AWSCostModel._get_nat_gateway_hourly() * 24 * 30
|
103
112
|
|
104
113
|
@staticmethod
|
runbooks/vpc/heatmap_engine.py
CHANGED
@@ -15,6 +15,8 @@ from botocore.exceptions import ClientError
|
|
15
15
|
from .config import VPCNetworkingConfig
|
16
16
|
from .cost_engine import NetworkingCostEngine
|
17
17
|
from ..common.env_utils import get_required_env_float
|
18
|
+
from ..common.aws_pricing_api import AWSPricingAPI
|
19
|
+
from ..common.rich_utils import console
|
18
20
|
|
19
21
|
logger = logging.getLogger(__name__)
|
20
22
|
|
@@ -253,22 +255,23 @@ class NetworkingCostHeatMapEngine:
|
|
253
255
|
|
254
256
|
for category, details in account_categories.items():
|
255
257
|
for i in range(details["count"]):
|
256
|
-
# Generate account costs
|
258
|
+
# Generate account costs with dynamic pricing (no hardcoded fallbacks)
|
257
259
|
account_matrix = self._generate_account_costs(str(account_id), category, details["cost_multiplier"])
|
258
260
|
|
259
|
-
# Add to aggregated
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
261
|
+
# Add to aggregated only if pricing data is available
|
262
|
+
if np.sum(account_matrix) > 0: # Only include accounts with valid pricing
|
263
|
+
aggregated_matrix += account_matrix
|
264
|
+
|
265
|
+
# Store breakdown
|
266
|
+
account_breakdown.append(
|
267
|
+
{
|
268
|
+
"account_id": str(account_id),
|
269
|
+
"category": category,
|
270
|
+
"monthly_cost": float(np.sum(account_matrix)),
|
271
|
+
"primary_region": self.config.regions[int(np.argmax(np.sum(account_matrix, axis=1)))],
|
272
|
+
"top_service": list(NETWORKING_SERVICES.keys())[int(np.argmax(np.sum(account_matrix, axis=0)))],
|
273
|
+
}
|
274
|
+
)
|
272
275
|
|
273
276
|
account_id += 1
|
274
277
|
|
@@ -304,8 +307,8 @@ class NetworkingCostHeatMapEngine:
|
|
304
307
|
time_series_data = {}
|
305
308
|
|
306
309
|
for period_name, days in periods.items():
|
307
|
-
# Dynamic base daily cost from
|
308
|
-
base_daily_cost =
|
310
|
+
# Dynamic base daily cost - calculate from dynamic pricing (no hardcoded fallback)
|
311
|
+
base_daily_cost = self._calculate_dynamic_base_daily_cost()
|
309
312
|
|
310
313
|
if period_name == "forecast_90_days":
|
311
314
|
# Forecast with growth trend
|
@@ -382,11 +385,15 @@ class NetworkingCostHeatMapEngine:
|
|
382
385
|
region_total = 0
|
383
386
|
|
384
387
|
for service_idx, (service_key, service_name) in enumerate(NETWORKING_SERVICES.items()):
|
385
|
-
base_cost = base_service_costs.get(service_key,
|
386
|
-
#
|
387
|
-
|
388
|
-
|
389
|
-
|
388
|
+
base_cost = base_service_costs.get(service_key, 0.0) # Default to free (no hardcoded fallback)
|
389
|
+
# Only calculate final cost if base cost is available
|
390
|
+
if base_cost > 0:
|
391
|
+
final_cost = base_cost * region_multiplier
|
392
|
+
regional_matrix[region_idx, service_idx] = max(0, final_cost)
|
393
|
+
region_total += final_cost
|
394
|
+
else:
|
395
|
+
# No pricing data available - set to zero
|
396
|
+
regional_matrix[region_idx, service_idx] = 0.0
|
390
397
|
|
391
398
|
# Track service breakdown
|
392
399
|
if service_key not in service_regional_breakdown:
|
@@ -528,18 +535,28 @@ class NetworkingCostHeatMapEngine:
|
|
528
535
|
|
529
536
|
pattern = patterns.get(category, patterns["development"])
|
530
537
|
|
531
|
-
# Apply costs based on pattern
|
538
|
+
# Apply costs based on pattern using dynamic pricing (NO hardcoded fallbacks)
|
539
|
+
region = self.config.regions[0] if self.config.regions else "us-east-1" # Use first configured region
|
540
|
+
|
541
|
+
# Get dynamic service pricing
|
542
|
+
service_pricing = self._get_dynamic_service_pricing(region)
|
543
|
+
|
532
544
|
for service_idx, service_key in enumerate(NETWORKING_SERVICES.keys()):
|
533
545
|
for region_idx in range(len(self.config.regions)):
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
546
|
+
cost = 0.0 # Default to free
|
547
|
+
|
548
|
+
# Only apply costs if we have valid pricing data
|
549
|
+
if service_key in service_pricing and service_pricing[service_key] > 0:
|
550
|
+
if service_key == "nat_gateway" and region_idx < pattern["nat_gateways"]:
|
551
|
+
cost = service_pricing[service_key] * multiplier
|
552
|
+
elif service_key == "transit_gateway" and pattern["transit_gateway"] and region_idx == 0:
|
553
|
+
cost = service_pricing[service_key] * multiplier
|
554
|
+
elif service_key == "vpc_endpoint" and region_idx < pattern["vpc_endpoints"]:
|
555
|
+
cost = service_pricing[service_key] * multiplier
|
556
|
+
elif service_key == "elastic_ip" and region_idx < pattern.get("elastic_ips", 0):
|
557
|
+
cost = service_pricing[service_key] * multiplier
|
558
|
+
|
559
|
+
matrix[region_idx, service_idx] = cost
|
543
560
|
|
544
561
|
return matrix
|
545
562
|
|
@@ -620,10 +637,14 @@ class NetworkingCostHeatMapEngine:
|
|
620
637
|
|
621
638
|
def _get_dynamic_service_pricing(self, region: str) -> Dict[str, float]:
|
622
639
|
"""
|
623
|
-
Get dynamic AWS service pricing
|
640
|
+
Get dynamic AWS service pricing following enterprise cascade:
|
641
|
+
|
642
|
+
a. ✅ Try Runbooks API with boto3 (dynamic)
|
643
|
+
b. ✅ Try MCP-Servers (dynamic) & gaps analysis with real AWS data
|
644
|
+
→ If failed, identify WHY option 'a' didn't work, then UPGRADE option 'a'
|
645
|
+
c. ✅ Fail gracefully with user guidance (NO hardcoded fallback)
|
624
646
|
|
625
|
-
ENTERPRISE COMPLIANCE: Zero tolerance for hardcoded
|
626
|
-
All pricing must be fetched from AWS APIs.
|
647
|
+
ENTERPRISE COMPLIANCE: Zero tolerance for hardcoded pricing fallbacks.
|
627
648
|
|
628
649
|
Args:
|
629
650
|
region: AWS region for pricing lookup
|
@@ -631,30 +652,381 @@ class NetworkingCostHeatMapEngine:
|
|
631
652
|
Returns:
|
632
653
|
Dictionary of service pricing (monthly USD)
|
633
654
|
"""
|
655
|
+
service_costs = {}
|
656
|
+
pricing_errors = []
|
657
|
+
|
658
|
+
# VPC itself is always free
|
659
|
+
service_costs["vpc"] = 0.0
|
660
|
+
|
661
|
+
# Step A: Try Runbooks Pricing API (Enhanced)
|
662
|
+
console.print(f"[blue]🔄 Step A: Attempting Runbooks Pricing API for {region}[/blue]")
|
634
663
|
try:
|
635
|
-
|
636
|
-
pricing_client = boto3.client('pricing', region_name='us-east-1') # Pricing API only in us-east-1
|
664
|
+
from ..common.aws_pricing_api import AWSPricingAPI
|
637
665
|
|
638
|
-
#
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
666
|
+
# Initialize with proper session management
|
667
|
+
profile = getattr(self, 'profile', None)
|
668
|
+
pricing_api = AWSPricingAPI(profile=profile)
|
669
|
+
|
670
|
+
# NAT Gateway pricing (primary VPC cost component)
|
671
|
+
try:
|
672
|
+
service_costs["nat_gateway"] = pricing_api.get_nat_gateway_monthly_cost(region)
|
673
|
+
console.print(f"[green]✅ NAT Gateway pricing: ${service_costs['nat_gateway']:.2f}/month from Runbooks API[/green]")
|
674
|
+
except Exception as e:
|
675
|
+
pricing_errors.append(f"NAT Gateway: {str(e)}")
|
676
|
+
logger.warning(f"Runbooks API NAT Gateway pricing failed: {e}")
|
677
|
+
|
678
|
+
# Try other services with existing API methods
|
679
|
+
for service_key in ["vpc_endpoint", "transit_gateway", "elastic_ip"]:
|
680
|
+
try:
|
681
|
+
# Check if API method exists for this service
|
682
|
+
if hasattr(pricing_api, f"get_{service_key}_monthly_cost"):
|
683
|
+
method = getattr(pricing_api, f"get_{service_key}_monthly_cost")
|
684
|
+
service_costs[service_key] = method(region)
|
685
|
+
console.print(f"[green]✅ {service_key} pricing: ${service_costs[service_key]:.2f}/month from Runbooks API[/green]")
|
686
|
+
else:
|
687
|
+
pricing_errors.append(f"{service_key}: API method not implemented")
|
688
|
+
except Exception as e:
|
689
|
+
pricing_errors.append(f"{service_key}: {str(e)}")
|
690
|
+
|
691
|
+
# Data transfer pricing (if API available)
|
692
|
+
if "data_transfer" not in service_costs:
|
693
|
+
pricing_errors.append("data_transfer: API method not implemented")
|
645
694
|
|
646
695
|
except Exception as e:
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
696
|
+
pricing_errors.append(f"Runbooks API initialization failed: {str(e)}")
|
697
|
+
console.print(f"[yellow]⚠️ Runbooks Pricing API unavailable: {e}[/yellow]")
|
698
|
+
|
699
|
+
# Step B: MCP Gap Analysis & Validation
|
700
|
+
console.print(f"[blue]🔄 Step B: MCP Gap Analysis for missing pricing data[/blue]")
|
701
|
+
try:
|
702
|
+
missing_services = []
|
703
|
+
for required_service in ["nat_gateway", "vpc_endpoint", "transit_gateway", "elastic_ip", "data_transfer"]:
|
704
|
+
if required_service not in service_costs:
|
705
|
+
missing_services.append(required_service)
|
706
|
+
|
707
|
+
if missing_services:
|
708
|
+
# Use MCP to identify why Runbooks API failed
|
709
|
+
mcp_analysis = self._perform_mcp_pricing_gap_analysis(missing_services, region, pricing_errors)
|
710
|
+
|
711
|
+
# Display MCP analysis results
|
712
|
+
console.print(f"[cyan]📊 MCP Gap Analysis Results:[/cyan]")
|
713
|
+
for service, analysis in mcp_analysis.items():
|
714
|
+
if analysis.get('mcp_validated_cost'):
|
715
|
+
service_costs[service] = analysis['mcp_validated_cost']
|
716
|
+
console.print(f"[green]✅ {service}: ${analysis['mcp_validated_cost']:.2f}/month via MCP validation[/green]")
|
717
|
+
else:
|
718
|
+
console.print(f"[yellow]⚠️ {service}: {analysis.get('gap_reason', 'Unknown gap')}[/yellow]")
|
719
|
+
|
720
|
+
except Exception as e:
|
721
|
+
pricing_errors.append(f"MCP gap analysis failed: {str(e)}")
|
722
|
+
console.print(f"[yellow]⚠️ MCP gap analysis failed: {e}[/yellow]")
|
723
|
+
|
724
|
+
# Step C: Graceful Failure with User Guidance (NO hardcoded fallback)
|
725
|
+
missing_services = []
|
726
|
+
for required_service in ["nat_gateway", "vpc_endpoint", "transit_gateway", "elastic_ip", "data_transfer"]:
|
727
|
+
if required_service not in service_costs:
|
728
|
+
missing_services.append(required_service)
|
729
|
+
|
730
|
+
if missing_services:
|
731
|
+
console.print(f"[red]🚫 ENTERPRISE COMPLIANCE: Cannot proceed with missing pricing data[/red]")
|
732
|
+
|
733
|
+
# Generate comprehensive guidance
|
734
|
+
self._provide_pricing_resolution_guidance(missing_services, pricing_errors, region)
|
735
|
+
|
736
|
+
# Return empty dict to signal failure - DO NOT use hardcoded fallback
|
737
|
+
return {"vpc": 0.0} # Only VPC (free) can be returned
|
738
|
+
|
739
|
+
logger.info(f"✅ Successfully retrieved all service pricing for region: {region}")
|
740
|
+
console.print(f"[green]✅ Complete dynamic pricing loaded for {region} - {len(service_costs)} services[/green]")
|
741
|
+
return service_costs
|
742
|
+
|
743
|
+
def _calculate_dynamic_base_daily_cost(self) -> float:
|
744
|
+
"""
|
745
|
+
Calculate dynamic base daily cost from current pricing data.
|
746
|
+
|
747
|
+
Returns:
|
748
|
+
Daily cost estimate based on dynamic pricing, or 0.0 if unavailable
|
749
|
+
"""
|
750
|
+
try:
|
751
|
+
# Use primary region for calculation
|
752
|
+
region = self.config.regions[0] if self.config.regions else "us-east-1"
|
753
|
+
|
754
|
+
# Get dynamic service pricing
|
755
|
+
service_pricing = self._get_dynamic_service_pricing(region)
|
756
|
+
|
757
|
+
if not service_pricing or len(service_pricing) <= 1: # Only VPC (free) available
|
758
|
+
console.print(f"[yellow]⚠️ No dynamic pricing available for daily cost calculation[/yellow]")
|
759
|
+
return 0.0
|
760
|
+
|
761
|
+
# Calculate daily cost from monthly costs
|
762
|
+
monthly_total = sum(cost for cost in service_pricing.values() if cost > 0)
|
763
|
+
daily_cost = monthly_total / 30.0 # Convert monthly to daily
|
764
|
+
|
765
|
+
console.print(f"[cyan]📊 Dynamic daily cost calculated: ${daily_cost:.2f}/day from available services[/cyan]")
|
766
|
+
return daily_cost
|
767
|
+
|
768
|
+
except Exception as e:
|
769
|
+
logger.warning(f"Dynamic daily cost calculation failed: {e}")
|
770
|
+
console.print(f"[yellow]⚠️ Dynamic daily cost calculation failed: {e}[/yellow]")
|
771
|
+
return 0.0
|
772
|
+
|
773
|
+
def _perform_mcp_pricing_gap_analysis(self, missing_services: List[str], region: str, pricing_errors: List[str]) -> Dict[str, Dict[str, Any]]:
|
774
|
+
"""
|
775
|
+
Perform MCP gap analysis to identify why Runbooks API failed and validate alternatives.
|
776
|
+
|
777
|
+
Args:
|
778
|
+
missing_services: List of services missing pricing data
|
779
|
+
region: AWS region for analysis
|
780
|
+
pricing_errors: List of errors from previous attempts
|
781
|
+
|
782
|
+
Returns:
|
783
|
+
Dictionary of gap analysis results per service
|
784
|
+
"""
|
785
|
+
gap_analysis = {}
|
786
|
+
|
787
|
+
try:
|
788
|
+
# Initialize MCP integration if available
|
789
|
+
from ..common.mcp_integration import EnterpriseMCPIntegrator
|
790
|
+
|
791
|
+
# Get profile for MCP integration
|
792
|
+
profile = getattr(self, 'profile', None)
|
793
|
+
mcp_integrator = EnterpriseMCPIntegrator(user_profile=profile, console_instance=console)
|
794
|
+
|
795
|
+
console.print(f"[cyan]🔍 MCP analyzing {len(missing_services)} missing services...[/cyan]")
|
796
|
+
|
797
|
+
for service in missing_services:
|
798
|
+
analysis = {
|
799
|
+
'service': service,
|
800
|
+
'mcp_validated_cost': None,
|
801
|
+
'gap_reason': None,
|
802
|
+
'resolution_steps': [],
|
803
|
+
'cost_explorer_available': False
|
804
|
+
}
|
805
|
+
|
806
|
+
try:
|
807
|
+
# Step 1: Try Cost Explorer for historical cost data
|
808
|
+
if 'billing' in mcp_integrator.aws_sessions:
|
809
|
+
billing_session = mcp_integrator.aws_sessions['billing']
|
810
|
+
cost_client = billing_session.client('ce')
|
811
|
+
|
812
|
+
# Query for service-specific historical costs
|
813
|
+
historical_cost = self._query_cost_explorer_for_service(cost_client, service, region)
|
814
|
+
|
815
|
+
if historical_cost > 0:
|
816
|
+
analysis['mcp_validated_cost'] = historical_cost
|
817
|
+
analysis['cost_explorer_available'] = True
|
818
|
+
console.print(f"[green]✅ MCP: {service} cost validated via Cost Explorer: ${historical_cost:.2f}/month[/green]")
|
819
|
+
else:
|
820
|
+
analysis['gap_reason'] = f"No historical cost data found in Cost Explorer for {service}"
|
821
|
+
|
822
|
+
# Step 2: Analyze why Runbooks API failed for this service
|
823
|
+
service_errors = [err for err in pricing_errors if service in err.lower()]
|
824
|
+
if service_errors:
|
825
|
+
analysis['gap_reason'] = f"Runbooks API issue: {service_errors[0]}"
|
826
|
+
|
827
|
+
# Determine resolution steps based on error pattern
|
828
|
+
if 'not implemented' in service_errors[0]:
|
829
|
+
analysis['resolution_steps'] = [
|
830
|
+
f"Add get_{service}_monthly_cost() method to AWSPricingAPI class",
|
831
|
+
f"Implement AWS Pricing API query for {service}",
|
832
|
+
"Test with enterprise profiles"
|
833
|
+
]
|
834
|
+
elif 'permission' in service_errors[0].lower() or 'access' in service_errors[0].lower():
|
835
|
+
analysis['resolution_steps'] = [
|
836
|
+
"Add pricing:GetProducts permission to IAM policy",
|
837
|
+
"Ensure profile has Cost Explorer access",
|
838
|
+
f"Test pricing API access for {region} region"
|
839
|
+
]
|
840
|
+
|
841
|
+
except Exception as e:
|
842
|
+
analysis['gap_reason'] = f"MCP analysis failed: {str(e)}"
|
843
|
+
logger.warning(f"MCP gap analysis failed for {service}: {e}")
|
844
|
+
|
845
|
+
gap_analysis[service] = analysis
|
846
|
+
|
847
|
+
return gap_analysis
|
848
|
+
|
849
|
+
except ImportError:
|
850
|
+
console.print(f"[yellow]⚠️ MCP integration not available - basic gap analysis only[/yellow]")
|
851
|
+
# Provide basic gap analysis without MCP
|
852
|
+
for service in missing_services:
|
853
|
+
gap_analysis[service] = {
|
854
|
+
'service': service,
|
855
|
+
'mcp_validated_cost': None,
|
856
|
+
'gap_reason': 'MCP integration not available',
|
857
|
+
'resolution_steps': [
|
858
|
+
'Install MCP dependencies',
|
859
|
+
'Configure MCP integration',
|
860
|
+
'Retry with MCP validation'
|
861
|
+
]
|
862
|
+
}
|
863
|
+
return gap_analysis
|
864
|
+
|
865
|
+
except Exception as e:
|
866
|
+
console.print(f"[yellow]⚠️ MCP gap analysis error: {e}[/yellow]")
|
867
|
+
# Return basic analysis on error
|
868
|
+
for service in missing_services:
|
869
|
+
gap_analysis[service] = {
|
870
|
+
'service': service,
|
871
|
+
'mcp_validated_cost': None,
|
872
|
+
'gap_reason': f'MCP analysis error: {str(e)}',
|
873
|
+
'resolution_steps': ['Check MCP configuration', 'Verify AWS profiles', 'Retry analysis']
|
874
|
+
}
|
875
|
+
return gap_analysis
|
876
|
+
|
877
|
+
def _query_cost_explorer_for_service(self, cost_client, service: str, region: str) -> float:
|
878
|
+
"""
|
879
|
+
Query Cost Explorer for historical service costs to validate pricing.
|
880
|
+
|
881
|
+
Args:
|
882
|
+
cost_client: Boto3 Cost Explorer client
|
883
|
+
service: Service key (nat_gateway, vpc_endpoint, etc.)
|
884
|
+
region: AWS region
|
885
|
+
|
886
|
+
Returns:
|
887
|
+
Monthly cost estimate based on historical data
|
888
|
+
"""
|
889
|
+
try:
|
890
|
+
from datetime import datetime, timedelta
|
891
|
+
|
892
|
+
# Map service keys to AWS Cost Explorer service names
|
893
|
+
service_mapping = {
|
894
|
+
'nat_gateway': 'Amazon Virtual Private Cloud',
|
895
|
+
'vpc_endpoint': 'Amazon Virtual Private Cloud',
|
896
|
+
'transit_gateway': 'Amazon VPC',
|
897
|
+
'elastic_ip': 'Amazon Elastic Compute Cloud - Compute',
|
898
|
+
'data_transfer': 'Amazon CloudFront'
|
657
899
|
}
|
900
|
+
|
901
|
+
aws_service_name = service_mapping.get(service)
|
902
|
+
if not aws_service_name:
|
903
|
+
return 0.0
|
904
|
+
|
905
|
+
# Query last 3 months for more reliable average
|
906
|
+
end_date = datetime.now().date()
|
907
|
+
start_date = end_date - timedelta(days=90)
|
908
|
+
|
909
|
+
response = cost_client.get_cost_and_usage(
|
910
|
+
TimePeriod={
|
911
|
+
'Start': start_date.strftime('%Y-%m-%d'),
|
912
|
+
'End': end_date.strftime('%Y-%m-%d')
|
913
|
+
},
|
914
|
+
Granularity='MONTHLY',
|
915
|
+
Metrics=['BlendedCost'],
|
916
|
+
GroupBy=[
|
917
|
+
{'Type': 'DIMENSION', 'Key': 'SERVICE'},
|
918
|
+
{'Type': 'DIMENSION', 'Key': 'REGION'}
|
919
|
+
],
|
920
|
+
Filter={
|
921
|
+
'And': [
|
922
|
+
{'Dimensions': {'Key': 'SERVICE', 'Values': [aws_service_name]}},
|
923
|
+
{'Dimensions': {'Key': 'REGION', 'Values': [region]}}
|
924
|
+
]
|
925
|
+
}
|
926
|
+
)
|
927
|
+
|
928
|
+
total_cost = 0.0
|
929
|
+
months_with_data = 0
|
930
|
+
|
931
|
+
for result in response['ResultsByTime']:
|
932
|
+
for group in result['Groups']:
|
933
|
+
cost_amount = float(group['Metrics']['BlendedCost']['Amount'])
|
934
|
+
if cost_amount > 0:
|
935
|
+
total_cost += cost_amount
|
936
|
+
months_with_data += 1
|
937
|
+
|
938
|
+
# Calculate average monthly cost
|
939
|
+
if months_with_data > 0:
|
940
|
+
average_monthly_cost = total_cost / months_with_data
|
941
|
+
console.print(f"[cyan]📊 Cost Explorer: {service} average ${average_monthly_cost:.2f}/month over {months_with_data} months[/cyan]")
|
942
|
+
return average_monthly_cost
|
943
|
+
|
944
|
+
return 0.0
|
945
|
+
|
946
|
+
except Exception as e:
|
947
|
+
logger.warning(f"Cost Explorer query failed for {service}: {e}")
|
948
|
+
return 0.0
|
949
|
+
|
950
|
+
def _provide_pricing_resolution_guidance(self, missing_services: List[str], pricing_errors: List[str], region: str) -> None:
|
951
|
+
"""
|
952
|
+
Provide comprehensive guidance for resolving pricing issues.
|
953
|
+
|
954
|
+
Args:
|
955
|
+
missing_services: List of services missing pricing data
|
956
|
+
pricing_errors: List of errors encountered
|
957
|
+
region: AWS region being analyzed
|
958
|
+
"""
|
959
|
+
console.print(f"\n[bold red]🚫 VPC HEAT MAP PRICING RESOLUTION REQUIRED[/bold red]")
|
960
|
+
console.print(f"[red]Cannot generate accurate heat map without dynamic pricing for {len(missing_services)} services[/red]\n")
|
961
|
+
|
962
|
+
# Display comprehensive resolution steps
|
963
|
+
resolution_panel = f"""[bold yellow]📋 ENTERPRISE RESOLUTION STEPS:[/bold yellow]
|
964
|
+
|
965
|
+
[bold cyan]1. IAM Permissions (Most Common Fix):[/bold cyan]
|
966
|
+
Add these policies to your AWS profile:
|
967
|
+
• pricing:GetProducts
|
968
|
+
• ce:GetCostAndUsage
|
969
|
+
• ce:GetDimensionValues
|
970
|
+
|
971
|
+
[bold cyan]2. Runbooks API Enhancement:[/bold cyan]
|
972
|
+
Missing API methods for: {', '.join(missing_services)}
|
973
|
+
|
974
|
+
Add to src/runbooks/common/aws_pricing_api.py:
|
975
|
+
"""
|
976
|
+
|
977
|
+
for service in missing_services:
|
978
|
+
resolution_panel += f"\n • def get_{service}_monthly_cost(self, region: str) -> float"
|
979
|
+
|
980
|
+
resolution_panel += f"""
|
981
|
+
|
982
|
+
[bold cyan]3. Alternative Region Testing:[/bold cyan]
|
983
|
+
Try with regions with better Pricing API support:
|
984
|
+
• --region us-east-1 (best support)
|
985
|
+
• --region us-west-2 (good support)
|
986
|
+
• --region eu-west-1 (EU support)
|
987
|
+
|
988
|
+
[bold cyan]4. Enterprise Override (Temporary):[/bold cyan]"""
|
989
|
+
|
990
|
+
for service in missing_services:
|
991
|
+
service_upper = service.upper()
|
992
|
+
resolution_panel += f"\n export AWS_PRICING_OVERRIDE_{service_upper}_MONTHLY=<cost>"
|
993
|
+
|
994
|
+
resolution_panel += f"""
|
995
|
+
|
996
|
+
[bold cyan]5. MCP Server Integration:[/bold cyan]
|
997
|
+
Ensure MCP servers are accessible and operational
|
998
|
+
Check .mcp.json configuration
|
999
|
+
|
1000
|
+
[bold cyan]6. Profile Validation:[/bold cyan]
|
1001
|
+
Current region: {region}
|
1002
|
+
Verify profile has access to:
|
1003
|
+
• AWS Pricing API (pricing:GetProducts)
|
1004
|
+
• Cost Explorer API (ce:GetCostAndUsage)
|
1005
|
+
|
1006
|
+
💡 [bold green]QUICK TEST:[/bold green]
|
1007
|
+
aws pricing get-products --service-code AmazonVPC --region {region}
|
1008
|
+
|
1009
|
+
🔧 [bold green]DEBUG ERRORS:[/bold green]"""
|
1010
|
+
|
1011
|
+
for i, error in enumerate(pricing_errors[:5], 1): # Show first 5 errors
|
1012
|
+
resolution_panel += f"\n {i}. {error}"
|
1013
|
+
|
1014
|
+
from rich.panel import Panel
|
1015
|
+
guidance_panel = Panel(
|
1016
|
+
resolution_panel,
|
1017
|
+
title="🔧 VPC Pricing Resolution Guide",
|
1018
|
+
style="bold yellow",
|
1019
|
+
expand=True
|
1020
|
+
)
|
1021
|
+
|
1022
|
+
console.print(guidance_panel)
|
1023
|
+
|
1024
|
+
# Specific next steps
|
1025
|
+
console.print(f"\n[bold green]✅ IMMEDIATE NEXT STEPS:[/bold green]")
|
1026
|
+
console.print(f"1. Run: aws pricing get-products --service-code AmazonVPC --region {region}")
|
1027
|
+
console.print(f"2. Check IAM permissions for your current profile")
|
1028
|
+
console.print(f"3. Try alternative region: runbooks vpc --region us-east-1")
|
1029
|
+
console.print(f"4. Contact CloudOps team if pricing API access is restricted\n")
|
658
1030
|
|
659
1031
|
def _add_mcp_validation(self, heat_maps: Dict) -> Dict:
|
660
1032
|
"""Add MCP validation results"""
|