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.
Files changed (247) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
  8. runbooks/cfat/assessment/compliance.py +8 -8
  9. runbooks/cfat/assessment/runner.py +1 -0
  10. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  11. runbooks/cfat/models.py +6 -2
  12. runbooks/cfat/tests/__init__.py +6 -1
  13. runbooks/cli/__init__.py +13 -0
  14. runbooks/cli/commands/cfat.py +274 -0
  15. runbooks/cli/commands/finops.py +1164 -0
  16. runbooks/cli/commands/inventory.py +379 -0
  17. runbooks/cli/commands/operate.py +239 -0
  18. runbooks/cli/commands/security.py +248 -0
  19. runbooks/cli/commands/validation.py +825 -0
  20. runbooks/cli/commands/vpc.py +310 -0
  21. runbooks/cli/registry.py +107 -0
  22. runbooks/cloudops/__init__.py +23 -30
  23. runbooks/cloudops/base.py +96 -107
  24. runbooks/cloudops/cost_optimizer.py +549 -547
  25. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  26. runbooks/cloudops/interfaces.py +226 -227
  27. runbooks/cloudops/lifecycle_manager.py +5 -4
  28. runbooks/cloudops/mcp_cost_validation.py +252 -235
  29. runbooks/cloudops/models.py +78 -53
  30. runbooks/cloudops/monitoring_automation.py +5 -4
  31. runbooks/cloudops/notebook_framework.py +179 -215
  32. runbooks/cloudops/security_enforcer.py +125 -159
  33. runbooks/common/accuracy_validator.py +11 -0
  34. runbooks/common/aws_pricing.py +349 -326
  35. runbooks/common/aws_pricing_api.py +211 -212
  36. runbooks/common/aws_profile_manager.py +341 -0
  37. runbooks/common/aws_utils.py +75 -80
  38. runbooks/common/business_logic.py +127 -105
  39. runbooks/common/cli_decorators.py +36 -60
  40. runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
  41. runbooks/common/cross_account_manager.py +198 -205
  42. runbooks/common/date_utils.py +27 -39
  43. runbooks/common/decorators.py +235 -0
  44. runbooks/common/dry_run_examples.py +173 -208
  45. runbooks/common/dry_run_framework.py +157 -155
  46. runbooks/common/enhanced_exception_handler.py +15 -4
  47. runbooks/common/enhanced_logging_example.py +50 -64
  48. runbooks/common/enhanced_logging_integration_example.py +65 -37
  49. runbooks/common/env_utils.py +16 -16
  50. runbooks/common/error_handling.py +40 -38
  51. runbooks/common/lazy_loader.py +41 -23
  52. runbooks/common/logging_integration_helper.py +79 -86
  53. runbooks/common/mcp_cost_explorer_integration.py +478 -495
  54. runbooks/common/mcp_integration.py +63 -74
  55. runbooks/common/memory_optimization.py +140 -118
  56. runbooks/common/module_cli_base.py +37 -58
  57. runbooks/common/organizations_client.py +176 -194
  58. runbooks/common/patterns.py +204 -0
  59. runbooks/common/performance_monitoring.py +67 -71
  60. runbooks/common/performance_optimization_engine.py +283 -274
  61. runbooks/common/profile_utils.py +248 -39
  62. runbooks/common/rich_utils.py +643 -92
  63. runbooks/common/sre_performance_suite.py +177 -186
  64. runbooks/enterprise/__init__.py +1 -1
  65. runbooks/enterprise/logging.py +144 -106
  66. runbooks/enterprise/security.py +187 -204
  67. runbooks/enterprise/validation.py +43 -56
  68. runbooks/finops/__init__.py +29 -33
  69. runbooks/finops/account_resolver.py +1 -1
  70. runbooks/finops/advanced_optimization_engine.py +980 -0
  71. runbooks/finops/automation_core.py +268 -231
  72. runbooks/finops/business_case_config.py +184 -179
  73. runbooks/finops/cli.py +660 -139
  74. runbooks/finops/commvault_ec2_analysis.py +157 -164
  75. runbooks/finops/compute_cost_optimizer.py +336 -320
  76. runbooks/finops/config.py +20 -20
  77. runbooks/finops/cost_optimizer.py +488 -622
  78. runbooks/finops/cost_processor.py +332 -214
  79. runbooks/finops/dashboard_runner.py +1006 -172
  80. runbooks/finops/ebs_cost_optimizer.py +991 -657
  81. runbooks/finops/elastic_ip_optimizer.py +317 -257
  82. runbooks/finops/enhanced_mcp_integration.py +340 -0
  83. runbooks/finops/enhanced_progress.py +40 -37
  84. runbooks/finops/enhanced_trend_visualization.py +3 -2
  85. runbooks/finops/enterprise_wrappers.py +230 -292
  86. runbooks/finops/executive_export.py +203 -160
  87. runbooks/finops/helpers.py +130 -288
  88. runbooks/finops/iam_guidance.py +1 -1
  89. runbooks/finops/infrastructure/__init__.py +80 -0
  90. runbooks/finops/infrastructure/commands.py +506 -0
  91. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  92. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  93. runbooks/finops/markdown_exporter.py +338 -175
  94. runbooks/finops/mcp_validator.py +1952 -0
  95. runbooks/finops/nat_gateway_optimizer.py +1513 -482
  96. runbooks/finops/network_cost_optimizer.py +657 -587
  97. runbooks/finops/notebook_utils.py +226 -188
  98. runbooks/finops/optimization_engine.py +1136 -0
  99. runbooks/finops/optimizer.py +25 -29
  100. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  101. runbooks/finops/reservation_optimizer.py +427 -363
  102. runbooks/finops/scenario_cli_integration.py +77 -78
  103. runbooks/finops/scenarios.py +1278 -439
  104. runbooks/finops/schemas.py +218 -182
  105. runbooks/finops/snapshot_manager.py +2289 -0
  106. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  107. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  108. runbooks/finops/tests/test_single_account_features.py +17 -17
  109. runbooks/finops/tests/validate_test_suite.py +1 -1
  110. runbooks/finops/types.py +3 -3
  111. runbooks/finops/validation_framework.py +263 -269
  112. runbooks/finops/vpc_cleanup_exporter.py +191 -146
  113. runbooks/finops/vpc_cleanup_optimizer.py +593 -575
  114. runbooks/finops/workspaces_analyzer.py +171 -182
  115. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  116. runbooks/integration/__init__.py +89 -0
  117. runbooks/integration/mcp_integration.py +1920 -0
  118. runbooks/inventory/CLAUDE.md +816 -0
  119. runbooks/inventory/README.md +3 -3
  120. runbooks/inventory/Tests/common_test_data.py +30 -30
  121. runbooks/inventory/__init__.py +2 -2
  122. runbooks/inventory/cloud_foundations_integration.py +144 -149
  123. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  124. runbooks/inventory/collectors/aws_networking.py +111 -101
  125. runbooks/inventory/collectors/base.py +4 -0
  126. runbooks/inventory/core/collector.py +495 -313
  127. runbooks/inventory/discovery.md +2 -2
  128. runbooks/inventory/drift_detection_cli.py +69 -96
  129. runbooks/inventory/find_ec2_security_groups.py +1 -1
  130. runbooks/inventory/inventory_mcp_cli.py +48 -46
  131. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  132. runbooks/inventory/mcp_inventory_validator.py +549 -465
  133. runbooks/inventory/mcp_vpc_validator.py +359 -442
  134. runbooks/inventory/organizations_discovery.py +56 -52
  135. runbooks/inventory/rich_inventory_display.py +33 -32
  136. runbooks/inventory/unified_validation_engine.py +278 -251
  137. runbooks/inventory/vpc_analyzer.py +733 -696
  138. runbooks/inventory/vpc_architecture_validator.py +293 -348
  139. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  140. runbooks/inventory/vpc_flow_analyzer.py +3 -3
  141. runbooks/main.py +152 -9147
  142. runbooks/main_final.py +91 -60
  143. runbooks/main_minimal.py +22 -10
  144. runbooks/main_optimized.py +131 -100
  145. runbooks/main_ultra_minimal.py +7 -2
  146. runbooks/mcp/__init__.py +36 -0
  147. runbooks/mcp/integration.py +679 -0
  148. runbooks/metrics/dora_metrics_engine.py +2 -2
  149. runbooks/monitoring/performance_monitor.py +9 -4
  150. runbooks/operate/dynamodb_operations.py +3 -1
  151. runbooks/operate/ec2_operations.py +145 -137
  152. runbooks/operate/iam_operations.py +146 -152
  153. runbooks/operate/mcp_integration.py +1 -1
  154. runbooks/operate/networking_cost_heatmap.py +33 -10
  155. runbooks/operate/privatelink_operations.py +1 -1
  156. runbooks/operate/rds_operations.py +223 -254
  157. runbooks/operate/s3_operations.py +107 -118
  158. runbooks/operate/vpc_endpoints.py +1 -1
  159. runbooks/operate/vpc_operations.py +648 -618
  160. runbooks/remediation/base.py +1 -1
  161. runbooks/remediation/commons.py +10 -7
  162. runbooks/remediation/commvault_ec2_analysis.py +71 -67
  163. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  164. runbooks/remediation/multi_account.py +24 -21
  165. runbooks/remediation/rds_snapshot_list.py +91 -65
  166. runbooks/remediation/remediation_cli.py +92 -146
  167. runbooks/remediation/universal_account_discovery.py +83 -79
  168. runbooks/remediation/workspaces_list.py +49 -44
  169. runbooks/security/__init__.py +19 -0
  170. runbooks/security/assessment_runner.py +1150 -0
  171. runbooks/security/baseline_checker.py +812 -0
  172. runbooks/security/cloudops_automation_security_validator.py +509 -535
  173. runbooks/security/compliance_automation_engine.py +17 -17
  174. runbooks/security/config/__init__.py +2 -2
  175. runbooks/security/config/compliance_config.py +50 -50
  176. runbooks/security/config_template_generator.py +63 -76
  177. runbooks/security/enterprise_security_framework.py +1 -1
  178. runbooks/security/executive_security_dashboard.py +519 -508
  179. runbooks/security/integration_test_enterprise_security.py +5 -3
  180. runbooks/security/multi_account_security_controls.py +959 -1210
  181. runbooks/security/real_time_security_monitor.py +422 -444
  182. runbooks/security/run_script.py +1 -1
  183. runbooks/security/security_baseline_tester.py +1 -1
  184. runbooks/security/security_cli.py +143 -112
  185. runbooks/security/test_2way_validation.py +439 -0
  186. runbooks/security/two_way_validation_framework.py +852 -0
  187. runbooks/sre/mcp_reliability_engine.py +6 -6
  188. runbooks/sre/production_monitoring_framework.py +167 -177
  189. runbooks/tdd/__init__.py +15 -0
  190. runbooks/tdd/cli.py +1071 -0
  191. runbooks/utils/__init__.py +14 -17
  192. runbooks/utils/logger.py +7 -2
  193. runbooks/utils/version_validator.py +51 -48
  194. runbooks/validation/__init__.py +6 -6
  195. runbooks/validation/cli.py +9 -3
  196. runbooks/validation/comprehensive_2way_validator.py +754 -708
  197. runbooks/validation/mcp_validator.py +906 -228
  198. runbooks/validation/terraform_citations_validator.py +104 -115
  199. runbooks/validation/terraform_drift_detector.py +447 -451
  200. runbooks/vpc/README.md +617 -0
  201. runbooks/vpc/__init__.py +8 -1
  202. runbooks/vpc/analyzer.py +577 -0
  203. runbooks/vpc/cleanup_wrapper.py +476 -413
  204. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  205. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  206. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  207. runbooks/vpc/config.py +92 -97
  208. runbooks/vpc/cost_engine.py +411 -148
  209. runbooks/vpc/cost_explorer_integration.py +553 -0
  210. runbooks/vpc/cross_account_session.py +101 -106
  211. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  212. runbooks/vpc/eni_gate_validator.py +961 -0
  213. runbooks/vpc/heatmap_engine.py +190 -162
  214. runbooks/vpc/mcp_no_eni_validator.py +681 -640
  215. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  216. runbooks/vpc/networking_wrapper.py +15 -8
  217. runbooks/vpc/pdca_remediation_planner.py +528 -0
  218. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  219. runbooks/vpc/runbooks_adapter.py +1167 -241
  220. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  221. runbooks/vpc/test_data_loader.py +358 -0
  222. runbooks/vpc/tests/conftest.py +314 -4
  223. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  224. runbooks/vpc/tests/test_cost_engine.py +0 -2
  225. runbooks/vpc/topology_generator.py +326 -0
  226. runbooks/vpc/unified_scenarios.py +1302 -1129
  227. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  228. runbooks-1.1.5.dist-info/METADATA +328 -0
  229. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
  230. runbooks/finops/README.md +0 -414
  231. runbooks/finops/accuracy_cross_validator.py +0 -647
  232. runbooks/finops/business_cases.py +0 -950
  233. runbooks/finops/dashboard_router.py +0 -922
  234. runbooks/finops/ebs_optimizer.py +0 -956
  235. runbooks/finops/embedded_mcp_validator.py +0 -1629
  236. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  237. runbooks/finops/finops_dashboard.py +0 -584
  238. runbooks/finops/finops_scenarios.py +0 -1218
  239. runbooks/finops/legacy_migration.py +0 -730
  240. runbooks/finops/multi_dashboard.py +0 -1519
  241. runbooks/finops/single_dashboard.py +0 -1113
  242. runbooks/finops/unlimited_scenarios.py +0 -393
  243. runbooks-1.1.3.dist-info/METADATA +0 -799
  244. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  245. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  246. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  247. {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)