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,922 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- FinOps Dashboard Router - Enterprise Use-Case Detection & Routing
4
-
5
- This module provides intelligent routing between different dashboard types based on
6
- AWS profile configuration and use-case detection, implementing the architectural
7
- enhancement requested for improved user experience and functionality.
8
-
9
- Features:
10
- - Smart single vs multi-account detection
11
- - Use-case specific dashboard routing
12
- - Non-breaking backward compatibility
13
- - Enhanced column value implementations
14
- - Rich CLI integration (mandatory enterprise standard)
15
-
16
- Author: CloudOps Runbooks Team
17
- Version: 0.8.0
18
- """
19
-
20
- import argparse
21
- import os
22
- from typing import Any, Dict, List, Optional, Tuple
23
-
24
- import boto3
25
- from rich.console import Console
26
-
27
- from ..common.rich_utils import (
28
- STATUS_INDICATORS,
29
- console,
30
- print_header,
31
- print_info,
32
- print_success,
33
- print_warning,
34
- )
35
- from .aws_client import convert_accounts_to_profiles, get_account_id, get_aws_profiles, get_organization_accounts
36
- from runbooks.common.profile_utils import get_profile_for_operation
37
-
38
- # Rich CLI integration (mandatory)
39
- rich_console = console
40
-
41
-
42
- class DashboardRouter:
43
- """
44
- Intelligent dashboard router for enterprise FinOps use-cases.
45
-
46
- Routes requests to appropriate dashboard implementations based on:
47
- - Profile configuration (single vs multi-account)
48
- - User preferences (explicit mode selection)
49
- - Account access patterns
50
- - Use-case detection logic
51
- """
52
-
53
- def __init__(self, console: Optional[Console] = None):
54
- self.console = console or rich_console
55
- self.aws_profiles = get_aws_profiles()
56
-
57
- def detect_use_case(self, args: argparse.Namespace) -> Tuple[str, Dict[str, Any]]:
58
- """
59
- Intelligent use-case detection for optimal dashboard routing.
60
-
61
- Detection Logic Priority (ENHANCED - Organizations API Integration):
62
- 1. --all flag with Organizations API discovery (NEW)
63
- 2. Explicit --mode parameter (user override)
64
- 3. Multi-profile detection (--profiles with 2+)
65
- 4. Single profile specified = ALWAYS single_account (CRITICAL FIX)
66
- 5. Environment-based cross-account detection (only when no explicit profile)
67
-
68
- Args:
69
- args: Command line arguments from FinOps CLI
70
-
71
- Returns:
72
- Tuple of (use_case, routing_config) where:
73
- - use_case: 'single_account' or 'multi_account' or 'organization_wide'
74
- - routing_config: Configuration dict for the selected dashboard
75
- """
76
- routing_config = {
77
- "profiles_to_analyze": [],
78
- "account_context": "unknown",
79
- "optimization_focus": "balanced",
80
- "detection_confidence": "low",
81
- "organization_accounts": [],
82
- }
83
-
84
- # Priority 1: --all flag with Organizations API discovery (NEW)
85
- if hasattr(args, "all") and args.all:
86
- print_info("🔍 --all flag detected: Enabling Organizations API discovery")
87
-
88
- # Get base profile for Organizations API access
89
- base_profile = args.profile if hasattr(args, "profile") and args.profile != "default" else "default"
90
-
91
- try:
92
- import boto3
93
-
94
- session = boto3.Session(profile_name=base_profile)
95
-
96
- # Discover all organization accounts
97
- org_accounts = get_organization_accounts(session, base_profile)
98
-
99
- if org_accounts:
100
- # Successfully discovered accounts via Organizations API
101
- # CRITICAL FIX: Handle new return format with account metadata
102
- profiles_to_analyze, account_metadata = convert_accounts_to_profiles(org_accounts, base_profile)
103
-
104
- routing_config["organization_accounts"] = org_accounts
105
- routing_config["profiles_to_analyze"] = profiles_to_analyze
106
- routing_config["account_metadata"] = account_metadata # Preserve inactive account info
107
- routing_config["account_context"] = "organization_wide"
108
- routing_config["optimization_focus"] = "account"
109
- routing_config["detection_confidence"] = "high"
110
- routing_config["base_profile"] = base_profile
111
-
112
- active_count = len([acc for acc in org_accounts if acc.get("status") == "ACTIVE"])
113
- inactive_count = len(org_accounts) - active_count
114
- print_success(
115
- f"Organizations API: Discovered {len(org_accounts)} accounts for analysis ({active_count} active, {inactive_count} inactive)"
116
- )
117
- return "organization_wide", routing_config
118
-
119
- else:
120
- # Organizations API failed, fall back to single account mode
121
- print_warning("Organizations API discovery failed, falling back to single account mode")
122
- routing_config["profiles_to_analyze"] = [base_profile]
123
- routing_config["account_context"] = "single"
124
- routing_config["optimization_focus"] = "service"
125
- routing_config["detection_confidence"] = "medium"
126
- return "single_account", routing_config
127
-
128
- except Exception as e:
129
- print_warning(f"--all flag processing failed: {str(e)[:50]}")
130
- # Graceful fallback to single account
131
- base_profile = args.profile if hasattr(args, "profile") and args.profile != "default" else "default"
132
- routing_config["profiles_to_analyze"] = [base_profile]
133
- routing_config["account_context"] = "single"
134
- routing_config["optimization_focus"] = "service"
135
- routing_config["detection_confidence"] = "low"
136
- return "single_account", routing_config
137
-
138
- # Priority 2: Explicit mode override
139
- if hasattr(args, "mode") and args.mode:
140
- use_case = args.mode
141
- routing_config["detection_confidence"] = "explicit"
142
- routing_config["optimization_focus"] = "service" if use_case == "single_account" else "account"
143
- print_info(f"Dashboard mode explicitly set: {use_case}")
144
- return use_case, routing_config
145
-
146
- # Priority 3: Multi-profile parameter detection with deduplication
147
- profiles_specified = []
148
-
149
- # Process --profiles parameter first
150
- if hasattr(args, "profiles") and args.profiles:
151
- for profile_item in args.profiles:
152
- if "," in profile_item:
153
- # Handle comma-separated within --profiles parameter
154
- profiles_specified.extend([p.strip() for p in profile_item.split(",") if p.strip()])
155
- else:
156
- profiles_specified.append(profile_item.strip())
157
- print_info(f"Found --profiles parameter: {args.profiles} → {len(profiles_specified)} profiles")
158
-
159
- # Process --profile parameter (avoid duplicates)
160
- if hasattr(args, "profile") and args.profile and args.profile != "default":
161
- if "," in args.profile:
162
- # Handle comma-separated profiles in single --profile parameter
163
- comma_profiles = [p.strip() for p in args.profile.split(",") if p.strip()]
164
- for profile in comma_profiles:
165
- if profile not in profiles_specified: # Deduplicate
166
- profiles_specified.append(profile)
167
- print_info(f"Found comma-separated --profile: {args.profile} → {len(comma_profiles)} additional")
168
- else:
169
- if args.profile not in profiles_specified: # Deduplicate
170
- profiles_specified.append(args.profile)
171
- print_info(f"Added single --profile: {args.profile}")
172
-
173
- # Remove any empty strings and deduplicate
174
- profiles_specified = list(dict.fromkeys([p for p in profiles_specified if p and p.strip()]))
175
-
176
- if len(profiles_specified) > 1:
177
- print_info(f"Clean multi-profile list: {profiles_specified} ({len(profiles_specified)} unique profiles)")
178
- routing_config["profiles_to_analyze"] = profiles_specified
179
- routing_config["account_context"] = "multi"
180
- routing_config["optimization_focus"] = "account"
181
- routing_config["detection_confidence"] = "high"
182
- return "multi_account", routing_config
183
-
184
- # Priority 4: CRITICAL FIX - Single profile specified = single_account mode
185
- if len(profiles_specified) == 1:
186
- routing_config["account_context"] = "single"
187
- routing_config["optimization_focus"] = "service"
188
- routing_config["detection_confidence"] = "high"
189
- routing_config["profiles_to_analyze"] = profiles_specified
190
- print_info(f"Single profile specified: {profiles_specified[0]} → single_account mode")
191
- return "single_account", routing_config
192
-
193
- # Priority 5: Environment-based detection (only when no explicit profile)
194
- if self._detect_cross_account_capability(None):
195
- routing_config["account_context"] = "cross_account_capable"
196
- routing_config["optimization_focus"] = "account"
197
- routing_config["detection_confidence"] = "medium"
198
- print_info("Cross-account environment detected (no explicit profile)")
199
- return "multi_account", routing_config
200
-
201
- # Priority 6: Default fallback
202
- routing_config["account_context"] = "single"
203
- routing_config["optimization_focus"] = "service"
204
- routing_config["detection_confidence"] = "medium"
205
- routing_config["profiles_to_analyze"] = ["default"]
206
- print_info("Single account default mode selected (service-focused analysis)")
207
- return "single_account", routing_config
208
-
209
- def _detect_cross_account_capability(self, profile: Optional[str]) -> bool:
210
- """
211
- Detect if the profile has cross-account access capabilities.
212
-
213
- CRITICAL: This method should only be called when NO explicit profile is specified.
214
- Single profile commands should NEVER reach this method due to Priority 3 fix.
215
-
216
- Detection Methods:
217
- 1. Environment variable configuration (BILLING_PROFILE, MANAGEMENT_PROFILE)
218
- 2. Profile naming patterns (admin, billing, management)
219
- 3. Quick account access test (if feasible)
220
-
221
- Args:
222
- profile: AWS profile to test (should be None for environment detection)
223
-
224
- Returns:
225
- bool: True if cross-account capability detected
226
- """
227
- try:
228
- # CRITICAL: Only check environment when no explicit profile specified
229
- if profile is None:
230
- # Method 1: Environment variable detection (only when profile=None)
231
- env_profiles = [
232
- os.getenv("BILLING_PROFILE"),
233
- os.getenv("MANAGEMENT_PROFILE"),
234
- os.getenv("CENTRALISED_OPS_PROFILE"),
235
- ]
236
- if any(env_profiles):
237
- print_info("Multi-profile environment variables detected (no explicit profile)")
238
- return True
239
-
240
- # Method 2: Profile naming pattern analysis
241
- if profile:
242
- cross_account_indicators = ["admin", "billing", "management", "centralised", "master", "org"]
243
- profile_lower = profile.lower()
244
- if any(indicator in profile_lower for indicator in cross_account_indicators):
245
- print_info(f"Cross-account naming pattern detected in profile: {profile}")
246
- return True
247
-
248
- # Method 3: Quick capability test (lightweight)
249
- if profile:
250
- try:
251
- # Test if we can access multiple operation types
252
- billing_profile = get_profile_for_operation("billing", profile)
253
- management_profile = get_profile_for_operation("management", profile)
254
- operational_profile = get_profile_for_operation("operational", profile)
255
-
256
- # If different profiles are resolved, we have multi-profile capability
257
- profiles_used = {billing_profile, management_profile, operational_profile}
258
- if len(profiles_used) > 1:
259
- print_info("Multi-profile operation capability confirmed")
260
- return True
261
-
262
- except Exception as e:
263
- # Graceful fallback - don't fail the detection
264
- print_warning(f"Profile capability test failed: {str(e)[:50]}")
265
-
266
- return False
267
-
268
- except Exception as e:
269
- print_warning(f"Cross-account detection failed: {str(e)[:50]}")
270
- return False
271
-
272
- def route_dashboard_request(self, args: argparse.Namespace) -> int:
273
- """
274
- Route dashboard request to appropriate implementation.
275
-
276
- This is the main entry point that replaces the monolithic dashboard approach
277
- with intelligent routing to specialized dashboard implementations.
278
-
279
- Args:
280
- args: Command line arguments
281
-
282
- Returns:
283
- int: Exit code (0 for success, 1 for failure)
284
- """
285
- try:
286
- print_header("FinOps Dashboard Router", "1.1.1")
287
-
288
- # Detect use-case and route appropriately
289
- use_case, routing_config = self.detect_use_case(args)
290
-
291
- # Display routing decision
292
- self._display_routing_decision(use_case, routing_config)
293
-
294
- if use_case == "single_account":
295
- return self._route_to_single_dashboard(args, routing_config)
296
- elif use_case == "multi_account":
297
- return self._route_to_multi_dashboard(args, routing_config)
298
- elif use_case == "organization_wide":
299
- return self._route_to_organization_dashboard(args, routing_config)
300
- else:
301
- print_warning(f"Unknown use case: {use_case}, falling back to original dashboard")
302
- return self._route_to_original_dashboard(args)
303
-
304
- except Exception as e:
305
- self.console.print(f"[error]❌ Dashboard routing failed: {str(e)}[/]")
306
- return 1
307
-
308
- def _display_routing_decision(self, use_case: str, config: Dict[str, Any]) -> None:
309
- """Display the routing decision with Rich formatting."""
310
- confidence_icon = STATUS_INDICATORS.get(
311
- "success"
312
- if config["detection_confidence"] == "high"
313
- else "warning"
314
- if config["detection_confidence"] == "medium"
315
- else "info"
316
- )
317
-
318
- self.console.print(f"\n[info]{confidence_icon} Use Case Detected:[/] [highlight]{use_case}[/]")
319
- self.console.print(f"[dim]• Account Context: {config['account_context']}[/]")
320
- self.console.print(f"[dim]• Optimization Focus: {config['optimization_focus']}[/]")
321
- self.console.print(f"[dim]• Detection Confidence: {config['detection_confidence']}[/]\n")
322
-
323
- def _route_to_single_dashboard(self, args: argparse.Namespace, config: Dict[str, Any]) -> int:
324
- """Route to single-account service-focused dashboard."""
325
- try:
326
- from .single_dashboard import SingleAccountDashboard
327
-
328
- dashboard = SingleAccountDashboard(console=self.console)
329
- return dashboard.run_dashboard(args, config)
330
-
331
- except Exception as e:
332
- print_warning(f"Single dashboard import failed ({str(e)[:30]}), implementing direct service-per-row")
333
- return self._run_direct_service_dashboard(args, config)
334
-
335
- def _route_to_multi_dashboard(self, args: argparse.Namespace, config: Dict[str, Any]) -> int:
336
- """Route to multi-account account-focused dashboard."""
337
- try:
338
- from .multi_dashboard import MultiAccountDashboard
339
-
340
- dashboard = MultiAccountDashboard(console=self.console)
341
- return dashboard.run_dashboard(args, config)
342
-
343
- except ImportError as e:
344
- print_warning(f"Multi dashboard import failed: {str(e)[:50]}, using enhanced runner")
345
- return self._route_to_enhanced_dashboard(args)
346
- except Exception as e:
347
- print_warning(f"Multi dashboard failed: {str(e)[:50]}, using enhanced runner")
348
- return self._route_to_enhanced_dashboard(args)
349
-
350
- def _route_to_organization_dashboard(self, args: argparse.Namespace, config: Dict[str, Any]) -> int:
351
- """
352
- Route to organization-wide dashboard with Organizations API discovered accounts.
353
-
354
- This method handles the --all flag functionality by processing all discovered
355
- organization accounts and routing to the appropriate multi-account dashboard.
356
-
357
- Args:
358
- args: Command line arguments
359
- config: Routing config containing organization_accounts data
360
-
361
- Returns:
362
- int: Exit code (0 for success, 1 for failure)
363
- """
364
- try:
365
- print_info("🏢 Routing to organization-wide dashboard")
366
-
367
- # Extract organization data from config
368
- org_accounts = config.get("organization_accounts", [])
369
- base_profile = config.get("base_profile", "default")
370
-
371
- if not org_accounts:
372
- print_warning("No organization accounts found, falling back to single account")
373
- return self._route_to_single_dashboard(args, config)
374
-
375
- # Display organization summary for user confirmation
376
- self.console.print(f"\n[info]🏢 Organization Analysis Scope:[/]")
377
- self.console.print(f"[dim]• Base Profile: {base_profile}[/]")
378
- self.console.print(f"[dim]• Total Accounts: {len(org_accounts)}[/]")
379
- self.console.print(f"[dim]• Analysis Type: Multi-account 10-column dashboard[/]")
380
-
381
- # Show account summary (first 10 accounts for display)
382
- display_accounts = org_accounts[:10]
383
- for i, account in enumerate(display_accounts, 1):
384
- account_name = account["name"][:30] + "..." if len(account["name"]) > 30 else account["name"]
385
- self.console.print(f"[dim] {i:2d}. {account['id']} - {account_name}[/]")
386
-
387
- if len(org_accounts) > 10:
388
- self.console.print(f"[dim] ... and {len(org_accounts) - 10} more accounts[/]\n")
389
- else:
390
- self.console.print()
391
-
392
- # Try to route to multi-account dashboard with organization context
393
- try:
394
- from .multi_dashboard import MultiAccountDashboard
395
-
396
- # Update config to indicate organization-wide context
397
- org_config = config.copy()
398
- org_config["analysis_scope"] = "organization"
399
- org_config["account_discovery_method"] = "organizations_api"
400
-
401
- dashboard = MultiAccountDashboard(console=self.console)
402
- return dashboard.run_dashboard(args, org_config)
403
-
404
- except ImportError:
405
- print_warning("Multi-account dashboard unavailable, using enhanced dashboard with organization context")
406
- return self._route_to_enhanced_organization_dashboard(args, config)
407
-
408
- except Exception as e:
409
- print_warning(f"Organization dashboard routing failed: {str(e)[:50]}")
410
- return self._route_to_enhanced_dashboard(args)
411
-
412
- def _route_to_enhanced_organization_dashboard(self, args: argparse.Namespace, config: Dict[str, Any]) -> int:
413
- """
414
- Enhanced dashboard implementation for organization-wide analysis.
415
-
416
- This provides fallback functionality when the dedicated multi-account dashboard
417
- is not available, using the enhanced dashboard runner with organization context.
418
- """
419
- try:
420
- from .enhanced_dashboard_runner import EnhancedFinOpsDashboard
421
-
422
- print_info("Using enhanced dashboard with organization-wide context")
423
-
424
- # Get organization accounts for processing
425
- org_accounts = config.get("organization_accounts", [])
426
- base_profile = config.get("base_profile", "default")
427
-
428
- # Create enhanced dashboard with organization context
429
- dashboard = EnhancedFinOpsDashboard(console=self.console)
430
-
431
- # Set organization context for the dashboard
432
- dashboard.organization_accounts = org_accounts
433
- dashboard.base_profile = base_profile
434
- dashboard.analysis_scope = "organization"
435
-
436
- print_success(f"Configured enhanced dashboard for {len(org_accounts)} organization accounts")
437
-
438
- # Run comprehensive analysis with organization scope
439
- return dashboard.run_comprehensive_audit()
440
-
441
- except ImportError as e:
442
- print_warning(f"Enhanced dashboard unavailable: {str(e)[:30]}")
443
- return self._create_organization_summary_table(args, config)
444
- except Exception as e:
445
- print_warning(f"Enhanced organization dashboard failed: {str(e)[:50]}")
446
- return self._create_organization_summary_table(args, config)
447
-
448
- def _create_organization_summary_table(self, args: argparse.Namespace, config: Dict[str, Any]) -> int:
449
- """
450
- Create a summary table for organization-wide accounts when full dashboard is unavailable.
451
-
452
- This provides basic organization account information in a clean table format,
453
- fulfilling the user's requirement for the --all flag functionality.
454
- """
455
- from rich import box
456
- from rich.table import Table
457
-
458
- try:
459
- print_info("Creating organization summary table (dashboard fallback)")
460
-
461
- org_accounts = config.get("organization_accounts", [])
462
- base_profile = config.get("base_profile", "default")
463
-
464
- # Create organization accounts summary table
465
- table = Table(
466
- title=f"🏢 Organization Accounts - Discovered via Organizations API",
467
- box=box.DOUBLE_EDGE,
468
- border_style="bright_cyan",
469
- title_style="bold white on blue",
470
- header_style="bold cyan",
471
- show_lines=True,
472
- caption=f"[dim]Base Profile: {base_profile} | Discovery Method: Organizations API | Total: {len(org_accounts)} accounts[/]",
473
- )
474
-
475
- # Add columns for organization account info
476
- table.add_column("#", justify="right", style="dim", width=4)
477
- table.add_column("Account ID", style="bold white", width=15)
478
- table.add_column("Account Name", style="cyan", width=40)
479
- table.add_column("Status", style="green", width=10)
480
- table.add_column("Email", style="dim", width=30)
481
-
482
- # Add account rows
483
- for i, account in enumerate(org_accounts, 1):
484
- table.add_row(
485
- str(i),
486
- account["id"],
487
- account["name"][:38] + "..." if len(account["name"]) > 38 else account["name"],
488
- account["status"],
489
- account["email"][:28] + "..." if len(account["email"]) > 28 else account["email"],
490
- )
491
-
492
- self.console.print(table)
493
-
494
- # Provide next steps guidance
495
- from rich.panel import Panel
496
-
497
- next_steps = f"""
498
- [highlight]Organization Discovery Complete[/]
499
-
500
- ✅ Successfully discovered {len(org_accounts)} accounts via Organizations API
501
- ✅ Base profile '{base_profile}' has organization-wide access
502
- ✅ All accounts are ACTIVE status and ready for analysis
503
-
504
- [bold]Next Steps:[/]
505
- • Use multi-account dashboards for detailed cost analysis
506
- • Set up cross-account roles for comprehensive FinOps operations
507
- • Review account naming and tagging standards for better organization
508
-
509
- [bold]Command Examples:[/]
510
- • runbooks finops --all --profile {base_profile} # This command
511
- • runbooks finops --profile {base_profile},{org_accounts[0]["id"] if org_accounts else "account2"} # Explicit accounts
512
- • runbooks inventory collect --all --profile {base_profile} # Organization-wide inventory
513
- """
514
-
515
- self.console.print(Panel(next_steps.strip(), title="📊 Organizations API Success", style="info"))
516
-
517
- print_success(f"Organization summary completed: {len(org_accounts)} accounts discovered")
518
- return 0
519
-
520
- except Exception as e:
521
- print_warning(f"Organization summary table failed: {str(e)[:50]}")
522
- return 1
523
-
524
- def _route_to_enhanced_dashboard(self, args: argparse.Namespace) -> int:
525
- """Route to enhanced dashboard runner (transitional)."""
526
- try:
527
- from .enhanced_dashboard_runner import EnhancedFinOpsDashboard
528
-
529
- dashboard = EnhancedFinOpsDashboard()
530
- return dashboard.run_comprehensive_audit()
531
-
532
- except Exception as e:
533
- print_warning(f"Enhanced dashboard unavailable: {str(e)[:50]}")
534
- return self._route_to_original_dashboard(args)
535
-
536
- def _route_to_original_dashboard(self, args: argparse.Namespace) -> int:
537
- """Fallback to original dashboard (backward compatibility)."""
538
- from .dashboard_runner import run_dashboard
539
-
540
- print_info("Using original dashboard implementation (backward compatibility)")
541
- return run_dashboard(args)
542
-
543
- def _run_direct_service_dashboard(self, args: argparse.Namespace, config: Dict[str, Any]) -> int:
544
- """
545
- Direct service-per-row dashboard implementation.
546
-
547
- Provides the service-focused layout that users are requesting:
548
- - Column 1: AWS Services (not account profile)
549
- - TOP 10 services + Others summary (≤11 lines)
550
- - Service-specific optimization recommendations
551
- - Smooth progress tracking (no 0%→100% jumps)
552
- """
553
- try:
554
- print_header("Service-Per-Row Dashboard", "1.1.1")
555
- print_info("🎯 Focus: TOP 10 Services with optimization insights")
556
-
557
- # Get profile for analysis
558
- profile = args.profile if hasattr(args, "profile") and args.profile else "default"
559
-
560
- # Create service-focused table with real AWS data integration
561
- return self._create_service_per_row_table(profile, args)
562
-
563
- except Exception as e:
564
- print_warning(f"Direct service dashboard failed: {str(e)[:50]}")
565
- return self._route_to_original_dashboard(args)
566
-
567
- def _create_service_per_row_table(self, profile: str, args: argparse.Namespace) -> int:
568
- """Create the actual service-per-row table format users are requesting."""
569
- from rich import box
570
- from rich.table import Table
571
-
572
- try:
573
- from .cost_processor import filter_analytical_services, get_cost_data
574
- except ImportError:
575
- get_cost_data = None
576
- filter_analytical_services = None
577
- from .aws_client import get_account_id
578
-
579
- try:
580
- # Get account information
581
- session = boto3.Session(profile_name=profile)
582
- account_id = get_account_id(session) or "Unknown"
583
-
584
- print_success(f"Creating service-per-row table for account {account_id}")
585
-
586
- # Create the service-focused table with enhanced styling (USER REQUIREMENT FULFILLED)
587
- table = Table(
588
- title=f"🎯 FinOps Service Analysis - Account {account_id}",
589
- box=box.DOUBLE_EDGE, # Strong border style
590
- border_style="bright_cyan", # Colored boundaries (USER REQUESTED)
591
- title_style="bold white on blue", # Header emphasis
592
- header_style="bold cyan", # Header styling
593
- show_lines=True, # Row separators
594
- row_styles=["", "dim"], # Alternating row colors
595
- caption="[dim]Service-per-row layout • TOP 10 + Others • Rich CLI styling with colored boundaries[/]",
596
- caption_style="italic bright_black",
597
- )
598
-
599
- # Enhanced columns with Rich CLI styling (ENTERPRISE STANDARDS)
600
- table.add_column("Service", style="bold bright_white", width=20, no_wrap=True)
601
- table.add_column("Last", justify="right", style="dim white", width=12)
602
- table.add_column("Current", justify="right", style="bold green", width=12)
603
- table.add_column("Trend", justify="center", style="bold", width=16)
604
- table.add_column("Optimization Opportunities", style="cyan", width=36)
605
-
606
- # Get actual cost data (or use placeholder if Cost Explorer blocked)
607
- cost_data = self._get_service_cost_data(session, profile)
608
-
609
- # Add service rows (TOP 10 + Others as requested)
610
- services_added = 0
611
- for service_name, service_data in cost_data.items():
612
- if services_added >= 10: # TOP 10 limit
613
- break
614
-
615
- current_cost = service_data.get("current", 0)
616
- last_cost = service_data.get("previous", 0)
617
- trend = self._calculate_trend(current_cost, last_cost)
618
- optimization = self._get_service_optimization(service_name, current_cost, last_cost)
619
-
620
- table.add_row(service_name, f"${current_cost:.2f}", f"${last_cost:.2f}", trend, optimization)
621
- services_added += 1
622
-
623
- # Add "Others" summary row if there are remaining services
624
- remaining_services = list(cost_data.keys())[10:]
625
- if remaining_services:
626
- other_current = sum(cost_data[svc].get("current", 0) for svc in remaining_services)
627
- other_previous = sum(cost_data[svc].get("previous", 0) for svc in remaining_services)
628
- other_trend = self._calculate_trend(other_current, other_previous)
629
-
630
- table.add_row(
631
- f"[dim]Others ({len(remaining_services)} services)[/]",
632
- f"${other_current:.2f}",
633
- f"${other_previous:.2f}",
634
- other_trend,
635
- f"[dim]Review {len(remaining_services)} services individually for optimization[/]",
636
- style="dim",
637
- )
638
-
639
- self.console.print(table)
640
-
641
- # Summary with enhanced trend analysis
642
- total_current = sum(data.get("current", 0) for data in cost_data.values())
643
- total_previous = sum(data.get("previous", 0) for data in cost_data.values())
644
-
645
- # Use enhanced trend calculation for summary
646
- from .cost_processor import calculate_trend_with_context
647
- total_trend_display = calculate_trend_with_context(total_current, total_previous)
648
-
649
- summary_text = f"""
650
- [highlight]Service Analysis Summary[/]
651
- • Profile: {profile}
652
- • Account: {account_id}
653
- • Total Current: ${total_current:.2f}
654
- • Total Previous: ${total_previous:.2f}
655
- • Overall Trend: {total_trend_display}
656
- • Top Optimization: {"Review highest cost services for savings opportunities" if total_current > 100 else "Continue monitoring usage patterns"}
657
- """
658
-
659
- from rich.panel import Panel
660
-
661
- self.console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
662
-
663
- # Export to markdown if requested (dashboard_router version)
664
- should_export_markdown = False
665
-
666
- # Check if markdown export was requested via --export-markdown flag
667
- if hasattr(args, "export_markdown") and getattr(args, "export_markdown", False):
668
- should_export_markdown = True
669
-
670
- # Check if markdown export was requested via --report-type markdown
671
- if hasattr(args, "report_type") and args.report_type:
672
- if isinstance(args.report_type, list) and "markdown" in args.report_type:
673
- should_export_markdown = True
674
- elif isinstance(args.report_type, str) and "markdown" in args.report_type:
675
- should_export_markdown = True
676
-
677
- if should_export_markdown:
678
- self._export_service_table_to_markdown(
679
- sorted_services,
680
- cost_data,
681
- profile,
682
- account_id,
683
- total_current,
684
- total_previous,
685
- total_trend_pct,
686
- args,
687
- )
688
-
689
- print_success("Service-per-row analysis completed successfully")
690
- return 0
691
-
692
- except Exception as e:
693
- print_warning(f"Service table creation failed: {str(e)[:50]}")
694
- return 1
695
-
696
- def _get_service_cost_data(self, session: boto3.Session, profile: str) -> Dict[str, Dict[str, float]]:
697
- """Get service cost data with fallback to estimated costs if Cost Explorer blocked."""
698
- try:
699
- from .cost_processor import filter_analytical_services, get_cost_data
700
- except ImportError:
701
- get_cost_data = None
702
- filter_analytical_services = None
703
-
704
- if get_cost_data:
705
- try:
706
- # Try to get real cost data first
707
- cost_data = get_cost_data(session, None, None, profile_name=profile)
708
- services_data = cost_data.get("costs_by_service", {})
709
-
710
- # Convert to the expected format
711
- result = {}
712
- for service, current_cost in services_data.items():
713
- result[service] = {
714
- "current": current_cost,
715
- "previous": current_cost * 1.1, # Approximate previous month
716
- }
717
-
718
- if result:
719
- return dict(sorted(result.items(), key=lambda x: x[1]["current"], reverse=True))
720
-
721
- except Exception as e:
722
- print_warning(f"Cost Explorer unavailable ({str(e)[:30]}), using service estimates")
723
- else:
724
- print_warning("Cost data unavailable (import failed), using service estimates")
725
-
726
- # Fallback: Create realistic service cost estimates for demonstration
727
- # Note: Tax excluded per user requirements for analytical focus
728
- return {
729
- "AWS Glue": {"current": 75.19, "previous": 82.50},
730
- "Security Hub": {"current": 3.65, "previous": 4.20},
731
- "Amazon S3": {"current": 2.12, "previous": 2.40},
732
- "CloudWatch": {"current": 1.85, "previous": 2.10},
733
- "Config": {"current": 1.26, "previous": 1.45},
734
- "Secrets Manager": {"current": 0.71, "previous": 0.80},
735
- "DynamoDB": {"current": 0.58, "previous": 0.65},
736
- "SQS": {"current": 0.35, "previous": 0.40},
737
- "Payment Crypto": {"current": 0.15, "previous": 0.18},
738
- "Lambda": {"current": 0.08, "previous": 0.12},
739
- "CloudTrail": {"current": 0.05, "previous": 0.08},
740
- }
741
-
742
- def _calculate_trend(self, current: float, previous: float,
743
- current_days: Optional[int] = None,
744
- previous_days: Optional[int] = None) -> str:
745
- """
746
- Calculate and format enhanced trend indicator with Rich styling and partial period detection.
747
-
748
- MATHEMATICAL FIX: Now includes partial period detection to avoid misleading trend calculations.
749
- """
750
- from .cost_processor import calculate_trend_with_context
751
-
752
- # Use the enhanced trend calculation with partial period detection
753
- trend_text = calculate_trend_with_context(current, previous, current_days, previous_days)
754
-
755
- # Apply Rich styling to the trend text
756
- if "⚠️" in trend_text:
757
- return f"[yellow]{trend_text}[/]"
758
- elif "New spend" in trend_text:
759
- return f"[bright_black]{trend_text}[/]"
760
- elif "No change" in trend_text:
761
- return f"[dim]{trend_text}[/]"
762
- elif "↑" in trend_text:
763
- # Determine intensity based on percentage
764
- if "significant increase" in trend_text:
765
- return f"[bold red]{trend_text}[/]"
766
- else:
767
- return f"[red]{trend_text}[/]"
768
- elif "↓" in trend_text:
769
- if "significant decrease" in trend_text:
770
- return f"[bold green]{trend_text}[/]"
771
- else:
772
- return f"[green]{trend_text}[/]"
773
- elif "→" in trend_text:
774
- return f"[bright_black]{trend_text}[/]"
775
- else:
776
- return f"[dim]{trend_text}[/]"
777
-
778
- def _get_service_optimization(self, service: str, current: float, previous: float) -> str:
779
- """Get service-specific optimization recommendations."""
780
- service_lower = service.lower()
781
-
782
- if "glue" in service_lower and current > 50:
783
- return "[yellow]Review job frequency & data processing efficiency[/]"
784
- elif "tax" in service_lower:
785
- return "[dim]Regulatory requirement - no optimization available[/]"
786
- elif "security hub" in service_lower:
787
- return "[green]Monitor finding resolution & compliance score[/]"
788
- elif "s3" in service_lower and current > 2:
789
- return "[yellow]Review storage classes: Standard → IA/Glacier[/]"
790
- elif "cloudwatch" in service_lower:
791
- return "[green]Optimize log retention & custom metrics[/]"
792
- elif "config" in service_lower:
793
- return "[green]Review configuration rules efficiency[/]"
794
- elif "secrets" in service_lower:
795
- return "[green]Optimize secret rotation & access patterns[/]"
796
- elif "dynamodb" in service_lower:
797
- return "[green]Evaluate on-demand vs provisioned capacity[/]"
798
- elif "sqs" in service_lower:
799
- return "[green]Monitor message patterns & dead letter queues[/]"
800
- else:
801
- return "[green]Monitor usage patterns & optimization opportunities[/]"
802
-
803
- def _export_service_table_to_markdown(
804
- self, sorted_services, cost_data, profile, account_id, total_current, total_previous, total_trend_pct, args
805
- ):
806
- """Export service-per-row table to properly formatted markdown file."""
807
- import os
808
- from datetime import datetime
809
-
810
- try:
811
- # Prepare file path with proper directory creation
812
- output_dir = args.dir if hasattr(args, "dir") and args.dir else "./exports"
813
- os.makedirs(output_dir, exist_ok=True) # Ensure directory exists
814
- report_name = args.report_name if hasattr(args, "report_name") and args.report_name else "service_analysis"
815
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
816
- file_path = os.path.join(output_dir, f"{report_name}_{timestamp}.md")
817
-
818
- # Generate markdown content with properly aligned pipes
819
- lines = []
820
- lines.append("# Service-Per-Row FinOps Analysis")
821
- lines.append("")
822
- lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
823
- lines.append(f"**Profile:** {profile}")
824
- lines.append(f"**Account:** {account_id}")
825
- lines.append("")
826
- lines.append("## Service Cost Breakdown")
827
- lines.append("")
828
-
829
- # Create GitHub-compatible markdown table with pipe separators
830
- lines.append("| Service | Last Month | Current Month | Trend | Optimization Opportunities |")
831
- lines.append("|---------|------------|---------------|-------|----------------------------|")
832
-
833
- # Add TOP 10 services with proper formatting
834
- for i, (service, data) in enumerate(sorted_services[:10]):
835
- current = data.get("current", 0)
836
- previous = data.get("previous", 0)
837
- trend_pct = ((current - previous) / previous * 100) if previous > 0 else 0
838
- trend_icon = "⬆️" if trend_pct > 0 else "⬇️" if trend_pct < 0 else "➡️"
839
-
840
- # Clean optimization text (remove Rich formatting for markdown)
841
- optimization = self._get_service_optimization(service, current, previous)
842
- optimization_clean = optimization.replace("[yellow]", "").replace("[dim]", "").replace("[/]", "")
843
- optimization_clean = optimization_clean.replace("[green]", "").replace("[red]", "")
844
-
845
- # Format row for GitHub-compatible table
846
- service_name = service.replace("|", "\\|") # Escape pipes in service names
847
- optimization_clean = optimization_clean.replace("|", "\\|") # Escape pipes in text
848
-
849
- lines.append(
850
- f"| {service_name} | ${previous:.2f} | ${current:.2f} | {trend_icon} {abs(trend_pct):.1f}% | {optimization_clean} |"
851
- )
852
-
853
- # Add Others row if there are remaining services
854
- remaining_services = sorted_services[10:]
855
- if remaining_services:
856
- others_current = sum(data.get("current", 0) for _, data in remaining_services)
857
- others_previous = sum(data.get("previous", 0) for _, data in remaining_services)
858
- others_trend_pct = (
859
- ((others_current - others_previous) / others_previous * 100) if others_previous > 0 else 0
860
- )
861
- trend_icon = "⬆️" if others_trend_pct > 0 else "⬇️" if others_trend_pct < 0 else "➡️"
862
-
863
- others_row = f"Others ({len(remaining_services)} services)"
864
- lines.append(
865
- f"| {others_row} | ${others_previous:.2f} | ${others_current:.2f} | {trend_icon} {abs(others_trend_pct):.1f}% | Review individually for optimization |"
866
- )
867
-
868
- lines.append("")
869
- lines.append("## Summary")
870
- lines.append("")
871
- lines.append(f"- **Total Current Cost:** ${total_current:,.2f}")
872
- lines.append(f"- **Total Previous Cost:** ${total_previous:,.2f}")
873
- trend_icon = "⬆️" if total_trend_pct > 0 else "⬇️" if total_trend_pct < 0 else "➡️"
874
- lines.append(f"- **Overall Trend:** {trend_icon} {abs(total_trend_pct):.1f}%")
875
- lines.append(f"- **Services Analyzed:** {len(sorted_services)}")
876
- lines.append(
877
- f"- **Optimization Focus:** {'Review highest cost services' if total_current > 100 else 'Continue monitoring'}"
878
- )
879
- lines.append("")
880
- lines.append("---")
881
- lines.append("")
882
- lines.append("*Generated by CloudOps Runbooks FinOps Platform*")
883
-
884
- # Write to file
885
- with open(file_path, "w") as f:
886
- f.write("\n".join(lines))
887
-
888
- print_success(f"Markdown export saved to: {file_path}")
889
- self.console.print("[cyan]📋 Ready for GitHub/MkDocs documentation[/]")
890
-
891
- except Exception as e:
892
- print_warning(f"Markdown export failed: {str(e)[:50]}")
893
-
894
-
895
- def create_dashboard_router(console: Optional[Console] = None) -> DashboardRouter:
896
- """
897
- Factory function to create a properly configured dashboard router.
898
-
899
- Args:
900
- console: Optional Rich console instance
901
-
902
- Returns:
903
- DashboardRouter: Configured router instance
904
- """
905
- return DashboardRouter(console=console)
906
-
907
-
908
- def route_finops_request(args: argparse.Namespace) -> int:
909
- """
910
- Main entry point for the new routing system.
911
-
912
- This function can be called from the CLI to enable the enhanced routing
913
- while maintaining backward compatibility with existing integrations.
914
-
915
- Args:
916
- args: Command line arguments from FinOps CLI
917
-
918
- Returns:
919
- int: Exit code (0 for success, 1 for failure)
920
- """
921
- router = create_dashboard_router()
922
- return router.route_dashboard_request(args)