runbooks 0.7.9__py3-none-any.whl → 0.9.1__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/cfat/README.md +12 -1
- runbooks/cfat/__init__.py +1 -1
- runbooks/cfat/assessment/compliance.py +4 -1
- runbooks/cfat/assessment/runner.py +42 -34
- runbooks/cfat/models.py +1 -1
- runbooks/cloudops/__init__.py +123 -0
- runbooks/cloudops/base.py +385 -0
- runbooks/cloudops/cost_optimizer.py +811 -0
- runbooks/cloudops/infrastructure_optimizer.py +29 -0
- runbooks/cloudops/interfaces.py +828 -0
- runbooks/cloudops/lifecycle_manager.py +29 -0
- runbooks/cloudops/mcp_cost_validation.py +678 -0
- runbooks/cloudops/models.py +251 -0
- runbooks/cloudops/monitoring_automation.py +29 -0
- runbooks/cloudops/notebook_framework.py +676 -0
- runbooks/cloudops/security_enforcer.py +449 -0
- runbooks/common/__init__.py +152 -0
- runbooks/common/accuracy_validator.py +1039 -0
- runbooks/common/context_logger.py +440 -0
- runbooks/common/cross_module_integration.py +594 -0
- runbooks/common/enhanced_exception_handler.py +1108 -0
- runbooks/common/enterprise_audit_integration.py +634 -0
- runbooks/common/mcp_cost_explorer_integration.py +900 -0
- runbooks/common/mcp_integration.py +548 -0
- runbooks/common/performance_monitor.py +387 -0
- runbooks/common/profile_utils.py +216 -0
- runbooks/common/rich_utils.py +172 -1
- runbooks/feedback/user_feedback_collector.py +440 -0
- runbooks/finops/README.md +377 -458
- runbooks/finops/__init__.py +4 -21
- runbooks/finops/account_resolver.py +279 -0
- runbooks/finops/accuracy_cross_validator.py +638 -0
- runbooks/finops/aws_client.py +721 -36
- runbooks/finops/budget_integration.py +313 -0
- runbooks/finops/cli.py +59 -5
- runbooks/finops/cost_optimizer.py +1340 -0
- runbooks/finops/cost_processor.py +211 -37
- runbooks/finops/dashboard_router.py +900 -0
- runbooks/finops/dashboard_runner.py +990 -232
- runbooks/finops/embedded_mcp_validator.py +288 -0
- runbooks/finops/enhanced_dashboard_runner.py +8 -7
- runbooks/finops/enhanced_progress.py +327 -0
- runbooks/finops/enhanced_trend_visualization.py +423 -0
- runbooks/finops/finops_dashboard.py +184 -1829
- runbooks/finops/helpers.py +509 -196
- runbooks/finops/iam_guidance.py +400 -0
- runbooks/finops/markdown_exporter.py +466 -0
- runbooks/finops/multi_dashboard.py +1502 -0
- runbooks/finops/optimizer.py +15 -15
- runbooks/finops/profile_processor.py +2 -2
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/schemas.py +589 -0
- runbooks/finops/service_mapping.py +195 -0
- runbooks/finops/single_dashboard.py +710 -0
- runbooks/finops/tests/test_reference_images_validation.py +1 -1
- runbooks/inventory/README.md +12 -1
- runbooks/inventory/core/collector.py +157 -29
- runbooks/inventory/list_ec2_instances.py +9 -6
- runbooks/inventory/list_ssm_parameters.py +10 -10
- runbooks/inventory/organizations_discovery.py +210 -164
- runbooks/inventory/rich_inventory_display.py +74 -107
- runbooks/inventory/run_on_multi_accounts.py +13 -13
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- runbooks/main.py +1371 -240
- runbooks/metrics/dora_metrics_engine.py +711 -17
- runbooks/monitoring/performance_monitor.py +433 -0
- runbooks/operate/README.md +394 -0
- runbooks/operate/base.py +215 -47
- runbooks/operate/ec2_operations.py +435 -5
- runbooks/operate/iam_operations.py +598 -3
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +508 -0
- runbooks/operate/s3_operations.py +508 -0
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/remediation/README.md +489 -13
- runbooks/remediation/base.py +5 -3
- runbooks/remediation/commons.py +8 -4
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
- runbooks/security/README.md +12 -1
- runbooks/security/__init__.py +265 -33
- runbooks/security/cloudops_automation_security_validator.py +1164 -0
- runbooks/security/compliance_automation.py +12 -10
- runbooks/security/compliance_automation_engine.py +1021 -0
- runbooks/security/enterprise_security_framework.py +930 -0
- runbooks/security/enterprise_security_policies.json +293 -0
- runbooks/security/executive_security_dashboard.py +1247 -0
- runbooks/security/integration_test_enterprise_security.py +879 -0
- runbooks/security/module_security_integrator.py +641 -0
- runbooks/security/multi_account_security_controls.py +2254 -0
- runbooks/security/real_time_security_monitor.py +1196 -0
- runbooks/security/report_generator.py +1 -1
- runbooks/security/run_script.py +4 -8
- runbooks/security/security_baseline_tester.py +39 -52
- runbooks/security/security_export.py +99 -120
- runbooks/sre/README.md +472 -0
- runbooks/sre/__init__.py +33 -0
- runbooks/sre/mcp_reliability_engine.py +1049 -0
- runbooks/sre/performance_optimization_engine.py +1032 -0
- runbooks/sre/production_monitoring_framework.py +584 -0
- runbooks/sre/reliability_monitoring_framework.py +1011 -0
- runbooks/validation/__init__.py +2 -2
- runbooks/validation/benchmark.py +154 -149
- runbooks/validation/cli.py +159 -147
- runbooks/validation/mcp_validator.py +291 -248
- runbooks/vpc/README.md +478 -0
- runbooks/vpc/__init__.py +2 -2
- runbooks/vpc/manager_interface.py +366 -351
- runbooks/vpc/networking_wrapper.py +68 -36
- runbooks/vpc/rich_formatters.py +22 -8
- runbooks-0.9.1.dist-info/METADATA +308 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/RECORD +120 -59
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/entry_points.txt +1 -1
- runbooks/finops/cross_validation.py +0 -375
- runbooks-0.7.9.dist-info/METADATA +0 -636
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/WHEEL +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,466 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Rich-styled Markdown Export Module for CloudOps Runbooks FinOps
|
4
|
+
|
5
|
+
This module provides Rich table to markdown conversion functionality with
|
6
|
+
MkDocs compatibility for copy-pasteable documentation tables.
|
7
|
+
|
8
|
+
Features:
|
9
|
+
- Rich table to markdown conversion with styled borders
|
10
|
+
- 10-column format for multi-account analysis
|
11
|
+
- MkDocs compatible table syntax
|
12
|
+
- Intelligent file management and organization
|
13
|
+
- Preserves color coding through markdown syntax
|
14
|
+
- Automated timestamping and metadata
|
15
|
+
|
16
|
+
Author: CloudOps Runbooks Team
|
17
|
+
Version: 0.7.8
|
18
|
+
"""
|
19
|
+
|
20
|
+
import os
|
21
|
+
from datetime import datetime
|
22
|
+
from pathlib import Path
|
23
|
+
from typing import Any, Dict, List, Optional, Union
|
24
|
+
|
25
|
+
from rich import box
|
26
|
+
from rich.table import Table
|
27
|
+
from rich.text import Text
|
28
|
+
|
29
|
+
from runbooks.common.rich_utils import (
|
30
|
+
STATUS_INDICATORS,
|
31
|
+
console,
|
32
|
+
create_table,
|
33
|
+
format_cost,
|
34
|
+
print_info,
|
35
|
+
print_success,
|
36
|
+
print_warning,
|
37
|
+
)
|
38
|
+
|
39
|
+
|
40
|
+
class MarkdownExporter:
|
41
|
+
"""Rich-styled markdown export functionality for FinOps analysis."""
|
42
|
+
|
43
|
+
def __init__(self, output_dir: str = "./exports"):
|
44
|
+
"""
|
45
|
+
Initialize the markdown exporter.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
output_dir: Directory to save markdown exports
|
49
|
+
"""
|
50
|
+
self.output_dir = Path(output_dir)
|
51
|
+
self.output_dir.mkdir(exist_ok=True)
|
52
|
+
|
53
|
+
def rich_table_to_markdown(self, table: Table, preserve_styling: bool = True) -> str:
|
54
|
+
"""
|
55
|
+
Convert Rich table to markdown format with optional styling preservation.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
table: Rich Table object to convert
|
59
|
+
preserve_styling: Whether to preserve Rich styling through markdown syntax
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
Markdown formatted table string
|
63
|
+
"""
|
64
|
+
if not table.columns:
|
65
|
+
return ""
|
66
|
+
|
67
|
+
# Extract column headers
|
68
|
+
headers = []
|
69
|
+
for column in table.columns:
|
70
|
+
header_text = column.header or ""
|
71
|
+
if hasattr(header_text, "plain"):
|
72
|
+
headers.append(header_text.plain)
|
73
|
+
else:
|
74
|
+
headers.append(str(header_text))
|
75
|
+
|
76
|
+
# Create markdown table header
|
77
|
+
markdown_lines = []
|
78
|
+
markdown_lines.append("| " + " | ".join(headers) + " |")
|
79
|
+
|
80
|
+
# Create GitHub-compliant separator line with proper alignment syntax
|
81
|
+
separators = []
|
82
|
+
for column in table.columns:
|
83
|
+
if column.justify == "right":
|
84
|
+
separators.append("---:") # GitHub right alignment (minimum 3 hyphens)
|
85
|
+
elif column.justify == "center":
|
86
|
+
separators.append(":---:") # GitHub center alignment
|
87
|
+
else:
|
88
|
+
separators.append("---") # GitHub left alignment (default, minimum 3 hyphens)
|
89
|
+
markdown_lines.append("| " + " | ".join(separators) + " |")
|
90
|
+
|
91
|
+
# Extract and format data rows
|
92
|
+
for row in table.rows:
|
93
|
+
row_cells = []
|
94
|
+
for cell in row:
|
95
|
+
if isinstance(cell, Text):
|
96
|
+
if preserve_styling:
|
97
|
+
# Convert Rich Text to markdown with styling
|
98
|
+
cell_text = self._rich_text_to_markdown(cell)
|
99
|
+
else:
|
100
|
+
cell_text = cell.plain
|
101
|
+
else:
|
102
|
+
cell_text = str(cell)
|
103
|
+
|
104
|
+
# GitHub tables don't support multi-line content - convert to single line
|
105
|
+
cell_text = cell_text.replace("\n", " • ").strip()
|
106
|
+
|
107
|
+
# Escape pipes in cell content for GitHub compatibility
|
108
|
+
cell_text = cell_text.replace("|", "\\|")
|
109
|
+
|
110
|
+
# Remove excessive Rich formatting that doesn't render well in GitHub
|
111
|
+
cell_text = self._clean_rich_formatting_for_github(cell_text)
|
112
|
+
|
113
|
+
row_cells.append(cell_text)
|
114
|
+
|
115
|
+
markdown_lines.append("| " + " | ".join(row_cells) + " |")
|
116
|
+
|
117
|
+
return "\n".join(markdown_lines)
|
118
|
+
|
119
|
+
def _rich_text_to_markdown(self, rich_text: Text) -> str:
|
120
|
+
"""
|
121
|
+
Convert Rich Text object to markdown with style preservation.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
rich_text: Rich Text object with styling
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
Markdown formatted string with preserved styling
|
128
|
+
"""
|
129
|
+
# Start with plain text
|
130
|
+
text = rich_text.plain
|
131
|
+
|
132
|
+
# Extract style information and apply markdown equivalents
|
133
|
+
if hasattr(rich_text, "_spans") and rich_text._spans:
|
134
|
+
for span in reversed(rich_text._spans): # Reverse to handle overlapping spans
|
135
|
+
style = span.style
|
136
|
+
start, end = span.start, span.end
|
137
|
+
|
138
|
+
# Apply markdown formatting based on Rich styles
|
139
|
+
if style and hasattr(style, "color"):
|
140
|
+
if "green" in str(style.color):
|
141
|
+
# Green text (success/positive) - use ✅ emoji
|
142
|
+
text = text[:start] + "✅ " + text[start:end] + text[end:]
|
143
|
+
elif "red" in str(style.color):
|
144
|
+
# Red text (error/negative) - use ❌ emoji
|
145
|
+
text = text[:start] + "❌ " + text[start:end] + text[end:]
|
146
|
+
elif "yellow" in str(style.color):
|
147
|
+
# Yellow text (warning) - use ⚠️ emoji
|
148
|
+
text = text[:start] + "⚠️ " + text[start:end] + text[end:]
|
149
|
+
elif "cyan" in str(style.color):
|
150
|
+
# Cyan text (info) - use **bold** markdown
|
151
|
+
text = text[:start] + "**" + text[start:end] + "**" + text[end:]
|
152
|
+
|
153
|
+
if style and hasattr(style, "bold") and style.bold:
|
154
|
+
text = text[:start] + "**" + text[start:end] + "**" + text[end:]
|
155
|
+
|
156
|
+
if style and hasattr(style, "italic") and style.italic:
|
157
|
+
text = text[:start] + "*" + text[start:end] + "*" + text[end:]
|
158
|
+
|
159
|
+
return text
|
160
|
+
|
161
|
+
def _clean_rich_formatting_for_github(self, text: str) -> str:
|
162
|
+
"""
|
163
|
+
Clean Rich formatting for better GitHub markdown compatibility.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
text: Text with Rich formatting tags
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
Cleaned text suitable for GitHub markdown tables
|
170
|
+
"""
|
171
|
+
# Remove Rich color/style tags that don't render well in GitHub
|
172
|
+
import re
|
173
|
+
|
174
|
+
# Remove Rich markup tags but preserve content
|
175
|
+
text = re.sub(r"\[/?(?:red|green|yellow|cyan|blue|magenta|white|black|bright_\w+|dim|bold|italic)\]", "", text)
|
176
|
+
text = re.sub(r"\[/?[^\]]*\]", "", text) # Remove any remaining Rich tags
|
177
|
+
|
178
|
+
# Clean up multiple spaces and trim
|
179
|
+
text = re.sub(r"\s+", " ", text).strip()
|
180
|
+
|
181
|
+
return text
|
182
|
+
|
183
|
+
def create_single_account_export(self, profile_data: Dict[str, Any], account_id: str, profile_name: str) -> str:
|
184
|
+
"""
|
185
|
+
Create markdown export for single account analysis.
|
186
|
+
|
187
|
+
Args:
|
188
|
+
profile_data: Single profile cost data
|
189
|
+
account_id: AWS account ID
|
190
|
+
profile_name: AWS profile name
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
Markdown formatted single account analysis
|
194
|
+
"""
|
195
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
|
196
|
+
|
197
|
+
# Create markdown content
|
198
|
+
markdown_content = f"""# AWS Cost Analysis - Account {account_id}
|
199
|
+
|
200
|
+
**Generated**: {timestamp}
|
201
|
+
**Profile**: {profile_name}
|
202
|
+
**Organization**: Single Account Analysis
|
203
|
+
|
204
|
+
## Executive Summary
|
205
|
+
|
206
|
+
| Metric | Value | Status |
|
207
|
+
|--------|-------|--------|
|
208
|
+
| Total Cost | ${profile_data.get("total_cost", 0):,.2f} | {self._get_cost_status_emoji(profile_data.get("total_cost", 0))} |
|
209
|
+
| Service Count | {len(profile_data.get("service_breakdown", []))} services | 📊 |
|
210
|
+
| Cost Trend | {profile_data.get("cost_trend", "Stable")} | {self._get_trend_emoji(profile_data.get("cost_trend", ""))} |
|
211
|
+
|
212
|
+
## Service Breakdown
|
213
|
+
|
214
|
+
| Service | Current Cost | Percentage | Trend | Optimization |
|
215
|
+
|---------|--------------|------------|-------|--------------|
|
216
|
+
"""
|
217
|
+
|
218
|
+
# Add service breakdown rows
|
219
|
+
services = profile_data.get("service_breakdown", [])
|
220
|
+
for service in services[:10]: # Top 10 services
|
221
|
+
service_name = service.get("service", "Unknown")
|
222
|
+
cost = service.get("cost", 0)
|
223
|
+
percentage = service.get("percentage", 0)
|
224
|
+
trend = service.get("trend", "Stable")
|
225
|
+
optimization = service.get("optimization_opportunity", "Monitor")
|
226
|
+
|
227
|
+
markdown_content += f"| {service_name} | ${cost:,.2f} | {percentage:.1f}% | {self._get_trend_emoji(trend)} {trend} | {optimization} |\n"
|
228
|
+
|
229
|
+
# Add resource optimization section
|
230
|
+
markdown_content += f"""
|
231
|
+
|
232
|
+
## Resource Optimization Opportunities
|
233
|
+
|
234
|
+
| Resource Type | Count | Potential Savings | Action Required |
|
235
|
+
|---------------|-------|------------------|-----------------|
|
236
|
+
| Stopped EC2 Instances | {profile_data.get("stopped_ec2", 0)} | ${profile_data.get("stopped_ec2_savings", 0):,.2f} | Review and terminate |
|
237
|
+
| Unused EBS Volumes | {profile_data.get("unused_volumes", 0)} | ${profile_data.get("unused_volume_savings", 0):,.2f} | Clean up unused storage |
|
238
|
+
| Unused Elastic IPs | {profile_data.get("unused_eips", 0)} | ${profile_data.get("unused_eip_savings", 0):,.2f} | Release unused IPs |
|
239
|
+
| Untagged Resources | {profile_data.get("untagged_resources", 0)} | N/A | Implement tagging strategy |
|
240
|
+
|
241
|
+
---
|
242
|
+
*Generated by CloudOps Runbooks FinOps Module v0.7.8*
|
243
|
+
"""
|
244
|
+
|
245
|
+
return markdown_content
|
246
|
+
|
247
|
+
def create_multi_account_export(
|
248
|
+
self, multi_profile_data: List[Dict[str, Any]], organization_info: Optional[Dict[str, Any]] = None
|
249
|
+
) -> str:
|
250
|
+
"""
|
251
|
+
Create 10-column markdown export for multi-account analysis.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
multi_profile_data: List of profile data dictionaries
|
255
|
+
organization_info: Optional organization metadata
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
Markdown formatted multi-account analysis with 10 columns
|
259
|
+
"""
|
260
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
|
261
|
+
account_count = len(multi_profile_data)
|
262
|
+
|
263
|
+
# Calculate organization totals
|
264
|
+
total_cost = sum(profile.get("total_cost", 0) for profile in multi_profile_data)
|
265
|
+
total_savings = sum(profile.get("potential_savings", 0) for profile in multi_profile_data)
|
266
|
+
|
267
|
+
# Create markdown content with 10-column format
|
268
|
+
markdown_content = f"""# AWS Multi-Account Cost Analysis
|
269
|
+
|
270
|
+
**Generated**: {timestamp}
|
271
|
+
**Organization**: {account_count} accounts
|
272
|
+
**Total Cost**: ${total_cost:,.2f}
|
273
|
+
**Potential Savings**: ${total_savings:,.2f}
|
274
|
+
|
275
|
+
## Executive Dashboard
|
276
|
+
|
277
|
+
| Metric | Value | Status |
|
278
|
+
|--------|-------|--------|
|
279
|
+
| Total Monthly Cost | ${total_cost:,.2f} | 💰 |
|
280
|
+
| Average per Account | ${total_cost / account_count:,.2f} | 📊 |
|
281
|
+
| Optimization Opportunity | ${total_savings:,.2f} ({total_savings / total_cost * 100:.1f}%) | 🎯 |
|
282
|
+
|
283
|
+
## Multi-Account Analysis (10-Column Format)
|
284
|
+
|
285
|
+
| Profile | Last Month | Current Month | Top 3 Services | Budget | Stopped EC2 | Unused Vol | Unused EIP | Savings | Untagged |
|
286
|
+
|---------|------------|---------------|-----------------|---------|-------------|------------|------------|---------|----------|
|
287
|
+
"""
|
288
|
+
|
289
|
+
# Add data rows for each profile
|
290
|
+
for profile_data in multi_profile_data:
|
291
|
+
profile_name = profile_data.get("profile_name", "Unknown")[:15] # Truncate for table width
|
292
|
+
last_month = profile_data.get("last_month_cost", 0)
|
293
|
+
current_month = profile_data.get("total_cost", 0)
|
294
|
+
|
295
|
+
# Get top 3 services
|
296
|
+
services = profile_data.get("service_breakdown", [])
|
297
|
+
top_services = [s.get("service", "")[:6] for s in services[:3]] # Truncate service names
|
298
|
+
top_services_str = ",".join(top_services) if top_services else "N/A"
|
299
|
+
|
300
|
+
# Budget status
|
301
|
+
budget_status = self._get_budget_status_emoji(profile_data.get("budget_status", "unknown"))
|
302
|
+
|
303
|
+
# Resource optimization data
|
304
|
+
stopped_ec2 = profile_data.get("stopped_ec2", 0)
|
305
|
+
unused_volumes = profile_data.get("unused_volumes", 0)
|
306
|
+
unused_eips = profile_data.get("unused_eips", 0)
|
307
|
+
potential_savings = profile_data.get("potential_savings", 0)
|
308
|
+
untagged_resources = profile_data.get("untagged_resources", 0)
|
309
|
+
|
310
|
+
# Add row to table
|
311
|
+
markdown_content += f"| {profile_name} | ${last_month:,.0f} | ${current_month:,.0f} | {top_services_str} | {budget_status} | {stopped_ec2} | {unused_volumes} | {unused_eips} | ${potential_savings:,.0f} | {untagged_resources} |\n"
|
312
|
+
|
313
|
+
# Add summary section
|
314
|
+
markdown_content += f"""
|
315
|
+
|
316
|
+
## Organization Summary
|
317
|
+
|
318
|
+
### Cost Trends
|
319
|
+
- **Month-over-Month Change**: {self._calculate_mom_change(multi_profile_data):.1f}%
|
320
|
+
- **Highest Cost Account**: {self._get_highest_cost_account(multi_profile_data)}
|
321
|
+
- **Most Opportunities**: {self._get_most_optimization_account(multi_profile_data)}
|
322
|
+
|
323
|
+
### Optimization Recommendations
|
324
|
+
1. **Immediate Actions**: Review {sum(p.get("stopped_ec2", 0) for p in multi_profile_data)} stopped EC2 instances
|
325
|
+
2. **Storage Cleanup**: Clean up {sum(p.get("unused_volumes", 0) for p in multi_profile_data)} unused EBS volumes
|
326
|
+
3. **Network Optimization**: Release {sum(p.get("unused_eips", 0) for p in multi_profile_data)} unused Elastic IPs
|
327
|
+
4. **Governance**: Tag {sum(p.get("untagged_resources", 0) for p in multi_profile_data)} untagged resources
|
328
|
+
|
329
|
+
---
|
330
|
+
*Generated by CloudOps Runbooks FinOps Module v0.7.8*
|
331
|
+
"""
|
332
|
+
|
333
|
+
return markdown_content
|
334
|
+
|
335
|
+
def export_to_file(self, markdown_content: str, filename: str, account_type: str = "single") -> str:
|
336
|
+
"""
|
337
|
+
Export markdown content to file with intelligent naming.
|
338
|
+
|
339
|
+
Args:
|
340
|
+
markdown_content: Markdown content to export
|
341
|
+
filename: Base filename (without extension)
|
342
|
+
account_type: Type of analysis (single, multi, organization)
|
343
|
+
|
344
|
+
Returns:
|
345
|
+
Path to exported file
|
346
|
+
"""
|
347
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
348
|
+
|
349
|
+
if not filename.endswith(".md"):
|
350
|
+
filename = f"{filename}_{account_type}_account_{timestamp}.md"
|
351
|
+
|
352
|
+
filepath = self.output_dir / filename
|
353
|
+
|
354
|
+
# Show progress indication
|
355
|
+
print_info(f"📝 Generating markdown export: {filename}")
|
356
|
+
|
357
|
+
try:
|
358
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
359
|
+
f.write(markdown_content)
|
360
|
+
|
361
|
+
print_success(f"Rich-styled markdown export saved: {filepath}")
|
362
|
+
print_info(f"🔗 Ready for MkDocs or GitHub documentation sharing")
|
363
|
+
return str(filepath)
|
364
|
+
|
365
|
+
except Exception as e:
|
366
|
+
print_warning(f"Failed to save markdown export: {e}")
|
367
|
+
return ""
|
368
|
+
|
369
|
+
def _get_cost_status_emoji(self, cost: float) -> str:
|
370
|
+
"""Get emoji based on cost level."""
|
371
|
+
if cost >= 10000:
|
372
|
+
return "🔴 High"
|
373
|
+
elif cost >= 1000:
|
374
|
+
return "🟡 Medium"
|
375
|
+
else:
|
376
|
+
return "🟢 Low"
|
377
|
+
|
378
|
+
def _get_trend_emoji(self, trend: str) -> str:
|
379
|
+
"""Get emoji for cost trend."""
|
380
|
+
trend_lower = trend.lower()
|
381
|
+
if "up" in trend_lower or "increas" in trend_lower:
|
382
|
+
return "📈"
|
383
|
+
elif "down" in trend_lower or "decreas" in trend_lower:
|
384
|
+
return "📉"
|
385
|
+
else:
|
386
|
+
return "➡️"
|
387
|
+
|
388
|
+
def _get_budget_status_emoji(self, status: str) -> str:
|
389
|
+
"""Get emoji for budget status."""
|
390
|
+
status_lower = status.lower()
|
391
|
+
if "over" in status_lower or "exceeded" in status_lower:
|
392
|
+
return "❌ Over"
|
393
|
+
elif "warn" in status_lower:
|
394
|
+
return "⚠️ Warn"
|
395
|
+
elif "ok" in status_lower or "good" in status_lower:
|
396
|
+
return "✅ OK"
|
397
|
+
else:
|
398
|
+
return "❓ Unknown"
|
399
|
+
|
400
|
+
def _calculate_mom_change(self, profiles: List[Dict[str, Any]]) -> float:
|
401
|
+
"""Calculate month-over-month change percentage."""
|
402
|
+
total_current = sum(p.get("total_cost", 0) for p in profiles)
|
403
|
+
total_last = sum(p.get("last_month_cost", 0) for p in profiles)
|
404
|
+
|
405
|
+
if total_last == 0:
|
406
|
+
return 0.0
|
407
|
+
|
408
|
+
return ((total_current - total_last) / total_last) * 100
|
409
|
+
|
410
|
+
def _get_highest_cost_account(self, profiles: List[Dict[str, Any]]) -> str:
|
411
|
+
"""Get the account with highest cost."""
|
412
|
+
if not profiles:
|
413
|
+
return "N/A"
|
414
|
+
|
415
|
+
highest = max(profiles, key=lambda p: p.get("total_cost", 0))
|
416
|
+
return highest.get("profile_name", "Unknown")[:20]
|
417
|
+
|
418
|
+
def _get_most_optimization_account(self, profiles: List[Dict[str, Any]]) -> str:
|
419
|
+
"""Get the account with most optimization opportunities."""
|
420
|
+
if not profiles:
|
421
|
+
return "N/A"
|
422
|
+
|
423
|
+
highest = max(profiles, key=lambda p: p.get("potential_savings", 0))
|
424
|
+
return highest.get("profile_name", "Unknown")[:20]
|
425
|
+
|
426
|
+
|
427
|
+
def export_finops_to_markdown(
|
428
|
+
profile_data: Union[Dict[str, Any], List[Dict[str, Any]]],
|
429
|
+
output_dir: str = "./exports",
|
430
|
+
filename: str = "finops_analysis",
|
431
|
+
account_type: str = "auto",
|
432
|
+
) -> str:
|
433
|
+
"""
|
434
|
+
Export FinOps analysis to markdown format.
|
435
|
+
|
436
|
+
Args:
|
437
|
+
profile_data: Single profile dict or list of profiles
|
438
|
+
output_dir: Output directory for exports
|
439
|
+
filename: Base filename for export
|
440
|
+
account_type: Type of analysis (single, multi, auto)
|
441
|
+
|
442
|
+
Returns:
|
443
|
+
Path to exported markdown file
|
444
|
+
"""
|
445
|
+
exporter = MarkdownExporter(output_dir)
|
446
|
+
|
447
|
+
# Determine account type if auto
|
448
|
+
if account_type == "auto":
|
449
|
+
account_type = "multi" if isinstance(profile_data, list) else "single"
|
450
|
+
|
451
|
+
# Generate appropriate markdown content
|
452
|
+
if account_type == "single" and isinstance(profile_data, dict):
|
453
|
+
markdown_content = exporter.create_single_account_export(
|
454
|
+
profile_data, profile_data.get("account_id", "Unknown"), profile_data.get("profile_name", "Unknown")
|
455
|
+
)
|
456
|
+
elif account_type == "multi" and isinstance(profile_data, list):
|
457
|
+
markdown_content = exporter.create_multi_account_export(profile_data)
|
458
|
+
else:
|
459
|
+
raise ValueError(f"Invalid combination: account_type={account_type}, data_type={type(profile_data)}")
|
460
|
+
|
461
|
+
# Export to file
|
462
|
+
return exporter.export_to_file(markdown_content, filename, account_type)
|
463
|
+
|
464
|
+
|
465
|
+
# Export public interface
|
466
|
+
__all__ = ["MarkdownExporter", "export_finops_to_markdown"]
|