runbooks 1.1.4__py3-none-any.whl → 1.1.6__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.
Files changed (273) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +135 -91
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +17 -12
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +99 -79
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +315 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/aws_decorators.py +2 -3
  113. runbooks/inventory/check_cloudtrail_compliance.py +2 -4
  114. runbooks/inventory/check_controltower_readiness.py +152 -151
  115. runbooks/inventory/check_landingzone_readiness.py +85 -84
  116. runbooks/inventory/cloud_foundations_integration.py +144 -149
  117. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  118. runbooks/inventory/collectors/aws_networking.py +109 -99
  119. runbooks/inventory/collectors/base.py +4 -0
  120. runbooks/inventory/core/collector.py +495 -313
  121. runbooks/inventory/core/formatter.py +11 -0
  122. runbooks/inventory/draw_org_structure.py +8 -9
  123. runbooks/inventory/drift_detection_cli.py +69 -96
  124. runbooks/inventory/ec2_vpc_utils.py +2 -2
  125. runbooks/inventory/find_cfn_drift_detection.py +5 -7
  126. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
  127. runbooks/inventory/find_cfn_stackset_drift.py +5 -6
  128. runbooks/inventory/find_ec2_security_groups.py +48 -42
  129. runbooks/inventory/find_landingzone_versions.py +4 -6
  130. runbooks/inventory/find_vpc_flow_logs.py +7 -9
  131. runbooks/inventory/inventory_mcp_cli.py +48 -46
  132. runbooks/inventory/inventory_modules.py +103 -91
  133. runbooks/inventory/list_cfn_stacks.py +9 -10
  134. runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
  135. runbooks/inventory/list_cfn_stackset_operations.py +79 -57
  136. runbooks/inventory/list_cfn_stacksets.py +8 -10
  137. runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
  138. runbooks/inventory/list_ds_directories.py +65 -53
  139. runbooks/inventory/list_ec2_availability_zones.py +2 -4
  140. runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
  141. runbooks/inventory/list_ec2_instances.py +23 -28
  142. runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
  143. runbooks/inventory/list_elbs_load_balancers.py +22 -20
  144. runbooks/inventory/list_enis_network_interfaces.py +26 -33
  145. runbooks/inventory/list_guardduty_detectors.py +2 -4
  146. runbooks/inventory/list_iam_policies.py +2 -4
  147. runbooks/inventory/list_iam_roles.py +5 -7
  148. runbooks/inventory/list_iam_saml_providers.py +4 -6
  149. runbooks/inventory/list_lambda_functions.py +38 -38
  150. runbooks/inventory/list_org_accounts.py +6 -8
  151. runbooks/inventory/list_org_accounts_users.py +55 -44
  152. runbooks/inventory/list_rds_db_instances.py +31 -33
  153. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  154. runbooks/inventory/list_route53_hosted_zones.py +3 -5
  155. runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
  156. runbooks/inventory/list_sns_topics.py +2 -4
  157. runbooks/inventory/list_ssm_parameters.py +4 -7
  158. runbooks/inventory/list_vpc_subnets.py +2 -4
  159. runbooks/inventory/list_vpcs.py +7 -10
  160. runbooks/inventory/mcp_inventory_validator.py +554 -468
  161. runbooks/inventory/mcp_vpc_validator.py +359 -442
  162. runbooks/inventory/organizations_discovery.py +63 -55
  163. runbooks/inventory/recover_cfn_stack_ids.py +7 -8
  164. runbooks/inventory/requirements.txt +0 -1
  165. runbooks/inventory/rich_inventory_display.py +35 -34
  166. runbooks/inventory/run_on_multi_accounts.py +3 -5
  167. runbooks/inventory/unified_validation_engine.py +281 -253
  168. runbooks/inventory/verify_ec2_security_groups.py +1 -1
  169. runbooks/inventory/vpc_analyzer.py +735 -697
  170. runbooks/inventory/vpc_architecture_validator.py +293 -348
  171. runbooks/inventory/vpc_dependency_analyzer.py +384 -380
  172. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  173. runbooks/main.py +49 -34
  174. runbooks/main_final.py +91 -60
  175. runbooks/main_minimal.py +22 -10
  176. runbooks/main_optimized.py +131 -100
  177. runbooks/main_ultra_minimal.py +7 -2
  178. runbooks/mcp/__init__.py +36 -0
  179. runbooks/mcp/integration.py +679 -0
  180. runbooks/monitoring/performance_monitor.py +9 -4
  181. runbooks/operate/dynamodb_operations.py +3 -1
  182. runbooks/operate/ec2_operations.py +145 -137
  183. runbooks/operate/iam_operations.py +146 -152
  184. runbooks/operate/networking_cost_heatmap.py +29 -8
  185. runbooks/operate/rds_operations.py +223 -254
  186. runbooks/operate/s3_operations.py +107 -118
  187. runbooks/operate/vpc_operations.py +646 -616
  188. runbooks/remediation/base.py +1 -1
  189. runbooks/remediation/commons.py +10 -7
  190. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  191. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  192. runbooks/remediation/multi_account.py +24 -21
  193. runbooks/remediation/rds_snapshot_list.py +86 -60
  194. runbooks/remediation/remediation_cli.py +92 -146
  195. runbooks/remediation/universal_account_discovery.py +83 -79
  196. runbooks/remediation/workspaces_list.py +46 -41
  197. runbooks/security/__init__.py +19 -0
  198. runbooks/security/assessment_runner.py +1150 -0
  199. runbooks/security/baseline_checker.py +812 -0
  200. runbooks/security/cloudops_automation_security_validator.py +509 -535
  201. runbooks/security/compliance_automation_engine.py +17 -17
  202. runbooks/security/config/__init__.py +2 -2
  203. runbooks/security/config/compliance_config.py +50 -50
  204. runbooks/security/config_template_generator.py +63 -76
  205. runbooks/security/enterprise_security_framework.py +1 -1
  206. runbooks/security/executive_security_dashboard.py +519 -508
  207. runbooks/security/multi_account_security_controls.py +959 -1210
  208. runbooks/security/real_time_security_monitor.py +422 -444
  209. runbooks/security/security_baseline_tester.py +1 -1
  210. runbooks/security/security_cli.py +143 -112
  211. runbooks/security/test_2way_validation.py +439 -0
  212. runbooks/security/two_way_validation_framework.py +852 -0
  213. runbooks/sre/production_monitoring_framework.py +167 -177
  214. runbooks/tdd/__init__.py +15 -0
  215. runbooks/tdd/cli.py +1071 -0
  216. runbooks/utils/__init__.py +14 -17
  217. runbooks/utils/logger.py +7 -2
  218. runbooks/utils/version_validator.py +50 -47
  219. runbooks/validation/__init__.py +6 -6
  220. runbooks/validation/cli.py +9 -3
  221. runbooks/validation/comprehensive_2way_validator.py +745 -704
  222. runbooks/validation/mcp_validator.py +906 -228
  223. runbooks/validation/terraform_citations_validator.py +104 -115
  224. runbooks/validation/terraform_drift_detector.py +461 -454
  225. runbooks/vpc/README.md +617 -0
  226. runbooks/vpc/__init__.py +8 -1
  227. runbooks/vpc/analyzer.py +577 -0
  228. runbooks/vpc/cleanup_wrapper.py +476 -413
  229. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  230. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  231. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  232. runbooks/vpc/config.py +92 -97
  233. runbooks/vpc/cost_engine.py +411 -148
  234. runbooks/vpc/cost_explorer_integration.py +553 -0
  235. runbooks/vpc/cross_account_session.py +101 -106
  236. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  237. runbooks/vpc/eni_gate_validator.py +961 -0
  238. runbooks/vpc/heatmap_engine.py +185 -160
  239. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  240. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  241. runbooks/vpc/networking_wrapper.py +15 -8
  242. runbooks/vpc/pdca_remediation_planner.py +528 -0
  243. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  244. runbooks/vpc/runbooks_adapter.py +1167 -241
  245. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  246. runbooks/vpc/test_data_loader.py +358 -0
  247. runbooks/vpc/tests/conftest.py +314 -4
  248. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  249. runbooks/vpc/tests/test_cost_engine.py +0 -2
  250. runbooks/vpc/topology_generator.py +326 -0
  251. runbooks/vpc/unified_scenarios.py +1297 -1124
  252. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  253. runbooks-1.1.6.dist-info/METADATA +327 -0
  254. runbooks-1.1.6.dist-info/RECORD +489 -0
  255. runbooks/finops/README.md +0 -414
  256. runbooks/finops/accuracy_cross_validator.py +0 -647
  257. runbooks/finops/business_cases.py +0 -950
  258. runbooks/finops/dashboard_router.py +0 -922
  259. runbooks/finops/ebs_optimizer.py +0 -973
  260. runbooks/finops/embedded_mcp_validator.py +0 -1629
  261. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  262. runbooks/finops/finops_dashboard.py +0 -584
  263. runbooks/finops/finops_scenarios.py +0 -1218
  264. runbooks/finops/legacy_migration.py +0 -730
  265. runbooks/finops/multi_dashboard.py +0 -1519
  266. runbooks/finops/single_dashboard.py +0 -1113
  267. runbooks/finops/unlimited_scenarios.py +0 -393
  268. runbooks-1.1.4.dist-info/METADATA +0 -800
  269. runbooks-1.1.4.dist-info/RECORD +0 -468
  270. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
  271. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
  272. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
  273. {runbooks-1.1.4.dist-info → runbooks-1.1.6.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, print_header, print_success, print_error, print_warning,
26
- create_table, create_progress_bar, format_cost
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
- from ..common.aws_pricing import DynamicAWSPricing
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
116
  print_header("Cost Optimizer - Idle Instance Detection", "latest version")
104
-
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('ec2', region_name=region)
147
- cloudwatch_client = self.session.client('cloudwatch', region_name=region)
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
- Filters=[{'Name': 'instance-state-name', 'Values': ['running']}]
152
- )
153
-
154
- for reservation in response['Reservations']:
155
- for instance in reservation['Instances']:
156
- instance_id = instance['InstanceId']
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['Key']: tag['Value'] for tag in instance.get('Tags', [])}
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('InstanceType', 'unknown'),
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('InstanceType', 't3.micro')
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
- datapoint["Average"] for datapoint in cpu_stats["Datapoints"]
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
- "Processing idle instances...",
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(f"[yellow]DRY RUN: Would stop {instance.instance_id} "
258
- f"(${instance.estimated_monthly_cost:.2f}/month savings)[/yellow]")
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['success']:
254
+ if result["success"]:
263
255
  stopped_instances.append(instance)
