runbooks 1.1.1__py3-none-any.whl → 1.1.3__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 (39) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/assessment/collectors.py +3 -2
  3. runbooks/cloudops/cost_optimizer.py +235 -83
  4. runbooks/cloudops/models.py +8 -2
  5. runbooks/common/aws_pricing.py +12 -0
  6. runbooks/common/business_logic.py +1 -1
  7. runbooks/common/profile_utils.py +213 -310
  8. runbooks/common/rich_utils.py +15 -21
  9. runbooks/finops/README.md +3 -3
  10. runbooks/finops/__init__.py +13 -5
  11. runbooks/finops/business_case_config.py +5 -5
  12. runbooks/finops/cli.py +170 -95
  13. runbooks/finops/cost_optimizer.py +2 -1
  14. runbooks/finops/cost_processor.py +69 -22
  15. runbooks/finops/dashboard_router.py +3 -3
  16. runbooks/finops/dashboard_runner.py +3 -4
  17. runbooks/finops/embedded_mcp_validator.py +101 -23
  18. runbooks/finops/enhanced_progress.py +213 -0
  19. runbooks/finops/finops_scenarios.py +90 -16
  20. runbooks/finops/markdown_exporter.py +4 -2
  21. runbooks/finops/multi_dashboard.py +1 -1
  22. runbooks/finops/nat_gateway_optimizer.py +85 -57
  23. runbooks/finops/rds_snapshot_optimizer.py +1389 -0
  24. runbooks/finops/scenario_cli_integration.py +212 -22
  25. runbooks/finops/scenarios.py +41 -25
  26. runbooks/finops/single_dashboard.py +68 -9
  27. runbooks/finops/tests/run_tests.py +5 -3
  28. runbooks/finops/vpc_cleanup_optimizer.py +1 -1
  29. runbooks/finops/workspaces_analyzer.py +40 -16
  30. runbooks/inventory/list_rds_snapshots_aggregator.py +745 -0
  31. runbooks/main.py +393 -61
  32. runbooks/operate/executive_dashboard.py +4 -3
  33. runbooks/remediation/rds_snapshot_list.py +13 -0
  34. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/METADATA +234 -40
  35. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/RECORD +39 -37
  36. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/WHEEL +0 -0
  37. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/entry_points.txt +0 -0
  38. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/licenses/LICENSE +0 -0
  39. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/top_level.txt +0 -0
@@ -95,14 +95,18 @@ def get_context_aware_console():
95
95
  return console
96
96
 
97
97
 
98
- def print_header(title: str, version: str = "0.7.8") -> None:
98
+ def print_header(title: str, version: Optional[str] = None) -> None:
99
99
  """
100
100
  Print a consistent header for all modules.
101
101
 
102
102
  Args:
103
103
  title: Module title
104
- version: Module version
104
+ version: Module version (defaults to package version)
105
105
  """
106
+ if version is None:
107
+ from runbooks import __version__
108
+ version = __version__
109
+
106
110
  header_text = Text()
107
111
  header_text.append("CloudOps Runbooks ", style="header")
108
112
  header_text.append(f"| {title} ", style="subheader")
@@ -114,20 +118,10 @@ def print_header(title: str, version: str = "0.7.8") -> None:
114
118
 
115
119
 
116
120
  def print_banner() -> None:
117
- """Print the CloudOps Runbooks ASCII banner."""
118
- banner = r"""
119
- ╔═══════════════════════════════════════════════════════════════╗
120
- ║ _____ _ _ ____ ____ ║
121
- ║ / ____| | | |/ __ \ | _ \ ║
122
- ║ | | | | ___ _ _ __| | | | |_ __ ___ | |_) |_ _ __ ║
123
- ║ | | | |/ _ \| | | |/ _` | | | | '_ \/ __| | _ <| | | '_ \ ║
124
- ║ | |____| | (_) | |_| | (_| | |__| | |_) \__ \ | |_) | |_| | | |║
125
- ║ \_____|_|\___/ \__,_|\__,_|\____/| .__/|___/ |____/ \__,_|_| |║
126
- ║ | | ║
127
- ║ Enterprise AWS Automation |_| Platform v1.0.0 ║
128
- ╚═══════════════════════════════════════════════════════════════╝
129
- """
130
- console.print(banner, style="header")
121
+ """Print a clean, minimal CloudOps Runbooks banner."""
122
+ from runbooks import __version__
123
+ console.print(f"\n[header]CloudOps Runbooks[/header] [subheader]Enterprise AWS Automation Platform[/subheader] [dim]v{__version__}[/dim]")
124
+ console.print()
131
125
 
