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
runbooks/finops/helpers.py
CHANGED
@@ -9,10 +9,12 @@ from typing import Any, Dict, List, Optional
|
|
9
9
|
|
10
10
|
import yaml
|
11
11
|
from reportlab.lib import colors
|
12
|
-
from reportlab.lib.pagesizes import landscape, letter
|
12
|
+
from reportlab.lib.pagesizes import A4, landscape, letter
|
13
13
|
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
14
|
+
from reportlab.lib.units import inch
|
14
15
|
from reportlab.platypus import (
|
15
16
|
Flowable,
|
17
|
+
PageBreak,
|
16
18
|
Paragraph,
|
17
19
|
SimpleDocTemplate,
|
18
20
|
Spacer,
|
@@ -73,10 +75,22 @@ def export_audit_report_to_pdf(
|
|
73
75
|
"Unused Volumes",
|
74
76
|
"Unused EIPs",
|
75
77
|
"Budget Alerts",
|
78
|
+
"Risk Score",
|
76
79
|
]
|
77
80
|
table_data = [headers]
|
78
81
|
|
79
82
|
for row in audit_data_list:
|
83
|
+
# Format risk score for PDF display
|
84
|
+
risk_score = row.get("risk_score", 0)
|
85
|
+
if risk_score == 0:
|
86
|
+
risk_display = "LOW (0)"
|
87
|
+
elif risk_score <= 10:
|
88
|
+
risk_display = f"MEDIUM ({risk_score})"
|
89
|
+
elif risk_score <= 25:
|
90
|
+
risk_display = f"HIGH ({risk_score})"
|
91
|
+
else:
|
92
|
+
risk_display = f"CRITICAL ({risk_score})"
|
93
|
+
|
80
94
|
table_data.append(
|
81
95
|
[
|
82
96
|
row.get("profile", ""),
|
@@ -86,6 +100,7 @@ def export_audit_report_to_pdf(
|
|
86
100
|
row.get("unused_volumes", ""),
|
87
101
|
row.get("unused_eips", ""),
|
88
102
|
row.get("budget_alerts", ""),
|
103
|
+
risk_display,
|
89
104
|
]
|
90
105
|
)
|
91
106
|
|
@@ -105,20 +120,24 @@ def export_audit_report_to_pdf(
|
|
105
120
|
)
|
106
121
|
)
|
107
122
|
|
108
|
-
elements.append(Paragraph("
|
123
|
+
elements.append(Paragraph("🎯 CloudOps Runbooks FinOps - Enterprise Audit Report", styles["Title"]))
|
109
124
|
elements.append(Spacer(1, 12))
|
110
125
|
elements.append(table)
|
111
126
|
elements.append(Spacer(1, 4))
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
127
|
+
|
128
|
+
# Enhanced notes with PDCA information
|
129
|
+
pdca_info = Paragraph(
|
130
|
+
"📊 PDCA Framework: This report follows Plan-Do-Check-Act continuous improvement methodology.<br/>"
|
131
|
+
"📝 Coverage: Scans EC2, RDS, Lambda, ELBv2 resources across all accessible regions.<br/>"
|
132
|
+
"🎯 Risk Scoring: LOW (0-10), MEDIUM (11-25), HIGH (26-50), CRITICAL (>50)",
|
133
|
+
audit_footer_style,
|
117
134
|
)
|
135
|
+
elements.append(pdca_info)
|
136
|
+
|
118
137
|
elements.append(Spacer(1, 2))
|
119
138
|
current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
120
139
|
footer_text = (
|
121
|
-
f"
|
140
|
+
f"🚀 Generated using CloudOps-Runbooks FinOps Dashboard (PDCA Enhanced) \u00a9 2025 on {current_time_str}"
|
122
141
|
)
|
123
142
|
elements.append(Paragraph(footer_text, audit_footer_style))
|
124
143
|
|
@@ -129,6 +148,93 @@ def export_audit_report_to_pdf(
|
|
129
148
|
return None
|
130
149
|
|
131
150
|
|
151
|
+
def _truncate_service_costs(services_data: str, max_length: int = 500) -> str:
|
152
|
+
"""
|
153
|
+
Truncate service costs data for PDF display if too long.
|
154
|
+
|
155
|
+
:param services_data: Service costs formatted as string
|
156
|
+
:param max_length: Maximum character length before truncation
|
157
|
+
:return: Truncated service costs string
|
158
|
+
"""
|
159
|
+
if len(services_data) <= max_length:
|
160
|
+
return services_data
|
161
|
+
|
162
|
+
lines = services_data.split("\n")
|
163
|
+
truncated_lines = []
|
164
|
+
current_length = 0
|
165
|
+
|
166
|
+
for line in lines:
|
167
|
+
if current_length + len(line) + 1 <= max_length - 50: # Reserve space for truncation message
|
168
|
+
truncated_lines.append(line)
|
169
|
+
current_length += len(line) + 1
|
170
|
+
else:
|
171
|
+
break
|
172
|
+
|
173
|
+
# Add truncation indicator with service count
|
174
|
+
remaining_services = len(lines) - len(truncated_lines)
|
175
|
+
if remaining_services > 0:
|
176
|
+
truncated_lines.append(f"... and {remaining_services} more services")
|
177
|
+
|
178
|
+
return "\n".join(truncated_lines)
|
179
|
+
|
180
|
+
|
181
|
+
def _optimize_table_for_pdf(table_data: List[List[str]], max_col_width: int = 120) -> List[List[str]]:
|
182
|
+
"""
|
183
|
+
Optimize table data for PDF rendering by managing column widths.
|
184
|
+
|
185
|
+
:param table_data: Raw table data with headers and rows
|
186
|
+
:param max_col_width: Maximum character width for any column
|
187
|
+
:return: Optimized table data
|
188
|
+
"""
|
189
|
+
optimized_data = []
|
190
|
+
|
191
|
+
for row_idx, row in enumerate(table_data):
|
192
|
+
optimized_row = []
|
193
|
+
|
194
|
+
for col_idx, cell in enumerate(row):
|
195
|
+
if col_idx == 4: # "Cost By Service" column (index 4)
|
196
|
+
# Apply special handling to service costs column
|
197
|
+
optimized_cell = _truncate_service_costs(str(cell), max_col_width)
|
198
|
+
else:
|
199
|
+
# General cell optimization
|
200
|
+
cell_str = str(cell)
|
201
|
+
if len(cell_str) > max_col_width:
|
202
|
+
# Truncate long content with ellipsis
|
203
|
+
optimized_cell = cell_str[: max_col_width - 3] + "..."
|
204
|
+
else:
|
205
|
+
optimized_cell = cell_str
|
206
|
+
|
207
|
+
optimized_row.append(optimized_cell)
|
208
|
+
|
209
|
+
optimized_data.append(optimized_row)
|
210
|
+
|
211
|
+
return optimized_data
|
212
|
+
|
213
|
+
|
214
|
+
def _create_paginated_tables(table_data: List[List[str]], max_rows_per_page: int = 15) -> List[List[List[str]]]:
|
215
|
+
"""
|
216
|
+
Split large table data into multiple pages for PDF generation.
|
217
|
+
|
218
|
+
:param table_data: Complete table data including headers
|
219
|
+
:param max_rows_per_page: Maximum data rows per page (excluding header)
|
220
|
+
:return: List of table data chunks, each with headers
|
221
|
+
"""
|
222
|
+
if len(table_data) <= max_rows_per_page + 1: # +1 for header
|
223
|
+
return [table_data]
|
224
|
+
|
225
|
+
headers = table_data[0]
|
226
|
+
data_rows = table_data[1:]
|
227
|
+
|
228
|
+
paginated_tables = []
|
229
|
+
|
230
|
+
for i in range(0, len(data_rows), max_rows_per_page):
|
231
|
+
chunk = data_rows[i : i + max_rows_per_page]
|
232
|
+
table_chunk = [headers] + chunk
|
233
|
+
paginated_tables.append(table_chunk)
|
234
|
+
|
235
|
+
return paginated_tables
|
236
|
+
|
237
|
+
|
132
238
|
def clean_rich_tags(text: str) -> str:
|
133
239
|
"""
|
134
240
|
Clean the rich text before writing the data to a pdf.
|
@@ -161,6 +267,7 @@ def export_audit_report_to_csv(
|
|
161
267
|
"Unused Volumes",
|
162
268
|
"Unused EIPs",
|
163
269
|
"Budget Alerts",
|
270
|
+
"Risk Score",
|
164
271
|
]
|
165
272
|
# Corresponding keys in the audit_data_list dictionaries
|
166
273
|
data_keys = [
|
@@ -171,6 +278,7 @@ def export_audit_report_to_csv(
|
|
171
278
|
"unused_volumes",
|
172
279
|
"unused_eips",
|
173
280
|
"budget_alerts",
|
281
|
+
"risk_score",
|
174
282
|
]
|
175
283
|
|
176
284
|
with open(output_filename, "w", newline="") as csvfile:
|
@@ -231,7 +339,7 @@ def export_cost_dashboard_to_pdf(
|
|
231
339
|
previous_period_dates: str = "N/A",
|
232
340
|
current_period_dates: str = "N/A",
|
233
341
|
) -> Optional[str]:
|
234
|
-
"""Export dashboard data to a PDF file."""
|
342
|
+
"""Export dashboard data to a PDF file with enterprise-grade layout handling."""
|
235
343
|
try:
|
236
344
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
237
345
|
base_filename = f"{filename}_{timestamp}.pdf"
|
@@ -242,71 +350,213 @@ def export_cost_dashboard_to_pdf(
|
|
242
350
|
else:
|
243
351
|
output_filename = base_filename
|
244
352
|
|
245
|
-
|
353
|
+
# Use A4 landscape for better space utilization
|
354
|
+
doc = SimpleDocTemplate(
|
355
|
+
output_filename,
|
356
|
+
pagesize=landscape(A4),
|
357
|
+
rightMargin=0.5 * inch,
|
358
|
+
leftMargin=0.5 * inch,
|
359
|
+
topMargin=0.5 * inch,
|
360
|
+
bottomMargin=0.5 * inch,
|
361
|
+
)
|
246
362
|
styles = getSampleStyleSheet()
|
247
363
|
elements: List[Flowable] = []
|
248
364
|
|
249
|
-
|
250
|
-
|
365
|
+
# Enhanced title with executive summary
|
366
|
+
title_style = ParagraphStyle(
|
367
|
+
name="EnhancedTitle",
|
368
|
+
parent=styles["Title"],
|
369
|
+
fontSize=16,
|
370
|
+
spaceAfter=12,
|
371
|
+
textColor=colors.darkblue,
|
372
|
+
alignment=1, # Center alignment
|
373
|
+
)
|
374
|
+
|
375
|
+
# Calculate summary metrics
|
376
|
+
total_accounts = len(data)
|
377
|
+
total_current_cost = sum(row["current_month"] for row in data if row.get("current_month", 0))
|
378
|
+
total_previous_cost = sum(row["last_month"] for row in data if row.get("last_month", 0))
|
379
|
+
cost_change = (
|
380
|
+
((total_current_cost - total_previous_cost) / total_previous_cost * 100) if total_previous_cost > 0 else 0
|
381
|
+
)
|
382
|
+
|
383
|
+
elements.append(Paragraph("🏢 CloudOps Runbooks FinOps - Enterprise Cost Report", title_style))
|
384
|
+
|
385
|
+
# Executive summary
|
386
|
+
summary_style = ParagraphStyle(
|
387
|
+
name="Summary", parent=styles["Normal"], fontSize=10, spaceAfter=8, textColor=colors.darkgreen
|
388
|
+
)
|
389
|
+
|
390
|
+
summary_text = (
|
391
|
+
f"📊 Executive Summary: {total_accounts} accounts analyzed | "
|
392
|
+
f"Total Current Cost: ${total_current_cost:,.2f} | "
|
393
|
+
f"Cost Change: {cost_change:+.1f}% | "
|
394
|
+
f"Report Period: {current_period_dates}"
|
395
|
+
)
|
396
|
+
elements.append(Paragraph(summary_text, summary_style))
|
397
|
+
elements.append(Spacer(1, 12))
|
398
|
+
|
399
|
+
# Prepare table data with optimization
|
400
|
+
previous_period_header = f"Cost Period\n({previous_period_dates})"
|
401
|
+
current_period_header = f"Cost Period\n({current_period_dates})"
|
251
402
|
|
252
403
|
headers = [
|
253
|
-
"
|
254
|
-
"
|
404
|
+
"Profile",
|
405
|
+
"Account ID",
|
255
406
|
previous_period_header,
|
256
407
|
current_period_header,
|
257
|
-
"
|
408
|
+
"Top Services",
|
258
409
|
"Budget Status",
|
259
|
-
"EC2
|
410
|
+
"EC2 Summary",
|
260
411
|
]
|
261
|
-
table_data = [headers]
|
262
412
|
|
263
|
-
|
264
|
-
services_data = "\n".join([f"{service}: ${cost:.2f}" for service, cost in row["service_costs"]])
|
265
|
-
budgets_data = "\n".join(row["budget_info"]) if row["budget_info"] else "No budgets"
|
266
|
-
ec2_data_summary = "\n".join(
|
267
|
-
[f"{state}: {count}" for state, count in row["ec2_summary"].items() if count > 0]
|
268
|
-
)
|
413
|
+
raw_table_data = [headers]
|
269
414
|
|
270
|
-
|
415
|
+
for row in data:
|
416
|
+
# Optimize service costs for PDF display
|
417
|
+
if row["service_costs"]:
|
418
|
+
# Show only top 10 services to prevent width issues
|
419
|
+
top_services = row["service_costs"][:10]
|
420
|
+
services_data = "\n".join([f"{service}: ${cost:.2f}" for service, cost in top_services])
|
421
|
+
if len(row["service_costs"]) > 10:
|
422
|
+
remaining_count = len(row["service_costs"]) - 10
|
423
|
+
services_data += f"\n... and {remaining_count} more services"
|
424
|
+
else:
|
425
|
+
services_data = "No costs"
|
426
|
+
|
427
|
+
# Optimize budget display
|
428
|
+
budget_lines = row["budget_info"][:3] if row["budget_info"] else ["No budgets"]
|
429
|
+
budgets_data = "\n".join(budget_lines)
|
430
|
+
if len(row["budget_info"]) > 3:
|
431
|
+
budgets_data += f"\n... +{len(row['budget_info']) - 3} more"
|
432
|
+
|
433
|
+
# Optimize EC2 summary
|
434
|
+
ec2_items = [(state, count) for state, count in row["ec2_summary"].items() if count > 0]
|
435
|
+
if ec2_items:
|
436
|
+
ec2_data_summary = "\n".join([f"{state}: {count}" for state, count in ec2_items[:5]])
|
437
|
+
if len(ec2_items) > 5:
|
438
|
+
ec2_data_summary += f"\n... +{len(ec2_items) - 5} more"
|
439
|
+
else:
|
440
|
+
ec2_data_summary = "No instances"
|
441
|
+
|
442
|
+
# Format cost change indicator
|
443
|
+
cost_change_text = ""
|
444
|
+
if row.get("percent_change_in_total_cost") is not None:
|
445
|
+
change = row["percent_change_in_total_cost"]
|
446
|
+
if change > 0:
|
447
|
+
cost_change_text = f"\n↑ +{change:.1f}%"
|
448
|
+
elif change < 0:
|
449
|
+
cost_change_text = f"\n↓ {change:.1f}%"
|
450
|
+
else:
|
451
|
+
cost_change_text = "\n→ 0%"
|
452
|
+
|
453
|
+
raw_table_data.append(
|
271
454
|
[
|
272
455
|
row["profile"],
|
273
456
|
row["account_id"],
|
274
|
-
f"${row['last_month']
|
275
|
-
f"${row['current_month']
|
276
|
-
services_data
|
277
|
-
budgets_data
|
278
|
-
ec2_data_summary
|
457
|
+
f"${row['last_month']:,.2f}",
|
458
|
+
f"${row['current_month']:,.2f}{cost_change_text}",
|
459
|
+
services_data,
|
460
|
+
budgets_data,
|
461
|
+
ec2_data_summary,
|
279
462
|
]
|
280
463
|
)
|
281
464
|
|
282
|
-
table
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
465
|
+
# Optimize table data for PDF rendering
|
466
|
+
optimized_table_data = _optimize_table_for_pdf(raw_table_data, max_col_width=80)
|
467
|
+
|
468
|
+
# Create paginated tables for large datasets
|
469
|
+
paginated_tables = _create_paginated_tables(optimized_table_data, max_rows_per_page=12)
|
470
|
+
|
471
|
+
# Style configuration for optimal PDF rendering
|
472
|
+
table_style = TableStyle(
|
473
|
+
[
|
474
|
+
# Header styling
|
475
|
+
("BACKGROUND", (0, 0), (-1, 0), colors.navy),
|
476
|
+
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
|
477
|
+
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
478
|
+
("FONTSIZE", (0, 0), (-1, 0), 7),
|
479
|
+
# Data styling
|
480
|
+
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
|
481
|
+
("FONTSIZE", (0, 1), (-1, -1), 6),
|
482
|
+
("ALIGN", (0, 0), (-1, -1), "LEFT"),
|
483
|
+
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
484
|
+
# Grid and background
|
485
|
+
("GRID", (0, 0), (-1, -1), 0.5, colors.darkgrey),
|
486
|
+
("BACKGROUND", (0, 1), (-1, -1), colors.beige),
|
487
|
+
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
|
488
|
+
# Column-specific styling
|
489
|
+
("ALIGN", (2, 0), (3, -1), "RIGHT"), # Cost columns right-aligned
|
490
|
+
("FONTNAME", (2, 1), (3, -1), "Helvetica-Bold"), # Bold cost values
|
491
|
+
]
|
296
492
|
)
|
297
493
|
|
298
|
-
|
494
|
+
# Generate tables with pagination
|
495
|
+
for page_idx, table_data_chunk in enumerate(paginated_tables):
|
496
|
+
if page_idx > 0:
|
497
|
+
elements.append(PageBreak())
|
498
|
+
# Add page header for continuation pages
|
499
|
+
page_header = Paragraph(
|
500
|
+
f"CloudOps Runbooks FinOps - Page {page_idx + 1} of {len(paginated_tables)}",
|
501
|
+
ParagraphStyle(
|
502
|
+
name="PageHeader", parent=styles["Heading2"], fontSize=12, textColor=colors.darkblue
|
503
|
+
),
|
504
|
+
)
|
505
|
+
elements.append(page_header)
|
506
|
+
elements.append(Spacer(1, 8))
|
507
|
+
|
508
|
+
# Create table with dynamic column widths
|
509
|
+
available_width = landscape(A4)[0] - 1 * inch # Account for margins
|
510
|
+
col_widths = [
|
511
|
+
available_width * 0.12, # Profile (12%)
|
512
|
+
available_width * 0.15, # Account ID (15%)
|
513
|
+
available_width * 0.12, # Previous Cost (12%)
|
514
|
+
available_width * 0.12, # Current Cost (12%)
|
515
|
+
available_width * 0.25, # Services (25%)
|
516
|
+
available_width * 0.12, # Budget (12%)
|
517
|
+
available_width * 0.12, # EC2 (12%)
|
518
|
+
]
|
519
|
+
|
520
|
+
table = Table(table_data_chunk, repeatRows=1, colWidths=col_widths)
|
521
|
+
table.setStyle(table_style)
|
522
|
+
elements.append(table)
|
523
|
+
|
524
|
+
# Enhanced footer with metadata
|
299
525
|
elements.append(Spacer(1, 12))
|
300
|
-
elements.append(table)
|
301
|
-
elements.append(Spacer(1, 4))
|
302
|
-
current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
303
|
-
footer_text = f"This report is generated using AWS FinOps Dashboard (CLI) \u00a9 2025 on {current_time_str}"
|
304
|
-
elements.append(Paragraph(footer_text, audit_footer_style))
|
305
526
|
|
527
|
+
footer_style = ParagraphStyle(
|
528
|
+
name="EnhancedFooter", parent=styles["Normal"], fontSize=8, textColor=colors.grey, alignment=1
|
529
|
+
)
|
530
|
+
|
531
|
+
current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
|
532
|
+
footer_text = (
|
533
|
+
f"🚀 Generated by CloudOps-Runbooks FinOps Dashboard v0.7.8 | "
|
534
|
+
f"Report Generated: {current_time_str} | "
|
535
|
+
f"Accounts Analyzed: {total_accounts} | "
|
536
|
+
f"© 2025 CloudOps Enterprise"
|
537
|
+
)
|
538
|
+
elements.append(Paragraph(footer_text, footer_style))
|
539
|
+
|
540
|
+
# Build PDF with error handling
|
306
541
|
doc.build(elements)
|
307
|
-
|
542
|
+
|
543
|
+
# Verify file creation
|
544
|
+
if os.path.exists(output_filename):
|
545
|
+
file_size = os.path.getsize(output_filename)
|
546
|
+
console.print(
|
547
|
+
f"[bright_green]✅ PDF generated successfully: {os.path.abspath(output_filename)} ({file_size:,} bytes)[/]"
|
548
|
+
)
|
549
|
+
return os.path.abspath(output_filename)
|
550
|
+
else:
|
551
|
+
console.print("[bold red]❌ PDF file was not created[/]")
|
552
|
+
return None
|
553
|
+
|
308
554
|
except Exception as e:
|
309
|
-
console.print(f"[bold red]Error exporting to PDF: {str(e)}[/]")
|
555
|
+
console.print(f"[bold red]❌ Error exporting to PDF: {str(e)}[/]")
|
556
|
+
# Print more detailed error information for debugging
|
557
|
+
import traceback
|
558
|
+
|
559
|
+
console.print(f"[red]Detailed error trace: {traceback.format_exc()}[/]")
|
310
560
|
return None
|
311
561
|
|
312
562
|
|
@@ -353,3 +603,187 @@ def load_config_file(file_path: str) -> Optional[Dict[str, Any]]:
|
|
353
603
|
except Exception as e:
|
354
604
|
console.print(f"[bold red]Error loading configuration file {file_path}: {e}[/]")
|
355
605
|
return None
|
606
|
+
|
607
|
+
|
608
|
+
def generate_pdca_improvement_report(
|
609
|
+
pdca_metrics: List[Dict[str, Any]], file_name: str = "pdca_improvement", path: Optional[str] = None
|
610
|
+
) -> Optional[str]:
|
611
|
+
"""
|
612
|
+
Generate PDCA (Plan-Do-Check-Act) continuous improvement report.
|
613
|
+
|
614
|
+
:param pdca_metrics: List of PDCA metrics for each profile
|
615
|
+
:param file_name: The base name of the output file
|
616
|
+
:param path: Optional directory where the file will be saved
|
617
|
+
:return: Full path of the generated report or None on error
|
618
|
+
"""
|
619
|
+
try:
|
620
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
621
|
+
base_filename = f"{file_name}_pdca_report_{timestamp}.json"
|
622
|
+
|
623
|
+
if path:
|
624
|
+
os.makedirs(path, exist_ok=True)
|
625
|
+
output_filename = os.path.join(path, base_filename)
|
626
|
+
else:
|
627
|
+
output_filename = base_filename
|
628
|
+
|
629
|
+
# Calculate aggregate metrics
|
630
|
+
total_risk = sum(m["risk_score"] for m in pdca_metrics)
|
631
|
+
avg_risk = total_risk / len(pdca_metrics) if pdca_metrics else 0
|
632
|
+
|
633
|
+
high_risk_accounts = [m for m in pdca_metrics if m["risk_score"] > 25]
|
634
|
+
medium_risk_accounts = [m for m in pdca_metrics if 10 < m["risk_score"] <= 25]
|
635
|
+
low_risk_accounts = [m for m in pdca_metrics if m["risk_score"] <= 10]
|
636
|
+
|
637
|
+
total_untagged = sum(m["untagged_count"] for m in pdca_metrics)
|
638
|
+
total_stopped = sum(m["stopped_count"] for m in pdca_metrics)
|
639
|
+
total_unused_volumes = sum(m["unused_volumes_count"] for m in pdca_metrics)
|
640
|
+
total_unused_eips = sum(m["unused_eips_count"] for m in pdca_metrics)
|
641
|
+
total_budget_overruns = sum(m["budget_overruns"] for m in pdca_metrics)
|
642
|
+
|
643
|
+
# Generate improvement recommendations
|
644
|
+
recommendations = []
|
645
|
+
|
646
|
+
# PLAN phase recommendations
|
647
|
+
if total_untagged > 50:
|
648
|
+
recommendations.append(
|
649
|
+
{
|
650
|
+
"phase": "PLAN",
|
651
|
+
"priority": "HIGH",
|
652
|
+
"category": "Compliance",
|
653
|
+
"issue": f"Found {total_untagged} untagged resources across all accounts",
|
654
|
+
"action": "Implement mandatory tagging strategy using AWS Config rules",
|
655
|
+
"expected_outcome": "100% resource compliance within 30 days",
|
656
|
+
"owner": "Cloud Governance Team",
|
657
|
+
}
|
658
|
+
)
|
659
|
+
|
660
|
+
if total_unused_eips > 5:
|
661
|
+
recommendations.append(
|
662
|
+
{
|
663
|
+
"phase": "PLAN",
|
664
|
+
"priority": "MEDIUM",
|
665
|
+
"category": "Cost Optimization",
|
666
|
+
"issue": f"Found {total_unused_eips} unused Elastic IPs",
|
667
|
+
"action": "Schedule monthly EIP cleanup automation",
|
668
|
+
"expected_outcome": f"Save ~${total_unused_eips * 3.65:.2f}/month",
|
669
|
+
"owner": "FinOps Team",
|
670
|
+
}
|
671
|
+
)
|
672
|
+
|
673
|
+
# DO phase recommendations
|
674
|
+
if high_risk_accounts:
|
675
|
+
recommendations.append(
|
676
|
+
{
|
677
|
+
"phase": "DO",
|
678
|
+
"priority": "CRITICAL",
|
679
|
+
"category": "Risk Management",
|
680
|
+
"issue": f"{len(high_risk_accounts)} accounts have critical risk scores",
|
681
|
+
"action": "Execute immediate remediation on high-risk accounts",
|
682
|
+
"expected_outcome": "Reduce risk scores by 70% within 2 weeks",
|
683
|
+
"owner": "Security Team",
|
684
|
+
}
|
685
|
+
)
|
686
|
+
|
687
|
+
# CHECK phase recommendations
|
688
|
+
if avg_risk > 15:
|
689
|
+
recommendations.append(
|
690
|
+
{
|
691
|
+
"phase": "CHECK",
|
692
|
+
"priority": "HIGH",
|
693
|
+
"category": "Monitoring",
|
694
|
+
"issue": f"Average risk score ({avg_risk:.1f}) exceeds threshold",
|
695
|
+
"action": "Implement automated risk scoring dashboard",
|
696
|
+
"expected_outcome": "Real-time risk visibility and alerting",
|
697
|
+
"owner": "DevOps Team",
|
698
|
+
}
|
699
|
+
)
|
700
|
+
|
701
|
+
# ACT phase recommendations
|
702
|
+
recommendations.append(
|
703
|
+
{
|
704
|
+
"phase": "ACT",
|
705
|
+
"priority": "MEDIUM",
|
706
|
+
"category": "Process Improvement",
|
707
|
+
"issue": "Need continuous improvement framework",
|
708
|
+
"action": "Establish monthly PDCA review cycles",
|
709
|
+
"expected_outcome": "25% reduction in average risk score per quarter",
|
710
|
+
"owner": "Cloud Center of Excellence",
|
711
|
+
}
|
712
|
+
)
|
713
|
+
|
714
|
+
# Create comprehensive PDCA report
|
715
|
+
pdca_report = {
|
716
|
+
"report_metadata": {
|
717
|
+
"generated_at": datetime.now().isoformat(),
|
718
|
+
"report_type": "PDCA Continuous Improvement Analysis",
|
719
|
+
"accounts_analyzed": len(pdca_metrics),
|
720
|
+
"framework_version": "v1.0",
|
721
|
+
},
|
722
|
+
"executive_summary": {
|
723
|
+
"overall_risk_score": total_risk,
|
724
|
+
"average_risk_score": round(avg_risk, 2),
|
725
|
+
"risk_distribution": {
|
726
|
+
"critical_accounts": len(high_risk_accounts),
|
727
|
+
"high_risk_accounts": len(medium_risk_accounts),
|
728
|
+
"low_risk_accounts": len(low_risk_accounts),
|
729
|
+
},
|
730
|
+
"key_findings": {
|
731
|
+
"untagged_resources": total_untagged,
|
732
|
+
"stopped_instances": total_stopped,
|
733
|
+
"unused_volumes": total_unused_volumes,
|
734
|
+
"unused_elastic_ips": total_unused_eips,
|
735
|
+
"budget_overruns": total_budget_overruns,
|
736
|
+
},
|
737
|
+
},
|
738
|
+
"pdca_analysis": {
|
739
|
+
"plan_phase": {
|
740
|
+
"description": "Strategic planning based on current state analysis",
|
741
|
+
"metrics_collected": len(pdca_metrics),
|
742
|
+
"baseline_established": True,
|
743
|
+
},
|
744
|
+
"do_phase": {
|
745
|
+
"description": "Implementation of audit data collection",
|
746
|
+
"data_sources": ["EC2", "RDS", "Lambda", "ELBv2", "Budgets"],
|
747
|
+
"regions_scanned": "All accessible regions",
|
748
|
+
},
|
749
|
+
"check_phase": {
|
750
|
+
"description": "Analysis of collected audit data",
|
751
|
+
"risk_assessment_completed": True,
|
752
|
+
"trends_identified": True,
|
753
|
+
},
|
754
|
+
"act_phase": {
|
755
|
+
"description": "Actionable recommendations for improvement",
|
756
|
+
"recommendations_generated": len(recommendations),
|
757
|
+
"prioritization_completed": True,
|
758
|
+
},
|
759
|
+
},
|
760
|
+
"detailed_metrics": pdca_metrics,
|
761
|
+
"improvement_recommendations": recommendations,
|
762
|
+
"next_steps": {
|
763
|
+
"immediate_actions": [
|
764
|
+
"Review high-risk accounts within 48 hours",
|
765
|
+
"Implement automated tagging for untagged resources",
|
766
|
+
"Schedule EIP cleanup automation",
|
767
|
+
],
|
768
|
+
"medium_term_goals": [
|
769
|
+
"Establish monthly PDCA review cycle",
|
770
|
+
"Implement risk scoring dashboard",
|
771
|
+
"Create automated remediation workflows",
|
772
|
+
],
|
773
|
+
"long_term_objectives": [
|
774
|
+
"Achieve average risk score below 5",
|
775
|
+
"Maintain 100% resource compliance",
|
776
|
+
"Reduce cloud waste by 25%",
|
777
|
+
],
|
778
|
+
},
|
779
|
+
}
|
780
|
+
|
781
|
+
# Export to JSON
|
782
|
+
with open(output_filename, "w", encoding="utf-8") as jsonfile:
|
783
|
+
json.dump(pdca_report, jsonfile, indent=4, default=str)
|
784
|
+
|
785
|
+
return os.path.abspath(output_filename)
|
786
|
+
|
787
|
+
except Exception as e:
|
788
|
+
console.print(f"[bold red]Error generating PDCA improvement report: {str(e)}[/]")
|
789
|
+
return None
|