runbooks 1.1.3__py3-none-any.whl → 1.1.5__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 +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
- runbooks/cfat/assessment/compliance.py +8 -8
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cfat/models.py +6 -2
- runbooks/cfat/tests/__init__.py +6 -1
- runbooks/cli/__init__.py +13 -0
- runbooks/cli/commands/cfat.py +274 -0
- runbooks/cli/commands/finops.py +1164 -0
- runbooks/cli/commands/inventory.py +379 -0
- runbooks/cli/commands/operate.py +239 -0
- runbooks/cli/commands/security.py +248 -0
- runbooks/cli/commands/validation.py +825 -0
- runbooks/cli/commands/vpc.py +310 -0
- runbooks/cli/registry.py +107 -0
- runbooks/cloudops/__init__.py +23 -30
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +549 -547
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +226 -227
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +179 -215
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +341 -0
- runbooks/common/aws_utils.py +75 -80
- runbooks/common/business_logic.py +127 -105
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
- runbooks/common/cross_account_manager.py +198 -205
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +235 -0
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +478 -495
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +176 -194
- runbooks/common/patterns.py +204 -0
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +248 -39
- runbooks/common/rich_utils.py +643 -92
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +29 -33
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +488 -622
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +40 -37
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +230 -292
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +338 -175
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1513 -482
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +25 -29
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +77 -78
- runbooks/finops/scenarios.py +1278 -439
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/tests/test_finops_dashboard.py +3 -3
- runbooks/finops/tests/test_reference_images_validation.py +2 -2
- runbooks/finops/tests/test_single_account_features.py +17 -17
- runbooks/finops/tests/validate_test_suite.py +1 -1
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +263 -269
- runbooks/finops/vpc_cleanup_exporter.py +191 -146
- runbooks/finops/vpc_cleanup_optimizer.py +593 -575
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/hitl/enhanced_workflow_engine.py +1 -1
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/README.md +3 -3
- runbooks/inventory/Tests/common_test_data.py +30 -30
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +28 -11
- runbooks/inventory/collectors/aws_networking.py +111 -101
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/discovery.md +2 -2
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/find_ec2_security_groups.py +1 -1
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +56 -52
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +733 -696
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +3 -3
- runbooks/main.py +152 -9147
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/metrics/dora_metrics_engine.py +2 -2
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/mcp_integration.py +1 -1
- runbooks/operate/networking_cost_heatmap.py +33 -10
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/operate/vpc_operations.py +648 -618
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +71 -67
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +91 -65
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +49 -44
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/integration_test_enterprise_security.py +5 -3
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/run_script.py +1 -1
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/mcp_reliability_engine.py +6 -6
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +51 -48
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +754 -708
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +190 -162
- runbooks/vpc/mcp_no_eni_validator.py +681 -640
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1302 -1129
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -956
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.3.dist-info/METADATA +0 -799
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
runbooks/remediation/base.py
CHANGED
@@ -392,7 +392,7 @@ class BaseRemediation(ABC):
|
|
392
392
|
if self._session is None:
|
393
393
|
try:
|
394
394
|
# Use management profile for remediation operations requiring cross-account access
|
395
|
-
self._session = create_management_session(
|
395
|
+
self._session = create_management_session(profile_name=self.profile)
|
396
396
|
except Exception as e:
|
397
397
|
logger.warning(f"Failed to create session with profile {self.profile}: {e}")
|
398
398
|
self._session = create_management_session() # Use default profile
|
runbooks/remediation/commons.py
CHANGED
@@ -154,19 +154,20 @@ def get_method_details(client, rest_api_id, resource_id, http_method):
|
|
154
154
|
# Global profile variable for cost optimization commands
|
155
155
|
_profile = None
|
156
156
|
|
157
|
+
|
157
158
|
def get_client(client_name: str, profile_name: str = None, region_name: str = None):
|
158
159
|
"""
|
159
160
|
Enhanced client creation with profile support for cost optimization commands.
|
160
|
-
|
161
|
+
|
161
162
|
Enterprise pattern: Uses profile-based sessions like other runbooks modules.
|
162
163
|
Supports both environment variables and profile-based authentication.
|
163
164
|
"""
|
164
165
|
# Determine the profile to use (priority order)
|
165
166
|
profile_to_use = profile_name or _profile or os.environ.get("AWS_PROFILE")
|
166
|
-
|
167
|
+
|
167
168
|
# Determine the region to use
|
168
169
|
region_to_use = region_name or os.environ.get("AWS_REGION", "us-east-1")
|
169
|
-
|
170
|
+
|
170
171
|
if profile_to_use:
|
171
172
|
# Use profile-based session (enterprise pattern)
|
172
173
|
session = boto3.Session(profile_name=profile_to_use)
|
@@ -185,16 +186,16 @@ def get_client(client_name: str, profile_name: str = None, region_name: str = No
|
|
185
186
|
def get_resource(client_name: str, profile_name: str = None, region_name: str = None):
|
186
187
|
"""
|
187
188
|
Enhanced resource creation with profile support for cost optimization commands.
|
188
|
-
|
189
|
+
|
189
190
|
Enterprise pattern: Uses profile-based sessions like other runbooks modules.
|
190
191
|
Supports both environment variables and profile-based authentication.
|
191
192
|
"""
|
192
193
|
# Determine the profile to use (priority order)
|
193
194
|
profile_to_use = profile_name or _profile or os.environ.get("AWS_PROFILE")
|
194
|
-
|
195
|
+
|
195
196
|
# Determine the region to use
|
196
197
|
region_to_use = region_name or os.environ.get("AWS_REGION", "us-east-1")
|
197
|
-
|
198
|
+
|
198
199
|
if profile_to_use:
|
199
200
|
# Use profile-based session (enterprise pattern)
|
200
201
|
session = boto3.Session(profile_name=profile_to_use)
|
@@ -372,7 +373,9 @@ def get_price(service_code, region_name, instance_type):
|
|
372
373
|
@LRU_cache(maxsize=32)
|
373
374
|
def get_product_pricing(instance_type, region_name, service_code):
|
374
375
|
# Pricing API available only in selected regions
|
375
|
-
pricing = botocore.session.get_session().create_client(
|
376
|
+
pricing = botocore.session.get_session().create_client(
|
377
|
+
"pricing", region_name=os.getenv("AWS_DEFAULT_REGION", "us-east-1")
|
378
|
+
)
|
376
379
|
response = pricing.get_products(
|
377
380
|
ServiceCode=service_code,
|
378
381
|
Filters=[
|
@@ -15,8 +15,14 @@ from botocore.exceptions import ClientError
|
|
15
15
|
|
16
16
|
from .commons import display_aws_account_info, get_client, write_to_csv
|
17
17
|
from ..common.rich_utils import (
|
18
|
-
console,
|
19
|
-
|
18
|
+
console,
|
19
|
+
print_header,
|
20
|
+
print_success,
|
21
|
+
print_error,
|
22
|
+
print_warning,
|
23
|
+
create_table,
|
24
|
+
create_progress_bar,
|
25
|
+
format_cost,
|
20
26
|
)
|
21
27
|
from ..common.profile_utils import create_operational_session
|
22
28
|
|
@@ -26,41 +32,41 @@ logger = logging.getLogger(__name__)
|
|
26
32
|
def _calculate_instance_monthly_cost(instance_type: str, region: str = "us-east-1") -> float:
|
27
33
|
"""
|
28
34
|
Calculate monthly cost for EC2 instance type using dynamic AWS Pricing API.
|
29
|
-
|
35
|
+
|
30
36
|
ENTERPRISE COMPLIANCE: Uses AWS Pricing API to eliminate hardcoded pricing violations.
|
31
|
-
|
37
|
+
|
32
38
|
Args:
|
33
39
|
instance_type: EC2 instance type (e.g., 't3.micro', 'm5.large')
|
34
40
|
region: AWS region for pricing lookup
|
35
|
-
|
41
|
+
|
36
42
|
Returns:
|
37
43
|
float: Monthly cost in USD from AWS Pricing API
|
38
44
|
"""
|
39
45
|
from ..common.aws_pricing import get_ec2_monthly_cost
|
40
46
|
from ..common.rich_utils import console
|
41
|
-
|
47
|
+
|
42
48
|
try:
|
43
49
|
# Use dynamic AWS pricing - NO hardcoded values allowed
|
44
50
|
monthly_cost = get_ec2_monthly_cost(instance_type, region)
|
45
51
|
logger.debug(f"Dynamic pricing for {instance_type}: ${monthly_cost:.2f}/month")
|
46
52
|
return monthly_cost
|
47
|
-
|
53
|
+
|
48
54
|
except Exception as e:
|
49
55
|
console.print(f"[red]⚠ ENTERPRISE WARNING: Cannot get dynamic pricing for {instance_type}: {e}[/red]")
|
50
56
|
console.print(f"[yellow]Falling back to AWS pricing pattern calculation...[/yellow]")
|
51
|
-
|
57
|
+
|
52
58
|
# Use AWS pricing engine's fallback calculation (which uses documented AWS patterns)
|
53
59
|
from ..common.aws_pricing import get_aws_pricing_engine
|
54
|
-
|
60
|
+
|
55
61
|
try:
|
56
62
|
pricing_engine = get_aws_pricing_engine(enable_fallback=True)
|
57
63
|
result = pricing_engine.get_ec2_instance_pricing(instance_type, region)
|
58
64
|
logger.warning(f"Using fallback pricing for {instance_type}: ${result.monthly_cost:.2f}/month")
|
59
65
|
return result.monthly_cost
|
60
|
-
|
66
|
+
|
61
67
|
except Exception as fallback_error:
|
62
68
|
logger.error(f"All pricing methods failed for {instance_type}: {fallback_error}")
|
63
|
-
|
69
|
+
|
64
70
|
# Complete failure - cannot proceed without violating enterprise standards
|
65
71
|
raise RuntimeError(
|
66
72
|
f"ENTERPRISE VIOLATION: Cannot get dynamic pricing for {instance_type} "
|
@@ -70,27 +76,25 @@ def _calculate_instance_monthly_cost(instance_type: str, region: str = "us-east-
|
|
70
76
|
) from e
|
71
77
|
|
72
78
|
|
73
|
-
|
74
|
-
|
75
79
|
def calculate_ec2_cost_impact(instances_data: List[Dict]) -> Dict[str, float]:
|
76
80
|
"""
|
77
81
|
Calculate potential cost impact for EC2 instances.
|
78
|
-
|
82
|
+
|
79
83
|
JIRA FinOps-25: Focuses on Commvault backup account utilization analysis
|
80
84
|
"""
|
81
85
|
total_instances = len(instances_data)
|
82
86
|
running_instances = [i for i in instances_data if i.get("State", {}).get("Name") == "running"]
|
83
87
|
idle_instances = [i for i in instances_data if i.get("CpuUtilization", 0) < 5.0] # <5% CPU
|
84
|
-
|
88
|
+
|
85
89
|
# Dynamic cost estimation using environment configuration
|
86
90
|
estimated_monthly_cost = 0.0
|
87
91
|
for instance in running_instances:
|
88
92
|
instance_type = instance.get("InstanceType", "t3.micro")
|
89
|
-
|
93
|
+
|
90
94
|
# Dynamic cost calculation based on instance type
|
91
95
|
# Using AWS pricing calculation: hours * daily_hours * monthly_days
|
92
96
|
estimated_monthly_cost += _calculate_instance_monthly_cost(instance_type)
|
93
|
-
|
97
|
+
|
94
98
|
return {
|
95
99
|
"total_instances": total_instances,
|
96
100
|
"running_instances": len(running_instances),
|
@@ -103,30 +107,32 @@ def calculate_ec2_cost_impact(instances_data: List[Dict]) -> Dict[str, float]:
|
|
103
107
|
|
104
108
|
@click.command()
|
105
109
|
@click.option("--output-file", default="./tmp/commvault_ec2_investigation.csv", help="Output CSV file path")
|
106
|
-
@click.option(
|
110
|
+
@click.option(
|
111
|
+
"--account", help="Commvault backup account ID (JIRA FinOps-25). If not specified, uses current AWS account."
|
112
|
+
)
|
107
113
|
@click.option("--investigate-utilization", is_flag=True, help="Investigate EC2 utilization patterns")
|
108
114
|
@click.option("--days", default=7, help="Number of days to analyze for utilization metrics")
|
109
115
|
@click.option("--dry-run", is_flag=True, default=True, help="Preview analysis without execution")
|
110
116
|
def investigate_commvault_ec2(output_file, account, investigate_utilization, days, dry_run):
|
111
117
|
"""
|
112
118
|
FinOps-25: Commvault EC2 investigation for cost optimization.
|
113
|
-
|
119
|
+
|
114
120
|
Challenge: Determine if EC2 instances are actively used for backups or idle
|
115
121
|
"""
|
116
|
-
print_header("JIRA FinOps-25: Commvault EC2 Investigation", "
|
117
|
-
|
122
|
+
print_header("JIRA FinOps-25: Commvault EC2 Investigation", "latest version")
|
123
|
+
|
118
124
|
# Auto-detect account if not specified
|
119
125
|
if not account:
|
120
126
|
try:
|
121
127
|
session = create_operational_session()
|
122
|
-
sts = session.client(
|
128
|
+
sts = session.client("sts")
|
123
129
|
identity = sts.get_caller_identity()
|
124
|
-
account = identity[
|
130
|
+
account = identity["Account"]
|
125
131
|
console.print(f"[dim cyan]Auto-detected account: {account}[/dim cyan]")
|
126
132
|
except Exception as e:
|
127
133
|
console.print(f"[red]Error detecting account: {e}[/red]")
|
128
134
|
raise click.ClickException("Could not determine AWS account. Please specify --account parameter.")
|
129
|
-
|
135
|
+
|
130
136
|
account_info = display_aws_account_info()
|
131
137
|
console.print(f"[cyan]Account: {account} - Investigating EC2 usage patterns[/cyan]")
|
132
138
|
|
@@ -137,10 +143,10 @@ def investigate_commvault_ec2(output_file, account, investigate_utilization, day
|
|
137
143
|
# Get all EC2 instances
|
138
144
|
console.print("[yellow]Collecting EC2 instance data...[/yellow]")
|
139
145
|
response = ec2_client.describe_instances()
|
140
|
-
|
146
|
+
|
141
147
|
instances_data = []
|
142
148
|
all_instances = []
|
143
|
-
|
149
|
+
|
144
150
|
# Flatten instance data
|
145
151
|
for reservation in response.get("Reservations", []):
|
146
152
|
all_instances.extend(reservation.get("Instances", []))
|
@@ -157,8 +163,7 @@ def investigate_commvault_ec2(output_file, account, investigate_utilization, day
|
|
157
163
|
|
158
164
|
with create_progress_bar() as progress:
|
159
165
|
task_id = progress.add_task(
|
160
|
-
f"Investigating {len(all_instances)} EC2 instances...",
|
161
|
-
total=len(all_instances)
|
166
|
+
f"Investigating {len(all_instances)} EC2 instances...", total=len(all_instances)
|
162
167
|
)
|
163
168
|
|
164
169
|
for instance in all_instances:
|
@@ -166,7 +171,7 @@ def investigate_commvault_ec2(output_file, account, investigate_utilization, day
|
|
166
171
|
instance_type = instance.get("InstanceType", "unknown")
|
167
172
|
state = instance.get("State", {}).get("Name", "unknown")
|
168
173
|
launch_time = instance.get("LaunchTime")
|
169
|
-
|
174
|
+
|
170
175
|
# Get CPU utilization metrics
|
171
176
|
cpu_utilization = 0.0
|
172
177
|
if investigate_utilization and state == "running":
|
@@ -180,18 +185,18 @@ def investigate_commvault_ec2(output_file, account, investigate_utilization, day
|
|
180
185
|
Period=3600, # 1 hour intervals
|
181
186
|
Statistics=["Average"],
|
182
187
|
)
|
183
|
-
|
188
|
+
|
184
189
|
if cpu_response.get("Datapoints"):
|
185
|
-
cpu_utilization = sum(
|
186
|
-
|
187
|
-
)
|
188
|
-
|
190
|
+
cpu_utilization = sum(dp["Average"] for dp in cpu_response["Datapoints"]) / len(
|
191
|
+
cpu_response["Datapoints"]
|
192
|
+
)
|
193
|
+
|
189
194
|
except ClientError as e:
|
190
195
|
logger.warning(f"Could not get CPU metrics for {instance_id}: {e}")
|
191
196
|
|
192
197
|
# Get tags for context
|
193
198
|
tags = {tag["Key"]: tag["Value"] for tag in instance.get("Tags", [])}
|
194
|
-
|
199
|
+
|
195
200
|
instance_data = {
|
196
201
|
"InstanceId": instance_id,
|
197
202
|
"InstanceType": instance_type,
|
@@ -203,7 +208,7 @@ def investigate_commvault_ec2(output_file, account, investigate_utilization, day
|
|
203
208
|
"Purpose": tags.get("Purpose", tags.get("Application", "Unknown")),
|
204
209
|
"IsLowUtilization": cpu_utilization < 5.0 if investigate_utilization else False,
|
205
210
|
}
|
206
|
-
|
211
|
+
|
207
212
|
instances_data.append(instance_data)
|
208
213
|
progress.advance(task_id)
|
209
214
|
|
@@ -213,47 +218,44 @@ def investigate_commvault_ec2(output_file, account, investigate_utilization, day
|
|
213
218
|
|
214
219
|
# Calculate cost impact analysis
|
215
220
|
cost_analysis = calculate_ec2_cost_impact(instances_data)
|
216
|
-
|
221
|
+
|
217
222
|
# Create summary table
|
218
223
|
print_header("Commvault EC2 Investigation Summary")
|
219
|
-
|
224
|
+
|
220
225
|
summary_table = create_table(
|
221
226
|
title="EC2 Cost Impact Analysis - JIRA FinOps-25",
|
222
227
|
columns=[
|
223
228
|
{"header": "Metric", "style": "cyan"},
|
224
229
|
{"header": "Count", "style": "green bold"},
|
225
230
|
{"header": "Monthly Cost", "style": "red"},
|
226
|
-
{"header": "Annual Cost", "style": "red bold"}
|
227
|
-
]
|
231
|
+
{"header": "Annual Cost", "style": "red bold"},
|
232
|
+
],
|
228
233
|
)
|
229
234
|
|
230
235
|
summary_table.add_row(
|
231
|
-
"Total EC2 Instances",
|
232
|
-
str(cost_analysis["total_instances"]),
|
233
|
-
"Mixed Types",
|
234
|
-
"Mixed Types"
|
236
|
+
"Total EC2 Instances", str(cost_analysis["total_instances"]), "Mixed Types", "Mixed Types"
|
235
237
|
)
|
236
|
-
|
238
|
+
|
237
239
|
summary_table.add_row(
|
238
240
|
"Running Instances",
|
239
241
|
str(cost_analysis["running_instances"]),
|
240
242
|
format_cost(cost_analysis["estimated_monthly_cost"]),
|
241
|
-
format_cost(cost_analysis["estimated_annual_cost"])
|
243
|
+
format_cost(cost_analysis["estimated_annual_cost"]),
|
242
244
|
)
|
243
|
-
|
245
|
+
|
244
246
|
if investigate_utilization:
|
245
247
|
summary_table.add_row(
|
246
248
|
"Low Utilization (<5% CPU)",
|
247
249
|
str(cost_analysis["idle_instances"]),
|
248
250
|
"Investigation Required",
|
249
|
-
"Investigation Required"
|
251
|
+
"Investigation Required",
|
250
252
|
)
|
251
|
-
|
253
|
+
|
252
254
|
summary_table.add_row(
|
253
255
|
"🎯 Potential Savings (if idle)",
|
254
256
|
f"{cost_analysis['idle_instances']} instances",
|
255
257
|
format_cost(cost_analysis["potential_savings_if_idle"] / 12),
|
256
|
-
format_cost(cost_analysis["potential_savings_if_idle"])
|
258
|
+
format_cost(cost_analysis["potential_savings_if_idle"]),
|
257
259
|
)
|
258
260
|
|
259
261
|
console.print(summary_table)
|
@@ -261,10 +263,10 @@ def investigate_commvault_ec2(output_file, account, investigate_utilization, day
|
|
261
263
|
# Investigation recommendations
|
262
264
|
if investigate_utilization:
|
263
265
|
low_util_instances = [i for i in instances_data if i["IsLowUtilization"]]
|
264
|
-
|
266
|
+
|
265
267
|
if low_util_instances:
|
266
268
|
print_warning(f"🔍 Found {len(low_util_instances)} instances with low utilization:")
|
267
|
-
|
269
|
+
|
268
270
|
# Create detailed investigation table
|
269
271
|
investigation_table = create_table(
|
270
272
|
title="Low Utilization Instances (Investigation Required)",
|
@@ -274,33 +276,35 @@ def investigate_commvault_ec2(output_file, account, investigate_utilization, day
|
|
274
276
|
{"header": "CPU %", "style": "yellow"},
|
275
277
|
{"header": "Name", "style": "green"},
|
276
278
|
{"header": "Purpose", "style": "magenta"},
|
277
|
-
{"header": "State", "style": "red"}
|
278
|
-
]
|
279
|
+
{"header": "State", "style": "red"},
|
280
|
+
],
|
279
281
|
)
|
280
|
-
|
282
|
+
|
281
283
|
for instance in low_util_instances[:10]: # Show first 10
|
282
284
|
investigation_table.add_row(
|
283
|
-
instance[
|
284
|
-
instance[
|
285
|
+
instance["InstanceId"],
|
286
|
+
instance["InstanceType"],
|
285
287
|
f"{instance['CpuUtilization']:.1f}%",
|
286
|
-
instance[
|
287
|
-
instance[
|
288
|
-
instance[
|
288
|
+
instance["Name"],
|
289
|
+
instance["Purpose"],
|
290
|
+
instance["State"],
|
289
291
|
)
|
290
|
-
|
292
|
+
|
291
293
|
console.print(investigation_table)
|
292
|
-
|
294
|
+
|
293
295
|
if len(low_util_instances) > 10:
|
294
|
-
console.print(
|
295
|
-
|
296
|
+
console.print(
|
297
|
+
f"[dim]... and {len(low_util_instances) - 10} more instances requiring investigation[/dim]"
|
298
|
+
)
|
299
|
+
|
296
300
|
# Manual verification checklist
|
297
301
|
print_header("Manual Verification Checklist")
|
298
302
|
console.print("[yellow]For each low-utilization instance, verify:[/yellow]")
|
299
303
|
console.print("[dim]1. Is this instance actively used for Commvault backup operations?[/dim]")
|
300
|
-
console.print("[dim]2. Are there scheduled backup jobs running on this instance?[/dim]")
|
304
|
+
console.print("[dim]2. Are there scheduled backup jobs running on this instance?[/dim]")
|
301
305
|
console.print("[dim]3. Can this instance be right-sized or terminated safely?[/dim]")
|
302
306
|
console.print("[dim]4. Are there any dependencies or integrations that require this instance?[/dim]")
|
303
|
-
|
307
|
+
|
304
308
|
else:
|
305
309
|
print_success("✓ No low-utilization instances detected")
|
306
310
|
|
@@ -310,4 +314,4 @@ def investigate_commvault_ec2(output_file, account, investigate_utilization, day
|
|
310
314
|
|
311
315
|
except Exception as e:
|
312
316
|
logger.error(f"Failed to investigate Commvault EC2: {e}")
|
313
|
-
raise
|
317
|
+
raise
|
@@ -307,6 +307,7 @@ def detect_and_delete_volumes(dry_run: bool, max_age_days: int, output_file: Opt
|
|
307
307
|
|
308
308
|
# Real-time EBS cost from AWS Pricing API - NO hardcoded defaults
|
309
309
|
from runbooks.common.aws_pricing_api import pricing_api
|
310
|
+
|
310
311
|
if volume_type == "gp3":
|
311
312
|
cost_per_gb = pricing_api.get_ebs_gp3_cost_per_gb(region_name)
|
312
313
|
else:
|
@@ -71,7 +71,10 @@ from loguru import logger
|
|
71
71
|
from runbooks.inventory.models.account import AWSAccount
|
72
72
|
from runbooks.remediation.base import BaseRemediation, RemediationContext, RemediationResult, RemediationStatus
|
73
73
|
from runbooks.common.profile_utils import create_management_session
|
74
|
-
from runbooks.remediation.universal_account_discovery import
|
74
|
+
from runbooks.remediation.universal_account_discovery import (
|
75
|
+
UniversalAccountDiscovery,
|
76
|
+
AWSAccount as UniversalAWSAccount,
|
77
|
+
)
|
75
78
|
|
76
79
|
|
77
80
|
class MultiAccountRemediator:
|
@@ -572,16 +575,16 @@ class MultiAccountRemediator:
|
|
572
575
|
def get_accounts_from_environment(profile: Optional[str] = None) -> Optional[List[AWSAccount]]:
|
573
576
|
"""
|
574
577
|
Get AWS accounts using universal account discovery system.
|
575
|
-
|
578
|
+
|
576
579
|
Uses enhanced discovery with support for:
|
577
580
|
- Environment variables (REMEDIATION_TARGET_ACCOUNTS)
|
578
581
|
- Configuration files (REMEDIATION_ACCOUNT_CONFIG)
|
579
582
|
- AWS Organizations API (automatic discovery)
|
580
583
|
- Current account fallback (single account mode)
|
581
|
-
|
584
|
+
|
582
585
|
Args:
|
583
586
|
profile: AWS profile to use for discovery
|
584
|
-
|
587
|
+
|
585
588
|
Returns:
|
586
589
|
List of AWSAccount objects or None if not configured
|
587
590
|
"""
|
@@ -589,22 +592,22 @@ def get_accounts_from_environment(profile: Optional[str] = None) -> Optional[Lis
|
|
589
592
|
# Use universal account discovery system
|
590
593
|
discovery = UniversalAccountDiscovery(profile=profile)
|
591
594
|
universal_accounts = discovery.discover_target_accounts()
|
592
|
-
|
595
|
+
|
593
596
|
if not universal_accounts:
|
594
597
|
return None
|
595
|
-
|
598
|
+
|
596
599
|
# Convert to legacy AWSAccount format for compatibility
|
597
600
|
legacy_accounts = []
|
598
601
|
for universal_account in universal_accounts:
|
599
602
|
legacy_account = AWSAccount(
|
600
603
|
universal_account.account_id,
|
601
|
-
universal_account.account_name or f"account-{universal_account.account_id}"
|
604
|
+
universal_account.account_name or f"account-{universal_account.account_id}",
|
602
605
|
)
|
603
606
|
legacy_accounts.append(legacy_account)
|
604
|
-
|
607
|
+
|
605
608
|
logger.info(f"Using {len(legacy_accounts)} accounts discovered via universal discovery system")
|
606
609
|
return legacy_accounts
|
607
|
-
|
610
|
+
|
608
611
|
except Exception as e:
|
609
612
|
logger.warning(f"Failed to discover accounts using universal discovery: {e}")
|
610
613
|
return None
|
@@ -613,16 +616,16 @@ def get_accounts_from_environment(profile: Optional[str] = None) -> Optional[Lis
|
|
613
616
|
def discover_organization_accounts(profile: Optional[str] = None) -> List[AWSAccount]:
|
614
617
|
"""
|
615
618
|
Discover AWS accounts using universal discovery system.
|
616
|
-
|
619
|
+
|
617
620
|
Enhanced to use the universal account discovery system which provides:
|
618
621
|
- Organizations API discovery (if available)
|
619
622
|
- Environment variable fallback
|
620
623
|
- Configuration file support
|
621
624
|
- Current account fallback
|
622
|
-
|
625
|
+
|
623
626
|
Args:
|
624
627
|
profile: AWS profile for discovery (universal profile management)
|
625
|
-
|
628
|
+
|
626
629
|
Returns:
|
627
630
|
List of discovered AWSAccount objects
|
628
631
|
"""
|
@@ -630,25 +633,25 @@ def discover_organization_accounts(profile: Optional[str] = None) -> List[AWSAcc
|
|
630
633
|
# Use universal account discovery system for Organizations discovery
|
631
634
|
discovery = UniversalAccountDiscovery(profile=profile)
|
632
635
|
universal_accounts = discovery._get_accounts_from_organizations()
|
633
|
-
|
636
|
+
|
634
637
|
if not universal_accounts:
|
635
638
|
# Fallback to other discovery methods
|
636
639
|
logger.info("Organizations API not available, trying other discovery methods...")
|
637
640
|
universal_accounts = discovery.discover_target_accounts()
|
638
|
-
|
641
|
+
|
639
642
|
# Convert to legacy AWSAccount format for compatibility
|
640
643
|
legacy_accounts = []
|
641
644
|
for universal_account in universal_accounts:
|
642
645
|
if universal_account.status == "ACTIVE":
|
643
646
|
legacy_account = AWSAccount(
|
644
647
|
universal_account.account_id,
|
645
|
-
universal_account.account_name or f"org-account-{universal_account.account_id}"
|
648
|
+
universal_account.account_name or f"org-account-{universal_account.account_id}",
|
646
649
|
)
|
647
650
|
legacy_accounts.append(legacy_account)
|
648
|
-
|
651
|
+
|
649
652
|
logger.info(f"Discovered {len(legacy_accounts)} active AWS accounts via universal discovery")
|
650
653
|
return legacy_accounts
|
651
|
-
|
654
|
+
|
652
655
|
except Exception as e:
|
653
656
|
logger.warning(f"Failed to discover organization accounts: {e}")
|
654
657
|
# Universal discovery handles all fallback scenarios
|
@@ -658,20 +661,20 @@ def discover_organization_accounts(profile: Optional[str] = None) -> List[AWSAcc
|
|
658
661
|
def _determine_account_environment(account_name: str) -> str:
|
659
662
|
"""
|
660
663
|
Determine account environment based on account name patterns.
|
661
|
-
|
664
|
+
|
662
665
|
Args:
|
663
666
|
account_name: AWS account name
|
664
|
-
|
667
|
+
|
665
668
|
Returns:
|
666
669
|
Environment classification
|
667
670
|
"""
|
668
671
|
name_lower = account_name.lower()
|
669
|
-
|
672
|
+
|
670
673
|
# Common environment patterns
|
671
674
|
if any(env in name_lower for env in ["prod", "production"]):
|
672
675
|
return "production"
|
673
676
|
elif any(env in name_lower for env in ["staging", "stage", "uat"]):
|
674
|
-
return "staging"
|
677
|
+
return "staging"
|
675
678
|
elif any(env in name_lower for env in ["dev", "development"]):
|
676
679
|
return "development"
|
677
680
|
elif any(env in name_lower for env in ["test", "testing"]):
|