264
256
  total_savings += instance.estimated_monthly_cost
265
- print_success(f"Stopped {instance.instance_id} - "
266
- f"${instance.estimated_monthly_cost:.2f}/month saved")
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
- 'total_instances_processed': len(idle_instances),
281
- 'successful_stops': len(stopped_instances),
282
- 'errors': errors,
283
- 'dry_run': dry_run,
284
- 'estimated_annual_savings': total_savings * 12
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('ec2', region_name=instance.region)
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['StoppingInstances']:
307
- instance_state[stopping_instance['InstanceId']] = stopping_instance['CurrentState']
308
-
309
- return {
310
- 'success': True,
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
- 'success': False,
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('ec2', region_name='us-east-1')
312
+ ec2_client = self.session.client("ec2", region_name="us-east-1")
332
313
  response = ec2_client.describe_regions()
333
- return [region['RegionName'] for region in response['Regions']]
314
+ return [region["RegionName"] for region in response["Regions"]]
334
315
  except Exception:
335
316
  # Fallback to common regions
336
- return [
337
- 'us-east-1', 'us-west-2', 'eu-west-1', 'eu-central-1',
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
- 't3.micro': 8.76,
349
- 't3.small': 17.52,
350
- 't3.medium': 35.04,
351
- 't3.large': 70.08,
352
- 't3.xlarge': 140.16,
353
- 't3.2xlarge': 280.32,
354
- 'm5.large': 87.60,
355
- 'm5.xlarge': 175.20,
356
- 'm5.2xlarge': 350.40,
357
- 'c5.large': 78.84,
358
- 'c5.xlarge': 157.68,
359
- 'r5.large': 116.8,
360
- 'r5.xlarge': 233.6,
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 = ', '.join([f"{k}:{v}" for k, v in list(instance.tags.items())[:2]])
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
388
  print_header("Cost Optimizer - Low Usage EBS Volume Detection", "latest version")
413
-
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('ec2', region_name=region)
455
- cloudwatch_client = self.session.client('cloudwatch', region_name=region)
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('describe_volumes')
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['Volumes']:
465
- volume_id = volume['VolumeId']
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='AWS/EBS',
471
- MetricName='VolumeReadBytes', # Changed from VolumeUsage to more standard metric
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=['Sum']
445
+ Statistics=["Sum"],
482
446
  )
483
-
447
+
484
448
  # Calculate average usage
485
- total_bytes = sum(dp['Sum'] for dp in metrics_response['Datapoints'])
486
- avg_daily_bytes = total_bytes / max(len(metrics_response['Datapoints']), 1)
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['Datapoints']:
491
-
454
+ if avg_daily_gb < 1.0 or not metrics_response["Datapoints"]:
492
455
  # Extract tags
493
- tags = {tag['Key']: tag['Value'] for tag in volume.get('Tags', [])}
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('VolumeType', 'unknown'),
499
- size_gb=volume.get('Size', 0),
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('CreateTime', '').isoformat() if volume.get('CreateTime') else None,
502
- tags=tags
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('VolumeType', 'gp3'),
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 'Throttling' not in str(e):
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
- "Processing low usage volumes...",
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(f"[yellow]DRY RUN: Would delete {volume.volume_id} "
564
- f"({volume.size_gb}GB {volume.volume_type}) - "
565
- f"${volume.estimated_monthly_cost:.2f}/month savings[/yellow]")
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['success']:
529
+ if result["success"]:
570
530
  deleted_volumes.append(volume)
571
531
  total_savings += volume.estimated_monthly_cost
572
- print_success(f"Deleted {volume.volume_id} - "
573
- f"${volume.estimated_monthly_cost:.2f}/month saved")
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
- 'total_volumes_processed': len(low_usage_volumes),
588
- 'successful_deletions': len(deleted_volumes),
589
- 'errors': errors,
590
- 'dry_run': dry_run,
591
- 'snapshots_created': create_snapshots,
592
- 'estimated_annual_savings': total_savings * 12
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('ec2', region_name=volume.region)
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['SnapshotId']
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
- 'success': True,
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
- 'success': False,
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
- 'gp3': 0.08,
650
- 'gp2': 0.10,
651
- 'io1': 0.125,
652
- 'io2': 0.125,
653
- 'st1': 0.045,
654
- 'sc1': 0.025,
655
- 'standard': 0.05
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 = ', '.join([f"{k}:{v}" for k, v in list(volume.tags.items())[:2]])
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['total_volumes_processed']))
711
- summary_table.add_row("Successfully Deleted", str(summary['successful_deletions']))
712
- summary_table.add_row("Errors", str(len(summary['errors'])))
713
- summary_table.add_row("Snapshots Created", "Yes" if summary['snapshots_created'] else "No")
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['estimated_annual_savings']))
716
- summary_table.add_row("Mode", "DRY RUN" if summary['dry_run'] else "LIVE EXECUTION")
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['errors']:
663
+
664
+ if summary["errors"]:
721
665
  print_warning(f"Encountered {len(summary['errors'])} errors:")
