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.
- runbooks/__init__.py +1 -1
- runbooks/cfat/assessment/collectors.py +3 -2
- runbooks/cloudops/cost_optimizer.py +235 -83
- runbooks/cloudops/models.py +8 -2
- runbooks/common/aws_pricing.py +12 -0
- runbooks/common/business_logic.py +1 -1
- runbooks/common/profile_utils.py +213 -310
- runbooks/common/rich_utils.py +15 -21
- runbooks/finops/README.md +3 -3
- runbooks/finops/__init__.py +13 -5
- runbooks/finops/business_case_config.py +5 -5
- runbooks/finops/cli.py +170 -95
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/cost_processor.py +69 -22
- runbooks/finops/dashboard_router.py +3 -3
- runbooks/finops/dashboard_runner.py +3 -4
- runbooks/finops/embedded_mcp_validator.py +101 -23
- runbooks/finops/enhanced_progress.py +213 -0
- runbooks/finops/finops_scenarios.py +90 -16
- runbooks/finops/markdown_exporter.py +4 -2
- runbooks/finops/multi_dashboard.py +1 -1
- runbooks/finops/nat_gateway_optimizer.py +85 -57
- runbooks/finops/rds_snapshot_optimizer.py +1389 -0
- runbooks/finops/scenario_cli_integration.py +212 -22
- runbooks/finops/scenarios.py +41 -25
- runbooks/finops/single_dashboard.py +68 -9
- runbooks/finops/tests/run_tests.py +5 -3
- runbooks/finops/vpc_cleanup_optimizer.py +1 -1
- runbooks/finops/workspaces_analyzer.py +40 -16
- runbooks/inventory/list_rds_snapshots_aggregator.py +745 -0
- runbooks/main.py +393 -61
- runbooks/operate/executive_dashboard.py +4 -3
- runbooks/remediation/rds_snapshot_list.py +13 -0
- {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/METADATA +234 -40
- {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/RECORD +39 -37
- {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/WHEEL +0 -0
- {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/top_level.txt +0 -0
runbooks/common/rich_utils.py
CHANGED
@@ -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 =
|
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
|
118
|
-
|
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
|
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
|
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]
|
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
|
29
|
-
runbooks finops --scenario snapshots --profile PROFILE
|
30
|
-
runbooks finops --scenario
|
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)
|
runbooks/finops/__init__.py
CHANGED
@@ -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
|
-
|
73
|
-
|
74
|
-
|
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
|
-
#
|
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="
|
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,
|
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
|
-
#
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
profile
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
#
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
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
|
-
|
444
|
+
# Initialize PDCA engine
|
445
|
+
artifacts_dir = args.dir or "artifacts"
|
366
446
|
|
367
|
-
|
447
|
+
# Ensure artifacts directory exists
|
448
|
+
import os
|
449
|
+
os.makedirs(artifacts_dir, exist_ok=True)
|
368
450
|
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
900
|
-
if
|
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
|
-
#
|
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
|
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}
|
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:
|