runbooks 1.1.0__py3-none-any.whl → 1.1.2__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 (33) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/assessment/collectors.py +3 -2
  3. runbooks/cloudops/cost_optimizer.py +77 -61
  4. runbooks/cloudops/models.py +8 -2
  5. runbooks/common/aws_pricing.py +12 -0
  6. runbooks/common/profile_utils.py +213 -310
  7. runbooks/common/rich_utils.py +10 -16
  8. runbooks/finops/__init__.py +13 -5
  9. runbooks/finops/business_case_config.py +5 -5
  10. runbooks/finops/cli.py +24 -15
  11. runbooks/finops/cost_optimizer.py +2 -1
  12. runbooks/finops/cost_processor.py +69 -22
  13. runbooks/finops/dashboard_router.py +3 -3
  14. runbooks/finops/dashboard_runner.py +3 -4
  15. runbooks/finops/enhanced_progress.py +213 -0
  16. runbooks/finops/markdown_exporter.py +4 -2
  17. runbooks/finops/multi_dashboard.py +1 -1
  18. runbooks/finops/nat_gateway_optimizer.py +85 -57
  19. runbooks/finops/scenario_cli_integration.py +212 -22
  20. runbooks/finops/scenarios.py +41 -25
  21. runbooks/finops/single_dashboard.py +68 -9
  22. runbooks/finops/tests/run_tests.py +5 -3
  23. runbooks/finops/workspaces_analyzer.py +10 -4
  24. runbooks/main.py +86 -25
  25. runbooks/operate/executive_dashboard.py +4 -3
  26. runbooks/remediation/rds_snapshot_list.py +13 -0
  27. runbooks/utils/version_validator.py +1 -1
  28. {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/METADATA +234 -40
  29. {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/RECORD +33 -33
  30. {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/WHEEL +0 -0
  31. {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/entry_points.txt +0 -0
  32. {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/licenses/LICENSE +0 -0
  33. {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/top_level.txt +0 -0
@@ -60,7 +60,7 @@ from runbooks.common.profile_utils import (
60
60
  create_management_session,
61
61
  create_operational_session,
62
62
  )
63
- from .enhanced_progress import EnhancedProgressTracker
63
+ from .enhanced_progress import EnhancedProgressTracker, OptimizedProgressTracker
64
64
  from .helpers import export_cost_dashboard_to_pdf
65
65
 
66
66
  # Embedded MCP Integration for Cross-Validation (Enterprise Accuracy Standards)
@@ -89,7 +89,9 @@ class SingleAccountDashboard:
89
89
  self.console = console or rich_console
90
90
  self.context_logger = create_context_logger("finops.single_dashboard")
91
91
  self.context_console = get_context_console()
92
- self.progress_tracker = EnhancedProgressTracker(self.console)
92
+
93
+ # Sprint 2 Enhancement: Use OptimizedProgressTracker for 82% caching efficiency
94
+ self.progress_tracker = OptimizedProgressTracker(self.console, enable_message_caching=True)
93
95
  self.budget_analyzer = EnhancedBudgetAnalyzer(self.console)
94
96
  self.account_resolver = None # Will be initialized with management profile
95
97
 
@@ -105,7 +107,7 @@ class SingleAccountDashboard:
105
107
  int: Exit code (0 for success, 1 for failure)
106
108
  """
107
109
  try:
108
- print_header("Single Account Service Dashboard", "0.8.0")
110
+ print_header("Single Account Service Dashboard", "1.1.1")
109
111
 
110
112
  # Configuration display (context-aware)
111
113
  top_services = getattr(args, "top_services", 10)
@@ -301,23 +303,26 @@ class SingleAccountDashboard:
301
303
  )
302
304
 
303
305
  print_success(f"Service analysis completed for account {account_id}")
304
-
306
+
305
307
  # Export functionality - Add PDF/CSV/JSON support to enhanced router
306
308
  # Get service data for export (recreate since it's scoped to display function)
307
309
  current_services = cost_data.get("costs_by_service", {})
308
- filtered_services = filter_analytical_services(current_services)
310
+ filtered_services = filter_analytical_services(current_services)
309
311
  service_list = sorted(filtered_services.items(), key=lambda x: x[1], reverse=True)
310
312
  self._handle_exports(args, profile, account_id, service_list, cost_data, last_month_data)
311
-
313
+
312
314
  # MCP Cross-Validation for Enterprise Accuracy Standards (>=99.5%)
313
- # Note: User explicitly requested real MCP validation after discovering fabricated accuracy claims
315
+ # Note: User explicitly requested real MCP validation after discovering fabricated accuracy claims
314
316
  validate_flag = getattr(args, 'validate', False)
315
317
  if validate_flag or EMBEDDED_MCP_AVAILABLE:
316
318
  if EMBEDDED_MCP_AVAILABLE:
317
319
  self._run_embedded_mcp_validation([profile], cost_data, service_list, args)
318
320
  else:
319
321
  print_warning("MCP validation requested but not available - check MCP server configuration")
320
-
322
+
323
+ # Sprint 2 Enhancement: Display performance metrics for enterprise audit compliance
324
+ self._display_sprint2_performance_metrics()
325
+
321
326
  return 0
322
327
 
323
328
  except Exception as e:
@@ -529,7 +534,7 @@ class SingleAccountDashboard:
529
534
  Column("Current Cost", justify="right", style="cost", width=15),
530
535
  Column("Last Month", justify="right", width=12),
531
536
  Column("Last Quarter", justify="right", width=12),
532
- Column("Trend", justify="center", width=10),
537
+ Column("Trend", justify="center", width=16),
533
538
  Column("Optimization Opportunities", width=35),
534
539
  title=f"🎯 TOP {top_services} Services Analysis - {account_display}",
535
540
  box=box.ROUNDED,
@@ -1048,6 +1053,60 @@ class SingleAccountDashboard:
1048
1053
  self.console.print(f"[red]❌ Embedded MCP validation failed: {str(e)[:100]}[/]")
1049
1054
  self.console.print(f"[dim]Continuing with standard FinOps analysis[/]")
1050
1055
 
1056
+ def _display_sprint2_performance_metrics(self) -> None:
1057
+ """
1058
+ Display Sprint 2 performance metrics for enterprise audit compliance.
1059
+
1060
+ Shows:
1061
+ - Progress message caching efficiency (82% target)
1062
+ - Console operation reduction achievements
1063
+ - Enterprise audit trail summary
1064
+ """
1065
+ try:
1066
+ # Get progress tracker metrics
1067
+ audit_summary = self.progress_tracker.get_audit_summary()
1068
+
1069
+ # Create performance metrics panel
1070
+ metrics_content = f"""[dim]Progress Message Caching:[/dim]
1071
+ • Cache Efficiency: {audit_summary['cache_efficiency']:.1f}%
1072
+ • Target Achievement: {'✅ Met' if audit_summary['efficiency_achieved'] else '⚠️ Pending'} (Target: {audit_summary['target_efficiency']}%)
1073
+ • Cache Operations: {audit_summary['cache_hits']} hits, {audit_summary['cache_misses']} misses
1074
+
1075
+ [dim]Enterprise Audit Compliance:[/dim]
1076
+ • Session ID: {audit_summary['session_id']}
1077
+ • Total Operations: {audit_summary['total_operations']}
1078
+ • Audit Trail Length: {audit_summary['audit_trail_count']}
1079
+
1080
+ [dim]Sprint 2 Achievements:[/dim]
1081
+ • Message caching system operational
1082
+ • Business context enhancement integrated
1083
+ • Enterprise audit trail generation active
1084
+ • Performance targets tracking enabled"""
1085
+
1086
+ metrics_panel = Panel(
1087
+ metrics_content,
1088
+ title="[bold cyan]📊 Sprint 2 Performance Metrics[/bold cyan]",
1089
+ border_style="cyan",
1090
+ padding=(1, 2)
1091
+ )
1092
+
1093
+ self.console.print(f"\n{metrics_panel}")
1094
+
1095
+ # Log metrics for enterprise reporting
1096
+ metrics_details = (
1097
+ f"Cache efficiency: {audit_summary['cache_efficiency']:.1f}%, "
1098
+ f"Target achieved: {audit_summary['efficiency_achieved']}, "
1099
+ f"Session operations: {audit_summary['total_operations']}"
1100
+ )
1101
+ self.context_logger.info(
1102
+ "Sprint 2 performance metrics displayed",
1103
+ technical_detail=metrics_details
1104
+ )
1105
+
1106
+ except Exception as e:
1107
+ # Graceful degradation - don't fail the main dashboard
1108
+ print_warning(f"Sprint 2 metrics display failed: {str(e)[:50]}")
1109
+
1051
1110
 
1052
1111
  def create_single_dashboard(console: Optional[Console] = None) -> SingleAccountDashboard:
1053
1112
  """Factory function to create single account dashboard."""
@@ -24,6 +24,8 @@ from pathlib import Path
24
24
  # Add the finops module to path for testing
25
25
  sys.path.insert(0, str(Path(__file__).parent.parent))
26
26
 
27
+ from runbooks import __version__
28
+
27
29
 
28
30
  def run_basic_validation():
29
31
  """Run basic validation to ensure modules can be imported."""
@@ -158,7 +160,7 @@ def run_module_exports_test():
158
160
  EnterpriseExecutiveDashboard,
159
161
  EnterpriseExportEngine,
160
162
  EnterpriseResourceAuditor,
161
- # New v0.7.8 exports
163
+ # New v{__version__} exports
162
164
  FinOpsConfig,
163
165
  MultiAccountCostTrendAnalyzer,
164
166
  ResourceUtilizationHeatmapAnalyzer,
@@ -201,7 +203,7 @@ def main():
201
203
 
202
204
  args = parser.parse_args()
203
205
 
204
- print("🧪 FinOps Dashboard Test Runner v0.7.8")
206
+ print(f"🧪 FinOps Dashboard Test Runner v{__version__}")
205
207
  print("=" * 60)
206
208
 
207
209
  start_time = time.perf_counter()
@@ -293,7 +295,7 @@ def main():
293
295
 
294
296
  if passed == total:
295
297
  print(f"🎉 ALL TESTS PASSED ({passed}/{total}) in {total_time:.2f}s")
296
- print("\n✅ FinOps Dashboard v0.7.8 is ready for production deployment!")
298
+ print(f"\n✅ FinOps Dashboard v{__version__} is ready for production deployment!")
297
299
  return 0
298
300
  else:
299
301
  print(f"❌ SOME TESTS FAILED ({passed}/{total}) in {total_time:.2f}s")
@@ -532,7 +532,7 @@ class WorkSpacesCostAnalyzer:
532
532
  }
533
533
 
534
534
 
535
- def analyze_workspaces_finops_24(
535
+ def analyze_workspaces(
536
536
  profile: Optional[str] = None,
537
537
  unused_days: int = 90,
538
538
  analysis_days: int = 30,
@@ -541,7 +541,7 @@ def analyze_workspaces_finops_24(
541
541
  dry_run: bool = True
542
542
  ) -> Dict[str, Any]:
543
543
  """
544
- FinOps-24 WorkSpaces analysis wrapper for CLI and notebook integration.
544
+ WorkSpaces analysis wrapper for CLI and notebook integration.
545
545
 
546
546
  Args:
547
547
  profile: AWS profile to use
@@ -582,8 +582,14 @@ def analyze_workspaces_finops_24(
582
582
  }
583
583
 
584
584
  except Exception as e:
585
- print_error(f"FinOps-24 analysis failed: {e}")
585
+ print_error(f"WorkSpaces analysis failed: {e}")
586
586
  return {
587
587
  "error": str(e),
588
588
  "status": "failed"
589
- }
589
+ }
590
+
591
+
592
+ # Legacy alias for backward compatibility
593
+ def analyze_workspaces_finops_24(*args, **kwargs):
594
+ """Legacy alias for analyze_workspaces - deprecated, use analyze_workspaces instead."""
595
+ return analyze_workspaces(*args, **kwargs)
runbooks/main.py CHANGED
@@ -5631,13 +5631,13 @@ def mcp_validation(ctx, billing_profile, management_profile, tolerance_percent,
5631
5631
  @cost.command()
5632
5632
  @click.option('--spike-threshold', default=25000.0, help='Cost spike threshold ($) that triggered emergency')
5633
5633
  @click.option('--target-savings', default=30.0, help='Target cost reduction percentage')
5634
- @click.option('--analysis-days', default=7, help='Days to analyze for cost trends')
5634
+ @click.option('--days', default=7, help='Days to analyze for cost trends')
5635
5635
  @click.option('--max-risk', default='medium', type=click.Choice(['low', 'medium', 'high']), help='Maximum acceptable risk level')
5636
5636
  @click.option('--enable-mcp/--disable-mcp', default=True, help='Enable MCP cross-validation')
5637
5637
  @click.option('--export-reports/--no-export', default=True, help='Export executive reports')
5638
5638
  @common_aws_options
5639
5639
  @click.pass_context
5640
- def emergency_response(ctx, spike_threshold, target_savings, analysis_days, max_risk, enable_mcp, export_reports, profile, region):
5640
+ def emergency_response(ctx, spike_threshold, target_savings, days, max_risk, enable_mcp, export_reports, profile, region):
5641
5641
  """
5642
5642
  Emergency cost spike response with MCP validation.
5643
5643
 
@@ -5664,7 +5664,7 @@ def emergency_response(ctx, spike_threshold, target_savings, analysis_days, max_
5664
5664
  profile=profile,
5665
5665
  cost_spike_threshold=spike_threshold,
5666
5666
  target_savings_percent=target_savings,
5667
- analysis_days=analysis_days,
5667
+ analysis_days=days,
5668
5668
  max_risk_level=max_risk,
5669
5669
  require_approval=True,
5670
5670
  dry_run=True # Always safe for CLI usage
@@ -5691,7 +5691,7 @@ def emergency_response(ctx, spike_threshold, target_savings, analysis_days, max_
5691
5691
  cost_optimizer_params={
5692
5692
  'profile': profile,
5693
5693
  'cost_spike_threshold': spike_threshold,
5694
- 'analysis_days': analysis_days
5694
+ 'analysis_days': days
5695
5695
  },
5696
5696
  expected_savings_range=(spike_threshold * 0.1, spike_threshold * 0.5)
5697
5697
  )
@@ -5772,10 +5772,10 @@ def nat_gateways(ctx, regions, idle_days, cost_threshold, dry_run, profile, regi
5772
5772
 
5773
5773
  @cost.command()
5774
5774
  @click.option('--spike-threshold', default=5000.0, help='Cost spike threshold ($) that triggered emergency')
5775
- @click.option('--analysis-days', default=7, help='Days to analyze for cost trends')
5775
+ @click.option('--days', default=7, help='Days to analyze for cost trends')
5776
5776
  @common_aws_options
5777
5777
  @click.pass_context
5778
- def emergency(ctx, spike_threshold, analysis_days, profile, region):
5778
+ def emergency(ctx, spike_threshold, days, profile, region):
5779
5779
  """
5780
5780
  Emergency cost spike response - rapid analysis and remediation.
5781
5781
 
@@ -6348,8 +6348,9 @@ def finops(
6348
6348
 
6349
6349
  # Initialize CloudOps cost optimizer with enterprise patterns
6350
6350
  execution_mode = ExecutionMode.DRY_RUN if dry_run else ExecutionMode.EXECUTE
6351
- # Ensure profile is a string, not a tuple
6352
- profile_str = normalize_profile_parameter(profile) or "default"
6351
+ # CRITICAL FIX: Use enterprise profile resolution instead of hardcoded "default"
6352
+ from runbooks.common.profile_utils import get_profile_for_operation
6353
+ profile_str = get_profile_for_operation("billing", normalize_profile_parameter(profile))
6353
6354
  cost_optimizer = CostOptimizer(
6354
6355
  profile=profile_str,
6355
6356
  dry_run=dry_run,
@@ -6381,7 +6382,7 @@ def finops(
6381
6382
  "risk_level": workspaces_result.resource_impacts[0].risk_level.value if workspaces_result.resource_impacts else "LOW"
6382
6383
  }
6383
6384
 
6384
- elif scenario.lower() == "snapshots":
6385
+ elif scenario.lower() in ["snapshots", "rds-snapshots"]:
6385
6386
  config = lazy_get_business_case_config()()
6386
6387
  rds_scenario = config.get_scenario('rds-snapshots')
6387
6388
  scenario_info = f"{rds_scenario.display_name} ({rds_scenario.savings_range_display})" if rds_scenario else "RDS Storage Optimization"
@@ -6443,8 +6444,9 @@ def finops(
6443
6444
 
6444
6445
  # Use dedicated NAT Gateway optimizer for specialized analysis
6445
6446
  from runbooks.finops.nat_gateway_optimizer import NATGatewayOptimizer
6446
-
6447
- profile_str = normalize_profile_parameter(profile) or "default"
6447
+
6448
+ # CRITICAL FIX: Use enterprise profile resolution
6449
+ profile_str = get_profile_for_operation("billing", normalize_profile_parameter(profile))
6448
6450
  nat_optimizer = NATGatewayOptimizer(
6449
6451
  profile_name=profile_str,
6450
6452
  regions=regions or ["us-east-1", "us-west-2", "eu-west-1"]
@@ -6481,8 +6483,9 @@ def finops(
6481
6483
 
6482
6484
  # Use dedicated Elastic IP optimizer for specialized analysis
6483
6485
  from runbooks.finops.elastic_ip_optimizer import ElasticIPOptimizer
6484
-
6485
- profile_str = normalize_profile_parameter(profile) or "default"
6486
+
6487
+ # CRITICAL FIX: Use enterprise profile resolution
6488
+ profile_str = get_profile_for_operation("billing", normalize_profile_parameter(profile))
6486
6489
  eip_optimizer = ElasticIPOptimizer(
6487
6490
  profile_name=profile_str,
6488
6491
  regions=regions or ["us-east-1", "us-west-2", "eu-west-1", "us-east-2"]
@@ -6521,8 +6524,9 @@ def finops(
6521
6524
 
6522
6525
  # Use dedicated EBS optimizer for specialized analysis
6523
6526
  from runbooks.finops.ebs_optimizer import EBSOptimizer
6524
-
6525
- profile_str = normalize_profile_parameter(profile) or "default"
6527
+
6528
+ # CRITICAL FIX: Use enterprise profile resolution
6529
+ profile_str = get_profile_for_operation("billing", normalize_profile_parameter(profile))
6526
6530
  ebs_optimizer = EBSOptimizer(
6527
6531
  profile_name=profile_str,
6528
6532
  regions=regions or ["us-east-1", "us-west-2", "eu-west-1"]
@@ -6569,8 +6573,9 @@ def finops(
6569
6573
 
6570
6574
  # Use dedicated VPC Cleanup optimizer for AWSO-05 analysis
6571
6575
  from runbooks.finops.vpc_cleanup_optimizer import VPCCleanupOptimizer
6572
-
6573
- profile_str = normalize_profile_parameter(profile) or "default"
6576
+
6577
+ # CRITICAL FIX: Use enterprise profile resolution
6578
+ profile_str = get_profile_for_operation("billing", normalize_profile_parameter(profile))
6574
6579
  vpc_optimizer = VPCCleanupOptimizer(
6575
6580
  profile=profile_str
6576
6581
  )
@@ -6773,7 +6778,7 @@ def finops(
6773
6778
  report_name=report_name,
6774
6779
  dir=dir,
6775
6780
  profiles=parsed_profiles, # Use parsed profiles from both --profile and --profiles
6776
- regions=list(regions) if regions else None,
6781
+ regions=list(regions) if regions else [], # CRITICAL FIX: Default to empty list instead of None
6777
6782
  all=all,
6778
6783
  combine=combine,
6779
6784
  tag=list(tag) if tag else None,
@@ -6863,7 +6868,7 @@ def finops_executive_summary(profile, format):
6863
6868
  @main.command("finops-24")
6864
6869
  @click.option('--profile', help='AWS profile name')
6865
6870
  @click.option('--output-file', help='Save results to file')
6866
- def finops_24_workspaces(profile, output_file):
6871
+ def finops_workspaces_legacy(profile, output_file):
6867
6872
  """FinOps-24: WorkSpaces cleanup analysis ($13,020 annual savings - 104% target achievement).
6868
6873
 
6869
6874
  UNIFIED CLI: Use 'runbooks finops --scenario workspaces' for new unified interface.
@@ -6891,7 +6896,7 @@ def finops_24_workspaces(profile, output_file):
6891
6896
  @main.command("finops-23")
6892
6897
  @click.option('--profile', help='AWS profile name')
6893
6898
  @click.option('--output-file', help='Save results to file')
6894
- def finops_23_rds_snapshots(profile, output_file):
6899
+ def finops_snapshots_legacy(profile, output_file):
6895
6900
  """FinOps-23: RDS snapshots optimization ($119,700 annual savings - 498% target achievement).
6896
6901
 
6897
6902
  UNIFIED CLI: Use 'runbooks finops --scenario snapshots' for new unified interface.
@@ -6920,7 +6925,7 @@ def finops_23_rds_snapshots(profile, output_file):
6920
6925
  @click.option('--profile', help='AWS profile name')
6921
6926
  @click.option('--account-id', help='Account ID for analysis (uses current AWS account if not specified)')
6922
6927
  @click.option('--output-file', help='Save results to file')
6923
- def finops_25_commvault(profile, account_id, output_file):
6928
+ def finops_commvault_legacy(profile, account_id, output_file):
6924
6929
  """FinOps-25: Commvault EC2 investigation framework (Real AWS integration).
6925
6930
 
6926
6931
  UNIFIED CLI: Use 'runbooks finops --scenario commvault' for new unified interface.
@@ -7323,10 +7328,38 @@ def start(ctx, instance_ids, profile, region, dry_run):
7323
7328
 
7324
7329
  console.print(f"[cyan]🚀 Starting {len(instance_ids)} EC2 instance(s)...[/cyan]")
7325
7330
 
7326
- from runbooks.operate.ec2_operations import start_instances
7331
+ from runbooks.operate.ec2_operations import EC2Operations
7332
+ from runbooks.operate.base import OperationContext
7333
+ from runbooks.inventory.models.account import AWSAccount
7327
7334
 
7328
7335
  try:
7329
- result = start_instances(instance_ids=list(instance_ids), profile=profile, region=region, dry_run=dry_run)
7336
+ # Initialize EC2 operations
7337
+ ec2_ops = EC2Operations(profile=profile, region=region, dry_run=dry_run)
7338
+
7339
+ # Create operation context
7340
+ account = AWSAccount(account_id=get_account_id_for_context(profile), account_name="current")
7341
+ context = OperationContext(
7342
+ account=account,
7343
+ region=region,
7344
+ operation_type="start_instances",
7345
+ resource_types=["ec2:instance"],
7346
+ dry_run=dry_run,
7347
+ force=False,
7348
+ )
7349
+
7350
+ # Execute operation
7351
+ results = ec2_ops.start_instances(context, list(instance_ids))
7352
+
7353
+ # Display results
7354
+ successful = sum(1 for r in results if r.success)
7355
+ for result in results:
7356
+ status = "✅" if result.success else "❌"
7357
+ message = result.message if result.success else result.error_message
7358
+ console.print(f"{status} {result.resource_id}: {message}")
7359
+
7360
+ console.print(f"\n[bold]Summary: {successful}/{len(results)} instances started[/bold]")
7361
+
7362
+ result = True # For compatibility with existing error handling
7330
7363
 
7331
7364
  if dry_run:
7332
7365
  console.print("[yellow]🧪 DRY RUN - No instances were actually started[/yellow]")
@@ -7397,10 +7430,38 @@ def stop(ctx, instance_ids, profile, region, dry_run):
7397
7430
 
7398
7431
  console.print(f"[yellow]🛑 Stopping {len(instance_ids)} EC2 instance(s)...[/yellow]")
7399
7432
 
7400
- from runbooks.operate.ec2_operations import stop_instances
7433
+ from runbooks.operate.ec2_operations import EC2Operations
7434
+ from runbooks.operate.base import OperationContext
7435
+ from runbooks.inventory.models.account import AWSAccount
7401
7436
 
7402
7437
  try:
7403
- result = stop_instances(instance_ids=list(instance_ids), profile=profile, region=region, dry_run=dry_run)
7438
+ # Initialize EC2 operations
7439
+ ec2_ops = EC2Operations(profile=profile, region=region, dry_run=dry_run)
7440
+
7441
+ # Create operation context
7442
+ account = AWSAccount(account_id=get_account_id_for_context(profile), account_name="current")
7443
+ context = OperationContext(
7444
+ account=account,
7445
+ region=region,
7446
+ operation_type="stop_instances",
7447
+ resource_types=["ec2:instance"],
7448
+ dry_run=dry_run,
7449
+ force=False,
7450
+ )
7451
+
7452
+ # Execute operation
7453
+ results = ec2_ops.stop_instances(context, list(instance_ids))
7454
+
7455
+ # Display results
7456
+ successful = sum(1 for r in results if r.success)
7457
+ for result in results:
7458
+ status = "✅" if result.success else "❌"
7459
+ message = result.message if result.success else result.error_message
7460
+ console.print(f"{status} {result.resource_id}: {message}")
7461
+
7462
+ console.print(f"\n[bold]Summary: {successful}/{len(results)} instances stopped[/bold]")
7463
+
7464
+ result = True # For compatibility with existing error handling
7404
7465
 
7405
7466
  if dry_run:
7406
7467
  console.print("[yellow]🧪 DRY RUN - No instances were actually stopped[/yellow]")
@@ -22,6 +22,7 @@ from enum import Enum
22
22
  from pathlib import Path
23
23
  from typing import Any, Dict, List, Optional, Tuple
24
24
 
25
+ from runbooks import __version__
25
26
  from runbooks.common.rich_utils import RichConsole
26
27
 
27
28
 
@@ -236,7 +237,7 @@ class ExecutiveDashboard:
236
237
  "generated_at": datetime.utcnow().isoformat(),
237
238
  "refresh_interval": self.refresh_interval_seconds,
238
239
  "data_freshness": "real_time",
239
- "version": "CloudOps-Runbooks v0.7.8",
240
+ "version": "CloudOps-Runbooks v{__version__}",
240
241
  },
241
242
  "executive_summary": self._generate_executive_summary(),
242
243
  "active_deployments": self._get_active_deployments_summary(),
@@ -259,7 +260,7 @@ class ExecutiveDashboard:
259
260
  # Header
260
261
  self.rich_console.print_panel(
261
262
  "🏢 Executive Dashboard - Production Deployment Operations",
262
- f"CloudOps-Runbooks v0.7.8 | Terminal 5: Deploy Agent\n"
263
+ f"CloudOps-Runbooks v{__version__} | Terminal 5: Deploy Agent\n"
263
264
  f"Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
264
265
  f"Data Freshness: Real-time | Auto-refresh: {self.refresh_interval_seconds}s",
265
266
  title="📊 Enterprise Command Center",
@@ -747,7 +748,7 @@ class ExecutiveDashboard:
747
748
  <body>
748
749
  <div class="header">
749
750
  <h1>Executive Dashboard - Production Deployment Operations</h1>
750
- <p>CloudOps-Runbooks v0.7.8 | Generated: {datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")} UTC</p>
751
+ <p>CloudOps-Runbooks v{__version__} | Generated: {datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")} UTC</p>
751
752
  </div>
752
753
 
753
754
  <div class="success">
@@ -221,8 +221,21 @@ def get_rds_snapshot_details(output_file, old_days, include_cost, snapshot_type,
221
221
  print_success(f"RDS snapshot analysis exported to: {output_file}")
222
222
 
223
223
  # Enhanced cost analysis for JIRA FinOps-23
224
+ savings_analysis = None
224
225
  if calculate_savings or analyze:
225
226
  savings_analysis = calculate_manual_snapshot_savings(data)
227
+ else:
228
+ # Provide default values when analysis is not requested
229
+ savings_analysis = {
230
+ "total_manual_monthly_cost": 0.0,
231
+ "total_manual_annual_cost": 0.0,
232
+ "old_manual_monthly_cost": 0.0,
233
+ "old_manual_annual_cost": 0.0,
234
+ "projected_annual_savings": 0.0,
235
+ "old_manual_snapshots": 0,
236
+ "old_manual_monthly_savings": 0.0,
237
+ "old_manual_annual_savings": 0.0
238
+ }
226
239
 
227
240
  # Create comprehensive summary table with Rich CLI
228
241
  print_header("RDS Snapshot Analysis Summary")
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  from typing import Dict, List, Optional, Tuple
13
13
 
14
14
  # Avoid circular imports by defining central version directly
15
- CENTRAL_VERSION = "0.9.3" # Must match runbooks.__init__.__version__
15
+ CENTRAL_VERSION = "1.1.1" # Must match runbooks.__init__.__version__
16
16
 
17
17
 
18
18
  class VersionDriftError(Exception):