722
- for error in summary['errors']:
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
684
  print_header("Cost Optimizer - Unused NAT Gateway Detection", "latest version")
743
-
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('ec2', region_name=region)
785
- cloudwatch_client = self.session.client('cloudwatch', region_name=region)
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['NatGateways']:
794
- if nat_gateway['State'] == 'deleted':
727
+
728
+ for nat_gateway in response["NatGateways"]:
729
+ if nat_gateway["State"] == "deleted":
795
730
  continue
796
-
797
- nat_gateway_id = nat_gateway['NatGatewayId']
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['Key']: tag['Value'] for tag in nat_gateway.get('Tags', [])}
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('VpcId', ''),
810
- state=nat_gateway.get('State', ''),
811
- creation_date=nat_gateway.get('CreateTime', '').isoformat() if nat_gateway.get('CreateTime') else None,
812
- tags=tags
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['State'] != 'available':
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='AWS/NATGateway',
841
- MetricName='ActiveConnectionCount',
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=['Sum']
783
+ Statistics=["Sum"],
852
784
  )
853
-
854
- datapoints = metrics_response.get('Datapoints', [])
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['Sum'] for dp in datapoints)
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
- "Processing unused NAT Gateways...",
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(f"[yellow]DRY RUN: Would delete {gateway.nat_gateway_id} "
903
- f"in VPC {gateway.vpc_id} - "
904
- f"${gateway.estimated_monthly_cost:.2f}/month savings[/yellow]")
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['success']:
837
+ if result["success"]:
909
838
  deleted_gateways.append(gateway)
