runbooks 0.9.2__py3-none-any.whl → 0.9.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +15 -6
- runbooks/cfat/__init__.py +3 -1
- runbooks/cloudops/__init__.py +3 -1
- runbooks/common/aws_utils.py +367 -0
- runbooks/common/enhanced_logging_example.py +239 -0
- runbooks/common/enhanced_logging_integration_example.py +257 -0
- runbooks/common/logging_integration_helper.py +344 -0
- runbooks/common/profile_utils.py +8 -6
- runbooks/common/rich_utils.py +347 -3
- runbooks/enterprise/logging.py +400 -38
- runbooks/finops/README.md +262 -406
- runbooks/finops/__init__.py +44 -1
- runbooks/finops/accuracy_cross_validator.py +12 -3
- runbooks/finops/business_cases.py +552 -0
- runbooks/finops/commvault_ec2_analysis.py +415 -0
- runbooks/finops/cost_processor.py +718 -42
- runbooks/finops/dashboard_router.py +44 -22
- runbooks/finops/dashboard_runner.py +302 -39
- runbooks/finops/embedded_mcp_validator.py +358 -48
- runbooks/finops/finops_scenarios.py +1122 -0
- runbooks/finops/helpers.py +182 -0
- runbooks/finops/multi_dashboard.py +30 -15
- runbooks/finops/scenarios.py +789 -0
- runbooks/finops/single_dashboard.py +386 -58
- runbooks/finops/types.py +29 -4
- runbooks/inventory/__init__.py +2 -1
- runbooks/main.py +522 -29
- runbooks/operate/__init__.py +3 -1
- runbooks/remediation/__init__.py +3 -1
- runbooks/remediation/commons.py +55 -16
- runbooks/remediation/commvault_ec2_analysis.py +259 -0
- runbooks/remediation/rds_snapshot_list.py +267 -102
- runbooks/remediation/workspaces_list.py +182 -31
- runbooks/security/__init__.py +3 -1
- runbooks/sre/__init__.py +2 -1
- runbooks/utils/__init__.py +81 -6
- runbooks/utils/version_validator.py +241 -0
- runbooks/vpc/__init__.py +2 -1
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/METADATA +98 -60
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/RECORD +44 -39
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/entry_points.txt +1 -0
- runbooks/inventory/cloudtrail.md +0 -727
- runbooks/inventory/discovery.md +0 -81
- runbooks/remediation/CLAUDE.md +0 -100
- runbooks/remediation/DOME9.md +0 -218
- runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +0 -506
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/WHEEL +0 -0
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.2.dist-info → runbooks-0.9.5.dist-info}/top_level.txt +0 -0
@@ -55,13 +55,22 @@ from .cost_processor import (
|
|
55
55
|
get_cost_data,
|
56
56
|
process_service_costs,
|
57
57
|
)
|
58
|
-
from .
|
59
|
-
|
60
|
-
|
61
|
-
|
58
|
+
from runbooks.common.profile_utils import (
|
59
|
+
create_cost_session,
|
60
|
+
create_management_session,
|
61
|
+
create_operational_session,
|
62
62
|
)
|
63
63
|
from .enhanced_progress import EnhancedProgressTracker
|
64
64
|
from .helpers import export_cost_dashboard_to_pdf
|
65
|
+
|
66
|
+
# Embedded MCP Integration for Cross-Validation (Enterprise Accuracy Standards)
|
67
|
+
try:
|
68
|
+
from .embedded_mcp_validator import EmbeddedMCPValidator, validate_finops_results_with_embedded_mcp
|
69
|
+
EMBEDDED_MCP_AVAILABLE = True
|
70
|
+
print_info("Enterprise accuracy validation enabled - Embedded MCP validator loaded successfully")
|
71
|
+
except ImportError:
|
72
|
+
EMBEDDED_MCP_AVAILABLE = False
|
73
|
+
print_warning("Cross-validation unavailable - Embedded MCP validation module not found")
|
65
74
|
from .service_mapping import get_service_display_name
|
66
75
|
|
67
76
|
|
@@ -155,9 +164,9 @@ class SingleAccountDashboard:
|
|
155
164
|
"""Execute the service-focused cost analysis."""
|
156
165
|
try:
|
157
166
|
# Initialize sessions
|
158
|
-
cost_session =
|
159
|
-
mgmt_session =
|
160
|
-
ops_session =
|
167
|
+
cost_session = create_cost_session(profile)
|
168
|
+
mgmt_session = create_management_session(profile)
|
169
|
+
ops_session = create_operational_session(profile)
|
161
170
|
|
162
171
|
# Initialize account resolver for readable account names
|
163
172
|
management_profile = os.getenv("MANAGEMENT_PROFILE") or profile
|
@@ -262,31 +271,53 @@ class SingleAccountDashboard:
|
|
262
271
|
# Prepare service data for markdown export with Tax filtering
|
263
272
|
current_services = cost_data.get("costs_by_service", {})
|
264
273
|
previous_services = last_month_data.get("costs_by_service", {}) # Use already collected data
|
274
|
+
quarterly_services = last_month_data.get("quarterly_costs_by_service", {}) # Add quarterly data
|
265
275
|
|
266
276
|
# Apply same Tax filtering for consistent markdown export
|
267
277
|
filtered_current_services = filter_analytical_services(current_services)
|
268
278
|
filtered_previous_services = filter_analytical_services(previous_services)
|
279
|
+
filtered_quarterly_services = filter_analytical_services(quarterly_services)
|
269
280
|
|
270
281
|
all_services_sorted = sorted(filtered_current_services.items(), key=lambda x: x[1], reverse=True)
|
271
282
|
|
272
|
-
# Calculate totals for markdown export
|
283
|
+
# Calculate totals for markdown export with quarterly context
|
273
284
|
total_current = cost_data.get("current_month", 0)
|
274
285
|
total_previous = cost_data.get("last_month", 0)
|
286
|
+
total_quarterly = sum(filtered_quarterly_services.values())
|
275
287
|
total_trend_pct = ((total_current - total_previous) / total_previous * 100) if total_previous > 0 else 0
|
276
288
|
|
277
289
|
self._export_service_table_to_markdown(
|
278
290
|
all_services_sorted,
|
279
291
|
filtered_current_services,
|
280
292
|
filtered_previous_services,
|
293
|
+
filtered_quarterly_services,
|
281
294
|
profile,
|
282
295
|
account_id,
|
283
296
|
total_current,
|
284
297
|
total_previous,
|
298
|
+
total_quarterly,
|
285
299
|
total_trend_pct,
|
286
300
|
args,
|
287
301
|
)
|
288
302
|
|
289
303
|
print_success(f"Service analysis completed for account {account_id}")
|
304
|
+
|
305
|
+
# Export functionality - Add PDF/CSV/JSON support to enhanced router
|
306
|
+
# Get service data for export (recreate since it's scoped to display function)
|
307
|
+
current_services = cost_data.get("costs_by_service", {})
|
308
|
+
filtered_services = filter_analytical_services(current_services)
|
309
|
+
service_list = sorted(filtered_services.items(), key=lambda x: x[1], reverse=True)
|
310
|
+
self._handle_exports(args, profile, account_id, service_list, cost_data, last_month_data)
|
311
|
+
|
312
|
+
# MCP Cross-Validation for Enterprise Accuracy Standards (>=99.5%)
|
313
|
+
# Note: User explicitly requested real MCP validation after discovering fabricated accuracy claims
|
314
|
+
validate_flag = getattr(args, 'validate', False)
|
315
|
+
if validate_flag or EMBEDDED_MCP_AVAILABLE:
|
316
|
+
if EMBEDDED_MCP_AVAILABLE:
|
317
|
+
self._run_embedded_mcp_validation([profile], cost_data, service_list, args)
|
318
|
+
else:
|
319
|
+
self.console.print(f"[yellow]⚠️ MCP validation requested but not available - check MCP server configuration[/]")
|
320
|
+
|
290
321
|
return 0
|
291
322
|
|
292
323
|
except Exception as e:
|
@@ -294,14 +325,57 @@ class SingleAccountDashboard:
|
|
294
325
|
return 1
|
295
326
|
|
296
327
|
def _get_last_month_trends(self, cost_session: boto3.Session, profile: str) -> Dict[str, Any]:
|
297
|
-
"""
|
328
|
+
"""
|
329
|
+
Get accurate trend data using equal-period comparisons with quarterly context.
|
330
|
+
|
331
|
+
MATHEMATICAL FIX: Replaces the previous implementation that used 60-day time ranges
|
332
|
+
which created unequal period comparisons (e.g., 2 days vs 31 days).
|
333
|
+
|
334
|
+
Now uses month-to-date vs same period from previous month for accurate trends,
|
335
|
+
enhanced with quarterly data for strategic financial intelligence.
|
336
|
+
"""
|
298
337
|
try:
|
299
|
-
#
|
300
|
-
|
301
|
-
|
338
|
+
# Use the corrected get_cost_data function without time_range parameter
|
339
|
+
# This will use the enhanced logic for equal-period comparisons
|
340
|
+
corrected_trend_data = get_cost_data(cost_session, None, None, profile_name=profile)
|
341
|
+
|
342
|
+
# ENHANCEMENT: Add quarterly cost data for strategic context
|
343
|
+
from .cost_processor import get_quarterly_cost_data
|
344
|
+
quarterly_costs = get_quarterly_cost_data(cost_session, profile_name=profile)
|
345
|
+
|
346
|
+
# Integrate quarterly data into trend data structure
|
347
|
+
corrected_trend_data["quarterly_costs_by_service"] = quarterly_costs
|
348
|
+
|
349
|
+
# Log the trend analysis context for transparency
|
350
|
+
if "period_metadata" in corrected_trend_data:
|
351
|
+
metadata = corrected_trend_data["period_metadata"]
|
352
|
+
current_days = metadata.get("current_days", 0)
|
353
|
+
previous_days = metadata.get("previous_days", 0)
|
354
|
+
reliability = metadata.get("trend_reliability", "unknown")
|
355
|
+
|
356
|
+
if metadata.get("is_partial_comparison", False):
|
357
|
+
print_warning(f"Partial period comparison detected: {current_days} vs {previous_days} days")
|
358
|
+
print_info(f"Trend reliability: {reliability}")
|
359
|
+
else:
|
360
|
+
print_success(f"Equal period comparison: {current_days} vs {previous_days} days")
|
361
|
+
|
362
|
+
return corrected_trend_data
|
363
|
+
|
302
364
|
except Exception as e:
|
303
|
-
print_warning(f"
|
304
|
-
|
365
|
+
print_warning(f"Enhanced trend data collection failed: {str(e)[:50]}")
|
366
|
+
# Return basic structure to prevent downstream errors
|
367
|
+
return {
|
368
|
+
"current_month": 0,
|
369
|
+
"last_month": 0,
|
370
|
+
"costs_by_service": {},
|
371
|
+
"quarterly_costs_by_service": {}, # Added for quarterly intelligence
|
372
|
+
"period_metadata": {
|
373
|
+
"current_days": 0,
|
374
|
+
"previous_days": 0,
|
375
|
+
"is_partial_comparison": True,
|
376
|
+
"trend_reliability": "unavailable"
|
377
|
+
}
|
378
|
+
}
|
305
379
|
|
306
380
|
def _analyze_service_utilization(self, ops_session: boto3.Session, cost_data: Dict[str, Any]) -> Dict[str, Any]:
|
307
381
|
"""Analyze service utilization patterns for optimization opportunities."""
|
@@ -437,45 +511,80 @@ class SingleAccountDashboard:
|
|
437
511
|
table = Table(
|
438
512
|
Column("Service", style="resource", width=20),
|
439
513
|
Column("Current Cost", justify="right", style="cost", width=15),
|
440
|
-
Column("Last Month", justify="right", width=
|
514
|
+
Column("Last Month", justify="right", width=12),
|
515
|
+
Column("Last Quarter", justify="right", width=12),
|
441
516
|
Column("Trend", justify="center", width=10),
|
442
517
|
Column("Optimization Opportunities", width=35),
|
443
518
|
title=f"🎯 TOP {top_services} Services Analysis - {account_display}",
|
444
519
|
box=box.ROUNDED,
|
445
520
|
show_lines=True,
|
446
521
|
style="bright_cyan",
|
447
|
-
caption=f"[dim]Service-focused analysis • {account_caption} • Each row represents one service[/]",
|
522
|
+
caption=f"[dim]Service-focused analysis with quarterly intelligence • {account_caption} • Each row represents one service[/]",
|
448
523
|
)
|
449
524
|
|
450
|
-
# Get current and
|
525
|
+
# Get current, previous, and quarterly service costs
|
451
526
|
current_services = cost_data.get("costs_by_service", {})
|
452
527
|
previous_services = last_month_data.get("costs_by_service", {})
|
528
|
+
quarterly_services = last_month_data.get("quarterly_costs_by_service", {})
|
453
529
|
|
454
530
|
# WIP.md requirement: Exclude "Tax" service as it provides no analytical insights
|
455
531
|
# Use centralized filtering function for consistency across all dashboards
|
456
532
|
filtered_current_services = filter_analytical_services(current_services)
|
457
533
|
filtered_previous_services = filter_analytical_services(previous_services)
|
458
|
-
|
459
|
-
|
460
|
-
|
534
|
+
filtered_quarterly_services = filter_analytical_services(quarterly_services)
|
535
|
+
|
536
|
+
# Create comprehensive service list from current, previous, and quarterly periods
|
537
|
+
# This ensures services appear even when current costs are $0 but historical costs existed
|
538
|
+
all_service_names = set(filtered_current_services.keys()) | set(filtered_previous_services.keys()) | set(filtered_quarterly_services.keys())
|
539
|
+
|
540
|
+
# Build service data with current, previous, and quarterly costs for intelligent sorting
|
541
|
+
service_data = []
|
542
|
+
for service_name in all_service_names:
|
543
|
+
current_cost = filtered_current_services.get(service_name, 0.0)
|
544
|
+
previous_cost = filtered_previous_services.get(service_name, 0.0)
|
545
|
+
quarterly_cost = filtered_quarterly_services.get(service_name, 0.0)
|
546
|
+
|
547
|
+
# Sort by max(current_cost, previous_cost, quarterly_cost) to show most relevant services first
|
548
|
+
# This ensures services with historical significance appear prominently
|
549
|
+
max_cost = max(current_cost, previous_cost, quarterly_cost)
|
550
|
+
service_data.append((service_name, current_cost, previous_cost, quarterly_cost, max_cost))
|
551
|
+
|
552
|
+
# Sort by maximum cost across current, previous, and quarterly periods
|
553
|
+
all_services = sorted(service_data, key=lambda x: x[4], reverse=True)
|
461
554
|
top_services_list = all_services[:top_services]
|
462
555
|
remaining_services = all_services[top_services:]
|
463
556
|
|
464
557
|
# Add individual service rows
|
465
|
-
for service, current_cost in top_services_list:
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
558
|
+
for service, current_cost, previous_cost, quarterly_cost, _ in top_services_list:
|
559
|
+
|
560
|
+
# Calculate trend using quarterly-enhanced intelligence
|
561
|
+
from .cost_processor import calculate_quarterly_enhanced_trend
|
562
|
+
|
563
|
+
# Get period metadata for intelligent trend analysis
|
564
|
+
period_metadata = last_month_data.get("period_metadata", {})
|
565
|
+
current_days = period_metadata.get("current_days")
|
566
|
+
previous_days = period_metadata.get("previous_days")
|
567
|
+
|
568
|
+
# Use quarterly-enhanced trend calculation with strategic context
|
569
|
+
trend_display = calculate_quarterly_enhanced_trend(
|
570
|
+
current_cost,
|
571
|
+
previous_cost,
|
572
|
+
quarterly_cost,
|
573
|
+
current_days,
|
574
|
+
previous_days
|
575
|
+
)
|
576
|
+
|
577
|
+
# Apply Rich formatting to the trend display
|
578
|
+
if "⚠️" in trend_display:
|
579
|
+
trend_display = f"[yellow]{trend_display}[/]"
|
580
|
+
elif "↑" in trend_display:
|
581
|
+
trend_display = f"[red]{trend_display}[/]"
|
582
|
+
elif "↓" in trend_display:
|
583
|
+
trend_display = f"[green]{trend_display}[/]"
|
584
|
+
elif "→" in trend_display:
|
585
|
+
trend_display = f"[yellow]{trend_display}[/]"
|
477
586
|
else:
|
478
|
-
trend_display = "[dim]
|
587
|
+
trend_display = f"[dim]{trend_display}[/]"
|
479
588
|
|
480
589
|
# Enhanced service-specific optimization recommendations
|
481
590
|
optimization_rec = self._get_enhanced_service_recommendation(service, current_cost, previous_cost)
|
@@ -484,24 +593,35 @@ class SingleAccountDashboard:
|
|
484
593
|
display_name = get_service_display_name(service)
|
485
594
|
|
486
595
|
table.add_row(
|
487
|
-
display_name, format_cost(current_cost), format_cost(previous_cost), trend_display, optimization_rec
|
596
|
+
display_name, format_cost(current_cost), format_cost(previous_cost), format_cost(quarterly_cost), trend_display, optimization_rec
|
488
597
|
)
|
489
598
|
|
490
599
|
# Add "Other Services" summary row if there are remaining services
|
491
600
|
if remaining_services:
|
492
|
-
other_current = sum(
|
493
|
-
other_previous = sum(
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
601
|
+
other_current = sum(current_cost for _, current_cost, _, _, _ in remaining_services)
|
602
|
+
other_previous = sum(previous_cost for _, _, previous_cost, _, _ in remaining_services)
|
603
|
+
other_quarterly = sum(quarterly_cost for _, _, _, quarterly_cost, _ in remaining_services)
|
604
|
+
|
605
|
+
# Use quarterly-enhanced trend calculation for "Other Services" as well
|
606
|
+
other_trend = calculate_quarterly_enhanced_trend(
|
607
|
+
other_current,
|
608
|
+
other_previous,
|
609
|
+
other_quarterly,
|
610
|
+
current_days,
|
611
|
+
previous_days
|
612
|
+
)
|
613
|
+
|
614
|
+
# Apply Rich formatting
|
615
|
+
if "⚠️" in other_trend:
|
616
|
+
other_trend = f"[yellow]{other_trend}[/]"
|
617
|
+
elif "↑" in other_trend:
|
618
|
+
other_trend = f"[red]{other_trend}[/]"
|
619
|
+
elif "↓" in other_trend:
|
620
|
+
other_trend = f"[green]{other_trend}[/]"
|
621
|
+
elif "→" in other_trend:
|
622
|
+
other_trend = f"[yellow]{other_trend}[/]"
|
503
623
|
else:
|
504
|
-
other_trend = "[dim]
|
624
|
+
other_trend = f"[dim]{other_trend}[/]"
|
505
625
|
|
506
626
|
other_optimization = (
|
507
627
|
f"[dim]{len(remaining_services)} services: review individually for optimization opportunities[/]"
|
@@ -512,6 +632,7 @@ class SingleAccountDashboard:
|
|
512
632
|
"[dim]Other Services[/]",
|
513
633
|
format_cost(other_current),
|
514
634
|
format_cost(other_previous),
|
635
|
+
format_cost(other_quarterly),
|
515
636
|
other_trend,
|
516
637
|
other_optimization,
|
517
638
|
style="dim",
|
@@ -522,7 +643,16 @@ class SingleAccountDashboard:
|
|
522
643
|
# Summary panel (using filtered services for consistent analysis)
|
523
644
|
total_current = sum(filtered_current_services.values())
|
524
645
|
total_previous = sum(filtered_previous_services.values())
|
525
|
-
|
646
|
+
total_quarterly = sum(filtered_quarterly_services.values())
|
647
|
+
|
648
|
+
# Use quarterly-enhanced trend calculation for total trend as well
|
649
|
+
total_trend_display = calculate_quarterly_enhanced_trend(
|
650
|
+
total_current,
|
651
|
+
total_previous,
|
652
|
+
total_quarterly,
|
653
|
+
current_days,
|
654
|
+
previous_days
|
655
|
+
)
|
526
656
|
|
527
657
|
# Use readable account name in summary
|
528
658
|
if self.account_resolver and account_id != "Unknown":
|
@@ -534,13 +664,21 @@ class SingleAccountDashboard:
|
|
534
664
|
else:
|
535
665
|
account_summary_line = f"• Profile: {profile}"
|
536
666
|
|
667
|
+
# Add period information to summary for transparency
|
668
|
+
period_info = ""
|
669
|
+
if period_metadata.get("is_partial_comparison", False):
|
670
|
+
period_info = f"\n• Period Comparison: {current_days} vs {previous_days} days (partial month)"
|
671
|
+
else:
|
672
|
+
period_info = f"\n• Period Comparison: {current_days} vs {previous_days} days (equal periods)"
|
673
|
+
|
537
674
|
summary_text = f"""
|
538
675
|
[highlight]Account Summary[/]
|
539
676
|
{account_summary_line}
|
540
677
|
• Total Current: {format_cost(total_current)}
|
541
678
|
• Total Previous: {format_cost(total_previous)}
|
542
|
-
•
|
543
|
-
•
|
679
|
+
• Total Quarterly: {format_cost(total_quarterly)}
|
680
|
+
• Overall Trend: {total_trend_display}
|
681
|
+
• Services Analyzed: {len(all_services)}{period_info}
|
544
682
|
"""
|
545
683
|
|
546
684
|
self.console.print(Panel(summary_text.strip(), title="📊 Analysis Summary", style="info"))
|
@@ -579,10 +717,12 @@ class SingleAccountDashboard:
|
|
579
717
|
sorted_services,
|
580
718
|
current_services,
|
581
719
|
previous_services,
|
720
|
+
quarterly_services,
|
582
721
|
profile,
|
583
722
|
account_id,
|
584
723
|
total_current,
|
585
724
|
total_previous,
|
725
|
+
total_quarterly,
|
586
726
|
total_trend_pct,
|
587
727
|
args,
|
588
728
|
):
|
@@ -618,32 +758,34 @@ class SingleAccountDashboard:
|
|
618
758
|
lines.append("## Service Cost Breakdown")
|
619
759
|
lines.append("")
|
620
760
|
|
621
|
-
# Create GitHub-compatible markdown table with
|
622
|
-
lines.append("| Service | Last Month |
|
623
|
-
lines.append("| --- | ---: | ---: | :---: | --- |") # GitHub-compliant alignment
|
761
|
+
# Create GitHub-compatible markdown table with quarterly intelligence
|
762
|
+
lines.append("| Service | Current Cost | Last Month | Last Quarter | Trend | Optimization Opportunities |")
|
763
|
+
lines.append("| --- | ---: | ---: | ---: | :---: | --- |") # GitHub-compliant alignment with quarterly column
|
624
764
|
|
625
|
-
# Add TOP 10 services with
|
765
|
+
# Add TOP 10 services with quarterly context
|
626
766
|
for i, (service_name, current_cost) in enumerate(sorted_services[:10]):
|
627
767
|
previous_cost = previous_services.get(service_name, 0)
|
768
|
+
quarterly_cost = quarterly_services.get(service_name, 0)
|
628
769
|
trend_pct = ((current_cost - previous_cost) / previous_cost * 100) if previous_cost > 0 else 0
|
629
770
|
trend_icon = "⬆️" if trend_pct > 0 else "⬇️" if trend_pct < 0 else "➡️"
|
630
771
|
|
631
|
-
# Generate optimization recommendation
|
772
|
+
# Generate optimization recommendation with quarterly context
|
632
773
|
optimization = self._get_service_optimization(service_name, current_cost, previous_cost)
|
633
774
|
|
634
|
-
# Format row for GitHub-compatible table
|
775
|
+
# Format row for GitHub-compatible table with quarterly data
|
635
776
|
service_name_clean = service_name.replace("|", "\\|") # Escape pipes in service names
|
636
777
|
optimization_clean = optimization.replace("|", "\\|") # Escape pipes in text
|
637
778
|
|
638
779
|
lines.append(
|
639
|
-
f"| {service_name_clean} | ${previous_cost:.2f} | ${
|
780
|
+
f"| {service_name_clean} | ${current_cost:.2f} | ${previous_cost:.2f} | ${quarterly_cost:.2f} | {trend_icon} {abs(trend_pct):.1f}% | {optimization_clean} |"
|
640
781
|
)
|
641
782
|
|
642
|
-
# Add Others row if there are remaining services
|
783
|
+
# Add Others row with quarterly context if there are remaining services
|
643
784
|
remaining_services = sorted_services[10:]
|
644
785
|
if remaining_services:
|
645
786
|
others_current = sum(current_cost for _, current_cost in remaining_services)
|
646
787
|
others_previous = sum(previous_services.get(service_name, 0) for service_name, _ in remaining_services)
|
788
|
+
others_quarterly = sum(quarterly_services.get(service_name, 0) for service_name, _ in remaining_services)
|
647
789
|
others_trend_pct = (
|
648
790
|
((others_current - others_previous) / others_previous * 100) if others_previous > 0 else 0
|
649
791
|
)
|
@@ -651,7 +793,7 @@ class SingleAccountDashboard:
|
|
651
793
|
|
652
794
|
others_row = f"Others ({len(remaining_services)} services)"
|
653
795
|
lines.append(
|
654
|
-
f"| {others_row} | ${others_previous:.2f} | ${
|
796
|
+
f"| {others_row} | ${others_current:.2f} | ${others_previous:.2f} | ${others_quarterly:.2f} | {trend_icon} {abs(others_trend_pct):.1f}% | Review individually for optimization |"
|
655
797
|
)
|
656
798
|
|
657
799
|
lines.append("")
|
@@ -659,6 +801,7 @@ class SingleAccountDashboard:
|
|
659
801
|
lines.append("")
|
660
802
|
lines.append(f"- **Total Current Cost:** ${total_current:,.2f}")
|
661
803
|
lines.append(f"- **Total Previous Cost:** ${total_previous:,.2f}")
|
804
|
+
lines.append(f"- **Total Quarterly Cost:** ${total_quarterly:,.2f}")
|
662
805
|
trend_icon = "⬆️" if total_trend_pct > 0 else "⬇️" if total_trend_pct < 0 else "➡️"
|
663
806
|
lines.append(f"- **Overall Trend:** {trend_icon} {abs(total_trend_pct):.1f}%")
|
664
807
|
lines.append(f"- **Services Analyzed:** {len(sorted_services)}")
|
@@ -703,6 +846,191 @@ class SingleAccountDashboard:
|
|
703
846
|
return "Monitor usage patterns & optimization opportunities"
|
704
847
|
else: # Lower cost services
|
705
848
|
return "Continue monitoring for optimization opportunities"
|
849
|
+
|
850
|
+
def _handle_exports(self, args: argparse.Namespace, profile: str, account_id: str,
|
851
|
+
services_data, cost_data, last_month_data) -> None:
|
852
|
+
"""Handle all export formats for enhanced router."""
|
853
|
+
if not (hasattr(args, 'report_name') and args.report_name and
|
854
|
+
hasattr(args, 'report_type') and args.report_type):
|
855
|
+
return
|
856
|
+
|
857
|
+
self.console.print(f"[cyan]📊 Processing export requests...[/]")
|
858
|
+
|
859
|
+
# Convert service data to ProfileData format compatible with existing export functions
|
860
|
+
from .types import ProfileData
|
861
|
+
|
862
|
+
try:
|
863
|
+
# Create ProfileData compatible structure with dual-metric foundation
|
864
|
+
export_data = [ProfileData(
|
865
|
+
profile_name=profile,
|
866
|
+
account_id=account_id,
|
867
|
+
current_month=cost_data.get("current_month", 0), # Primary: UnblendedCost
|
868
|
+
current_month_formatted=f"${cost_data.get('current_month', 0):,.2f}",
|
869
|
+
previous_month=cost_data.get("last_month", 0), # Primary: UnblendedCost
|
870
|
+
previous_month_formatted=f"${cost_data.get('last_month', 0):,.2f}",
|
871
|
+
# Dual-metric architecture foundation (to be implemented)
|
872
|
+
current_month_amortized=None, # Secondary: AmortizedCost
|
873
|
+
previous_month_amortized=None, # Secondary: AmortizedCost
|
874
|
+
current_month_amortized_formatted=None,
|
875
|
+
previous_month_amortized_formatted=None,
|
876
|
+
metric_context="technical", # Default to technical context (UnblendedCost)
|
877
|
+
service_costs=[], # Service costs in simplified format
|
878
|
+
service_costs_formatted=[f"${cost:.2f}" for _, cost in services_data[:10]],
|
879
|
+
budget_info=[],
|
880
|
+
ec2_summary={},
|
881
|
+
ec2_summary_formatted=[],
|
882
|
+
success=True,
|
883
|
+
error=None,
|
884
|
+
current_period_name="Current Month",
|
885
|
+
previous_period_name="Previous Month",
|
886
|
+
percent_change_in_total_cost=None
|
887
|
+
)]
|
888
|
+
|
889
|
+
# Process each requested export type
|
890
|
+
export_count = 0
|
891
|
+
for report_type in args.report_type:
|
892
|
+
if report_type == "pdf":
|
893
|
+
self.console.print(f"[cyan]Generating PDF export...[/]")
|
894
|
+
pdf_path = export_cost_dashboard_to_pdf(
|
895
|
+
export_data,
|
896
|
+
args.report_name,
|
897
|
+
getattr(args, 'dir', None),
|
898
|
+
previous_period_dates="Previous Month",
|
899
|
+
current_period_dates="Current Month"
|
900
|
+
)
|
901
|
+
if pdf_path:
|
902
|
+
print_success(f"PDF export completed: {pdf_path}")
|
903
|
+
export_count += 1
|
904
|
+
else:
|
905
|
+
self.console.print(f"[red]PDF export failed[/]")
|
906
|
+
|
907
|
+
elif report_type == "csv":
|
908
|
+
self.console.print(f"[cyan]Generating CSV export...[/]")
|
909
|
+
from .cost_processor import export_to_csv
|
910
|
+
csv_path = export_to_csv(
|
911
|
+
export_data,
|
912
|
+
args.report_name,
|
913
|
+
getattr(args, 'dir', None),
|
914
|
+
previous_period_dates="Previous Month",
|
915
|
+
current_period_dates="Current Month"
|
916
|
+
)
|
917
|
+
if csv_path:
|
918
|
+
print_success(f"CSV export completed: {csv_path}")
|
919
|
+
export_count += 1
|
920
|
+
|
921
|
+
elif report_type == "json":
|
922
|
+
self.console.print(f"[cyan]Generating JSON export...[/]")
|
923
|
+
from .cost_processor import export_to_json
|
924
|
+
json_path = export_to_json(export_data, args.report_name, getattr(args, 'dir', None))
|
925
|
+
if json_path:
|
926
|
+
print_success(f"JSON export completed: {json_path}")
|
927
|
+
export_count += 1
|
928
|
+
|
929
|
+
elif report_type == "markdown":
|
930
|
+
self.console.print(f"[cyan]Generating Markdown export...[/]")
|
931
|
+
# Use existing markdown export functionality
|
932
|
+
self._export_service_table_to_markdown(
|
933
|
+
services_data[:10], {}, {}, # Simplified data structure
|
934
|
+
profile, account_id,
|
935
|
+
cost_data.get("current_month", 0),
|
936
|
+
cost_data.get("last_month", 0),
|
937
|
+
0, args # Simplified trend calculation
|
938
|
+
)
|
939
|
+
export_count += 1
|
940
|
+
|
941
|
+
if export_count > 0:
|
942
|
+
self.console.print(f"[bright_green]✅ {export_count} exports completed successfully[/]")
|
943
|
+
else:
|
944
|
+
self.console.print(f"[yellow]⚠️ No exports were generated[/]")
|
945
|
+
|
946
|
+
except Exception as e:
|
947
|
+
self.console.print(f"[red]❌ Export failed: {str(e)}[/]")
|
948
|
+
import traceback
|
949
|
+
self.console.print(f"[red]Details: {traceback.format_exc()}[/]")
|
950
|
+
|
951
|
+
def _run_embedded_mcp_validation(self, profiles: List[str], cost_data: Dict[str, Any],
|
952
|
+
service_list: List[Tuple[str, float]], args: argparse.Namespace) -> None:
|
953
|
+
"""
|
954
|
+
Run embedded MCP cross-validation for single account dashboard with real-time AWS API comparison.
|
955
|
+
|
956
|
+
This addresses the user's critical feedback about fabricated accuracy claims by providing
|
957
|
+
genuine MCP validation with actual AWS Cost Explorer API cross-validation.
|
958
|
+
"""
|
959
|
+
try:
|
960
|
+
self.console.print(f"\n[bright_cyan]🔍 Embedded MCP Cross-Validation: Enterprise Accuracy Check[/]")
|
961
|
+
self.console.print(f"[dim]Validating single account with direct AWS API integration[/]")
|
962
|
+
|
963
|
+
# Prepare runbooks data in format expected by MCP validator
|
964
|
+
runbooks_data = {
|
965
|
+
profiles[0]: {
|
966
|
+
"total_cost": cost_data.get("current_month", 0),
|
967
|
+
"services": dict(service_list) if service_list else {},
|
968
|
+
"profile": profiles[0],
|
969
|
+
}
|
970
|
+
}
|
971
|
+
|
972
|
+
# Run embedded validation
|
973
|
+
validator = EmbeddedMCPValidator(profiles=profiles, console=self.console)
|
974
|
+
validation_results = validator.validate_cost_data(runbooks_data)
|
975
|
+
|
976
|
+
# Enhanced results display with detailed variance information (same as dashboard_runner.py)
|
977
|
+
overall_accuracy = validation_results.get("total_accuracy", 0)
|
978
|
+
profiles_validated = validation_results.get("profiles_validated", 0)
|
979
|
+
passed = validation_results.get("passed_validation", False)
|
980
|
+
profile_results = validation_results.get("profile_results", [])
|
981
|
+
|
982
|
+
self.console.print(f"\n[bright_cyan]🔍 MCP Cross-Validation Results:[/]")
|
983
|
+
|
984
|
+
# Display detailed per-profile results
|
985
|
+
for profile_result in profile_results:
|
986
|
+
profile_name = profile_result.get("profile", "Unknown")[:30]
|
987
|
+
runbooks_cost = profile_result.get("runbooks_cost", 0)
|
988
|
+
aws_cost = profile_result.get("aws_api_cost", 0)
|
989
|
+
accuracy = profile_result.get("accuracy_percent", 0)
|
990
|
+
cost_diff = profile_result.get("cost_difference", 0)
|
991
|
+
|
992
|
+
if profile_result.get("error"):
|
993
|
+
self.console.print(f"├── {profile_name}: [red]❌ Error: {profile_result['error']}[/]")
|
994
|
+
else:
|
995
|
+
variance_pct = 100 - accuracy if accuracy > 0 else 100
|
996
|
+
self.console.print(f"├── {profile_name}:")
|
997
|
+
self.console.print(f"│ ├── Runbooks Cost: ${runbooks_cost:,.2f}")
|
998
|
+
self.console.print(f"│ ├── MCP API Cost: ${aws_cost:,.2f}")
|
999
|
+
self.console.print(f"│ ├── Variance: ${cost_diff:,.2f} ({variance_pct:.2f}%)")
|
1000
|
+
|
1001
|
+
if accuracy >= 99.5:
|
1002
|
+
self.console.print(f"│ └── Status: [green]✅ {accuracy:.2f}% accuracy[/]")
|
1003
|
+
elif accuracy >= 95.0:
|
1004
|
+
self.console.print(f"│ └── Status: [yellow]⚠️ {accuracy:.2f}% accuracy[/]")
|
1005
|
+
else:
|
1006
|
+
self.console.print(f"│ └── Status: [red]❌ {accuracy:.2f}% accuracy[/]")
|
1007
|
+
|
1008
|
+
# Overall summary
|
1009
|
+
if passed:
|
1010
|
+
self.console.print(f"└── [bright_green]✅ MCP Validation PASSED: {overall_accuracy:.2f}% overall accuracy[/]")
|
1011
|
+
self.console.print(f" [green]🏢 Enterprise compliance: {profiles_validated}/{len(profiles)} profiles validated[/]")
|
1012
|
+
else:
|
1013
|
+
self.console.print(f"└── [bright_yellow]⚠️ MCP Validation: {overall_accuracy:.2f}% overall accuracy[/]")
|
1014
|
+
self.console.print(f" [yellow]📊 Enterprise target: ≥99.5% accuracy required for compliance[/]")
|
1015
|
+
|
1016
|
+
# Save validation report
|
1017
|
+
import json
|
1018
|
+
import os
|
1019
|
+
from datetime import datetime
|
1020
|
+
|
1021
|
+
validation_file = (
|
1022
|
+
f"artifacts/validation/embedded_mcp_validation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
1023
|
+
)
|
1024
|
+
os.makedirs(os.path.dirname(validation_file), exist_ok=True)
|
1025
|
+
|
1026
|
+
with open(validation_file, "w") as f:
|
1027
|
+
json.dump(validation_results, f, indent=2, default=str)
|
1028
|
+
|
1029
|
+
self.console.print(f"[cyan]📋 Validation report saved: {validation_file}[/]")
|
1030
|
+
|
1031
|
+
except Exception as e:
|
1032
|
+
self.console.print(f"[red]❌ Embedded MCP validation failed: {str(e)[:100]}[/]")
|
1033
|
+
self.console.print(f"[dim]Continuing with standard FinOps analysis[/]")
|
706
1034
|
|
707
1035
|
|
708
1036
|
def create_single_dashboard(console: Optional[Console] = None) -> SingleAccountDashboard:
|