runbooks 0.7.6__py3-none-any.whl → 0.7.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +1 -1
- runbooks/base.py +5 -1
- runbooks/cfat/__init__.py +8 -4
- runbooks/cfat/assessment/collectors.py +171 -14
- runbooks/cfat/assessment/compliance.py +871 -0
- runbooks/cfat/assessment/runner.py +122 -11
- runbooks/cfat/models.py +6 -2
- runbooks/common/logger.py +14 -0
- runbooks/common/rich_utils.py +451 -0
- runbooks/enterprise/__init__.py +68 -0
- runbooks/enterprise/error_handling.py +411 -0
- runbooks/enterprise/logging.py +439 -0
- runbooks/enterprise/multi_tenant.py +583 -0
- runbooks/finops/README.md +468 -241
- runbooks/finops/__init__.py +39 -3
- runbooks/finops/cli.py +83 -18
- runbooks/finops/cross_validation.py +375 -0
- runbooks/finops/dashboard_runner.py +812 -164
- runbooks/finops/enhanced_dashboard_runner.py +525 -0
- runbooks/finops/finops_dashboard.py +1892 -0
- runbooks/finops/helpers.py +485 -51
- runbooks/finops/optimizer.py +823 -0
- runbooks/finops/tests/__init__.py +19 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
- runbooks/finops/tests/run_comprehensive_tests.py +421 -0
- runbooks/finops/tests/run_tests.py +305 -0
- runbooks/finops/tests/test_finops_dashboard.py +705 -0
- runbooks/finops/tests/test_integration.py +477 -0
- runbooks/finops/tests/test_performance.py +380 -0
- runbooks/finops/tests/test_performance_benchmarks.py +500 -0
- runbooks/finops/tests/test_reference_images_validation.py +867 -0
- runbooks/finops/tests/test_single_account_features.py +715 -0
- runbooks/finops/tests/validate_test_suite.py +220 -0
- runbooks/finops/types.py +1 -1
- runbooks/hitl/enhanced_workflow_engine.py +725 -0
- runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
- runbooks/inventory/collectors/aws_comprehensive.py +442 -0
- runbooks/inventory/collectors/enterprise_scale.py +281 -0
- runbooks/inventory/core/collector.py +172 -13
- runbooks/inventory/discovery.md +1 -1
- runbooks/inventory/list_ec2_instances.py +18 -20
- runbooks/inventory/list_ssm_parameters.py +31 -3
- runbooks/inventory/organizations_discovery.py +1269 -0
- runbooks/inventory/rich_inventory_display.py +393 -0
- runbooks/inventory/run_on_multi_accounts.py +35 -19
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/vpc_flow_analyzer.py +1030 -0
- runbooks/main.py +2215 -119
- runbooks/metrics/dora_metrics_engine.py +599 -0
- runbooks/operate/__init__.py +2 -2
- runbooks/operate/base.py +122 -10
- runbooks/operate/deployment_framework.py +1032 -0
- runbooks/operate/deployment_validator.py +853 -0
- runbooks/operate/dynamodb_operations.py +10 -6
- runbooks/operate/ec2_operations.py +319 -11
- runbooks/operate/executive_dashboard.py +779 -0
- runbooks/operate/mcp_integration.py +750 -0
- runbooks/operate/nat_gateway_operations.py +1120 -0
- runbooks/operate/networking_cost_heatmap.py +685 -0
- runbooks/operate/privatelink_operations.py +940 -0
- runbooks/operate/s3_operations.py +10 -6
- runbooks/operate/vpc_endpoints.py +644 -0
- runbooks/operate/vpc_operations.py +1038 -0
- runbooks/remediation/__init__.py +2 -2
- runbooks/remediation/acm_remediation.py +1 -1
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/cloudtrail_remediation.py +1 -1
- runbooks/remediation/cognito_remediation.py +1 -1
- runbooks/remediation/dynamodb_remediation.py +1 -1
- runbooks/remediation/ec2_remediation.py +1 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
- runbooks/remediation/kms_enable_key_rotation.py +1 -1
- runbooks/remediation/kms_remediation.py +1 -1
- runbooks/remediation/lambda_remediation.py +1 -1
- runbooks/remediation/multi_account.py +1 -1
- runbooks/remediation/rds_remediation.py +1 -1
- runbooks/remediation/s3_block_public_access.py +1 -1
- runbooks/remediation/s3_enable_access_logging.py +1 -1
- runbooks/remediation/s3_encryption.py +1 -1
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/remediation/vpc_remediation.py +475 -0
- runbooks/security/__init__.py +3 -1
- runbooks/security/compliance_automation.py +632 -0
- runbooks/security/report_generator.py +10 -0
- runbooks/security/run_script.py +31 -5
- runbooks/security/security_baseline_tester.py +169 -30
- runbooks/security/security_export.py +477 -0
- runbooks/validation/__init__.py +10 -0
- runbooks/validation/benchmark.py +484 -0
- runbooks/validation/cli.py +356 -0
- runbooks/validation/mcp_validator.py +768 -0
- runbooks/vpc/__init__.py +38 -0
- runbooks/vpc/config.py +212 -0
- runbooks/vpc/cost_engine.py +347 -0
- runbooks/vpc/heatmap_engine.py +605 -0
- runbooks/vpc/manager_interface.py +634 -0
- runbooks/vpc/networking_wrapper.py +1260 -0
- runbooks/vpc/rich_formatters.py +679 -0
- runbooks/vpc/tests/__init__.py +5 -0
- runbooks/vpc/tests/conftest.py +356 -0
- runbooks/vpc/tests/test_cli_integration.py +530 -0
- runbooks/vpc/tests/test_config.py +458 -0
- runbooks/vpc/tests/test_cost_engine.py +479 -0
- runbooks/vpc/tests/test_networking_wrapper.py +512 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,20 @@
|
|
1
1
|
import argparse
|
2
|
+
import os
|
2
3
|
from collections import defaultdict
|
3
4
|
from typing import Any, Dict, List, Optional, Tuple
|
4
5
|
|
5
6
|
import boto3
|
6
7
|
from rich import box
|
7
8
|
from rich.console import Console
|
8
|
-
from rich.progress import track
|
9
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn, track
|
9
10
|
from rich.status import Status
|
10
11
|
from rich.table import Column, Table
|
11
12
|
|
12
13
|
from runbooks.finops.aws_client import (
|
14
|
+
ec2_summary,
|
13
15
|
get_accessible_regions,
|
14
16
|
get_account_id,
|
17
|
+
get_aws_profiles,
|
15
18
|
get_budgets,
|
16
19
|
get_stopped_instances,
|
17
20
|
get_untagged_resources,
|
@@ -19,10 +22,14 @@ from runbooks.finops.aws_client import (
|
|
19
22
|
get_unused_volumes,
|
20
23
|
)
|
21
24
|
from runbooks.finops.cost_processor import (
|
25
|
+
change_in_total_cost,
|
22
26
|
export_to_csv,
|
23
27
|
export_to_json,
|
28
|
+
format_budget_info,
|
29
|
+
format_ec2_summary,
|
24
30
|
get_cost_data,
|
25
31
|
get_trend,
|
32
|
+
process_service_costs,
|
26
33
|
)
|
27
34
|
from runbooks.finops.helpers import (
|
28
35
|
clean_rich_tags,
|
@@ -31,6 +38,7 @@ from runbooks.finops.helpers import (
|
|
31
38
|
export_audit_report_to_pdf,
|
32
39
|
export_cost_dashboard_to_pdf,
|
33
40
|
export_trend_data_to_json,
|
41
|
+
generate_pdca_improvement_report,
|
34
42
|
)
|
35
43
|
from runbooks.finops.profile_processor import (
|
36
44
|
process_combined_profiles,
|
@@ -42,6 +50,153 @@ from runbooks.finops.visualisations import create_trend_bars
|
|
42
50
|
console = Console()
|
43
51
|
|
44
52
|
|
53
|
+
def _get_profile_for_operation(operation_type: str, default_profile: str) -> str:
|
54
|
+
"""
|
55
|
+
Get the appropriate AWS profile based on operation type.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
operation_type: Type of operation ('billing', 'management', 'operational')
|
59
|
+
default_profile: Default profile to fall back to
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
str: Profile name to use for the operation
|
63
|
+
"""
|
64
|
+
profile_map = {
|
65
|
+
"billing": os.getenv("BILLING_PROFILE"),
|
66
|
+
"management": os.getenv("MANAGEMENT_PROFILE"),
|
67
|
+
"operational": os.getenv("CENTRALISED_OPS_PROFILE"),
|
68
|
+
}
|
69
|
+
|
70
|
+
profile = profile_map.get(operation_type)
|
71
|
+
if profile:
|
72
|
+
# Verify profile exists
|
73
|
+
available_profiles = boto3.Session().available_profiles
|
74
|
+
if profile in available_profiles:
|
75
|
+
console.log(f"[dim cyan]Using {operation_type} profile: {profile}[/]")
|
76
|
+
return profile
|
77
|
+
else:
|
78
|
+
console.log(
|
79
|
+
f"[yellow]Warning: {operation_type.title()} profile '{profile}' not found in AWS config. Using default: {default_profile}[/]"
|
80
|
+
)
|
81
|
+
|
82
|
+
return default_profile
|
83
|
+
|
84
|
+
|
85
|
+
def _create_cost_session(profile: str) -> boto3.Session:
|
86
|
+
"""
|
87
|
+
Create a boto3 session specifically for cost operations.
|
88
|
+
Uses BILLING_PROFILE if available, falls back to provided profile.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
profile: Default profile to use
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
boto3.Session: Session configured for cost operations
|
95
|
+
"""
|
96
|
+
cost_profile = _get_profile_for_operation("billing", profile)
|
97
|
+
return boto3.Session(profile_name=cost_profile)
|
98
|
+
|
99
|
+
|
100
|
+
def _create_management_session(profile: str) -> boto3.Session:
|
101
|
+
"""
|
102
|
+
Create a boto3 session specifically for management operations.
|
103
|
+
Uses MANAGEMENT_PROFILE if available, falls back to provided profile.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
profile: Default profile to use
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
boto3.Session: Session configured for management operations
|
110
|
+
"""
|
111
|
+
mgmt_profile = _get_profile_for_operation("management", profile)
|
112
|
+
return boto3.Session(profile_name=mgmt_profile)
|
113
|
+
|
114
|
+
|
115
|
+
def _create_operational_session(profile: str) -> boto3.Session:
|
116
|
+
"""
|
117
|
+
Create a boto3 session specifically for operational tasks.
|
118
|
+
Uses CENTRALISED_OPS_PROFILE if available, falls back to provided profile.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
profile: Default profile to use
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
boto3.Session: Session configured for operational tasks
|
125
|
+
"""
|
126
|
+
ops_profile = _get_profile_for_operation("operational", profile)
|
127
|
+
return boto3.Session(profile_name=ops_profile)
|
128
|
+
|
129
|
+
|
130
|
+
def _calculate_risk_score(untagged, stopped, unused_vols, unused_eips, budget_data):
|
131
|
+
"""Calculate risk score based on audit findings for PDCA tracking."""
|
132
|
+
score = 0
|
133
|
+
|
134
|
+
# Untagged resources (high risk for compliance)
|
135
|
+
untagged_count = sum(len(ids) for region_map in untagged.values() for ids in region_map.values())
|
136
|
+
score += untagged_count * 2 # High weight for untagged
|
137
|
+
|
138
|
+
# Stopped instances (medium risk for cost)
|
139
|
+
stopped_count = sum(len(ids) for ids in stopped.values())
|
140
|
+
score += stopped_count * 1
|
141
|
+
|
142
|
+
# Unused volumes (medium risk for cost)
|
143
|
+
volume_count = sum(len(ids) for ids in unused_vols.values())
|
144
|
+
score += volume_count * 1
|
145
|
+
|
146
|
+
# Unused EIPs (high risk for cost)
|
147
|
+
eip_count = sum(len(ids) for ids in unused_eips.values())
|
148
|
+
score += eip_count * 3 # High cost impact
|
149
|
+
|
150
|
+
# Budget overruns (critical risk)
|
151
|
+
overruns = len([b for b in budget_data if b["actual"] > b["limit"]])
|
152
|
+
score += overruns * 5 # Critical weight
|
153
|
+
|
154
|
+
return score
|
155
|
+
|
156
|
+
|
157
|
+
def _format_risk_score(score):
|
158
|
+
"""Format risk score with visual indicators."""
|
159
|
+
if score == 0:
|
160
|
+
return "[bright_green]🟢 LOW\n(0)[/]"
|
161
|
+
elif score <= 10:
|
162
|
+
return f"[yellow]🟡 MEDIUM\n({score})[/]"
|
163
|
+
elif score <= 25:
|
164
|
+
return f"[orange1]🟠 HIGH\n({score})[/]"
|
165
|
+
else:
|
166
|
+
return f"[bright_red]🔴 CRITICAL\n({score})[/]"
|
167
|
+
|
168
|
+
|
169
|
+
def _display_pdca_summary(pdca_metrics):
|
170
|
+
"""Display PDCA improvement summary with actionable insights."""
|
171
|
+
if not pdca_metrics:
|
172
|
+
return
|
173
|
+
|
174
|
+
total_risk = sum(m["risk_score"] for m in pdca_metrics)
|
175
|
+
avg_risk = total_risk / len(pdca_metrics)
|
176
|
+
|
177
|
+
high_risk_accounts = [m for m in pdca_metrics if m["risk_score"] > 25]
|
178
|
+
total_untagged = sum(m["untagged_count"] for m in pdca_metrics)
|
179
|
+
total_unused_eips = sum(m["unused_eips_count"] for m in pdca_metrics)
|
180
|
+
|
181
|
+
summary_table = Table(title="🎯 PDCA Continuous Improvement Metrics", box=box.SIMPLE, style="cyan")
|
182
|
+
summary_table.add_column("Metric", style="bold")
|
183
|
+
summary_table.add_column("Value", justify="right")
|
184
|
+
summary_table.add_column("Action Required", style="yellow")
|
185
|
+
|
186
|
+
summary_table.add_row("Average Risk Score", f"{avg_risk:.1f}", "✅ Good" if avg_risk < 10 else "⚠️ Review Required")
|
187
|
+
summary_table.add_row(
|
188
|
+
"High-Risk Accounts", str(len(high_risk_accounts)), "🔴 Immediate Action" if high_risk_accounts else "✅ Good"
|
189
|
+
)
|
190
|
+
summary_table.add_row(
|
191
|
+
"Total Untagged Resources", str(total_untagged), "📋 Tag Management" if total_untagged > 50 else "✅ Good"
|
192
|
+
)
|
193
|
+
summary_table.add_row(
|
194
|
+
"Total Unused EIPs", str(total_unused_eips), "💰 Cost Optimization" if total_unused_eips > 5 else "✅ Good"
|
195
|
+
)
|
196
|
+
|
197
|
+
console.print(summary_table)
|
198
|
+
|
199
|
+
|
45
200
|
def _initialize_profiles(
|
46
201
|
args: argparse.Namespace,
|
47
202
|
) -> Tuple[List[str], Optional[List[str]], Optional[int]]:
|
@@ -74,102 +229,177 @@ def _initialize_profiles(
|
|
74
229
|
|
75
230
|
|
76
231
|
def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> None:
|
77
|
-
"""Generate and export an audit report."""
|
78
|
-
console.print("[bold bright_cyan]Preparing
|
232
|
+
"""Generate and export an audit report with PDCA continuous improvement."""
|
233
|
+
console.print("[bold bright_cyan]🔍 PLAN: Preparing comprehensive audit report...[/]")
|
234
|
+
|
235
|
+
# Display multi-profile configuration
|
236
|
+
billing_profile = os.getenv("BILLING_PROFILE")
|
237
|
+
mgmt_profile = os.getenv("MANAGEMENT_PROFILE")
|
238
|
+
ops_profile = os.getenv("CENTRALISED_OPS_PROFILE")
|
239
|
+
|
240
|
+
if any([billing_profile, mgmt_profile, ops_profile]):
|
241
|
+
console.print("[dim cyan]Multi-profile configuration detected:[/]")
|
242
|
+
if billing_profile:
|
243
|
+
console.print(f"[dim cyan] • Billing operations: {billing_profile}[/]")
|
244
|
+
if mgmt_profile:
|
245
|
+
console.print(f"[dim cyan] • Management operations: {mgmt_profile}[/]")
|
246
|
+
if ops_profile:
|
247
|
+
console.print(f"[dim cyan] • Operational tasks: {ops_profile}[/]")
|
248
|
+
console.print()
|
249
|
+
|
250
|
+
# Enhanced table with better visual hierarchy
|
79
251
|
table = Table(
|
80
|
-
Column("Profile", justify="center"),
|
81
|
-
Column("Account ID", justify="center"),
|
82
|
-
Column("Untagged Resources"),
|
83
|
-
Column("Stopped EC2 Instances"),
|
84
|
-
Column("Unused Volumes"),
|
85
|
-
Column("Unused EIPs"),
|
86
|
-
Column("Budget Alerts"),
|
87
|
-
|
252
|
+
Column("Profile", justify="center", style="bold magenta"),
|
253
|
+
Column("Account ID", justify="center", style="dim"),
|
254
|
+
Column("Untagged Resources", style="yellow"),
|
255
|
+
Column("Stopped EC2 Instances", style="red"),
|
256
|
+
Column("Unused Volumes", style="orange1"),
|
257
|
+
Column("Unused EIPs", style="cyan"),
|
258
|
+
Column("Budget Alerts", style="bright_red"),
|
259
|
+
Column("Risk Score", justify="center", style="bold"),
|
260
|
+
title="🎯 AWS FinOps Audit Report - PDCA Enhanced",
|
88
261
|
show_lines=True,
|
89
|
-
box=box.
|
262
|
+
box=box.ROUNDED,
|
90
263
|
style="bright_cyan",
|
264
|
+
caption="🚀 PDCA Cycle: Plan → Do → Check → Act",
|
91
265
|
)
|
92
266
|
|
93
267
|
audit_data = []
|
94
268
|
raw_audit_data = []
|
269
|
+
pdca_metrics = [] # New: Track PDCA improvement metrics
|
95
270
|
nl = "\n"
|
96
271
|
comma_nl = ",\n"
|
97
272
|
|
98
|
-
|
99
|
-
session = boto3.Session(profile_name=profile)
|
100
|
-
account_id = get_account_id(session) or "Unknown"
|
101
|
-
regions = args.regions or get_accessible_regions(session)
|
273
|
+
console.print("[bold green]⚙️ DO: Collecting audit data across profiles...[/]")
|
102
274
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
if not any(region_map for region_map in untagged.values()):
|
115
|
-
anomalies = ["None"]
|
116
|
-
except Exception as e:
|
117
|
-
anomalies = [f"Error: {str(e)}"]
|
118
|
-
|
119
|
-
stopped = get_stopped_instances(session, regions)
|
120
|
-
stopped_list = [f"{r}:\n[gold1]{nl.join(ids)}[/]" for r, ids in stopped.items()] or ["None"]
|
121
|
-
|
122
|
-
unused_vols = get_unused_volumes(session, regions)
|
123
|
-
vols_list = [f"{r}:\n[dark_orange]{nl.join(ids)}[/]" for r, ids in unused_vols.items()] or ["None"]
|
124
|
-
|
125
|
-
unused_eips = get_unused_eips(session, regions)
|
126
|
-
eips_list = [f"{r}:\n{comma_nl.join(ids)}" for r, ids in unused_eips.items()] or ["None"]
|
127
|
-
|
128
|
-
budget_data = get_budgets(session)
|
129
|
-
alerts = []
|
130
|
-
for b in budget_data:
|
131
|
-
if b["actual"] > b["limit"]:
|
132
|
-
alerts.append(f"[red1]{b['name']}[/]: ${b['actual']:.2f} > ${b['limit']:.2f}")
|
133
|
-
if not alerts:
|
134
|
-
alerts = ["No budgets exceeded"]
|
135
|
-
|
136
|
-
audit_data.append(
|
137
|
-
{
|
138
|
-
"profile": profile,
|
139
|
-
"account_id": account_id,
|
140
|
-
"untagged_resources": clean_rich_tags("\n".join(anomalies)),
|
141
|
-
"stopped_instances": clean_rich_tags("\n".join(stopped_list)),
|
142
|
-
"unused_volumes": clean_rich_tags("\n".join(vols_list)),
|
143
|
-
"unused_eips": clean_rich_tags("\n".join(eips_list)),
|
144
|
-
"budget_alerts": clean_rich_tags("\n".join(alerts)),
|
145
|
-
}
|
146
|
-
)
|
275
|
+
# Create progress tracker for enhanced user experience
|
276
|
+
with Progress(
|
277
|
+
SpinnerColumn(),
|
278
|
+
TextColumn("[progress.description]{task.description}"),
|
279
|
+
BarColumn(),
|
280
|
+
TaskProgressColumn(),
|
281
|
+
TimeElapsedColumn(),
|
282
|
+
console=console,
|
283
|
+
transient=True,
|
284
|
+
) as progress:
|
285
|
+
task = progress.add_task("Collecting audit data", total=len(profiles_to_use))
|
147
286
|
|
148
|
-
|
149
|
-
|
150
|
-
{
|
151
|
-
"profile": profile,
|
152
|
-
"account_id": account_id,
|
153
|
-
"untagged_resources": untagged,
|
154
|
-
"stopped_instances": stopped,
|
155
|
-
"unused_volumes": unused_vols,
|
156
|
-
"unused_eips": unused_eips,
|
157
|
-
"budget_alerts": budget_data,
|
158
|
-
}
|
159
|
-
)
|
287
|
+
for profile in profiles_to_use:
|
288
|
+
progress.update(task, description=f"Processing profile: {profile}")
|
160
289
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
"
|
169
|
-
|
290
|
+
# Use operational session for resource discovery
|
291
|
+
ops_session = _create_operational_session(profile)
|
292
|
+
# Use management session for account and governance operations
|
293
|
+
mgmt_session = _create_management_session(profile)
|
294
|
+
# Use billing session for cost and budget operations
|
295
|
+
billing_session = _create_cost_session(profile)
|
296
|
+
|
297
|
+
account_id = get_account_id(mgmt_session) or "Unknown"
|
298
|
+
regions = args.regions or get_accessible_regions(ops_session)
|
299
|
+
|
300
|
+
try:
|
301
|
+
# Use operational session for resource discovery
|
302
|
+
untagged = get_untagged_resources(ops_session, regions)
|
303
|
+
anomalies = []
|
304
|
+
for service, region_map in untagged.items():
|
305
|
+
if region_map:
|
306
|
+
service_block = f"[bright_yellow]{service}[/]:\n"
|
307
|
+
for region, ids in region_map.items():
|
308
|
+
if ids:
|
309
|
+
ids_block = "\n".join(f"[orange1]{res_id}[/]" for res_id in ids)
|
310
|
+
service_block += f"\n{region}:\n{ids_block}\n"
|
311
|
+
anomalies.append(service_block)
|
312
|
+
if not any(region_map for region_map in untagged.values()):
|
313
|
+
anomalies = ["None"]
|
314
|
+
except Exception as e:
|
315
|
+
anomalies = [f"Error: {str(e)}"]
|
316
|
+
|
317
|
+
# Use operational session for EC2 and resource operations
|
318
|
+
stopped = get_stopped_instances(ops_session, regions)
|
319
|
+
stopped_list = [f"{r}:\n[gold1]{nl.join(ids)}[/]" for r, ids in stopped.items()] or ["None"]
|
320
|
+
|
321
|
+
unused_vols = get_unused_volumes(ops_session, regions)
|
322
|
+
vols_list = [f"{r}:\n[dark_orange]{nl.join(ids)}[/]" for r, ids in unused_vols.items()] or ["None"]
|
323
|
+
|
324
|
+
unused_eips = get_unused_eips(ops_session, regions)
|
325
|
+
eips_list = [f"{r}:\n{comma_nl.join(ids)}" for r, ids in unused_eips.items()] or ["None"]
|
326
|
+
|
327
|
+
# Use billing session for budget data
|
328
|
+
budget_data = get_budgets(billing_session)
|
329
|
+
alerts = []
|
330
|
+
for b in budget_data:
|
331
|
+
if b["actual"] > b["limit"]:
|
332
|
+
alerts.append(f"[red1]{b['name']}[/]: ${b['actual']:.2f} > ${b['limit']:.2f}")
|
333
|
+
if not alerts:
|
334
|
+
alerts = ["✅ No budgets exceeded"]
|
335
|
+
|
336
|
+
# Calculate risk score for PDCA improvement tracking
|
337
|
+
risk_score = _calculate_risk_score(untagged, stopped, unused_vols, unused_eips, budget_data)
|
338
|
+
risk_display = _format_risk_score(risk_score)
|
339
|
+
|
340
|
+
# Track PDCA metrics
|
341
|
+
pdca_metrics.append(
|
342
|
+
{
|
343
|
+
"profile": profile,
|
344
|
+
"account_id": account_id,
|
345
|
+
"risk_score": risk_score,
|
346
|
+
"untagged_count": sum(len(ids) for region_map in untagged.values() for ids in region_map.values()),
|
347
|
+
"stopped_count": sum(len(ids) for ids in stopped.values()),
|
348
|
+
"unused_volumes_count": sum(len(ids) for ids in unused_vols.values()),
|
349
|
+
"unused_eips_count": sum(len(ids) for ids in unused_eips.values()),
|
350
|
+
"budget_overruns": len([b for b in budget_data if b["actual"] > b["limit"]]),
|
351
|
+
}
|
352
|
+
)
|
353
|
+
|
354
|
+
audit_data.append(
|
355
|
+
{
|
356
|
+
"profile": profile,
|
357
|
+
"account_id": account_id,
|
358
|
+
"untagged_resources": clean_rich_tags("\n".join(anomalies)),
|
359
|
+
"stopped_instances": clean_rich_tags("\n".join(stopped_list)),
|
360
|
+
"unused_volumes": clean_rich_tags("\n".join(vols_list)),
|
361
|
+
"unused_eips": clean_rich_tags("\n".join(eips_list)),
|
362
|
+
"budget_alerts": clean_rich_tags("\n".join(alerts)),
|
363
|
+
"risk_score": risk_score,
|
364
|
+
}
|
365
|
+
)
|
366
|
+
|
367
|
+
# Data for JSON which includes raw audit data
|
368
|
+
raw_audit_data.append(
|
369
|
+
{
|
370
|
+
"profile": profile,
|
371
|
+
"account_id": account_id,
|
372
|
+
"untagged_resources": untagged,
|
373
|
+
"stopped_instances": stopped,
|
374
|
+
"unused_volumes": unused_vols,
|
375
|
+
"unused_eips": unused_eips,
|
376
|
+
"budget_alerts": budget_data,
|
377
|
+
}
|
378
|
+
)
|
379
|
+
|
380
|
+
table.add_row(
|
381
|
+
f"[dark_magenta]{profile}[/]",
|
382
|
+
account_id,
|
383
|
+
"\n".join(anomalies),
|
384
|
+
"\n".join(stopped_list),
|
385
|
+
"\n".join(vols_list),
|
386
|
+
"\n".join(eips_list),
|
387
|
+
"\n".join(alerts),
|
388
|
+
risk_display,
|
389
|
+
)
|
390
|
+
|
391
|
+
progress.advance(task)
|
170
392
|
console.print(table)
|
171
|
-
console.print("[bold bright_cyan]Note: The dashboard only lists untagged EC2, RDS, Lambda, ELBv2.\n[/]")
|
172
393
|
|
394
|
+
# CHECK phase: Display PDCA improvement metrics
|
395
|
+
console.print("\n[bold yellow]📊 CHECK: PDCA Improvement Analysis[/]")
|
396
|
+
_display_pdca_summary(pdca_metrics)
|
397
|
+
|
398
|
+
console.print(
|
399
|
+
"[bold bright_cyan]📝 Note: Dashboard scans EC2, RDS, Lambda, ELBv2 resources across all accessible regions.\n[/]"
|
400
|
+
)
|
401
|
+
|
402
|
+
# ACT phase: Export reports with PDCA enhancements
|
173
403
|
if args.report_name: # Ensure report_name is provided for any export
|
174
404
|
if args.report_type:
|
175
405
|
for report_type in args.report_type:
|
@@ -184,59 +414,97 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
|
|
184
414
|
elif report_type == "pdf":
|
185
415
|
pdf_path = export_audit_report_to_pdf(audit_data, args.report_name, args.dir)
|
186
416
|
if pdf_path:
|
187
|
-
console.print(f"[bright_green]Successfully exported to PDF format: {pdf_path}[/]")
|
417
|
+
console.print(f"[bright_green]✅ Successfully exported to PDF format: {pdf_path}[/]")
|
418
|
+
|
419
|
+
# Generate PDCA improvement report
|
420
|
+
console.print("\n[bold cyan]🎯 ACT: Generating PDCA improvement recommendations...[/]")
|
421
|
+
pdca_path = generate_pdca_improvement_report(pdca_metrics, args.report_name, args.dir)
|
422
|
+
if pdca_path:
|
423
|
+
console.print(f"[bright_green]🚀 PDCA improvement report saved: {pdca_path}[/]")
|
188
424
|
|
189
425
|
|
190
426
|
def _run_trend_analysis(profiles_to_use: List[str], args: argparse.Namespace) -> None:
|
191
|
-
"""Analyze and display cost trends."""
|
427
|
+
"""Analyze and display cost trends with multi-profile support."""
|
192
428
|
console.print("[bold bright_cyan]Analysing cost trends...[/]")
|
429
|
+
|
430
|
+
# Display billing profile information
|
431
|
+
billing_profile = os.getenv("BILLING_PROFILE")
|
432
|
+
if billing_profile:
|
433
|
+
console.print(f"[dim cyan]Using billing profile for cost data: {billing_profile}[/]")
|
434
|
+
|
193
435
|
raw_trend_data = []
|
194
|
-
if args.combine:
|
195
|
-
account_profiles = defaultdict(list)
|
196
|
-
for profile in profiles_to_use:
|
197
|
-
try:
|
198
|
-
session = boto3.Session(profile_name=profile)
|
199
|
-
account_id = get_account_id(session)
|
200
|
-
if account_id:
|
201
|
-
account_profiles[account_id].append(profile)
|
202
|
-
except Exception as e:
|
203
|
-
console.print(f"[red]Error checking account ID for profile {profile}: {str(e)}[/]")
|
204
436
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
raw_trend_data.append(cost_data)
|
219
|
-
create_trend_bars(trend_data)
|
220
|
-
except Exception as e:
|
221
|
-
console.print(f"[red]Error getting trend for account {account_id}: {str(e)}[/]")
|
437
|
+
# Enhanced progress tracking for trend analysis
|
438
|
+
with Progress(
|
439
|
+
SpinnerColumn(),
|
440
|
+
TextColumn("[progress.description]{task.description}"),
|
441
|
+
BarColumn(),
|
442
|
+
TaskProgressColumn(),
|
443
|
+
TimeElapsedColumn(),
|
444
|
+
console=console,
|
445
|
+
transient=True,
|
446
|
+
) as progress:
|
447
|
+
if args.combine:
|
448
|
+
account_profiles = defaultdict(list)
|
449
|
+
task1 = progress.add_task("Grouping profiles by account", total=len(profiles_to_use))
|
222
450
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
451
|
+
for profile in profiles_to_use:
|
452
|
+
try:
|
453
|
+
# Use management session to get account ID
|
454
|
+
session = _create_management_session(profile)
|
455
|
+
account_id = get_account_id(session)
|
456
|
+
if account_id:
|
457
|
+
account_profiles[account_id].append(profile)
|
458
|
+
except Exception as e:
|
459
|
+
console.print(f"[red]Error checking account ID for profile {profile}: {str(e)}[/]")
|
460
|
+
progress.advance(task1)
|
461
|
+
|
462
|
+
task2 = progress.add_task("Fetching cost trends", total=len(account_profiles))
|
463
|
+
for account_id, profiles in account_profiles.items():
|
464
|
+
progress.update(task2, description=f"Fetching trends for account: {account_id}")
|
465
|
+
try:
|
466
|
+
primary_profile = profiles[0]
|
467
|
+
# Use billing session for cost trend data
|
468
|
+
cost_session = _create_cost_session(primary_profile)
|
469
|
+
cost_data = get_trend(cost_session, args.tag)
|
470
|
+
trend_data = cost_data.get("monthly_costs")
|
471
|
+
|
472
|
+
if not trend_data:
|
473
|
+
console.print(f"[yellow]No trend data available for account {account_id}[/]")
|
474
|
+
continue
|
475
|
+
|
476
|
+
profile_list = ", ".join(profiles)
|
477
|
+
console.print(f"\n[bright_yellow]Account: {account_id} (Profiles: {profile_list})[/]")
|
478
|
+
raw_trend_data.append(cost_data)
|
479
|
+
create_trend_bars(trend_data)
|
480
|
+
except Exception as e:
|
481
|
+
console.print(f"[red]Error getting trend for account {account_id}: {str(e)}[/]")
|
482
|
+
progress.advance(task2)
|
483
|
+
|
484
|
+
else:
|
485
|
+
task3 = progress.add_task("Fetching individual trends", total=len(profiles_to_use))
|
486
|
+
for profile in profiles_to_use:
|
487
|
+
progress.update(task3, description=f"Processing profile: {profile}")
|
488
|
+
try:
|
489
|
+
# Use billing session for cost data
|
490
|
+
cost_session = _create_cost_session(profile)
|
491
|
+
# Use management session for account ID
|
492
|
+
mgmt_session = _create_management_session(profile)
|
493
|
+
|
494
|
+
cost_data = get_trend(cost_session, args.tag)
|
495
|
+
trend_data = cost_data.get("monthly_costs")
|
496
|
+
account_id = get_account_id(mgmt_session) or cost_data.get("account_id", "Unknown")
|
497
|
+
|
498
|
+
if not trend_data:
|
499
|
+
console.print(f"[yellow]No trend data available for profile {profile}[/]")
|
500
|
+
continue
|
501
|
+
|
502
|
+
console.print(f"\n[bright_yellow]Account: {account_id} (Profile: {profile})[/]")
|
503
|
+
raw_trend_data.append(cost_data)
|
504
|
+
create_trend_bars(trend_data)
|
505
|
+
except Exception as e:
|
506
|
+
console.print(f"[red]Error getting trend for profile {profile}: {str(e)}[/]")
|
507
|
+
progress.advance(task3)
|
240
508
|
|
241
509
|
if raw_trend_data and args.report_name and args.report_type:
|
242
510
|
if "json" in args.report_type:
|
@@ -246,10 +514,11 @@ def _run_trend_analysis(profiles_to_use: List[str], args: argparse.Namespace) ->
|
|
246
514
|
|
247
515
|
|
248
516
|
def _get_display_table_period_info(profiles_to_use: List[str], time_range: Optional[int]) -> Tuple[str, str, str, str]:
|
249
|
-
"""Get period information for the display table."""
|
517
|
+
"""Get period information for the display table using appropriate billing profile."""
|
250
518
|
if profiles_to_use:
|
251
519
|
try:
|
252
|
-
|
520
|
+
# Use billing session for cost data period information
|
521
|
+
sample_session = _create_cost_session(profiles_to_use[0])
|
253
522
|
sample_cost_data = get_cost_data(sample_session, time_range)
|
254
523
|
previous_period_name = sample_cost_data.get("previous_period_name", "Last Month Due")
|
255
524
|
current_period_name = sample_cost_data.get("current_period_name", "Current Month Cost")
|
@@ -292,8 +561,8 @@ def create_display_table(
|
|
292
561
|
Column("Cost By Service", vertical="middle"),
|
293
562
|
Column("Budget Status", vertical="middle"),
|
294
563
|
Column("EC2 Instance Summary", justify="center", vertical="middle"),
|
295
|
-
title="
|
296
|
-
caption="
|
564
|
+
title="CloudOps Runbooks FinOps Platform",
|
565
|
+
caption="Enterprise Multi-Account Cost Optimization",
|
297
566
|
box=box.ASCII_DOUBLE_HEAD,
|
298
567
|
show_lines=True,
|
299
568
|
style="bright_cyan",
|
@@ -342,42 +611,220 @@ def _generate_dashboard_data(
|
|
342
611
|
args: argparse.Namespace,
|
343
612
|
table: Table,
|
344
613
|
) -> List[ProfileData]:
|
345
|
-
"""Fetch, process, and prepare the main dashboard data."""
|
614
|
+
"""Fetch, process, and prepare the main dashboard data with multi-profile support."""
|
346
615
|
export_data: List[ProfileData] = []
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
616
|
+
|
617
|
+
# Enhanced progress tracking with enterprise-grade progress indicators
|
618
|
+
with Progress(
|
619
|
+
SpinnerColumn(),
|
620
|
+
TextColumn("[progress.description]{task.description}"),
|
621
|
+
BarColumn(complete_style="bright_green", finished_style="bright_green"),
|
622
|
+
TaskProgressColumn(),
|
623
|
+
TimeElapsedColumn(),
|
624
|
+
console=console,
|
625
|
+
transient=False, # Keep progress visible
|
626
|
+
) as progress:
|
627
|
+
if args.combine:
|
628
|
+
account_profiles = defaultdict(list)
|
629
|
+
grouping_task = progress.add_task("Grouping profiles by account", total=len(profiles_to_use))
|
630
|
+
|
631
|
+
for profile in profiles_to_use:
|
632
|
+
progress.update(grouping_task, description=f"Checking account for profile: {profile}")
|
633
|
+
try:
|
634
|
+
# Use management session for account identification
|
635
|
+
mgmt_session = _create_management_session(profile)
|
636
|
+
current_account_id = get_account_id(mgmt_session)
|
637
|
+
if current_account_id:
|
638
|
+
account_profiles[current_account_id].append(profile)
|
639
|
+
else:
|
640
|
+
console.log(f"[yellow]Could not determine account ID for profile {profile}[/]")
|
641
|
+
except Exception as e:
|
642
|
+
console.log(f"[bold red]Error checking account ID for profile {profile}: {str(e)}[/]")
|
643
|
+
progress.advance(grouping_task)
|
644
|
+
|
645
|
+
# Process combined profiles with enhanced progress tracking
|
646
|
+
processing_task = progress.add_task("Processing account data", total=len(account_profiles))
|
647
|
+
for account_id_key, profiles_list in account_profiles.items():
|
648
|
+
progress.update(processing_task, description=f"Processing account: {account_id_key}")
|
649
|
+
|
650
|
+
if len(profiles_list) > 1:
|
651
|
+
profile_data = _process_combined_profiles_enhanced(
|
652
|
+
account_id_key, profiles_list, user_regions, time_range, args.tag
|
653
|
+
)
|
355
654
|
else:
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
#
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
)
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
else:
|
374
|
-
for profile in track(profiles_to_use, description="[bright_cyan]Fetching cost data..."):
|
375
|
-
profile_data = process_single_profile(profile, user_regions, time_range, args.tag)
|
376
|
-
export_data.append(profile_data)
|
377
|
-
add_profile_to_table(table, profile_data)
|
655
|
+
profile_data = _process_single_profile_enhanced(
|
656
|
+
profiles_list[0], user_regions, time_range, args.tag
|
657
|
+
)
|
658
|
+
export_data.append(profile_data)
|
659
|
+
add_profile_to_table(table, profile_data)
|
660
|
+
progress.advance(processing_task)
|
661
|
+
|
662
|
+
else:
|
663
|
+
# Process individual profiles with enhanced progress tracking
|
664
|
+
individual_task = progress.add_task("Processing individual profiles", total=len(profiles_to_use))
|
665
|
+
for profile in profiles_to_use:
|
666
|
+
progress.update(individual_task, description=f"Processing profile: {profile}")
|
667
|
+
profile_data = _process_single_profile_enhanced(profile, user_regions, time_range, args.tag)
|
668
|
+
export_data.append(profile_data)
|
669
|
+
add_profile_to_table(table, profile_data)
|
670
|
+
progress.advance(individual_task)
|
671
|
+
|
378
672
|
return export_data
|
379
673
|
|
380
674
|
|
675
|
+
def _process_single_profile_enhanced(
|
676
|
+
profile: str,
|
677
|
+
user_regions: Optional[List[str]] = None,
|
678
|
+
time_range: Optional[int] = None,
|
679
|
+
tag: Optional[List[str]] = None,
|
680
|
+
) -> ProfileData:
|
681
|
+
"""
|
682
|
+
Enhanced single profile processing with multi-profile session support.
|
683
|
+
Uses appropriate sessions for different operations: billing, management, operational.
|
684
|
+
"""
|
685
|
+
try:
|
686
|
+
# Use billing session for cost data
|
687
|
+
cost_session = _create_cost_session(profile)
|
688
|
+
cost_data = get_cost_data(cost_session, time_range, tag)
|
689
|
+
|
690
|
+
# Use operational session for EC2 and resource operations
|
691
|
+
ops_session = _create_operational_session(profile)
|
692
|
+
|
693
|
+
if user_regions:
|
694
|
+
profile_regions = user_regions
|
695
|
+
else:
|
696
|
+
profile_regions = get_accessible_regions(ops_session)
|
697
|
+
|
698
|
+
ec2_data = ec2_summary(ops_session, profile_regions)
|
699
|
+
service_costs, service_cost_data = process_service_costs(cost_data)
|
700
|
+
budget_info = format_budget_info(cost_data["budgets"])
|
701
|
+
account_id = cost_data.get("account_id", "Unknown") or "Unknown"
|
702
|
+
ec2_summary_text = format_ec2_summary(ec2_data)
|
703
|
+
percent_change_in_total_cost = change_in_total_cost(cost_data["current_month"], cost_data["last_month"])
|
704
|
+
|
705
|
+
return {
|
706
|
+
"profile": profile,
|
707
|
+
"account_id": account_id,
|
708
|
+
"last_month": cost_data["last_month"],
|
709
|
+
"current_month": cost_data["current_month"],
|
710
|
+
"service_costs": service_cost_data,
|
711
|
+
"service_costs_formatted": service_costs,
|
712
|
+
"budget_info": budget_info,
|
713
|
+
"ec2_summary": ec2_data,
|
714
|
+
"ec2_summary_formatted": ec2_summary_text,
|
715
|
+
"success": True,
|
716
|
+
"error": None,
|
717
|
+
"current_period_name": cost_data["current_period_name"],
|
718
|
+
"previous_period_name": cost_data["previous_period_name"],
|
719
|
+
"percent_change_in_total_cost": percent_change_in_total_cost,
|
720
|
+
}
|
721
|
+
|
722
|
+
except Exception as e:
|
723
|
+
console.log(f"[red]Error processing profile {profile}: {str(e)}[/]")
|
724
|
+
return {
|
725
|
+
"profile": profile,
|
726
|
+
"account_id": "Error",
|
727
|
+
"last_month": 0,
|
728
|
+
"current_month": 0,
|
729
|
+
"service_costs": [],
|
730
|
+
"service_costs_formatted": [f"Failed to process profile: {str(e)}"],
|
731
|
+
"budget_info": ["N/A"],
|
732
|
+
"ec2_summary": {"N/A": 0},
|
733
|
+
"ec2_summary_formatted": ["Error"],
|
734
|
+
"success": False,
|
735
|
+
"error": str(e),
|
736
|
+
"current_period_name": "Current month",
|
737
|
+
"previous_period_name": "Last month",
|
738
|
+
"percent_change_in_total_cost": None,
|
739
|
+
}
|
740
|
+
|
741
|
+
|
742
|
+
def _process_combined_profiles_enhanced(
|
743
|
+
account_id: str,
|
744
|
+
profiles: List[str],
|
745
|
+
user_regions: Optional[List[str]] = None,
|
746
|
+
time_range: Optional[int] = None,
|
747
|
+
tag: Optional[List[str]] = None,
|
748
|
+
) -> ProfileData:
|
749
|
+
"""
|
750
|
+
Enhanced combined profile processing with multi-profile session support.
|
751
|
+
Aggregates data from multiple profiles in the same AWS account.
|
752
|
+
"""
|
753
|
+
try:
|
754
|
+
primary_profile = profiles[0]
|
755
|
+
|
756
|
+
# Use billing session for cost data aggregation
|
757
|
+
primary_cost_session = _create_cost_session(primary_profile)
|
758
|
+
# Use operational session for resource data
|
759
|
+
primary_ops_session = _create_operational_session(primary_profile)
|
760
|
+
|
761
|
+
# Get cost data using billing session
|
762
|
+
account_cost_data = get_cost_data(primary_cost_session, time_range, tag)
|
763
|
+
|
764
|
+
if user_regions:
|
765
|
+
profile_regions = user_regions
|
766
|
+
else:
|
767
|
+
profile_regions = get_accessible_regions(primary_ops_session)
|
768
|
+
|
769
|
+
# Aggregate EC2 data from all profiles using operational sessions
|
770
|
+
combined_ec2_data = defaultdict(int)
|
771
|
+
for profile in profiles:
|
772
|
+
try:
|
773
|
+
profile_ops_session = _create_operational_session(profile)
|
774
|
+
profile_ec2_data = ec2_summary(profile_ops_session, profile_regions)
|
775
|
+
for instance_type, count in profile_ec2_data.items():
|
776
|
+
combined_ec2_data[instance_type] += count
|
777
|
+
except Exception as e:
|
778
|
+
console.log(f"[yellow]Warning: Could not get EC2 data for profile {profile}: {str(e)}[/]")
|
779
|
+
|
780
|
+
service_costs, service_cost_data = process_service_costs(account_cost_data)
|
781
|
+
budget_info = format_budget_info(account_cost_data["budgets"])
|
782
|
+
ec2_summary_text = format_ec2_summary(dict(combined_ec2_data))
|
783
|
+
percent_change_in_total_cost = change_in_total_cost(
|
784
|
+
account_cost_data["current_month"], account_cost_data["last_month"]
|
785
|
+
)
|
786
|
+
|
787
|
+
profile_list = ", ".join(profiles)
|
788
|
+
console.log(f"[dim cyan]Combined {len(profiles)} profiles for account {account_id}: {profile_list}[/]")
|
789
|
+
|
790
|
+
return {
|
791
|
+
"profile": f"Combined ({profile_list})",
|
792
|
+
"account_id": account_id,
|
793
|
+
"last_month": account_cost_data["last_month"],
|
794
|
+
"current_month": account_cost_data["current_month"],
|
795
|
+
"service_costs": service_cost_data,
|
796
|
+
"service_costs_formatted": service_costs,
|
797
|
+
"budget_info": budget_info,
|
798
|
+
"ec2_summary": dict(combined_ec2_data),
|
799
|
+
"ec2_summary_formatted": ec2_summary_text,
|
800
|
+
"success": True,
|
801
|
+
"error": None,
|
802
|
+
"current_period_name": account_cost_data["current_period_name"],
|
803
|
+
"previous_period_name": account_cost_data["previous_period_name"],
|
804
|
+
"percent_change_in_total_cost": percent_change_in_total_cost,
|
805
|
+
}
|
806
|
+
|
807
|
+
except Exception as e:
|
808
|
+
console.log(f"[red]Error processing combined profiles for account {account_id}: {str(e)}[/]")
|
809
|
+
profile_list = ", ".join(profiles)
|
810
|
+
return {
|
811
|
+
"profile": f"Combined ({profile_list})",
|
812
|
+
"account_id": account_id,
|
813
|
+
"last_month": 0,
|
814
|
+
"current_month": 0,
|
815
|
+
"service_costs": [],
|
816
|
+
"service_costs_formatted": [f"Failed to process combined profiles: {str(e)}"],
|
817
|
+
"budget_info": ["N/A"],
|
818
|
+
"ec2_summary": {"N/A": 0},
|
819
|
+
"ec2_summary_formatted": ["Error"],
|
820
|
+
"success": False,
|
821
|
+
"error": str(e),
|
822
|
+
"current_period_name": "Current month",
|
823
|
+
"previous_period_name": "Last month",
|
824
|
+
"percent_change_in_total_cost": None,
|
825
|
+
}
|
826
|
+
|
827
|
+
|
381
828
|
def _export_dashboard_reports(
|
382
829
|
export_data: List[ProfileData],
|
383
830
|
args: argparse.Namespace,
|
@@ -414,10 +861,34 @@ def _export_dashboard_reports(
|
|
414
861
|
|
415
862
|
|
416
863
|
def run_dashboard(args: argparse.Namespace) -> int:
|
417
|
-
"""Main function to run the
|
864
|
+
"""Main function to run the CloudOps Runbooks FinOps Platform with multi-profile support."""
|
418
865
|
with Status("[bright_cyan]Initialising...", spinner="aesthetic", speed=0.4):
|
419
866
|
profiles_to_use, user_regions, time_range = _initialize_profiles(args)
|
420
867
|
|
868
|
+
# Display multi-profile configuration at startup
|
869
|
+
billing_profile = os.getenv("BILLING_PROFILE")
|
870
|
+
mgmt_profile = os.getenv("MANAGEMENT_PROFILE")
|
871
|
+
ops_profile = os.getenv("CENTRALISED_OPS_PROFILE")
|
872
|
+
|
873
|
+
if any([billing_profile, mgmt_profile, ops_profile]):
|
874
|
+
console.print("\n[bold bright_cyan]🔧 Multi-Profile Configuration Detected[/]")
|
875
|
+
config_table = Table(
|
876
|
+
title="Profile Configuration", show_header=True, header_style="bold cyan", box=box.SIMPLE, style="dim"
|
877
|
+
)
|
878
|
+
config_table.add_column("Operation Type", style="bold")
|
879
|
+
config_table.add_column("Profile", style="bright_cyan")
|
880
|
+
config_table.add_column("Purpose", style="dim")
|
881
|
+
|
882
|
+
if billing_profile:
|
883
|
+
config_table.add_row("💰 Billing", billing_profile, "Cost Explorer & Budget API access")
|
884
|
+
if mgmt_profile:
|
885
|
+
config_table.add_row("🏛️ Management", mgmt_profile, "Account ID & Organizations operations")
|
886
|
+
if ops_profile:
|
887
|
+
config_table.add_row("⚙️ Operational", ops_profile, "EC2, S3, and resource discovery")
|
888
|
+
|
889
|
+
console.print(config_table)
|
890
|
+
console.print("[dim]Fallback: Using profile-specific sessions when env vars not set[/]\n")
|
891
|
+
|
421
892
|
if args.audit:
|
422
893
|
_run_audit_report(profiles_to_use, args)
|
423
894
|
return 0
|
@@ -446,3 +917,180 @@ def run_dashboard(args: argparse.Namespace) -> int:
|
|
446
917
|
_export_dashboard_reports(export_data, args, previous_period_dates, current_period_dates)
|
447
918
|
|
448
919
|
return 0
|
920
|
+
|
921
|
+
|
922
|
+
def _run_cost_trend_analysis(profiles: List[str], args: argparse.Namespace) -> Dict[str, Any]:
|
923
|
+
"""
|
924
|
+
Run cost trend analysis across multiple accounts.
|
925
|
+
|
926
|
+
Args:
|
927
|
+
profiles: List of AWS profiles to analyze
|
928
|
+
args: Command line arguments
|
929
|
+
|
930
|
+
Returns:
|
931
|
+
Dict containing cost trend analysis results
|
932
|
+
"""
|
933
|
+
try:
|
934
|
+
# Import the new dashboard module
|
935
|
+
from runbooks.finops.finops_dashboard import FinOpsConfig, MultiAccountCostTrendAnalyzer
|
936
|
+
|
937
|
+
# Create configuration
|
938
|
+
config = FinOpsConfig()
|
939
|
+
config.dry_run = not args.live_mode if hasattr(args, "live_mode") else True
|
940
|
+
|
941
|
+
# Run cost trend analysis
|
942
|
+
analyzer = MultiAccountCostTrendAnalyzer(config)
|
943
|
+
results = analyzer.analyze_cost_trends()
|
944
|
+
|
945
|
+
console.log(f"[green]✅ Cost trend analysis completed for {len(profiles)} profiles[/]")
|
946
|
+
|
947
|
+
if results.get("status") == "completed":
|
948
|
+
cost_data = results["cost_trends"]
|
949
|
+
optimization = results["optimization_opportunities"]
|
950
|
+
|
951
|
+
console.log(f"[cyan]📊 Analyzed {cost_data['total_accounts']} accounts[/]")
|
952
|
+
console.log(f"[cyan]💰 Total monthly spend: ${cost_data['total_monthly_spend']:,.2f}[/]")
|
953
|
+
console.log(f"[cyan]🎯 Potential savings: {optimization['savings_percentage']:.1f}%[/]")
|
954
|
+
|
955
|
+
return results
|
956
|
+
|
957
|
+
except Exception as e:
|
958
|
+
console.log(f"[red]❌ Cost trend analysis failed: {e}[/]")
|
959
|
+
return {"status": "error", "error": str(e)}
|
960
|
+
|
961
|
+
|
962
|
+
def _run_resource_heatmap_analysis(
|
963
|
+
profiles: List[str], cost_data: Dict[str, Any], args: argparse.Namespace
|
964
|
+
) -> Dict[str, Any]:
|
965
|
+
"""
|
966
|
+
Run resource utilization heatmap analysis.
|
967
|
+
|
968
|
+
Args:
|
969
|
+
profiles: List of AWS profiles to analyze
|
970
|
+
cost_data: Cost analysis data from previous step
|
971
|
+
args: Command line arguments
|
972
|
+
|
973
|
+
Returns:
|
974
|
+
Dict containing resource heatmap analysis results
|
975
|
+
"""
|
976
|
+
try:
|
977
|
+
# Import the new dashboard module
|
978
|
+
from runbooks.finops.finops_dashboard import FinOpsConfig, ResourceUtilizationHeatmapAnalyzer
|
979
|
+
|
980
|
+
# Create configuration
|
981
|
+
config = FinOpsConfig()
|
982
|
+
config.dry_run = not args.live_mode if hasattr(args, "live_mode") else True
|
983
|
+
|
984
|
+
# Run heatmap analysis
|
985
|
+
analyzer = ResourceUtilizationHeatmapAnalyzer(config, cost_data)
|
986
|
+
results = analyzer.analyze_resource_utilization()
|
987
|
+
|
988
|
+
console.log(f"[green]✅ Resource heatmap analysis completed[/]")
|
989
|
+
|
990
|
+
if results.get("status") == "completed":
|
991
|
+
heatmap_data = results["heatmap_data"]
|
992
|
+
efficiency = results["efficiency_scoring"]
|
993
|
+
|
994
|
+
console.log(f"[cyan]🔥 Analyzed {heatmap_data['total_resources']:,} resources[/]")
|
995
|
+
console.log(f"[cyan]⚡ Average efficiency: {efficiency['average_efficiency_score']:.1f}%[/]")
|
996
|
+
|
997
|
+
return results
|
998
|
+
|
999
|
+
except Exception as e:
|
1000
|
+
console.log(f"[red]❌ Resource heatmap analysis failed: {e}[/]")
|
1001
|
+
return {"status": "error", "error": str(e)}
|
1002
|
+
|
1003
|
+
|
1004
|
+
def _run_executive_dashboard(
|
1005
|
+
discovery_results: Dict[str, Any],
|
1006
|
+
cost_analysis: Dict[str, Any],
|
1007
|
+
audit_results: Dict[str, Any],
|
1008
|
+
args: argparse.Namespace,
|
1009
|
+
) -> Dict[str, Any]:
|
1010
|
+
"""
|
1011
|
+
Generate executive dashboard summary.
|
1012
|
+
|
1013
|
+
Args:
|
1014
|
+
discovery_results: Account discovery results
|
1015
|
+
cost_analysis: Cost analysis results
|
1016
|
+
audit_results: Audit results
|
1017
|
+
args: Command line arguments
|
1018
|
+
|
1019
|
+
Returns:
|
1020
|
+
Dict containing executive dashboard results
|
1021
|
+
"""
|
1022
|
+
try:
|
1023
|
+
# Import the new dashboard module
|
1024
|
+
from runbooks.finops.finops_dashboard import EnterpriseExecutiveDashboard, FinOpsConfig
|
1025
|
+
|
1026
|
+
# Create configuration
|
1027
|
+
config = FinOpsConfig()
|
1028
|
+
config.dry_run = not args.live_mode if hasattr(args, "live_mode") else True
|
1029
|
+
|
1030
|
+
# Generate executive dashboard
|
1031
|
+
dashboard = EnterpriseExecutiveDashboard(config, discovery_results, cost_analysis, audit_results)
|
1032
|
+
results = dashboard.generate_executive_summary()
|
1033
|
+
|
1034
|
+
console.log(f"[green]✅ Executive dashboard generated[/]")
|
1035
|
+
|
1036
|
+
# Display key metrics
|
1037
|
+
if "financial_overview" in results:
|
1038
|
+
fin = results["financial_overview"]
|
1039
|
+
status_icon = "✅" if fin["target_achieved"] else "⚠️"
|
1040
|
+
console.log(f"[cyan]💰 Monthly spend: ${fin['current_monthly_spend']:,.2f}[/]")
|
1041
|
+
console.log(f"[cyan]🎯 Target status: {status_icon}[/]")
|
1042
|
+
|
1043
|
+
return results
|
1044
|
+
|
1045
|
+
except Exception as e:
|
1046
|
+
console.log(f"[red]❌ Executive dashboard generation failed: {e}[/]")
|
1047
|
+
return {"status": "error", "error": str(e)}
|
1048
|
+
|
1049
|
+
|
1050
|
+
def run_complete_finops_workflow(profiles: List[str], args: argparse.Namespace) -> Dict[str, Any]:
|
1051
|
+
"""
|
1052
|
+
Run the complete FinOps analysis workflow.
|
1053
|
+
|
1054
|
+
Args:
|
1055
|
+
profiles: List of AWS profiles to analyze
|
1056
|
+
args: Command line arguments
|
1057
|
+
|
1058
|
+
Returns:
|
1059
|
+
Dict containing complete analysis results
|
1060
|
+
"""
|
1061
|
+
try:
|
1062
|
+
# Import the new dashboard module
|
1063
|
+
from runbooks.finops.finops_dashboard import FinOpsConfig, run_complete_finops_analysis
|
1064
|
+
|
1065
|
+
console.log("[blue]🚀 Starting complete FinOps analysis workflow...[/]")
|
1066
|
+
|
1067
|
+
# Create configuration from args
|
1068
|
+
config = FinOpsConfig()
|
1069
|
+
config.dry_run = not args.live_mode if hasattr(args, "live_mode") else True
|
1070
|
+
|
1071
|
+
# Run complete analysis
|
1072
|
+
results = run_complete_finops_analysis(config)
|
1073
|
+
|
1074
|
+
console.log("[green]✅ Complete FinOps workflow completed successfully[/]")
|
1075
|
+
|
1076
|
+
# Display summary
|
1077
|
+
if results.get("workflow_status") == "completed":
|
1078
|
+
if "cost_analysis" in results and results["cost_analysis"].get("status") == "completed":
|
1079
|
+
cost_data = results["cost_analysis"]["cost_trends"]
|
1080
|
+
optimization = results["cost_analysis"]["optimization_opportunities"]
|
1081
|
+
|
1082
|
+
console.log(f"[cyan]📊 Analyzed {cost_data['total_accounts']} accounts[/]")
|
1083
|
+
console.log(f"[cyan]💰 Monthly spend: ${cost_data['total_monthly_spend']:,.2f}[/]")
|
1084
|
+
console.log(f"[cyan]🎯 Potential savings: {optimization['savings_percentage']:.1f}%[/]")
|
1085
|
+
console.log(f"[cyan]💵 Annual impact: ${optimization['annual_savings_potential']:,.2f}[/]")
|
1086
|
+
|
1087
|
+
if "export_status" in results:
|
1088
|
+
successful = len(results["export_status"]["successful_exports"])
|
1089
|
+
failed = len(results["export_status"]["failed_exports"])
|
1090
|
+
console.log(f"[cyan]📄 Exports: {successful} successful, {failed} failed[/]")
|
1091
|
+
|
1092
|
+
return results
|
1093
|
+
|
1094
|
+
except Exception as e:
|
1095
|
+
console.log(f"[red]❌ Complete FinOps workflow failed: {e}[/]")
|
1096
|
+
return {"status": "error", "error": str(e)}
|