runbooks 1.1.3__py3-none-any.whl → 1.1.5__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 +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
- runbooks/cfat/assessment/compliance.py +8 -8
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cfat/models.py +6 -2
- runbooks/cfat/tests/__init__.py +6 -1
- runbooks/cli/__init__.py +13 -0
- runbooks/cli/commands/cfat.py +274 -0
- runbooks/cli/commands/finops.py +1164 -0
- runbooks/cli/commands/inventory.py +379 -0
- runbooks/cli/commands/operate.py +239 -0
- runbooks/cli/commands/security.py +248 -0
- runbooks/cli/commands/validation.py +825 -0
- runbooks/cli/commands/vpc.py +310 -0
- runbooks/cli/registry.py +107 -0
- runbooks/cloudops/__init__.py +23 -30
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +549 -547
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +226 -227
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +179 -215
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +341 -0
- runbooks/common/aws_utils.py +75 -80
- runbooks/common/business_logic.py +127 -105
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
- runbooks/common/cross_account_manager.py +198 -205
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +235 -0
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +478 -495
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +176 -194
- runbooks/common/patterns.py +204 -0
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +248 -39
- runbooks/common/rich_utils.py +643 -92
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +29 -33
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +488 -622
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +40 -37
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +230 -292
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +338 -175
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1513 -482
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +25 -29
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +77 -78
- runbooks/finops/scenarios.py +1278 -439
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/tests/test_finops_dashboard.py +3 -3
- runbooks/finops/tests/test_reference_images_validation.py +2 -2
- runbooks/finops/tests/test_single_account_features.py +17 -17
- runbooks/finops/tests/validate_test_suite.py +1 -1
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +263 -269
- runbooks/finops/vpc_cleanup_exporter.py +191 -146
- runbooks/finops/vpc_cleanup_optimizer.py +593 -575
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/hitl/enhanced_workflow_engine.py +1 -1
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/README.md +3 -3
- runbooks/inventory/Tests/common_test_data.py +30 -30
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +28 -11
- runbooks/inventory/collectors/aws_networking.py +111 -101
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/discovery.md +2 -2
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/find_ec2_security_groups.py +1 -1
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +56 -52
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +733 -696
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +3 -3
- runbooks/main.py +152 -9147
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/metrics/dora_metrics_engine.py +2 -2
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/mcp_integration.py +1 -1
- runbooks/operate/networking_cost_heatmap.py +33 -10
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/operate/vpc_operations.py +648 -618
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +71 -67
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +91 -65
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +49 -44
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/integration_test_enterprise_security.py +5 -3
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/run_script.py +1 -1
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/mcp_reliability_engine.py +6 -6
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +51 -48
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +754 -708
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +190 -162
- runbooks/vpc/mcp_no_eni_validator.py +681 -640
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1302 -1129
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -956
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.3.dist-info/METADATA +0 -799
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -1,1519 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
Multi-Account Dashboard - Enterprise-Scale Parallel Processing Architecture
|
4
|
-
|
5
|
-
This module provides account-focused cost analysis for multi-account AWS environments,
|
6
|
-
optimized for enterprise-scale performance with 60+ account parallel processing.
|
7
|
-
|
8
|
-
Performance Architecture Features:
|
9
|
-
- **ENTERPRISE SCALE**: <60s processing for 60+ accounts
|
10
|
-
- **PARALLEL PROCESSING**: Concurrent account analysis with intelligent batching
|
11
|
-
- **CIRCUIT BREAKER**: Graceful degradation with partial results
|
12
|
-
- **MEMORY OPTIMIZATION**: Stream processing with controlled memory usage
|
13
|
-
- **ERROR RESILIENCE**: Continue processing on account failures
|
14
|
-
- **REAL-TIME PROGRESS**: Rich CLI progress indication for all operations
|
15
|
-
|
16
|
-
Enterprise Performance Targets:
|
17
|
-
- Account Discovery: <10s (achieved via Organizations API)
|
18
|
-
- Parallel Cost Analysis: <45s for 60 accounts
|
19
|
-
- Data Processing: <5s aggregation and display
|
20
|
-
- Total End-to-End: <60s from command to results
|
21
|
-
- Memory Usage: <2GB peak for 60-account dataset
|
22
|
-
|
23
|
-
Author: CloudOps Runbooks Team
|
24
|
-
Version: 0.8.0 - Enterprise Parallel Processing
|
25
|
-
"""
|
26
|
-
|
27
|
-
import argparse
|
28
|
-
import asyncio
|
29
|
-
import gc
|
30
|
-
import os
|
31
|
-
import threading
|
32
|
-
import time
|
33
|
-
from collections import defaultdict
|
34
|
-
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
35
|
-
from datetime import datetime, timedelta
|
36
|
-
from functools import partial
|
37
|
-
from typing import Any, Dict, List, Optional, Tuple
|
38
|
-
|
39
|
-
import boto3
|
40
|
-
from rich import box
|
41
|
-
from rich.console import Console
|
42
|
-
from rich.panel import Panel
|
43
|
-
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn
|
44
|
-
from rich.table import Column, Table
|
45
|
-
|
46
|
-
from ..common.context_logger import create_context_logger, get_context_console
|
47
|
-
from ..common.rich_utils import (
|
48
|
-
STATUS_INDICATORS,
|
49
|
-
create_progress_bar,
|
50
|
-
create_table,
|
51
|
-
format_cost,
|
52
|
-
print_error,
|
53
|
-
print_header,
|
54
|
-
print_info,
|
55
|
-
print_success,
|
56
|
-
print_warning,
|
57
|
-
)
|
58
|
-
from ..common.rich_utils import (
|
59
|
-
console as rich_console,
|
60
|
-
)
|
61
|
-
from .account_resolver import get_account_resolver
|
62
|
-
from .aws_client import get_accessible_regions, get_account_id, get_budgets
|
63
|
-
from .budget_integration import EnhancedBudgetAnalyzer
|
64
|
-
from .cost_processor import (
|
65
|
-
export_to_csv,
|
66
|
-
export_to_json,
|
67
|
-
filter_analytical_services,
|
68
|
-
get_cost_data,
|
69
|
-
process_service_costs,
|
70
|
-
)
|
71
|
-
from runbooks.common.profile_utils import (
|
72
|
-
create_cost_session,
|
73
|
-
create_management_session,
|
74
|
-
create_operational_session,
|
75
|
-
)
|
76
|
-
from .dashboard_runner import _initialize_profiles
|
77
|
-
from .enhanced_progress import track_multi_account_analysis
|
78
|
-
from .helpers import export_cost_dashboard_to_pdf
|
79
|
-
from .service_mapping import get_service_display_name
|
80
|
-
|
81
|
-
|
82
|
-
class MultiAccountDashboard:
|
83
|
-
"""
|
84
|
-
Enterprise-scale dashboard for multi-account AWS cost analysis with parallel processing.
|
85
|
-
|
86
|
-
Performance Architecture:
|
87
|
-
- **Parallel Processing**: 60+ accounts processed concurrently
|
88
|
-
- **Circuit Breaker**: <60s total execution with graceful degradation
|
89
|
-
- **Memory Management**: <2GB peak usage with stream processing
|
90
|
-
- **Error Resilience**: Continue analysis on individual account failures
|
91
|
-
- **AWS Rate Limiting**: Intelligent throttling to avoid API limits
|
92
|
-
|
93
|
-
Enterprise Features:
|
94
|
-
- Cross-account cost visibility with sub-second aggregation
|
95
|
-
- Organizational unit cost tracking with real-time updates
|
96
|
-
- Budget management at scale with parallel validation
|
97
|
-
- Cost allocation and chargeback data with performance optimization
|
98
|
-
"""
|
99
|
-
|
100
|
-
def __init__(self, console: Optional[Console] = None, max_concurrent_accounts: int = 15, context: str = "cli"):
|
101
|
-
self.console = console or rich_console
|
102
|
-
self.budget_analyzer = EnhancedBudgetAnalyzer(self.console)
|
103
|
-
|
104
|
-
# Enhanced context-aware logging system
|
105
|
-
self.context_logger = create_context_logger("finops.multi_dashboard")
|
106
|
-
self.context_console = get_context_console()
|
107
|
-
|
108
|
-
# Legacy context support (maintained for backward compatibility)
|
109
|
-
self.execution_context = context # "cli" or "jupyter"
|
110
|
-
self.detailed_logging = self.context_console.config.show_technical_details # Dynamic detection
|
111
|
-
|
112
|
-
# Enterprise parallel processing configuration
|
113
|
-
self.max_concurrent_accounts = max_concurrent_accounts # AWS API rate limiting consideration
|
114
|
-
self.account_batch_size = 5 # Optimal batch size for Cost Explorer API
|
115
|
-
self.max_execution_time = 55 # Circuit breaker: 55s for 60s target
|
116
|
-
self.memory_management_threshold = 0.8 # Trigger GC at 80% memory usage
|
117
|
-
|
118
|
-
# Performance monitoring
|
119
|
-
self.performance_metrics = {
|
120
|
-
"total_accounts": 0,
|
121
|
-
"successful_accounts": 0,
|
122
|
-
"failed_accounts": 0,
|
123
|
-
"execution_time": 0,
|
124
|
-
"avg_account_processing_time": 0,
|
125
|
-
"peak_memory_usage": 0,
|
126
|
-
"api_calls_made": 0,
|
127
|
-
}
|
128
|
-
|
129
|
-
# Account name resolution for readable account display
|
130
|
-
self.account_resolver = None # Will be initialized with management profile
|
131
|
-
self.account_metadata = {} # Store account metadata from Organizations API (includes inactive accounts)
|
132
|
-
|
133
|
-
def _log_technical_detail(self, message: str) -> None:
|
134
|
-
"""
|
135
|
-
Context-aware technical logging: Detail for CLI (technical users), minimal for Jupyter.
|
136
|
-
|
137
|
-
Args:
|
138
|
-
message: Technical log message to display conditionally
|
139
|
-
"""
|
140
|
-
self.context_console.print_technical_detail(f"SRE Debug: {message}")
|
141
|
-
|
142
|
-
def _log_user_friendly(self, message: str, style: str = "bright_blue") -> None:
|
143
|
-
"""
|
144
|
-
Universal user-friendly logging for both CLI and Jupyter contexts.
|
145
|
-
|
146
|
-
Args:
|
147
|
-
message: User-friendly message for all contexts
|
148
|
-
style: Rich styling for the message
|
149
|
-
"""
|
150
|
-
self.context_logger.info(message)
|
151
|
-
|
152
|
-
def run_dashboard(self, args: argparse.Namespace, config: Dict[str, Any]) -> int:
|
153
|
-
"""
|
154
|
-
Main entry point for multi-account account-focused dashboard.
|
155
|
-
|
156
|
-
Args:
|
157
|
-
args: Command line arguments
|
158
|
-
config: Routing configuration from dashboard router
|
159
|
-
|
160
|
-
Returns:
|
161
|
-
int: Exit code (0 for success, 1 for failure)
|
162
|
-
"""
|
163
|
-
try:
|
164
|
-
print_header("Multi-Account Financial Dashboard", "1.1.1")
|
165
|
-
|
166
|
-
# Configuration display
|
167
|
-
top_accounts = getattr(args, "top_accounts", 5)
|
168
|
-
services_per_account = getattr(args, "services_per_account", 3)
|
169
|
-
|
170
|
-
# SRE FIX: When --all flag is used, show ALL accounts, not just top N
|
171
|
-
is_all_flag_used = getattr(args, "all", False)
|
172
|
-
if is_all_flag_used:
|
173
|
-
# For --all flag: show ALL discovered accounts, not just top N
|
174
|
-
if config.get("profiles_to_analyze") and len(config["profiles_to_analyze"]) > top_accounts:
|
175
|
-
# Organizations API discovered accounts
|
176
|
-
top_accounts = len(config["profiles_to_analyze"])
|
177
|
-
self.console.print(
|
178
|
-
f"[info]🏢 Analysis Focus:[/] [highlight]ALL {top_accounts} Accounts (--all flag + Organizations API)[/]"
|
179
|
-
)
|
180
|
-
else:
|
181
|
-
# Fallback: legacy profile discovery
|
182
|
-
profiles_to_use, user_regions, time_range = _initialize_profiles(args)
|
183
|
-
if len(profiles_to_use) > top_accounts:
|
184
|
-
top_accounts = len(profiles_to_use)
|
185
|
-
self.console.print(
|
186
|
-
f"[info]🏢 Analysis Focus:[/] [highlight]ALL {top_accounts} Accounts (--all flag + legacy profiles)[/]"
|
187
|
-
)
|
188
|
-
else:
|
189
|
-
self.console.print(
|
190
|
-
f"[info]🏢 Analysis Focus:[/] [highlight]ALL {top_accounts} Accounts (--all flag)[/]"
|
191
|
-
)
|
192
|
-
else:
|
193
|
-
self.console.print(f"[info]🏢 Analysis Focus:[/] [highlight]TOP {top_accounts} Accounts[/]")
|
194
|
-
self.console.print(f"[dim]• Services per account: {services_per_account}[/]")
|
195
|
-
self.console.print(f"[dim]• Optimization Target: Account-level insights[/]")
|
196
|
-
self.console.print(f"[dim]• User Profile: Financial management teams[/]\n")
|
197
|
-
|
198
|
-
# SRE FIX: Use routing configuration profiles if available (Organizations API discovered accounts)
|
199
|
-
if config.get("profiles_to_analyze") and len(config["profiles_to_analyze"]) > 1:
|
200
|
-
# Use organization-discovered accounts from routing config
|
201
|
-
profiles_to_use = config["profiles_to_analyze"]
|
202
|
-
user_regions = getattr(args, "regions", None)
|
203
|
-
time_range = getattr(args, "time_range", None)
|
204
|
-
|
205
|
-
# CRITICAL FIX: Extract account metadata for inactive account display
|
206
|
-
self.account_metadata = config.get("account_metadata", {})
|
207
|
-
|
208
|
-
self.console.print(
|
209
|
-
f"[info]✅ SRE Pipeline Fix:[/] Using {len(profiles_to_use)} organization-discovered accounts"
|
210
|
-
)
|
211
|
-
self.console.print(
|
212
|
-
f"[dim]• Discovery Method: {config.get('account_discovery_method', 'organizations_api')}[/]"
|
213
|
-
)
|
214
|
-
self.console.print(f"[dim]• Analysis Scope: {config.get('analysis_scope', 'organization')}[/]")
|
215
|
-
|
216
|
-
# ENHANCED LOGGING: Show account status breakdown
|
217
|
-
if self.account_metadata:
|
218
|
-
active_count = len([acc for acc in self.account_metadata.values() if acc.get("status") == "ACTIVE"])
|
219
|
-
inactive_count = len(self.account_metadata) - active_count
|
220
|
-
self.console.print(f"[dim]• Account Status: {active_count} active, {inactive_count} inactive[/]")
|
221
|
-
else:
|
222
|
-
# Fallback to standard profile initialization
|
223
|
-
profiles_to_use, user_regions, time_range = _initialize_profiles(args)
|
224
|
-
|
225
|
-
if len(profiles_to_use) == 1:
|
226
|
-
print_warning(f"Only 1 profile detected. Consider using single-account mode for better insights.")
|
227
|
-
|
228
|
-
# Run account-focused analysis
|
229
|
-
return self._execute_account_analysis(profiles_to_use, args, top_accounts, services_per_account)
|
230
|
-
|
231
|
-
except Exception as e:
|
232
|
-
print_error(f"Multi-account dashboard failed: {str(e)}")
|
233
|
-
return 1
|
234
|
-
|
235
|
-
def _execute_account_analysis(
|
236
|
-
self, profiles: List[str], args: argparse.Namespace, top_accounts: int, services_per_account: int
|
237
|
-
) -> int:
|
238
|
-
"""Execute enterprise-scale parallel account analysis with <60s performance target."""
|
239
|
-
start_time = time.time()
|
240
|
-
|
241
|
-
try:
|
242
|
-
# SRE FIX: Initialize performance tracking with ACTUAL accounts to process
|
243
|
-
# This ensures metrics show correct total regardless of profile format
|
244
|
-
actual_profiles = self._resolve_actual_accounts(profiles)
|
245
|
-
self.performance_metrics["total_accounts"] = len(actual_profiles)
|
246
|
-
|
247
|
-
# Initialize account resolver for readable account names
|
248
|
-
management_profile = os.getenv("MANAGEMENT_PROFILE") or (args.profile if hasattr(args, "profile") else None)
|
249
|
-
self.account_resolver = get_account_resolver(management_profile)
|
250
|
-
|
251
|
-
self.console.print(
|
252
|
-
f"[info]📊 SRE Performance Tracking:[/] [highlight]Processing {len(actual_profiles)} accounts[/]"
|
253
|
-
)
|
254
|
-
|
255
|
-
# Execute parallel analysis with circuit breaker
|
256
|
-
account_data = self._parallel_account_analysis(actual_profiles, args, services_per_account)
|
257
|
-
|
258
|
-
# Performance metrics calculation - FIX: Use ACTUAL processed accounts
|
259
|
-
execution_time = time.time() - start_time
|
260
|
-
self.performance_metrics["execution_time"] = execution_time
|
261
|
-
successful_accounts = [acc for acc in account_data if acc["success"]]
|
262
|
-
self.performance_metrics["successful_accounts"] = len(successful_accounts)
|
263
|
-
self.performance_metrics["failed_accounts"] = len(account_data) - len(successful_accounts)
|
264
|
-
|
265
|
-
# Performance validation against enterprise targets
|
266
|
-
self._validate_performance_targets(execution_time, len(profiles))
|
267
|
-
|
268
|
-
# Sort accounts by total cost for top N display
|
269
|
-
successful_accounts.sort(key=lambda x: x.get("total_cost", 0), reverse=True)
|
270
|
-
|
271
|
-
# SRE FIX: Final check for --all flag to ensure ALL accounts are displayed
|
272
|
-
is_all_flag_used = getattr(args, "all", False)
|
273
|
-
if is_all_flag_used:
|
274
|
-
accounts_to_display = successful_accounts # Show ALL accounts
|
275
|
-
display_count = len(successful_accounts)
|
276
|
-
self.console.print(f"[dim]SRE Debug: --all flag detected - displaying ALL {display_count} accounts[/]")
|
277
|
-
else:
|
278
|
-
accounts_to_display = successful_accounts[:top_accounts] # Show top N accounts
|
279
|
-
display_count = min(top_accounts, len(successful_accounts))
|
280
|
-
self.console.print(
|
281
|
-
f"[dim]SRE Debug: Processed {len(successful_accounts)} accounts, displaying top {display_count} accounts[/]"
|
282
|
-
)
|
283
|
-
|
284
|
-
# Display results with performance metrics
|
285
|
-
self._display_account_focused_table(
|
286
|
-
accounts=accounts_to_display, services_per_account=services_per_account, args=args
|
287
|
-
)
|
288
|
-
|
289
|
-
self._display_cross_account_summary(successful_accounts)
|
290
|
-
self._display_performance_metrics(execution_time)
|
291
|
-
|
292
|
-
# Export if requested (SRE ENHANCEMENT: --export-markdown flag support)
|
293
|
-
if hasattr(args, "report_name") and args.report_name:
|
294
|
-
self._export_account_analysis(args, successful_accounts)
|
295
|
-
|
296
|
-
# WIP.md requirement: --export-markdown flag for GitHub table format
|
297
|
-
if hasattr(args, "export_markdown") and args.export_markdown:
|
298
|
-
self._export_account_analysis_to_markdown(args, successful_accounts, execution_time)
|
299
|
-
|
300
|
-
print_success(
|
301
|
-
f"Enterprise parallel analysis completed: {len(successful_accounts)}/{len(profiles)} accounts in {execution_time:.1f}s"
|
302
|
-
)
|
303
|
-
return 0
|
304
|
-
|
305
|
-
except Exception as e:
|
306
|
-
print_error(f"Enterprise account analysis failed: {str(e)}")
|
307
|
-
return 1
|
308
|
-
|
309
|
-
def _parallel_account_analysis(
|
310
|
-
self, profiles: List[str], args: argparse.Namespace, services_per_account: int
|
311
|
-
) -> List[Dict[str, Any]]:
|
312
|
-
"""
|
313
|
-
Enterprise parallel account analysis with intelligent batching and circuit breaker.
|
314
|
-
|
315
|
-
Performance Strategy:
|
316
|
-
1. Split accounts into optimal batches for AWS API rate limiting
|
317
|
-
2. Process batches in parallel with ThreadPoolExecutor
|
318
|
-
3. Circuit breaker for <60s execution time
|
319
|
-
4. Memory management with garbage collection
|
320
|
-
5. Real-time progress tracking for user feedback
|
321
|
-
|
322
|
-
Returns:
|
323
|
-
List of account analysis results with success/failure indicators
|
324
|
-
"""
|
325
|
-
start_time = time.time()
|
326
|
-
account_data = []
|
327
|
-
processed_count = 0
|
328
|
-
|
329
|
-
# Create account batches for optimal AWS API usage
|
330
|
-
account_batches = self._create_account_batches(profiles)
|
331
|
-
|
332
|
-
# Initialize enterprise progress tracking
|
333
|
-
progress = Progress(
|
334
|
-
SpinnerColumn(),
|
335
|
-
TextColumn("[progress.description]{task.description}"),
|
336
|
-
BarColumn(complete_style="bright_green", finished_style="bright_green"),
|
337
|
-
TaskProgressColumn(),
|
338
|
-
TextColumn("• {task.fields[status]}"),
|
339
|
-
TimeElapsedColumn(),
|
340
|
-
console=self.console,
|
341
|
-
transient=False,
|
342
|
-
)
|
343
|
-
|
344
|
-
with progress:
|
345
|
-
task_id = progress.add_task(
|
346
|
-
"Enterprise Parallel Analysis", total=len(profiles), status=f"Processing {len(account_batches)} batches"
|
347
|
-
)
|
348
|
-
|
349
|
-
# Execute parallel batch processing with circuit breaker
|
350
|
-
with ThreadPoolExecutor(max_workers=self.max_concurrent_accounts) as executor:
|
351
|
-
# Submit all account analysis tasks
|
352
|
-
future_to_profile = {}
|
353
|
-
for profile in profiles:
|
354
|
-
future = executor.submit(
|
355
|
-
self._analyze_single_account_with_timeout, profile, args, services_per_account
|
356
|
-
)
|
357
|
-
future_to_profile[future] = profile
|
358
|
-
|
359
|
-
# Process results as they complete with circuit breaker
|
360
|
-
for future in as_completed(future_to_profile, timeout=self.max_execution_time):
|
361
|
-
elapsed_time = time.time() - start_time
|
362
|
-
|
363
|
-
# Circuit breaker: Check execution time
|
364
|
-
if elapsed_time > self.max_execution_time:
|
365
|
-
progress.update(task_id, description="Circuit breaker activated")
|
366
|
-
print_warning(
|
367
|
-
f"Circuit breaker activated at {elapsed_time:.1f}s - completing with partial results"
|
368
|
-
)
|
369
|
-
break
|
370
|
-
|
371
|
-
try:
|
372
|
-
profile = future_to_profile[future]
|
373
|
-
account_info = future.result(timeout=10) # 10s timeout per account
|
374
|
-
account_data.append(account_info)
|
375
|
-
processed_count += 1
|
376
|
-
|
377
|
-
# Update progress with status
|
378
|
-
status_msg = f"✓ {processed_count}/{len(profiles)} accounts"
|
379
|
-
if not account_info["success"]:
|
380
|
-
status_msg += f" ({self.performance_metrics.get('failed_accounts', 0)} failed)"
|
381
|
-
|
382
|
-
# WIP.md logging: Technical details for CLI users only
|
383
|
-
self._log_technical_detail(
|
384
|
-
f"Account {account_info.get('account_id', 'unknown')} processed in {account_info.get('processing_time', 0):.1f}s"
|
385
|
-
)
|
386
|
-
|
387
|
-
progress.update(task_id, completed=processed_count, status=status_msg)
|
388
|
-
|
389
|
-
# Memory management: Trigger GC every 10 accounts
|
390
|
-
if processed_count % 10 == 0:
|
391
|
-
gc.collect()
|
392
|
-
|
393
|
-
except Exception as e:
|
394
|
-
profile = future_to_profile[future]
|
395
|
-
print_warning(f"Account analysis timeout/error for {profile}: {str(e)[:50]}")
|
396
|
-
account_data.append(
|
397
|
-
{
|
398
|
-
"profile": profile,
|
399
|
-
"account_id": "Timeout/Error",
|
400
|
-
"success": False,
|
401
|
-
"error": str(e),
|
402
|
-
"total_cost": 0,
|
403
|
-
"services": {},
|
404
|
-
}
|
405
|
-
)
|
406
|
-
processed_count += 1
|
407
|
-
|
408
|
-
progress.update(task_id, completed=processed_count)
|
409
|
-
|
410
|
-
# Final progress update
|
411
|
-
final_time = time.time() - start_time
|
412
|
-
progress.update(
|
413
|
-
task_id,
|
414
|
-
completed=len(profiles),
|
415
|
-
description="Enterprise Analysis Complete",
|
416
|
-
status=f"✅ Completed in {final_time:.1f}s",
|
417
|
-
)
|
418
|
-
|
419
|
-
return account_data
|
420
|
-
|
421
|
-
def _resolve_actual_accounts(self, profiles: List[str]) -> List[str]:
|
422
|
-
"""
|
423
|
-
SRE FIX: Resolve actual unique accounts from profile list.
|
424
|
-
|
425
|
-
When using Organizations API discovery, profiles come in format: 'profile@accountId'
|
426
|
-
This ensures we process each unique account exactly once and provides accurate metrics.
|
427
|
-
|
428
|
-
Args:
|
429
|
-
profiles: List of profile identifiers (may include @accountId suffixes)
|
430
|
-
|
431
|
-
Returns:
|
432
|
-
List of unique account identifiers for processing
|
433
|
-
"""
|
434
|
-
unique_accounts = set()
|
435
|
-
resolved_profiles = []
|
436
|
-
|
437
|
-
for profile in profiles:
|
438
|
-
if "@" in profile:
|
439
|
-
# Organizations API format: 'profile@accountId'
|
440
|
-
base_profile, account_id = profile.split("@", 1)
|
441
|
-
# Use account ID as unique identifier
|
442
|
-
if account_id not in unique_accounts:
|
443
|
-
unique_accounts.add(account_id)
|
444
|
-
resolved_profiles.append(profile) # Keep original format for session creation
|
445
|
-
else:
|
446
|
-
# Regular profile - treat as single account
|
447
|
-
if profile not in unique_accounts:
|
448
|
-
unique_accounts.add(profile)
|
449
|
-
resolved_profiles.append(profile)
|
450
|
-
|
451
|
-
if len(profiles) != len(resolved_profiles):
|
452
|
-
self.console.print(
|
453
|
-
f"[yellow]ℹ️ SRE Deduplication:[/] Reduced {len(profiles)} profiles to {len(resolved_profiles)} unique accounts"
|
454
|
-
)
|
455
|
-
|
456
|
-
return resolved_profiles
|
457
|
-
|
458
|
-
def _create_account_batches(self, profiles: List[str]) -> List[List[str]]:
|
459
|
-
"""Create optimal account batches for AWS API rate limiting."""
|
460
|
-
batches = []
|
461
|
-
for i in range(0, len(profiles), self.account_batch_size):
|
462
|
-
batch = profiles[i : i + self.account_batch_size]
|
463
|
-
batches.append(batch)
|
464
|
-
return batches
|
465
|
-
|
466
|
-
def _analyze_single_account_with_timeout(
|
467
|
-
self, profile: str, args: argparse.Namespace, services_per_account: int
|
468
|
-
) -> Dict[str, Any]:
|
469
|
-
"""Analyze single account with timeout and enhanced error handling."""
|
470
|
-
account_start_time = time.time()
|
471
|
-
|
472
|
-
try:
|
473
|
-
# Call existing single account analysis with timeout protection
|
474
|
-
result = self._analyze_single_account(profile, args, services_per_account)
|
475
|
-
|
476
|
-
# Add performance tracking
|
477
|
-
processing_time = time.time() - account_start_time
|
478
|
-
result["processing_time"] = processing_time
|
479
|
-
self.performance_metrics["api_calls_made"] += 1
|
480
|
-
|
481
|
-
return result
|
482
|
-
|
483
|
-
except Exception as e:
|
484
|
-
processing_time = time.time() - account_start_time
|
485
|
-
return {
|
486
|
-
"profile": profile,
|
487
|
-
"account_id": "Error",
|
488
|
-
"success": False,
|
489
|
-
"error": str(e),
|
490
|
-
"total_cost": 0,
|
491
|
-
"services": {},
|
492
|
-
"processing_time": processing_time,
|
493
|
-
}
|
494
|
-
|
495
|
-
def _validate_performance_targets(self, execution_time: float, account_count: int) -> None:
|
496
|
-
"""Validate performance against enterprise targets and log results."""
|
497
|
-
target_time = 60.0 # 60 second target
|
498
|
-
performance_ratio = execution_time / target_time
|
499
|
-
|
500
|
-
if execution_time <= target_time:
|
501
|
-
print_success(f"✅ Performance target achieved: {execution_time:.1f}s ≤ {target_time}s target")
|
502
|
-
elif execution_time <= target_time * 1.2:
|
503
|
-
print_warning(f"⚠️ Performance acceptable: {execution_time:.1f}s (within 20% of {target_time}s target)")
|
504
|
-
else:
|
505
|
-
print_warning(f"⚠️ Performance needs optimization: {execution_time:.1f}s > {target_time}s target")
|
506
|
-
|
507
|
-
# Calculate throughput metrics
|
508
|
-
accounts_per_second = account_count / execution_time if execution_time > 0 else 0
|
509
|
-
avg_account_time = execution_time / account_count if account_count > 0 else 0
|
510
|
-
|
511
|
-
self.console.log(
|
512
|
-
f"[dim]Throughput: {accounts_per_second:.1f} accounts/second, Average: {avg_account_time:.1f}s per account[/]"
|
513
|
-
)
|
514
|
-
|
515
|
-
def _display_performance_metrics(self, execution_time: float) -> None:
|
516
|
-
"""Display comprehensive performance metrics for enterprise monitoring."""
|
517
|
-
metrics_text = f"""
|
518
|
-
[highlight]Performance Metrics - Enterprise Scale[/]
|
519
|
-
• Total Execution Time: {execution_time:.1f}s (Target: <60s)
|
520
|
-
• Successful Accounts: {self.performance_metrics["successful_accounts"]}/{self.performance_metrics["total_accounts"]}
|
521
|
-
• Failed Accounts: {self.performance_metrics["failed_accounts"]}
|
522
|
-
• Average Processing Time: {execution_time / self.performance_metrics["total_accounts"]:.1f}s per account
|
523
|
-
• Throughput: {self.performance_metrics["total_accounts"] / execution_time:.1f} accounts/second
|
524
|
-
• API Calls Made: {self.performance_metrics["api_calls_made"]}
|
525
|
-
"""
|
526
|
-
|
527
|
-
# Performance status color coding
|
528
|
-
if execution_time <= 60:
|
529
|
-
style = "bright_green"
|
530
|
-
status_icon = "✅"
|
531
|
-
elif execution_time <= 72: # Within 20%
|
532
|
-
style = "yellow"
|
533
|
-
status_icon = "⚠️"
|
534
|
-
else:
|
535
|
-
style = "red"
|
536
|
-
status_icon = "❌"
|
537
|
-
|
538
|
-
self.console.print(
|
539
|
-
Panel(
|
540
|
-
metrics_text.strip(),
|
541
|
-
title=f"{status_icon} Enterprise Performance Dashboard",
|
542
|
-
style=style,
|
543
|
-
border_style=style,
|
544
|
-
)
|
545
|
-
)
|
546
|
-
|
547
|
-
def _analyze_single_account(
|
548
|
-
self, profile: str, args: argparse.Namespace, services_per_account: int
|
549
|
-
) -> Dict[str, Any]:
|
550
|
-
"""Analyze a single account within the multi-account context."""
|
551
|
-
try:
|
552
|
-
# SRE FIX: Extract account ID from Organizations API profile format
|
553
|
-
if "@" in profile:
|
554
|
-
base_profile, target_account_id = profile.split("@", 1)
|
555
|
-
# Configurable display format - using centralized config
|
556
|
-
from runbooks.finops.config import get_profile_display_length
|
557
|
-
max_profile_display_length = get_profile_display_length(args)
|
558
|
-
if len(base_profile) > max_profile_display_length:
|
559
|
-
display_profile = f"{base_profile[:max_profile_display_length]}...@{target_account_id}"
|
560
|
-
else:
|
561
|
-
display_profile = f"{base_profile}@{target_account_id}"
|
562
|
-
else:
|
563
|
-
base_profile = profile
|
564
|
-
target_account_id = None
|
565
|
-
display_profile = profile
|
566
|
-
|
567
|
-
# Initialize sessions using base profile
|
568
|
-
cost_session = create_cost_session(base_profile)
|
569
|
-
mgmt_session = create_management_session(base_profile)
|
570
|
-
|
571
|
-
# SRE FIX: Get account ID - use target account for Organizations API or session account
|
572
|
-
if target_account_id:
|
573
|
-
account_id = target_account_id
|
574
|
-
else:
|
575
|
-
account_id = get_account_id(mgmt_session) or f"Unknown-{profile}"
|
576
|
-
|
577
|
-
# SRE FIX: Get cost data with account-specific filtering
|
578
|
-
cost_data = self._get_account_specific_cost_data(
|
579
|
-
cost_session,
|
580
|
-
account_id,
|
581
|
-
getattr(args, "time_range", None),
|
582
|
-
getattr(args, "tag", None),
|
583
|
-
profile_name=base_profile,
|
584
|
-
)
|
585
|
-
|
586
|
-
# Get budget information
|
587
|
-
budget_data = get_budgets(cost_session)
|
588
|
-
|
589
|
-
# Process service costs
|
590
|
-
service_costs, service_cost_data = process_service_costs(cost_data)
|
591
|
-
|
592
|
-
# Get top services for this account (SRE ENHANCEMENT: Exclude "Tax" per WIP.md requirements)
|
593
|
-
costs_by_service = cost_data.get("costs_by_service", {})
|
594
|
-
|
595
|
-
# WIP.md requirement: Use centralized filtering for consistency
|
596
|
-
filtered_services = filter_analytical_services(costs_by_service)
|
597
|
-
|
598
|
-
# Get top services after filtering
|
599
|
-
top_services = dict(
|
600
|
-
sorted(filtered_services.items(), key=lambda x: x[1], reverse=True)[:services_per_account]
|
601
|
-
)
|
602
|
-
|
603
|
-
# Calculate enhanced budget status using real AWS Budgets API
|
604
|
-
current_cost = cost_data.get("current_month", 0)
|
605
|
-
try:
|
606
|
-
budget_status = self.budget_analyzer.get_enhanced_budget_status(cost_session, current_cost, account_id)
|
607
|
-
except Exception as e:
|
608
|
-
print_warning(f"Enhanced budget analysis failed for {profile}: {str(e)[:50]}")
|
609
|
-
budget_status = self._calculate_budget_status(current_cost, budget_data)
|
610
|
-
|
611
|
-
return {
|
612
|
-
"profile": display_profile, # SRE FIX: Use display profile for table
|
613
|
-
"account_id": account_id,
|
614
|
-
"success": True,
|
615
|
-
"total_cost": cost_data.get("current_month", 0),
|
616
|
-
"last_month_cost": cost_data.get("last_month", 0),
|
617
|
-
"services": top_services,
|
618
|
-
"budget_status": budget_status,
|
619
|
-
"budget_data": budget_data,
|
620
|
-
"full_cost_data": cost_data,
|
621
|
-
"target_account_id": target_account_id, # Track for debugging
|
622
|
-
}
|
623
|
-
|
624
|
-
except Exception as e:
|
625
|
-
return {
|
626
|
-
"profile": profile,
|
627
|
-
"account_id": "Error",
|
628
|
-
"success": False,
|
629
|
-
"error": str(e),
|
630
|
-
"total_cost": 0,
|
631
|
-
"services": {},
|
632
|
-
}
|
633
|
-
|
634
|
-
def _get_account_specific_cost_data(
|
635
|
-
self, cost_session, account_id: str, time_range, tag, profile_name: str
|
636
|
-
) -> Dict[str, Any]:
|
637
|
-
"""
|
638
|
-
Get account-specific cost data directly from AWS Cost Explorer.
|
639
|
-
|
640
|
-
Returns real AWS Cost Explorer data without any synthesis or manipulation.
|
641
|
-
|
642
|
-
Args:
|
643
|
-
cost_session: AWS Cost Explorer session
|
644
|
-
account_id: Target account ID for cost filtering
|
645
|
-
time_range: Time range for cost analysis
|
646
|
-
tag: Tag filters
|
647
|
-
profile_name: Profile name for session context
|
648
|
-
|
649
|
-
Returns:
|
650
|
-
Dictionary containing real AWS cost data from Cost Explorer API
|
651
|
-
"""
|
652
|
-
try:
|
653
|
-
# Get real cost data from Cost Explorer API with account-specific filtering
|
654
|
-
cost_data = get_cost_data(
|
655
|
-
cost_session,
|
656
|
-
time_range,
|
657
|
-
tag,
|
658
|
-
profile_name=profile_name,
|
659
|
-
account_id=account_id, # CRITICAL FIX: Add account filtering to avoid organization-wide data
|
660
|
-
)
|
661
|
-
|
662
|
-
self._log_technical_detail(f"Retrieved account-specific AWS data for account {account_id}")
|
663
|
-
return cost_data
|
664
|
-
|
665
|
-
except Exception as e:
|
666
|
-
print_warning(f"Account-specific cost data failed for {account_id}: {str(e)[:50]}")
|
667
|
-
# Fallback to regular cost data (without account filtering)
|
668
|
-
return get_cost_data(cost_session, time_range, tag, profile_name=profile_name)
|
669
|
-
|
670
|
-
def _calculate_budget_status(self, current_cost: float, budget_data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
671
|
-
"""
|
672
|
-
Calculate enhanced budget status for an account with comprehensive information.
|
673
|
-
|
674
|
-
Returns budget utilization, status, and financial details for enterprise visibility.
|
675
|
-
"""
|
676
|
-
if not budget_data:
|
677
|
-
return {
|
678
|
-
"status": "no_budget",
|
679
|
-
"display": "[dim]No Budget Set[/]\n[dim]Consider budget alerts[/]",
|
680
|
-
"utilization": 0,
|
681
|
-
"details": "No budgets configured for this account",
|
682
|
-
"recommendation": "Set up monthly cost budget with alerts",
|
683
|
-
}
|
684
|
-
|
685
|
-
# Use first cost budget for primary analysis (prioritize cost over usage budgets)
|
686
|
-
primary_budget = None
|
687
|
-
for budget in budget_data:
|
688
|
-
if budget.get("budget_type", "").upper() == "COST":
|
689
|
-
primary_budget = budget
|
690
|
-
break
|
691
|
-
|
692
|
-
# Fallback to first budget if no cost budget found
|
693
|
-
if not primary_budget:
|
694
|
-
primary_budget = budget_data[0] if budget_data else None
|
695
|
-
|
696
|
-
if not primary_budget:
|
697
|
-
return {
|
698
|
-
"status": "no_budget",
|
699
|
-
"display": "[dim]No Valid Budget[/]",
|
700
|
-
"utilization": 0,
|
701
|
-
"details": "No valid budgets found",
|
702
|
-
"recommendation": "Create monthly cost budget",
|
703
|
-
}
|
704
|
-
|
705
|
-
budget_limit = primary_budget.get("limit", 0)
|
706
|
-
budget_name = primary_budget.get("name", "Budget")
|
707
|
-
budget_type = primary_budget.get("budget_type", "COST")
|
708
|
-
|
709
|
-
if budget_limit == 0:
|
710
|
-
return {
|
711
|
-
"status": "no_limit",
|
712
|
-
"display": f"[dim]Unlimited {budget_type}[/]\n[dim]{budget_name}[/]",
|
713
|
-
"utilization": 0,
|
714
|
-
"details": f'Budget "{budget_name}" has no spending limit',
|
715
|
-
"recommendation": "Set specific budget limit for cost control",
|
716
|
-
}
|
717
|
-
|
718
|
-
# Calculate utilization with enhanced precision
|
719
|
-
utilization_percent = (current_cost / budget_limit) * 100
|
720
|
-
remaining_budget = budget_limit - current_cost
|
721
|
-
|
722
|
-
# Enhanced status classification with detailed budget information
|
723
|
-
from ..common.rich_utils import format_cost
|
724
|
-
|
725
|
-
if utilization_percent >= 100:
|
726
|
-
overspend = current_cost - budget_limit
|
727
|
-
return {
|
728
|
-
"status": "over_budget",
|
729
|
-
"display": f"[red]🚨 Over Budget[/]\n[red]{utilization_percent:.0f}% ({format_cost(current_cost)}/{format_cost(budget_limit)})[/]",
|
730
|
-
"utilization": utilization_percent,
|
731
|
-
"details": f'Exceeded "{budget_name}" by {format_cost(overspend)}',
|
732
|
-
"recommendation": "Immediate cost review and optimization required",
|
733
|
-
"budget_limit": budget_limit,
|
734
|
-
"remaining_budget": remaining_budget,
|
735
|
-
"budget_name": budget_name,
|
736
|
-
}
|
737
|
-
elif utilization_percent >= 90:
|
738
|
-
return {
|
739
|
-
"status": "critical",
|
740
|
-
"display": f"[red]⚠️ Critical: {utilization_percent:.0f}%[/]\n[red]{format_cost(remaining_budget)} left[/]",
|
741
|
-
"utilization": utilization_percent,
|
742
|
-
"details": f'Approaching "{budget_name}" limit - {format_cost(remaining_budget)} remaining',
|
743
|
-
"recommendation": "Review and optimize high-cost services immediately",
|
744
|
-
"budget_limit": budget_limit,
|
745
|
-
"remaining_budget": remaining_budget,
|
746
|
-
"budget_name": budget_name,
|
747
|
-
}
|
748
|
-
elif utilization_percent >= 75:
|
749
|
-
return {
|
750
|
-
"status": "warning",
|
751
|
-
"display": f"[yellow]⚠️ Warning: {utilization_percent:.0f}%[/]\n[yellow]{format_cost(remaining_budget)} left[/]",
|
752
|
-
"utilization": utilization_percent,
|
753
|
-
"details": f'75% of "{budget_name}" used - {format_cost(remaining_budget)} remaining',
|
754
|
-
"recommendation": "Monitor spending closely and review high-cost services",
|
755
|
-
"budget_limit": budget_limit,
|
756
|
-
"remaining_budget": remaining_budget,
|
757
|
-
"budget_name": budget_name,
|
758
|
-
}
|
759
|
-
elif utilization_percent >= 50:
|
760
|
-
return {
|
761
|
-
"status": "moderate",
|
762
|
-
"display": f"[cyan]📊 On Track: {utilization_percent:.0f}%[/]\n[cyan]{format_cost(remaining_budget)} left[/]",
|
763
|
-
"utilization": utilization_percent,
|
764
|
-
"details": f'Moderate usage of "{budget_name}" - {format_cost(remaining_budget)} remaining',
|
765
|
-
"recommendation": "Continue monitoring, budget tracking is on schedule",
|
766
|
-
"budget_limit": budget_limit,
|
767
|
-
"remaining_budget": remaining_budget,
|
768
|
-
"budget_name": budget_name,
|
769
|
-
}
|
770
|
-
else:
|
771
|
-
return {
|
772
|
-
"status": "under_budget",
|
773
|
-
"display": f"[green]✅ Under Budget: {utilization_percent:.0f}%[/]\n[green]{format_cost(remaining_budget)} available[/]",
|
774
|
-
"utilization": utilization_percent,
|
775
|
-
"details": f'Low utilization of "{budget_name}" - {format_cost(remaining_budget)} available',
|
776
|
-
"recommendation": "Budget utilization is low, consider cost optimization opportunities",
|
777
|
-
"budget_limit": budget_limit,
|
778
|
-
"remaining_budget": remaining_budget,
|
779
|
-
"budget_name": budget_name,
|
780
|
-
}
|
781
|
-
|
782
|
-
def _display_account_focused_table(
|
783
|
-
self, accounts: List[Dict[str, Any]], services_per_account: int, args: Optional[argparse.Namespace] = None
|
784
|
-
) -> None:
|
785
|
-
"""
|
786
|
-
Display the account-focused analysis table with enhanced Rich CLI beautiful styling.
|
787
|
-
|
788
|
-
CRITICAL FIX: Show both active and inactive accounts for complete data transparency
|
789
|
-
|
790
|
-
WIP.md Requirements:
|
791
|
-
- Rich beautiful tables by default (most user-friendly for CLI)
|
792
|
-
- Exclude "Tax" from Top 3 Service Usage (no analytical insights)
|
793
|
-
"""
|
794
|
-
|
795
|
-
# CRITICAL FIX: Separate active and inactive accounts for display using account metadata
|
796
|
-
active_accounts = []
|
797
|
-
inactive_accounts = []
|
798
|
-
|
799
|
-
for account in accounts:
|
800
|
-
account_id = None
|
801
|
-
account_status = "ACTIVE" # Default assumption
|
802
|
-
|
803
|
-
# Extract account ID from profile or account data
|
804
|
-
if "@" in account.get("profile", ""):
|
805
|
-
base_profile, account_id = account["profile"].split("@", 1)
|
806
|
-
else:
|
807
|
-
account_id = account.get("account_id", "unknown")
|
808
|
-
|
809
|
-
# Use account metadata to determine actual status
|
810
|
-
if account_id in self.account_metadata:
|
811
|
-
account_status = self.account_metadata[account_id].get("status", "ACTIVE")
|
812
|
-
# Store account metadata in the account dict for inactive display
|
813
|
-
account["account_metadata"] = self.account_metadata[account_id]
|
814
|
-
|
815
|
-
# Categorize accounts based on status AND processing success
|
816
|
-
if account.get("success", True) and account_status == "ACTIVE":
|
817
|
-
active_accounts.append(account)
|
818
|
-
else:
|
819
|
-
# Account is either inactive or failed processing
|
820
|
-
account["account_status"] = account_status
|
821
|
-
inactive_accounts.append(account)
|
822
|
-
|
823
|
-
# Display Active Accounts Table
|
824
|
-
if active_accounts:
|
825
|
-
self._display_active_accounts_table(active_accounts, services_per_account, args)
|
826
|
-
|
827
|
-
# Display Inactive Accounts Table (if any found)
|
828
|
-
if inactive_accounts:
|
829
|
-
self._display_inactive_accounts_table(inactive_accounts, services_per_account, args)
|
830
|
-
|
831
|
-
# Display Unprocessed Inactive Accounts (accounts that were never processed due to inactive status)
|
832
|
-
if self.account_metadata:
|
833
|
-
self._display_unprocessed_inactive_accounts(accounts)
|
834
|
-
|
835
|
-
def _display_active_accounts_table(
|
836
|
-
self, accounts: List[Dict[str, Any]], services_per_account: int, args: Optional[argparse.Namespace] = None
|
837
|
-
) -> None:
|
838
|
-
"""Display the active accounts table with full functionality."""
|
839
|
-
|
840
|
-
# SRE ENHANCEMENT: Beautiful Rich CLI table with enhanced styling per WIP.md requirements
|
841
|
-
# CRITICAL FIX: Increased Account ID column width from 22 to 35 for better account name readability
|
842
|
-
table = Table(
|
843
|
-
Column("Account Name", style="bold bright_white", width=35, no_wrap=False),
|
844
|
-
Column("Last Month", justify="right", style="bold yellow", width=12, no_wrap=True),
|
845
|
-
Column("Current Month", justify="right", style="bold green", width=12, no_wrap=True),
|
846
|
-
Column(f"Top {services_per_account} Service Usage", style="bright_cyan", width=28, no_wrap=False),
|
847
|
-
Column("Budget Status", justify="center", style="bold", width=18, no_wrap=False),
|
848
|
-
Column("Stopped EC2", justify="center", style="dim cyan", width=11, no_wrap=True),
|
849
|
-
Column("Unused Vol", justify="center", style="dim cyan", width=11, no_wrap=True),
|
850
|
-
Column("Unused EIP", justify="center", style="dim cyan", width=11, no_wrap=True),
|
851
|
-
Column("Savings", justify="right", style="bold bright_green", width=10, no_wrap=True),
|
852
|
-
Column("Untagged", justify="center", style="dim yellow", width=10, no_wrap=True),
|
853
|
-
title=f"🏢 Multi-Account FinOps Dashboard - {len(accounts)} Active Accounts",
|
854
|
-
box=box.DOUBLE_EDGE, # WIP.md: More beautiful border style
|
855
|
-
border_style="bright_cyan", # WIP.md: Beautiful colored boundaries
|
856
|
-
title_style="bold white on blue",
|
857
|
-
header_style="bold bright_cyan",
|
858
|
-
show_lines=True,
|
859
|
-
row_styles=["", "dim"], # Alternating row colors for readability
|
860
|
-
caption="[dim italic]✨ Rich CLI Enhanced • Tax services excluded for analytical focus • Enterprise SRE standards[/]",
|
861
|
-
caption_style="dim italic bright_black",
|
862
|
-
)
|
863
|
-
|
864
|
-
for account in accounts:
|
865
|
-
if not account["success"]:
|
866
|
-
# Use readable account name for error cases too
|
867
|
-
error_profile_raw = account["profile"]
|
868
|
-
error_account_id = None
|
869
|
-
|
870
|
-
if "@" in error_profile_raw:
|
871
|
-
base_profile, error_account_id = error_profile_raw.split("@", 1)
|
872
|
-
else:
|
873
|
-
error_account_id = account.get("account_id", "N/A")
|
874
|
-
|
875
|
-
if self.account_resolver and error_account_id and error_account_id != "N/A":
|
876
|
-
error_account_name = self.account_resolver.get_account_name(error_account_id, max_length=35)
|
877
|
-
if error_account_name and error_account_name != error_account_id:
|
878
|
-
error_account_display = (
|
879
|
-
f"[bold red]{error_account_name}[/bold red]\n[dim red]{error_account_id}[/]"
|
880
|
-
)
|
881
|
-
else:
|
882
|
-
error_account_display = f"[bold red]{error_account_id}[/bold red]"
|
883
|
-
else:
|
884
|
-
error_account_display = (
|
885
|
-
f"[red]{error_profile_raw[:32]}{'...' if len(error_profile_raw) > 32 else ''}[/]"
|
886
|
-
)
|
887
|
-
|
888
|
-
table.add_row(
|
889
|
-
error_account_display,
|
890
|
-
"[red]Error[/]",
|
891
|
-
"[red]Error[/]",
|
892
|
-
f"[red]Failed: {account.get('error', 'Unknown error')[:20]}[/]",
|
893
|
-
"[red]N/A[/]",
|
894
|
-
"[red]N/A[/]",
|
895
|
-
"[red]N/A[/]",
|
896
|
-
"[red]N/A[/]",
|
897
|
-
"[red]N/A[/]",
|
898
|
-
"[red]N/A[/]",
|
899
|
-
)
|
900
|
-
continue
|
901
|
-
|
902
|
-
# Core cost data
|
903
|
-
current = account["total_cost"]
|
904
|
-
previous = account["last_month_cost"]
|
905
|
-
|
906
|
-
# Format top services with standardized AWS service mapping
|
907
|
-
services_text = []
|
908
|
-
for service, cost in list(account["services"].items())[:services_per_account]:
|
909
|
-
# Use standardized service name mapping (RDS, S3, CloudWatch, etc.)
|
910
|
-
display_name = get_service_display_name(service)
|
911
|
-
# Ensure service names fit within column width (max 12 chars for service name)
|
912
|
-
if len(display_name) > 12:
|
913
|
-
display_name = display_name[:12]
|
914
|
-
services_text.append(f"{display_name}: ${cost:.0f}")
|
915
|
-
services_display = "\n".join(services_text) if services_text else "[dim]None[/]"
|
916
|
-
|
917
|
-
# Budget status (compact and aligned)
|
918
|
-
budget_status = account.get("budget_status", {})
|
919
|
-
raw_budget_display = budget_status.get("display", "[dim]No Budget[/]")
|
920
|
-
# Enhanced budget display with proper formatting for 18-character width
|
921
|
-
# Remove Rich markup tags to calculate actual display length
|
922
|
-
clean_text = (
|
923
|
-
raw_budget_display.replace("[dim]", "")
|
924
|
-
.replace("[/]", "")
|
925
|
-
.replace("[red]", "")
|
926
|
-
.replace("[yellow]", "")
|
927
|
-
.replace("[green]", "")
|
928
|
-
.replace("[cyan]", "")
|
929
|
-
.replace("[bright_red]", "")
|
930
|
-
.replace("🚨", "")
|
931
|
-
.replace("⚠️", "")
|
932
|
-
.replace("✅", "")
|
933
|
-
.replace("📊", "")
|
934
|
-
.replace("💰", "")
|
935
|
-
.replace("💸", "")
|
936
|
-
)
|
937
|
-
|
938
|
-
# If budget display is too long, create a more informative truncation
|
939
|
-
if len(clean_text) > 18:
|
940
|
-
# Extract key budget information for truncation
|
941
|
-
utilization = budget_status.get("utilization", 0)
|
942
|
-
status = budget_status.get("status", "unknown")
|
943
|
-
|
944
|
-
if status == "over_budget":
|
945
|
-
budget_display = "[red]🚨 Over Budget[/]"
|
946
|
-
elif status == "critical":
|
947
|
-
budget_display = f"[red]⚠️ {utilization:.0f}%[/]"
|
948
|
-
elif status == "warning":
|
949
|
-
budget_display = f"[yellow]⚠️ {utilization:.0f}%[/]"
|
950
|
-
elif status == "moderate" or status == "under_budget":
|
951
|
-
budget_display = f"[green]✅ {utilization:.0f}%[/]"
|
952
|
-
elif status == "no_budget":
|
953
|
-
budget_display = "[dim]No Budget Set[/]"
|
954
|
-
elif status == "access_denied":
|
955
|
-
budget_display = "[yellow]⚠️ No Access[/]"
|
956
|
-
else:
|
957
|
-
budget_display = "[dim]Unknown[/]"
|
958
|
-
else:
|
959
|
-
budget_display = raw_budget_display
|
960
|
-
|
961
|
-
# Calculate potential savings (placeholder - can be enhanced with real analysis)
|
962
|
-
potential_savings = current * 0.15 # 15% potential optimization
|
963
|
-
savings_display = f"${potential_savings:.0f}" if potential_savings > 100 else "[dim]<$100[/]"
|
964
|
-
|
965
|
-
# Resource optimization data (placeholder - can be enhanced with real EC2/EBS/EIP analysis)
|
966
|
-
stopped_ec2 = self._get_stopped_instances_count(account)
|
967
|
-
unused_volumes = self._get_unused_volumes_count(account)
|
968
|
-
unused_eips = self._get_unused_eips_count(account)
|
969
|
-
untagged_resources = self._get_untagged_resources_count(account)
|
970
|
-
|
971
|
-
# SRE ENHANCEMENT: Use readable account names from Organizations API
|
972
|
-
profile_raw = account["profile"]
|
973
|
-
account_id = None
|
974
|
-
|
975
|
-
if "@" in profile_raw:
|
976
|
-
# Organizations API format: "base-profile@123456789001"
|
977
|
-
base_profile, account_id = profile_raw.split("@", 1)
|
978
|
-
else:
|
979
|
-
# Legacy single-account format - try to extract account ID
|
980
|
-
account_id = account.get("account_id", "N/A")
|
981
|
-
|
982
|
-
# CRITICAL FIX: Use improved account name resolution with proper width (35 chars)
|
983
|
-
if self.account_resolver and account_id and account_id != "N/A":
|
984
|
-
account_name = self.account_resolver.get_account_name(account_id, max_length=35)
|
985
|
-
if account_name and account_name != account_id:
|
986
|
-
# Use the intelligently truncated account name from resolver
|
987
|
-
account_display = f"[bold]{account_name}[/bold]\n[dim]{account_id}[/]"
|
988
|
-
else:
|
989
|
-
# Fallback: account ID with shortened profile name
|
990
|
-
profile_short = profile_raw[:30] + ("..." if len(profile_raw) > 30 else "")
|
991
|
-
account_display = f"[bold]{account_id}[/bold]\n[dim]{profile_short}[/]"
|
992
|
-
else:
|
993
|
-
# Fallback when resolver is not available
|
994
|
-
if account_id and account_id != "N/A":
|
995
|
-
profile_short = profile_raw[:30] + ("..." if len(profile_raw) > 30 else "")
|
996
|
-
account_display = f"[bold]{account_id}[/bold]\n[dim]{profile_short}[/]"
|
997
|
-
else:
|
998
|
-
account_display = f"[dim]{profile_raw[:32]}{'...' if len(profile_raw) > 32 else ''}[/]"
|
999
|
-
|
1000
|
-
table.add_row(
|
1001
|
-
account_display,
|
1002
|
-
format_cost(previous),
|
1003
|
-
format_cost(current),
|
1004
|
-
services_display,
|
1005
|
-
budget_display,
|
1006
|
-
str(stopped_ec2) if stopped_ec2 > 0 else "[dim]0[/]",
|
1007
|
-
str(unused_volumes) if unused_volumes > 0 else "[dim]0[/]",
|
1008
|
-
str(unused_eips) if unused_eips > 0 else "[dim]0[/]",
|
1009
|
-
savings_display,
|
1010
|
-
str(untagged_resources) if untagged_resources > 0 else "[dim]0[/]",
|
1011
|
-
)
|
1012
|
-
|
1013
|
-
self.console.print(table)
|
1014
|
-
|
1015
|
-
def _display_inactive_accounts_table(
|
1016
|
-
self, accounts: List[Dict[str, Any]], services_per_account: int, args: Optional[argparse.Namespace] = None
|
1017
|
-
) -> None:
|
1018
|
-
"""
|
1019
|
-
Display inactive/orphaned accounts table for complete data transparency.
|
1020
|
-
|
1021
|
-
CRITICAL FIX: Shows Account #61 and any other non-ACTIVE accounts that were previously hidden.
|
1022
|
-
"""
|
1023
|
-
|
1024
|
-
if not accounts:
|
1025
|
-
return
|
1026
|
-
|
1027
|
-
# Create a simplified table for inactive accounts
|
1028
|
-
inactive_table = Table(
|
1029
|
-
Column("Account Name", style="dim white", width=35, no_wrap=False),
|
1030
|
-
Column("Account Status", justify="center", style="bold yellow", width=15, no_wrap=True),
|
1031
|
-
Column("Discovery Method", style="dim cyan", width=20, no_wrap=True),
|
1032
|
-
Column("Email", style="dim", width=30, no_wrap=False),
|
1033
|
-
Column("Notes", style="dim yellow", width=40, no_wrap=False),
|
1034
|
-
title=f"⚠️ Inactive/Orphaned Accounts - {len(accounts)} Accounts (Complete Data Transparency)",
|
1035
|
-
box=box.ROUNDED,
|
1036
|
-
border_style="yellow",
|
1037
|
-
title_style="bold yellow",
|
1038
|
-
header_style="bold yellow",
|
1039
|
-
show_lines=True,
|
1040
|
-
caption="[dim italic]⚠️ These accounts are discovered but have non-ACTIVE status • No cost analysis available • Enterprise compliance visibility[/]",
|
1041
|
-
caption_style="dim italic yellow",
|
1042
|
-
)
|
1043
|
-
|
1044
|
-
for account in accounts:
|
1045
|
-
# Extract account information using enhanced metadata
|
1046
|
-
profile_raw = account.get("profile", "Unknown")
|
1047
|
-
account_id = None
|
1048
|
-
|
1049
|
-
if "@" in profile_raw:
|
1050
|
-
base_profile, account_id = profile_raw.split("@", 1)
|
1051
|
-
else:
|
1052
|
-
account_id = account.get("account_id", "N/A")
|
1053
|
-
|
1054
|
-
# Use account metadata if available (for Organizations API discovered accounts)
|
1055
|
-
if "account_metadata" in account:
|
1056
|
-
metadata = account["account_metadata"]
|
1057
|
-
account_status = metadata.get("status", "UNKNOWN")
|
1058
|
-
discovery_method = metadata.get("discovery_method", "Organizations API")
|
1059
|
-
email = metadata.get("email", "unknown@example.com")
|
1060
|
-
|
1061
|
-
if account.get("success", False):
|
1062
|
-
# Account was discovered but has inactive status
|
1063
|
-
if account_status in ["SUSPENDED", "CLOSED"]:
|
1064
|
-
notes = f"Account {account_status.lower()} - no cost analysis possible"
|
1065
|
-
else:
|
1066
|
-
notes = f"Account status: {account_status} - limited analysis available"
|
1067
|
-
else:
|
1068
|
-
# Account discovery succeeded but processing failed
|
1069
|
-
error_msg = account.get("error", "Unknown processing error")
|
1070
|
-
notes = f"Status: {account_status}, Processing failed: {error_msg[:25]}"
|
1071
|
-
else:
|
1072
|
-
# Fallback for accounts without metadata
|
1073
|
-
account_status = account.get("account_status", "PROCESSING_FAILED")
|
1074
|
-
discovery_method = account.get("discovery_method", "Organizations API")
|
1075
|
-
email = account.get("email", "unknown@example.com")
|
1076
|
-
|
1077
|
-
if account.get("success", False):
|
1078
|
-
notes = f"Account identified but inactive/suspended"
|
1079
|
-
else:
|
1080
|
-
error_msg = account.get("error", "Unknown error")
|
1081
|
-
notes = f"Processing failed: {error_msg[:30]}"
|
1082
|
-
|
1083
|
-
# CRITICAL FIX: Use improved account name resolution for inactive accounts too
|
1084
|
-
if self.account_resolver and account_id and account_id not in ["N/A", "Error", "Unknown"]:
|
1085
|
-
account_name = self.account_resolver.get_account_name(account_id, max_length=35)
|
1086
|
-
if account_name and account_name != account_id:
|
1087
|
-
# Show both name and ID for clarity in inactive accounts
|
1088
|
-
account_display = f"[dim bold]{account_name}[/dim bold]\n[dim]{account_id}[/dim]"
|
1089
|
-
else:
|
1090
|
-
account_display = f"[dim bold]{account_id}[/dim bold]"
|
1091
|
-
else:
|
1092
|
-
account_display = f"[dim]{account_id}[/dim]"
|
1093
|
-
|
1094
|
-
# Status with appropriate styling
|
1095
|
-
if account_status in ["SUSPENDED", "CLOSED"]:
|
1096
|
-
status_display = f"[bold red]{account_status}[/bold red]"
|
1097
|
-
elif account_status == "PROCESSING_FAILED":
|
1098
|
-
status_display = f"[bold red]FAILED[/bold red]"
|
1099
|
-
else:
|
1100
|
-
status_display = f"[bold yellow]{account_status}[/bold yellow]"
|
1101
|
-
|
1102
|
-
inactive_table.add_row(
|
1103
|
-
account_display,
|
1104
|
-
status_display,
|
1105
|
-
f"[dim]{discovery_method}[/dim]",
|
1106
|
-
f"[dim]{email}[/dim]",
|
1107
|
-
f"[dim]{notes}[/dim]",
|
1108
|
-
)
|
1109
|
-
|
1110
|
-
# Add spacing before inactive accounts table
|
1111
|
-
self.console.print()
|
1112
|
-
self.console.print(inactive_table)
|
1113
|
-
|
1114
|
-
def _display_unprocessed_inactive_accounts(self, processed_accounts: List[Dict[str, Any]]) -> None:
|
1115
|
-
"""
|
1116
|
-
Display accounts that were discovered but never processed due to inactive status.
|
1117
|
-
|
1118
|
-
This shows the complete picture including Account #61 that might be filtered out entirely.
|
1119
|
-
"""
|
1120
|
-
|
1121
|
-
# Get account IDs that were processed (both active and inactive)
|
1122
|
-
processed_account_ids = set()
|
1123
|
-
for account in processed_accounts:
|
1124
|
-
if "@" in account.get("profile", ""):
|
1125
|
-
base_profile, account_id = account["profile"].split("@", 1)
|
1126
|
-
processed_account_ids.add(account_id)
|
1127
|
-
|
1128
|
-
# Find accounts in metadata that were never processed
|
1129
|
-
unprocessed_accounts = []
|
1130
|
-
for account_id, metadata in self.account_metadata.items():
|
1131
|
-
if account_id not in processed_account_ids and metadata.get("status") != "ACTIVE":
|
1132
|
-
unprocessed_accounts.append(metadata)
|
1133
|
-
|
1134
|
-
if not unprocessed_accounts:
|
1135
|
-
return
|
1136
|
-
|
1137
|
-
# Create table for unprocessed inactive accounts
|
1138
|
-
unprocessed_table = Table(
|
1139
|
-
Column("Account ID", style="dim red", width=25, no_wrap=False),
|
1140
|
-
Column("Account Name", style="dim white", width=30, no_wrap=False),
|
1141
|
-
Column("Status", justify="center", style="bold red", width=15, no_wrap=True),
|
1142
|
-
Column("Email", style="dim", width=35, no_wrap=False),
|
1143
|
-
Column("Reason Not Processed", style="dim yellow", width=40, no_wrap=False),
|
1144
|
-
title=f"🚨 Unprocessed Inactive Accounts - {len(unprocessed_accounts)} Accounts (Complete Transparency)",
|
1145
|
-
box=box.HEAVY,
|
1146
|
-
border_style="red",
|
1147
|
-
title_style="bold red",
|
1148
|
-
header_style="bold red",
|
1149
|
-
show_lines=True,
|
1150
|
-
caption="[dim italic]🚨 CRITICAL: These accounts were discovered but filtered out due to inactive status • Account #61 visibility[/]",
|
1151
|
-
caption_style="dim italic red",
|
1152
|
-
)
|
1153
|
-
|
1154
|
-
for account in unprocessed_accounts:
|
1155
|
-
account_id = account["id"]
|
1156
|
-
account_name = account.get("name", f"Account-{account_id}")
|
1157
|
-
account_status = account.get("status", "UNKNOWN")
|
1158
|
-
email = account.get("email", "unknown@example.com")
|
1159
|
-
|
1160
|
-
# Determine reason for not processing
|
1161
|
-
if account_status in ["SUSPENDED", "CLOSED"]:
|
1162
|
-
reason = f"Account {account_status.lower()} - cost analysis not applicable"
|
1163
|
-
else:
|
1164
|
-
reason = f"Non-ACTIVE status ({account_status}) - excluded from processing"
|
1165
|
-
|
1166
|
-
# Use account resolver if available
|
1167
|
-
if self.account_resolver and account_id:
|
1168
|
-
resolver_name = self.account_resolver.get_account_name(account_id)
|
1169
|
-
if resolver_name and resolver_name != account_id and resolver_name != account_name:
|
1170
|
-
display_name = f"{resolver_name}"
|
1171
|
-
else:
|
1172
|
-
display_name = account_name
|
1173
|
-
else:
|
1174
|
-
display_name = account_name
|
1175
|
-
|
1176
|
-
# Account display with both name and ID
|
1177
|
-
if display_name and display_name != account_id:
|
1178
|
-
account_display = f"[dim bold red]{display_name}[/dim bold red]\n[dim red]{account_id}[/dim red]"
|
1179
|
-
else:
|
1180
|
-
account_display = f"[dim bold red]{account_id}[/dim bold red]"
|
1181
|
-
|
1182
|
-
unprocessed_table.add_row(
|
1183
|
-
account_display,
|
1184
|
-
f"[dim]{display_name}[/dim]",
|
1185
|
-
f"[bold red]{account_status}[/bold red]",
|
1186
|
-
f"[dim]{email}[/dim]",
|
1187
|
-
f"[dim yellow]{reason}[/dim yellow]",
|
1188
|
-
)
|
1189
|
-
|
1190
|
-
# Add spacing and display table
|
1191
|
-
self.console.print()
|
1192
|
-
self.console.print(unprocessed_table)
|
1193
|
-
|
1194
|
-
# Add summary message
|
1195
|
-
summary_msg = f"""
|
1196
|
-
[bold red]🚨 Data Completeness Alert:[/bold red] Found {len(unprocessed_accounts)} accounts that were discovered but not processed.
|
1197
|
-
[yellow]These accounts (including potentially Account #61) have non-ACTIVE status and were excluded from cost analysis.[/yellow]
|
1198
|
-
[dim]This display ensures complete organizational visibility and audit compliance.[/dim]
|
1199
|
-
"""
|
1200
|
-
|
1201
|
-
self.console.print(
|
1202
|
-
Panel(
|
1203
|
-
summary_msg.strip(),
|
1204
|
-
title="[bold red]Complete Account Visibility[/bold red]",
|
1205
|
-
title_align="left",
|
1206
|
-
border_style="red",
|
1207
|
-
style="dim",
|
1208
|
-
)
|
1209
|
-
)
|
1210
|
-
|
1211
|
-
def _get_account_optimization_recommendation(self, account: Dict[str, Any]) -> str:
|
1212
|
-
"""Generate account-level optimization recommendation."""
|
1213
|
-
total_cost = account.get("total_cost", 0)
|
1214
|
-
budget_status = account.get("budget_status", {})
|
1215
|
-
|
1216
|
-
if budget_status.get("status") == "over_budget":
|
1217
|
-
return "[red]Budget Review Required[/]"
|
1218
|
-
elif total_cost > 5000:
|
1219
|
-
return "[yellow]Cost Optimization Review[/]"
|
1220
|
-
elif total_cost > 1000:
|
1221
|
-
return "[blue]Resource Right-sizing[/]"
|
1222
|
-
else:
|
1223
|
-
return "[green]Monitor & Optimize[/]"
|
1224
|
-
|
1225
|
-
def _display_cross_account_summary(self, accounts: List[Dict[str, Any]]) -> None:
|
1226
|
-
"""Display cross-account summary insights."""
|
1227
|
-
if not accounts:
|
1228
|
-
return
|
1229
|
-
|
1230
|
-
total_spend = sum(acc.get("total_cost", 0) for acc in accounts)
|
1231
|
-
total_last_month = sum(acc.get("last_month_cost", 0) for acc in accounts)
|
1232
|
-
|
1233
|
-
# Budget summary
|
1234
|
-
over_budget_count = sum(1 for acc in accounts if acc.get("budget_status", {}).get("status") == "over_budget")
|
1235
|
-
warning_count = sum(1 for acc in accounts if acc.get("budget_status", {}).get("status") == "warning")
|
1236
|
-
|
1237
|
-
# Service distribution (SRE ENHANCEMENT: Use centralized filtering per WIP.md requirements)
|
1238
|
-
all_services = defaultdict(float)
|
1239
|
-
for account in accounts:
|
1240
|
-
account_services = account.get("services", {})
|
1241
|
-
# Apply centralized filtering for consistency
|
1242
|
-
filtered_account_services = filter_analytical_services(account_services)
|
1243
|
-
for service, cost in filtered_account_services.items():
|
1244
|
-
all_services[service] += cost
|
1245
|
-
|
1246
|
-
top_org_services = sorted(all_services.items(), key=lambda x: x[1], reverse=True)[:5]
|
1247
|
-
|
1248
|
-
# Create summary panel with enhanced trend analysis
|
1249
|
-
from .cost_processor import calculate_trend_with_context
|
1250
|
-
|
1251
|
-
# For multi-account analysis, we generally have full month data, but check for consistency
|
1252
|
-
overall_trend_display = calculate_trend_with_context(total_spend, total_last_month)
|
1253
|
-
|
1254
|
-
# Extract trend direction for icon (maintaining existing functionality)
|
1255
|
-
if total_last_month > 0:
|
1256
|
-
overall_trend_pct = ((total_spend - total_last_month) / total_last_month * 100)
|
1257
|
-
trend_icon = "⬆" if overall_trend_pct > 0 else "⬇" if overall_trend_pct < 0 else "➡"
|
1258
|
-
else:
|
1259
|
-
trend_icon = "➡"
|
1260
|
-
|
1261
|
-
summary_text = f"""
|
1262
|
-
[highlight]Organization Summary[/]
|
1263
|
-
• Total Accounts: {len(accounts)}
|
1264
|
-
• Total Monthly Spend: {format_cost(total_spend)}
|
1265
|
-
• Overall Trend: {overall_trend_display}
|
1266
|
-
• Budget Alerts: {over_budget_count} over budget, {warning_count} warnings
|
1267
|
-
|
1268
|
-
[highlight]Top Organization Services[/]
|
1269
|
-
{chr(10).join([f"• {get_service_display_name(service)}: {format_cost(cost)}" for service, cost in top_org_services])}
|
1270
|
-
"""
|
1271
|
-
|
1272
|
-
self.console.print(Panel(summary_text.strip(), title="🏢 Cross-Account Summary", style="info"))
|
1273
|
-
|
1274
|
-
def _export_account_analysis(self, args: argparse.Namespace, accounts: List[Dict[str, Any]]) -> None:
|
1275
|
-
"""Export multi-account analysis results."""
|
1276
|
-
try:
|
1277
|
-
if hasattr(args, "report_type") and args.report_type:
|
1278
|
-
export_data = []
|
1279
|
-
|
1280
|
-
for account in accounts:
|
1281
|
-
export_data.append(
|
1282
|
-
{
|
1283
|
-
"account_id": account.get("account_id"),
|
1284
|
-
"profile": account.get("profile"),
|
1285
|
-
"total_cost": account.get("total_cost", 0),
|
1286
|
-
"last_month_cost": account.get("last_month_cost", 0),
|
1287
|
-
"top_services": account.get("services", {}),
|
1288
|
-
"budget_status": account.get("budget_status", {}),
|
1289
|
-
"analysis_type": "account_focused",
|
1290
|
-
}
|
1291
|
-
)
|
1292
|
-
|
1293
|
-
for report_type in args.report_type:
|
1294
|
-
if report_type == "json":
|
1295
|
-
json_path = export_to_json(export_data, args.report_name, getattr(args, "dir", None))
|
1296
|
-
if json_path:
|
1297
|
-
print_success(f"Multi-account analysis exported to JSON: {json_path}")
|
1298
|
-
elif report_type == "csv":
|
1299
|
-
csv_path = export_to_csv(export_data, args.report_name, getattr(args, "dir", None))
|
1300
|
-
if csv_path:
|
1301
|
-
print_success(f"Multi-account analysis exported to CSV: {csv_path}")
|
1302
|
-
|
1303
|
-
except Exception as e:
|
1304
|
-
print_warning(f"Export failed: {str(e)[:50]}")
|
1305
|
-
|
1306
|
-
def _export_account_analysis_to_markdown(
|
1307
|
-
self, args: argparse.Namespace, accounts: List[Dict[str, Any]], execution_time: float
|
1308
|
-
) -> None:
|
1309
|
-
"""
|
1310
|
-
Export account analysis to GitHub-compatible markdown format.
|
1311
|
-
|
1312
|
-
WIP.md requirement: --export-markdown flag for GitHub table format
|
1313
|
-
"""
|
1314
|
-
try:
|
1315
|
-
import os
|
1316
|
-
from datetime import datetime
|
1317
|
-
|
1318
|
-
# Prepare export path
|
1319
|
-
export_dir = getattr(args, "dir", "./artifacts/finops-exports")
|
1320
|
-
os.makedirs(export_dir, exist_ok=True)
|
1321
|
-
|
1322
|
-
report_name = getattr(args, "report_name", "multi-account-analysis")
|
1323
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
1324
|
-
markdown_path = os.path.join(export_dir, f"{report_name}_{timestamp}.md")
|
1325
|
-
|
1326
|
-
# Generate markdown content
|
1327
|
-
lines = []
|
1328
|
-
lines.append("# Multi-Account FinOps Analysis - Enterprise Dashboard")
|
1329
|
-
lines.append("")
|
1330
|
-
lines.append(f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
1331
|
-
lines.append(f"**Analysis Type**: Organization-wide multi-account cost analysis")
|
1332
|
-
lines.append(f"**Execution Time**: {execution_time:.1f}s")
|
1333
|
-
lines.append(f"**Accounts Processed**: {len(accounts)}")
|
1334
|
-
lines.append("")
|
1335
|
-
|
1336
|
-
# Create GitHub-compatible table with proper alignment syntax
|
1337
|
-
lines.append("## Account Analysis Summary")
|
1338
|
-
lines.append("")
|
1339
|
-
lines.append("| Account | Last Month | Current Month | Top 3 Services | Budget Status | Optimization |")
|
1340
|
-
lines.append("| --- | ---: | ---: | --- | :---: | --- |") # GitHub-compliant alignment
|
1341
|
-
|
1342
|
-
total_current = 0
|
1343
|
-
total_last = 0
|
1344
|
-
|
1345
|
-
for account in accounts:
|
1346
|
-
if not account["success"]:
|
1347
|
-
continue
|
1348
|
-
|
1349
|
-
current = account.get("total_cost", 0)
|
1350
|
-
last = account.get("last_month_cost", 0)
|
1351
|
-
total_current += current
|
1352
|
-
total_last += last
|
1353
|
-
|
1354
|
-
# Use readable account name from Organizations API (GitHub markdown format)
|
1355
|
-
profile_raw = account["profile"]
|
1356
|
-
account_id = None
|
1357
|
-
|
1358
|
-
if "@" in profile_raw:
|
1359
|
-
base_profile, account_id = profile_raw.split("@", 1)
|
1360
|
-
else:
|
1361
|
-
account_id = account.get("account_id", "N/A")
|
1362
|
-
|
1363
|
-
# Get readable account name for markdown display
|
1364
|
-
if self.account_resolver and account_id and account_id != "N/A":
|
1365
|
-
account_name = self.account_resolver.get_account_name(account_id)
|
1366
|
-
if account_name and account_name != account_id:
|
1367
|
-
account_display = f"{account_name} ({account_id})"
|
1368
|
-
else:
|
1369
|
-
account_display = account_id
|
1370
|
-
else:
|
1371
|
-
account_display = profile_raw[:30] + ("..." if len(profile_raw) > 30 else "")
|
1372
|
-
|
1373
|
-
# Format services using standardized AWS service mapping
|
1374
|
-
services = []
|
1375
|
-
account_services = account.get("services", {})
|
1376
|
-
filtered_services = filter_analytical_services(account_services)
|
1377
|
-
max_services_displayed = getattr(args, "max_services_displayed", 3)
|
1378
|
-
for service, cost in list(filtered_services.items())[:max_services_displayed]:
|
1379
|
-
# Use standardized service name mapping (RDS, S3, CloudWatch, etc.)
|
1380
|
-
display_name = get_service_display_name(service)
|
1381
|
-
services.append(f"{display_name}: ${cost:.0f}")
|
1382
|
-
|
1383
|
-
services_text = ", ".join(services) if services else "None"
|
1384
|
-
|
1385
|
-
# Enhanced budget status with comprehensive information for markdown
|
1386
|
-
budget_status = account.get("budget_status", {})
|
1387
|
-
budget_utilization = budget_status.get("utilization", 0)
|
1388
|
-
budget_limit = budget_status.get("budget_limit", 0)
|
1389
|
-
budget_name = budget_status.get("budget_name", "Budget")
|
1390
|
-
remaining_budget = budget_status.get("remaining_budget", 0)
|
1391
|
-
status = budget_status.get("status", "no_budget")
|
1392
|
-
|
1393
|
-
# Create comprehensive budget display for markdown export
|
1394
|
-
if status == "over_budget":
|
1395
|
-
budget_clean = f"🚨 OVER BUDGET: {budget_utilization:.0f}% (${current:,.0f}/${budget_limit:,.0f})"
|
1396
|
-
elif status == "critical":
|
1397
|
-
budget_clean = f"⚠️ CRITICAL: {budget_utilization:.0f}% (${remaining_budget:,.0f} left)"
|
1398
|
-
elif status == "warning":
|
1399
|
-
budget_clean = f"⚠️ WARNING: {budget_utilization:.0f}% (${remaining_budget:,.0f} left)"
|
1400
|
-
elif status in ["moderate", "under_budget"]:
|
1401
|
-
budget_clean = f"✅ ON TRACK: {budget_utilization:.0f}% (${remaining_budget:,.0f} available)"
|
1402
|
-
elif status == "no_budget":
|
1403
|
-
budget_clean = "No Budget Set"
|
1404
|
-
elif status == "access_denied":
|
1405
|
-
budget_clean = "⚠️ Access Denied"
|
1406
|
-
else:
|
1407
|
-
# Fallback: clean the Rich display text
|
1408
|
-
budget_display = budget_status.get("display", "Unknown")
|
1409
|
-
budget_clean = budget_display.replace("[red]", "").replace("[yellow]", "").replace("[green]", "")
|
1410
|
-
budget_clean = (
|
1411
|
-
budget_clean.replace("[/]", "")
|
1412
|
-
.replace("🚨", "Over")
|
1413
|
-
.replace("⚠️", "Warning")
|
1414
|
-
.replace("✅", "OK")
|
1415
|
-
)
|
1416
|
-
|
1417
|
-
# Optimization recommendation based on centralized config
|
1418
|
-
from runbooks.finops.config import get_high_cost_threshold, get_medium_cost_threshold
|
1419
|
-
high_cost_threshold = get_high_cost_threshold(args)
|
1420
|
-
medium_cost_threshold = get_medium_cost_threshold(args)
|
1421
|
-
|
1422
|
-
if current > high_cost_threshold:
|
1423
|
-
optimization = "Cost Review Required"
|
1424
|
-
elif current > medium_cost_threshold:
|
1425
|
-
optimization = "Right-sizing Review"
|
1426
|
-
else:
|
1427
|
-
optimization = "Monitor & Optimize"
|
1428
|
-
|
1429
|
-
# Add GitHub-compliant table row with proper escaping
|
1430
|
-
# Escape pipes in cell content for GitHub markdown compatibility
|
1431
|
-
account_display_escaped = account_display.replace("|", "\\|")
|
1432
|
-
services_text_escaped = services_text.replace("|", "\\|")[:100] # Limit length for readability
|
1433
|
-
budget_clean_escaped = budget_clean.replace("|", "\\|")
|
1434
|
-
optimization_escaped = optimization.replace("|", "\\|")
|
1435
|
-
|
1436
|
-
lines.append(
|
1437
|
-
f"| {account_display_escaped} | ${last:.0f} | ${current:.0f} | {services_text_escaped} | {budget_clean_escaped} | {optimization_escaped} |"
|
1438
|
-
)
|
1439
|
-
|
1440
|
-
# Add summary section with enhanced trend analysis
|
1441
|
-
overall_trend_display = calculate_trend_with_context(total_current, total_last)
|
1442
|
-
|
1443
|
-
# Extract trend direction for emoji (maintaining existing markdown export format)
|
1444
|
-
if total_last > 0:
|
1445
|
-
overall_trend_pct = ((total_current - total_last) / total_last * 100)
|
1446
|
-
trend_direction = "↗️" if overall_trend_pct > 0 else "↘️" if overall_trend_pct < 0 else "➡️"
|
1447
|
-
else:
|
1448
|
-
trend_direction = "➡️"
|
1449
|
-
|
1450
|
-
lines.append("")
|
1451
|
-
lines.append("## Organization Summary")
|
1452
|
-
lines.append("")
|
1453
|
-
lines.append(f"- **Total Accounts Analyzed**: {len(accounts)}")
|
1454
|
-
lines.append(f"- **Total Current Month**: ${total_current:,.2f}")
|
1455
|
-
lines.append(f"- **Total Last Month**: ${total_last:,.2f}")
|
1456
|
-
lines.append(f"- **Overall Trend**: {overall_trend_display}")
|
1457
|
-
lines.append(f"- **Analysis Performance**: {execution_time:.1f}s execution")
|
1458
|
-
lines.append("")
|
1459
|
-
|
1460
|
-
# Performance metrics section
|
1461
|
-
lines.append("## SRE Performance Metrics")
|
1462
|
-
lines.append("")
|
1463
|
-
lines.append(f"- **Throughput**: {len(accounts) / execution_time:.1f} accounts/second")
|
1464
|
-
lines.append(f"- **Success Rate**: {len([a for a in accounts if a['success']])}/{len(accounts)} accounts")
|
1465
|
-
lines.append(f"- **Performance Target**: ✅ {execution_time:.1f}s ≤ 60s target")
|
1466
|
-
lines.append("")
|
1467
|
-
lines.append("---")
|
1468
|
-
lines.append("")
|
1469
|
-
lines.append("*Generated by CloudOps Runbooks FinOps Platform - SRE Enhanced Multi-Account Analysis*")
|
1470
|
-
lines.append("")
|
1471
|
-
lines.append("**Note**: Tax services excluded from analysis per WIP.md analytical requirements")
|
1472
|
-
|
1473
|
-
# Write markdown file
|
1474
|
-
with open(markdown_path, "w") as f:
|
1475
|
-
f.write("\n".join(lines))
|
1476
|
-
|
1477
|
-
self._log_user_friendly(f"Markdown export saved to: {markdown_path}", "bright_green")
|
1478
|
-
self._log_technical_detail(f"Export format: GitHub-compatible markdown with {len(accounts)} accounts")
|
1479
|
-
|
1480
|
-
except Exception as e:
|
1481
|
-
print_warning(f"Markdown export failed: {str(e)[:50]}")
|
1482
|
-
self._log_technical_detail(f"Export error details: {str(e)}")
|
1483
|
-
|
1484
|
-
def _get_stopped_instances_count(self, account: Dict[str, Any]) -> int:
|
1485
|
-
"""Get count of stopped EC2 instances for optimization opportunities."""
|
1486
|
-
# TODO: Implement real EC2 API calls to query stopped instances
|
1487
|
-
# This should use boto3 EC2 client to query actual stopped instances:
|
1488
|
-
# ec2_client.describe_instances(Filters=[{'Name': 'instance-state-name', 'Values': ['stopped']}])
|
1489
|
-
# For now, return 0 until real implementation is added
|
1490
|
-
return 0
|
1491
|
-
|
1492
|
-
def _get_unused_volumes_count(self, account: Dict[str, Any]) -> int:
|
1493
|
-
"""Get count of unused EBS volumes for cost optimization."""
|
1494
|
-
# TODO: Implement real EBS API calls to query unattached volumes
|
1495
|
-
# This should use boto3 EC2 client to query volumes with state 'available':
|
1496
|
-
# ec2_client.describe_volumes(Filters=[{'Name': 'status', 'Values': ['available']}])
|
1497
|
-
# For now, return 0 until real implementation is added
|
1498
|
-
return 0
|
1499
|
-
|
1500
|
-
def _get_unused_eips_count(self, account: Dict[str, Any]) -> int:
|
1501
|
-
"""Get count of unused Elastic IP addresses."""
|
1502
|
-
# TODO: Implement real EC2 API calls to query unassociated Elastic IPs
|
1503
|
-
# This should use boto3 EC2 client to query addresses not associated with instances:
|
1504
|
-
# ec2_client.describe_addresses() and check for addresses without AssociationId
|
1505
|
-
# For now, return 0 until real implementation is added
|
1506
|
-
return 0
|
1507
|
-
|
1508
|
-
def _get_untagged_resources_count(self, account: Dict[str, Any]) -> int:
|
1509
|
-
"""Get count of untagged resources for governance compliance."""
|
1510
|
-
# TODO: Implement real resource tagging analysis across multiple AWS services
|
1511
|
-
# This should query EC2, S3, RDS, Lambda, etc. for resources without required tags
|
1512
|
-
# Use Resource Groups Tagging API or service-specific describe calls with tag filters
|
1513
|
-
# For now, return 0 until real implementation is added
|
1514
|
-
return 0
|
1515
|
-
|
1516
|
-
|
1517
|
-
def create_multi_dashboard(console: Optional[Console] = None) -> MultiAccountDashboard:
|
1518
|
-
"""Factory function to create multi-account dashboard."""
|
1519
|
-
return MultiAccountDashboard(console=console)
|