910
839
  total_savings += gateway.estimated_monthly_cost
911
- print_success(f"Deleted {gateway.nat_gateway_id} - "
912
- f"${gateway.estimated_monthly_cost:.2f}/month saved")
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
- 'total_nat_gateways_processed': len(unused_nat_gateways),
927
- 'successful_deletions': len(deleted_gateways),
928
- 'errors': errors,
929
- 'dry_run': dry_run,
930
- 'estimated_annual_savings': total_savings * 12
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('ec2', region_name=gateway.region)
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
- 'success': True,
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
- 'success': False,
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 = ', '.join([f"{k}:{v}" for k, v in list(gateway.tags.items())[:2]])
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['total_nat_gateways_processed']))
1019
- summary_table.add_row("Successfully Deleted", str(summary['successful_deletions']))
1020
- summary_table.add_row("Errors", str(len(summary['errors'])))
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['estimated_annual_savings']))
1023
- summary_table.add_row("Mode", "DRY RUN" if summary['dry_run'] else "LIVE EXECUTION")
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['errors']:
941
+
942
+ if summary["errors"]:
1028
943
  print_warning(f"Encountered {len(summary['errors'])} errors:")
1029
- for error in summary['errors']:
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['total_instances_processed']))
1050
- summary_table.add_row("Successfully Stopped", str(summary['successful_stops']))
1051
- summary_table.add_row("Errors", str(len(summary['errors'])))
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['estimated_annual_savings']))
1054
- summary_table.add_row("Mode", "DRY RUN" if summary['dry_run'] else "LIVE EXECUTION")
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['errors']:
969
+
970
+ if summary["errors"]:
1059
971
  print_warning(f"Encountered {len(summary['errors'])} errors:")