132
126
 
133
127
  def create_table(
@@ -617,14 +611,14 @@ def create_columns(items: List[Any], equal: bool = True, expand: bool = True) ->
617
611
  # Manager's Cost Optimization Scenario Formatting Functions
618
612
  def format_workspaces_analysis(workspaces_data: Dict[str, Any], target_savings: int = 12518) -> Panel:
619
613
  """
620
- Format WorkSpaces cost analysis for manager's FinOps-24 scenario.
621
-
614
+ Format WorkSpaces cost analysis for manager's priority scenario.
615
+
622
616
  Based on manager's requirement for $12,518 annual savings through
623
617
  cleanup of unused WorkSpaces with zero usage in last 6 months.
624
-
618
+
625
619
  Args:
626
620
  workspaces_data: Dictionary containing WorkSpaces cost and utilization data
627
- target_savings: Annual savings target (default: $12,518 from FinOps-24)
621
+ target_savings: Annual savings target (default: $12,518)
628
622
 
629
623
  Returns:
630
624
  Rich Panel with formatted WorkSpaces analysis
@@ -660,7 +654,7 @@ def format_workspaces_analysis(workspaces_data: Dict[str, Any], target_savings:
660
654
 
661
655
  [{status_style}]{status}[/]"""
662
656
 
663
- return Panel(content, title="[bright_cyan]FinOps-24: WorkSpaces Cost Optimization[/bright_cyan]",
657
+ return Panel(content, title="[bright_cyan]WorkSpaces Cost Optimization[/bright_cyan]",
664
658
  border_style="bright_green" if target_achievement >= 90 else "yellow")
665
659
 
666
660
 
runbooks/finops/README.md CHANGED
@@ -25,9 +25,9 @@ runbooks finops --help # View all functionality
25
25
  runbooks finops --scenario workspaces --profile PROFILE # WorkSpaces optimization
26
26
  runbooks finops --scenario nat-gateway --profile PROFILE # NAT Gateway optimization
27
27
  runbooks finops --scenario elastic-ip --profile PROFILE # Elastic IP management
28
- runbooks finops --scenario ebs --profile PROFILE # EBS optimization
29
- runbooks finops --scenario snapshots --profile PROFILE # RDS snapshots cleanup
30
- runbooks finops --scenario commvault --profile PROFILE # Backup analysis
28
+ runbooks finops --scenario ebs-optimization --profile PROFILE # EBS optimization
29
+ runbooks finops --scenario rds-snapshots --profile PROFILE # RDS snapshots cleanup
30
+ runbooks finops --scenario backup-investigation --profile PROFILE # Backup analysis
31
31
  runbooks finops --scenario vpc-cleanup --profile PROFILE # VPC cleanup
32
32
 
33
33
  # AWS Cost Explorer metrics (working)
@@ -69,12 +69,16 @@ from runbooks.finops.finops_scenarios import (
69
69
 
70
70
  # NEW v0.9.5: Clean API wrapper for notebook consumption
71
71
  from runbooks.finops.scenarios import (
72
- finops_24_workspaces_cleanup,
73
- finops_23_rds_snapshots_optimization,
74
- finops_25_commvault_investigation,
72
+ finops_workspaces,
73
+ finops_snapshots,
74
+ finops_commvault,
75
75
  get_business_scenarios_summary,
76
76
  format_for_audience,
77
77
  validate_scenarios_accuracy,
78
+ # Legacy aliases for backward compatibility
79
+ finops_24_workspaces_cleanup,
80
+ finops_23_rds_snapshots_optimization,
81
+ finops_25_commvault_investigation,
78
82
  )
79
83
  from runbooks.finops.profile_processor import process_combined_profiles, process_single_profile
80
84
 
@@ -88,7 +92,7 @@ __all__ = [
88
92
  # Core functionality
89
93
  "run_dashboard",
90
94
  "run_complete_finops_workflow",
91
- # NEW v0.7.8: Enterprise FinOps Dashboard Functions
95
+ # Enterprise FinOps Dashboard Functions
92
96
  "_run_audit_report",
93
97
  "_run_cost_trend_analysis",
94
98
  "_run_resource_heatmap_analysis",
@@ -100,7 +104,11 @@ __all__ = [
100
104
  "format_for_business_audience",
101
105
  "format_for_technical_audience",
102
106
  "FinOpsBusinessScenarios",
103
- # NEW v0.9.5: Clean API wrapper functions
107
+ # NEW v0.9.5: Clean API wrapper functions (cleaned naming)
108
+ "finops_workspaces",
109
+ "finops_snapshots",
110
+ "finops_commvault",
111
+ # Legacy aliases (deprecated)
104
112
  "finops_24_workspaces_cleanup",
105
113
  "finops_23_rds_snapshots_optimization",
106
114
  "finops_25_commvault_investigation",
@@ -89,7 +89,7 @@ class BusinessCaseConfigManager:
89
89
  cli_command_suffix="workspaces"
90
90
  ),
91
91
  "rds-snapshots": BusinessScenario(
92
- scenario_id="rds-snapshots",
92
+ scenario_id="rds-snapshots",
93
93
  display_name="RDS Storage Optimization",
94
94
  business_case_type=BusinessCaseType.RESOURCE_CLEANUP,
95
95
  target_savings_min=5000,
@@ -97,17 +97,17 @@ class BusinessCaseConfigManager:
97
97
  business_description="Optimize manual RDS snapshots to reduce storage costs",
98
98
  technical_focus="Manual RDS snapshot lifecycle management",
99
99
  risk_level="Medium",
100
- cli_command_suffix="snapshots"
100
+ cli_command_suffix="rds-snapshots"
101
101
  ),
102
102
  "backup-investigation": BusinessScenario(
103
103
  scenario_id="backup-investigation",
104
- display_name="Backup Infrastructure Analysis",
104
+ display_name="Backup Infrastructure Analysis",
105
105
  business_case_type=BusinessCaseType.COMPLIANCE_FRAMEWORK,
106
106
  business_description="Investigate backup account utilization and optimization opportunities",
107
107
  technical_focus="Backup infrastructure resource utilization analysis",
108
108
  risk_level="Medium",
109
109
  implementation_status="Framework",
110
- cli_command_suffix="commvault"
110
+ cli_command_suffix="backup-investigation"
111
111
  ),
112
112
  "nat-gateway": BusinessScenario(
113
113
  scenario_id="nat-gateway",
@@ -135,7 +135,7 @@ class BusinessCaseConfigManager:
135
135
  business_case_type=BusinessCaseType.COST_OPTIMIZATION,
136
136
  business_description="Optimize EBS volume types and utilization for cost efficiency",
137
137
  technical_focus="EBS volume rightsizing and type optimization (15-20% potential)",
138
- cli_command_suffix="ebs"
138
+ cli_command_suffix="ebs-optimization"
139
139
  ),
140
140
  "vpc-cleanup": BusinessScenario(
141
141
  scenario_id="vpc-cleanup",
runbooks/finops/cli.py CHANGED
@@ -6,12 +6,11 @@ import requests
6
6
  from packaging import version
7
7
  from rich.console import Console
8
8
 
9
+ from runbooks import __version__
9
10
  from runbooks.finops.helpers import load_config_file
10
11
 
11
12
  console = Console()
12
13
 
13
- __version__ = "0.7.8"
14
-
15
14
 
16
15
  def welcome_banner() -> None:
17
16
  banner = rf"""
@@ -229,7 +228,7 @@ def main() -> int:
229
228
  parser.add_argument(
230
229
  "--scenario",
231
230
  type=str,
232
- help="Business scenario analysis (workspaces, snapshots, commvault, nat-gateway, elastic-ip, ebs, vpc-cleanup)",
231
+ help="Business scenario analysis (workspaces, rds-snapshots, backup-investigation, nat-gateway, elastic-ip, ebs-optimization, vpc-cleanup)",
233
232
  )
234
233
  parser.add_argument(
235
234
  "--help-scenario",
@@ -273,83 +272,58 @@ def main() -> int:
273
272
 
274
273
  console.print(f"[bold cyan]🎯 Executing Business Scenario: {args.scenario}[/bold cyan]")
275
274
 
276
- # Define scenario execution functions with proper parameters
277
- def execute_workspaces_scenario():
278
- from runbooks.finops.scenarios import finops_24_workspaces_cleanup
279
- profile_param = args.profiles[0] if args.profiles else None
280
- return finops_24_workspaces_cleanup(profile=profile_param)
281
-
282
- def execute_snapshots_scenario():
283
- from runbooks.finops.scenarios import finops_23_rds_snapshots_optimization
284
- profile_param = args.profiles[0] if args.profiles else None
285
- return finops_23_rds_snapshots_optimization(profile=profile_param)
286
-
287
- def execute_commvault_scenario():
288
- from runbooks.finops.scenarios import finops_25_commvault_investigation
289
- profile_param = args.profiles[0] if args.profiles else None
290
- return finops_25_commvault_investigation(profile=profile_param)
291
-
292
- def execute_nat_gateway_scenario():
293
- from runbooks.finops.nat_gateway_optimizer import nat_gateway_optimizer
294
- profile_param = args.profiles[0] if args.profiles else None
295
- regions = args.regions if args.regions else ['us-east-1']
296
- # Call the CLI function with default parameters
297
- nat_gateway_optimizer(
298
- profile=profile_param,
299
- regions=regions,
300
- dry_run=True,
301
- export_format='json',
302
- output_file=None,
303
- usage_threshold_days=7
304
- )
305
- return {"scenario": "nat-gateway", "status": "completed", "profile": profile_param}
306
-
307
- def execute_ebs_scenario():
308
- # Create a simplified EBS scenario execution
309
- print_info("EBS optimization scenario analysis")
310
- profile_param = args.profiles[0] if args.profiles else None
311
- return {"scenario": "ebs", "status": "completed", "profile": profile_param}
312
-
313
- def execute_vpc_cleanup_scenario():
314
- # Create a simplified VPC cleanup scenario execution
315
- print_info("VPC cleanup scenario analysis")
316
- profile_param = args.profiles[0] if args.profiles else None
317
- return {"scenario": "vpc-cleanup", "status": "completed", "profile": profile_param}
318
-
319
- def execute_elastic_ip_scenario():
320
- # Create a simplified elastic IP scenario execution
321
- print_info("Elastic IP optimization scenario analysis")
322
- profile_param = args.profiles[0] if args.profiles else None
323
- return {"scenario": "elastic-ip", "status": "completed", "profile": profile_param}
324
-
325
- # Map scenarios to execution functions
326
- scenario_map = {
327
- 'workspaces': execute_workspaces_scenario,
328
- 'snapshots': execute_snapshots_scenario,
329
- 'commvault': execute_commvault_scenario,
330
- 'nat-gateway': execute_nat_gateway_scenario,
331
- 'ebs': execute_ebs_scenario,
332
- 'vpc-cleanup': execute_vpc_cleanup_scenario,
333
- 'elastic-ip': execute_elastic_ip_scenario,
334
- }
335
-
336
- if args.scenario not in scenario_map:
337
- print_error(f"Unknown scenario: '{args.scenario}'")
338
- print_info("Available scenarios: " + ", ".join(scenario_map.keys()))
339
- return 1
340
-
341
- # Execute scenario
342
- scenario_func = scenario_map[args.scenario]
343
- result = scenario_func()
344
-
345
- print_success(f"✅ Scenario '{args.scenario}' completed successfully")
346
-
347
- # Export results if requested
348
- if args.report_type and result:
349
- from runbooks.finops.helpers import export_scenario_results
350
- export_scenario_results(result, args.scenario, args.report_type, args.dir)
351
-
352
- return 0
275
+ # CRITICAL FIX: Handle --all flag for scenarios by using dashboard router logic
276
+ if hasattr(args, "all") and args.all:
277
+ print_info("🔍 --all flag detected: Integrating with dashboard router for organization discovery")
278
+
279
+ # Use dashboard router to handle --all flag and get profiles
280
+ from runbooks.finops.dashboard_router import create_dashboard_router
281
+ router = create_dashboard_router()
282
+ use_case, routing_config = router.detect_use_case(args)
283
+
284
+ # Extract profiles from routing config
285
+ profiles_to_use = routing_config.get("profiles_to_analyze", [])
286
+ if not profiles_to_use:
287
+ print_error("--all flag failed to discover any profiles")
288
+ return 1
289
+
290
+ print_success(f"Discovered {len(profiles_to_use)} profiles for scenario execution")
291
+
292
+ # Execute scenario across all discovered profiles
293
+ all_results = []
294
+ for profile in profiles_to_use:
295
+ print_info(f"Executing scenario '{args.scenario}' for profile: {profile}")
296
+
297
+ # Create a copy of args with single profile for execution
298
+ single_profile_args = argparse.Namespace(**vars(args))
299
+ single_profile_args.profiles = [profile]
300
+ single_profile_args.all = False # Disable --all for individual execution
301
+
302
+ # Execute scenario with single profile (recursive call but with all=False)
303
+ result = _execute_single_scenario(single_profile_args)
304
+ if result:
305
+ all_results.append(result)
306
+
307
+ # Combine results and export if requested
308
+ combined_result = {
309
+ "scenario": args.scenario,
310
+ "status": "completed",
311
+ "profiles_analyzed": len(profiles_to_use),
312
+ "individual_results": all_results,
313
+ "organization_scope": use_case == "organization_wide"
314
+ }
315
+
316
+ print_success(f"✅ Scenario '{args.scenario}' completed for {len(profiles_to_use)} profiles")
317
+
318
+ # Export results if requested
319
+ if args.report_type and combined_result:
320
+ from runbooks.finops.helpers import export_scenario_results
321
+ export_scenario_results(combined_result, args.scenario, args.report_type, args.dir)
322
+
323
+ return 0
324
+ else:
325
+ # Handle single profile execution (existing logic)
326
+ return _execute_single_scenario(args)
353
327
 
354
328
  except ImportError as e:
355
329
  console.print(f"[red]❌ Scenario '{args.scenario}' not available: {e}[/red]")
@@ -358,26 +332,127 @@ def main() -> int:
358
332
  console.print(f"[red]❌ Scenario execution failed: {e}[/red]")
359
333
  return 1
360
334
 
335
+
336
+ def _execute_single_scenario(args: argparse.Namespace) -> int:
337
+ """Execute a scenario for a single profile (internal helper function)."""
338
+ import argparse
339
+ from runbooks.common.rich_utils import print_header, print_success, print_error, print_info
340
+ from runbooks.common.profile_utils import get_profile_for_operation
341
+
342
+ def execute_workspaces_scenario():
343
+ from runbooks.finops.scenarios import finops_workspaces
344
+ # Use enterprise profile resolution: User > Environment > Default
345
+ profile_param = get_profile_for_operation("billing", args.profiles[0] if args.profiles else None)
346
+ return finops_workspaces(profile=profile_param)
347
+
348
+ def execute_snapshots_scenario():
349
+ from runbooks.finops.scenarios import finops_snapshots
350
+ # Use enterprise profile resolution: User > Environment > Default
351
+ profile_param = get_profile_for_operation("billing", args.profiles[0] if args.profiles else None)
352
+ return finops_snapshots(profile=profile_param)
353
+
354
+ def execute_commvault_scenario():
355
+ from runbooks.finops.scenarios import finops_commvault
356
+ # Use enterprise profile resolution: User > Environment > Default
357
+ profile_param = get_profile_for_operation("billing", args.profiles[0] if args.profiles else None)
358
+ return finops_commvault(profile=profile_param)
359
+
360
+ def execute_nat_gateway_scenario():
361
+ from runbooks.finops.nat_gateway_optimizer import nat_gateway_optimizer
362
+ # Use enterprise profile resolution: User > Environment > Default
363
+ profile_param = get_profile_for_operation("billing", args.profiles[0] if args.profiles else None)
364
+ regions = args.regions if args.regions else ['us-east-1']
365
+ # Call the CLI function with default parameters
366
+ nat_gateway_optimizer(
367
+ profile=profile_param,
368
+ regions=regions,
369
+ dry_run=True,
370
+ export_format='json',
371
+ output_file=None,
372
+ usage_threshold_days=7
373
+ )
374
+ return {"scenario": "nat-gateway", "status": "completed", "profile": profile_param}
375
+
376
+ def execute_ebs_scenario():
377
+ # Create a simplified EBS scenario execution
378
+ print_info("EBS optimization scenario analysis")
379
+ # Use enterprise profile resolution: User > Environment > Default
380
+ profile_param = get_profile_for_operation("billing", args.profiles[0] if args.profiles else None)
381
+ return {"scenario": "ebs", "status": "completed", "profile": profile_param}
382
+
383
+ def execute_vpc_cleanup_scenario():
384
+ # Create a simplified VPC cleanup scenario execution
385
+ print_info("VPC cleanup scenario analysis")
386
+ # Use enterprise profile resolution: User > Environment > Default
387
+ profile_param = get_profile_for_operation("billing", args.profiles[0] if args.profiles else None)
388
+ return {"scenario": "vpc-cleanup", "status": "completed", "profile": profile_param}
389
+
390
+ def execute_elastic_ip_scenario():
391
+ # Create a simplified elastic IP scenario execution
392
+ print_info("Elastic IP optimization scenario analysis")
393
+ # Use enterprise profile resolution: User > Environment > Default
394
+ profile_param = get_profile_for_operation("billing", args.profiles[0] if args.profiles else None)
395
+ return {"scenario": "elastic-ip", "status": "completed", "profile": profile_param}
396
+
397
+ # Map scenarios to execution functions
398
+ scenario_map = {
399
+ 'workspaces': execute_workspaces_scenario,
400
+ 'rds-snapshots': execute_snapshots_scenario,
401
+ 'backup-investigation': execute_commvault_scenario,
402
+ 'nat-gateway': execute_nat_gateway_scenario,
403
+ 'ebs-optimization': execute_ebs_scenario,
404
+ 'vpc-cleanup': execute_vpc_cleanup_scenario,
405
+ 'elastic-ip': execute_elastic_ip_scenario,
406
+ }
407
+
408
+ if args.scenario not in scenario_map:
409
+ print_error(f"Unknown scenario: '{args.scenario}'")
410
+ print_info("Available scenarios: " + ", ".join(scenario_map.keys()))
411
+ return 1
412
+
413
+ # Execute scenario
414
+ scenario_func = scenario_map[args.scenario]
415
+ result = scenario_func()
416
+
417
+ print_success(f"✅ Scenario '{args.scenario}' completed successfully")
418
+
419
+ # Export results if requested
420
+ if args.report_type and result:
421
+ from runbooks.finops.helpers import export_scenario_results
422
+ export_scenario_results(result, args.scenario, args.report_type, args.dir)
423
+
424
+ return 0
425
+
426
+
361
427
  # Handle PDCA mode
362
428
  if args.pdca or args.pdca_continuous:
363
- import asyncio
429
+ try:
430
+ import asyncio
431
+ from runbooks.finops.pdca_engine import AutonomousPDCAEngine, PDCAThresholds
432
+
433
+ console.print("[bold bright_cyan]🤖 Launching Autonomous PDCA Engine...[/]")
434
+
435
+ # Configure PDCA thresholds
436
+ thresholds = PDCAThresholds(
437
+ max_risk_score=25,
438
+ max_cost_increase=10.0,
439
+ max_untagged_resources=50,
440
+ max_unused_eips=5,
441
+ max_budget_overruns=1,
442
+ )
364
443
 
365
- from runbooks.finops.pdca_engine import AutonomousPDCAEngine, PDCAThresholds
444
+ # Initialize PDCA engine
445
+ artifacts_dir = args.dir or "artifacts"
366
446
 
367
- console.print("[bold bright_cyan]🤖 Launching Autonomous PDCA Engine...[/]")
447
+ # Ensure artifacts directory exists
448
+ import os
449
+ os.makedirs(artifacts_dir, exist_ok=True)
368
450
 
369
- # Configure PDCA thresholds
370
- thresholds = PDCAThresholds(
371
- max_risk_score=25,
372
- max_cost_increase=10.0,
373
- max_untagged_resources=50,
374
- max_unused_eips=5,
375
- max_budget_overruns=1,
376
- )
377
-
378
- # Initialize PDCA engine
379
- artifacts_dir = args.dir or "artifacts"
380
- engine = AutonomousPDCAEngine(thresholds=thresholds, artifacts_dir=artifacts_dir)
451
+ engine = AutonomousPDCAEngine(thresholds=thresholds, artifacts_dir=artifacts_dir)
452
+ except ImportError as e:
453
+ console.print(f"[red]❌ PDCA Engine not available: {e}[/]")
454
+ console.print("[yellow]💡 PDCA functionality requires additional setup[/]")
455
+ return 1
381
456
 
382
457
  try:
383
458
  # Determine execution mode
@@ -60,13 +60,14 @@ class UnusedNATGateway:
60
60
  creation_date: Optional[str] = None
61
61
  tags: Dict[str, str] = Field(default_factory=dict)
62
62
 
63
- @dataclass
63
+ @dataclass
64
64
  class CostOptimizationResult:
65
65
  """Results from cost optimization operations"""
66
66
  stopped_instances: List[IdleInstance] = Field(default_factory=list)
67
67
  deleted_volumes: List[LowUsageVolume] = Field(default_factory=list)
68
68
  deleted_nat_gateways: List[UnusedNATGateway] = Field(default_factory=list)
69
69
  total_potential_savings: float = 0.0
70
+ annual_savings: float = 0.0 # Annual savings projection for business scenarios
70
71
  execution_summary: Dict[str, Any] = Field(default_factory=dict)
71
72
 
72
73
  class AWSCostOptimizer:
@@ -24,6 +24,17 @@ MAX_CONCURRENT_COST_CALLS = 10 # AWS Cost Explorer rate limit consideration
24
24
  # Service filtering configuration for analytical insights
25
25
  NON_ANALYTICAL_SERVICES = ["Tax"] # Services excluded from Top N analysis per user requirements
26
26
 
27
+ # Enhanced caching for filter operations to prevent redundant logging
28
+ _filter_cache: Dict[str, tuple] = {}
29
+ _filter_session_id: Optional[str] = None
30
+
31
+ def _get_filter_session_id() -> str:
32
+ """Generate filter session ID for cache scoping"""
33
+ global _filter_session_id
34
+ if _filter_session_id is None:
35
+ _filter_session_id = f"filter_session_{int(time.time())}"
36
+ return _filter_session_id
37
+
27
38
 
28
39
  def filter_analytical_services(
29
40
  services_dict: Dict[str, float], excluded_services: List[str] = None
@@ -57,12 +68,19 @@ def filter_analytical_services(
57
68
  else:
58
69
  filtered_count += 1
59
70
 
60
- # Debug logging for enterprise troubleshooting
71
+ # SESSION-AWARE LOGGING: Only log once per session to prevent redundant messages
61
72
  if filtered_count > 0:
62
73
  excluded_names = [
63
74
  name for name in services_dict.keys() if any(excluded in name for excluded in excluded_services)
64
75
  ]
65
- console.log(f"[dim yellow]🔍 Filtered {filtered_count} non-analytical services: {', '.join(excluded_names)}[/]")
76
+
77
+ # Create cache key for this filter operation
78
+ cache_key = f"{_get_filter_session_id()}:filtered_services"
79
+
80
+ # Only log if not already logged in this session
81
+ if cache_key not in _filter_cache:
82
+ console.log(f"[dim yellow]🔍 Filtered {filtered_count} non-analytical services: {', '.join(excluded_names)}[/]")
83
+ _filter_cache[cache_key] = (filtered_count, excluded_names)
66
84
 
67
85
  return filtered_services
68
86
 
@@ -871,7 +889,7 @@ def format_budget_info(budgets: List[BudgetInfo]) -> List[str]:
871
889
 
872
890
 
873
891
  def calculate_quarterly_enhanced_trend(
874
- current: float,
892
+ current: float,
875
893
  previous: float,
876
894
  quarterly: float,
877
895
  current_days: Optional[int] = None,
@@ -879,32 +897,52 @@ def calculate_quarterly_enhanced_trend(
879
897
  ) -> str:
880
898
  """
881
899
  Calculate trend with quarterly financial intelligence for strategic decision making.
882
-
900
+
883
901
  Enhanced FinOps trend analysis that combines monthly operational trends with quarterly
884
902
  strategic context to provide executive-ready financial intelligence.
885
-
903
+
886
904
  Args:
887
905
  current: Current period cost
888
- previous: Previous period cost
906
+ previous: Previous period cost
889
907
  quarterly: Last quarter (3-month) average cost
890
908
  current_days: Number of days in current period
891
909
  previous_days: Number of days in previous period
892
-
910
+
893
911
  Returns:
894
912
  Strategic trend indicator with quarterly context
895
913
  """
896
914
  # Start with existing monthly trend logic
897
915
  monthly_trend = calculate_trend_with_context(current, previous, current_days, previous_days)
898
-
899
- # Add quarterly strategic context if available
900
- if quarterly > 0:
916
+
917
+ # Handle edge case where trend calculation returns "0.0% ⚠️"
918
+ if "0.0%" in monthly_trend and "⚠️" in monthly_trend:
919
+ # This likely means partial period comparison issue - provide clearer message
920
+ if current_days and previous_days and abs(current_days - previous_days) > 5:
921
+ return "⚠️ Partial data"
922
+ elif previous == 0 and current == 0:
923
+ return "→ No activity"
924
+ elif previous == 0 and current > 0:
925
+ return "↑ New costs"
926
+ else:
927
+ # Recalculate with simplified logic
928
+ if previous > 0:
929
+ change_percent = ((current - previous) / previous) * 100
930
+ if abs(change_percent) < 0.1:
931
+ return "→ Stable"
932
+ elif change_percent > 0:
933
+ return f"↑ {change_percent:.1f}%"
934
+ else:
935
+ return f"↓ {abs(change_percent):.1f}%"
936
+
937
+ # Add quarterly strategic context if available and quarterly data is meaningful
938
+ if quarterly > 0.01: # Only use quarterly if significant amount
901
939
  # Calculate quarterly average for monthly comparison
902
940
  quarterly_monthly_avg = quarterly / 3.0 # 3-month average
903
-
941
+
904
942
  # Compare current month against quarterly average
905
- if current > 0:
943
+ if current > 0.01: # Only if current has significant amount
906
944
  quarterly_variance = ((current - quarterly_monthly_avg) / quarterly_monthly_avg) * 100
907
-
945
+
908
946
  # Strategic quarterly indicators
909
947
  if abs(quarterly_variance) < 10: # Within 10% of quarterly average
910
948
  quarterly_context = "📊" # Consistent with quarterly patterns
@@ -914,11 +952,11 @@ def calculate_quarterly_enhanced_trend(
914
952
  quarterly_context = "📉" # Below quarterly baseline
915
953
  else:
916
954
  quarterly_context = "📊" # Normal quarterly variation
917
-
955
+
918
956
  # Combine monthly operational trend with quarterly strategic context
919
957
  return f"{quarterly_context} {monthly_trend}"
920
-
921
- # Fallback to standard monthly trend if no quarterly data
958
+
959
+ # Fallback to standard monthly trend if no quarterly data or not meaningful
922
960
  return monthly_trend
923
961
 
924
962
 
@@ -977,16 +1015,25 @@ def calculate_trend_with_context(current: float, previous: float,
977
1015
 
978
1016
  # Calculate basic percentage change
979
1017
  change_percent = ((current - previous) / previous) * 100
980
-
981
- # Handle partial period comparisons with warnings
1018
+
1019
+ # FIXED: Show meaningful percentage trends instead of generic messages
1020
+ if abs(change_percent) < 0.01: # Less than 0.01%
1021
+ if current == previous:
1022
+ return "→ 0.0%" # Show actual zero change percentage
1023
+ elif abs(current - previous) < 0.01: # Very small absolute difference
1024
+ return "→ <0.1%" # Show near-zero change with percentage
1025
+ else:
1026
+ # Show actual small change with precise percentage
1027
+ return f"{'↑' if change_percent > 0 else '↓'} {abs(change_percent):.2f}%"
1028
+
1029
+ # Handle partial period comparisons with clean display
982
1030
  if partial_period_issue:
983
- period_info = f" (⚠️ {current_days}d vs {previous_days}d)"
984
1031
  if abs(change_percent) > 50:
985
- return f"⚠️ Partial data comparison{period_info} - trend not reliable"
1032
+ return "⚠️ Trend not reliable (partial data)"
986
1033
  else:
987
1034
  base_trend = f"↑ {change_percent:.1f}%" if change_percent > 0 else f"↓ {abs(change_percent):.1f}%"
988
- return f"{base_trend}{period_info}"
989
-
1035
+ return f"{base_trend} ⚠️"
1036
+
990
1037
  # Standard trend analysis for equal periods
991
1038
  if abs(change_percent) > 90:
992
1039
  if change_percent > 0: