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.
Files changed (35) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cloudops/models.py +20 -14
  3. runbooks/common/aws_pricing_api.py +276 -44
  4. runbooks/common/dry_run_examples.py +587 -0
  5. runbooks/common/dry_run_framework.py +520 -0
  6. runbooks/common/memory_optimization.py +533 -0
  7. runbooks/common/performance_optimization_engine.py +1153 -0
  8. runbooks/common/profile_utils.py +10 -3
  9. runbooks/common/sre_performance_suite.py +574 -0
  10. runbooks/finops/business_case_config.py +314 -0
  11. runbooks/finops/cost_processor.py +19 -4
  12. runbooks/finops/ebs_cost_optimizer.py +1 -1
  13. runbooks/finops/embedded_mcp_validator.py +642 -36
  14. runbooks/finops/executive_export.py +789 -0
  15. runbooks/finops/finops_scenarios.py +34 -27
  16. runbooks/finops/notebook_utils.py +1 -1
  17. runbooks/finops/schemas.py +73 -58
  18. runbooks/finops/single_dashboard.py +20 -4
  19. runbooks/finops/vpc_cleanup_exporter.py +2 -1
  20. runbooks/inventory/models/account.py +5 -3
  21. runbooks/inventory/models/inventory.py +1 -1
  22. runbooks/inventory/models/resource.py +5 -3
  23. runbooks/inventory/organizations_discovery.py +89 -5
  24. runbooks/main.py +182 -61
  25. runbooks/operate/vpc_operations.py +60 -31
  26. runbooks/remediation/workspaces_list.py +2 -2
  27. runbooks/vpc/config.py +17 -8
  28. runbooks/vpc/heatmap_engine.py +425 -53
  29. runbooks/vpc/performance_optimized_analyzer.py +546 -0
  30. {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/METADATA +15 -15
  31. {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/RECORD +35 -27
  32. {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/WHEEL +0 -0
  33. {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/entry_points.txt +0 -0
  34. {runbooks-1.0.1.dist-info → runbooks-1.0.3.dist-info}/licenses/LICENSE +0 -0
  35. {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 universal compatibility."""
84
+ """Get NAT Gateway hourly cost from AWS Pricing API with enhanced enterprise fallback."""
85
85
  if AWS_PRICING_AVAILABLE:
86
86
  try:
87
- return pricing_api.get_nat_gateway_monthly_cost() / (24 * 30)
88
- except Exception:
89
- pass
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 universal compatibility."""
100
+ """Get NAT Gateway monthly cost from AWS Pricing API with enhanced enterprise fallback."""
96
101
  if AWS_PRICING_AVAILABLE:
97
102
  try:
98
- return pricing_api.get_nat_gateway_monthly_cost()
99
- except Exception:
100
- pass
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
@@ -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
- aggregated_matrix += account_matrix
261
-
262
- # Store breakdown
263
- account_breakdown.append(
264
- {
265
- "account_id": str(account_id),
266
- "category": category,
267
- "monthly_cost": float(np.sum(account_matrix)),
268
- "primary_region": self.config.regions[int(np.argmax(np.sum(account_matrix, axis=1)))],
269
- "top_service": list(NETWORKING_SERVICES.keys())[int(np.argmax(np.sum(account_matrix, axis=0)))],
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 environment variable with fallback
308
- base_daily_cost = float(os.getenv('VPC_BASE_DAILY_COST', '10.0'))
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, 10.0)
386
- # REMOVED: Random variation violates enterprise standards
387
- final_cost = base_cost * region_multiplier
388
- regional_matrix[region_idx, service_idx] = max(0, final_cost)
389
- region_total += final_cost
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
- if service_key == "nat_gateway" and region_idx < pattern["nat_gateways"]:
535
- base_nat_cost = float(os.getenv("NAT_GATEWAY_MONTHLY_COST", "45.0"))
536
- matrix[region_idx, service_idx] = base_nat_cost * multiplier
537
- elif service_key == "transit_gateway" and pattern["transit_gateway"] and region_idx == 0:
538
- base_tgw_cost = float(os.getenv("TRANSIT_GATEWAY_MONTHLY_COST", "36.5"))
539
- matrix[region_idx, service_idx] = base_tgw_cost * multiplier
540
- elif service_key == "vpc_endpoint" and region_idx < pattern["vpc_endpoints"]:
541
- base_endpoint_cost = float(os.getenv("VPC_ENDPOINT_MONTHLY_COST", "10.0"))
542
- matrix[region_idx, service_idx] = base_endpoint_cost * multiplier
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 from AWS Pricing API or Cost Explorer.
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 values.
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
- # Try to get pricing from AWS Pricing API
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
- # For now, return error to force proper implementation
639
- logging.error("ENTERPRISE VIOLATION: Dynamic pricing not yet implemented")
640
- raise NotImplementedError(
641
- "CRITICAL: Dynamic pricing integration required. "
642
- "Hardcoded values violate enterprise zero-tolerance policy. "
643
- "Must integrate AWS Pricing API or Cost Explorer."
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
- logging.error(f"Failed to get dynamic pricing: {e}")
648
- # TEMPORARY: Return minimal structure to prevent crashes
649
- # THIS MUST BE REPLACED WITH REAL AWS PRICING API INTEGRATION
650
- return {
651
- "vpc": 0.0, # VPC itself is free
652
- "nat_gateway": 0.0, # MUST be calculated from AWS Pricing API
653
- "vpc_endpoint": 0.0, # MUST be calculated from AWS Pricing API
654
- "transit_gateway": 0.0, # MUST be calculated from AWS Pricing API
655
- "elastic_ip": 0.0, # MUST be calculated from AWS Pricing API
656
- "data_transfer": 0.0, # MUST be calculated from AWS Pricing API
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"""