1060
- for error in summary['errors']:
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
- 'idle_instances_found': 0,
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
- idle_instances=found_instances,
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
- 'idle_instances_found': len(found_instances),
1119
- 'instances_stopped': len(optimization_result.stopped_instances),
1120
- 'potential_monthly_savings': optimization_result.total_potential_savings,
1121
- 'potential_annual_savings': optimization_result.execution_summary['estimated_annual_savings'],
1122
- 'dry_run': dry_run,
1123
- 'status': 'completed',
1124
- 'details': optimization_result.execution_summary
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
- region=region,
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
- 'low_usage_volumes_found': 0,
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
- 'low_usage_volumes_found': len(found_volumes),
1183
- 'volumes_deleted': len(optimization_result.deleted_volumes),
1184
- 'potential_monthly_savings': optimization_result.total_potential_savings,
1185
- 'potential_annual_savings': optimization_result.execution_summary['estimated_annual_savings'],
1186
- 'snapshots_created': create_snapshots,
1187
- 'dry_run': dry_run,
1188
- 'status': 'completed',
1189
- 'details': optimization_result.execution_summary
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
-
1100
+
1209
1101
  print_header("Comprehensive AWS Cost Optimization", "latest version")
1210
-
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['ec2_optimization'] = ec2_result
1226
- total_monthly_savings += ec2_result.get('potential_monthly_savings', 0.0)
1227
- total_annual_savings += ec2_result.get('potential_annual_savings', 0.0)
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['ec2_optimization'] = {'error': str(e)}
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['ebs_optimization'] = ebs_result
1244
- total_monthly_savings += ebs_result.get('potential_monthly_savings', 0.0)
1245
- total_annual_savings += ebs_result.get('potential_annual_savings', 0.0)
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['ebs_optimization'] = {'error': str(e)}
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('ec2_optimization', {}).get('idle_instances_found', 0)
1266
- ec2_stopped = results.get('ec2_optimization', {}).get('instances_stopped', 0)
1267
- ec2_savings = results.get('ec2_optimization', {}).get('potential_monthly_savings', 0.0)
1268
-
1269
- summary_table.add_row(
1270
- "EC2 Instances",
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('ebs_optimization', {}).get('low_usage_volumes_found', 0)
1278
- ebs_deleted = results.get('ebs_optimization', {}).get('volumes_deleted', 0)
1279
- ebs_savings = results.get('ebs_optimization', {}).get('potential_monthly_savings', 0.0)
1280
-
1281
- summary_table.add_row(
1282
- "EBS Volumes",
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
- 'total_monthly_savings': total_monthly_savings,
1305
- 'total_annual_savings': total_annual_savings,
1306
- 'ec2_optimization': results.get('ec2_optimization', {}),
1307
- 'ebs_optimization': results.get('ebs_optimization', {}),
1308
- 'dry_run': dry_run,
1309
- 'status': 'completed'
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
- region="us-east-1",
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}")