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
@@ -14,22 +14,31 @@ Input: region, idle_cpu_threshold, idle_duration, instance_ids (optional)
|
|
14
14
|
Output: List of stopped instances and cost impact analysis
|
15
15
|
"""
|
16
16
|
|
17
|
-
from typing import Dict, List, Optional, Tuple, Any
|
18
17
|
import datetime
|
18
|
+
from dataclasses import dataclass
|
19
|
+
from typing import Any, Dict, List, Optional, Tuple
|
20
|
+
|
19
21
|
import boto3
|
20
22
|
from botocore.exceptions import ClientError
|
21
23
|
from pydantic import BaseModel, Field
|
22
|
-
from dataclasses import dataclass
|
23
24
|
|
25
|
+
from ..common.aws_pricing import DynamicAWSPricing
|
24
26
|
from ..common.rich_utils import (
|
25
|
-
console,
|
26
|
-
|
27
|
+
console,
|
28
|
+
create_progress_bar,
|
29
|
+
create_table,
|
30
|
+
format_cost,
|
31
|
+
print_error,
|
32
|
+
print_header,
|
33
|
+
print_success,
|
34
|
+
print_warning,
|
27
35
|
)
|
28
|
-
|
36
|
+
|
29
37
|
|
30
38
|
@dataclass
|
31
39
|
class IdleInstance:
|
32
40
|
"""Data class for idle EC2 instances"""
|
41
|
+
|
33
42
|
instance_id: str
|
34
43
|
region: str
|
35
44
|
instance_type: str = ""
|
@@ -37,9 +46,11 @@ class IdleInstance:
|
|
37
46
|
estimated_monthly_cost: float = 0.0
|
38
47
|
tags: Dict[str, str] = Field(default_factory=dict)
|
39
48
|
|
49
|
+
|
40
50
|
@dataclass
|
41
51
|
class LowUsageVolume:
|
42
52
|
"""Data class for low usage EBS volumes"""
|
53
|
+
|
43
54
|
volume_id: str
|
44
55
|
region: str
|
45
56
|
volume_type: str = ""
|
@@ -49,9 +60,11 @@ class LowUsageVolume:
|
|
49
60
|
creation_date: Optional[str] = None
|
50
61
|
tags: Dict[str, str] = Field(default_factory=dict)
|
51
62
|
|
63
|
+
|
52
64
|
@dataclass
|
53
65
|
class UnusedNATGateway:
|
54
66
|
"""Data class for unused NAT Gateways"""
|
67
|
+
|
55
68
|
nat_gateway_id: str
|
56
69
|
region: str
|
57
70
|
vpc_id: str = ""
|
@@ -60,70 +73,67 @@ class UnusedNATGateway:
|
|
60
73
|
creation_date: Optional[str] = None
|
61
74
|
tags: Dict[str, str] = Field(default_factory=dict)
|
62
75
|
|
76
|
+
|
63
77
|
@dataclass
|
64
78
|
class CostOptimizationResult:
|
65
79
|
"""Results from cost optimization operations"""
|
80
|
+
|
66
81
|
stopped_instances: List[IdleInstance] = Field(default_factory=list)
|
67
82
|
deleted_volumes: List[LowUsageVolume] = Field(default_factory=list)
|
68
83
|
deleted_nat_gateways: List[UnusedNATGateway] = Field(default_factory=list)
|
69
84
|
total_potential_savings: float = 0.0
|
70
85
|
annual_savings: float = 0.0 # Annual savings projection for business scenarios
|
71
86
|
execution_summary: Dict[str, Any] = Field(default_factory=dict)
|
72
|
-
|
87
|
+
|
88
|
+
|
73
89
|
class AWSCostOptimizer:
|
74
90
|
"""
|
75
91
|
Enterprise AWS Cost Optimization
|
76
92
|
Migrated and enhanced from unSkript notebooks
|
77
93
|
Handles EC2 instances, EBS volumes, and other cost optimization scenarios
|
78
94
|
"""
|
79
|
-
|
95
|
+
|
80
96
|
def __init__(self, profile: Optional[str] = None):
|
81
97
|
self.profile = profile
|
82
98
|
self.session = boto3.Session(profile_name=profile) if profile else boto3.Session()
|
83
|
-
|
99
|
+
|
84
100
|
def find_idle_instances(
|
85
|
-
self,
|
86
|
-
region: str = "",
|
87
|
-
idle_cpu_threshold: int = 5,
|
88
|
-
idle_duration: int = 6
|
101
|
+
self, region: str = "", idle_cpu_threshold: int = 5, idle_duration: int = 6
|
89
102
|
) -> Tuple[bool, Optional[List[IdleInstance]]]:
|
90
103
|
"""
|
91
104
|
Find idle EC2 instances based on CPU utilization
|
92
|
-
|
105
|
+
|
93
106
|
Migrated from: AWS_Stop_Idle_EC2_Instances.ipynb
|
94
|
-
|
107
|
+
|
95
108
|
Args:
|
96
109
|
region: AWS Region to scan (empty for all regions)
|
97
110
|
idle_cpu_threshold: CPU threshold percentage (default 5%)
|
98
111
|
idle_duration: Duration in hours to check (default 6h)
|
99
|
-
|
112
|
+
|
100
113
|
Returns:
|
101
114
|
Tuple (success, list_of_idle_instances)
|
102
115
|
"""
|
103
|
-
print_header("Cost Optimizer - Idle Instance Detection", "
|
104
|
-
|
116
|
+
print_header("Cost Optimizer - Idle Instance Detection", "latest version")
|
117
|
+
|
105
118
|
result = []
|
106
119
|
regions_to_check = [region] if region else self._get_all_regions()
|
107
|
-
|
120
|
+
|
108
121
|
with create_progress_bar() as progress:
|
109
122
|
task_id = progress.add_task(
|
110
|
-
f"Scanning {len(regions_to_check)} regions for idle instances...",
|
111
|
-
total=len(regions_to_check)
|
123
|
+
f"Scanning {len(regions_to_check)} regions for idle instances...", total=len(regions_to_check)
|
112
124
|
)
|
113
|
-
|
125
|
+
|
114
126
|
for reg in regions_to_check:
|
115
127
|
try:
|
116
|
-
idle_instances = self._scan_region_for_idle_instances(
|
117
|
-
reg, idle_cpu_threshold, idle_duration
|
118
|
-
)
|
128
|
+
idle_instances = self._scan_region_for_idle_instances(reg, idle_cpu_threshold, idle_duration)
|
119
129
|
result.extend(idle_instances)
|
120
130
|
progress.advance(task_id)
|
121
|
-
|
131
|
+
|
122
132
|
except Exception as e:
|
123
133
|
print_warning(f"Failed to scan region {reg}: {str(e)}")
|
124
134
|
progress.advance(task_id)
|
125
135
|
continue
|
126
|
-
|
136
|
+
|
127
137
|
if result:
|
128
138
|
print_success(f"Found {len(result)} idle instances across {len(regions_to_check)} regions")
|
129
139
|
self._display_idle_instances_table(result)
|
@@ -131,70 +141,59 @@ class AWSCostOptimizer:
|
|
131
141
|
else:
|
132
142
|
print_success("No idle instances found")
|
133
143
|
return (True, None) # True = no results (unSkript convention)
|
134
|
-
|
144
|
+
|
135
145
|
def _scan_region_for_idle_instances(
|
136
|
-
self,
|
137
|
-
region: str,
|
138
|
-
idle_cpu_threshold: int,
|
139
|
-
idle_duration: int
|
146
|
+
self, region: str, idle_cpu_threshold: int, idle_duration: int
|
140
147
|
) -> List[IdleInstance]:
|
141
148
|
"""Scan a specific region for idle instances"""
|
142
|
-
|
149
|
+
|
143
150
|
result = []
|
144
|
-
|
151
|
+
|
145
152
|
try:
|
146
|
-
ec2_client = self.session.client(
|
147
|
-
cloudwatch_client = self.session.client(
|
148
|
-
|
153
|
+
ec2_client = self.session.client("ec2", region_name=region)
|
154
|
+
cloudwatch_client = self.session.client("cloudwatch", region_name=region)
|
155
|
+
|
149
156
|
# Get all running instances
|
150
|
-
response = ec2_client.describe_instances(
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
instance_id
|
157
|
-
|
158
|
-
if self._is_instance_idle(
|
159
|
-
instance_id, idle_cpu_threshold, idle_duration, cloudwatch_client
|
160
|
-
):
|
157
|
+
response = ec2_client.describe_instances(Filters=[{"Name": "instance-state-name", "Values": ["running"]}])
|
158
|
+
|
159
|
+
for reservation in response["Reservations"]:
|
160
|
+
for instance in reservation["Instances"]:
|
161
|
+
instance_id = instance["InstanceId"]
|
162
|
+
|
163
|
+
if self._is_instance_idle(instance_id, idle_cpu_threshold, idle_duration, cloudwatch_client):
|
161
164
|
# Extract tags
|
162
|
-
tags = {tag[
|
163
|
-
|
165
|
+
tags = {tag["Key"]: tag["Value"] for tag in instance.get("Tags", [])}
|
166
|
+
|
164
167
|
idle_instance = IdleInstance(
|
165
168
|
instance_id=instance_id,
|
166
169
|
region=region,
|
167
|
-
instance_type=instance.get(
|
168
|
-
tags=tags
|
170
|
+
instance_type=instance.get("InstanceType", "unknown"),
|
171
|
+
tags=tags,
|
169
172
|
)
|
170
|
-
|
173
|
+
|
171
174
|
# Calculate estimated cost (simplified - real implementation would use pricing API)
|
172
175
|
idle_instance.estimated_monthly_cost = self._estimate_instance_monthly_cost(
|
173
|
-
instance.get(
|
176
|
+
instance.get("InstanceType", "t3.micro")
|
174
177
|
)
|
175
|
-
|
178
|
+
|
176
179
|
result.append(idle_instance)
|
177
|
-
|
180
|
+
|
178
181
|
except ClientError as e:
|
179
182
|
print_warning(f"AWS API error in region {region}: {e}")
|
180
183
|
except Exception as e:
|
181
184
|
print_error(f"Unexpected error in region {region}: {e}")
|
182
|
-
|
185
|
+
|
183
186
|
return result
|
184
|
-
|
187
|
+
|
185
188
|
def _is_instance_idle(
|
186
|
-
self,
|
187
|
-
instance_id: str,
|
188
|
-
idle_cpu_threshold: int,
|
189
|
-
idle_duration: int,
|
190
|
-
cloudwatch_client
|
189
|
+
self, instance_id: str, idle_cpu_threshold: int, idle_duration: int, cloudwatch_client
|
191
190
|
) -> bool:
|
192
191
|
"""Check if instance is idle based on CPU metrics"""
|
193
|
-
|
192
|
+
|
194
193
|
try:
|
195
194
|
now = datetime.datetime.utcnow()
|
196
195
|
start_time = now - datetime.timedelta(hours=idle_duration)
|
197
|
-
|
196
|
+
|
198
197
|
cpu_stats = cloudwatch_client.get_metric_statistics(
|
199
198
|
Namespace="AWS/EC2",
|
200
199
|
MetricName="CPUUtilization",
|
@@ -202,142 +201,121 @@ class AWSCostOptimizer:
|
|
202
201
|
StartTime=start_time,
|
203
202
|
EndTime=now,
|
204
203
|
Period=3600, # 1 hour periods
|
205
|
-
Statistics=["Average"]
|
204
|
+
Statistics=["Average"],
|
206
205
|
)
|
207
|
-
|
206
|
+
|
208
207
|
if not cpu_stats["Datapoints"]:
|
209
208
|
return False # No metrics = not idle (may be new instance)
|
210
|
-
|
209
|
+
|
211
210
|
# Calculate average CPU across all data points
|
212
|
-
avg_cpu = sum(
|
213
|
-
|
214
|
-
) / len(cpu_stats["Datapoints"])
|
215
|
-
|
211
|
+
avg_cpu = sum(datapoint["Average"] for datapoint in cpu_stats["Datapoints"]) / len(cpu_stats["Datapoints"])
|
212
|
+
|
216
213
|
return avg_cpu < idle_cpu_threshold
|
217
|
-
|
214
|
+
|
218
215
|
except Exception as e:
|
219
216
|
print_warning(f"Could not get metrics for {instance_id}: {e}")
|
220
217
|
return False
|
221
|
-
|
222
|
-
def stop_idle_instances(
|
223
|
-
self,
|
224
|
-
idle_instances: List[IdleInstance],
|
225
|
-
dry_run: bool = True
|
226
|
-
) -> CostOptimizationResult:
|
218
|
+
|
219
|
+
def stop_idle_instances(self, idle_instances: List[IdleInstance], dry_run: bool = True) -> CostOptimizationResult:
|
227
220
|
"""
|
228
221
|
Stop idle EC2 instances
|
229
|
-
|
222
|
+
|
230
223
|
Migrated from: AWS_Stop_Idle_EC2_Instances.ipynb
|
231
|
-
|
224
|
+
|
232
225
|
Args:
|
233
226
|
idle_instances: List of idle instances to stop
|
234
227
|
dry_run: If True, only simulate the action
|
235
|
-
|
228
|
+
|
236
229
|
Returns:
|
237
230
|
CostOptimizationResult with stopped instances and savings
|
238
231
|
"""
|
239
232
|
print_header(f"Cost Optimizer - Stop Idle Instances ({'DRY RUN' if dry_run else 'LIVE'})")
|
240
|
-
|
233
|
+
|
241
234
|
stopped_instances = []
|
242
235
|
total_savings = 0.0
|
243
236
|
errors = []
|
244
|
-
|
237
|
+
|
245
238
|
with create_progress_bar() as progress:
|
246
|
-
task_id = progress.add_task(
|
247
|
-
|
248
|
-
total=len(idle_instances)
|
249
|
-
)
|
250
|
-
|
239
|
+
task_id = progress.add_task("Processing idle instances...", total=len(idle_instances))
|
240
|
+
|
251
241
|
for instance in idle_instances:
|
252
242
|
try:
|
253
243
|
if dry_run:
|
254
244
|
# Simulate stop operation
|
255
245
|
stopped_instances.append(instance)
|
256
246
|
total_savings += instance.estimated_monthly_cost
|
257
|
-
console.print(
|
258
|
-
|
247
|
+
console.print(
|
248
|
+
f"[yellow]DRY RUN: Would stop {instance.instance_id} "
|
249
|
+
f"(${instance.estimated_monthly_cost:.2f}/month savings)[/yellow]"
|
250
|
+
)
|
259
251
|
else:
|
260
252
|
# Actually stop the instance
|
261
253
|
result = self._stop_single_instance(instance)
|
262
|
-
if result[
|
254
|
+
if result["success"]:
|
263
255
|
stopped_instances.append(instance)
|
264
256
|
total_savings += instance.estimated_monthly_cost
|
265
|
-
print_success(
|
266
|
-
|
257
|
+
print_success(
|
258
|
+
f"Stopped {instance.instance_id} - ${instance.estimated_monthly_cost:.2f}/month saved"
|
259
|
+
)
|
267
260
|
else:
|
268
261
|
errors.append(f"{instance.instance_id}: {result['error']}")
|
269
262
|
print_error(f"Failed to stop {instance.instance_id}: {result['error']}")
|
270
|
-
|
263
|
+
|
271
264
|
progress.advance(task_id)
|
272
|
-
|
265
|
+
|
273
266
|
except Exception as e:
|
274
267
|
errors.append(f"{instance.instance_id}: {str(e)}")
|
275
268
|
print_error(f"Error processing {instance.instance_id}: {e}")
|
276
269
|
progress.advance(task_id)
|
277
|
-
|
270
|
+
|
278
271
|
# Create summary
|
279
272
|
execution_summary = {
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
273
|
+
"total_instances_processed": len(idle_instances),
|
274
|
+
"successful_stops": len(stopped_instances),
|
275
|
+
"errors": errors,
|
276
|
+
"dry_run": dry_run,
|
277
|
+
"estimated_annual_savings": total_savings * 12,
|
285
278
|
}
|
286
|
-
|
279
|
+
|
287
280
|
result = CostOptimizationResult(
|
288
281
|
stopped_instances=stopped_instances,
|
289
282
|
total_potential_savings=total_savings,
|
290
|
-
execution_summary=execution_summary
|
283
|
+
execution_summary=execution_summary,
|
291
284
|
)
|
292
|
-
|
285
|
+
|
293
286
|
self._display_optimization_summary(result)
|
294
287
|
return result
|
295
|
-
|
288
|
+
|
296
289
|
def _stop_single_instance(self, instance: IdleInstance) -> Dict[str, Any]:
|
297
290
|
"""Stop a single EC2 instance"""
|
298
|
-
|
291
|
+
|
299
292
|
try:
|
300
|
-
ec2_client = self.session.client(
|
301
|
-
|
293
|
+
ec2_client = self.session.client("ec2", region_name=instance.region)
|
294
|
+
|
302
295
|
response = ec2_client.stop_instances(InstanceIds=[instance.instance_id])
|
303
|
-
|
296
|
+
|
304
297
|
# Extract state information
|
305
298
|
instance_state = {}
|
306
|
-
for stopping_instance in response[
|
307
|
-
instance_state[stopping_instance[
|
308
|
-
|
309
|
-
return {
|
310
|
-
|
311
|
-
'state_info': instance_state,
|
312
|
-
'instance_id': instance.instance_id
|
313
|
-
}
|
314
|
-
|
299
|
+
for stopping_instance in response["StoppingInstances"]:
|
300
|
+
instance_state[stopping_instance["InstanceId"]] = stopping_instance["CurrentState"]
|
301
|
+
|
302
|
+
return {"success": True, "state_info": instance_state, "instance_id": instance.instance_id}
|
303
|
+
|
315
304
|
except ClientError as e:
|
316
|
-
return {
|
317
|
-
'success': False,
|
318
|
-
'error': f"AWS API Error: {e}",
|
319
|
-
'instance_id': instance.instance_id
|
320
|
-
}
|
305
|
+
return {"success": False, "error": f"AWS API Error: {e}", "instance_id": instance.instance_id}
|
321
306
|
except Exception as e:
|
322
|
-
return {
|
323
|
-
|
324
|
-
'error': f"Unexpected error: {e}",
|
325
|
-
'instance_id': instance.instance_id
|
326
|
-
}
|
327
|
-
|
307
|
+
return {"success": False, "error": f"Unexpected error: {e}", "instance_id": instance.instance_id}
|
308
|
+
|
328
309
|
def _get_all_regions(self) -> List[str]:
|
329
310
|
"""Get list of all AWS regions"""
|
330
311
|
try:
|
331
|
-
ec2_client = self.session.client(
|
312
|
+
ec2_client = self.session.client("ec2", region_name="us-east-1")
|
332
313
|
response = ec2_client.describe_regions()
|
333
|
-
return [region[
|
314
|
+
return [region["RegionName"] for region in response["Regions"]]
|
334
315
|
except Exception:
|
335
316
|
# Fallback to common regions
|
336
|
-
return [
|
337
|
-
|
338
|
-
'ap-southeast-1', 'ap-northeast-1'
|
339
|
-
]
|
340
|
-
|
317
|
+
return ["us-east-1", "us-west-2", "eu-west-1", "eu-central-1", "ap-southeast-1", "ap-northeast-1"]
|
318
|
+
|
341
319
|
def _estimate_instance_monthly_cost(self, instance_type: str) -> float:
|
342
320
|
"""
|
343
321
|
Estimate monthly cost for instance type
|
@@ -345,26 +323,26 @@ class AWSCostOptimizer:
|
|
345
323
|
"""
|
346
324
|
# Simplified cost estimates (USD per month for common instance types)
|
347
325
|
cost_map = {
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
326
|
+
"t3.micro": 8.76,
|
327
|
+
"t3.small": 17.52,
|
328
|
+
"t3.medium": 35.04,
|
329
|
+
"t3.large": 70.08,
|
330
|
+
"t3.xlarge": 140.16,
|
331
|
+
"t3.2xlarge": 280.32,
|
332
|
+
"m5.large": 87.60,
|
333
|
+
"m5.xlarge": 175.20,
|
334
|
+
"m5.2xlarge": 350.40,
|
335
|
+
"c5.large": 78.84,
|
336
|
+
"c5.xlarge": 157.68,
|
337
|
+
"r5.large": 116.8,
|
338
|
+
"r5.xlarge": 233.6,
|
361
339
|
}
|
362
|
-
|
340
|
+
|
363
341
|
return cost_map.get(instance_type, 50.0) # Default estimate
|
364
|
-
|
342
|
+
|
365
343
|
def _display_idle_instances_table(self, idle_instances: List[IdleInstance]):
|
366
344
|
"""Display idle instances in a formatted table"""
|
367
|
-
|
345
|
+
|
368
346
|
table = create_table(
|
369
347
|
title="Idle EC2 Instances Found",
|
370
348
|
columns=[
|
@@ -373,66 +351,61 @@ class AWSCostOptimizer:
|
|
373
351
|
{"header": "Type", "style": "green"},
|
374
352
|
{"header": "Est. Monthly Cost", "style": "red"},
|
375
353
|
{"header": "Tags", "style": "yellow"},
|
376
|
-
]
|
354
|
+
],
|
377
355
|
)
|
378
|
-
|
356
|
+
|
379
357
|
for instance in idle_instances:
|
380
358
|
# Format tags for display
|
381
|
-
tag_display =
|
359
|
+
tag_display = ", ".join([f"{k}:{v}" for k, v in list(instance.tags.items())[:2]])
|
382
360
|
if len(instance.tags) > 2:
|
383
|
-
tag_display += f" (+{len(instance.tags)-2} more)"
|
384
|
-
|
361
|
+
tag_display += f" (+{len(instance.tags) - 2} more)"
|
362
|
+
|
385
363
|
table.add_row(
|
386
364
|
instance.instance_id,
|
387
365
|
instance.region,
|
388
366
|
instance.instance_type,
|
389
367
|
format_cost(instance.estimated_monthly_cost),
|
390
|
-
tag_display or "No tags"
|
368
|
+
tag_display or "No tags",
|
391
369
|
)
|
392
|
-
|
370
|
+
|
393
371
|
console.print(table)
|
394
|
-
|
372
|
+
|
395
373
|
def find_low_usage_volumes(
|
396
|
-
self,
|
397
|
-
region: str = "",
|
398
|
-
threshold_days: int = 10
|
374
|
+
self, region: str = "", threshold_days: int = 10
|
399
375
|
) -> Tuple[bool, Optional[List[LowUsageVolume]]]:
|
400
376
|
"""
|
401
377
|
Find EBS volumes with low usage based on CloudWatch metrics
|
402
|
-
|
378
|
+
|
403
379
|
Migrated from: AWS_Delete_EBS_Volumes_With_Low_Usage.ipynb
|
404
|
-
|
380
|
+
|
405
381
|
Args:
|
406
382
|
region: AWS Region to scan (empty for all regions)
|
407
383
|
threshold_days: Days to look back for usage metrics
|
408
|
-
|
384
|
+
|
409
385
|
Returns:
|
410
386
|
Tuple (success, list_of_low_usage_volumes)
|
411
387
|
"""
|
412
|
-
print_header("Cost Optimizer - Low Usage EBS Volume Detection", "
|
413
|
-
|
388
|
+
print_header("Cost Optimizer - Low Usage EBS Volume Detection", "latest version")
|
389
|
+
|
414
390
|
result = []
|
415
391
|
regions_to_check = [region] if region else self._get_all_regions()
|
416
|
-
|
392
|
+
|
417
393
|
with create_progress_bar() as progress:
|
418
394
|
task_id = progress.add_task(
|
419
|
-
f"Scanning {len(regions_to_check)} regions for low usage volumes...",
|
420
|
-
total=len(regions_to_check)
|
395
|
+
f"Scanning {len(regions_to_check)} regions for low usage volumes...", total=len(regions_to_check)
|
421
396
|
)
|
422
|
-
|
397
|
+
|
423
398
|
for reg in regions_to_check:
|
424
399
|
try:
|
425
|
-
low_usage_volumes = self._scan_region_for_low_usage_volumes(
|
426
|
-
reg, threshold_days
|
427
|
-
)
|
400
|
+
low_usage_volumes = self._scan_region_for_low_usage_volumes(reg, threshold_days)
|
428
401
|
result.extend(low_usage_volumes)
|
429
402
|
progress.advance(task_id)
|
430
|
-
|
403
|
+
|
431
404
|
except Exception as e:
|
432
405
|
print_warning(f"Failed to scan region {reg}: {str(e)}")
|
433
406
|
progress.advance(task_id)
|
434
407
|
continue
|
435
|
-
|
408
|
+
|
436
409
|
if result:
|
437
410
|
print_success(f"Found {len(result)} low usage volumes across {len(regions_to_check)} regions")
|
438
411
|
self._display_low_usage_volumes_table(result)
|
@@ -440,205 +413,179 @@ class AWSCostOptimizer:
|
|
440
413
|
else:
|
441
414
|
print_success("No low usage volumes found")
|
442
415
|
return (True, None) # True = no results (unSkript convention)
|
443
|
-
|
444
|
-
def _scan_region_for_low_usage_volumes(
|
445
|
-
self,
|
446
|
-
region: str,
|
447
|
-
threshold_days: int
|
448
|
-
) -> List[LowUsageVolume]:
|
416
|
+
|
417
|
+
def _scan_region_for_low_usage_volumes(self, region: str, threshold_days: int) -> List[LowUsageVolume]:
|
449
418
|
"""Scan a specific region for low usage EBS volumes"""
|
450
|
-
|
419
|
+
|
451
420
|
result = []
|
452
|
-
|
421
|
+
|
453
422
|
try:
|
454
|
-
ec2_client = self.session.client(
|
455
|
-
cloudwatch_client = self.session.client(
|
456
|
-
|
423
|
+
ec2_client = self.session.client("ec2", region_name=region)
|
424
|
+
cloudwatch_client = self.session.client("cloudwatch", region_name=region)
|
425
|
+
|
457
426
|
# Get all EBS volumes
|
458
|
-
paginator = ec2_client.get_paginator(
|
459
|
-
|
427
|
+
paginator = ec2_client.get_paginator("describe_volumes")
|
428
|
+
|
460
429
|
now = datetime.datetime.utcnow()
|
461
430
|
days_ago = now - datetime.timedelta(days=threshold_days)
|
462
|
-
|
431
|
+
|
463
432
|
for page in paginator.paginate():
|
464
|
-
for volume in page[
|
465
|
-
volume_id = volume[
|
466
|
-
|
433
|
+
for volume in page["Volumes"]:
|
434
|
+
volume_id = volume["VolumeId"]
|
435
|
+
|
467
436
|
# Get CloudWatch metrics for volume usage
|
468
437
|
try:
|
469
438
|
metrics_response = cloudwatch_client.get_metric_statistics(
|
470
|
-
Namespace=
|
471
|
-
MetricName=
|
472
|
-
Dimensions=[
|
473
|
-
{
|
474
|
-
'Name': 'VolumeId',
|
475
|
-
'Value': volume_id
|
476
|
-
}
|
477
|
-
],
|
439
|
+
Namespace="AWS/EBS",
|
440
|
+
MetricName="VolumeReadBytes", # Changed from VolumeUsage to more standard metric
|
441
|
+
Dimensions=[{"Name": "VolumeId", "Value": volume_id}],
|
478
442
|
StartTime=days_ago,
|
479
443
|
EndTime=now,
|
480
444
|
Period=86400, # Daily periods
|
481
|
-
Statistics=[
|
445
|
+
Statistics=["Sum"],
|
482
446
|
)
|
483
|
-
|
447
|
+
|
484
448
|
# Calculate average usage
|
485
|
-
total_bytes = sum(dp[
|
486
|
-
avg_daily_bytes = total_bytes / max(len(metrics_response[
|
449
|
+
total_bytes = sum(dp["Sum"] for dp in metrics_response["Datapoints"])
|
450
|
+
avg_daily_bytes = total_bytes / max(len(metrics_response["Datapoints"]), 1)
|
487
451
|
avg_daily_gb = avg_daily_bytes / (1024**3) # Convert to GB
|
488
|
-
|
452
|
+
|
489
453
|
# Consider volume as low usage if < 1GB daily average read
|
490
|
-
if avg_daily_gb < 1.0 or not metrics_response[
|
491
|
-
|
454
|
+
if avg_daily_gb < 1.0 or not metrics_response["Datapoints"]:
|
492
455
|
# Extract tags
|
493
|
-
tags = {tag[
|
494
|
-
|
456
|
+
tags = {tag["Key"]: tag["Value"] for tag in volume.get("Tags", [])}
|
457
|
+
|
495
458
|
low_usage_volume = LowUsageVolume(
|
496
459
|
volume_id=volume_id,
|
497
460
|
region=region,
|
498
|
-
volume_type=volume.get(
|
499
|
-
size_gb=volume.get(
|
461
|
+
volume_type=volume.get("VolumeType", "unknown"),
|
462
|
+
size_gb=volume.get("Size", 0),
|
500
463
|
avg_usage=avg_daily_gb,
|
501
|
-
creation_date=volume.get(
|
502
|
-
|
464
|
+
creation_date=volume.get("CreateTime", "").isoformat()
|
465
|
+
if volume.get("CreateTime")
|
466
|
+
else None,
|
467
|
+
tags=tags,
|
503
468
|
)
|
504
|
-
|
469
|
+
|
505
470
|
# Calculate estimated cost
|
506
471
|
low_usage_volume.estimated_monthly_cost = self._estimate_ebs_monthly_cost(
|
507
|
-
volume.get(
|
508
|
-
volume.get('Size', 0)
|
472
|
+
volume.get("VolumeType", "gp3"), volume.get("Size", 0)
|
509
473
|
)
|
510
|
-
|
474
|
+
|
511
475
|
result.append(low_usage_volume)
|
512
|
-
|
476
|
+
|
513
477
|
except ClientError as e:
|
514
478
|
# Skip volumes we can't get metrics for
|
515
|
-
if
|
479
|
+
if "Throttling" not in str(e):
|
516
480
|
print_warning(f"Could not get metrics for volume {volume_id}: {e}")
|
517
481
|
continue
|
518
|
-
|
482
|
+
|
519
483
|
except ClientError as e:
|
520
484
|
print_warning(f"AWS API error in region {region}: {e}")
|
521
485
|
except Exception as e:
|
522
486
|
print_error(f"Unexpected error in region {region}: {e}")
|
523
|
-
|
487
|
+
|
524
488
|
return result
|
525
|
-
|
489
|
+
|
526
490
|
def delete_low_usage_volumes(
|
527
|
-
self,
|
528
|
-
low_usage_volumes: List[LowUsageVolume],
|
529
|
-
create_snapshots: bool = True,
|
530
|
-
dry_run: bool = True
|
491
|
+
self, low_usage_volumes: List[LowUsageVolume], create_snapshots: bool = True, dry_run: bool = True
|
531
492
|
) -> CostOptimizationResult:
|
532
493
|
"""
|
533
494
|
Delete low usage EBS volumes (optionally creating snapshots first)
|
534
|
-
|
495
|
+
|
535
496
|
Migrated from: AWS_Delete_EBS_Volumes_With_Low_Usage.ipynb
|
536
|
-
|
497
|
+
|
537
498
|
Args:
|
538
499
|
low_usage_volumes: List of volumes to delete
|
539
500
|
create_snapshots: Create snapshots before deletion
|
540
501
|
dry_run: If True, only simulate the action
|
541
|
-
|
502
|
+
|
542
503
|
Returns:
|
543
504
|
CostOptimizationResult with deleted volumes and savings
|
544
505
|
"""
|
545
506
|
print_header(f"Cost Optimizer - Delete Low Usage Volumes ({'DRY RUN' if dry_run else 'LIVE'})")
|
546
|
-
|
507
|
+
|
547
508
|
deleted_volumes = []
|
548
509
|
total_savings = 0.0
|
549
510
|
errors = []
|
550
|
-
|
511
|
+
|
551
512
|
with create_progress_bar() as progress:
|
552
|
-
task_id = progress.add_task(
|
553
|
-
|
554
|
-
total=len(low_usage_volumes)
|
555
|
-
)
|
556
|
-
|
513
|
+
task_id = progress.add_task("Processing low usage volumes...", total=len(low_usage_volumes))
|
514
|
+
|
557
515
|
for volume in low_usage_volumes:
|
558
516
|
try:
|
559
517
|
if dry_run:
|
560
518
|
# Simulate deletion
|
561
519
|
deleted_volumes.append(volume)
|
562
520
|
total_savings += volume.estimated_monthly_cost
|
563
|
-
console.print(
|
564
|
-
|
565
|
-
|
521
|
+
console.print(
|
522
|
+
f"[yellow]DRY RUN: Would delete {volume.volume_id} "
|
523
|
+
f"({volume.size_gb}GB {volume.volume_type}) - "
|
524
|
+
f"${volume.estimated_monthly_cost:.2f}/month savings[/yellow]"
|
525
|
+
)
|
566
526
|
else:
|
567
527
|
# Actually delete the volume
|
568
528
|
result = self._delete_single_volume(volume, create_snapshots)
|
569
|
-
if result[
|
529
|
+
if result["success"]:
|
570
530
|
deleted_volumes.append(volume)
|
571
531
|
total_savings += volume.estimated_monthly_cost
|
572
|
-
print_success(
|
573
|
-
|
532
|
+
print_success(
|
533
|
+
f"Deleted {volume.volume_id} - ${volume.estimated_monthly_cost:.2f}/month saved"
|
534
|
+
)
|
574
535
|
else:
|
575
536
|
errors.append(f"{volume.volume_id}: {result['error']}")
|
576
537
|
print_error(f"Failed to delete {volume.volume_id}: {result['error']}")
|
577
|
-
|
538
|
+
|
578
539
|
progress.advance(task_id)
|
579
|
-
|
540
|
+
|
580
541
|
except Exception as e:
|
581
542
|
errors.append(f"{volume.volume_id}: {str(e)}")
|
582
543
|
print_error(f"Error processing {volume.volume_id}: {e}")
|
583
544
|
progress.advance(task_id)
|
584
|
-
|
545
|
+
|
585
546
|
# Create summary
|
586
547
|
execution_summary = {
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
548
|
+
"total_volumes_processed": len(low_usage_volumes),
|
549
|
+
"successful_deletions": len(deleted_volumes),
|
550
|
+
"errors": errors,
|
551
|
+
"dry_run": dry_run,
|
552
|
+
"snapshots_created": create_snapshots,
|
553
|
+
"estimated_annual_savings": total_savings * 12,
|
593
554
|
}
|
594
|
-
|
555
|
+
|
595
556
|
result = CostOptimizationResult(
|
596
|
-
deleted_volumes=deleted_volumes,
|
597
|
-
total_potential_savings=total_savings,
|
598
|
-
execution_summary=execution_summary
|
557
|
+
deleted_volumes=deleted_volumes, total_potential_savings=total_savings, execution_summary=execution_summary
|
599
558
|
)
|
600
|
-
|
559
|
+
|
601
560
|
self._display_volume_optimization_summary(result)
|
602
561
|
return result
|
603
|
-
|
562
|
+
|
604
563
|
def _delete_single_volume(self, volume: LowUsageVolume, create_snapshot: bool = True) -> Dict[str, Any]:
|
605
564
|
"""Delete a single EBS volume (with optional snapshot)"""
|
606
|
-
|
565
|
+
|
607
566
|
try:
|
608
|
-
ec2_client = self.session.client(
|
609
|
-
|
567
|
+
ec2_client = self.session.client("ec2", region_name=volume.region)
|
568
|
+
|
610
569
|
snapshot_id = None
|
611
570
|
if create_snapshot:
|
612
571
|
# Create snapshot first
|
613
572
|
snapshot_response = ec2_client.create_snapshot(
|
614
573
|
VolumeId=volume.volume_id,
|
615
|
-
Description=f"Automated backup before deleting low usage volume {volume.volume_id}"
|
574
|
+
Description=f"Automated backup before deleting low usage volume {volume.volume_id}",
|
616
575
|
)
|
617
|
-
snapshot_id = snapshot_response[
|
576
|
+
snapshot_id = snapshot_response["SnapshotId"]
|
618
577
|
print_success(f"Created snapshot {snapshot_id} for volume {volume.volume_id}")
|
619
|
-
|
578
|
+
|
620
579
|
# Delete the volume
|
621
580
|
ec2_client.delete_volume(VolumeId=volume.volume_id)
|
622
|
-
|
623
|
-
return {
|
624
|
-
|
625
|
-
'snapshot_id': snapshot_id,
|
626
|
-
'volume_id': volume.volume_id
|
627
|
-
}
|
628
|
-
|
581
|
+
|
582
|
+
return {"success": True, "snapshot_id": snapshot_id, "volume_id": volume.volume_id}
|
583
|
+
|
629
584
|
except ClientError as e:
|
630
|
-
return {
|
631
|
-
'success': False,
|
632
|
-
'error': f"AWS API Error: {e}",
|
633
|
-
'volume_id': volume.volume_id
|
634
|
-
}
|
585
|
+
return {"success": False, "error": f"AWS API Error: {e}", "volume_id": volume.volume_id}
|
635
586
|
except Exception as e:
|
636
|
-
return {
|
637
|
-
|
638
|
-
'error': f"Unexpected error: {e}",
|
639
|
-
'volume_id': volume.volume_id
|
640
|
-
}
|
641
|
-
|
587
|
+
return {"success": False, "error": f"Unexpected error: {e}", "volume_id": volume.volume_id}
|
588
|
+
|
642
589
|
def _estimate_ebs_monthly_cost(self, volume_type: str, size_gb: int) -> float:
|
643
590
|
"""
|
644
591
|
Estimate monthly cost for EBS volume
|
@@ -646,21 +593,21 @@ class AWSCostOptimizer:
|
|
646
593
|
"""
|
647
594
|
# Simplified cost estimates (USD per GB per month)
|
648
595
|
cost_per_gb = {
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
596
|
+
"gp3": 0.08,
|
597
|
+
"gp2": 0.10,
|
598
|
+
"io1": 0.125,
|
599
|
+
"io2": 0.125,
|
600
|
+
"st1": 0.045,
|
601
|
+
"sc1": 0.025,
|
602
|
+
"standard": 0.05,
|
656
603
|
}
|
657
|
-
|
604
|
+
|
658
605
|
rate = cost_per_gb.get(volume_type, 0.08) # Default to gp3
|
659
606
|
return size_gb * rate
|
660
|
-
|
607
|
+
|
661
608
|
def _display_low_usage_volumes_table(self, low_usage_volumes: List[LowUsageVolume]):
|
662
609
|
"""Display low usage volumes in a formatted table"""
|
663
|
-
|
610
|
+
|
664
611
|
table = create_table(
|
665
612
|
title="Low Usage EBS Volumes Found",
|
666
613
|
columns=[
|
@@ -670,99 +617,91 @@ class AWSCostOptimizer:
|
|
670
617
|
{"header": "Size (GB)", "style": "yellow"},
|
671
618
|
{"header": "Est. Monthly Cost", "style": "red"},
|
672
619
|
{"header": "Tags", "style": "magenta"},
|
673
|
-
]
|
620
|
+
],
|
674
621
|
)
|
675
|
-
|
622
|
+
|
676
623
|
for volume in low_usage_volumes:
|
677
624
|
# Format tags for display
|
678
|
-
tag_display =
|
625
|
+
tag_display = ", ".join([f"{k}:{v}" for k, v in list(volume.tags.items())[:2]])
|
679
626
|
if len(volume.tags) > 2:
|
680
|
-
tag_display += f" (+{len(volume.tags)-2} more)"
|
681
|
-
|
627
|
+
tag_display += f" (+{len(volume.tags) - 2} more)"
|
628
|
+
|
682
629
|
table.add_row(
|
683
630
|
volume.volume_id,
|
684
631
|
volume.region,
|
685
632
|
volume.volume_type,
|
686
633
|
str(volume.size_gb),
|
687
634
|
format_cost(volume.estimated_monthly_cost),
|
688
|
-
tag_display or "No tags"
|
635
|
+
tag_display or "No tags",
|
689
636
|
)
|
690
|
-
|
637
|
+
|
691
638
|
console.print(table)
|
692
|
-
|
639
|
+
|
693
640
|
def _display_volume_optimization_summary(self, result: CostOptimizationResult):
|
694
641
|
"""Display volume optimization summary"""
|
695
|
-
|
642
|
+
|
696
643
|
summary = result.execution_summary
|
697
|
-
|
644
|
+
|
698
645
|
console.print()
|
699
646
|
print_header("EBS Volume Optimization Summary")
|
700
|
-
|
647
|
+
|
701
648
|
# Create summary table
|
702
649
|
summary_table = create_table(
|
703
650
|
title="Volume Optimization Results",
|
704
|
-
columns=[
|
705
|
-
{"header": "Metric", "style": "cyan"},
|
706
|
-
{"header": "Value", "style": "green bold"}
|
707
|
-
]
|
651
|
+
columns=[{"header": "Metric", "style": "cyan"}, {"header": "Value", "style": "green bold"}],
|
708
652
|
)
|
709
|
-
|
710
|
-
summary_table.add_row("Volumes Processed", str(summary[
|
711
|
-
summary_table.add_row("Successfully Deleted", str(summary[
|
712
|
-
summary_table.add_row("Errors", str(len(summary[
|
713
|
-
summary_table.add_row("Snapshots Created", "Yes" if summary[
|
653
|
+
|
654
|
+
summary_table.add_row("Volumes Processed", str(summary["total_volumes_processed"]))
|
655
|
+
summary_table.add_row("Successfully Deleted", str(summary["successful_deletions"]))
|
656
|
+
summary_table.add_row("Errors", str(len(summary["errors"])))
|
657
|
+
summary_table.add_row("Snapshots Created", "Yes" if summary["snapshots_created"] else "No")
|
714
658
|
summary_table.add_row("Monthly Savings", format_cost(result.total_potential_savings))
|
715
|
-
summary_table.add_row("Annual Savings", format_cost(summary[
|
716
|
-
summary_table.add_row("Mode", "DRY RUN" if summary[
|
717
|
-
|
659
|
+
summary_table.add_row("Annual Savings", format_cost(summary["estimated_annual_savings"]))
|
660
|
+
summary_table.add_row("Mode", "DRY RUN" if summary["dry_run"] else "LIVE EXECUTION")
|
661
|
+
|
718
662
|
console.print(summary_table)
|
719
|
-
|
720
|
-
if summary[
|
663
|
+
|
664
|
+
if summary["errors"]:
|
721
665
|
print_warning(f"Encountered {len(summary['errors'])} errors:")
|
722
|
-
for error in summary[
|
666
|
+
for error in summary["errors"]:
|
723
667
|
console.print(f" [red]• {error}[/red]")
|
724
|
-
|
668
|
+
|
725
669
|
def find_unused_nat_gateways(
|
726
|
-
self,
|
727
|
-
region: str = "",
|
728
|
-
number_of_days: int = 7
|
670
|
+
self, region: str = "", number_of_days: int = 7
|
729
671
|
) -> Tuple[bool, Optional[List[UnusedNATGateway]]]:
|
730
672
|
"""
|
731
673
|
Find unused NAT Gateways based on CloudWatch connection metrics
|
732
|
-
|
674
|
+
|
733
675
|
Migrated from: AWS_Delete_Unused_NAT_Gateways.ipynb
|
734
|
-
|
676
|
+
|
735
677
|
Args:
|
736
678
|
region: AWS Region to scan (empty for all regions)
|
737
679
|
number_of_days: Days to look back for usage metrics
|
738
|
-
|
680
|
+
|
739
681
|
Returns:
|
740
682
|
Tuple (success, list_of_unused_nat_gateways)
|
741
683
|
"""
|
742
|
-
print_header("Cost Optimizer - Unused NAT Gateway Detection", "
|
743
|
-
|
684
|
+
print_header("Cost Optimizer - Unused NAT Gateway Detection", "latest version")
|
685
|
+
|
744
686
|
result = []
|
745
687
|
regions_to_check = [region] if region else self._get_all_regions()
|
746
|
-
|
688
|
+
|
747
689
|
with create_progress_bar() as progress:
|
748
690
|
task_id = progress.add_task(
|
749
|
-
f"Scanning {len(regions_to_check)} regions for unused NAT Gateways...",
|
750
|
-
total=len(regions_to_check)
|
691
|
+
f"Scanning {len(regions_to_check)} regions for unused NAT Gateways...", total=len(regions_to_check)
|
751
692
|
)
|
752
|
-
|
693
|
+
|
753
694
|
for reg in regions_to_check:
|
754
695
|
try:
|
755
|
-
unused_gateways = self._scan_region_for_unused_nat_gateways(
|
756
|
-
reg, number_of_days
|
757
|
-
)
|
696
|
+
unused_gateways = self._scan_region_for_unused_nat_gateways(reg, number_of_days)
|
758
697
|
result.extend(unused_gateways)
|
759
698
|
progress.advance(task_id)
|
760
|
-
|
699
|
+
|
761
700
|
except Exception as e:
|
762
701
|
print_warning(f"Failed to scan region {reg}: {str(e)}")
|
763
702
|
progress.advance(task_id)
|
764
703
|
continue
|
765
|
-
|
704
|
+
|
766
705
|
if result:
|
767
706
|
print_success(f"Found {len(result)} unused NAT Gateways across {len(regions_to_check)} regions")
|
768
707
|
self._display_unused_nat_gateways_table(result)
|
@@ -770,205 +709,184 @@ class AWSCostOptimizer:
|
|
770
709
|
else:
|
771
710
|
print_success("No unused NAT Gateways found")
|
772
711
|
return (True, None) # True = no results (unSkript convention)
|
773
|
-
|
774
|
-
def _scan_region_for_unused_nat_gateways(
|
775
|
-
self,
|
776
|
-
region: str,
|
777
|
-
number_of_days: int
|
778
|
-
) -> List[UnusedNATGateway]:
|
712
|
+
|
713
|
+
def _scan_region_for_unused_nat_gateways(self, region: str, number_of_days: int) -> List[UnusedNATGateway]:
|
779
714
|
"""Scan a specific region for unused NAT Gateways"""
|
780
|
-
|
715
|
+
|
781
716
|
result = []
|
782
|
-
|
717
|
+
|
783
718
|
try:
|
784
|
-
ec2_client = self.session.client(
|
785
|
-
cloudwatch_client = self.session.client(
|
786
|
-
|
719
|
+
ec2_client = self.session.client("ec2", region_name=region)
|
720
|
+
cloudwatch_client = self.session.client("cloudwatch", region_name=region)
|
721
|
+
|
787
722
|
# Get all NAT Gateways
|
788
723
|
response = ec2_client.describe_nat_gateways()
|
789
|
-
|
724
|
+
|
790
725
|
end_time = datetime.datetime.utcnow()
|
791
726
|
start_time = end_time - datetime.timedelta(days=number_of_days)
|
792
|
-
|
793
|
-
for nat_gateway in response[
|
794
|
-
if nat_gateway[
|
727
|
+
|
728
|
+
for nat_gateway in response["NatGateways"]:
|
729
|
+
if nat_gateway["State"] == "deleted":
|
795
730
|
continue
|
796
|
-
|
797
|
-
nat_gateway_id = nat_gateway[
|
798
|
-
|
731
|
+
|
732
|
+
nat_gateway_id = nat_gateway["NatGatewayId"]
|
733
|
+
|
799
734
|
# Check if NAT Gateway is used based on connection metrics
|
800
|
-
if not self._is_nat_gateway_used(
|
801
|
-
cloudwatch_client, nat_gateway, start_time, end_time, number_of_days
|
802
|
-
):
|
735
|
+
if not self._is_nat_gateway_used(cloudwatch_client, nat_gateway, start_time, end_time, number_of_days):
|
803
736
|
# Extract tags
|
804
|
-
tags = {tag[
|
805
|
-
|
737
|
+
tags = {tag["Key"]: tag["Value"] for tag in nat_gateway.get("Tags", [])}
|
738
|
+
|
806
739
|
unused_gateway = UnusedNATGateway(
|
807
740
|
nat_gateway_id=nat_gateway_id,
|
808
741
|
region=region,
|
809
|
-
vpc_id=nat_gateway.get(
|
810
|
-
state=nat_gateway.get(
|
811
|
-
creation_date=nat_gateway.get(
|
812
|
-
|
742
|
+
vpc_id=nat_gateway.get("VpcId", ""),
|
743
|
+
state=nat_gateway.get("State", ""),
|
744
|
+
creation_date=nat_gateway.get("CreateTime", "").isoformat()
|
745
|
+
if nat_gateway.get("CreateTime")
|
746
|
+
else None,
|
747
|
+
tags=tags,
|
813
748
|
)
|
814
|
-
|
749
|
+
|
815
750
|
result.append(unused_gateway)
|
816
|
-
|
751
|
+
|
817
752
|
except ClientError as e:
|
818
753
|
print_warning(f"AWS API error in region {region}: {e}")
|
819
754
|
except Exception as e:
|
820
755
|
print_error(f"Unexpected error in region {region}: {e}")
|
821
|
-
|
756
|
+
|
822
757
|
return result
|
823
|
-
|
758
|
+
|
824
759
|
def _is_nat_gateway_used(
|
825
760
|
self,
|
826
761
|
cloudwatch_client,
|
827
762
|
nat_gateway: Dict[str, Any],
|
828
763
|
start_time: datetime.datetime,
|
829
764
|
end_time: datetime.datetime,
|
830
|
-
number_of_days: int
|
765
|
+
number_of_days: int,
|
831
766
|
) -> bool:
|
832
767
|
"""Check if NAT Gateway is used based on connection metrics"""
|
833
|
-
|
768
|
+
|
834
769
|
try:
|
835
|
-
if nat_gateway[
|
770
|
+
if nat_gateway["State"] != "available":
|
836
771
|
return True # Consider non-available gateways as "used"
|
837
|
-
|
772
|
+
|
838
773
|
# Get ActiveConnectionCount metrics
|
839
774
|
metrics_response = cloudwatch_client.get_metric_statistics(
|
840
|
-
Namespace=
|
841
|
-
MetricName=
|
775
|
+
Namespace="AWS/NATGateway",
|
776
|
+
MetricName="ActiveConnectionCount",
|
842
777
|
Dimensions=[
|
843
|
-
{
|
844
|
-
'Name': 'NatGatewayId',
|
845
|
-
'Value': nat_gateway['NatGatewayId']
|
846
|
-
},
|
778
|
+
{"Name": "NatGatewayId", "Value": nat_gateway["NatGatewayId"]},
|
847
779
|
],
|
848
780
|
StartTime=start_time,
|
849
781
|
EndTime=end_time,
|
850
782
|
Period=86400 * number_of_days, # Daily periods
|
851
|
-
Statistics=[
|
783
|
+
Statistics=["Sum"],
|
852
784
|
)
|
853
|
-
|
854
|
-
datapoints = metrics_response.get(
|
855
|
-
|
785
|
+
|
786
|
+
datapoints = metrics_response.get("Datapoints", [])
|
787
|
+
|
856
788
|
if not datapoints:
|
857
789
|
return False # No metrics = unused
|
858
|
-
|
790
|
+
|
859
791
|
# Check if there are any active connections
|
860
|
-
total_connections = sum(dp[
|
792
|
+
total_connections = sum(dp["Sum"] for dp in datapoints)
|
861
793
|
return total_connections > 0
|
862
|
-
|
794
|
+
|
863
795
|
except Exception as e:
|
864
796
|
print_warning(f"Could not get metrics for NAT Gateway {nat_gateway['NatGatewayId']}: {e}")
|
865
797
|
return True # Assume used if we can't get metrics
|
866
|
-
|
798
|
+
|
867
799
|
def delete_unused_nat_gateways(
|
868
|
-
self,
|
869
|
-
unused_nat_gateways: List[UnusedNATGateway],
|
870
|
-
dry_run: bool = True
|
800
|
+
self, unused_nat_gateways: List[UnusedNATGateway], dry_run: bool = True
|
871
801
|
) -> CostOptimizationResult:
|
872
802
|
"""
|
873
803
|
Delete unused NAT Gateways
|
874
|
-
|
804
|
+
|
875
805
|
Migrated from: AWS_Delete_Unused_NAT_Gateways.ipynb
|
876
|
-
|
806
|
+
|
877
807
|
Args:
|
878
808
|
unused_nat_gateways: List of NAT Gateways to delete
|
879
809
|
dry_run: If True, only simulate the action
|
880
|
-
|
810
|
+
|
881
811
|
Returns:
|
882
812
|
CostOptimizationResult with deleted NAT Gateways and savings
|
883
813
|
"""
|
884
814
|
print_header(f"Cost Optimizer - Delete Unused NAT Gateways ({'DRY RUN' if dry_run else 'LIVE'})")
|
885
|
-
|
815
|
+
|
886
816
|
deleted_gateways = []
|
887
817
|
total_savings = 0.0
|
888
818
|
errors = []
|
889
|
-
|
819
|
+
|
890
820
|
with create_progress_bar() as progress:
|
891
|
-
task_id = progress.add_task(
|
892
|
-
|
893
|
-
total=len(unused_nat_gateways)
|
894
|
-
)
|
895
|
-
|
821
|
+
task_id = progress.add_task("Processing unused NAT Gateways...", total=len(unused_nat_gateways))
|
822
|
+
|
896
823
|
for gateway in unused_nat_gateways:
|
897
824
|
try:
|
898
825
|
if dry_run:
|
899
826
|
# Simulate deletion
|
900
827
|
deleted_gateways.append(gateway)
|
901
828
|
total_savings += gateway.estimated_monthly_cost
|
902
|
-
console.print(
|
903
|
-
|
904
|
-
|
829
|
+
console.print(
|
830
|
+
f"[yellow]DRY RUN: Would delete {gateway.nat_gateway_id} "
|
831
|
+
f"in VPC {gateway.vpc_id} - "
|
832
|
+
f"${gateway.estimated_monthly_cost:.2f}/month savings[/yellow]"
|
833
|
+
)
|
905
834
|
else:
|
906
835
|
# Actually delete the NAT Gateway
|
907
836
|
result = self._delete_single_nat_gateway(gateway)
|
908
|
-
if result[
|
837
|
+
if result["success"]:
|
909
838
|
deleted_gateways.append(gateway)
|
910
839
|
total_savings += gateway.estimated_monthly_cost
|
911
|
-
print_success(
|
912
|
-
|
840
|
+
print_success(
|
841
|
+
f"Deleted {gateway.nat_gateway_id} - ${gateway.estimated_monthly_cost:.2f}/month saved"
|
842
|
+
)
|
913
843
|
else:
|
914
844
|
errors.append(f"{gateway.nat_gateway_id}: {result['error']}")
|
915
845
|
print_error(f"Failed to delete {gateway.nat_gateway_id}: {result['error']}")
|
916
|
-
|
846
|
+
|
917
847
|
progress.advance(task_id)
|
918
|
-
|
848
|
+
|
919
849
|
except Exception as e:
|
920
850
|
errors.append(f"{gateway.nat_gateway_id}: {str(e)}")
|
921
851
|
print_error(f"Error processing {gateway.nat_gateway_id}: {e}")
|
922
852
|
progress.advance(task_id)
|
923
|
-
|
853
|
+
|
924
854
|
# Create summary
|
925
855
|
execution_summary = {
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
856
|
+
"total_nat_gateways_processed": len(unused_nat_gateways),
|
857
|
+
"successful_deletions": len(deleted_gateways),
|
858
|
+
"errors": errors,
|
859
|
+
"dry_run": dry_run,
|
860
|
+
"estimated_annual_savings": total_savings * 12,
|
931
861
|
}
|
932
|
-
|
862
|
+
|
933
863
|
result = CostOptimizationResult(
|
934
864
|
deleted_nat_gateways=deleted_gateways,
|
935
865
|
total_potential_savings=total_savings,
|
936
|
-
execution_summary=execution_summary
|
866
|
+
execution_summary=execution_summary,
|
937
867
|
)
|
938
|
-
|
868
|
+
|
939
869
|
self._display_nat_gateway_optimization_summary(result)
|
940
870
|
return result
|
941
|
-
|
871
|
+
|
942
872
|
def _delete_single_nat_gateway(self, gateway: UnusedNATGateway) -> Dict[str, Any]:
|
943
873
|
"""Delete a single NAT Gateway"""
|
944
|
-
|
874
|
+
|
945
875
|
try:
|
946
|
-
ec2_client = self.session.client(
|
947
|
-
|
876
|
+
ec2_client = self.session.client("ec2", region_name=gateway.region)
|
877
|
+
|
948
878
|
response = ec2_client.delete_nat_gateway(NatGatewayId=gateway.nat_gateway_id)
|
949
|
-
|
950
|
-
return {
|
951
|
-
|
952
|
-
'response': response,
|
953
|
-
'nat_gateway_id': gateway.nat_gateway_id
|
954
|
-
}
|
955
|
-
|
879
|
+
|
880
|
+
return {"success": True, "response": response, "nat_gateway_id": gateway.nat_gateway_id}
|
881
|
+
|
956
882
|
except ClientError as e:
|
957
|
-
return {
|
958
|
-
'success': False,
|
959
|
-
'error': f"AWS API Error: {e}",
|
960
|
-
'nat_gateway_id': gateway.nat_gateway_id
|
961
|
-
}
|
883
|
+
return {"success": False, "error": f"AWS API Error: {e}", "nat_gateway_id": gateway.nat_gateway_id}
|
962
884
|
except Exception as e:
|
963
|
-
return {
|
964
|
-
|
965
|
-
'error': f"Unexpected error: {e}",
|
966
|
-
'nat_gateway_id': gateway.nat_gateway_id
|
967
|
-
}
|
968
|
-
|
885
|
+
return {"success": False, "error": f"Unexpected error: {e}", "nat_gateway_id": gateway.nat_gateway_id}
|
886
|
+
|
969
887
|
def _display_unused_nat_gateways_table(self, unused_gateways: List[UnusedNATGateway]):
|
970
888
|
"""Display unused NAT Gateways in a formatted table"""
|
971
|
-
|
889
|
+
|
972
890
|
table = create_table(
|
973
891
|
title="Unused NAT Gateways Found",
|
974
892
|
columns=[
|
@@ -978,86 +896,80 @@ class AWSCostOptimizer:
|
|
978
896
|
{"header": "State", "style": "yellow"},
|
979
897
|
{"header": "Est. Monthly Cost", "style": "red"},
|
980
898
|
{"header": "Tags", "style": "magenta"},
|
981
|
-
]
|
899
|
+
],
|
982
900
|
)
|
983
|
-
|
901
|
+
|
984
902
|
for gateway in unused_gateways:
|
985
903
|
# Format tags for display
|
986
|
-
tag_display =
|
904
|
+
tag_display = ", ".join([f"{k}:{v}" for k, v in list(gateway.tags.items())[:2]])
|
987
905
|
if len(gateway.tags) > 2:
|
988
|
-
tag_display += f" (+{len(gateway.tags)-2} more)"
|
989
|
-
|
906
|
+
tag_display += f" (+{len(gateway.tags) - 2} more)"
|
907
|
+
|
990
908
|
table.add_row(
|
991
909
|
gateway.nat_gateway_id,
|
992
910
|
gateway.region,
|
993
911
|
gateway.vpc_id,
|
994
912
|
gateway.state,
|
995
913
|
format_cost(gateway.estimated_monthly_cost),
|
996
|
-
tag_display or "No tags"
|
914
|
+
tag_display or "No tags",
|
997
915
|
)
|
998
|
-
|
916
|
+
|
999
917
|
console.print(table)
|
1000
|
-
|
918
|
+
|
1001
919
|
def _display_nat_gateway_optimization_summary(self, result: CostOptimizationResult):
|
1002
920
|
"""Display NAT Gateway optimization summary"""
|
1003
|
-
|
921
|
+
|
1004
922
|
summary = result.execution_summary
|
1005
|
-
|
923
|
+
|
1006
924
|
console.print()
|
1007
925
|
print_header("NAT Gateway Optimization Summary")
|
1008
|
-
|
926
|
+
|
1009
927
|
# Create summary table
|
1010
928
|
summary_table = create_table(
|
1011
929
|
title="NAT Gateway Optimization Results",
|
1012
|
-
columns=[
|
1013
|
-
{"header": "Metric", "style": "cyan"},
|
1014
|
-
{"header": "Value", "style": "green bold"}
|
1015
|
-
]
|
930
|
+
columns=[{"header": "Metric", "style": "cyan"}, {"header": "Value", "style": "green bold"}],
|
1016
931
|
)
|
1017
|
-
|
1018
|
-
summary_table.add_row("NAT Gateways Processed", str(summary[
|
1019
|
-
summary_table.add_row("Successfully Deleted", str(summary[
|
1020
|
-
summary_table.add_row("Errors", str(len(summary[
|
932
|
+
|
933
|
+
summary_table.add_row("NAT Gateways Processed", str(summary["total_nat_gateways_processed"]))
|
934
|
+
summary_table.add_row("Successfully Deleted", str(summary["successful_deletions"]))
|
935
|
+
summary_table.add_row("Errors", str(len(summary["errors"])))
|
1021
936
|
summary_table.add_row("Monthly Savings", format_cost(result.total_potential_savings))
|
1022
|
-
summary_table.add_row("Annual Savings", format_cost(summary[
|
1023
|
-
summary_table.add_row("Mode", "DRY RUN" if summary[
|
1024
|
-
|
937
|
+
summary_table.add_row("Annual Savings", format_cost(summary["estimated_annual_savings"]))
|
938
|
+
summary_table.add_row("Mode", "DRY RUN" if summary["dry_run"] else "LIVE EXECUTION")
|
939
|
+
|
1025
940
|
console.print(summary_table)
|
1026
|
-
|
1027
|
-
if summary[
|
941
|
+
|
942
|
+
if summary["errors"]:
|
1028
943
|
print_warning(f"Encountered {len(summary['errors'])} errors:")
|
1029
|
-
for error in summary[
|
944
|
+
for error in summary["errors"]:
|
1030
945
|
console.print(f" [red]• {error}[/red]")
|
1031
|
-
|
946
|
+
|
1032
947
|
def _display_optimization_summary(self, result: CostOptimizationResult):
|
1033
948
|
"""Display cost optimization summary"""
|
1034
|
-
|
949
|
+
|
1035
950
|
summary = result.execution_summary
|
1036
|
-
|
951
|
+
|
1037
952
|
console.print()
|
1038
953
|
print_header("Cost Optimization Summary")
|
1039
|
-
|
954
|
+
|
1040
955
|
# Create summary table
|
1041
956
|
summary_table = create_table(
|
1042
957
|
title="Optimization Results",
|
1043
|
-
columns=[
|
1044
|
-
{"header": "Metric", "style": "cyan"},
|
1045
|
-
{"header": "Value", "style": "green bold"}
|
1046
|
-
]
|
958
|
+
columns=[{"header": "Metric", "style": "cyan"}, {"header": "Value", "style": "green bold"}],
|
1047
959
|
)
|
1048
|
-
|
1049
|
-
summary_table.add_row("Instances Processed", str(summary[
|
1050
|
-
summary_table.add_row("Successfully Stopped", str(summary[
|
1051
|
-
summary_table.add_row("Errors", str(len(summary[
|
960
|
+
|
961
|
+
summary_table.add_row("Instances Processed", str(summary["total_instances_processed"]))
|
962
|
+
summary_table.add_row("Successfully Stopped", str(summary["successful_stops"]))
|
963
|
+
summary_table.add_row("Errors", str(len(summary["errors"])))
|
1052
964
|
summary_table.add_row("Monthly Savings", format_cost(result.total_potential_savings))
|
1053
|
-
summary_table.add_row("Annual Savings", format_cost(summary[
|
1054
|
-
summary_table.add_row("Mode", "DRY RUN" if summary[
|
1055
|
-
|
965
|
+
summary_table.add_row("Annual Savings", format_cost(summary["estimated_annual_savings"]))
|
966
|
+
summary_table.add_row("Mode", "DRY RUN" if summary["dry_run"] else "LIVE EXECUTION")
|
967
|
+
|
1056
968
|
console.print(summary_table)
|
1057
|
-
|
1058
|
-
if summary[
|
969
|
+
|
970
|
+
if summary["errors"]:
|
1059
971
|
print_warning(f"Encountered {len(summary['errors'])} errors:")
|
1060
|
-
for error in summary[
|
972
|
+
for error in summary["errors"]:
|
1061
973
|
console.print(f" [red]• {error}[/red]")
|
1062
974
|
|
1063
975
|
|
@@ -1068,16 +980,16 @@ def find_and_stop_idle_instances(
|
|
1068
980
|
idle_cpu_threshold: int = 5,
|
1069
981
|
idle_duration: int = 6,
|
1070
982
|
instance_ids: Optional[List[str]] = None,
|
1071
|
-
dry_run: bool = True
|
983
|
+
dry_run: bool = True,
|
1072
984
|
) -> Dict[str, Any]:
|
1073
985
|
"""
|
1074
986
|
Main function for cost optimization - find and stop idle EC2 instances
|
1075
|
-
|
987
|
+
|
1076
988
|
This function replicates the complete unSkript notebook workflow
|
1077
989
|
"""
|
1078
|
-
|
990
|
+
|
1079
991
|
optimizer = AWSCostOptimizer(profile=profile)
|
1080
|
-
|
992
|
+
|
1081
993
|
# Step 1: Find idle instances (or use provided instance IDs)
|
1082
994
|
if instance_ids:
|
1083
995
|
print_warning("Using provided instance IDs - skipping idle detection")
|
@@ -1087,41 +999,31 @@ def find_and_stop_idle_instances(
|
|
1087
999
|
idle_instance = IdleInstance(
|
1088
1000
|
instance_id=instance_id,
|
1089
1001
|
region=region,
|
1090
|
-
estimated_monthly_cost=50.0 # Default estimate
|
1002
|
+
estimated_monthly_cost=50.0, # Default estimate
|
1091
1003
|
)
|
1092
1004
|
idle_instances.append(idle_instance)
|
1093
1005
|
success = False
|
1094
1006
|
found_instances = idle_instances
|
1095
1007
|
else:
|
1096
1008
|
success, found_instances = optimizer.find_idle_instances(
|
1097
|
-
region=region,
|
1098
|
-
idle_cpu_threshold=idle_cpu_threshold,
|
1099
|
-
idle_duration=idle_duration
|
1009
|
+
region=region, idle_cpu_threshold=idle_cpu_threshold, idle_duration=idle_duration
|
1100
1010
|
)
|
1101
|
-
|
1011
|
+
|
1102
1012
|
if success or not found_instances: # No idle instances found
|
1103
1013
|
print_success("No idle instances to process")
|
1104
|
-
return {
|
1105
|
-
|
1106
|
-
'instances_stopped': 0,
|
1107
|
-
'potential_savings': 0.0,
|
1108
|
-
'status': 'completed'
|
1109
|
-
}
|
1110
|
-
|
1014
|
+
return {"idle_instances_found": 0, "instances_stopped": 0, "potential_savings": 0.0, "status": "completed"}
|
1015
|
+
|
1111
1016
|
# Step 2: Stop idle instances
|
1112
|
-
optimization_result = optimizer.stop_idle_instances(
|
1113
|
-
|
1114
|
-
dry_run=dry_run
|
1115
|
-
)
|
1116
|
-
|
1017
|
+
optimization_result = optimizer.stop_idle_instances(idle_instances=found_instances, dry_run=dry_run)
|
1018
|
+
|
1117
1019
|
return {
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1020
|
+
"idle_instances_found": len(found_instances),
|
1021
|
+
"instances_stopped": len(optimization_result.stopped_instances),
|
1022
|
+
"potential_monthly_savings": optimization_result.total_potential_savings,
|
1023
|
+
"potential_annual_savings": optimization_result.execution_summary["estimated_annual_savings"],
|
1024
|
+
"dry_run": dry_run,
|
1025
|
+
"status": "completed",
|
1026
|
+
"details": optimization_result.execution_summary,
|
1125
1027
|
}
|
1126
1028
|
|
1127
1029
|
|
@@ -1132,16 +1034,16 @@ def find_and_delete_low_usage_volumes(
|
|
1132
1034
|
threshold_days: int = 10,
|
1133
1035
|
volume_ids: Optional[List[str]] = None,
|
1134
1036
|
create_snapshots: bool = True,
|
1135
|
-
dry_run: bool = True
|
1037
|
+
dry_run: bool = True,
|
1136
1038
|
) -> Dict[str, Any]:
|
1137
1039
|
"""
|
1138
1040
|
Main function for EBS cost optimization - find and delete low usage volumes
|
1139
|
-
|
1041
|
+
|
1140
1042
|
Migrated from: AWS_Delete_EBS_Volumes_With_Low_Usage.ipynb
|
1141
1043
|
"""
|
1142
|
-
|
1044
|
+
|
1143
1045
|
optimizer = AWSCostOptimizer(profile=profile)
|
1144
|
-
|
1046
|
+
|
1145
1047
|
# Step 1: Find low usage volumes (or use provided volume IDs)
|
1146
1048
|
if volume_ids:
|
1147
1049
|
print_warning("Using provided volume IDs - skipping usage detection")
|
@@ -1151,42 +1053,32 @@ def find_and_delete_low_usage_volumes(
|
|
1151
1053
|
low_usage_volume = LowUsageVolume(
|
1152
1054
|
volume_id=volume_id,
|
1153
1055
|
region=region,
|
1154
|
-
estimated_monthly_cost=5.0 # Default estimate
|
1056
|
+
estimated_monthly_cost=5.0, # Default estimate
|
1155
1057
|
)
|
1156
1058
|
low_usage_volumes.append(low_usage_volume)
|
1157
1059
|
success = False
|
1158
1060
|
found_volumes = low_usage_volumes
|
1159
1061
|
else:
|
1160
|
-
success, found_volumes = optimizer.find_low_usage_volumes(
|
1161
|
-
|
1162
|
-
threshold_days=threshold_days
|
1163
|
-
)
|
1164
|
-
|
1062
|
+
success, found_volumes = optimizer.find_low_usage_volumes(region=region, threshold_days=threshold_days)
|
1063
|
+
|
1165
1064
|
if success or not found_volumes: # No low usage volumes found
|
1166
1065
|
print_success("No low usage volumes to process")
|
1167
|
-
return {
|
1168
|
-
|
1169
|
-
'volumes_deleted': 0,
|
1170
|
-
'potential_savings': 0.0,
|
1171
|
-
'status': 'completed'
|
1172
|
-
}
|
1173
|
-
|
1066
|
+
return {"low_usage_volumes_found": 0, "volumes_deleted": 0, "potential_savings": 0.0, "status": "completed"}
|
1067
|
+
|
1174
1068
|
# Step 2: Delete low usage volumes
|
1175
1069
|
optimization_result = optimizer.delete_low_usage_volumes(
|
1176
|
-
low_usage_volumes=found_volumes,
|
1177
|
-
create_snapshots=create_snapshots,
|
1178
|
-
dry_run=dry_run
|
1070
|
+
low_usage_volumes=found_volumes, create_snapshots=create_snapshots, dry_run=dry_run
|
1179
1071
|
)
|
1180
|
-
|
1072
|
+
|
1181
1073
|
return {
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1074
|
+
"low_usage_volumes_found": len(found_volumes),
|
1075
|
+
"volumes_deleted": len(optimization_result.deleted_volumes),
|
1076
|
+
"potential_monthly_savings": optimization_result.total_potential_savings,
|
1077
|
+
"potential_annual_savings": optimization_result.execution_summary["estimated_annual_savings"],
|
1078
|
+
"snapshots_created": create_snapshots,
|
1079
|
+
"dry_run": dry_run,
|
1080
|
+
"status": "completed",
|
1081
|
+
"details": optimization_result.execution_summary,
|
1190
1082
|
}
|
1191
1083
|
|
1192
1084
|
|
@@ -1196,22 +1088,22 @@ def comprehensive_cost_optimization(
|
|
1196
1088
|
idle_cpu_threshold: int = 5,
|
1197
1089
|
idle_duration: int = 6,
|
1198
1090
|
volume_threshold_days: int = 10,
|
1199
|
-
dry_run: bool = True
|
1091
|
+
dry_run: bool = True,
|
1200
1092
|
) -> Dict[str, Any]:
|
1201
1093
|
"""
|
1202
1094
|
Comprehensive cost optimization combining EC2 and EBS optimizations
|
1203
|
-
|
1095
|
+
|
1204
1096
|
This combines multiple unSkript notebooks:
|
1205
|
-
- AWS_Stop_Idle_EC2_Instances.ipynb
|
1097
|
+
- AWS_Stop_Idle_EC2_Instances.ipynb
|
1206
1098
|
- AWS_Delete_EBS_Volumes_With_Low_Usage.ipynb
|
1207
1099
|
"""
|
1208
|
-
|
1209
|
-
print_header("Comprehensive AWS Cost Optimization", "
|
1210
|
-
|
1100
|
+
|
1101
|
+
print_header("Comprehensive AWS Cost Optimization", "latest version")
|
1102
|
+
|
1211
1103
|
total_monthly_savings = 0.0
|
1212
1104
|
total_annual_savings = 0.0
|
1213
1105
|
results = {}
|
1214
|
-
|
1106
|
+
|
1215
1107
|
# Step 1: EC2 Instance Optimization
|
1216
1108
|
try:
|
1217
1109
|
print_header("Phase 1: EC2 Instance Optimization")
|
@@ -1220,123 +1112,97 @@ def comprehensive_cost_optimization(
|
|
1220
1112
|
region=region,
|
1221
1113
|
idle_cpu_threshold=idle_cpu_threshold,
|
1222
1114
|
idle_duration=idle_duration,
|
1223
|
-
dry_run=dry_run
|
1115
|
+
dry_run=dry_run,
|
1224
1116
|
)
|
1225
|
-
results[
|
1226
|
-
total_monthly_savings += ec2_result.get(
|
1227
|
-
total_annual_savings += ec2_result.get(
|
1228
|
-
|
1117
|
+
results["ec2_optimization"] = ec2_result
|
1118
|
+
total_monthly_savings += ec2_result.get("potential_monthly_savings", 0.0)
|
1119
|
+
total_annual_savings += ec2_result.get("potential_annual_savings", 0.0)
|
1120
|
+
|
1229
1121
|
except Exception as e:
|
1230
1122
|
print_error(f"EC2 optimization failed: {e}")
|
1231
|
-
results[
|
1232
|
-
|
1233
|
-
# Step 2: EBS Volume Optimization
|
1123
|
+
results["ec2_optimization"] = {"error": str(e)}
|
1124
|
+
|
1125
|
+
# Step 2: EBS Volume Optimization
|
1234
1126
|
try:
|
1235
1127
|
print_header("Phase 2: EBS Volume Optimization")
|
1236
1128
|
ebs_result = find_and_delete_low_usage_volumes(
|
1237
|
-
profile=profile,
|
1238
|
-
region=region,
|
1239
|
-
threshold_days=volume_threshold_days,
|
1240
|
-
create_snapshots=True,
|
1241
|
-
dry_run=dry_run
|
1129
|
+
profile=profile, region=region, threshold_days=volume_threshold_days, create_snapshots=True, dry_run=dry_run
|
1242
1130
|
)
|
1243
|
-
results[
|
1244
|
-
total_monthly_savings += ebs_result.get(
|
1245
|
-
total_annual_savings += ebs_result.get(
|
1246
|
-
|
1131
|
+
results["ebs_optimization"] = ebs_result
|
1132
|
+
total_monthly_savings += ebs_result.get("potential_monthly_savings", 0.0)
|
1133
|
+
total_annual_savings += ebs_result.get("potential_annual_savings", 0.0)
|
1134
|
+
|
1247
1135
|
except Exception as e:
|
1248
1136
|
print_error(f"EBS optimization failed: {e}")
|
1249
|
-
results[
|
1250
|
-
|
1137
|
+
results["ebs_optimization"] = {"error": str(e)}
|
1138
|
+
|
1251
1139
|
# Summary
|
1252
1140
|
print_header("Comprehensive Cost Optimization Summary")
|
1253
|
-
|
1141
|
+
|
1254
1142
|
summary_table = create_table(
|
1255
1143
|
title="Total Cost Optimization Impact",
|
1256
1144
|
columns=[
|
1257
1145
|
{"header": "Resource Type", "style": "cyan"},
|
1258
|
-
{"header": "Items Found", "style": "yellow"},
|
1146
|
+
{"header": "Items Found", "style": "yellow"},
|
1259
1147
|
{"header": "Items Processed", "style": "green"},
|
1260
1148
|
{"header": "Monthly Savings", "style": "red bold"},
|
1261
|
-
]
|
1149
|
+
],
|
1262
1150
|
)
|
1263
|
-
|
1151
|
+
|
1264
1152
|
# EC2 Summary
|
1265
|
-
ec2_found = results.get(
|
1266
|
-
ec2_stopped = results.get(
|
1267
|
-
ec2_savings = results.get(
|
1268
|
-
|
1269
|
-
summary_table.add_row(
|
1270
|
-
|
1271
|
-
str(ec2_found),
|
1272
|
-
str(ec2_stopped),
|
1273
|
-
format_cost(ec2_savings)
|
1274
|
-
)
|
1275
|
-
|
1153
|
+
ec2_found = results.get("ec2_optimization", {}).get("idle_instances_found", 0)
|
1154
|
+
ec2_stopped = results.get("ec2_optimization", {}).get("instances_stopped", 0)
|
1155
|
+
ec2_savings = results.get("ec2_optimization", {}).get("potential_monthly_savings", 0.0)
|
1156
|
+
|
1157
|
+
summary_table.add_row("EC2 Instances", str(ec2_found), str(ec2_stopped), format_cost(ec2_savings))
|
1158
|
+
|
1276
1159
|
# EBS Summary
|
1277
|
-
ebs_found = results.get(
|
1278
|
-
ebs_deleted = results.get(
|
1279
|
-
ebs_savings = results.get(
|
1280
|
-
|
1281
|
-
summary_table.add_row(
|
1282
|
-
|
1283
|
-
str(ebs_found),
|
1284
|
-
str(ebs_deleted),
|
1285
|
-
format_cost(ebs_savings)
|
1286
|
-
)
|
1287
|
-
|
1160
|
+
ebs_found = results.get("ebs_optimization", {}).get("low_usage_volumes_found", 0)
|
1161
|
+
ebs_deleted = results.get("ebs_optimization", {}).get("volumes_deleted", 0)
|
1162
|
+
ebs_savings = results.get("ebs_optimization", {}).get("potential_monthly_savings", 0.0)
|
1163
|
+
|
1164
|
+
summary_table.add_row("EBS Volumes", str(ebs_found), str(ebs_deleted), format_cost(ebs_savings))
|
1165
|
+
|
1288
1166
|
# Total
|
1289
1167
|
summary_table.add_row(
|
1290
1168
|
"[bold]TOTAL[/bold]",
|
1291
1169
|
"[bold]" + str(ec2_found + ebs_found) + "[/bold]",
|
1292
1170
|
"[bold]" + str(ec2_stopped + ebs_deleted) + "[/bold]",
|
1293
|
-
"[bold]" + format_cost(total_monthly_savings) + "[/bold]"
|
1171
|
+
"[bold]" + format_cost(total_monthly_savings) + "[/bold]",
|
1294
1172
|
)
|
1295
|
-
|
1173
|
+
|
1296
1174
|
console.print(summary_table)
|
1297
|
-
|
1175
|
+
|
1298
1176
|
print_success(f"Total Annual Savings Potential: {format_cost(total_annual_savings)}")
|
1299
|
-
|
1177
|
+
|
1300
1178
|
if dry_run:
|
1301
1179
|
print_warning("This was a DRY RUN. No actual changes were made.")
|
1302
|
-
|
1180
|
+
|
1303
1181
|
return {
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1182
|
+
"total_monthly_savings": total_monthly_savings,
|
1183
|
+
"total_annual_savings": total_annual_savings,
|
1184
|
+
"ec2_optimization": results.get("ec2_optimization", {}),
|
1185
|
+
"ebs_optimization": results.get("ebs_optimization", {}),
|
1186
|
+
"dry_run": dry_run,
|
1187
|
+
"status": "completed",
|
1310
1188
|
}
|
1311
1189
|
|
1312
1190
|
|
1313
1191
|
if __name__ == "__main__":
|
1314
1192
|
# Direct execution for testing
|
1315
1193
|
print("Testing Cost Optimization Module...")
|
1316
|
-
|
1194
|
+
|
1317
1195
|
# Test 1: EC2 Instance Optimization
|
1318
1196
|
print("\n=== Testing EC2 Optimization ===")
|
1319
|
-
ec2_result = find_and_stop_idle_instances(
|
1320
|
-
region="us-east-1",
|
1321
|
-
idle_cpu_threshold=10,
|
1322
|
-
idle_duration=24,
|
1323
|
-
dry_run=True
|
1324
|
-
)
|
1197
|
+
ec2_result = find_and_stop_idle_instances(region="us-east-1", idle_cpu_threshold=10, idle_duration=24, dry_run=True)
|
1325
1198
|
print(f"EC2 Result: {ec2_result}")
|
1326
|
-
|
1327
|
-
# Test 2: EBS Volume Optimization
|
1199
|
+
|
1200
|
+
# Test 2: EBS Volume Optimization
|
1328
1201
|
print("\n=== Testing EBS Optimization ===")
|
1329
|
-
ebs_result = find_and_delete_low_usage_volumes(
|
1330
|
-
region="us-east-1",
|
1331
|
-
threshold_days=30,
|
1332
|
-
dry_run=True
|
1333
|
-
)
|
1202
|
+
ebs_result = find_and_delete_low_usage_volumes(region="us-east-1", threshold_days=30, dry_run=True)
|
1334
1203
|
print(f"EBS Result: {ebs_result}")
|
1335
|
-
|
1204
|
+
|
1336
1205
|
# Test 3: Comprehensive Optimization
|
1337
1206
|
print("\n=== Testing Comprehensive Optimization ===")
|
1338
|
-
comprehensive_result = comprehensive_cost_optimization(
|
1339
|
-
|
1340
|
-
dry_run=True
|
1341
|
-
)
|
1342
|
-
print(f"Comprehensive Result: {comprehensive_result}")
|
1207
|
+
comprehensive_result = comprehensive_cost_optimization(region="us-east-1", dry_run=True)
|
1208
|
+
print(f"Comprehensive Result: {comprehensive_result}")
|