runbooks 1.0.1__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/cloudops/models.py +20 -14
- runbooks/common/aws_pricing_api.py +276 -44
- runbooks/common/dry_run_examples.py +587 -0
- runbooks/common/dry_run_framework.py +520 -0
- runbooks/common/memory_optimization.py +533 -0
- runbooks/common/performance_optimization_engine.py +1153 -0
- runbooks/common/profile_utils.py +10 -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/ebs_cost_optimizer.py +1 -1
- runbooks/finops/embedded_mcp_validator.py +642 -36
- runbooks/finops/executive_export.py +789 -0
- runbooks/finops/finops_scenarios.py +34 -27
- runbooks/finops/notebook_utils.py +1 -1
- runbooks/finops/schemas.py +73 -58
- runbooks/finops/single_dashboard.py +20 -4
- runbooks/finops/vpc_cleanup_exporter.py +2 -1
- 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 +89 -5
- runbooks/main.py +182 -61
- runbooks/operate/vpc_operations.py +60 -31
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/vpc/config.py +17 -8
- runbooks/vpc/heatmap_engine.py +425 -53
- runbooks/vpc/performance_optimized_analyzer.py +546 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/METADATA +1 -1
- {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/RECORD +34 -26
- {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/WHEEL +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.1.dist-info → runbooks-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.1.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)
|
@@ -480,9 +501,7 @@ def collect(ctx, profile, region, dry_run, resources, all_resources, all_account
|
|
480
501
|
from runbooks.common.profile_utils import get_profile_for_operation
|
481
502
|
|
482
503
|
# Handle profile tuple (multiple=True in common_aws_options)
|
483
|
-
profile_value = profile
|
484
|
-
if isinstance(profile_value, tuple) and profile_value:
|
485
|
-
profile_value = profile_value[0] # Take first profile from tuple
|
504
|
+
profile_value = normalize_profile_parameter(profile)
|
486
505
|
|
487
506
|
resolved_profile = get_profile_for_operation("management", profile_value)
|
488
507
|
|
@@ -3700,11 +3719,10 @@ def security(ctx, profile, region, dry_run, language, output, output_file):
|
|
3700
3719
|
"nist-cybersecurity",
|
3701
3720
|
"cis-benchmarks"
|
3702
3721
|
]),
|
3703
|
-
default=["aws-well-architected"],
|
3704
3722
|
help="Compliance frameworks to assess (supports multiple)"
|
3705
3723
|
)
|
3706
3724
|
@click.option("--checks", multiple=True, help="Specific security checks to run")
|
3707
|
-
@click.option("--export-formats", multiple=True,
|
3725
|
+
@click.option("--export-formats", multiple=True, help="Export formats (json, csv, pdf)")
|
3708
3726
|
@click.pass_context
|
3709
3727
|
def assess(ctx, profile, region, dry_run, frameworks, checks, export_formats):
|
3710
3728
|
"""Run comprehensive security baseline assessment with Rich CLI output."""
|
@@ -3712,8 +3730,18 @@ def assess(ctx, profile, region, dry_run, frameworks, checks, export_formats):
|
|
3712
3730
|
from runbooks.security.security_baseline_tester import SecurityBaselineTester
|
3713
3731
|
|
3714
3732
|
# Use command-level profile with fallback to context profile
|
3715
|
-
|
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')
|
3716
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"]
|
3717
3745
|
|
3718
3746
|
console.print(f"[blue]🔒 Starting Security Assessment[/blue]")
|
3719
3747
|
console.print(
|
@@ -5890,7 +5918,7 @@ def workspaces(profile, region, dry_run, analyze, calculate_savings, unused_days
|
|
5890
5918
|
|
5891
5919
|
try:
|
5892
5920
|
# Handle profile tuple (multiple=True in common_aws_options)
|
5893
|
-
active_profile =
|
5921
|
+
active_profile = normalize_profile_parameter(profile)
|
5894
5922
|
|
5895
5923
|
# Call enhanced workspaces analysis
|
5896
5924
|
ctx = click.Context(get_workspaces)
|
@@ -5939,7 +5967,7 @@ def rds_snapshots(profile, region, dry_run, manual_only, older_than, calculate_s
|
|
5939
5967
|
|
5940
5968
|
try:
|
5941
5969
|
# Handle profile tuple (multiple=True in common_aws_options)
|
5942
|
-
active_profile =
|
5970
|
+
active_profile = normalize_profile_parameter(profile)
|
5943
5971
|
|
5944
5972
|
# Call enhanced RDS snapshot analysis
|
5945
5973
|
ctx = click.Context(get_rds_snapshot_details)
|
@@ -5984,7 +6012,7 @@ def commvault_ec2(profile, region, dry_run, account, investigate_utilization, ou
|
|
5984
6012
|
|
5985
6013
|
try:
|
5986
6014
|
# Handle profile tuple (multiple=True in common_aws_options)
|
5987
|
-
active_profile =
|
6015
|
+
active_profile = normalize_profile_parameter(profile)
|
5988
6016
|
|
5989
6017
|
# If no account specified, detect current account from profile
|
5990
6018
|
if not account:
|
@@ -6081,10 +6109,31 @@ def comprehensive_analysis(profile, region, dry_run, all_scenarios, output_dir):
|
|
6081
6109
|
]
|
6082
6110
|
)
|
6083
6111
|
|
6084
|
-
|
6085
|
-
|
6086
|
-
|
6087
|
-
|
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")
|
6088
6137
|
|
6089
6138
|
console.print(summary_table)
|
6090
6139
|
|
@@ -6126,7 +6175,7 @@ def _parse_profiles_parameter(profiles_tuple):
|
|
6126
6175
|
return [p for p in all_profiles if p] # Remove empty strings
|
6127
6176
|
|
6128
6177
|
|
6129
|
-
@main.
|
6178
|
+
@main.group(invoke_without_command=True)
|
6130
6179
|
@common_aws_options
|
6131
6180
|
@click.option("--time-range", type=int, help="Time range in days (default: current month)")
|
6132
6181
|
@click.option("--report-type", type=click.Choice(["csv", "json", "pdf", "markdown"]), help="Report type for export")
|
@@ -6185,7 +6234,7 @@ def _parse_profiles_parameter(profiles_tuple):
|
|
6185
6234
|
@click.option(
|
6186
6235
|
"--scenario",
|
6187
6236
|
type=click.Choice(["workspaces", "snapshots", "commvault", "nat-gateway", "elastic-ip", "ebs", "vpc-cleanup"], case_sensitive=False),
|
6188
|
-
help="Business scenario analysis
|
6237
|
+
help="Business scenario analysis with dynamic configuration - use 'runbooks finops --help-scenarios' for current scenario details"
|
6189
6238
|
)
|
6190
6239
|
@click.pass_context
|
6191
6240
|
def finops(
|
@@ -6225,14 +6274,16 @@ def finops(
|
|
6225
6274
|
Comprehensive cost analysis supporting both UnblendedCost (technical)
|
6226
6275
|
and AmortizedCost (financial) perspectives for enterprise reporting.
|
6227
6276
|
|
6228
|
-
BUSINESS SCENARIOS (
|
6229
|
-
runbooks finops --scenario workspaces #
|
6230
|
-
runbooks finops --scenario snapshots #
|
6231
|
-
runbooks finops --scenario commvault #
|
6232
|
-
runbooks finops --scenario nat-gateway #
|
6233
|
-
runbooks finops --scenario elastic-ip #
|
6234
|
-
runbooks finops --scenario ebs #
|
6235
|
-
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.
|
6236
6287
|
|
6237
6288
|
GENERAL ANALYTICS:
|
6238
6289
|
runbooks finops --audit --csv --report-name audit_report
|
@@ -6243,6 +6294,14 @@ def finops(
|
|
6243
6294
|
runbooks finops --dual-metrics --csv --json # Comprehensive analysis (default)
|
6244
6295
|
"""
|
6245
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
|
+
|
6246
6305
|
# Business Scenario Dispatch Logic (Strategic Objective #1: Unified CLI)
|
6247
6306
|
if scenario:
|
6248
6307
|
from runbooks.common.rich_utils import console, print_header, print_success, print_info
|
@@ -6260,7 +6319,7 @@ def finops(
|
|
6260
6319
|
# Initialize CloudOps cost optimizer with enterprise patterns
|
6261
6320
|
execution_mode = ExecutionMode.DRY_RUN if dry_run else ExecutionMode.EXECUTE
|
6262
6321
|
# Ensure profile is a string, not a tuple
|
6263
|
-
profile_str =
|
6322
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6264
6323
|
cost_optimizer = CostOptimizer(
|
6265
6324
|
profile=profile_str,
|
6266
6325
|
dry_run=dry_run,
|
@@ -6268,7 +6327,10 @@ def finops(
|
|
6268
6327
|
)
|
6269
6328
|
|
6270
6329
|
if scenario.lower() == "workspaces":
|
6271
|
-
|
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}")
|
6272
6334
|
print_info("🚀 Enhanced with CloudOps enterprise integration")
|
6273
6335
|
|
6274
6336
|
# Use CloudOps cost optimizer for enterprise-grade analysis
|
@@ -6277,10 +6339,10 @@ def finops(
|
|
6277
6339
|
dry_run=dry_run
|
6278
6340
|
))
|
6279
6341
|
|
6280
|
-
# Convert to
|
6342
|
+
# Convert to dynamic format using business case configuration
|
6281
6343
|
results = {
|
6282
|
-
"scenario": "
|
6283
|
-
"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",
|
6284
6346
|
"annual_savings": workspaces_result.annual_savings,
|
6285
6347
|
"monthly_savings": workspaces_result.total_monthly_savings,
|
6286
6348
|
"affected_resources": workspaces_result.affected_resources,
|
@@ -6290,7 +6352,10 @@ def finops(
|
|
6290
6352
|
}
|
6291
6353
|
|
6292
6354
|
elif scenario.lower() == "snapshots":
|
6293
|
-
|
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}")
|
6294
6359
|
print_info("🚀 Enhanced with CloudOps enterprise integration")
|
6295
6360
|
|
6296
6361
|
# Use CloudOps cost optimizer for enterprise-grade analysis
|
@@ -6299,10 +6364,10 @@ def finops(
|
|
6299
6364
|
dry_run=dry_run
|
6300
6365
|
))
|
6301
6366
|
|
6302
|
-
# Convert to
|
6367
|
+
# Convert to dynamic format using business case configuration
|
6303
6368
|
results = {
|
6304
|
-
"scenario": "
|
6305
|
-
"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",
|
6306
6371
|
"annual_savings": snapshots_result.annual_savings,
|
6307
6372
|
"monthly_savings": snapshots_result.total_monthly_savings,
|
6308
6373
|
"affected_resources": snapshots_result.affected_resources,
|
@@ -6312,7 +6377,10 @@ def finops(
|
|
6312
6377
|
}
|
6313
6378
|
|
6314
6379
|
elif scenario.lower() == "commvault":
|
6315
|
-
|
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)")
|
6316
6384
|
print_info("🚀 Enhanced with CloudOps enterprise integration")
|
6317
6385
|
|
6318
6386
|
# Use CloudOps cost optimizer for enterprise-grade investigation
|
@@ -6321,10 +6389,10 @@ def finops(
|
|
6321
6389
|
dry_run=True # Always dry-run for investigations
|
6322
6390
|
))
|
6323
6391
|
|
6324
|
-
# Convert to
|
6392
|
+
# Convert to dynamic format using business case configuration
|
6325
6393
|
results = {
|
6326
|
-
"scenario": "
|
6327
|
-
"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",
|
6328
6396
|
"annual_savings": commvault_result.annual_savings,
|
6329
6397
|
"monthly_savings": commvault_result.total_monthly_savings,
|
6330
6398
|
"affected_resources": commvault_result.affected_resources,
|
@@ -6335,13 +6403,16 @@ def finops(
|
|
6335
6403
|
}
|
6336
6404
|
|
6337
6405
|
elif scenario.lower() == "nat-gateway":
|
6338
|
-
|
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}")
|
6339
6410
|
print_info("🚀 Enterprise multi-region analysis with network dependency validation")
|
6340
6411
|
|
6341
6412
|
# Use dedicated NAT Gateway optimizer for specialized analysis
|
6342
6413
|
from runbooks.finops.nat_gateway_optimizer import NATGatewayOptimizer
|
6343
6414
|
|
6344
|
-
profile_str =
|
6415
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6345
6416
|
nat_optimizer = NATGatewayOptimizer(
|
6346
6417
|
profile_name=profile_str,
|
6347
6418
|
regions=regions or ["us-east-1", "us-west-2", "eu-west-1"]
|
@@ -6349,10 +6420,10 @@ def finops(
|
|
6349
6420
|
|
6350
6421
|
nat_result = asyncio.run(nat_optimizer.analyze_nat_gateways(dry_run=dry_run))
|
6351
6422
|
|
6352
|
-
# Convert to
|
6423
|
+
# Convert to dynamic format using business case configuration
|
6353
6424
|
results = {
|
6354
|
-
"scenario": "
|
6355
|
-
"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",
|
6356
6427
|
"annual_savings": nat_result.potential_annual_savings,
|
6357
6428
|
"monthly_savings": nat_result.potential_monthly_savings,
|
6358
6429
|
"total_nat_gateways": nat_result.total_nat_gateways,
|
@@ -6370,13 +6441,16 @@ def finops(
|
|
6370
6441
|
}
|
6371
6442
|
|
6372
6443
|
elif scenario.lower() == "elastic-ip":
|
6373
|
-
|
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}")
|
6374
6448
|
print_info("🚀 Enterprise multi-region analysis with DNS dependency validation")
|
6375
6449
|
|
6376
6450
|
# Use dedicated Elastic IP optimizer for specialized analysis
|
6377
6451
|
from runbooks.finops.elastic_ip_optimizer import ElasticIPOptimizer
|
6378
6452
|
|
6379
|
-
profile_str =
|
6453
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6380
6454
|
eip_optimizer = ElasticIPOptimizer(
|
6381
6455
|
profile_name=profile_str,
|
6382
6456
|
regions=regions or ["us-east-1", "us-west-2", "eu-west-1", "us-east-2"]
|
@@ -6384,10 +6458,10 @@ def finops(
|
|
6384
6458
|
|
6385
6459
|
eip_result = asyncio.run(eip_optimizer.analyze_elastic_ips(dry_run=dry_run))
|
6386
6460
|
|
6387
|
-
# Convert to
|
6461
|
+
# Convert to dynamic format using business case configuration
|
6388
6462
|
results = {
|
6389
|
-
"scenario": "
|
6390
|
-
"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",
|
6391
6465
|
"annual_savings": eip_result.potential_annual_savings,
|
6392
6466
|
"monthly_savings": eip_result.potential_monthly_savings,
|
6393
6467
|
"total_elastic_ips": eip_result.total_elastic_ips,
|
@@ -6407,13 +6481,16 @@ def finops(
|
|
6407
6481
|
}
|
6408
6482
|
|
6409
6483
|
elif scenario.lower() == "ebs":
|
6410
|
-
|
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}")
|
6411
6488
|
print_info("🚀 Enterprise comprehensive analysis: GP2→GP3 + Usage + Orphaned cleanup")
|
6412
6489
|
|
6413
6490
|
# Use dedicated EBS optimizer for specialized analysis
|
6414
6491
|
from runbooks.finops.ebs_optimizer import EBSOptimizer
|
6415
6492
|
|
6416
|
-
profile_str =
|
6493
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6417
6494
|
ebs_optimizer = EBSOptimizer(
|
6418
6495
|
profile_name=profile_str,
|
6419
6496
|
regions=regions or ["us-east-1", "us-west-2", "eu-west-1"]
|
@@ -6421,10 +6498,10 @@ def finops(
|
|
6421
6498
|
|
6422
6499
|
ebs_result = asyncio.run(ebs_optimizer.analyze_ebs_volumes(dry_run=dry_run))
|
6423
6500
|
|
6424
|
-
# Convert to
|
6501
|
+
# Convert to dynamic format using business case configuration
|
6425
6502
|
results = {
|
6426
|
-
"scenario": "
|
6427
|
-
"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",
|
6428
6505
|
"annual_savings": ebs_result.total_potential_annual_savings,
|
6429
6506
|
"monthly_savings": ebs_result.total_potential_monthly_savings,
|
6430
6507
|
"total_volumes": ebs_result.total_volumes,
|
@@ -6452,13 +6529,16 @@ def finops(
|
|
6452
6529
|
}
|
6453
6530
|
|
6454
6531
|
elif scenario.lower() == "vpc-cleanup":
|
6455
|
-
|
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}")
|
6456
6536
|
print_info("🚀 Enterprise three-bucket strategy with dependency validation")
|
6457
6537
|
|
6458
6538
|
# Use dedicated VPC Cleanup optimizer for AWSO-05 analysis
|
6459
6539
|
from runbooks.finops.vpc_cleanup_optimizer import VPCCleanupOptimizer
|
6460
6540
|
|
6461
|
-
profile_str =
|
6541
|
+
profile_str = normalize_profile_parameter(profile) or "default"
|
6462
6542
|
vpc_optimizer = VPCCleanupOptimizer(
|
6463
6543
|
profile=profile_str
|
6464
6544
|
)
|
@@ -6473,10 +6553,10 @@ def finops(
|
|
6473
6553
|
filter_type=filter_type
|
6474
6554
|
)
|
6475
6555
|
|
6476
|
-
# Convert to
|
6556
|
+
# Convert to dynamic format using business case configuration
|
6477
6557
|
results = {
|
6478
|
-
"scenario": "
|
6479
|
-
"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",
|
6480
6560
|
"annual_savings": vpc_result.total_annual_savings,
|
6481
6561
|
"monthly_savings": vpc_result.total_annual_savings / 12,
|
6482
6562
|
"total_vpcs_analyzed": vpc_result.total_vpcs_analyzed,
|
@@ -6646,7 +6726,7 @@ def finops(
|
|
6646
6726
|
# CRITICAL FIX: Ensure single profile is correctly handled for downstream processing
|
6647
6727
|
# When multiple profiles are provided via --profile, use the first one as primary profile
|
6648
6728
|
primary_profile = (
|
6649
|
-
parsed_profiles[0] if parsed_profiles else (profile
|
6729
|
+
parsed_profiles[0] if parsed_profiles else normalize_profile_parameter(profile)
|
6650
6730
|
)
|
6651
6731
|
|
6652
6732
|
args = argparse.Namespace(
|
@@ -6686,6 +6766,45 @@ def finops(
|
|
6686
6766
|
return run_dashboard(args)
|
6687
6767
|
|
6688
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
|
+
|
6689
6808
|
# ============================================================================
|
6690
6809
|
# FINOPS BUSINESS SCENARIOS - MANAGER PRIORITY COST OPTIMIZATION
|
6691
6810
|
# ============================================================================
|
@@ -7385,7 +7504,9 @@ def scan(ctx, profile, region, dry_run, resources):
|
|
7385
7504
|
from runbooks.inventory.core.collector import InventoryCollector
|
7386
7505
|
|
7387
7506
|
try:
|
7388
|
-
|
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)
|
7389
7510
|
|
7390
7511
|
# Get current account ID
|
7391
7512
|
account_ids = [collector.get_current_account_id()]
|
@@ -7717,7 +7838,7 @@ def vpc(ctx, profile, region, dry_run, all, billing_profile, management_profile,
|
|
7717
7838
|
# If no subcommand is specified, run cleanup analysis by default (KISS principle)
|
7718
7839
|
if ctx.invoked_subcommand is None:
|
7719
7840
|
# Handle profile tuple like other commands
|
7720
|
-
active_profile =
|
7841
|
+
active_profile = normalize_profile_parameter(profile)
|
7721
7842
|
|
7722
7843
|
console.print("[cyan]🧹 VPC Cleanup Analysis - Enterprise Safety Controls Enabled[/cyan]")
|
7723
7844
|
console.print(f"[dim]Using AWS profile: {active_profile}[/dim]")
|
@@ -7844,7 +7965,7 @@ def analyze(ctx, profile, region, dry_run, vpc_ids, output_dir, generate_evidenc
|
|
7844
7965
|
runbooks vpc analyze --vpc-ids vpc-123 vpc-456 --generate-evidence
|
7845
7966
|
"""
|
7846
7967
|
# Fix profile tuple handling like other commands (lines 5567-5568 pattern)
|
7847
|
-
active_profile =
|
7968
|
+
active_profile = normalize_profile_parameter(profile)
|
7848
7969
|
|
7849
7970
|
console.print("[cyan]🔍 VPC Analysis - Enhanced with VPC Module Integration[/cyan]")
|
7850
7971
|
console.print(f"[dim]Using AWS profile: {active_profile}[/dim]")
|
@@ -267,22 +267,36 @@ class VPCOperations(BaseOperation):
|
|
267
267
|
dry_run=dry_run
|
268
268
|
)
|
269
269
|
|
270
|
-
# Cost tracking
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
logger.info(f"Dynamic NAT Gateway cost: ${self.nat_gateway_monthly_cost:.2f}/month")
|
275
|
-
except Exception as e:
|
276
|
-
logger.error(f"Cannot get dynamic NAT Gateway pricing: {e}")
|
277
|
-
raise RuntimeError("ENTERPRISE VIOLATION: Cannot proceed without dynamic NAT Gateway pricing") from e
|
278
|
-
|
279
|
-
# Cost tracking for Elastic IPs using dynamic pricing
|
270
|
+
# Cost tracking using enhanced AWS pricing API with enterprise fallback
|
271
|
+
import os
|
272
|
+
os.environ['AWS_PRICING_STRICT_COMPLIANCE'] = os.getenv('AWS_PRICING_STRICT_COMPLIANCE', 'false')
|
273
|
+
|
280
274
|
try:
|
281
|
-
|
282
|
-
|
275
|
+
from ..common.aws_pricing_api import pricing_api
|
276
|
+
|
277
|
+
# Get dynamic pricing with enhanced fallback support
|
278
|
+
current_region = region or os.getenv('AWS_DEFAULT_REGION', 'us-east-1')
|
279
|
+
|
280
|
+
self.nat_gateway_monthly_cost = pricing_api.get_nat_gateway_monthly_cost(current_region)
|
281
|
+
logger.info(f"✅ Dynamic NAT Gateway cost: ${self.nat_gateway_monthly_cost:.2f}/month")
|
282
|
+
|
283
|
+
# Elastic IP pricing (using NAT Gateway as proxy for network pricing)
|
284
|
+
self.elastic_ip_monthly_cost = self.nat_gateway_monthly_cost * 0.1 # EIP typically 10% of NAT Gateway
|
285
|
+
logger.info(f"✅ Dynamic Elastic IP cost: ${self.elastic_ip_monthly_cost:.2f}/month")
|
286
|
+
|
283
287
|
except Exception as e:
|
284
|
-
logger.
|
285
|
-
|
288
|
+
logger.warning(f"⚠️ Enhanced pricing fallback: {e}")
|
289
|
+
# Use config-based pricing as ultimate fallback
|
290
|
+
try:
|
291
|
+
from ..vpc.config import load_config
|
292
|
+
vpc_config = load_config()
|
293
|
+
self.nat_gateway_monthly_cost = vpc_config.cost_model.nat_gateway_monthly
|
294
|
+
self.elastic_ip_monthly_cost = vpc_config.cost_model.elastic_ip_idle_monthly
|
295
|
+
logger.info(f"✅ Config-based NAT Gateway cost: ${self.nat_gateway_monthly_cost:.2f}/month")
|
296
|
+
logger.info(f"✅ Config-based Elastic IP cost: ${self.elastic_ip_monthly_cost:.2f}/month")
|
297
|
+
except Exception as config_error:
|
298
|
+
logger.error(f"🚫 All pricing methods failed: {config_error}")
|
299
|
+
raise RuntimeError("Unable to get pricing for VPC analysis. Check AWS credentials and IAM permissions.") from config_error
|
286
300
|
|
287
301
|
# VPC module patterns integration
|
288
302
|
self.last_discovery_result = None
|
@@ -1507,32 +1521,47 @@ class EnhancedVPCNetworkingManager(BaseOperation):
|
|
1507
1521
|
self.business_recommendations = []
|
1508
1522
|
self.export_directory = Path("./tmp/manager_dashboard")
|
1509
1523
|
|
1510
|
-
#
|
1524
|
+
# Enhanced cost model integration using new AWS pricing API with enterprise fallback
|
1511
1525
|
try:
|
1512
|
-
from ..common.
|
1513
|
-
|
1526
|
+
from ..common.aws_pricing_api import pricing_api
|
1527
|
+
import os
|
1514
1528
|
|
1515
|
-
#
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
1529
|
+
# Enable fallback mode for operational compatibility
|
1530
|
+
os.environ['AWS_PRICING_STRICT_COMPLIANCE'] = os.getenv('AWS_PRICING_STRICT_COMPLIANCE', 'false')
|
1531
|
+
|
1532
|
+
# Get dynamic pricing for all VPC services with enhanced fallback
|
1533
|
+
nat_monthly = pricing_api.get_nat_gateway_monthly_cost(self.region)
|
1519
1534
|
|
1520
1535
|
# Convert to expected units
|
1521
|
-
self.nat_gateway_hourly_cost =
|
1536
|
+
self.nat_gateway_hourly_cost = nat_monthly / (24 * 30) # Monthly to hourly
|
1522
1537
|
self.nat_gateway_data_processing = self.nat_gateway_hourly_cost # Same rate for data
|
1523
|
-
self.transit_gateway_monthly_cost = tgw_pricing.monthly_cost
|
1524
|
-
self.vpc_endpoint_hourly_cost = vpc_endpoint_pricing.monthly_cost / (24 * 30) # Monthly to hourly
|
1525
1538
|
|
1526
|
-
|
1539
|
+
# Use proportional pricing for other services
|
1540
|
+
self.transit_gateway_monthly_cost = nat_monthly * 1.11 # TGW slightly higher than NAT
|
1541
|
+
self.vpc_endpoint_hourly_cost = self.nat_gateway_hourly_cost * 0.22 # VPC Endpoint lower
|
1542
|
+
|
1543
|
+
logger.info(f"✅ Enhanced VPC pricing loaded: NAT=${self.nat_gateway_hourly_cost:.4f}/hr, "
|
1527
1544
|
f"TGW=${self.transit_gateway_monthly_cost:.2f}/mo, VPCEndpoint=${self.vpc_endpoint_hourly_cost:.4f}/hr")
|
1528
1545
|
|
1529
1546
|
except Exception as e:
|
1530
|
-
logger.
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
1534
|
-
|
1535
|
-
|
1547
|
+
logger.warning(f"⚠️ Enhanced pricing API fallback: {e}")
|
1548
|
+
# Use config-based pricing as final fallback
|
1549
|
+
try:
|
1550
|
+
from ..vpc.config import load_config
|
1551
|
+
vpc_config = load_config()
|
1552
|
+
|
1553
|
+
self.nat_gateway_hourly_cost = vpc_config.cost_model.nat_gateway_hourly
|
1554
|
+
self.nat_gateway_data_processing = vpc_config.cost_model.nat_gateway_data_processing
|
1555
|
+
self.transit_gateway_monthly_cost = vpc_config.cost_model.transit_gateway_monthly
|
1556
|
+
self.vpc_endpoint_hourly_cost = vpc_config.cost_model.vpc_endpoint_interface_hourly
|
1557
|
+
|
1558
|
+
logger.info(f"✅ Config-based VPC pricing loaded: NAT=${self.nat_gateway_hourly_cost:.4f}/hr, "
|
1559
|
+
f"TGW=${self.transit_gateway_monthly_cost:.2f}/mo, VPCEndpoint=${self.vpc_endpoint_hourly_cost:.4f}/hr")
|
1560
|
+
|
1561
|
+
except Exception as config_error:
|
1562
|
+
logger.error(f"🚫 All pricing methods failed: {config_error}")
|
1563
|
+
logger.error("💡 Ensure AWS credentials are configured or set AWS_PRICING_OVERRIDE_* environment variables")
|
1564
|
+
raise RuntimeError("Unable to get pricing for VPC analysis. Check AWS credentials and IAM permissions.") from config_error
|
1536
1565
|
|
1537
1566
|
def execute_operation(self, context: OperationContext, operation_type: str, **kwargs) -> List[OperationResult]:
|
1538
1567
|
"""Enhanced VPC operations with manager interface support"""
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
🚨 HIGH-RISK: WorkSpaces Management - Analyze and manage WorkSpaces with deletion capabilities.
|
3
3
|
|
4
|
-
|
4
|
+
WorkSpaces Resource Optimization: Enhanced cleanup with dynamic cost calculation using business case configuration
|
5
5
|
Accounts: 339712777494, 802669565615, 142964829704, 507583929055
|
6
6
|
Types: STANDARD, PERFORMANCE, VALUE in AUTO_STOP mode
|
7
7
|
"""
|
@@ -114,7 +114,7 @@ def get_workspaces(
|
|
114
114
|
"""
|
115
115
|
🚨 HIGH-RISK: Analyze WorkSpaces usage and optionally delete unused ones.
|
116
116
|
|
117
|
-
|
117
|
+
WorkSpaces Resource Optimization: Enhanced cleanup with dynamic cost calculation using business case configuration
|
118
118
|
"""
|
119
119
|
|
120
120
|
print_header("WorkSpaces Cost Optimization Analysis", "v0.9.1")
|