runbooks 1.0.0__py3-none-any.whl → 1.0.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.
- runbooks/__init__.py +1 -1
- runbooks/cfat/WEIGHT_CONFIG_README.md +368 -0
- runbooks/cfat/app.ts +27 -19
- runbooks/cfat/assessment/runner.py +6 -5
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/cloudops/models.py +20 -14
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1070 -105
- runbooks/common/aws_pricing_api.py +276 -44
- runbooks/common/date_utils.py +115 -0
- runbooks/common/dry_run_examples.py +587 -0
- runbooks/common/dry_run_framework.py +520 -0
- runbooks/common/enhanced_exception_handler.py +10 -7
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/memory_optimization.py +533 -0
- runbooks/common/performance_optimization_engine.py +1153 -0
- runbooks/common/profile_utils.py +86 -118
- runbooks/common/rich_utils.py +3 -3
- runbooks/common/sre_performance_suite.py +574 -0
- runbooks/finops/business_case_config.py +314 -0
- runbooks/finops/cost_processor.py +19 -4
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_cost_optimizer.py +1 -1
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/embedded_mcp_validator.py +642 -36
- runbooks/finops/enhanced_trend_visualization.py +7 -2
- runbooks/finops/executive_export.py +789 -0
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/finops_scenarios.py +34 -27
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/nat_gateway_optimizer.py +46 -27
- runbooks/finops/notebook_utils.py +1 -1
- runbooks/finops/schemas.py +73 -58
- runbooks/finops/single_dashboard.py +20 -4
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_exporter.py +2 -1
- runbooks/finops/vpc_cleanup_optimizer.py +22 -29
- runbooks/inventory/core/collector.py +51 -28
- runbooks/inventory/discovery.md +197 -247
- runbooks/inventory/inventory_modules.py +2 -2
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/models/account.py +5 -3
- runbooks/inventory/models/inventory.py +1 -1
- runbooks/inventory/models/resource.py +5 -3
- runbooks/inventory/organizations_discovery.py +102 -13
- runbooks/inventory/unified_validation_engine.py +2 -15
- runbooks/main.py +255 -92
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +17 -13
- runbooks/operate/vpc_operations.py +82 -13
- runbooks/remediation/base.py +3 -1
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +66 -18
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/security/compliance_automation_engine.py +99 -20
- runbooks/security/config/__init__.py +24 -0
- runbooks/security/config/compliance_config.py +255 -0
- runbooks/security/config/compliance_weights_example.json +22 -0
- runbooks/security/config_template_generator.py +500 -0
- runbooks/security/security_cli.py +377 -0
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +26 -15
- runbooks/validation/mcp_validator.py +62 -8
- runbooks/vpc/config.py +49 -15
- runbooks/vpc/cross_account_session.py +5 -1
- runbooks/vpc/heatmap_engine.py +438 -59
- runbooks/vpc/mcp_no_eni_validator.py +115 -36
- runbooks/vpc/performance_optimized_analyzer.py +546 -0
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +3 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/METADATA +1 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/RECORD +85 -79
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
- runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/vpc/runbooks.security.report_generator.log +0 -0
- runbooks/vpc/runbooks.security.run_script.log +0 -0
- runbooks/vpc/runbooks.security.security_export.log +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/WHEEL +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/top_level.txt +0 -0
runbooks/main.py
CHANGED
@@ -104,9 +104,31 @@ from runbooks.common.rich_utils import console, create_table, print_banner, prin
|
|
104
104
|
from runbooks.config import load_config, save_config
|
105
105
|
from runbooks.inventory.core.collector import InventoryCollector
|
106
106
|
from runbooks.utils import setup_logging, setup_enhanced_logging
|
107
|
+
from runbooks.finops.business_case_config import get_business_case_config, format_business_achievement
|
107
108
|
|
108
109
|
console = Console()
|
109
110
|
|
111
|
+
# ============================================================================
|
112
|
+
# CLI ARGUMENT FIXES - Handle Profile Tuples and Export Format Issues
|
113
|
+
# ============================================================================
|
114
|
+
|
115
|
+
def normalize_profile_parameter(profile_param):
|
116
|
+
"""
|
117
|
+
Normalize profile parameter from Click multiple=True tuple to string.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
profile_param: Profile parameter from Click (could be tuple, list, or string)
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
str: Single profile name for AWS operations
|
124
|
+
"""
|
125
|
+
if isinstance(profile_param, (tuple, list)) and profile_param:
|
126
|
+
return profile_param[0] # Take first profile from tuple/list
|
127
|
+
elif isinstance(profile_param, str):
|
128
|
+
return profile_param
|
129
|
+
else:
|
130
|
+
return "default"
|
131
|
+
|
110
132
|
# ============================================================================
|
111
133
|
# ACCOUNT ID RESOLUTION HELPER
|
112
134
|
# ============================================================================
|
@@ -219,7 +241,6 @@ def common_aws_options(f):
|
|
219
241
|
f = click.option(
|
220
242
|
"--profile",
|
221
243
|
multiple=True,
|
222
|
-
default=("default",), # Tuple default for multiple=True
|
223
244
|
help="AWS profile(s) - supports: --profile prof1 prof2 OR --profile prof1 --profile prof2 OR --profile prof1,prof2",
|
224
245
|
)(f)
|
225
246
|
f = click.option("--region", default="ap-southeast-2", help="AWS region (default: 'ap-southeast-2')")(f)
|
@@ -283,7 +304,7 @@ def common_filter_options(f):
|
|
283
304
|
Examples:
|
284
305
|
```bash
|
285
306
|
runbooks inventory ec2 --tags Environment=production Team=platform
|
286
|
-
runbooks inventory s3 --accounts
|
307
|
+
runbooks inventory s3 --accounts 111111111111 222222222222
|
287
308
|
runbooks inventory vpc --regions us-east-1 us-west-2 --tags CostCenter=engineering
|
288
309
|
```
|
289
310
|
"""
|
@@ -376,15 +397,23 @@ def main(ctx, debug, log_level, json_output, profile, region, dry_run, config):
|
|
376
397
|
@click.pass_context
|
377
398
|
def inventory(ctx, profile, region, dry_run, output, output_file, tags, accounts, regions):
|
378
399
|
"""
|
379
|
-
|
400
|
+
Universal AWS resource discovery and inventory - works with ANY AWS environment.
|
380
401
|
|
381
|
-
|
382
|
-
|
402
|
+
✅ Universal Compatibility: Works with single accounts, Organizations, and any profile setup
|
403
|
+
🔍 Read-only operations for safe resource discovery across AWS services
|
404
|
+
🚀 Intelligent fallback: Organizations → standalone account detection
|
405
|
+
|
406
|
+
Profile Options:
|
407
|
+
--profile PROFILE Use specific AWS profile (highest priority)
|
408
|
+
No --profile Uses AWS_PROFILE environment variable
|
409
|
+
No configuration Uses 'default' profile (universal AWS CLI compatibility)
|
383
410
|
|
384
411
|
Examples:
|
385
|
-
runbooks inventory collect
|
386
|
-
runbooks inventory collect --
|
387
|
-
runbooks inventory collect --
|
412
|
+
runbooks inventory collect # Use default profile
|
413
|
+
runbooks inventory collect --profile my-profile # Use specific profile
|
414
|
+
runbooks inventory collect --resources ec2,rds # Specific resources
|
415
|
+
runbooks inventory collect --all-accounts # Multi-account (if Organizations access)
|
416
|
+
runbooks inventory collect --tags Environment=prod # Filtered discovery
|
388
417
|
"""
|
389
418
|
# Update context with inventory-specific options
|
390
419
|
ctx.obj.update(
|
@@ -427,29 +456,35 @@ def inventory(ctx, profile, region, dry_run, output, output_file, tags, accounts
|
|
427
456
|
def collect(ctx, profile, region, dry_run, resources, all_resources, all_accounts, include_costs, parallel, validate, validate_all,
|
428
457
|
all, combine, csv, json, pdf, markdown, export_format, output_dir, report_name):
|
429
458
|
"""
|
430
|
-
🔍
|
459
|
+
🔍 Universal AWS resource inventory collection - works with ANY AWS environment.
|
431
460
|
|
432
|
-
|
433
|
-
-
|
434
|
-
-
|
435
|
-
-
|
436
|
-
-
|
461
|
+
✅ Universal Compatibility Features:
|
462
|
+
- Works with single accounts, AWS Organizations, and standalone setups
|
463
|
+
- Profile override priority: User > Environment > Default ('default' profile fallback)
|
464
|
+
- Intelligent Organizations detection with graceful standalone fallback
|
465
|
+
- 50+ AWS services discovery across any account configuration
|
466
|
+
- Multi-format exports: CSV, JSON, PDF, Markdown, YAML
|
437
467
|
- MCP validation for ≥99.5% accuracy
|
438
|
-
- 3-way comprehensive validation: runbooks + MCP + terraform
|
439
468
|
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
469
|
+
Universal Profile Usage:
|
470
|
+
- ANY AWS profile works (no hardcoded assumptions)
|
471
|
+
- Organizations permissions auto-detected (graceful fallback to single account)
|
472
|
+
- AWS_PROFILE environment variable used when available
|
473
|
+
- 'default' profile used as universal fallback
|
444
474
|
|
445
475
|
Examples:
|
446
|
-
|
447
|
-
runbooks inventory collect
|
448
|
-
runbooks inventory collect --profile
|
449
|
-
runbooks inventory collect --all-accounts
|
450
|
-
|
451
|
-
|
452
|
-
runbooks inventory collect --
|
476
|
+
# Universal compatibility - works with any AWS setup
|
477
|
+
runbooks inventory collect # Default profile
|
478
|
+
runbooks inventory collect --profile my-aws-profile # Any profile
|
479
|
+
runbooks inventory collect --all-accounts # Auto-detects Organizations
|
480
|
+
|
481
|
+
# Resource-specific discovery
|
482
|
+
runbooks inventory collect --resources ec2,rds,s3 # Specific services
|
483
|
+
runbooks inventory collect --all-resources # All 50+ services
|
484
|
+
|
485
|
+
# Multi-format exports
|
486
|
+
runbooks inventory collect --csv --json --pdf # Multiple formats
|
487
|
+
runbooks inventory collect --profile prod --validate --markdown
|
453
488
|
"""
|
454
489
|
try:
|
455
490
|
console.print(f"[blue]📊 Starting AWS Resource Inventory Collection[/blue]")
|
@@ -466,9 +501,7 @@ def collect(ctx, profile, region, dry_run, resources, all_resources, all_account
|
|
466
501
|
from runbooks.common.profile_utils import get_profile_for_operation
|
467
502
|
|
468
503
|
# Handle profile tuple (multiple=True in common_aws_options)
|
469
|
-
profile_value = profile
|
470
|
-
if isinstance(profile_value, tuple) and profile_value:
|
471
|
-
profile_value = profile_value[0] # Take first profile from tuple
|
504
|
+
profile_value = normalize_profile_parameter(profile)
|
472
505
|
|
473
506
|
resolved_profile = get_profile_for_operation("management", profile_value)
|
474
507
|
|
@@ -3674,30 +3707,58 @@ def security(ctx, profile, region, dry_run, language, output, output_file):
|
|
3674
3707
|
|
3675
3708
|
@security.command()
|
3676
3709
|
@common_aws_options
|
3710
|
+
@click.option(
|
3711
|
+
"--frameworks",
|
3712
|
+
multiple=True,
|
3713
|
+
type=click.Choice([
|
3714
|
+
"aws-well-architected",
|
3715
|
+
"soc2-type-ii",
|
3716
|
+
"pci-dss",
|
3717
|
+
"hipaa",
|
3718
|
+
"iso27001",
|
3719
|
+
"nist-cybersecurity",
|
3720
|
+
"cis-benchmarks"
|
3721
|
+
]),
|
3722
|
+
help="Compliance frameworks to assess (supports multiple)"
|
3723
|
+
)
|
3677
3724
|
@click.option("--checks", multiple=True, help="Specific security checks to run")
|
3678
|
-
@click.option("--export-formats", multiple=True,
|
3725
|
+
@click.option("--export-formats", multiple=True, help="Export formats (json, csv, pdf)")
|
3679
3726
|
@click.pass_context
|
3680
|
-
def assess(ctx, profile, region, dry_run, checks, export_formats):
|
3727
|
+
def assess(ctx, profile, region, dry_run, frameworks, checks, export_formats):
|
3681
3728
|
"""Run comprehensive security baseline assessment with Rich CLI output."""
|
3682
3729
|
try:
|
3683
3730
|
from runbooks.security.security_baseline_tester import SecurityBaselineTester
|
3684
3731
|
|
3685
3732
|
# Use command-level profile with fallback to context profile
|
3686
|
-
|
3733
|
+
# Handle profile tuple (multiple=True in common_aws_options) - CRITICAL CLI FIX
|
3734
|
+
profile_str = normalize_profile_parameter(profile)
|
3735
|
+
resolved_profile = profile_str or ctx.obj.get('profile', 'default')
|
3687
3736
|
resolved_region = region or ctx.obj.get('region', 'us-east-1')
|
3737
|
+
|
3738
|
+
# CRITICAL FIX: Handle empty export_formats after removing default value
|
3739
|
+
if not export_formats:
|
3740
|
+
export_formats = ["json", "csv"]
|
3741
|
+
|
3742
|
+
# CRITICAL FIX: Handle empty frameworks after removing default value
|
3743
|
+
if not frameworks:
|
3744
|
+
frameworks = ["aws-well-architected"]
|
3688
3745
|
|
3689
3746
|
console.print(f"[blue]🔒 Starting Security Assessment[/blue]")
|
3690
3747
|
console.print(
|
3691
|
-
f"[dim]Profile: {resolved_profile} | Language: {ctx.obj['language']} | Export: {', '.join(export_formats)}[/dim]"
|
3748
|
+
f"[dim]Profile: {resolved_profile} | Language: {ctx.obj['language']} | Frameworks: {', '.join(frameworks)} | Export: {', '.join(export_formats)}[/dim]"
|
3692
3749
|
)
|
3693
3750
|
|
3694
3751
|
# Initialize tester with export formats
|
3752
|
+
# TODO: Add frameworks support to SecurityBaselineTester for SOC2, PCI-DSS, HIPAA compliance
|
3695
3753
|
tester = SecurityBaselineTester(
|
3696
3754
|
profile=resolved_profile,
|
3697
3755
|
lang_code=ctx.obj["language"],
|
3698
3756
|
output_dir=ctx.obj.get("output_file"),
|
3699
3757
|
export_formats=list(export_formats),
|
3700
3758
|
)
|
3759
|
+
|
3760
|
+
# Store frameworks for future enhancement (currently using default aws-well-architected)
|
3761
|
+
console.print(f"[dim]Note: Using AWS Well-Architected baseline checks (frameworks parameter accepted for future enhancement)[/dim]")
|
3701
3762
|
|
3702
3763
|
# Run assessment with Rich CLI
|
3703
3764
|
tester.run()
|
@@ -5488,8 +5549,8 @@ def cost():
|
|
5488
5549
|
pass
|
5489
5550
|
|
5490
5551
|
@cost.command()
|
5491
|
-
@click.option('--billing-profile', default=None, help='AWS
|
5492
|
-
@click.option('--management-profile', default=None, help='AWS
|
5552
|
+
@click.option('--billing-profile', default=None, help='AWS profile for Cost Explorer access (uses universal profile selection if not specified)')
|
5553
|
+
@click.option('--management-profile', default=None, help='AWS profile for Organizations access (uses universal profile selection if not specified)')
|
5493
5554
|
@click.option('--tolerance-percent', default=5.0, help='MCP cross-validation tolerance percentage')
|
5494
5555
|
@click.option('--performance-target-ms', default=30000.0, help='Performance target in milliseconds')
|
5495
5556
|
@click.option('--export-evidence/--no-export', default=True, help='Export DoD validation evidence')
|
@@ -5857,7 +5918,7 @@ def workspaces(profile, region, dry_run, analyze, calculate_savings, unused_days
|
|
5857
5918
|
|
5858
5919
|
try:
|
5859
5920
|
# Handle profile tuple (multiple=True in common_aws_options)
|
5860
|
-
active_profile =
|
5921
|
+
active_profile = normalize_profile_parameter(profile)
|
5861
5922
|
|
5862
5923
|
# Call enhanced workspaces analysis
|
5863
5924
|
ctx = click.Context(get_workspaces)
|
@@ -5906,7 +5967,7 @@ def rds_snapshots(profile, region, dry_run, manual_only, older_than, calculate_s
|
|
5906
5967
|
|
5907
5968
|
try:
|
5908
5969
|
# Handle profile tuple (multiple=True in common_aws_options)
|
5909
|
-
active_profile =
|
5970
|
+
active_profile = normalize_profile_parameter(profile)
|
5910
5971
|
|
5911
5972
|
# Call enhanced RDS snapshot analysis
|
5912
5973
|
ctx = click.Context(get_rds_snapshot_details)
|
@@ -5935,25 +5996,34 @@ def rds_snapshots(profile, region, dry_run, manual_only, older_than, calculate_s
|
|
5935
5996
|
|
5936
5997
|
@cost_optimization.command()
|
5937
5998
|
@common_aws_options
|
5938
|
-
@click.option("--account",
|
5999
|
+
@click.option("--account", help="Commvault backup account ID (JIRA FinOps-25). If not specified, uses current AWS account.")
|
5939
6000
|
@click.option("--investigate-utilization", is_flag=True, help="Investigate EC2 utilization patterns")
|
5940
6001
|
@click.option("--output-file", default="./tmp/commvault_ec2_analysis.csv", help="Output CSV file path")
|
5941
6002
|
def commvault_ec2(profile, region, dry_run, account, investigate_utilization, output_file):
|
5942
6003
|
"""
|
5943
6004
|
FinOps-25: Commvault EC2 investigation for cost optimization.
|
5944
6005
|
|
5945
|
-
Account: 637423383469 (Commvault backup account)
|
5946
6006
|
Challenge: Determine if EC2 instances are actively used for backups or idle
|
5947
6007
|
"""
|
5948
6008
|
from runbooks.remediation.commvault_ec2_analysis import investigate_commvault_ec2
|
5949
6009
|
from runbooks.common.rich_utils import console, print_header
|
5950
6010
|
|
5951
6011
|
print_header("JIRA FinOps-25: Commvault EC2 Investigation", "v0.9.1")
|
5952
|
-
console.print(f"[cyan]Account: {account} - Investigating EC2 usage patterns[/cyan]")
|
5953
6012
|
|
5954
6013
|
try:
|
5955
6014
|
# Handle profile tuple (multiple=True in common_aws_options)
|
5956
|
-
active_profile =
|
6015
|
+
active_profile = normalize_profile_parameter(profile)
|
6016
|
+
|
6017
|
+
# If no account specified, detect current account from profile
|
6018
|
+
if not account:
|
6019
|
+
from runbooks.common.profile_utils import create_operational_session
|
6020
|
+
session = create_operational_session(active_profile)
|
6021
|
+
sts = session.client('sts')
|
6022
|
+
identity = sts.get_caller_identity()
|
6023
|
+
account = identity['Account']
|
6024
|
+
console.print(f"[dim cyan]Auto-detected account: {account}[/dim cyan]")
|
6025
|
+
|
6026
|
+
console.print(f"[cyan]Account: {account} - Investigating EC2 usage patterns[/cyan]")
|
5957
6027
|
|
5958
6028
|
# Call enhanced Commvault EC2 investigation
|
5959
6029
|
ctx = click.Context(investigate_commvault_ec2)
|
@@ -6039,10 +6109,31 @@ def comprehensive_analysis(profile, region, dry_run, all_scenarios, output_dir):
|
|
6039
6109
|
]
|
6040
6110
|
)
|
6041
6111
|
|
6042
|
-
|
6043
|
-
|
6044
|
-
|
6045
|
-
|
6112
|
+
# Use dynamic configuration for summary table
|
6113
|
+
config = get_business_case_config()
|
6114
|
+
workspaces_scenario = config.get_scenario('workspaces')
|
6115
|
+
rds_scenario = config.get_scenario('rds-snapshots')
|
6116
|
+
backup_scenario = config.get_scenario('backup-investigation')
|
6117
|
+
|
6118
|
+
summary_table.add_row(
|
6119
|
+
workspaces_scenario.scenario_id if workspaces_scenario else "workspaces",
|
6120
|
+
workspaces_scenario.display_name if workspaces_scenario else "WorkSpaces Resource Optimization",
|
6121
|
+
workspaces_scenario.savings_range_display if workspaces_scenario else "$12K-15K/year",
|
6122
|
+
"Analysis Complete"
|
6123
|
+
)
|
6124
|
+
summary_table.add_row(
|
6125
|
+
rds_scenario.scenario_id if rds_scenario else "rds-snapshots",
|
6126
|
+
rds_scenario.display_name if rds_scenario else "RDS Storage Optimization",
|
6127
|
+
rds_scenario.savings_range_display if rds_scenario else "$5K-24K/year",
|
6128
|
+
"Analysis Complete"
|
6129
|
+
)
|
6130
|
+
summary_table.add_row(
|
6131
|
+
backup_scenario.scenario_id if backup_scenario else "backup-investigation",
|
6132
|
+
backup_scenario.display_name if backup_scenario else "Backup Infrastructure Analysis",
|
6133
|
+
"Framework Ready",
|
6134
|
+
"Investigation Complete"
|
6135
|
+
)
|
6136
|
+
summary_table.add_row("📊 TOTAL", "All Scenarios", "Dynamic Configuration", "Ready for Implementation")
|
6046
6137
|
|
6047
6138
|
console.print(summary_table)
|
6048
6139
|
|
@@ -6084,7 +6175,7 @@ def _parse_profiles_parameter(profiles_tuple):
|
|
6084
6175
|
return [p for p in all_profiles if p] # Remove empty strings
|
6085
6176
|
|
6086
6177
|
|
6087
|
-
@main.
|
6178
|
+
@main.group(invoke_without_command=True)
|
6088
6179
|
@common_aws_options
|
6089
6180
|
@click.option("--time-range", type=int, help="Time range in days (default: current month)")
|
6090
6181
|
@click.option("--report-type", type=click.Choice(["csv", "json", "pdf", "markdown"]), help="Report type for export")
|
@@ -6143,7 +6234,7 @@ def _parse_profiles_parameter(profiles_tuple):
|
|
6143
6234
|
@click.option(
|
6144
6235
|
"--scenario",
|
6145
6236
|
type=click.Choice(["workspaces", "snapshots", "commvault", "nat-gateway", "elastic-ip", "ebs", "vpc-cleanup"], case_sensitive=False),
|
6146
|
-
help="Business scenario analysis
|
6237
|
+
help="Business scenario analysis with dynamic configuration - use 'runbooks finops --help-scenarios' for current scenario details"
|
6147
6238
|
)
|
6148
6239
|
@click.pass_context
|
6149
6240
|
def finops(
|
@@ -6183,14 +6274,16 @@ def finops(
|
|
6183
6274
|
Comprehensive cost analysis supporting both UnblendedCost (technical)
|
6184
6275
|
and AmortizedCost (financial) perspectives for enterprise reporting.
|
6185
6276
|
|
6186
|
-
BUSINESS SCENARIOS (
|
6187
|
-
runbooks finops --scenario workspaces #
|
6188
|
-
runbooks finops --scenario snapshots #
|
6189
|
-
runbooks finops --scenario commvault #
|
6190
|
-
runbooks finops --scenario nat-gateway #
|
6191
|
-
runbooks finops --scenario elastic-ip #
|
6192
|
-
runbooks finops --scenario ebs #
|
6193
|
-
runbooks finops --scenario vpc-cleanup #
|
6277
|
+
BUSINESS SCENARIOS (Dynamic Configuration):
|
6278
|
+
runbooks finops --scenario workspaces # WorkSpaces Resource Optimization
|
6279
|
+
runbooks finops --scenario snapshots # RDS Storage Optimization
|
6280
|
+
runbooks finops --scenario commvault # Backup Infrastructure Analysis
|
6281
|
+
runbooks finops --scenario nat-gateway # Network Gateway Optimization
|
6282
|
+
runbooks finops --scenario elastic-ip # IP Address Resource Management
|
6283
|
+
runbooks finops --scenario ebs # Storage Volume Optimization
|
6284
|
+
runbooks finops --scenario vpc-cleanup # Network Infrastructure Cleanup
|
6285
|
+
|
6286
|
+
Use --help-scenarios to see current configured targets and savings ranges.
|
6194
6287
|
|
6195
6288
|
GENERAL ANALYTICS:
|
6196
6289
|
runbooks finops --audit --csv --report-name audit_report
|
@@ -6201,6 +6294,14 @@ def finops(
|
|
6201
6294
|
runbooks finops --dual-metrics --csv --json # Comprehensive analysis (default)
|
6202
6295
|
"""
|
6203
6296
|
|
6297
|
+
# Handle group behavior: if no subcommand invoked, execute main functionality
|
6298
|
+
if ctx.invoked_subcommand is None:
|
6299
|
+
# Continue with original finops functionality
|
6300
|
+
pass
|
6301
|
+
else:
|
6302
|
+
# Subcommand will handle execution
|
6303
|
+
return
|
6304
|
+
|
6204
6305
|
# Business Scenario Dispatch Logic (Strategic Objective #1: Unified CLI)
|
6205
6306
|
if scenario:
|
6206
6307
|
from runbooks.common.rich_utils import console, print_header, print_success, print_info
|
@@ -6218,7 +6319,7 @@ def finops(
|
|
6218
6319
|
# Initialize CloudOps cost optimizer with enterprise patterns
|
6219
6320
|
execution_mode = ExecutionMode.DRY_RUN if dry_run else ExecutionMode.EXECUTE
|
6220
6321
|
# Ensure profile is a string, not a tuple
|
6221
|
-
profile_str =
|
6322
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6222
6323
|
cost_optimizer = CostOptimizer(
|
6223
6324
|
profile=profile_str,
|
6224
6325
|
dry_run=dry_run,
|
@@ -6226,7 +6327,10 @@ def finops(
|
|
6226
6327
|
)
|
6227
6328
|
|
6228
6329
|
if scenario.lower() == "workspaces":
|
6229
|
-
|
6330
|
+
config = get_business_case_config()
|
6331
|
+
workspaces_scenario = config.get_scenario('workspaces')
|
6332
|
+
scenario_info = f"{workspaces_scenario.display_name} ({workspaces_scenario.savings_range_display})" if workspaces_scenario else "WorkSpaces Resource Optimization"
|
6333
|
+
print_info(f"{scenario_info}")
|
6230
6334
|
print_info("🚀 Enhanced with CloudOps enterprise integration")
|
6231
6335
|
|
6232
6336
|
# Use CloudOps cost optimizer for enterprise-grade analysis
|
@@ -6235,10 +6339,10 @@ def finops(
|
|
6235
6339
|
dry_run=dry_run
|
6236
6340
|
))
|
6237
6341
|
|
6238
|
-
# Convert to
|
6342
|
+
# Convert to dynamic format using business case configuration
|
6239
6343
|
results = {
|
6240
|
-
"scenario": "
|
6241
|
-
"business_case": "WorkSpaces
|
6344
|
+
"scenario": workspaces_scenario.scenario_id if workspaces_scenario else "workspaces",
|
6345
|
+
"business_case": workspaces_scenario.display_name if workspaces_scenario else "WorkSpaces Resource Optimization",
|
6242
6346
|
"annual_savings": workspaces_result.annual_savings,
|
6243
6347
|
"monthly_savings": workspaces_result.total_monthly_savings,
|
6244
6348
|
"affected_resources": workspaces_result.affected_resources,
|
@@ -6248,7 +6352,10 @@ def finops(
|
|
6248
6352
|
}
|
6249
6353
|
|
6250
6354
|
elif scenario.lower() == "snapshots":
|
6251
|
-
|
6355
|
+
config = get_business_case_config()
|
6356
|
+
rds_scenario = config.get_scenario('rds-snapshots')
|
6357
|
+
scenario_info = f"{rds_scenario.display_name} ({rds_scenario.savings_range_display})" if rds_scenario else "RDS Storage Optimization"
|
6358
|
+
print_info(f"{scenario_info}")
|
6252
6359
|
print_info("🚀 Enhanced with CloudOps enterprise integration")
|
6253
6360
|
|
6254
6361
|
# Use CloudOps cost optimizer for enterprise-grade analysis
|
@@ -6257,10 +6364,10 @@ def finops(
|
|
6257
6364
|
dry_run=dry_run
|
6258
6365
|
))
|
6259
6366
|
|
6260
|
-
# Convert to
|
6367
|
+
# Convert to dynamic format using business case configuration
|
6261
6368
|
results = {
|
6262
|
-
"scenario": "
|
6263
|
-
"business_case": "RDS
|
6369
|
+
"scenario": rds_scenario.scenario_id if rds_scenario else "rds-snapshots",
|
6370
|
+
"business_case": rds_scenario.display_name if rds_scenario else "RDS Storage Optimization",
|
6264
6371
|
"annual_savings": snapshots_result.annual_savings,
|
6265
6372
|
"monthly_savings": snapshots_result.total_monthly_savings,
|
6266
6373
|
"affected_resources": snapshots_result.affected_resources,
|
@@ -6270,7 +6377,10 @@ def finops(
|
|
6270
6377
|
}
|
6271
6378
|
|
6272
6379
|
elif scenario.lower() == "commvault":
|
6273
|
-
|
6380
|
+
config = get_business_case_config()
|
6381
|
+
backup_scenario = config.get_scenario('backup-investigation')
|
6382
|
+
scenario_info = f"{backup_scenario.display_name}" if backup_scenario else "Backup Infrastructure Analysis"
|
6383
|
+
print_info(f"{scenario_info} (Real AWS integration)")
|
6274
6384
|
print_info("🚀 Enhanced with CloudOps enterprise integration")
|
6275
6385
|
|
6276
6386
|
# Use CloudOps cost optimizer for enterprise-grade investigation
|
@@ -6279,10 +6389,10 @@ def finops(
|
|
6279
6389
|
dry_run=True # Always dry-run for investigations
|
6280
6390
|
))
|
6281
6391
|
|
6282
|
-
# Convert to
|
6392
|
+
# Convert to dynamic format using business case configuration
|
6283
6393
|
results = {
|
6284
|
-
"scenario": "
|
6285
|
-
"business_case": "
|
6394
|
+
"scenario": backup_scenario.scenario_id if backup_scenario else "backup-investigation",
|
6395
|
+
"business_case": backup_scenario.display_name if backup_scenario else "Backup Infrastructure Analysis",
|
6286
6396
|
"annual_savings": commvault_result.annual_savings,
|
6287
6397
|
"monthly_savings": commvault_result.total_monthly_savings,
|
6288
6398
|
"affected_resources": commvault_result.affected_resources,
|
@@ -6293,13 +6403,16 @@ def finops(
|
|
6293
6403
|
}
|
6294
6404
|
|
6295
6405
|
elif scenario.lower() == "nat-gateway":
|
6296
|
-
|
6406
|
+
config = get_business_case_config()
|
6407
|
+
nat_scenario = config.get_scenario('nat-gateway')
|
6408
|
+
scenario_info = f"{nat_scenario.display_name} ({nat_scenario.savings_range_display})" if nat_scenario else "Network Gateway Optimization"
|
6409
|
+
print_info(f"{scenario_info}")
|
6297
6410
|
print_info("🚀 Enterprise multi-region analysis with network dependency validation")
|
6298
6411
|
|
6299
6412
|
# Use dedicated NAT Gateway optimizer for specialized analysis
|
6300
6413
|
from runbooks.finops.nat_gateway_optimizer import NATGatewayOptimizer
|
6301
6414
|
|
6302
|
-
profile_str =
|
6415
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6303
6416
|
nat_optimizer = NATGatewayOptimizer(
|
6304
6417
|
profile_name=profile_str,
|
6305
6418
|
regions=regions or ["us-east-1", "us-west-2", "eu-west-1"]
|
@@ -6307,10 +6420,10 @@ def finops(
|
|
6307
6420
|
|
6308
6421
|
nat_result = asyncio.run(nat_optimizer.analyze_nat_gateways(dry_run=dry_run))
|
6309
6422
|
|
6310
|
-
# Convert to
|
6423
|
+
# Convert to dynamic format using business case configuration
|
6311
6424
|
results = {
|
6312
|
-
"scenario": "
|
6313
|
-
"business_case": "
|
6425
|
+
"scenario": nat_scenario.scenario_id if nat_scenario else "nat-gateway",
|
6426
|
+
"business_case": nat_scenario.display_name if nat_scenario else "Network Gateway Optimization",
|
6314
6427
|
"annual_savings": nat_result.potential_annual_savings,
|
6315
6428
|
"monthly_savings": nat_result.potential_monthly_savings,
|
6316
6429
|
"total_nat_gateways": nat_result.total_nat_gateways,
|
@@ -6328,13 +6441,16 @@ def finops(
|
|
6328
6441
|
}
|
6329
6442
|
|
6330
6443
|
elif scenario.lower() == "elastic-ip":
|
6331
|
-
|
6444
|
+
config = get_business_case_config()
|
6445
|
+
eip_scenario = config.get_scenario('elastic-ip')
|
6446
|
+
scenario_info = f"{eip_scenario.display_name} ({eip_scenario.savings_range_display})" if eip_scenario else "IP Address Resource Management"
|
6447
|
+
print_info(f"{scenario_info}")
|
6332
6448
|
print_info("🚀 Enterprise multi-region analysis with DNS dependency validation")
|
6333
6449
|
|
6334
6450
|
# Use dedicated Elastic IP optimizer for specialized analysis
|
6335
6451
|
from runbooks.finops.elastic_ip_optimizer import ElasticIPOptimizer
|
6336
6452
|
|
6337
|
-
profile_str =
|
6453
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6338
6454
|
eip_optimizer = ElasticIPOptimizer(
|
6339
6455
|
profile_name=profile_str,
|
6340
6456
|
regions=regions or ["us-east-1", "us-west-2", "eu-west-1", "us-east-2"]
|
@@ -6342,10 +6458,10 @@ def finops(
|
|
6342
6458
|
|
6343
6459
|
eip_result = asyncio.run(eip_optimizer.analyze_elastic_ips(dry_run=dry_run))
|
6344
6460
|
|
6345
|
-
# Convert to
|
6461
|
+
# Convert to dynamic format using business case configuration
|
6346
6462
|
results = {
|
6347
|
-
"scenario": "
|
6348
|
-
"business_case": "
|
6463
|
+
"scenario": eip_scenario.scenario_id if eip_scenario else "elastic-ip",
|
6464
|
+
"business_case": eip_scenario.display_name if eip_scenario else "IP Address Resource Management",
|
6349
6465
|
"annual_savings": eip_result.potential_annual_savings,
|
6350
6466
|
"monthly_savings": eip_result.potential_monthly_savings,
|
6351
6467
|
"total_elastic_ips": eip_result.total_elastic_ips,
|
@@ -6365,13 +6481,16 @@ def finops(
|
|
6365
6481
|
}
|
6366
6482
|
|
6367
6483
|
elif scenario.lower() == "ebs":
|
6368
|
-
|
6484
|
+
config = get_business_case_config()
|
6485
|
+
ebs_scenario = config.get_scenario('ebs-optimization')
|
6486
|
+
scenario_info = f"{ebs_scenario.display_name}" if ebs_scenario else "Storage Volume Optimization (15-20% cost reduction potential)"
|
6487
|
+
print_info(f"{scenario_info}")
|
6369
6488
|
print_info("🚀 Enterprise comprehensive analysis: GP2→GP3 + Usage + Orphaned cleanup")
|
6370
6489
|
|
6371
6490
|
# Use dedicated EBS optimizer for specialized analysis
|
6372
6491
|
from runbooks.finops.ebs_optimizer import EBSOptimizer
|
6373
6492
|
|
6374
|
-
profile_str =
|
6493
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6375
6494
|
ebs_optimizer = EBSOptimizer(
|
6376
6495
|
profile_name=profile_str,
|
6377
6496
|
regions=regions or ["us-east-1", "us-west-2", "eu-west-1"]
|
@@ -6379,10 +6498,10 @@ def finops(
|
|
6379
6498
|
|
6380
6499
|
ebs_result = asyncio.run(ebs_optimizer.analyze_ebs_volumes(dry_run=dry_run))
|
6381
6500
|
|
6382
|
-
# Convert to
|
6501
|
+
# Convert to dynamic format using business case configuration
|
6383
6502
|
results = {
|
6384
|
-
"scenario": "
|
6385
|
-
"business_case": "
|
6503
|
+
"scenario": ebs_scenario.scenario_id if ebs_scenario else "ebs-optimization",
|
6504
|
+
"business_case": ebs_scenario.display_name if ebs_scenario else "Storage Volume Optimization",
|
6386
6505
|
"annual_savings": ebs_result.total_potential_annual_savings,
|
6387
6506
|
"monthly_savings": ebs_result.total_potential_monthly_savings,
|
6388
6507
|
"total_volumes": ebs_result.total_volumes,
|
@@ -6410,13 +6529,16 @@ def finops(
|
|
6410
6529
|
}
|
6411
6530
|
|
6412
6531
|
elif scenario.lower() == "vpc-cleanup":
|
6413
|
-
|
6532
|
+
config = get_business_case_config()
|
6533
|
+
vpc_scenario = config.get_scenario('vpc-cleanup')
|
6534
|
+
scenario_info = f"{vpc_scenario.display_name} ({vpc_scenario.savings_range_display})" if vpc_scenario else "Network Infrastructure Cleanup"
|
6535
|
+
print_info(f"{scenario_info}")
|
6414
6536
|
print_info("🚀 Enterprise three-bucket strategy with dependency validation")
|
6415
6537
|
|
6416
6538
|
# Use dedicated VPC Cleanup optimizer for AWSO-05 analysis
|
6417
6539
|
from runbooks.finops.vpc_cleanup_optimizer import VPCCleanupOptimizer
|
6418
6540
|
|
6419
|
-
profile_str =
|
6541
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6420
6542
|
vpc_optimizer = VPCCleanupOptimizer(
|
6421
6543
|
profile=profile_str
|
6422
6544
|
)
|
@@ -6431,10 +6553,10 @@ def finops(
|
|
6431
6553
|
filter_type=filter_type
|
6432
6554
|
)
|
6433
6555
|
|
6434
|
-
# Convert to
|
6556
|
+
# Convert to dynamic format using business case configuration
|
6435
6557
|
results = {
|
6436
|
-
"scenario": "
|
6437
|
-
"business_case":
|
6558
|
+
"scenario": vpc_scenario.scenario_id if vpc_scenario else "vpc-cleanup",
|
6559
|
+
"business_case": vpc_scenario.display_name if vpc_scenario else "Network Infrastructure Cleanup",
|
6438
6560
|
"annual_savings": vpc_result.total_annual_savings,
|
6439
6561
|
"monthly_savings": vpc_result.total_annual_savings / 12,
|
6440
6562
|
"total_vpcs_analyzed": vpc_result.total_vpcs_analyzed,
|
@@ -6604,7 +6726,7 @@ def finops(
|
|
6604
6726
|
# CRITICAL FIX: Ensure single profile is correctly handled for downstream processing
|
6605
6727
|
# When multiple profiles are provided via --profile, use the first one as primary profile
|
6606
6728
|
primary_profile = (
|
6607
|
-
parsed_profiles[0] if parsed_profiles else (profile
|
6729
|
+
parsed_profiles[0] if parsed_profiles else normalize_profile_parameter(profile)
|
6608
6730
|
)
|
6609
6731
|
|
6610
6732
|
args = argparse.Namespace(
|
@@ -6644,6 +6766,45 @@ def finops(
|
|
6644
6766
|
return run_dashboard(args)
|
6645
6767
|
|
6646
6768
|
|
6769
|
+
# ============================================================================
|
6770
|
+
# FINOPS SUBCOMMANDS - Enhanced CLI Structure
|
6771
|
+
# ============================================================================
|
6772
|
+
|
6773
|
+
@finops.command()
|
6774
|
+
@click.option("--profile", help="AWS profile to use")
|
6775
|
+
@click.option("--region", help="AWS region")
|
6776
|
+
@click.option("--dry-run", is_flag=True, help="Run in dry-run mode")
|
6777
|
+
@click.option("--output", type=click.Choice(["json", "csv", "pdf", "html"]), help="Output format")
|
6778
|
+
def dashboard(profile, region, dry_run, output):
|
6779
|
+
"""
|
6780
|
+
FinOps cost analytics dashboard.
|
6781
|
+
|
6782
|
+
Interactive cost analysis with Rich CLI formatting and enterprise-grade reporting.
|
6783
|
+
"""
|
6784
|
+
from runbooks.common.rich_utils import console, print_header
|
6785
|
+
from runbooks.finops.dashboard_runner import run_dashboard
|
6786
|
+
import argparse
|
6787
|
+
|
6788
|
+
print_header("FinOps Dashboard", "Cost Analytics & Optimization")
|
6789
|
+
|
6790
|
+
# Create args namespace compatible with existing dashboard runner
|
6791
|
+
args = argparse.Namespace(
|
6792
|
+
profile=profile or "default",
|
6793
|
+
region=region or "ap-southeast-2",
|
6794
|
+
dry_run=dry_run,
|
6795
|
+
time_range=30,
|
6796
|
+
report_type=output,
|
6797
|
+
csv=(output == "csv") if output else False,
|
6798
|
+
json=(output == "json") if output else False,
|
6799
|
+
pdf=(output == "pdf") if output else False,
|
6800
|
+
audit=False,
|
6801
|
+
trend=False,
|
6802
|
+
validate=False
|
6803
|
+
)
|
6804
|
+
|
6805
|
+
return run_dashboard(args)
|
6806
|
+
|
6807
|
+
|
6647
6808
|
# ============================================================================
|
6648
6809
|
# FINOPS BUSINESS SCENARIOS - MANAGER PRIORITY COST OPTIMIZATION
|
6649
6810
|
# ============================================================================
|
@@ -7343,7 +7504,9 @@ def scan(ctx, profile, region, dry_run, resources):
|
|
7343
7504
|
from runbooks.inventory.core.collector import InventoryCollector
|
7344
7505
|
|
7345
7506
|
try:
|
7346
|
-
|
7507
|
+
# Handle profile tuple (multiple=True in common_aws_options) - CRITICAL CLI FIX
|
7508
|
+
profile_str = normalize_profile_parameter(profile)
|
7509
|
+
collector = InventoryCollector(profile=profile_str, region=region)
|
7347
7510
|
|
7348
7511
|
# Get current account ID
|
7349
7512
|
account_ids = [collector.get_current_account_id()]
|
@@ -7675,7 +7838,7 @@ def vpc(ctx, profile, region, dry_run, all, billing_profile, management_profile,
|
|
7675
7838
|
# If no subcommand is specified, run cleanup analysis by default (KISS principle)
|
7676
7839
|
if ctx.invoked_subcommand is None:
|
7677
7840
|
# Handle profile tuple like other commands
|
7678
|
-
active_profile =
|
7841
|
+
active_profile = normalize_profile_parameter(profile)
|
7679
7842
|
|
7680
7843
|
console.print("[cyan]🧹 VPC Cleanup Analysis - Enterprise Safety Controls Enabled[/cyan]")
|
7681
7844
|
console.print(f"[dim]Using AWS profile: {active_profile}[/dim]")
|
@@ -7802,7 +7965,7 @@ def analyze(ctx, profile, region, dry_run, vpc_ids, output_dir, generate_evidenc
|
|
7802
7965
|
runbooks vpc analyze --vpc-ids vpc-123 vpc-456 --generate-evidence
|
7803
7966
|
"""
|
7804
7967
|
# Fix profile tuple handling like other commands (lines 5567-5568 pattern)
|
7805
|
-
active_profile =
|
7968
|
+
active_profile = normalize_profile_parameter(profile)
|
7806
7969
|
|
7807
7970
|
console.print("[cyan]🔍 VPC Analysis - Enhanced with VPC Module Integration[/cyan]")
|
7808
7971
|
console.print(f"[dim]Using AWS profile: {active_profile}[/dim]")
|