runbooks 1.0.2__py3-none-any.whl → 1.1.0__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 +9 -4
- runbooks/__init__.py.backup +134 -0
- runbooks/__init___optimized.py +110 -0
- runbooks/cloudops/base.py +56 -3
- runbooks/cloudops/cost_optimizer.py +496 -42
- runbooks/common/aws_pricing.py +236 -80
- runbooks/common/business_logic.py +485 -0
- runbooks/common/cli_decorators.py +219 -0
- runbooks/common/error_handling.py +424 -0
- runbooks/common/lazy_loader.py +186 -0
- runbooks/common/module_cli_base.py +378 -0
- runbooks/common/performance_monitoring.py +512 -0
- runbooks/common/profile_utils.py +133 -6
- runbooks/enterprise/logging.py +30 -2
- runbooks/enterprise/validation.py +177 -0
- runbooks/finops/README.md +311 -236
- runbooks/finops/aws_client.py +1 -1
- runbooks/finops/business_case_config.py +723 -19
- runbooks/finops/cli.py +136 -0
- runbooks/finops/commvault_ec2_analysis.py +25 -9
- runbooks/finops/config.py +272 -0
- runbooks/finops/dashboard_runner.py +136 -23
- runbooks/finops/ebs_cost_optimizer.py +39 -40
- runbooks/finops/enhanced_trend_visualization.py +7 -2
- runbooks/finops/enterprise_wrappers.py +45 -18
- runbooks/finops/finops_dashboard.py +50 -25
- runbooks/finops/finops_scenarios.py +22 -7
- runbooks/finops/helpers.py +115 -2
- runbooks/finops/multi_dashboard.py +7 -5
- runbooks/finops/optimizer.py +97 -6
- runbooks/finops/scenario_cli_integration.py +247 -0
- runbooks/finops/scenarios.py +12 -1
- runbooks/finops/unlimited_scenarios.py +393 -0
- runbooks/finops/validation_framework.py +19 -7
- runbooks/finops/workspaces_analyzer.py +1 -5
- runbooks/inventory/mcp_inventory_validator.py +2 -1
- runbooks/main.py +132 -94
- runbooks/main_final.py +358 -0
- runbooks/main_minimal.py +84 -0
- runbooks/main_optimized.py +493 -0
- runbooks/main_ultra_minimal.py +47 -0
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/METADATA +15 -15
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/RECORD +47 -31
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/WHEEL +0 -0
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/top_level.txt +0 -0
runbooks/finops/helpers.py
CHANGED
@@ -286,9 +286,17 @@ def export_audit_report_to_pdf(
|
|
286
286
|
untagged_display = ""
|
287
287
|
if row.get("untagged_count", 0) > 0:
|
288
288
|
# Format like reference: "EC2: us-east-1: i-1234567890"
|
289
|
-
|
289
|
+
account_id = row.get('account_id', 'unknown')
|
290
|
+
if account_id and account_id != 'unknown':
|
291
|
+
untagged_display = f"EC2:\nus-east-1:\ni-{account_id[:10]}"
|
292
|
+
else:
|
293
|
+
untagged_display = f"EC2:\nus-east-1:\ni-unavailable"
|
290
294
|
if row.get("untagged_count", 0) > 1:
|
291
|
-
|
295
|
+
account_id = row.get('account_id', 'unknown')
|
296
|
+
if account_id and account_id != 'unknown':
|
297
|
+
untagged_display += f"\n\nRDS:\nus-west-2:\ndb-{account_id[:10]}"
|
298
|
+
else:
|
299
|
+
untagged_display += f"\n\nRDS:\nus-west-2:\ndb-unavailable"
|
292
300
|
|
293
301
|
# Format stopped instances like reference
|
294
302
|
stopped_display = ""
|
@@ -1282,3 +1290,108 @@ def generate_pdca_improvement_report(
|
|
1282
1290
|
except Exception as e:
|
1283
1291
|
console.print(f"[bold red]Error generating PDCA improvement report: {str(e)}[/]")
|
1284
1292
|
return None
|
1293
|
+
|
1294
|
+
|
1295
|
+
def export_scenario_results(results: Dict[str, Any], scenario_name: str,
|
1296
|
+
report_types: List[str], output_dir: Optional[str] = None) -> bool:
|
1297
|
+
"""
|
1298
|
+
Export business scenario results in specified formats.
|
1299
|
+
|
1300
|
+
Args:
|
1301
|
+
results: Scenario analysis results dictionary
|
1302
|
+
scenario_name: Name of the business scenario
|
1303
|
+
report_types: List of export formats ('json', 'csv', 'pdf', 'markdown')
|
1304
|
+
output_dir: Output directory (defaults to current directory)
|
1305
|
+
|
1306
|
+
Returns:
|
1307
|
+
True if all exports succeeded
|
1308
|
+
"""
|
1309
|
+
try:
|
1310
|
+
from runbooks.common.rich_utils import print_success, print_error, print_info
|
1311
|
+
|
1312
|
+
output_dir = output_dir or "."
|
1313
|
+
base_filename = f"finops-scenario-{scenario_name}-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
1314
|
+
|
1315
|
+
success_count = 0
|
1316
|
+
total_exports = len(report_types)
|
1317
|
+
|
1318
|
+
for report_type in report_types:
|
1319
|
+
try:
|
1320
|
+
if report_type == 'json':
|
1321
|
+
output_path = os.path.join(output_dir, f"{base_filename}.json")
|
1322
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
1323
|
+
json.dump(results, f, indent=2, default=str)
|
1324
|
+
print_info(f"📄 JSON export: {output_path}")
|
1325
|
+
|
1326
|
+
elif report_type == 'csv':
|
1327
|
+
output_path = os.path.join(output_dir, f"{base_filename}.csv")
|
1328
|
+
# Extract key metrics for CSV export
|
1329
|
+
if 'business_impact' in results:
|
1330
|
+
business_data = results['business_impact']
|
1331
|
+
with open(output_path, 'w', newline='', encoding='utf-8') as f:
|
1332
|
+
writer = csv.DictWriter(f, fieldnames=business_data.keys())
|
1333
|
+
writer.writeheader()
|
1334
|
+
writer.writerow(business_data)
|
1335
|
+
print_info(f"📊 CSV export: {output_path}")
|
1336
|
+
|
1337
|
+
elif report_type == 'pdf':
|
1338
|
+
output_path = os.path.join(output_dir, f"{base_filename}.pdf")
|
1339
|
+
# Create basic PDF with scenario results
|
1340
|
+
doc = SimpleDocTemplate(output_path, pagesize=letter)
|
1341
|
+
story = []
|
1342
|
+
|
1343
|
+
# Title
|
1344
|
+
title = Paragraph(f"FinOps Business Scenario: {scenario_name.title()}",
|
1345
|
+
styles['Heading1'])
|
1346
|
+
story.append(title)
|
1347
|
+
story.append(Spacer(1, 12))
|
1348
|
+
|
1349
|
+
# Add results summary
|
1350
|
+
if 'business_impact' in results:
|
1351
|
+
business_data = results['business_impact']
|
1352
|
+
for key, value in business_data.items():
|
1353
|
+
text = Paragraph(f"<b>{key.replace('_', ' ').title()}:</b> {value}",
|
1354
|
+
styles['Normal'])
|
1355
|
+
story.append(text)
|
1356
|
+
|
1357
|
+
doc.build(story)
|
1358
|
+
print_info(f"📋 PDF export: {output_path}")
|
1359
|
+
|
1360
|
+
elif report_type == 'markdown':
|
1361
|
+
output_path = os.path.join(output_dir, f"{base_filename}.md")
|
1362
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
1363
|
+
f.write(f"# FinOps Business Scenario: {scenario_name.title()}\n\n")
|
1364
|
+
f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
1365
|
+
|
1366
|
+
# Add business impact section
|
1367
|
+
if 'business_impact' in results:
|
1368
|
+
f.write("## Business Impact\n\n")
|
1369
|
+
business_data = results['business_impact']
|
1370
|
+
for key, value in business_data.items():
|
1371
|
+
f.write(f"- **{key.replace('_', ' ').title()}**: {value}\n")
|
1372
|
+
f.write("\n")
|
1373
|
+
|
1374
|
+
# Add technical details section
|
1375
|
+
if 'technical_details' in results:
|
1376
|
+
f.write("## Technical Details\n\n")
|
1377
|
+
tech_data = results['technical_details']
|
1378
|
+
for key, value in tech_data.items():
|
1379
|
+
f.write(f"- **{key.replace('_', ' ').title()}**: {value}\n")
|
1380
|
+
|
1381
|
+
print_info(f"📝 Markdown export: {output_path}")
|
1382
|
+
|
1383
|
+
success_count += 1
|
1384
|
+
|
1385
|
+
except Exception as e:
|
1386
|
+
print_error(f"Failed to export {report_type}: {e}")
|
1387
|
+
|
1388
|
+
if success_count == total_exports:
|
1389
|
+
print_success(f"✅ All {success_count} export formats completed successfully")
|
1390
|
+
return True
|
1391
|
+
else:
|
1392
|
+
print_info(f"⚠️ {success_count}/{total_exports} exports completed")
|
1393
|
+
return False
|
1394
|
+
|
1395
|
+
except Exception as e:
|
1396
|
+
print_error(f"Export failed: {e}")
|
1397
|
+
return False
|
@@ -552,8 +552,9 @@ class MultiAccountDashboard:
|
|
552
552
|
# SRE FIX: Extract account ID from Organizations API profile format
|
553
553
|
if "@" in profile:
|
554
554
|
base_profile, target_account_id = profile.split("@", 1)
|
555
|
-
# Configurable display format -
|
556
|
-
|
555
|
+
# Configurable display format - using centralized config
|
556
|
+
from runbooks.finops.config import get_profile_display_length
|
557
|
+
max_profile_display_length = get_profile_display_length(args)
|
557
558
|
if len(base_profile) > max_profile_display_length:
|
558
559
|
display_profile = f"{base_profile[:max_profile_display_length]}...@{target_account_id}"
|
559
560
|
else:
|
@@ -1413,9 +1414,10 @@ class MultiAccountDashboard:
|
|
1413
1414
|
.replace("✅", "OK")
|
1414
1415
|
)
|
1415
1416
|
|
1416
|
-
# Optimization recommendation based on
|
1417
|
-
|
1418
|
-
|
1417
|
+
# Optimization recommendation based on centralized config
|
1418
|
+
from runbooks.finops.config import get_high_cost_threshold, get_medium_cost_threshold
|
1419
|
+
high_cost_threshold = get_high_cost_threshold(args)
|
1420
|
+
medium_cost_threshold = get_medium_cost_threshold(args)
|
1419
1421
|
|
1420
1422
|
if current > high_cost_threshold:
|
1421
1423
|
optimization = "Cost Review Required"
|
runbooks/finops/optimizer.py
CHANGED
@@ -511,9 +511,87 @@ class CostOptimizer:
|
|
511
511
|
return size_gb * rate
|
512
512
|
|
513
513
|
def _get_cpu_utilization(self, cloudwatch, instance_id: str, days: int = 30) -> float:
|
514
|
-
"""Get average CPU utilization for instance."""
|
515
|
-
|
516
|
-
|
514
|
+
"""Get average CPU utilization for instance from CloudWatch."""
|
515
|
+
try:
|
516
|
+
from datetime import datetime, timedelta
|
517
|
+
|
518
|
+
end_time = datetime.utcnow()
|
519
|
+
start_time = end_time - timedelta(days=days)
|
520
|
+
|
521
|
+
response = cloudwatch.get_metric_statistics(
|
522
|
+
Namespace='AWS/EC2',
|
523
|
+
MetricName='CPUUtilization',
|
524
|
+
Dimensions=[{'Name': 'InstanceId', 'Value': instance_id}],
|
525
|
+
StartTime=start_time,
|
526
|
+
EndTime=end_time,
|
527
|
+
Period=3600, # 1 hour
|
528
|
+
Statistics=['Average']
|
529
|
+
)
|
530
|
+
|
531
|
+
if response['Datapoints']:
|
532
|
+
cpu_avg = sum(dp['Average'] for dp in response['Datapoints']) / len(response['Datapoints'])
|
533
|
+
return cpu_avg
|
534
|
+
else:
|
535
|
+
console.print(f"[yellow]⚠️ No CPU metrics found for {instance_id}[/yellow]")
|
536
|
+
return 0.0
|
537
|
+
|
538
|
+
except Exception as e:
|
539
|
+
console.print(f"[red]❌ Error getting CPU metrics for {instance_id}: {e}[/red]")
|
540
|
+
return 0.0
|
541
|
+
|
542
|
+
def _get_memory_utilization(self, cloudwatch, instance_id: str, days: int = 30) -> float:
|
543
|
+
"""Get average memory utilization for instance from CloudWatch."""
|
544
|
+
try:
|
545
|
+
from datetime import datetime, timedelta
|
546
|
+
|
547
|
+
end_time = datetime.utcnow()
|
548
|
+
start_time = end_time - timedelta(days=days)
|
549
|
+
|
550
|
+
response = cloudwatch.get_metric_statistics(
|
551
|
+
Namespace='CWAgent',
|
552
|
+
MetricName='mem_used_percent',
|
553
|
+
Dimensions=[{'Name': 'InstanceId', 'Value': instance_id}],
|
554
|
+
StartTime=start_time,
|
555
|
+
EndTime=end_time,
|
556
|
+
Period=3600,
|
557
|
+
Statistics=['Average']
|
558
|
+
)
|
559
|
+
|
560
|
+
if response['Datapoints']:
|
561
|
+
memory_avg = sum(dp['Average'] for dp in response['Datapoints']) / len(response['Datapoints'])
|
562
|
+
return memory_avg
|
563
|
+
else:
|
564
|
+
return 0.0 # No memory metrics available
|
565
|
+
|
566
|
+
except Exception:
|
567
|
+
return 0.0 # Memory metrics might not be available
|
568
|
+
|
569
|
+
def _get_network_utilization(self, cloudwatch, instance_id: str, days: int = 30) -> float:
|
570
|
+
"""Get average network utilization for instance from CloudWatch."""
|
571
|
+
try:
|
572
|
+
from datetime import datetime, timedelta
|
573
|
+
|
574
|
+
end_time = datetime.utcnow()
|
575
|
+
start_time = end_time - timedelta(days=days)
|
576
|
+
|
577
|
+
response = cloudwatch.get_metric_statistics(
|
578
|
+
Namespace='AWS/EC2',
|
579
|
+
MetricName='NetworkIn',
|
580
|
+
Dimensions=[{'Name': 'InstanceId', 'Value': instance_id}],
|
581
|
+
StartTime=start_time,
|
582
|
+
EndTime=end_time,
|
583
|
+
Period=3600,
|
584
|
+
Statistics=['Average']
|
585
|
+
)
|
586
|
+
|
587
|
+
if response['Datapoints']:
|
588
|
+
network_avg = sum(dp['Average'] for dp in response['Datapoints']) / len(response['Datapoints'])
|
589
|
+
return network_avg / 1024 / 1024 # Convert to MB
|
590
|
+
else:
|
591
|
+
return 0.0
|
592
|
+
|
593
|
+
except Exception:
|
594
|
+
return 0.0
|
517
595
|
|
518
596
|
def _get_running_instances(self, ec2_client):
|
519
597
|
"""Get all running EC2 instances."""
|
@@ -524,9 +602,22 @@ class CostOptimizer:
|
|
524
602
|
return instances
|
525
603
|
|
526
604
|
def _analyze_instance_utilization(self, cloudwatch, instance_id: str) -> Dict[str, float]:
|
527
|
-
"""Analyze instance utilization metrics."""
|
528
|
-
|
529
|
-
|
605
|
+
"""Analyze instance utilization metrics from CloudWatch."""
|
606
|
+
try:
|
607
|
+
cpu_avg = self._get_cpu_utilization(cloudwatch, instance_id)
|
608
|
+
|
609
|
+
# Get additional metrics if available
|
610
|
+
memory_avg = self._get_memory_utilization(cloudwatch, instance_id)
|
611
|
+
network_avg = self._get_network_utilization(cloudwatch, instance_id)
|
612
|
+
|
613
|
+
return {
|
614
|
+
"cpu_avg": cpu_avg,
|
615
|
+
"memory_avg": memory_avg,
|
616
|
+
"network_avg": network_avg
|
617
|
+
}
|
618
|
+
except Exception as e:
|
619
|
+
console.print(f"[red]❌ Error analyzing utilization for {instance_id}: {e}[/red]")
|
620
|
+
return {"cpu_avg": 0.0, "memory_avg": 0.0, "network_avg": 0.0}
|
530
621
|
|
531
622
|
def _suggest_smaller_instance(self, current_type: str) -> Optional[str]:
|
532
623
|
"""Suggest a smaller instance type."""
|
@@ -0,0 +1,247 @@
|
|
1
|
+
"""
|
2
|
+
FinOps Scenario CLI Integration - Phase 1 Priority 2
|
3
|
+
|
4
|
+
This module provides CLI integration for the Business Scenario Matrix with intelligent
|
5
|
+
parameter defaults and scenario-specific help generation.
|
6
|
+
|
7
|
+
Strategic Achievement: Manager requires business scenario intelligence with smart
|
8
|
+
parameter recommendations per business case type.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import click
|
12
|
+
from typing import Dict, List, Optional, Any
|
13
|
+
from rich.console import Console
|
14
|
+
from rich.table import Table
|
15
|
+
from rich.panel import Panel
|
16
|
+
|
17
|
+
from .business_case_config import (
|
18
|
+
get_business_case_config,
|
19
|
+
get_business_scenario_matrix,
|
20
|
+
BusinessScenarioMatrix,
|
21
|
+
ScenarioParameter
|
22
|
+
)
|
23
|
+
from ..common.rich_utils import print_header, print_info, print_success, print_warning
|
24
|
+
|
25
|
+
|
26
|
+
class ScenarioCliHelper:
|
27
|
+
"""
|
28
|
+
CLI integration helper for business scenario intelligence.
|
29
|
+
|
30
|
+
Provides intelligent parameter recommendations and scenario-specific help.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(self):
|
34
|
+
"""Initialize CLI helper with scenario matrix."""
|
35
|
+
self.console = Console()
|
36
|
+
self.business_config = get_business_case_config()
|
37
|
+
self.scenario_matrix = get_business_scenario_matrix()
|
38
|
+
|
39
|
+
def display_scenario_help(self, scenario_key: Optional[str] = None) -> None:
|
40
|
+
"""Display scenario-specific help with parameter recommendations."""
|
41
|
+
print_header("FinOps Business Scenarios", "Parameter Intelligence")
|
42
|
+
|
43
|
+
if scenario_key:
|
44
|
+
self._display_single_scenario_help(scenario_key)
|
45
|
+
else:
|
46
|
+
self._display_all_scenarios_help()
|
47
|
+
|
48
|
+
def _display_single_scenario_help(self, scenario_key: str) -> None:
|
49
|
+
"""Display detailed help for a single scenario."""
|
50
|
+
scenario_config = self.business_config.get_scenario(scenario_key)
|
51
|
+
if not scenario_config:
|
52
|
+
print_warning(f"Unknown scenario: {scenario_key}")
|
53
|
+
return
|
54
|
+
|
55
|
+
# Display scenario overview
|
56
|
+
self.console.print(f"\n[bold cyan]Scenario: {scenario_config.display_name}[/bold cyan]")
|
57
|
+
self.console.print(f"[dim]Business Case: {scenario_config.business_description}[/dim]")
|
58
|
+
self.console.print(f"[dim]Technical Focus: {scenario_config.technical_focus}[/dim]")
|
59
|
+
self.console.print(f"[dim]Savings Target: {scenario_config.savings_range_display}[/dim]")
|
60
|
+
self.console.print(f"[dim]Risk Level: {scenario_config.risk_level}[/dim]")
|
61
|
+
|
62
|
+
# Display parameter recommendations
|
63
|
+
recommendations = self.scenario_matrix.get_parameter_recommendations(scenario_key)
|
64
|
+
if recommendations:
|
65
|
+
self.console.print(f"\n[bold green]🎯 Intelligent Parameter Recommendations[/bold green]")
|
66
|
+
|
67
|
+
for param_key, param in recommendations.items():
|
68
|
+
self._display_parameter_recommendation(param)
|
69
|
+
|
70
|
+
# Display optimal command
|
71
|
+
optimal_command = self._generate_optimal_command(scenario_key, recommendations)
|
72
|
+
self.console.print(f"\n[bold yellow]💡 Optimal Command Example:[/bold yellow]")
|
73
|
+
self.console.print(f"[dim]runbooks finops --scenario {scenario_key} {optimal_command}[/dim]")
|
74
|
+
else:
|
75
|
+
print_info("Using standard parameters for this scenario")
|
76
|
+
|
77
|
+
def _display_all_scenarios_help(self) -> None:
|
78
|
+
"""Display overview of all scenarios with parameter summaries."""
|
79
|
+
# Create scenarios overview table
|
80
|
+
table = Table(
|
81
|
+
title="🎯 Business Scenarios with Intelligent Parameter Defaults",
|
82
|
+
show_header=True,
|
83
|
+
header_style="bold cyan"
|
84
|
+
)
|
85
|
+
|
86
|
+
table.add_column("Scenario", style="bold white", width=15)
|
87
|
+
table.add_column("Business Case", style="cyan", width=25)
|
88
|
+
table.add_column("Savings Target", style="green", width=15)
|
89
|
+
table.add_column("Optimal Parameters", style="yellow", width=35)
|
90
|
+
table.add_column("Tier", style="magenta", width=8)
|
91
|
+
|
92
|
+
# Get scenario summaries
|
93
|
+
scenario_summaries = self.scenario_matrix.get_all_scenario_summaries()
|
94
|
+
|
95
|
+
# Tier classification for display
|
96
|
+
tier_mapping = {
|
97
|
+
'workspaces': 'Tier 1',
|
98
|
+
'nat-gateway': 'Tier 1',
|
99
|
+
'rds-snapshots': 'Tier 1',
|
100
|
+
'ebs-optimization': 'Tier 2',
|
101
|
+
'vpc-cleanup': 'Tier 2',
|
102
|
+
'elastic-ip': 'Tier 2',
|
103
|
+
'backup-investigation': 'Tier 3'
|
104
|
+
}
|
105
|
+
|
106
|
+
for scenario_key, scenario in self.business_config.get_all_scenarios().items():
|
107
|
+
parameter_summary = scenario_summaries.get(scenario_key, "Standard")
|
108
|
+
tier = tier_mapping.get(scenario_key, "Standard")
|
109
|
+
|
110
|
+
table.add_row(
|
111
|
+
scenario_key,
|
112
|
+
scenario.display_name,
|
113
|
+
scenario.savings_range_display,
|
114
|
+
parameter_summary,
|
115
|
+
tier
|
116
|
+
)
|
117
|
+
|
118
|
+
self.console.print(table)
|
119
|
+
|
120
|
+
# Display usage instructions
|
121
|
+
usage_panel = Panel(
|
122
|
+
"""[bold]Usage Examples:[/bold]
|
123
|
+
|
124
|
+
[cyan]Tier 1 High-Value Scenarios:[/cyan]
|
125
|
+
• runbooks finops --scenario workspaces --time-range 90 --pdf
|
126
|
+
• runbooks finops --scenario nat-gateway --time-range 30 --json --amortized
|
127
|
+
• runbooks finops --scenario rds-snapshots --time-range 90 --csv --dual-metrics
|
128
|
+
|
129
|
+
[cyan]Tier 2 Strategic Scenarios:[/cyan]
|
130
|
+
• runbooks finops --scenario ebs-optimization --time-range 180 --pdf --dual-metrics
|
131
|
+
• runbooks finops --scenario vpc-cleanup --time-range 30 --csv --unblended
|
132
|
+
• runbooks finops --scenario elastic-ip --time-range 7 --json --unblended
|
133
|
+
|
134
|
+
[cyan]Get Scenario-Specific Help:[/cyan]
|
135
|
+
• runbooks finops --scenario workspaces --help-scenario
|
136
|
+
• runbooks finops --help-scenarios # All scenarios overview
|
137
|
+
""",
|
138
|
+
title="📚 Scenario Usage Guide",
|
139
|
+
style="cyan"
|
140
|
+
)
|
141
|
+
self.console.print(usage_panel)
|
142
|
+
|
143
|
+
def _display_parameter_recommendation(self, param: ScenarioParameter) -> None:
|
144
|
+
"""Display a single parameter recommendation."""
|
145
|
+
# Format parameter display
|
146
|
+
if isinstance(param.optimal_value, bool) and param.optimal_value:
|
147
|
+
param_display = f"[bold]{param.name}[/bold]"
|
148
|
+
else:
|
149
|
+
param_display = f"[bold]{param.name} {param.optimal_value}[/bold]"
|
150
|
+
|
151
|
+
self.console.print(f" {param_display}")
|
152
|
+
self.console.print(f" [dim]→ {param.business_justification}[/dim]")
|
153
|
+
|
154
|
+
if param.alternative_values:
|
155
|
+
alternatives = ', '.join(str(v) for v in param.alternative_values)
|
156
|
+
self.console.print(f" [dim]Alternatives: {alternatives}[/dim]")
|
157
|
+
self.console.print()
|
158
|
+
|
159
|
+
def _generate_optimal_command(self, scenario_key: str, recommendations: Dict[str, ScenarioParameter]) -> str:
|
160
|
+
"""Generate optimal command example from recommendations."""
|
161
|
+
command_parts = []
|
162
|
+
|
163
|
+
for param_key, param in recommendations.items():
|
164
|
+
if isinstance(param.optimal_value, bool) and param.optimal_value:
|
165
|
+
command_parts.append(param.name)
|
166
|
+
else:
|
167
|
+
command_parts.append(f"{param.name} {param.optimal_value}")
|
168
|
+
|
169
|
+
return " ".join(command_parts)
|
170
|
+
|
171
|
+
def validate_scenario_parameters(self, scenario_key: str, provided_params: Dict[str, Any]) -> None:
|
172
|
+
"""Validate and provide suggestions for scenario parameters."""
|
173
|
+
suggestions = self.scenario_matrix.validate_parameters_for_scenario(scenario_key, provided_params)
|
174
|
+
|
175
|
+
if suggestions:
|
176
|
+
self.console.print(f"\n[bold yellow]💡 Parameter Optimization Suggestions for '{scenario_key}':[/bold yellow]")
|
177
|
+
for param_type, suggestion in suggestions.items():
|
178
|
+
self.console.print(f" [yellow]→[/yellow] {suggestion}")
|
179
|
+
self.console.print()
|
180
|
+
|
181
|
+
def get_scenario_cli_choices(self) -> List[str]:
|
182
|
+
"""Get list of valid scenario choices for Click options."""
|
183
|
+
return self.business_config.get_scenario_choices()
|
184
|
+
|
185
|
+
def get_enhanced_scenario_help_text(self) -> str:
|
186
|
+
"""Get enhanced help text including parameter intelligence."""
|
187
|
+
base_help = self.business_config.get_scenario_help_text()
|
188
|
+
return f"{base_help}\n\nUse --scenario [scenario-name] for specific optimization analysis."
|
189
|
+
|
190
|
+
|
191
|
+
def display_scenario_matrix_help(scenario_key: Optional[str] = None) -> None:
|
192
|
+
"""
|
193
|
+
Display business scenario matrix help with parameter intelligence.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
scenario_key: Specific scenario to show help for, or None for all scenarios
|
197
|
+
"""
|
198
|
+
helper = ScenarioCliHelper()
|
199
|
+
helper.display_scenario_help(scenario_key)
|
200
|
+
|
201
|
+
|
202
|
+
def validate_and_suggest_parameters(scenario_key: str, cli_params: Dict[str, Any]) -> None:
|
203
|
+
"""
|
204
|
+
Validate CLI parameters against scenario recommendations and provide suggestions.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
scenario_key: The business scenario being executed
|
208
|
+
cli_params: Dictionary of provided CLI parameters
|
209
|
+
"""
|
210
|
+
helper = ScenarioCliHelper()
|
211
|
+
helper.validate_scenario_parameters(scenario_key, cli_params)
|
212
|
+
|
213
|
+
|
214
|
+
def get_scenario_parameter_defaults(scenario_key: str) -> Dict[str, Any]:
|
215
|
+
"""
|
216
|
+
Get parameter defaults for a specific scenario.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
scenario_key: The business scenario key
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
Dictionary of parameter defaults that can be applied to CLI arguments
|
223
|
+
"""
|
224
|
+
matrix = get_business_scenario_matrix()
|
225
|
+
recommendations = matrix.get_parameter_recommendations(scenario_key)
|
226
|
+
|
227
|
+
defaults = {}
|
228
|
+
|
229
|
+
for param_key, param in recommendations.items():
|
230
|
+
if param.name == '--time-range':
|
231
|
+
defaults['time_range'] = param.optimal_value
|
232
|
+
elif param.name == '--unblended':
|
233
|
+
defaults['unblended'] = True
|
234
|
+
elif param.name == '--amortized':
|
235
|
+
defaults['amortized'] = True
|
236
|
+
elif param.name == '--dual-metrics':
|
237
|
+
defaults['dual_metrics'] = True
|
238
|
+
elif param.name == '--pdf':
|
239
|
+
defaults['pdf'] = True
|
240
|
+
elif param.name == '--csv':
|
241
|
+
defaults['csv'] = True
|
242
|
+
elif param.name == '--json':
|
243
|
+
defaults['json'] = True
|
244
|
+
elif param.name == '--markdown':
|
245
|
+
defaults['export_markdown'] = True
|
246
|
+
|
247
|
+
return defaults
|
runbooks/finops/scenarios.py
CHANGED
@@ -50,6 +50,17 @@ from .business_cases import BusinessCaseAnalyzer, BusinessCaseFormatter
|
|
50
50
|
logger = logging.getLogger(__name__)
|
51
51
|
|
52
52
|
|
53
|
+
def _get_account_from_profile(profile: Optional[str] = None) -> str:
|
54
|
+
"""Get account ID from AWS profile with dynamic resolution."""
|
55
|
+
try:
|
56
|
+
import boto3
|
57
|
+
session = boto3.Session(profile_name=profile)
|
58
|
+
return session.client('sts').get_caller_identity()['Account']
|
59
|
+
except Exception as e:
|
60
|
+
logger.warning(f"Could not resolve account ID from profile {profile}: {e}")
|
61
|
+
return "unknown"
|
62
|
+
|
63
|
+
|
53
64
|
# ============================================================================
|
54
65
|
# CLEAN API FUNCTIONS FOR NOTEBOOK CONSUMPTION
|
55
66
|
# ============================================================================
|
@@ -400,7 +411,7 @@ def finops_25_commvault_investigation(profile: Optional[str] = None, account: Op
|
|
400
411
|
'monthly_cost': technical_findings.get('total_monthly_cost', 0),
|
401
412
|
'optimization_candidates': technical_findings.get('optimization_candidates', 0),
|
402
413
|
'investigation_required': technical_findings.get('investigation_required', 0),
|
403
|
-
'target_account': raw_analysis.get('target_account', account or
|
414
|
+
'target_account': raw_analysis.get('target_account', account or _get_account_from_profile(profile))
|
404
415
|
},
|
405
416
|
'implementation': {
|
406
417
|
'timeline': raw_analysis.get('deployment_timeline', '3-4 weeks investigation + systematic implementation'),
|