runbooks 1.1.3__py3-none-any.whl → 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
- runbooks/cfat/assessment/compliance.py +8 -8
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cfat/models.py +6 -2
- runbooks/cfat/tests/__init__.py +6 -1
- runbooks/cli/__init__.py +13 -0
- runbooks/cli/commands/cfat.py +274 -0
- runbooks/cli/commands/finops.py +1164 -0
- runbooks/cli/commands/inventory.py +379 -0
- runbooks/cli/commands/operate.py +239 -0
- runbooks/cli/commands/security.py +248 -0
- runbooks/cli/commands/validation.py +825 -0
- runbooks/cli/commands/vpc.py +310 -0
- runbooks/cli/registry.py +107 -0
- runbooks/cloudops/__init__.py +23 -30
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +549 -547
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +226 -227
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +179 -215
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +341 -0
- runbooks/common/aws_utils.py +75 -80
- runbooks/common/business_logic.py +127 -105
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
- runbooks/common/cross_account_manager.py +198 -205
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +235 -0
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +478 -495
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +176 -194
- runbooks/common/patterns.py +204 -0
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +248 -39
- runbooks/common/rich_utils.py +643 -92
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +29 -33
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +488 -622
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +40 -37
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +230 -292
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +338 -175
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1513 -482
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +25 -29
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +77 -78
- runbooks/finops/scenarios.py +1278 -439
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/tests/test_finops_dashboard.py +3 -3
- runbooks/finops/tests/test_reference_images_validation.py +2 -2
- runbooks/finops/tests/test_single_account_features.py +17 -17
- runbooks/finops/tests/validate_test_suite.py +1 -1
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +263 -269
- runbooks/finops/vpc_cleanup_exporter.py +191 -146
- runbooks/finops/vpc_cleanup_optimizer.py +593 -575
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/hitl/enhanced_workflow_engine.py +1 -1
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/README.md +3 -3
- runbooks/inventory/Tests/common_test_data.py +30 -30
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +28 -11
- runbooks/inventory/collectors/aws_networking.py +111 -101
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/discovery.md +2 -2
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/find_ec2_security_groups.py +1 -1
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +56 -52
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +733 -696
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +3 -3
- runbooks/main.py +152 -9147
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/metrics/dora_metrics_engine.py +2 -2
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/mcp_integration.py +1 -1
- runbooks/operate/networking_cost_heatmap.py +33 -10
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/operate/vpc_operations.py +648 -618
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +71 -67
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +91 -65
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +49 -44
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/integration_test_enterprise_security.py +5 -3
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/run_script.py +1 -1
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/mcp_reliability_engine.py +6 -6
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +51 -48
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +754 -708
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +190 -162
- runbooks/vpc/mcp_no_eni_validator.py +681 -640
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1302 -1129
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -956
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.3.dist-info/METADATA +0 -799
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -1,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)
|