runbooks 0.9.9__py3-none-any.whl → 1.0.0__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/cfat/cloud_foundations_assessment.py +626 -0
- runbooks/cloudops/cost_optimizer.py +95 -33
- runbooks/common/aws_pricing.py +388 -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/enhanced_exception_handler.py +4 -0
- runbooks/common/env_utils.py +96 -0
- runbooks/common/mcp_integration.py +49 -2
- runbooks/common/organizations_client.py +579 -0
- runbooks/common/profile_utils.py +96 -2
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/elastic_ip_optimizer.py +13 -9
- runbooks/finops/embedded_mcp_validator.py +31 -0
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/markdown_exporter.py +217 -2
- runbooks/finops/nat_gateway_optimizer.py +57 -20
- runbooks/finops/vpc_cleanup_exporter.py +28 -26
- runbooks/finops/vpc_cleanup_optimizer.py +370 -16
- runbooks/inventory/__init__.py +10 -1
- runbooks/inventory/cloud_foundations_integration.py +409 -0
- runbooks/inventory/core/collector.py +1148 -88
- runbooks/inventory/discovery.md +389 -0
- runbooks/inventory/drift_detection_cli.py +327 -0
- runbooks/inventory/inventory_mcp_cli.py +171 -0
- runbooks/inventory/inventory_modules.py +4 -7
- runbooks/inventory/mcp_inventory_validator.py +2149 -0
- runbooks/inventory/mcp_vpc_validator.py +23 -6
- runbooks/inventory/organizations_discovery.py +91 -1
- runbooks/inventory/rich_inventory_display.py +129 -1
- runbooks/inventory/unified_validation_engine.py +1292 -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 +654 -35
- runbooks/monitoring/performance_monitor.py +11 -7
- runbooks/operate/dynamodb_operations.py +6 -5
- runbooks/operate/ec2_operations.py +3 -2
- runbooks/operate/networking_cost_heatmap.py +4 -3
- runbooks/operate/s3_operations.py +13 -12
- runbooks/operate/vpc_operations.py +49 -1
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commvault_ec2_analysis.py +6 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
- runbooks/remediation/rds_snapshot_list.py +5 -3
- runbooks/validation/__init__.py +21 -1
- runbooks/validation/comprehensive_2way_validator.py +1996 -0
- runbooks/validation/mcp_validator.py +904 -94
- 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 +310 -62
- runbooks/vpc/cross_account_session.py +308 -0
- runbooks/vpc/heatmap_engine.py +96 -29
- runbooks/vpc/manager_interface.py +9 -9
- runbooks/vpc/mcp_no_eni_validator.py +1551 -0
- runbooks/vpc/networking_wrapper.py +14 -8
- 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/vpc/tests/test_cost_engine.py +1 -1
- runbooks/vpc/unified_scenarios.py +73 -3
- runbooks/vpc/vpc_cleanup_integration.py +512 -78
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/RECORD +71 -49
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
runbooks/vpc/cleanup_wrapper.py
CHANGED
@@ -194,7 +194,7 @@ class VPCCleanupCLI:
|
|
194
194
|
'analysis_summary': {
|
195
195
|
'total_vpcs': len(candidates),
|
196
196
|
'immediate_cleanup': len([c for c in candidates if c.cleanup_phase == VPCCleanupPhase.IMMEDIATE]),
|
197
|
-
'total_annual_savings': sum(c.annual_savings for c in candidates),
|
197
|
+
'total_annual_savings': sum((c.annual_savings or 0.0) for c in candidates),
|
198
198
|
'safety_mode_enabled': self.safety_mode
|
199
199
|
}
|
200
200
|
}
|
@@ -289,9 +289,9 @@ class VPCCleanupCLI:
|
|
289
289
|
execution_results['execution_plan'].append(vpc_plan)
|
290
290
|
|
291
291
|
# Safety warnings
|
292
|
-
if candidate.blocking_dependencies > 0:
|
292
|
+
if (candidate.blocking_dependencies or 0) > 0:
|
293
293
|
execution_results['warnings'].append(
|
294
|
-
f"VPC {candidate.vpc_id} has {candidate.blocking_dependencies} blocking dependencies"
|
294
|
+
f"VPC {candidate.vpc_id} has {candidate.blocking_dependencies or 0} blocking dependencies"
|
295
295
|
)
|
296
296
|
|
297
297
|
if candidate.is_default:
|
@@ -417,7 +417,7 @@ class VPCCleanupCLI:
|
|
417
417
|
safety_results = {
|
418
418
|
'vpc_id': vpc_id,
|
419
419
|
'safety_score': 'SAFE',
|
420
|
-
'blocking_dependencies': vpc_candidate.blocking_dependencies,
|
420
|
+
'blocking_dependencies': vpc_candidate.blocking_dependencies or 0,
|
421
421
|
'risk_level': vpc_candidate.risk_level.value,
|
422
422
|
'safety_checks': [],
|
423
423
|
'warnings': [],
|
@@ -504,7 +504,7 @@ class VPCCleanupCLI:
|
|
504
504
|
f"Investigation Required: [yellow]{exec_summary.get('investigation_required', 0)}[/yellow]\n"
|
505
505
|
f"Governance Approval Needed: [blue]{exec_summary.get('governance_approval_needed', 0)}[/blue]\n"
|
506
506
|
f"Complex Migration Required: [red]{exec_summary.get('complex_migration_required', 0)}[/red]\n\n"
|
507
|
-
f"Total Annual Savings: [bold green]${cleanup_plan['metadata']['total_annual_savings']:,.2f}[/bold green]\n"
|
507
|
+
f"Total Annual Savings: [bold green]${(cleanup_plan['metadata']['total_annual_savings'] or 0.0):,.2f}[/bold green]\n"
|
508
508
|
f"Business Case Strength: [cyan]{exec_summary.get('business_case_strength', 'Unknown')}[/cyan]"
|
509
509
|
)
|
510
510
|
|
@@ -518,7 +518,7 @@ class VPCCleanupCLI:
|
|
518
518
|
f"[bold blue]🚀 EXECUTION PLAN[/bold blue]\n\n"
|
519
519
|
f"Mode: {mode_text}\n"
|
520
520
|
f"VPCs to Process: [yellow]{len(candidates)}[/yellow]\n"
|
521
|
-
f"Total Dependencies to Remove: [red]{sum(c.blocking_dependencies for c in candidates)}[/red]\n"
|
521
|
+
f"Total Dependencies to Remove: [red]{sum((c.blocking_dependencies or 0) for c in candidates)}[/red]\n"
|
522
522
|
f"High Risk VPCs: [red]{len([c for c in candidates if c.risk_level == VPCCleanupRisk.HIGH])}[/red]\n"
|
523
523
|
f"Default VPCs: [magenta]{len([c for c in candidates if c.is_default])}[/magenta]"
|
524
524
|
)
|
@@ -655,7 +655,9 @@ def analyze_cleanup_candidates(
|
|
655
655
|
vpc_ids: Optional[List[str]] = None,
|
656
656
|
all_accounts: bool = False,
|
657
657
|
region: str = "us-east-1",
|
658
|
-
export_results: bool = True
|
658
|
+
export_results: bool = True,
|
659
|
+
account_limit: Optional[int] = None,
|
660
|
+
region_limit: Optional[int] = None
|
659
661
|
) -> Dict[str, Any]:
|
660
662
|
"""
|
661
663
|
CLI function to analyze VPC cleanup candidates
|
@@ -666,6 +668,8 @@ def analyze_cleanup_candidates(
|
|
666
668
|
all_accounts: Analyze across all accessible accounts
|
667
669
|
region: AWS region
|
668
670
|
export_results: Export results to files
|
671
|
+
account_limit: Limit number of accounts to process for faster testing
|
672
|
+
region_limit: Limit number of regions to scan per account
|
669
673
|
|
670
674
|
Returns:
|
671
675
|
Dictionary with analysis results
|
@@ -683,9 +687,92 @@ def analyze_cleanup_candidates(
|
|
683
687
|
# Handle multi-account analysis
|
684
688
|
account_profiles = None
|
685
689
|
if all_accounts:
|
686
|
-
#
|
687
|
-
|
688
|
-
|
690
|
+
# Use Organizations API to discover all accounts
|
691
|
+
console.print("[blue]🔍 Discovering organization accounts for multi-account VPC analysis...[/blue]")
|
692
|
+
|
693
|
+
try:
|
694
|
+
# Import Organizations discovery functionality from FinOps module
|
695
|
+
from runbooks.finops.aws_client import get_organization_accounts
|
696
|
+
from runbooks.common.profile_utils import create_operational_session, create_management_session
|
697
|
+
from runbooks.vpc.cross_account_session import convert_accounts_to_sessions
|
698
|
+
|
699
|
+
# Check for cached Organizations data first (performance optimization)
|
700
|
+
|
701
|
+
# Use CENTRALISED_OPS_PROFILE if available for operational accounts
|
702
|
+
import os
|
703
|
+
centralised_ops_profile = os.getenv('CENTRALISED_OPS_PROFILE')
|
704
|
+
if centralised_ops_profile:
|
705
|
+
console.print(f"[green]✅ Using CENTRALISED_OPS_PROFILE: {centralised_ops_profile}[/green]")
|
706
|
+
from .mcp_no_eni_validator import _get_cached_organizations_data, _cache_organizations_data
|
707
|
+
|
708
|
+
org_accounts = _get_cached_organizations_data()
|
709
|
+
|
710
|
+
if not org_accounts:
|
711
|
+
# Create management session for Organizations discovery (needs Organizations permissions)
|
712
|
+
session = create_management_session(profile=operational_profile)
|
713
|
+
|
714
|
+
# Discover all organization accounts
|
715
|
+
org_accounts = get_organization_accounts(session, operational_profile)
|
716
|
+
|
717
|
+
# Cache the results for future use (prevents duplicate calls)
|
718
|
+
if org_accounts:
|
719
|
+
_cache_organizations_data(org_accounts)
|
720
|
+
|
721
|
+
if org_accounts:
|
722
|
+
# Apply account limit for performance optimization before session creation
|
723
|
+
if account_limit and account_limit < len(org_accounts):
|
724
|
+
console.print(f"[yellow]🎯 Performance mode: limiting to first {account_limit} accounts[/yellow]")
|
725
|
+
org_accounts = org_accounts[:account_limit]
|
726
|
+
|
727
|
+
# Convert accounts to cross-account sessions using STS AssumeRole
|
728
|
+
account_sessions, account_metadata = convert_accounts_to_sessions(org_accounts, operational_profile)
|
729
|
+
|
730
|
+
console.print(f"[green]✅ Discovered {len(org_accounts)} organization accounts[/green]")
|
731
|
+
console.print(f"[cyan]📋 Created {len(account_sessions)} cross-account sessions for VPC analysis[/cyan]")
|
732
|
+
|
733
|
+
# Log account discovery for transparency
|
734
|
+
active_count = len([acc for acc in org_accounts if acc.get("status") == "ACTIVE"])
|
735
|
+
inactive_count = len(org_accounts) - active_count
|
736
|
+
console.print(f"[dim]Organization scope: {active_count} active, {inactive_count} inactive accounts[/dim]")
|
737
|
+
|
738
|
+
# Detect STS AssumeRole failures and switch to multi-profile discovery
|
739
|
+
if len(account_sessions) == 0 and len(org_accounts) > 0:
|
740
|
+
console.print(f"[red]❌ STS AssumeRole failed for all {len(org_accounts)} accounts - cross-account access denied[/red]")
|
741
|
+
console.print("[yellow]💡 Enhancing to multi-profile discovery for comprehensive VPC scanning[/yellow]")
|
742
|
+
|
743
|
+
# Enhanced multi-profile discovery pattern (KISS & DRY)
|
744
|
+
console.print("[blue]🔍 Discovering VPC profiles from available AWS configurations...[/blue]")
|
745
|
+
account_profiles = _discover_vpc_profiles_from_available_aws_profiles(operational_profile)
|
746
|
+
|
747
|
+
if account_profiles and len(account_profiles) > 1:
|
748
|
+
console.print(f"[green]✅ Enhanced discovery found {len(account_profiles)} profiles with VPC access[/green]")
|
749
|
+
else:
|
750
|
+
console.print("[yellow]⚠️ Enhanced discovery fallback to single profile[/yellow]")
|
751
|
+
else:
|
752
|
+
# Store sessions for VPC discovery instead of profiles
|
753
|
+
account_profiles = account_sessions # Pass sessions instead of profile strings
|
754
|
+
|
755
|
+
else:
|
756
|
+
console.print("[yellow]⚠️ No organization accounts found, falling back to single profile[/yellow]")
|
757
|
+
account_profiles = [operational_profile] if operational_profile else None
|
758
|
+
|
759
|
+
except ImportError as e:
|
760
|
+
console.print(f"[red]❌ Organizations discovery unavailable: {e}[/red]")
|
761
|
+
console.print("[yellow]💡 Falling back to single profile analysis[/yellow]")
|
762
|
+
account_profiles = [operational_profile] if operational_profile else None
|
763
|
+
|
764
|
+
except Exception as e:
|
765
|
+
console.print(f"[red]❌ Organizations discovery failed: {e}[/red]")
|
766
|
+
console.print("[yellow]💡 Enhancing to multi-profile discovery for comprehensive VPC scanning[/yellow]")
|
767
|
+
|
768
|
+
# Enhanced multi-profile discovery pattern (KISS & DRY)
|
769
|
+
console.print("[blue]🔍 Discovering VPC profiles from available AWS configurations...[/blue]")
|
770
|
+
account_profiles = _discover_vpc_profiles_from_available_aws_profiles(operational_profile)
|
771
|
+
|
772
|
+
if account_profiles and len(account_profiles) > 1:
|
773
|
+
console.print(f"[green]✅ Enhanced discovery found {len(account_profiles)} profiles with VPC access[/green]")
|
774
|
+
else:
|
775
|
+
console.print("[yellow]⚠️ Enhanced discovery fallback to single profile[/yellow]")
|
689
776
|
|
690
777
|
return cleanup_cli.analyze_vpc_cleanup_candidates(
|
691
778
|
vpc_ids=vpc_ids,
|
@@ -724,6 +811,140 @@ def validate_cleanup_safety(
|
|
724
811
|
)
|
725
812
|
|
726
813
|
|
814
|
+
def _discover_vpc_profiles_from_available_aws_profiles(primary_profile: str) -> List[str]:
|
815
|
+
"""
|
816
|
+
Enhanced multi-profile discovery for comprehensive VPC scanning across all available AWS profiles.
|
817
|
+
|
818
|
+
KISS & DRY approach: Use boto3's available_profiles to discover VPCs across Landing Zone
|
819
|
+
when Organizations API cross-account role assumption fails.
|
820
|
+
|
821
|
+
Args:
|
822
|
+
primary_profile: Primary operational profile to include
|
823
|
+
|
824
|
+
Returns:
|
825
|
+
List of validated AWS profile names for VPC discovery
|
826
|
+
"""
|
827
|
+
import boto3
|
828
|
+
from rich.progress import Progress, TaskID
|
829
|
+
|
830
|
+
console.print("[blue]🔍 Discovering VPC profiles from available AWS configurations...[/blue]")
|
831
|
+
|
832
|
+
# Get all available AWS profiles
|
833
|
+
try:
|
834
|
+
session = boto3.Session()
|
835
|
+
available_profiles = session.available_profiles
|
836
|
+
|
837
|
+
if not available_profiles:
|
838
|
+
console.print("[yellow]⚠️ No AWS profiles found in configuration[/yellow]")
|
839
|
+
return [primary_profile] if primary_profile else []
|
840
|
+
|
841
|
+
console.print(f"[cyan]📋 Found {len(available_profiles)} AWS profiles in configuration[/cyan]")
|
842
|
+
|
843
|
+
# Enhanced multi-region discovery for comprehensive Landing Zone coverage
|
844
|
+
# Based on user's confirmed NO-ENI VPCs in: us-east-1, us-west-2, ap-southeast-2
|
845
|
+
regions_to_check = [
|
846
|
+
'us-east-1', # Primary US region - user confirmed VPCs here
|
847
|
+
'us-west-2', # Secondary US region - user confirmed VPCs here
|
848
|
+
'ap-southeast-2', # APAC region - user confirmed VPCs here
|
849
|
+
'eu-west-1', # Europe primary
|
850
|
+
'ca-central-1', # Canada
|
851
|
+
'ap-northeast-1', # Tokyo (common enterprise region)
|
852
|
+
]
|
853
|
+
|
854
|
+
# Validate profiles by attempting to create sessions and check VPC access
|
855
|
+
validated_profiles = []
|
856
|
+
profile_vpc_details = {}
|
857
|
+
|
858
|
+
with Progress() as progress:
|
859
|
+
profile_task = progress.add_task("🔍 Validating profiles for VPC access...", total=len(available_profiles))
|
860
|
+
|
861
|
+
for profile_name in available_profiles:
|
862
|
+
try:
|
863
|
+
# Skip obvious non-VPC profiles but be less restrictive
|
864
|
+
if 'billing' in profile_name.lower() and 'readonly' in profile_name.lower():
|
865
|
+
console.print(f"[dim]⏭️ Skipping {profile_name} (billing-only profile)[/dim]")
|
866
|
+
progress.advance(profile_task)
|
867
|
+
continue
|
868
|
+
|
869
|
+
# Create test session
|
870
|
+
test_session = boto3.Session(profile_name=profile_name)
|
871
|
+
total_vpcs = 0
|
872
|
+
regions_with_vpcs = []
|
873
|
+
|
874
|
+
# Check multiple regions for VPCs (Landing Zone accounts may have VPCs in different regions)
|
875
|
+
for region in regions_to_check:
|
876
|
+
try:
|
877
|
+
ec2_client = test_session.client('ec2', region_name=region)
|
878
|
+
vpc_response = ec2_client.describe_vpcs(MaxResults=10) # Check more VPCs per region
|
879
|
+
region_vpc_count = len(vpc_response.get('Vpcs', []))
|
880
|
+
|
881
|
+
if region_vpc_count > 0:
|
882
|
+
total_vpcs += region_vpc_count
|
883
|
+
regions_with_vpcs.append(f"{region}:{region_vpc_count}")
|
884
|
+
|
885
|
+
except Exception as region_error:
|
886
|
+
# Log region-specific errors but don't fail the whole profile
|
887
|
+
if "UnauthorizedOperation" not in str(region_error):
|
888
|
+
console.print(f"[dim]⚠️ {profile_name} in {region}: {str(region_error)[:30]}...[/dim]")
|
889
|
+
continue
|
890
|
+
|
891
|
+
# Add profile if it has VPCs in any region OR if it's the primary profile
|
892
|
+
if total_vpcs > 0:
|
893
|
+
validated_profiles.append(profile_name)
|
894
|
+
profile_vpc_details[profile_name] = {
|
895
|
+
'total_vpcs': total_vpcs,
|
896
|
+
'regions': regions_with_vpcs
|
897
|
+
}
|
898
|
+
console.print(f"[green]✅ {profile_name}: {total_vpcs} VPCs across {len(regions_with_vpcs)} regions[/green]")
|
899
|
+
elif profile_name == primary_profile:
|
900
|
+
# Always include primary profile even if no VPCs found
|
901
|
+
validated_profiles.append(profile_name)
|
902
|
+
console.print(f"[yellow]🔑 {profile_name}: Primary profile (included despite no VPCs found)[/yellow]")
|
903
|
+
else:
|
904
|
+
console.print(f"[dim]⚪ {profile_name}: No VPCs found in {len(regions_to_check)} regions[/dim]")
|
905
|
+
|
906
|
+
except Exception as e:
|
907
|
+
console.print(f"[dim]❌ {profile_name}: Access failed ({str(e)[:50]}...)[/dim]")
|
908
|
+
|
909
|
+
progress.advance(profile_task)
|
910
|
+
|
911
|
+
# Ensure primary profile is included if it was validated
|
912
|
+
if primary_profile and primary_profile not in validated_profiles:
|
913
|
+
try:
|
914
|
+
# Test primary profile separately
|
915
|
+
test_session = boto3.Session(profile_name=primary_profile)
|
916
|
+
ec2_client = test_session.client('ec2', region_name='us-east-1')
|
917
|
+
ec2_client.describe_vpcs(MaxResults=1)
|
918
|
+
validated_profiles.insert(0, primary_profile) # Add at front
|
919
|
+
console.print(f"[green]✅ Primary profile {primary_profile} added[/green]")
|
920
|
+
except Exception:
|
921
|
+
console.print(f"[yellow]⚠️ Primary profile {primary_profile} validation failed[/yellow]")
|
922
|
+
|
923
|
+
# Enhanced VPC discovery summary
|
924
|
+
total_vpcs_found = sum(details.get('total_vpcs', 0) for details in profile_vpc_details.values())
|
925
|
+
console.print(f"[bold green]🎯 VPC Discovery Ready: {len(validated_profiles)} validated profiles[/bold green]")
|
926
|
+
|
927
|
+
if total_vpcs_found > 0:
|
928
|
+
console.print(f"[bold cyan]📊 Total VPCs discovered: {total_vpcs_found} across {len(profile_vpc_details)} accounts[/bold cyan]")
|
929
|
+
|
930
|
+
# Show detailed breakdown for profiles with VPCs
|
931
|
+
for profile, details in profile_vpc_details.items():
|
932
|
+
if details['total_vpcs'] > 0:
|
933
|
+
regions_str = ', '.join(details['regions'])
|
934
|
+
console.print(f"[dim] • {profile}: {regions_str}[/dim]")
|
935
|
+
else:
|
936
|
+
console.print(f"[yellow]⚠️ No VPCs found across {len(validated_profiles)} profiles - Landing Zone accounts may be empty[/yellow]")
|
937
|
+
|
938
|
+
console.print(f"[dim]Profiles: {', '.join(validated_profiles[:3])}{'...' if len(validated_profiles) > 3 else ''}[/dim]")
|
939
|
+
|
940
|
+
return validated_profiles
|
941
|
+
|
942
|
+
except Exception as e:
|
943
|
+
console.print(f"[red]❌ Profile discovery failed: {e}[/red]")
|
944
|
+
console.print(f"[yellow]💡 Falling back to primary profile: {primary_profile}[/yellow]")
|
945
|
+
return [primary_profile] if primary_profile else []
|
946
|
+
|
947
|
+
|
727
948
|
def generate_business_report(
|
728
949
|
profile: Optional[str] = None,
|
729
950
|
region: str = "us-east-1",
|