runbooks 0.9.9__py3-none-any.whl → 1.0.1__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/cloud_foundations_assessment.py +626 -0
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/cloudops/cost_optimizer.py +95 -33
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1353 -0
- runbooks/common/aws_pricing_api.py +205 -0
- runbooks/common/aws_utils.py +2 -2
- runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
- runbooks/common/cross_account_manager.py +606 -0
- runbooks/common/date_utils.py +115 -0
- runbooks/common/enhanced_exception_handler.py +14 -7
- runbooks/common/env_utils.py +96 -0
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/mcp_integration.py +49 -2
- runbooks/common/organizations_client.py +579 -0
- runbooks/common/profile_utils.py +127 -72
- runbooks/common/rich_utils.py +3 -3
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/elastic_ip_optimizer.py +13 -9
- runbooks/finops/embedded_mcp_validator.py +31 -0
- runbooks/finops/enhanced_trend_visualization.py +10 -4
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/markdown_exporter.py +217 -2
- runbooks/finops/nat_gateway_optimizer.py +76 -20
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_exporter.py +28 -26
- runbooks/finops/vpc_cleanup_optimizer.py +363 -16
- runbooks/inventory/__init__.py +10 -1
- runbooks/inventory/cloud_foundations_integration.py +409 -0
- runbooks/inventory/core/collector.py +1177 -94
- runbooks/inventory/discovery.md +339 -0
- runbooks/inventory/drift_detection_cli.py +327 -0
- runbooks/inventory/inventory_mcp_cli.py +171 -0
- runbooks/inventory/inventory_modules.py +6 -9
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/mcp_inventory_validator.py +2149 -0
- runbooks/inventory/mcp_vpc_validator.py +23 -6
- runbooks/inventory/organizations_discovery.py +104 -9
- runbooks/inventory/rich_inventory_display.py +129 -1
- runbooks/inventory/unified_validation_engine.py +1279 -0
- runbooks/inventory/verify_ec2_security_groups.py +3 -1
- runbooks/inventory/vpc_analyzer.py +825 -7
- runbooks/inventory/vpc_flow_analyzer.py +36 -42
- runbooks/main.py +708 -47
- runbooks/monitoring/performance_monitor.py +11 -7
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/dynamodb_operations.py +6 -5
- runbooks/operate/ec2_operations.py +3 -2
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +21 -16
- runbooks/operate/s3_operations.py +13 -12
- runbooks/operate/vpc_operations.py +100 -12
- runbooks/remediation/base.py +4 -2
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +68 -15
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/rds_snapshot_list.py +5 -3
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- 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/__init__.py +21 -1
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +2007 -0
- runbooks/validation/mcp_validator.py +965 -101
- runbooks/validation/terraform_citations_validator.py +363 -0
- runbooks/validation/terraform_drift_detector.py +1098 -0
- runbooks/vpc/cleanup_wrapper.py +231 -10
- runbooks/vpc/config.py +346 -73
- runbooks/vpc/cross_account_session.py +312 -0
- runbooks/vpc/heatmap_engine.py +115 -41
- runbooks/vpc/manager_interface.py +9 -9
- runbooks/vpc/mcp_no_eni_validator.py +1630 -0
- runbooks/vpc/networking_wrapper.py +14 -8
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +4 -2
- runbooks/vpc/unified_scenarios.py +73 -3
- runbooks/vpc/vpc_cleanup_integration.py +512 -78
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/METADATA +94 -52
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/RECORD +101 -81
- 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-0.9.9.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
@@ -22,7 +22,8 @@ AWS_AVAILABLE = True
|
|
22
22
|
|
23
23
|
def get_aws_profiles() -> List[str]:
|
24
24
|
"""Stub implementation - use dashboard_runner.py instead."""
|
25
|
-
|
25
|
+
import os
|
26
|
+
return ["default", os.getenv("BILLING_PROFILE", "default-billing-profile")]
|
26
27
|
|
27
28
|
|
28
29
|
def get_account_id(profile: str = "default") -> str:
|
@@ -46,10 +47,10 @@ class FinOpsConfig:
|
|
46
47
|
include_budget_data: bool = True
|
47
48
|
include_resource_analysis: bool = True
|
48
49
|
|
49
|
-
# Legacy compatibility properties with environment
|
50
|
-
billing_profile: str = "
|
51
|
-
management_profile: str = "
|
52
|
-
operational_profile: str = "
|
50
|
+
# Legacy compatibility properties with universal environment support
|
51
|
+
billing_profile: str = field(default_factory=lambda: os.getenv("BILLING_PROFILE", "default-billing-profile"))
|
52
|
+
management_profile: str = field(default_factory=lambda: os.getenv("MANAGEMENT_PROFILE", "default-management-profile"))
|
53
|
+
operational_profile: str = field(default_factory=lambda: os.getenv("CENTRALISED_OPS_PROFILE", "default-ops-profile"))
|
53
54
|
|
54
55
|
# Additional expected attributes from tests
|
55
56
|
time_range_days: int = 30
|
runbooks/finops/iam_guidance.py
CHANGED
@@ -11,6 +11,8 @@ from rich.console import Console
|
|
11
11
|
from rich.panel import Panel
|
12
12
|
from rich.table import Table
|
13
13
|
|
14
|
+
from runbooks.common import get_aws_cli_example_period
|
15
|
+
|
14
16
|
console = Console()
|
15
17
|
|
16
18
|
|
@@ -311,6 +313,9 @@ def handle_cost_explorer_error(error: Exception, profile_name: Optional[str] = N
|
|
311
313
|
|
312
314
|
def _display_single_account_cost_explorer_guidance(error: Exception, profile_name: Optional[str] = None):
|
313
315
|
"""Display context-aware guidance for single account Cost Explorer limitations."""
|
316
|
+
|
317
|
+
# Get dynamic date period for CLI examples
|
318
|
+
start_date, end_date = get_aws_cli_example_period()
|
314
319
|
|
315
320
|
# Main explanation panel
|
316
321
|
explanation_panel = Panel(
|
@@ -354,7 +359,7 @@ def _display_single_account_cost_explorer_guidance(error: Exception, profile_nam
|
|
354
359
|
f"[green]✅ Recommended Solutions:[/green]\n\n"
|
355
360
|
f"{solution_commands}\n\n"
|
356
361
|
f"[bold]🎯 Quick Test Commands:[/bold]\n"
|
357
|
-
f"• Test billing access: `aws ce get-cost-and-usage --time-period Start=
|
362
|
+
f"• Test billing access: `aws ce get-cost-and-usage --time-period Start={start_date},End={end_date} --granularity MONTHLY --metrics UnblendedCost --profile your-billing-profile`\n"
|
358
363
|
f"• List available profiles: `aws configure list-profiles`\n"
|
359
364
|
f"• Check current identity: `aws sts get-caller-identity --profile {profile_name or 'your-profile'}`\n\n"
|
360
365
|
f"[bold]💡 Alternative Approach:[/bold]\n"
|
@@ -458,7 +458,7 @@ class MarkdownExporter:
|
|
458
458
|
"| " + " | ".join(["---" for _ in headers]) + " |"
|
459
459
|
]
|
460
460
|
|
461
|
-
# Process each VPC candidate
|
461
|
+
# Process each VPC candidate with enhanced data extraction
|
462
462
|
for candidate in vpc_candidates:
|
463
463
|
# Extract data with safe attribute access and formatting
|
464
464
|
account_id = getattr(candidate, 'account_id', 'Unknown')
|
@@ -466,7 +466,44 @@ class MarkdownExporter:
|
|
466
466
|
vpc_name = getattr(candidate, 'vpc_name', '') or 'Unnamed'
|
467
467
|
cidr_block = getattr(candidate, 'cidr_block', 'Unknown')
|
468
468
|
|
469
|
-
# Handle overlapping logic -
|
469
|
+
# Handle overlapping logic - check CIDR conflicts
|
470
|
+
overlapping = self._check_cidr_overlapping(cidr_block, vpc_candidates)
|
471
|
+
|
472
|
+
# Enhanced is_default handling
|
473
|
+
is_default = getattr(candidate, 'is_default', False)
|
474
|
+
is_default_display = "⚠️ Yes" if is_default else "No"
|
475
|
+
|
476
|
+
# Enhanced ENI count
|
477
|
+
dependency_analysis = getattr(candidate, 'dependency_analysis', None)
|
478
|
+
eni_count = dependency_analysis.eni_count if dependency_analysis else 0
|
479
|
+
|
480
|
+
# Enhanced tags with owner focus
|
481
|
+
tags_dict = getattr(candidate, 'tags', {}) or {}
|
482
|
+
tags_display = self._format_tags_for_owners_display(tags_dict)
|
483
|
+
|
484
|
+
# Flow logs detection
|
485
|
+
flow_logs = self._detect_flow_logs(candidate)
|
486
|
+
|
487
|
+
# TGW/Peering detection
|
488
|
+
tgw_peering = self._detect_tgw_peering(candidate)
|
489
|
+
|
490
|
+
# Load balancers detection
|
491
|
+
lbs_present = self._detect_load_balancers(candidate)
|
492
|
+
|
493
|
+
# IaC detection from tags
|
494
|
+
iac_detected = self._detect_iac_from_tags(tags_dict)
|
495
|
+
|
496
|
+
# Timeline estimation based on VPC state
|
497
|
+
timeline = self._estimate_cleanup_timeline(candidate)
|
498
|
+
|
499
|
+
# Decision based on bucket classification
|
500
|
+
decision = self._determine_cleanup_decision(candidate)
|
501
|
+
|
502
|
+
# Enhanced owners/approvals extraction
|
503
|
+
owners_approvals = self._extract_owners_approvals(tags_dict, is_default)
|
504
|
+
|
505
|
+
# Notes based on VPC characteristics
|
506
|
+
notes = self._generate_vpc_notes(candidate)
|
470
507
|
overlapping = "Yes" if getattr(candidate, 'overlapping', False) else "No"
|
471
508
|
|
472
509
|
# Format boolean indicators with emoji
|
@@ -649,6 +686,184 @@ class MarkdownExporter:
|
|
649
686
|
print_warning(f"❌ Failed to export VPC analysis: {e}")
|
650
687
|
return ""
|
651
688
|
|
689
|
+
def _check_cidr_overlapping(self, cidr_block: str, vpc_candidates: List[Any]) -> str:
|
690
|
+
"""Check for CIDR block overlapping across VPCs."""
|
691
|
+
if not cidr_block or not vpc_candidates:
|
692
|
+
return "No"
|
693
|
+
|
694
|
+
# Simple overlapping check - in enterprise scenario, this would use more sophisticated logic
|
695
|
+
current_cidr = cidr_block
|
696
|
+
for candidate in vpc_candidates:
|
697
|
+
other_cidr = getattr(candidate, 'cidr_block', None)
|
698
|
+
if other_cidr and other_cidr != current_cidr and current_cidr.startswith(other_cidr.split('/')[0].rsplit('.', 1)[0]):
|
699
|
+
return "Yes"
|
700
|
+
|
701
|
+
return "No"
|
702
|
+
|
703
|
+
def _detect_flow_logs(self, candidate: Any) -> str:
|
704
|
+
"""Detect if VPC has flow logs enabled."""
|
705
|
+
return "Yes" if getattr(candidate, 'flow_logs_enabled', False) else "No"
|
706
|
+
|
707
|
+
def _detect_tgw_peering(self, candidate: Any) -> str:
|
708
|
+
"""Analyze Transit Gateway and VPC peering connections."""
|
709
|
+
# Check for TGW attachments and peering connections
|
710
|
+
tgw_attachments = getattr(candidate, 'tgw_attachments', []) or []
|
711
|
+
peering_connections = getattr(candidate, 'peering_connections', []) or []
|
712
|
+
|
713
|
+
if tgw_attachments or peering_connections:
|
714
|
+
connection_count = len(tgw_attachments) + len(peering_connections)
|
715
|
+
return f"Yes ({connection_count})"
|
716
|
+
return "No"
|
717
|
+
|
718
|
+
def _detect_load_balancers(self, candidate: Any) -> str:
|
719
|
+
"""Detect load balancers in the VPC."""
|
720
|
+
load_balancers = getattr(candidate, 'load_balancers', []) or []
|
721
|
+
return "Yes" if load_balancers else "No"
|
722
|
+
|
723
|
+
def _detect_iac_from_tags(self, tags_dict: dict) -> str:
|
724
|
+
"""Detect Infrastructure as Code management from tags."""
|
725
|
+
iac_keys = ['aws:cloudformation:stack-name', 'terraform:module', 'cdktf:stack', 'pulumi:project']
|
726
|
+
for key in iac_keys:
|
727
|
+
if key in tags_dict and tags_dict[key]:
|
728
|
+
return "Yes"
|
729
|
+
return "No"
|
730
|
+
|
731
|
+
def _estimate_cleanup_timeline(self, candidate: Any) -> str:
|
732
|
+
"""Estimate cleanup timeline based on complexity."""
|
733
|
+
# Simple heuristic based on dependencies
|
734
|
+
if hasattr(candidate, 'dependency_analysis') and candidate.dependency_analysis:
|
735
|
+
eni_count = getattr(candidate.dependency_analysis, 'eni_count', 0)
|
736
|
+
else:
|
737
|
+
eni_count = 0
|
738
|
+
|
739
|
+
if eni_count == 0:
|
740
|
+
return "1-2 days"
|
741
|
+
elif eni_count < 5:
|
742
|
+
return "3-5 days"
|
743
|
+
else:
|
744
|
+
return "1-2 weeks"
|
745
|
+
|
746
|
+
def _format_cleanup_decision(self, candidate: Any) -> str:
|
747
|
+
"""Format cleanup decision recommendation."""
|
748
|
+
recommendation = getattr(candidate, 'cleanup_recommendation', 'unknown')
|
749
|
+
if recommendation == 'delete':
|
750
|
+
return "Delete"
|
751
|
+
elif recommendation == 'keep':
|
752
|
+
return "Keep"
|
753
|
+
elif recommendation == 'review':
|
754
|
+
return "Review"
|
755
|
+
else:
|
756
|
+
return "TBD"
|
757
|
+
|
758
|
+
def _format_tags_for_owners_display(self, tags_dict: dict) -> str:
|
759
|
+
"""Format tags for display with priority on ownership information."""
|
760
|
+
if not tags_dict:
|
761
|
+
return "No tags"
|
762
|
+
|
763
|
+
# Priority keys focusing on ownership and approvals
|
764
|
+
priority_keys = ['Name', 'Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact']
|
765
|
+
relevant_tags = []
|
766
|
+
|
767
|
+
for key in priority_keys:
|
768
|
+
if key in tags_dict and tags_dict[key]:
|
769
|
+
relevant_tags.append(f"{key}:{tags_dict[key]}")
|
770
|
+
if len(relevant_tags) >= 3: # Limit for table readability
|
771
|
+
break
|
772
|
+
|
773
|
+
return "; ".join(relevant_tags) if relevant_tags else f"({len(tags_dict)} tags)"
|
774
|
+
|
775
|
+
def _determine_cleanup_decision(self, candidate: Any) -> str:
|
776
|
+
"""Determine cleanup decision based on VPC analysis."""
|
777
|
+
# Check the cleanup bucket from three-bucket strategy
|
778
|
+
cleanup_bucket = getattr(candidate, 'cleanup_bucket', 'unknown')
|
779
|
+
|
780
|
+
if cleanup_bucket == 'bucket_1':
|
781
|
+
return "Delete"
|
782
|
+
elif cleanup_bucket == 'bucket_2':
|
783
|
+
return "Review"
|
784
|
+
elif cleanup_bucket == 'bucket_3':
|
785
|
+
return "Keep"
|
786
|
+
else:
|
787
|
+
# Fallback logic based on other attributes
|
788
|
+
is_default = getattr(candidate, 'is_default', False)
|
789
|
+
has_eni = getattr(candidate, 'eni_count', 0) > 0
|
790
|
+
|
791
|
+
if is_default and not has_eni:
|
792
|
+
return "Delete"
|
793
|
+
elif has_eni:
|
794
|
+
return "Review"
|
795
|
+
else:
|
796
|
+
return "TBD"
|
797
|
+
|
798
|
+
def _extract_owners_approvals(self, tags_dict: dict, is_default: bool) -> str:
|
799
|
+
"""Extract owners and approval information from tags and VPC status."""
|
800
|
+
# Extract from tags with enhanced owner detection
|
801
|
+
owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
|
802
|
+
|
803
|
+
extracted_owners = []
|
804
|
+
for key in owner_keys:
|
805
|
+
if key in tags_dict and tags_dict[key]:
|
806
|
+
value = tags_dict[key]
|
807
|
+
if 'business' in key.lower():
|
808
|
+
extracted_owners.append(f"{value} (Business)")
|
809
|
+
elif 'technical' in key.lower():
|
810
|
+
extracted_owners.append(f"{value} (Technical)")
|
811
|
+
elif 'team' in key.lower():
|
812
|
+
extracted_owners.append(f"{value} (Team)")
|
813
|
+
else:
|
814
|
+
extracted_owners.append(f"{value} ({key})")
|
815
|
+
|
816
|
+
if len(extracted_owners) >= 2: # Limit for table readability
|
817
|
+
break
|
818
|
+
|
819
|
+
if extracted_owners:
|
820
|
+
return "; ".join(extracted_owners)
|
821
|
+
|
822
|
+
# Fallback based on VPC type
|
823
|
+
if is_default:
|
824
|
+
return "System Default VPC"
|
825
|
+
else:
|
826
|
+
# Check for IaC tags
|
827
|
+
iac_keys = ['aws:cloudformation:stack-name', 'terraform:module', 'cdktf:stack', 'pulumi:project']
|
828
|
+
for key in iac_keys:
|
829
|
+
if key in tags_dict and tags_dict[key]:
|
830
|
+
return "IaC Managed"
|
831
|
+
return "No owner tags found"
|
832
|
+
|
833
|
+
def _generate_vpc_notes(self, candidate: Any) -> str:
|
834
|
+
"""Generate comprehensive notes for VPC candidate."""
|
835
|
+
notes = []
|
836
|
+
|
837
|
+
# Add bucket classification note
|
838
|
+
cleanup_bucket = getattr(candidate, 'cleanup_bucket', 'unknown')
|
839
|
+
if cleanup_bucket == 'bucket_1':
|
840
|
+
notes.append("Internal data plane - safe for cleanup")
|
841
|
+
elif cleanup_bucket == 'bucket_2':
|
842
|
+
notes.append("External interconnects - requires analysis")
|
843
|
+
elif cleanup_bucket == 'bucket_3':
|
844
|
+
notes.append("Control plane - manual review required")
|
845
|
+
|
846
|
+
# Add ENI count if significant
|
847
|
+
if hasattr(candidate, 'dependency_analysis') and candidate.dependency_analysis:
|
848
|
+
eni_count = getattr(candidate.dependency_analysis, 'eni_count', 0)
|
849
|
+
if eni_count > 0:
|
850
|
+
notes.append(f"{eni_count} ENI attachments")
|
851
|
+
|
852
|
+
# Add default VPC note
|
853
|
+
if getattr(candidate, 'is_default', False):
|
854
|
+
notes.append("Default VPC (CIS compliance issue)")
|
855
|
+
|
856
|
+
# Add IaC detection
|
857
|
+
if getattr(candidate, 'iac_detected', False):
|
858
|
+
notes.append("IaC managed")
|
859
|
+
|
860
|
+
# Add security concerns
|
861
|
+
risk_level = getattr(candidate, 'risk_level', 'unknown')
|
862
|
+
if risk_level == 'high':
|
863
|
+
notes.append("High security risk")
|
864
|
+
|
865
|
+
return "; ".join(notes) if notes else "Standard VPC cleanup candidate"
|
866
|
+
|
652
867
|
|
653
868
|
def export_finops_to_markdown(
|
654
869
|
profile_data: Union[Dict[str, Any], List[Dict[str, Any]]],
|
@@ -49,6 +49,7 @@ from ..common.rich_utils import (
|
|
49
49
|
console, print_header, print_success, print_error, print_warning, print_info,
|
50
50
|
create_table, create_progress_bar, format_cost, create_panel, STATUS_INDICATORS
|
51
51
|
)
|
52
|
+
from ..common.aws_pricing import get_service_monthly_cost, calculate_annual_cost
|
52
53
|
from .embedded_mcp_validator import EmbeddedMCPValidator
|
53
54
|
from ..common.profile_utils import get_profile_for_operation
|
54
55
|
|
@@ -139,14 +140,24 @@ class NATGatewayOptimizer:
|
|
139
140
|
profile_name=get_profile_for_operation("operational", profile_name)
|
140
141
|
)
|
141
142
|
|
142
|
-
# NAT Gateway pricing
|
143
|
-
|
144
|
-
self.
|
143
|
+
# NAT Gateway pricing - using dynamic pricing engine
|
144
|
+
# Base monthly cost calculation (will be applied per region)
|
145
|
+
self._base_monthly_cost_us_east_1 = get_service_monthly_cost("nat_gateway", "us-east-1")
|
146
|
+
self.nat_gateway_data_processing_cost = get_service_monthly_cost("data_transfer", "us-east-1") # Dynamic data transfer pricing
|
145
147
|
|
146
148
|
# Enterprise thresholds for optimization recommendations
|
147
149
|
self.low_usage_threshold_connections = 10 # Active connections per day
|
148
150
|
self.low_usage_threshold_bytes = 1_000_000 # 1MB per day
|
149
151
|
self.analysis_period_days = 7 # CloudWatch analysis period
|
152
|
+
|
153
|
+
def _get_regional_monthly_cost(self, region: str) -> float:
|
154
|
+
"""Get dynamic monthly NAT Gateway cost for specified region."""
|
155
|
+
try:
|
156
|
+
return get_service_monthly_cost("nat_gateway", region)
|
157
|
+
except Exception:
|
158
|
+
# Fallback to regional cost calculation using dynamic pricing
|
159
|
+
from ..common.aws_pricing import calculate_regional_cost
|
160
|
+
return calculate_regional_cost(self._base_monthly_cost_us_east_1, region, "nat_gateway", self.profile_name)
|
150
161
|
|
151
162
|
async def analyze_nat_gateways(self, dry_run: bool = True) -> NATGatewayOptimizerResults:
|
152
163
|
"""
|
@@ -413,9 +424,9 @@ class NATGatewayOptimizer:
|
|
413
424
|
metrics = usage_metrics.get(nat_gateway.nat_gateway_id)
|
414
425
|
route_tables = dependencies.get(nat_gateway.nat_gateway_id, [])
|
415
426
|
|
416
|
-
# Calculate current costs
|
417
|
-
monthly_cost = self.
|
418
|
-
annual_cost = monthly_cost
|
427
|
+
# Calculate current costs using dynamic pricing
|
428
|
+
monthly_cost = self._get_regional_monthly_cost(nat_gateway.region)
|
429
|
+
annual_cost = calculate_annual_cost(monthly_cost)
|
419
430
|
|
420
431
|
# Determine optimization recommendation
|
421
432
|
recommendation = "retain" # Default: keep the NAT Gateway
|
@@ -724,9 +735,9 @@ class TransitGatewayCostAnalysis(BaseModel):
|
|
724
735
|
"""Transit Gateway cost analysis results"""
|
725
736
|
transit_gateway_id: str
|
726
737
|
region: str
|
727
|
-
monthly_base_cost: float =
|
738
|
+
monthly_base_cost: float = 0.0 # Will be calculated dynamically based on region
|
728
739
|
attachment_count: int = 0
|
729
|
-
attachment_hourly_cost: float = 0.05 # $0.05/hour per attachment
|
740
|
+
attachment_hourly_cost: float = 0.05 # $0.05/hour per attachment (attachment pricing)
|
730
741
|
data_processing_cost: float = 0.0
|
731
742
|
total_monthly_cost: float = 0.0
|
732
743
|
annual_cost: float = 0.0
|
@@ -737,7 +748,7 @@ class NetworkDataTransferCostAnalysis(BaseModel):
|
|
737
748
|
"""Network data transfer cost analysis"""
|
738
749
|
region_pair: str # e.g., "us-east-1 -> us-west-2"
|
739
750
|
monthly_gb_transferred: float = 0.0
|
740
|
-
cost_per_gb: float = 0.
|
751
|
+
cost_per_gb: float = 0.0 # Will be calculated dynamically based on region pair
|
741
752
|
monthly_transfer_cost: float = 0.0
|
742
753
|
annual_transfer_cost: float = 0.0
|
743
754
|
optimization_recommendations: List[str] = Field(default_factory=list)
|
@@ -763,17 +774,62 @@ class EnhancedVPCCostOptimizer:
|
|
763
774
|
self.profile = profile
|
764
775
|
self.nat_optimizer = NATGatewayOptimizer(profile=profile)
|
765
776
|
|
766
|
-
#
|
767
|
-
self.cost_model =
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
"
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
+
# Dynamic cost model using AWS pricing engine
|
778
|
+
self.cost_model = self._initialize_dynamic_cost_model()
|
779
|
+
|
780
|
+
def _initialize_dynamic_cost_model(self) -> Dict[str, float]:
|
781
|
+
"""Initialize dynamic cost model using AWS pricing engine with universal compatibility."""
|
782
|
+
try:
|
783
|
+
# Get base pricing for us-east-1, then apply regional multipliers as needed
|
784
|
+
base_region = "us-east-1"
|
785
|
+
|
786
|
+
return {
|
787
|
+
"nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region, self.profile),
|
788
|
+
"nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region, self.profile),
|
789
|
+
"transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region, self.profile),
|
790
|
+
"vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region, self.profile),
|
791
|
+
"vpc_endpoint_interface_hourly": get_service_monthly_cost("vpc_endpoint_interface", base_region, self.profile) / (24 * 30),
|
792
|
+
"transit_gateway_attachment_hourly": get_service_monthly_cost("transit_gateway_attachment", base_region, self.profile) / (24 * 30),
|
793
|
+
"data_transfer_regional": get_service_monthly_cost("data_transfer", base_region, self.profile),
|
794
|
+
"data_transfer_internet": get_service_monthly_cost("data_transfer", base_region, self.profile) * 4.5, # Internet is ~4.5x higher
|
795
|
+
}
|
796
|
+
except Exception as e:
|
797
|
+
print_warning(f"Dynamic pricing initialization failed: {e}")
|
798
|
+
print_warning("Attempting AWS Pricing API fallback with universal profile support")
|
799
|
+
# Enhanced fallback with AWS Pricing API integration
|
800
|
+
from ..common.aws_pricing import get_aws_pricing_engine, AWSOfficialPricingEngine
|
801
|
+
|
802
|
+
try:
|
803
|
+
# Use AWS Pricing API with profile support for universal compatibility
|
804
|
+
pricing_engine = get_aws_pricing_engine(profile=self.profile, enable_fallback=True)
|
805
|
+
|
806
|
+
# Get actual AWS pricing instead of hardcoded values
|
807
|
+
nat_gateway_pricing = pricing_engine.get_nat_gateway_pricing("us-east-1")
|
808
|
+
transit_gateway_pricing = pricing_engine.get_transit_gateway_pricing("us-east-1")
|
809
|
+
vpc_endpoint_pricing = pricing_engine.get_vpc_endpoint_pricing("us-east-1")
|
810
|
+
data_transfer_pricing = pricing_engine.get_data_transfer_pricing("us-east-1", "internet")
|
811
|
+
|
812
|
+
return {
|
813
|
+
"nat_gateway_monthly": nat_gateway_pricing.monthly_cost,
|
814
|
+
"nat_gateway_data_processing": data_transfer_pricing.cost_per_gb,
|
815
|
+
"transit_gateway_monthly": transit_gateway_pricing.monthly_cost,
|
816
|
+
"transit_gateway_attachment_hourly": transit_gateway_pricing.attachment_hourly_cost,
|
817
|
+
"vpc_endpoint_interface_hourly": vpc_endpoint_pricing.interface_hourly_cost,
|
818
|
+
"data_transfer_regional": data_transfer_pricing.cost_per_gb * 0.1, # Regional is ~10% of internet cost
|
819
|
+
"data_transfer_cross_region": data_transfer_pricing.cost_per_gb * 0.2, # Cross-region is ~20% of internet cost
|
820
|
+
"data_transfer_internet": data_transfer_pricing.cost_per_gb
|
821
|
+
}
|
822
|
+
|
823
|
+
except Exception as pricing_error:
|
824
|
+
print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine pricing without AWS API access: {pricing_error}")
|
825
|
+
print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
|
826
|
+
|
827
|
+
# Return error state instead of hardcoded values to maintain enterprise compliance
|
828
|
+
raise RuntimeError(
|
829
|
+
"Universal compatibility mode requires dynamic AWS pricing API access. "
|
830
|
+
"Please ensure your AWS profile has pricing:GetProducts permissions or configure "
|
831
|
+
"appropriate billing/management profile access."
|
832
|
+
)
|
777
833
|
|
778
834
|
async def analyze_comprehensive_vpc_costs(self, profile: Optional[str] = None,
|
779
835
|
regions: Optional[List[str]] = None) -> Dict[str, Any]:
|
@@ -34,6 +34,8 @@ try:
|
|
34
34
|
except ImportError:
|
35
35
|
# Define mock_costexplorer as a no-op decorator for compatibility
|
36
36
|
def mock_costexplorer(func):
|
37
|
+
# Dynamic test period for consistent test data
|
38
|
+
test_period = get_test_date_period(30)
|
37
39
|
def wrapper(*args, **kwargs):
|
38
40
|
return func(*args, **kwargs)
|
39
41
|
|
@@ -128,7 +130,7 @@ class TestAWSIntegrationWithMoto:
|
|
128
130
|
mock_cost_data = {
|
129
131
|
"ResultsByTime": [
|
130
132
|
{
|
131
|
-
"TimePeriod": {"Start": "
|
133
|
+
"TimePeriod": {"Start": test_period["Start"], "End": test_period["End"]},
|
132
134
|
"Total": {"UnblendedCost": {"Amount": "50000.00", "Unit": "USD"}},
|
133
135
|
"Groups": [
|
134
136
|
{"Keys": ["EC2-Instance"], "Metrics": {"UnblendedCost": {"Amount": "20000.00", "Unit": "USD"}}},
|
@@ -19,20 +19,28 @@ from .markdown_exporter import MarkdownExporter
|
|
19
19
|
|
20
20
|
|
21
21
|
def _format_tags_for_display(tags_dict: Dict[str, str]) -> str:
|
22
|
-
"""Format tags for display with priority order."""
|
22
|
+
"""Format tags for display with priority order, emphasizing ownership tags."""
|
23
23
|
if not tags_dict:
|
24
24
|
return "No tags"
|
25
25
|
|
26
|
-
|
26
|
+
# Enhanced priority keys with focus on ownership and approvals
|
27
|
+
priority_keys = ['Name', 'Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact',
|
28
|
+
'Environment', 'Project', 'CostCenter', 'CreatedBy', 'ManagedBy']
|
27
29
|
relevant_tags = []
|
28
30
|
|
29
31
|
for key in priority_keys:
|
30
32
|
if key in tags_dict and tags_dict[key]:
|
31
33
|
relevant_tags.append(f"{key}:{tags_dict[key]}")
|
32
34
|
|
35
|
+
# Add CloudFormation/Terraform tags for IaC detection
|
36
|
+
iac_keys = ['aws:cloudformation:stack-name', 'terraform:module', 'cdktf:stack', 'pulumi:project']
|
37
|
+
for key in iac_keys:
|
38
|
+
if key in tags_dict and tags_dict[key] and len(relevant_tags) < 6:
|
39
|
+
relevant_tags.append(f"IaC:{tags_dict[key]}")
|
40
|
+
|
33
41
|
# Add other important tags
|
34
42
|
for key, value in tags_dict.items():
|
35
|
-
if key not in priority_keys and value and len(relevant_tags) < 5:
|
43
|
+
if key not in priority_keys + iac_keys and value and len(relevant_tags) < 5:
|
36
44
|
relevant_tags.append(f"{key}:{value}")
|
37
45
|
|
38
46
|
return "; ".join(relevant_tags) if relevant_tags else f"({len(tags_dict)} tags)"
|
@@ -118,40 +126,34 @@ def _export_vpc_candidates_csv(vpc_candidates: List[Any], output_dir: str) -> st
|
|
118
126
|
# Extract data with enhanced tag and owner handling
|
119
127
|
tags_dict = getattr(candidate, 'tags', {}) or {}
|
120
128
|
|
121
|
-
#
|
122
|
-
|
123
|
-
priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team']
|
124
|
-
relevant_tags = []
|
125
|
-
for key in priority_keys:
|
126
|
-
if key in tags_dict and tags_dict[key]:
|
127
|
-
relevant_tags.append(f"{key}:{tags_dict[key]}")
|
128
|
-
|
129
|
-
# Add other important tags
|
130
|
-
for key, value in tags_dict.items():
|
131
|
-
if key not in priority_keys and value and len(relevant_tags) < 5:
|
132
|
-
relevant_tags.append(f"{key}:{value}")
|
133
|
-
|
134
|
-
tags_str = "; ".join(relevant_tags)
|
135
|
-
else:
|
136
|
-
tags_str = "No tags"
|
129
|
+
# Use enhanced tag formatting function
|
130
|
+
tags_str = _format_tags_for_display(tags_dict)
|
137
131
|
|
138
132
|
load_balancers = getattr(candidate, 'load_balancers', []) or []
|
139
133
|
lbs_present = "Yes" if load_balancers else "No"
|
140
134
|
|
141
|
-
# Enhanced owner extraction
|
135
|
+
# Enhanced owner extraction from multiple sources
|
142
136
|
owners = getattr(candidate, 'owners_approvals', []) or []
|
143
137
|
|
144
|
-
#
|
138
|
+
# Extract owners from tags with enhanced logic
|
145
139
|
if not owners and tags_dict:
|
146
140
|
owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
|
147
141
|
for key in owner_keys:
|
148
142
|
if key in tags_dict and tags_dict[key]:
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
143
|
+
value = tags_dict[key]
|
144
|
+
if 'business' in key.lower() or 'manager' in value.lower():
|
145
|
+
owners.append(f"{value} (Business)")
|
146
|
+
elif 'technical' in key.lower() or 'engineer' in value.lower():
|
147
|
+
owners.append(f"{value} (Technical)")
|
148
|
+
elif 'team' in key.lower():
|
149
|
+
owners.append(f"{value} (Team)")
|
153
150
|
else:
|
154
|
-
owners.append(
|
151
|
+
owners.append(f"{value} ({key})")
|
152
|
+
|
153
|
+
# For default VPCs, add system indicator
|
154
|
+
is_default = getattr(candidate, 'is_default', False)
|
155
|
+
if is_default and not owners:
|
156
|
+
owners.append("System Default")
|
155
157
|
|
156
158
|
if owners:
|
157
159
|
owners_str = "; ".join(owners)
|