runbooks 0.7.7__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 +2 -2
- runbooks/cfat/__init__.py +8 -4
- runbooks/cfat/assessment/collectors.py +171 -14
- runbooks/cfat/assessment/compliance.py +546 -522
- 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 +31 -28
- runbooks/finops/cross_validation.py +375 -0
- runbooks/finops/dashboard_runner.py +384 -207
- runbooks/finops/enhanced_dashboard_runner.py +525 -0
- runbooks/finops/finops_dashboard.py +1892 -0
- runbooks/finops/helpers.py +176 -173
- runbooks/finops/optimizer.py +384 -383
- 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 +192 -185
- runbooks/inventory/collectors/enterprise_scale.py +281 -0
- runbooks/inventory/core/collector.py +172 -13
- 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 +2124 -174
- 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.7.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
- {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/RECORD +110 -52
- {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
- {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
- {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.7.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -6,8 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
6
6
|
import boto3
|
7
7
|
from rich import box
|
8
8
|
from rich.console import Console
|
9
|
-
from rich.progress import Progress, SpinnerColumn,
|
10
|
-
from rich.progress import track
|
9
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn, TimeElapsedColumn, track
|
11
10
|
from rich.status import Status
|
12
11
|
from rich.table import Column, Table
|
13
12
|
|
@@ -23,13 +22,13 @@ from runbooks.finops.aws_client import (
|
|
23
22
|
get_unused_volumes,
|
24
23
|
)
|
25
24
|
from runbooks.finops.cost_processor import (
|
25
|
+
change_in_total_cost,
|
26
26
|
export_to_csv,
|
27
27
|
export_to_json,
|
28
|
-
get_cost_data,
|
29
|
-
get_trend,
|
30
|
-
change_in_total_cost,
|
31
28
|
format_budget_info,
|
32
29
|
format_ec2_summary,
|
30
|
+
get_cost_data,
|
31
|
+
get_trend,
|
33
32
|
process_service_costs,
|
34
33
|
)
|
35
34
|
from runbooks.finops.helpers import (
|
@@ -54,20 +53,20 @@ console = Console()
|
|
54
53
|
def _get_profile_for_operation(operation_type: str, default_profile: str) -> str:
|
55
54
|
"""
|
56
55
|
Get the appropriate AWS profile based on operation type.
|
57
|
-
|
56
|
+
|
58
57
|
Args:
|
59
58
|
operation_type: Type of operation ('billing', 'management', 'operational')
|
60
59
|
default_profile: Default profile to fall back to
|
61
|
-
|
60
|
+
|
62
61
|
Returns:
|
63
62
|
str: Profile name to use for the operation
|
64
63
|
"""
|
65
64
|
profile_map = {
|
66
|
-
|
67
|
-
|
68
|
-
|
65
|
+
"billing": os.getenv("BILLING_PROFILE"),
|
66
|
+
"management": os.getenv("MANAGEMENT_PROFILE"),
|
67
|
+
"operational": os.getenv("CENTRALISED_OPS_PROFILE"),
|
69
68
|
}
|
70
|
-
|
69
|
+
|
71
70
|
profile = profile_map.get(operation_type)
|
72
71
|
if profile:
|
73
72
|
# Verify profile exists
|
@@ -76,8 +75,10 @@ def _get_profile_for_operation(operation_type: str, default_profile: str) -> str
|
|
76
75
|
console.log(f"[dim cyan]Using {operation_type} profile: {profile}[/]")
|
77
76
|
return profile
|
78
77
|
else:
|
79
|
-
console.log(
|
80
|
-
|
78
|
+
console.log(
|
79
|
+
f"[yellow]Warning: {operation_type.title()} profile '{profile}' not found in AWS config. Using default: {default_profile}[/]"
|
80
|
+
)
|
81
|
+
|
81
82
|
return default_profile
|
82
83
|
|
83
84
|
|
@@ -85,14 +86,14 @@ def _create_cost_session(profile: str) -> boto3.Session:
|
|
85
86
|
"""
|
86
87
|
Create a boto3 session specifically for cost operations.
|
87
88
|
Uses BILLING_PROFILE if available, falls back to provided profile.
|
88
|
-
|
89
|
+
|
89
90
|
Args:
|
90
91
|
profile: Default profile to use
|
91
|
-
|
92
|
+
|
92
93
|
Returns:
|
93
94
|
boto3.Session: Session configured for cost operations
|
94
95
|
"""
|
95
|
-
cost_profile = _get_profile_for_operation(
|
96
|
+
cost_profile = _get_profile_for_operation("billing", profile)
|
96
97
|
return boto3.Session(profile_name=cost_profile)
|
97
98
|
|
98
99
|
|
@@ -100,14 +101,14 @@ def _create_management_session(profile: str) -> boto3.Session:
|
|
100
101
|
"""
|
101
102
|
Create a boto3 session specifically for management operations.
|
102
103
|
Uses MANAGEMENT_PROFILE if available, falls back to provided profile.
|
103
|
-
|
104
|
+
|
104
105
|
Args:
|
105
106
|
profile: Default profile to use
|
106
|
-
|
107
|
+
|
107
108
|
Returns:
|
108
|
-
boto3.Session: Session configured for management operations
|
109
|
+
boto3.Session: Session configured for management operations
|
109
110
|
"""
|
110
|
-
mgmt_profile = _get_profile_for_operation(
|
111
|
+
mgmt_profile = _get_profile_for_operation("management", profile)
|
111
112
|
return boto3.Session(profile_name=mgmt_profile)
|
112
113
|
|
113
114
|
|
@@ -115,41 +116,41 @@ def _create_operational_session(profile: str) -> boto3.Session:
|
|
115
116
|
"""
|
116
117
|
Create a boto3 session specifically for operational tasks.
|
117
118
|
Uses CENTRALISED_OPS_PROFILE if available, falls back to provided profile.
|
118
|
-
|
119
|
+
|
119
120
|
Args:
|
120
121
|
profile: Default profile to use
|
121
|
-
|
122
|
+
|
122
123
|
Returns:
|
123
124
|
boto3.Session: Session configured for operational tasks
|
124
125
|
"""
|
125
|
-
ops_profile = _get_profile_for_operation(
|
126
|
+
ops_profile = _get_profile_for_operation("operational", profile)
|
126
127
|
return boto3.Session(profile_name=ops_profile)
|
127
128
|
|
128
129
|
|
129
130
|
def _calculate_risk_score(untagged, stopped, unused_vols, unused_eips, budget_data):
|
130
131
|
"""Calculate risk score based on audit findings for PDCA tracking."""
|
131
132
|
score = 0
|
132
|
-
|
133
|
+
|
133
134
|
# Untagged resources (high risk for compliance)
|
134
135
|
untagged_count = sum(len(ids) for region_map in untagged.values() for ids in region_map.values())
|
135
136
|
score += untagged_count * 2 # High weight for untagged
|
136
|
-
|
137
|
+
|
137
138
|
# Stopped instances (medium risk for cost)
|
138
139
|
stopped_count = sum(len(ids) for ids in stopped.values())
|
139
140
|
score += stopped_count * 1
|
140
|
-
|
141
|
+
|
141
142
|
# Unused volumes (medium risk for cost)
|
142
143
|
volume_count = sum(len(ids) for ids in unused_vols.values())
|
143
144
|
score += volume_count * 1
|
144
|
-
|
145
|
+
|
145
146
|
# Unused EIPs (high risk for cost)
|
146
147
|
eip_count = sum(len(ids) for ids in unused_eips.values())
|
147
148
|
score += eip_count * 3 # High cost impact
|
148
|
-
|
149
|
+
|
149
150
|
# Budget overruns (critical risk)
|
150
151
|
overruns = len([b for b in budget_data if b["actual"] > b["limit"]])
|
151
152
|
score += overruns * 5 # Critical weight
|
152
|
-
|
153
|
+
|
153
154
|
return score
|
154
155
|
|
155
156
|
|
@@ -169,32 +170,30 @@ def _display_pdca_summary(pdca_metrics):
|
|
169
170
|
"""Display PDCA improvement summary with actionable insights."""
|
170
171
|
if not pdca_metrics:
|
171
172
|
return
|
172
|
-
|
173
|
+
|
173
174
|
total_risk = sum(m["risk_score"] for m in pdca_metrics)
|
174
175
|
avg_risk = total_risk / len(pdca_metrics)
|
175
|
-
|
176
|
+
|
176
177
|
high_risk_accounts = [m for m in pdca_metrics if m["risk_score"] > 25]
|
177
178
|
total_untagged = sum(m["untagged_count"] for m in pdca_metrics)
|
178
179
|
total_unused_eips = sum(m["unused_eips_count"] for m in pdca_metrics)
|
179
|
-
|
180
|
-
summary_table = Table(
|
181
|
-
title="🎯 PDCA Continuous Improvement Metrics",
|
182
|
-
box=box.SIMPLE,
|
183
|
-
style="cyan"
|
184
|
-
)
|
180
|
+
|
181
|
+
summary_table = Table(title="🎯 PDCA Continuous Improvement Metrics", box=box.SIMPLE, style="cyan")
|
185
182
|
summary_table.add_column("Metric", style="bold")
|
186
183
|
summary_table.add_column("Value", justify="right")
|
187
184
|
summary_table.add_column("Action Required", style="yellow")
|
188
|
-
|
189
|
-
summary_table.add_row("Average Risk Score", f"{avg_risk:.1f}",
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
summary_table.add_row(
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
+
|
198
197
|
console.print(summary_table)
|
199
198
|
|
200
199
|
|
@@ -232,12 +231,12 @@ def _initialize_profiles(
|
|
232
231
|
def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> None:
|
233
232
|
"""Generate and export an audit report with PDCA continuous improvement."""
|
234
233
|
console.print("[bold bright_cyan]🔍 PLAN: Preparing comprehensive audit report...[/]")
|
235
|
-
|
234
|
+
|
236
235
|
# Display multi-profile configuration
|
237
|
-
billing_profile = os.getenv(
|
238
|
-
mgmt_profile = os.getenv(
|
239
|
-
ops_profile = os.getenv(
|
240
|
-
|
236
|
+
billing_profile = os.getenv("BILLING_PROFILE")
|
237
|
+
mgmt_profile = os.getenv("MANAGEMENT_PROFILE")
|
238
|
+
ops_profile = os.getenv("CENTRALISED_OPS_PROFILE")
|
239
|
+
|
241
240
|
if any([billing_profile, mgmt_profile, ops_profile]):
|
242
241
|
console.print("[dim cyan]Multi-profile configuration detected:[/]")
|
243
242
|
if billing_profile:
|
@@ -247,7 +246,7 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
|
|
247
246
|
if ops_profile:
|
248
247
|
console.print(f"[dim cyan] • Operational tasks: {ops_profile}[/]")
|
249
248
|
console.print()
|
250
|
-
|
249
|
+
|
251
250
|
# Enhanced table with better visual hierarchy
|
252
251
|
table = Table(
|
253
252
|
Column("Profile", justify="center", style="bold magenta"),
|
@@ -272,8 +271,8 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
|
|
272
271
|
comma_nl = ",\n"
|
273
272
|
|
274
273
|
console.print("[bold green]⚙️ DO: Collecting audit data across profiles...[/]")
|
275
|
-
|
276
|
-
# Create progress tracker for enhanced user experience
|
274
|
+
|
275
|
+
# Create progress tracker for enhanced user experience
|
277
276
|
with Progress(
|
278
277
|
SpinnerColumn(),
|
279
278
|
TextColumn("[progress.description]{task.description}"),
|
@@ -281,20 +280,20 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
|
|
281
280
|
TaskProgressColumn(),
|
282
281
|
TimeElapsedColumn(),
|
283
282
|
console=console,
|
284
|
-
transient=True
|
283
|
+
transient=True,
|
285
284
|
) as progress:
|
286
285
|
task = progress.add_task("Collecting audit data", total=len(profiles_to_use))
|
287
|
-
|
286
|
+
|
288
287
|
for profile in profiles_to_use:
|
289
288
|
progress.update(task, description=f"Processing profile: {profile}")
|
290
|
-
|
289
|
+
|
291
290
|
# Use operational session for resource discovery
|
292
291
|
ops_session = _create_operational_session(profile)
|
293
292
|
# Use management session for account and governance operations
|
294
293
|
mgmt_session = _create_management_session(profile)
|
295
294
|
# Use billing session for cost and budget operations
|
296
295
|
billing_session = _create_cost_session(profile)
|
297
|
-
|
296
|
+
|
298
297
|
account_id = get_account_id(mgmt_session) or "Unknown"
|
299
298
|
regions = args.regions or get_accessible_regions(ops_session)
|
300
299
|
|
@@ -337,18 +336,20 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
|
|
337
336
|
# Calculate risk score for PDCA improvement tracking
|
338
337
|
risk_score = _calculate_risk_score(untagged, stopped, unused_vols, unused_eips, budget_data)
|
339
338
|
risk_display = _format_risk_score(risk_score)
|
340
|
-
|
339
|
+
|
341
340
|
# Track PDCA metrics
|
342
|
-
pdca_metrics.append(
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
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
|
+
)
|
352
353
|
|
353
354
|
audit_data.append(
|
354
355
|
{
|
@@ -386,15 +387,17 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
|
|
386
387
|
"\n".join(alerts),
|
387
388
|
risk_display,
|
388
389
|
)
|
389
|
-
|
390
|
+
|
390
391
|
progress.advance(task)
|
391
392
|
console.print(table)
|
392
|
-
|
393
|
+
|
393
394
|
# CHECK phase: Display PDCA improvement metrics
|
394
395
|
console.print("\n[bold yellow]📊 CHECK: PDCA Improvement Analysis[/]")
|
395
396
|
_display_pdca_summary(pdca_metrics)
|
396
|
-
|
397
|
-
console.print(
|
397
|
+
|
398
|
+
console.print(
|
399
|
+
"[bold bright_cyan]📝 Note: Dashboard scans EC2, RDS, Lambda, ELBv2 resources across all accessible regions.\n[/]"
|
400
|
+
)
|
398
401
|
|
399
402
|
# ACT phase: Export reports with PDCA enhancements
|
400
403
|
if args.report_name: # Ensure report_name is provided for any export
|
@@ -412,7 +415,7 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
|
|
412
415
|
pdf_path = export_audit_report_to_pdf(audit_data, args.report_name, args.dir)
|
413
416
|
if pdf_path:
|
414
417
|
console.print(f"[bright_green]✅ Successfully exported to PDF format: {pdf_path}[/]")
|
415
|
-
|
418
|
+
|
416
419
|
# Generate PDCA improvement report
|
417
420
|
console.print("\n[bold cyan]🎯 ACT: Generating PDCA improvement recommendations...[/]")
|
418
421
|
pdca_path = generate_pdca_improvement_report(pdca_metrics, args.report_name, args.dir)
|
@@ -423,14 +426,14 @@ def _run_audit_report(profiles_to_use: List[str], args: argparse.Namespace) -> N
|
|
423
426
|
def _run_trend_analysis(profiles_to_use: List[str], args: argparse.Namespace) -> None:
|
424
427
|
"""Analyze and display cost trends with multi-profile support."""
|
425
428
|
console.print("[bold bright_cyan]Analysing cost trends...[/]")
|
426
|
-
|
429
|
+
|
427
430
|
# Display billing profile information
|
428
|
-
billing_profile = os.getenv(
|
431
|
+
billing_profile = os.getenv("BILLING_PROFILE")
|
429
432
|
if billing_profile:
|
430
433
|
console.print(f"[dim cyan]Using billing profile for cost data: {billing_profile}[/]")
|
431
|
-
|
434
|
+
|
432
435
|
raw_trend_data = []
|
433
|
-
|
436
|
+
|
434
437
|
# Enhanced progress tracking for trend analysis
|
435
438
|
with Progress(
|
436
439
|
SpinnerColumn(),
|
@@ -439,69 +442,69 @@ def _run_trend_analysis(profiles_to_use: List[str], args: argparse.Namespace) ->
|
|
439
442
|
TaskProgressColumn(),
|
440
443
|
TimeElapsedColumn(),
|
441
444
|
console=console,
|
442
|
-
transient=True
|
445
|
+
transient=True,
|
443
446
|
) as progress:
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
447
|
+
if args.combine:
|
448
|
+
account_profiles = defaultdict(list)
|
449
|
+
task1 = progress.add_task("Grouping profiles by account", total=len(profiles_to_use))
|
450
|
+
|
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)
|
480
483
|
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
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)
|
505
508
|
|
506
509
|
if raw_trend_data and args.report_name and args.report_type:
|
507
510
|
if "json" in args.report_type:
|
@@ -558,8 +561,8 @@ def create_display_table(
|
|
558
561
|
Column("Cost By Service", vertical="middle"),
|
559
562
|
Column("Budget Status", vertical="middle"),
|
560
563
|
Column("EC2 Instance Summary", justify="center", vertical="middle"),
|
561
|
-
title="
|
562
|
-
caption="
|
564
|
+
title="CloudOps Runbooks FinOps Platform",
|
565
|
+
caption="Enterprise Multi-Account Cost Optimization",
|
563
566
|
box=box.ASCII_DOUBLE_HEAD,
|
564
567
|
show_lines=True,
|
565
568
|
style="bright_cyan",
|
@@ -610,8 +613,8 @@ def _generate_dashboard_data(
|
|
610
613
|
) -> List[ProfileData]:
|
611
614
|
"""Fetch, process, and prepare the main dashboard data with multi-profile support."""
|
612
615
|
export_data: List[ProfileData] = []
|
613
|
-
|
614
|
-
# Enhanced progress tracking with
|
616
|
+
|
617
|
+
# Enhanced progress tracking with enterprise-grade progress indicators
|
615
618
|
with Progress(
|
616
619
|
SpinnerColumn(),
|
617
620
|
TextColumn("[progress.description]{task.description}"),
|
@@ -619,52 +622,53 @@ def _generate_dashboard_data(
|
|
619
622
|
TaskProgressColumn(),
|
620
623
|
TimeElapsedColumn(),
|
621
624
|
console=console,
|
622
|
-
transient=False # Keep progress visible
|
625
|
+
transient=False, # Keep progress visible
|
623
626
|
) as progress:
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
for profile
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
account_profiles[current_account_id].append(profile)
|
637
|
-
else:
|
638
|
-
console.log(f"[yellow]Could not determine account ID for profile {profile}[/]")
|
639
|
-
except Exception as e:
|
640
|
-
console.log(f"[bold red]Error checking account ID for profile {profile}: {str(e)}[/]")
|
641
|
-
progress.advance(grouping_task)
|
642
|
-
|
643
|
-
# Process combined profiles with enhanced progress tracking
|
644
|
-
processing_task = progress.add_task("Processing account data", total=len(account_profiles))
|
645
|
-
for account_id_key, profiles_list in account_profiles.items():
|
646
|
-
progress.update(processing_task, description=f"Processing account: {account_id_key}")
|
647
|
-
|
648
|
-
if len(profiles_list) > 1:
|
649
|
-
profile_data = _process_combined_profiles_enhanced(
|
650
|
-
account_id_key, profiles_list, user_regions, time_range, args.tag
|
651
|
-
)
|
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)
|
652
639
|
else:
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
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
|
+
)
|
654
|
+
else:
|
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
|
+
|
668
672
|
return export_data
|
669
673
|
|
670
674
|
|
@@ -682,10 +686,10 @@ def _process_single_profile_enhanced(
|
|
682
686
|
# Use billing session for cost data
|
683
687
|
cost_session = _create_cost_session(profile)
|
684
688
|
cost_data = get_cost_data(cost_session, time_range, tag)
|
685
|
-
|
689
|
+
|
686
690
|
# Use operational session for EC2 and resource operations
|
687
691
|
ops_session = _create_operational_session(profile)
|
688
|
-
|
692
|
+
|
689
693
|
if user_regions:
|
690
694
|
profile_regions = user_regions
|
691
695
|
else:
|
@@ -748,15 +752,15 @@ def _process_combined_profiles_enhanced(
|
|
748
752
|
"""
|
749
753
|
try:
|
750
754
|
primary_profile = profiles[0]
|
751
|
-
|
755
|
+
|
752
756
|
# Use billing session for cost data aggregation
|
753
757
|
primary_cost_session = _create_cost_session(primary_profile)
|
754
758
|
# Use operational session for resource data
|
755
759
|
primary_ops_session = _create_operational_session(primary_profile)
|
756
|
-
|
760
|
+
|
757
761
|
# Get cost data using billing session
|
758
762
|
account_cost_data = get_cost_data(primary_cost_session, time_range, tag)
|
759
|
-
|
763
|
+
|
760
764
|
if user_regions:
|
761
765
|
profile_regions = user_regions
|
762
766
|
else:
|
@@ -779,7 +783,7 @@ def _process_combined_profiles_enhanced(
|
|
779
783
|
percent_change_in_total_cost = change_in_total_cost(
|
780
784
|
account_cost_data["current_month"], account_cost_data["last_month"]
|
781
785
|
)
|
782
|
-
|
786
|
+
|
783
787
|
profile_list = ", ".join(profiles)
|
784
788
|
console.log(f"[dim cyan]Combined {len(profiles)} profiles for account {account_id}: {profile_list}[/]")
|
785
789
|
|
@@ -857,35 +861,31 @@ def _export_dashboard_reports(
|
|
857
861
|
|
858
862
|
|
859
863
|
def run_dashboard(args: argparse.Namespace) -> int:
|
860
|
-
"""Main function to run the
|
864
|
+
"""Main function to run the CloudOps Runbooks FinOps Platform with multi-profile support."""
|
861
865
|
with Status("[bright_cyan]Initialising...", spinner="aesthetic", speed=0.4):
|
862
866
|
profiles_to_use, user_regions, time_range = _initialize_profiles(args)
|
863
|
-
|
867
|
+
|
864
868
|
# Display multi-profile configuration at startup
|
865
|
-
billing_profile = os.getenv(
|
866
|
-
mgmt_profile = os.getenv(
|
867
|
-
ops_profile = os.getenv(
|
868
|
-
|
869
|
+
billing_profile = os.getenv("BILLING_PROFILE")
|
870
|
+
mgmt_profile = os.getenv("MANAGEMENT_PROFILE")
|
871
|
+
ops_profile = os.getenv("CENTRALISED_OPS_PROFILE")
|
872
|
+
|
869
873
|
if any([billing_profile, mgmt_profile, ops_profile]):
|
870
874
|
console.print("\n[bold bright_cyan]🔧 Multi-Profile Configuration Detected[/]")
|
871
875
|
config_table = Table(
|
872
|
-
title="Profile Configuration",
|
873
|
-
show_header=True,
|
874
|
-
header_style="bold cyan",
|
875
|
-
box=box.SIMPLE,
|
876
|
-
style="dim"
|
876
|
+
title="Profile Configuration", show_header=True, header_style="bold cyan", box=box.SIMPLE, style="dim"
|
877
877
|
)
|
878
878
|
config_table.add_column("Operation Type", style="bold")
|
879
879
|
config_table.add_column("Profile", style="bright_cyan")
|
880
880
|
config_table.add_column("Purpose", style="dim")
|
881
|
-
|
881
|
+
|
882
882
|
if billing_profile:
|
883
883
|
config_table.add_row("💰 Billing", billing_profile, "Cost Explorer & Budget API access")
|
884
884
|
if mgmt_profile:
|
885
885
|
config_table.add_row("🏛️ Management", mgmt_profile, "Account ID & Organizations operations")
|
886
886
|
if ops_profile:
|
887
887
|
config_table.add_row("⚙️ Operational", ops_profile, "EC2, S3, and resource discovery")
|
888
|
-
|
888
|
+
|
889
889
|
console.print(config_table)
|
890
890
|
console.print("[dim]Fallback: Using profile-specific sessions when env vars not set[/]\n")
|
891
891
|
|
@@ -917,3 +917,180 @@ def run_dashboard(args: argparse.Namespace) -> int:
|
|
917
917
|
_export_dashboard_reports(export_data, args, previous_period_dates, current_period_dates)
|
918
918
|
|
919
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)}
|