runbooks 1.0.3__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.
Files changed (47) hide show
  1. runbooks/__init__.py +10 -5
  2. runbooks/__init__.py.backup +134 -0
  3. runbooks/__init___optimized.py +110 -0
  4. runbooks/cloudops/base.py +56 -3
  5. runbooks/cloudops/cost_optimizer.py +496 -42
  6. runbooks/common/aws_pricing.py +236 -80
  7. runbooks/common/business_logic.py +485 -0
  8. runbooks/common/cli_decorators.py +219 -0
  9. runbooks/common/error_handling.py +424 -0
  10. runbooks/common/lazy_loader.py +186 -0
  11. runbooks/common/module_cli_base.py +378 -0
  12. runbooks/common/performance_monitoring.py +512 -0
  13. runbooks/common/profile_utils.py +133 -6
  14. runbooks/enterprise/logging.py +30 -2
  15. runbooks/enterprise/validation.py +177 -0
  16. runbooks/finops/README.md +311 -236
  17. runbooks/finops/aws_client.py +1 -1
  18. runbooks/finops/business_case_config.py +723 -19
  19. runbooks/finops/cli.py +136 -0
  20. runbooks/finops/commvault_ec2_analysis.py +25 -9
  21. runbooks/finops/config.py +272 -0
  22. runbooks/finops/dashboard_runner.py +136 -23
  23. runbooks/finops/ebs_cost_optimizer.py +39 -40
  24. runbooks/finops/enhanced_trend_visualization.py +7 -2
  25. runbooks/finops/enterprise_wrappers.py +45 -18
  26. runbooks/finops/finops_dashboard.py +50 -25
  27. runbooks/finops/finops_scenarios.py +22 -7
  28. runbooks/finops/helpers.py +115 -2
  29. runbooks/finops/multi_dashboard.py +7 -5
  30. runbooks/finops/optimizer.py +97 -6
  31. runbooks/finops/scenario_cli_integration.py +247 -0
  32. runbooks/finops/scenarios.py +12 -1
  33. runbooks/finops/unlimited_scenarios.py +393 -0
  34. runbooks/finops/validation_framework.py +19 -7
  35. runbooks/finops/workspaces_analyzer.py +1 -5
  36. runbooks/inventory/mcp_inventory_validator.py +2 -1
  37. runbooks/main.py +132 -94
  38. runbooks/main_final.py +358 -0
  39. runbooks/main_minimal.py +84 -0
  40. runbooks/main_optimized.py +493 -0
  41. runbooks/main_ultra_minimal.py +47 -0
  42. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/METADATA +1 -1
  43. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/RECORD +47 -31
  44. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/WHEEL +0 -0
  45. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/entry_points.txt +0 -0
  46. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/licenses/LICENSE +0 -0
  47. {runbooks-1.0.3.dist-info → runbooks-1.1.0.dist-info}/top_level.txt +0 -0
@@ -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
- untagged_display = f"EC2:\nus-east-1:\ni-{row.get('account_id', '1234567890')[:10]}"
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
- untagged_display += f"\n\nRDS:\nus-west-2:\ndb-{row.get('account_id', '9876543210')[:10]}"
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 - no hardcoded truncation
556
- max_profile_display_length = getattr(args, "max_profile_display_length", 50)
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 configurable thresholds
1417
- high_cost_threshold = getattr(args, "high_cost_threshold", 5000)
1418
- medium_cost_threshold = getattr(args, "medium_cost_threshold", 1000)
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"
@@ -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
- # Mock implementation - in production would query CloudWatch
516
- return 3.5 # Mock low utilization
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
- # Mock implementation
529
- return {"cpu_avg": 15.0, "memory_avg": 25.0, "network_avg": 5.0}
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
@@ -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 '637423383469')
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'),