runbooks 0.9.8__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/__init__.py +1 -1
- 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/common/rich_utils.py +3 -0
- 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 +441 -0
- runbooks/finops/nat_gateway_optimizer.py +57 -20
- runbooks/finops/optimizer.py +2 -0
- runbooks/finops/single_dashboard.py +2 -2
- runbooks/finops/vpc_cleanup_exporter.py +330 -0
- runbooks/finops/vpc_cleanup_optimizer.py +895 -40
- 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 +969 -42
- 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 +50 -2
- 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 +3269 -0
- runbooks/vpc/vpc_cleanup_integration.py +516 -82
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/RECORD +75 -51
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
@@ -423,6 +423,447 @@ class MarkdownExporter:
|
|
423
423
|
highest = max(profiles, key=lambda p: p.get("potential_savings", 0))
|
424
424
|
return highest.get("profile_name", "Unknown")[:20]
|
425
425
|
|
426
|
+
def format_vpc_cleanup_table(self, vpc_candidates: List[Any]) -> str:
|
427
|
+
"""
|
428
|
+
Format VPC cleanup candidates into 15-column markdown table.
|
429
|
+
|
430
|
+
Args:
|
431
|
+
vpc_candidates: List of VPCCandidate objects from vpc.unified_scenarios
|
432
|
+
|
433
|
+
Returns:
|
434
|
+
Markdown formatted table string with VPC cleanup analysis
|
435
|
+
"""
|
436
|
+
if not vpc_candidates:
|
437
|
+
return "⚠️ No VPC candidates to format"
|
438
|
+
|
439
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
|
440
|
+
|
441
|
+
# Build table header and separator
|
442
|
+
headers = [
|
443
|
+
"Account_ID", "VPC_ID", "VPC_Name", "CIDR_Block", "Overlapping",
|
444
|
+
"Is_Default", "ENI_Count", "Tags", "Flow_Logs", "TGW/Peering",
|
445
|
+
"LBs_Present", "IaC", "Timeline", "Decision", "Owners/Approvals", "Notes"
|
446
|
+
]
|
447
|
+
|
448
|
+
markdown_lines = [
|
449
|
+
"# VPC Cleanup Analysis Report",
|
450
|
+
"",
|
451
|
+
f"**Generated**: {timestamp}",
|
452
|
+
f"**Total VPC Candidates**: {len(vpc_candidates)}",
|
453
|
+
f"**Analysis Source**: CloudOps Runbooks VPC Module v0.9.9",
|
454
|
+
"",
|
455
|
+
"## VPC Cleanup Decision Table",
|
456
|
+
"",
|
457
|
+
"| " + " | ".join(headers) + " |",
|
458
|
+
"| " + " | ".join(["---" for _ in headers]) + " |"
|
459
|
+
]
|
460
|
+
|
461
|
+
# Process each VPC candidate with enhanced data extraction
|
462
|
+
for candidate in vpc_candidates:
|
463
|
+
# Extract data with safe attribute access and formatting
|
464
|
+
account_id = getattr(candidate, 'account_id', 'Unknown')
|
465
|
+
vpc_id = getattr(candidate, 'vpc_id', 'Unknown')
|
466
|
+
vpc_name = getattr(candidate, 'vpc_name', '') or 'Unnamed'
|
467
|
+
cidr_block = getattr(candidate, 'cidr_block', 'Unknown')
|
468
|
+
|
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)
|
507
|
+
overlapping = "Yes" if getattr(candidate, 'overlapping', False) else "No"
|
508
|
+
|
509
|
+
# Format boolean indicators with emoji
|
510
|
+
is_default = "⚠️ Yes" if getattr(candidate, 'is_default', False) else "✅ No"
|
511
|
+
flow_logs = "✅ Yes" if getattr(candidate, 'flow_logs_enabled', False) else "❌ No"
|
512
|
+
tgw_peering = "✅ Yes" if getattr(candidate, 'tgw_peering_attached', False) else "❌ No"
|
513
|
+
load_balancers = "✅ Yes" if getattr(candidate, 'load_balancers_present', False) else "❌ No"
|
514
|
+
iac_managed = "✅ Yes" if getattr(candidate, 'iac_managed', False) else "❌ No"
|
515
|
+
|
516
|
+
# ENI Count handling
|
517
|
+
eni_count = getattr(candidate, 'eni_count', 0)
|
518
|
+
|
519
|
+
# Tags formatting - prioritize important tags with enhanced display
|
520
|
+
tags = getattr(candidate, 'tags', {}) or {}
|
521
|
+
relevant_tags = []
|
522
|
+
if tags:
|
523
|
+
# Priority order for business-relevant tags
|
524
|
+
priority_keys = ['Name', 'Environment', 'Project', 'Owner', 'BusinessOwner', 'Team', 'CostCenter', 'Application']
|
525
|
+
for key in priority_keys:
|
526
|
+
if key in tags and tags[key] and len(relevant_tags) < 3: # Increased limit for better visibility
|
527
|
+
relevant_tags.append(f"{key}:{tags[key]}")
|
528
|
+
|
529
|
+
# Add other important tags if space available
|
530
|
+
for key, value in tags.items():
|
531
|
+
if key not in priority_keys and value and len(relevant_tags) < 3:
|
532
|
+
relevant_tags.append(f"{key}:{value}")
|
533
|
+
|
534
|
+
# Enhanced display logic for tags
|
535
|
+
if relevant_tags:
|
536
|
+
tags_display = "; ".join(relevant_tags)
|
537
|
+
if len(tags_display) > 35: # Slightly increased width for better readability
|
538
|
+
tags_display = tags_display[:32] + "..."
|
539
|
+
elif tags:
|
540
|
+
# If tags exist but none were priority, show count
|
541
|
+
tags_display = f"({len(tags)} tags)"
|
542
|
+
else:
|
543
|
+
# No tags at all
|
544
|
+
tags_display = "No tags"
|
545
|
+
|
546
|
+
# Timeline and Decision
|
547
|
+
timeline = getattr(candidate, 'cleanup_timeline', '') or getattr(candidate, 'implementation_timeline', 'Unknown')
|
548
|
+
|
549
|
+
# Decision handling - check for different decision attribute names
|
550
|
+
decision_attr = getattr(candidate, 'decision', None)
|
551
|
+
if decision_attr:
|
552
|
+
if hasattr(decision_attr, 'value'):
|
553
|
+
decision = decision_attr.value
|
554
|
+
else:
|
555
|
+
decision = str(decision_attr)
|
556
|
+
else:
|
557
|
+
# Fallback decision logic based on risk/dependencies
|
558
|
+
decision = getattr(candidate, 'cleanup_bucket', 'Unknown')
|
559
|
+
|
560
|
+
# Owners/Approvals - Enhanced extraction from tags if not populated
|
561
|
+
owners = getattr(candidate, 'owners_approvals', []) or getattr(candidate, 'stakeholders', [])
|
562
|
+
|
563
|
+
# If no owners found via attributes, try to extract from tags directly
|
564
|
+
if not owners and tags:
|
565
|
+
owner_keys = ['Owner', 'BusinessOwner', 'TechnicalOwner', 'Team', 'Contact', 'CreatedBy', 'ManagedBy']
|
566
|
+
for key in owner_keys:
|
567
|
+
if key in tags and tags[key]:
|
568
|
+
if 'business' in key.lower() or 'manager' in tags[key].lower():
|
569
|
+
owners.append(f"{tags[key]} (Business)")
|
570
|
+
elif 'technical' in key.lower() or any(tech in tags[key].lower() for tech in ['ops', 'devops', 'engineering']):
|
571
|
+
owners.append(f"{tags[key]} (Technical)")
|
572
|
+
else:
|
573
|
+
owners.append(tags[key])
|
574
|
+
break # Take first found owner to avoid clutter
|
575
|
+
|
576
|
+
if owners:
|
577
|
+
owners_display = "; ".join(owners)
|
578
|
+
if len(owners_display) > 30: # Increased width for better display
|
579
|
+
owners_display = owners_display[:27] + "..."
|
580
|
+
else:
|
581
|
+
# Enhanced "unknown" display based on VPC characteristics
|
582
|
+
if getattr(candidate, 'is_default', False):
|
583
|
+
owners_display = "System Default"
|
584
|
+
elif getattr(candidate, 'iac_detected', False):
|
585
|
+
owners_display = "IaC Managed"
|
586
|
+
else:
|
587
|
+
owners_display = "No owner tags"
|
588
|
+
|
589
|
+
# Notes - combination of risk assessment and business impact
|
590
|
+
notes_parts = []
|
591
|
+
risk_level = getattr(candidate, 'risk_level', None)
|
592
|
+
if risk_level:
|
593
|
+
risk_val = risk_level.value if hasattr(risk_level, 'value') else str(risk_level)
|
594
|
+
notes_parts.append(f"Risk:{risk_val}")
|
595
|
+
|
596
|
+
business_impact = getattr(candidate, 'business_impact', '')
|
597
|
+
if business_impact:
|
598
|
+
notes_parts.append(business_impact[:15]) # Truncate
|
599
|
+
|
600
|
+
notes = "; ".join(notes_parts) if notes_parts else getattr(candidate, 'notes', 'No notes')
|
601
|
+
if len(notes) > 30: # Truncate for table formatting
|
602
|
+
notes = notes[:27] + "..."
|
603
|
+
|
604
|
+
# Create table row - escape pipes for markdown compatibility
|
605
|
+
row_data = [
|
606
|
+
account_id, vpc_id, vpc_name, cidr_block, overlapping,
|
607
|
+
is_default, str(eni_count), tags_display, flow_logs, tgw_peering,
|
608
|
+
load_balancers, iac_managed, timeline, decision, owners_display, notes
|
609
|
+
]
|
610
|
+
|
611
|
+
# Escape pipes and format row
|
612
|
+
escaped_data = [str(cell).replace("|", "\\|") for cell in row_data]
|
613
|
+
markdown_lines.append("| " + " | ".join(escaped_data) + " |")
|
614
|
+
|
615
|
+
# Add summary statistics
|
616
|
+
total_vpcs = len(vpc_candidates)
|
617
|
+
default_vpcs = sum(1 for c in vpc_candidates if getattr(c, 'is_default', False))
|
618
|
+
flow_logs_enabled = sum(1 for c in vpc_candidates if getattr(c, 'flow_logs_enabled', False))
|
619
|
+
iac_managed_count = sum(1 for c in vpc_candidates if getattr(c, 'iac_managed', False))
|
620
|
+
zero_eni_vpcs = sum(1 for c in vpc_candidates if getattr(c, 'eni_count', 1) == 0)
|
621
|
+
|
622
|
+
markdown_lines.extend([
|
623
|
+
"",
|
624
|
+
"## Analysis Summary",
|
625
|
+
"",
|
626
|
+
f"- **Total VPCs Analyzed**: {total_vpcs}",
|
627
|
+
f"- **Default VPCs**: {default_vpcs} ({(default_vpcs/total_vpcs*100):.1f}%)",
|
628
|
+
f"- **Flow Logs Enabled**: {flow_logs_enabled} ({(flow_logs_enabled/total_vpcs*100):.1f}%)",
|
629
|
+
f"- **IaC Managed**: {iac_managed_count} ({(iac_managed_count/total_vpcs*100):.1f}%)",
|
630
|
+
f"- **Zero ENI Attachments**: {zero_eni_vpcs} ({(zero_eni_vpcs/total_vpcs*100):.1f}%)",
|
631
|
+
"",
|
632
|
+
"## Cleanup Recommendations",
|
633
|
+
"",
|
634
|
+
"1. **Priority 1**: VPCs with zero ENI attachments and no dependencies",
|
635
|
+
"2. **Priority 2**: Default VPCs with no active resources",
|
636
|
+
"3. **Priority 3**: Non-IaC managed VPCs requiring manual cleanup",
|
637
|
+
"4. **Review Required**: VPCs with unclear ownership or business impact",
|
638
|
+
"",
|
639
|
+
"---",
|
640
|
+
f"*Generated by CloudOps Runbooks VPC Module v0.9.9 at {timestamp}*"
|
641
|
+
])
|
642
|
+
|
643
|
+
return "\n".join(markdown_lines)
|
644
|
+
|
645
|
+
def export_vpc_analysis_to_file(self, vpc_candidates: List[Any], filename: str = None, output_dir: str = "./exports") -> str:
|
646
|
+
"""
|
647
|
+
Export VPC analysis to markdown file with intelligent naming.
|
648
|
+
|
649
|
+
Args:
|
650
|
+
vpc_candidates: List of VPC candidates from analysis
|
651
|
+
filename: Base filename (optional, auto-generated if not provided)
|
652
|
+
output_dir: Output directory path
|
653
|
+
|
654
|
+
Returns:
|
655
|
+
Path to exported file
|
656
|
+
"""
|
657
|
+
if not filename:
|
658
|
+
timestamp = datetime.now().strftime("%Y-%m-%d")
|
659
|
+
filename = f"vpc-cleanup-analysis-{timestamp}.md"
|
660
|
+
|
661
|
+
# Ensure .md extension
|
662
|
+
if not filename.endswith('.md'):
|
663
|
+
filename = f"{filename}.md"
|
664
|
+
|
665
|
+
# Create output directory
|
666
|
+
output_path = Path(output_dir)
|
667
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
668
|
+
|
669
|
+
# Generate markdown content
|
670
|
+
markdown_content = self.format_vpc_cleanup_table(vpc_candidates)
|
671
|
+
|
672
|
+
# Write to file
|
673
|
+
filepath = output_path / filename
|
674
|
+
|
675
|
+
print_info(f"📝 Exporting VPC analysis to: {filename}")
|
676
|
+
|
677
|
+
try:
|
678
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
679
|
+
f.write(markdown_content)
|
680
|
+
|
681
|
+
print_success(f"✅ VPC analysis exported: {filepath}")
|
682
|
+
print_info(f"🔗 Ready for executive review or documentation systems")
|
683
|
+
return str(filepath)
|
684
|
+
|
685
|
+
except Exception as e:
|
686
|
+
print_warning(f"❌ Failed to export VPC analysis: {e}")
|
687
|
+
return ""
|
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
|
+
|
426
867
|
|
427
868
|
def export_finops_to_markdown(
|
428
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 = 0.045 # $0.045/GB (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
|
159
|
+
from ..common.aws_pricing import calculate_regional_cost
|
160
|
+
return calculate_regional_cost(self._base_monthly_cost_us_east_1, region)
|
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,43 @@ 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."""
|
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),
|
788
|
+
"nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region),
|
789
|
+
"transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region),
|
790
|
+
"vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region),
|
791
|
+
"data_transfer_regional": get_service_monthly_cost("data_transfer", base_region),
|
792
|
+
"data_transfer_internet": get_service_monthly_cost("data_transfer", base_region) * 4.5, # Internet is ~4.5x higher
|
793
|
+
}
|
794
|
+
except Exception as e:
|
795
|
+
print_warning(f"Dynamic pricing initialization failed: {e}")
|
796
|
+
# Fallback to regional cost calculation
|
797
|
+
from ..common.aws_pricing import calculate_regional_cost
|
798
|
+
base_costs = {
|
799
|
+
"nat_gateway_hourly": 0.045,
|
800
|
+
"nat_gateway_data_processing": 0.045, # per GB
|
801
|
+
"transit_gateway_monthly": 36.50,
|
802
|
+
"transit_gateway_attachment_hourly": 0.05,
|
803
|
+
"vpc_endpoint_interface_hourly": 0.01,
|
804
|
+
"data_transfer_regional": 0.01, # per GB within region
|
805
|
+
"data_transfer_cross_region": 0.02, # per GB cross-region
|
806
|
+
"data_transfer_internet": 0.09 # per GB to internet
|
807
|
+
}
|
808
|
+
|
809
|
+
# Apply regional multipliers to fallback costs
|
810
|
+
return {
|
811
|
+
key: calculate_regional_cost(value, "us-east-1")
|
812
|
+
for key, value in base_costs.items()
|
813
|
+
}
|
777
814
|
|
778
815
|
async def analyze_comprehensive_vpc_costs(self, profile: Optional[str] = None,
|
779
816
|
regions: Optional[List[str]] = None) -> Dict[str, Any]:
|
runbooks/finops/optimizer.py
CHANGED
@@ -638,7 +638,7 @@ class SingleAccountDashboard:
|
|
638
638
|
style="dim",
|
639
639
|
)
|
640
640
|
|
641
|
-
|
641
|
+
rich_console.print(table)
|
642
642
|
|
643
643
|
# Summary panel (using filtered services for consistent analysis)
|
644
644
|
total_current = sum(filtered_current_services.values())
|
@@ -681,7 +681,7 @@ class SingleAccountDashboard:
|
|
681
681
|
• Services Analyzed: {len(all_services)}{period_info}
|
682
682
|
"""
|
683
683
|
|
684
|
-
|
684
|
+
rich_console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
|
685
685
|
|
686
686
|
def _export_service_analysis(
|
687
687
|
self, args: argparse.Namespace, cost_data: Dict[str, Any], service_costs: List[str], account_id: str
|