runbooks 0.7.6__py3-none-any.whl โ†’ 0.7.9__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 (111) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/base.py +5 -1
  3. runbooks/cfat/__init__.py +8 -4
  4. runbooks/cfat/assessment/collectors.py +171 -14
  5. runbooks/cfat/assessment/compliance.py +871 -0
  6. runbooks/cfat/assessment/runner.py +122 -11
  7. runbooks/cfat/models.py +6 -2
  8. runbooks/common/logger.py +14 -0
  9. runbooks/common/rich_utils.py +451 -0
  10. runbooks/enterprise/__init__.py +68 -0
  11. runbooks/enterprise/error_handling.py +411 -0
  12. runbooks/enterprise/logging.py +439 -0
  13. runbooks/enterprise/multi_tenant.py +583 -0
  14. runbooks/finops/README.md +468 -241
  15. runbooks/finops/__init__.py +39 -3
  16. runbooks/finops/cli.py +83 -18
  17. runbooks/finops/cross_validation.py +375 -0
  18. runbooks/finops/dashboard_runner.py +812 -164
  19. runbooks/finops/enhanced_dashboard_runner.py +525 -0
  20. runbooks/finops/finops_dashboard.py +1892 -0
  21. runbooks/finops/helpers.py +485 -51
  22. runbooks/finops/optimizer.py +823 -0
  23. runbooks/finops/tests/__init__.py +19 -0
  24. runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
  25. runbooks/finops/tests/run_comprehensive_tests.py +421 -0
  26. runbooks/finops/tests/run_tests.py +305 -0
  27. runbooks/finops/tests/test_finops_dashboard.py +705 -0
  28. runbooks/finops/tests/test_integration.py +477 -0
  29. runbooks/finops/tests/test_performance.py +380 -0
  30. runbooks/finops/tests/test_performance_benchmarks.py +500 -0
  31. runbooks/finops/tests/test_reference_images_validation.py +867 -0
  32. runbooks/finops/tests/test_single_account_features.py +715 -0
  33. runbooks/finops/tests/validate_test_suite.py +220 -0
  34. runbooks/finops/types.py +1 -1
  35. runbooks/hitl/enhanced_workflow_engine.py +725 -0
  36. runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
  37. runbooks/inventory/collectors/aws_comprehensive.py +442 -0
  38. runbooks/inventory/collectors/enterprise_scale.py +281 -0
  39. runbooks/inventory/core/collector.py +172 -13
  40. runbooks/inventory/discovery.md +1 -1
  41. runbooks/inventory/list_ec2_instances.py +18 -20
  42. runbooks/inventory/list_ssm_parameters.py +31 -3
  43. runbooks/inventory/organizations_discovery.py +1269 -0
  44. runbooks/inventory/rich_inventory_display.py +393 -0
  45. runbooks/inventory/run_on_multi_accounts.py +35 -19
  46. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  47. runbooks/inventory/runbooks.security.run_script.log +0 -0
  48. runbooks/inventory/vpc_flow_analyzer.py +1030 -0
  49. runbooks/main.py +2215 -119
  50. runbooks/metrics/dora_metrics_engine.py +599 -0
  51. runbooks/operate/__init__.py +2 -2
  52. runbooks/operate/base.py +122 -10
  53. runbooks/operate/deployment_framework.py +1032 -0
  54. runbooks/operate/deployment_validator.py +853 -0
  55. runbooks/operate/dynamodb_operations.py +10 -6
  56. runbooks/operate/ec2_operations.py +319 -11
  57. runbooks/operate/executive_dashboard.py +779 -0
  58. runbooks/operate/mcp_integration.py +750 -0
  59. runbooks/operate/nat_gateway_operations.py +1120 -0
  60. runbooks/operate/networking_cost_heatmap.py +685 -0
  61. runbooks/operate/privatelink_operations.py +940 -0
  62. runbooks/operate/s3_operations.py +10 -6
  63. runbooks/operate/vpc_endpoints.py +644 -0
  64. runbooks/operate/vpc_operations.py +1038 -0
  65. runbooks/remediation/__init__.py +2 -2
  66. runbooks/remediation/acm_remediation.py +1 -1
  67. runbooks/remediation/base.py +1 -1
  68. runbooks/remediation/cloudtrail_remediation.py +1 -1
  69. runbooks/remediation/cognito_remediation.py +1 -1
  70. runbooks/remediation/dynamodb_remediation.py +1 -1
  71. runbooks/remediation/ec2_remediation.py +1 -1
  72. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  73. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  74. runbooks/remediation/kms_remediation.py +1 -1
  75. runbooks/remediation/lambda_remediation.py +1 -1
  76. runbooks/remediation/multi_account.py +1 -1
  77. runbooks/remediation/rds_remediation.py +1 -1
  78. runbooks/remediation/s3_block_public_access.py +1 -1
  79. runbooks/remediation/s3_enable_access_logging.py +1 -1
  80. runbooks/remediation/s3_encryption.py +1 -1
  81. runbooks/remediation/s3_remediation.py +1 -1
  82. runbooks/remediation/vpc_remediation.py +475 -0
  83. runbooks/security/__init__.py +3 -1
  84. runbooks/security/compliance_automation.py +632 -0
  85. runbooks/security/report_generator.py +10 -0
  86. runbooks/security/run_script.py +31 -5
  87. runbooks/security/security_baseline_tester.py +169 -30
  88. runbooks/security/security_export.py +477 -0
  89. runbooks/validation/__init__.py +10 -0
  90. runbooks/validation/benchmark.py +484 -0
  91. runbooks/validation/cli.py +356 -0
  92. runbooks/validation/mcp_validator.py +768 -0
  93. runbooks/vpc/__init__.py +38 -0
  94. runbooks/vpc/config.py +212 -0
  95. runbooks/vpc/cost_engine.py +347 -0
  96. runbooks/vpc/heatmap_engine.py +605 -0
  97. runbooks/vpc/manager_interface.py +634 -0
  98. runbooks/vpc/networking_wrapper.py +1260 -0
  99. runbooks/vpc/rich_formatters.py +679 -0
  100. runbooks/vpc/tests/__init__.py +5 -0
  101. runbooks/vpc/tests/conftest.py +356 -0
  102. runbooks/vpc/tests/test_cli_integration.py +530 -0
  103. runbooks/vpc/tests/test_config.py +458 -0
  104. runbooks/vpc/tests/test_cost_engine.py +479 -0
  105. runbooks/vpc/tests/test_networking_wrapper.py +512 -0
  106. {runbooks-0.7.6.dist-info โ†’ runbooks-0.7.9.dist-info}/METADATA +40 -12
  107. {runbooks-0.7.6.dist-info โ†’ runbooks-0.7.9.dist-info}/RECORD +111 -50
  108. {runbooks-0.7.6.dist-info โ†’ runbooks-0.7.9.dist-info}/WHEEL +0 -0
  109. {runbooks-0.7.6.dist-info โ†’ runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.7.6.dist-info โ†’ runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.7.6.dist-info โ†’ runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1038 @@
1
+ """
2
+ VPC Operations Module - GitHub Issue #96 TOP PRIORITY
3
+
4
+ Enterprise-grade VPC and NAT Gateway operations for multi-account AWS environments.
5
+ Addresses manager-raised VPC infrastructure automation requirements with cost optimization focus.
6
+
7
+ This module provides comprehensive VPC lifecycle management including:
8
+ - NAT Gateway operations ($45/month cost optimization)
9
+ - VPC creation and deletion with best practices
10
+ - VPC peering and cross-account connectivity
11
+ - Network security optimization
12
+ - Cost analysis and recommendations
13
+
14
+ Features:
15
+ - Multi-account support (1-200+ accounts)
16
+ - Rich CLI integration with beautiful terminal output
17
+ - Enterprise safety (dry-run, confirmation, rollback)
18
+ - Cost optimization integration
19
+ - Comprehensive error handling and logging
20
+ """
21
+
22
+ import time
23
+ from dataclasses import dataclass
24
+ from datetime import datetime
25
+ from typing import Any, Dict, List, Optional, Union
26
+
27
+ import boto3
28
+ from botocore.exceptions import ClientError
29
+ from loguru import logger
30
+
31
+ from runbooks.common.rich_utils import RichConsole
32
+ from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
33
+
34
+
35
+ @dataclass
36
+ class VPCConfiguration:
37
+ """Configuration for VPC creation with best practices."""
38
+
39
+ cidr_block: str
40
+ name: str
41
+ enable_dns_hostnames: bool = True
42
+ enable_dns_support: bool = True
43
+ instance_tenancy: str = "default"
44
+ tags: Dict[str, str] = None
45
+
46
+ def __post_init__(self):
47
+ if self.tags is None:
48
+ self.tags = {}
49
+
50
+ # Add required tags for enterprise environments
51
+ if "Environment" not in self.tags:
52
+ self.tags["Environment"] = "production"
53
+ if "CreatedBy" not in self.tags:
54
+ self.tags["CreatedBy"] = "CloudOps-Runbooks"
55
+ if "CostCenter" not in self.tags:
56
+ self.tags["CostCenter"] = "Infrastructure"
57
+
58
+
59
+ @dataclass
60
+ class NATGatewayConfiguration:
61
+ """Configuration for NAT Gateway creation and optimization."""
62
+
63
+ subnet_id: str
64
+ allocation_id: Optional[str] = None
65
+ connectivity_type: str = "public" # "public" or "private"
66
+ name: Optional[str] = None
67
+ tags: Dict[str, str] = None
68
+
69
+ def __post_init__(self):
70
+ if self.tags is None:
71
+ self.tags = {}
72
+
73
+ # Add cost tracking tags ($45/month awareness)
74
+ if "MonthlyCostEstimate" not in self.tags:
75
+ self.tags["MonthlyCostEstimate"] = "$45"
76
+ if "CostOptimizationReviewed" not in self.tags:
77
+ self.tags["CostOptimizationReviewed"] = datetime.utcnow().strftime("%Y-%m-%d")
78
+
79
+
80
+ @dataclass
81
+ class VPCPeeringConfiguration:
82
+ """Configuration for VPC peering connections."""
83
+
84
+ vpc_id: str
85
+ peer_vpc_id: str
86
+ peer_region: Optional[str] = None
87
+ peer_owner_id: Optional[str] = None
88
+ name: Optional[str] = None
89
+ tags: Dict[str, str] = None
90
+
91
+
92
+ class VPCOperations(BaseOperation):
93
+ """
94
+ Enterprise VPC & NAT Gateway Operations - GitHub Issue #96
95
+
96
+ Top priority VPC infrastructure automation addressing manager requirements:
97
+ - NAT Gateway lifecycle with $45/month cost optimization
98
+ - VPC creation/deletion with enterprise best practices
99
+ - Cross-account VPC peering and connectivity
100
+ - Network security optimization and compliance
101
+ - Multi-account operations (1-200+ accounts)
102
+ - Rich CLI integration with beautiful output
103
+ """
104
+
105
+ service_name = "ec2"
106
+ supported_operations = {
107
+ # VPC Core Operations
108
+ "create_vpc",
109
+ "delete_vpc",
110
+ "modify_vpc",
111
+ "describe_vpcs",
112
+ # NAT Gateway Operations (TOP PRIORITY - $45/month cost focus)
113
+ "create_nat_gateway",
114
+ "delete_nat_gateway",
115
+ "describe_nat_gateways",
116
+ "optimize_nat_placement",
117
+ "analyze_nat_costs",
118
+ # Elastic IP Operations (MIGRATED FROM CLOUDOPS-AUTOMATION - $3.60/month per EIP)
119
+ "discover_unused_eips",
120
+ "release_elastic_ip",
121
+ "release_all_unused_eips",
122
+ "analyze_eip_costs",
123
+ "cleanup_unused_eips",
124
+ # Subnet Operations
125
+ "create_subnet",
126
+ "delete_subnet",
127
+ "modify_subnet",
128
+ "describe_subnets",
129
+ # Gateway Operations
130
+ "create_internet_gateway",
131
+ "delete_internet_gateway",
132
+ "attach_internet_gateway",
133
+ "detach_internet_gateway",
134
+ # Route Table Operations
135
+ "create_route_table",
136
+ "delete_route_table",
137
+ "create_route",
138
+ "delete_route",
139
+ # VPC Peering Operations
140
+ "create_vpc_peering",
141
+ "accept_vpc_peering",
142
+ "delete_vpc_peering",
143
+ # Security Operations
144
+ "optimize_security_groups",
145
+ "validate_network_architecture",
146
+ # Cost Operations
147
+ "analyze_network_costs",
148
+ "recommend_cost_optimizations",
149
+ }
150
+ requires_confirmation = True # Critical for $45/month NAT Gateway operations
151
+
152
+ def __init__(self, profile: Optional[str] = None, region: Optional[str] = None, dry_run: bool = False):
153
+ """
154
+ Initialize VPC Operations with Enterprise safety features.
155
+
156
+ Args:
157
+ profile: AWS profile for authentication
158
+ region: AWS region for operations (defaults to us-east-1)
159
+ dry_run: Enable dry-run mode for safe testing
160
+ """
161
+ super().__init__(profile, region, dry_run)
162
+ self.rich_console = RichConsole()
163
+
164
+ # Cost tracking for NAT Gateways ($45/month awareness)
165
+ self.nat_gateway_monthly_cost = 45.0
166
+
167
+ # Cost tracking for Elastic IPs ($3.60/month awareness)
168
+ self.elastic_ip_monthly_cost = 3.60
169
+
170
+ logger.info(f"VPC Operations initialized - Profile: {profile}, Region: {region}, Dry-run: {dry_run}")
171
+
172
+ def execute_operation(self, context: OperationContext, operation_type: str, **kwargs) -> List[OperationResult]:
173
+ """
174
+ Execute VPC operation with comprehensive error handling and logging.
175
+
176
+ Args:
177
+ context: Operation context with account and region info
178
+ operation_type: Type of VPC operation to execute
179
+ **kwargs: Operation-specific arguments
180
+
181
+ Returns:
182
+ List of operation results
183
+
184
+ Raises:
185
+ ValueError: If operation type is not supported
186
+ ClientError: AWS API errors
187
+ """
188
+ self.validate_context(context)
189
+
190
+ logger.info(f"Executing VPC operation: {operation_type} in {context.region}")
191
+
192
+ # Route to specific operation handlers
193
+ if operation_type.startswith("create_vpc"):
194
+ return self._create_vpc(context, **kwargs)
195
+ elif operation_type.startswith("delete_vpc"):
196
+ return self._delete_vpc(context, **kwargs)
197
+ elif operation_type.startswith("create_nat_gateway"):
198
+ return self._create_nat_gateway(context, **kwargs)
199
+ elif operation_type.startswith("delete_nat_gateway"):
200
+ return self._delete_nat_gateway(context, **kwargs)
201
+ elif operation_type.startswith("describe_nat_gateways"):
202
+ return self._describe_nat_gateways(context, **kwargs)
203
+ elif operation_type.startswith("optimize_nat_placement"):
204
+ return self._optimize_nat_placement(context, **kwargs)
205
+ elif operation_type.startswith("analyze_nat_costs"):
206
+ return self._analyze_nat_costs(context, **kwargs)
207
+ elif operation_type.startswith("discover_unused_eips"):
208
+ return self._discover_unused_eips(context, **kwargs)
209
+ elif operation_type.startswith("release_elastic_ip"):
210
+ return self._release_elastic_ip(context, **kwargs)
211
+ elif operation_type.startswith("release_all_unused_eips"):
212
+ return self._release_all_unused_eips(context, **kwargs)
213
+ elif operation_type.startswith("analyze_eip_costs"):
214
+ return self._analyze_eip_costs(context, **kwargs)
215
+ elif operation_type.startswith("cleanup_unused_eips"):
216
+ return self._cleanup_unused_eips(context, **kwargs)
217
+ elif operation_type.startswith("create_vpc_peering"):
218
+ return self._create_vpc_peering(context, **kwargs)
219
+ elif operation_type.startswith("analyze_network_costs"):
220
+ return self._analyze_network_costs(context, **kwargs)
221
+ else:
222
+ raise ValueError(f"Unsupported VPC operation: {operation_type}")
223
+
224
+ def _create_vpc(self, context: OperationContext, vpc_config: VPCConfiguration) -> List[OperationResult]:
225
+ """
226
+ Create VPC with enterprise best practices.
227
+
228
+ Args:
229
+ context: Operation context
230
+ vpc_config: VPC configuration with security settings
231
+
232
+ Returns:
233
+ List containing VPC creation result
234
+ """
235
+ result = self.create_operation_result(context, "create_vpc", "vpc", f"vpc-{vpc_config.name}")
236
+
237
+ try:
238
+ ec2_client = self.get_client("ec2", context.region)
239
+
240
+ # Display cost and configuration info
241
+ self.rich_console.print_panel(
242
+ f"Creating VPC: {vpc_config.name}",
243
+ f"CIDR Block: {vpc_config.cidr_block}\n"
244
+ f"Region: {context.region}\n"
245
+ f"DNS Hostnames: {vpc_config.enable_dns_hostnames}\n"
246
+ f"Instance Tenancy: {vpc_config.instance_tenancy}",
247
+ title="๐Ÿ—๏ธ VPC Creation",
248
+ )
249
+
250
+ if context.dry_run:
251
+ result.mark_completed(OperationStatus.DRY_RUN)
252
+ result.response_data = {"message": f"[DRY-RUN] Would create VPC {vpc_config.name}"}
253
+ logger.info(f"[DRY-RUN] VPC creation simulated for {vpc_config.name}")
254
+ return [result]
255
+
256
+ # Create VPC
257
+ response = self.execute_aws_call(
258
+ ec2_client,
259
+ "create_vpc",
260
+ CidrBlock=vpc_config.cidr_block,
261
+ InstanceTenancy=vpc_config.instance_tenancy,
262
+ TagSpecifications=[
263
+ {"ResourceType": "vpc", "Tags": [{"Key": k, "Value": v} for k, v in vpc_config.tags.items()]}
264
+ ],
265
+ )
266
+
267
+ vpc_id = response["Vpc"]["VpcId"]
268
+ result.resource_id = vpc_id
269
+
270
+ # Enable DNS features
271
+ if vpc_config.enable_dns_hostnames:
272
+ self.execute_aws_call(
273
+ ec2_client, "modify_vpc_attribute", VpcId=vpc_id, EnableDnsHostnames={"Value": True}
274
+ )
275
+
276
+ if vpc_config.enable_dns_support:
277
+ self.execute_aws_call(
278
+ ec2_client, "modify_vpc_attribute", VpcId=vpc_id, EnableDnsSupport={"Value": True}
279
+ )
280
+
281
+ result.mark_completed(OperationStatus.SUCCESS)
282
+ result.response_data = {
283
+ "vpc_id": vpc_id,
284
+ "cidr_block": vpc_config.cidr_block,
285
+ "state": response["Vpc"]["State"],
286
+ }
287
+
288
+ self.rich_console.print_success(f"โœ… VPC created successfully: {vpc_id}")
289
+ logger.info(f"VPC created successfully: {vpc_id} in {context.region}")
290
+
291
+ except ClientError as e:
292
+ error_msg = f"Failed to create VPC {vpc_config.name}: {e}"
293
+ result.mark_completed(OperationStatus.FAILED, error_msg)
294
+ self.rich_console.print_error(f"โŒ {error_msg}")
295
+ logger.error(error_msg)
296
+
297
+ except Exception as e:
298
+ error_msg = f"Unexpected error creating VPC {vpc_config.name}: {e}"
299
+ result.mark_completed(OperationStatus.FAILED, error_msg)
300
+ self.rich_console.print_error(f"โŒ {error_msg}")
301
+ logger.error(error_msg)
302
+
303
+ return [result]
304
+
305
+ def _create_nat_gateway(
306
+ self, context: OperationContext, nat_config: NATGatewayConfiguration
307
+ ) -> List[OperationResult]:
308
+ """
309
+ Create NAT Gateway with cost optimization awareness.
310
+
311
+ CRITICAL: NAT Gateways cost $45/month - requires manager approval for cost impact.
312
+
313
+ Args:
314
+ context: Operation context
315
+ nat_config: NAT Gateway configuration
316
+
317
+ Returns:
318
+ List containing NAT Gateway creation result
319
+ """
320
+ result = self.create_operation_result(
321
+ context, "create_nat_gateway", "nat-gateway", f"natgw-{nat_config.name or 'unnamed'}"
322
+ )
323
+
324
+ try:
325
+ ec2_client = self.get_client("ec2", context.region)
326
+
327
+ # COST IMPACT WARNING - $45/month
328
+ self.rich_console.print_warning(
329
+ f"๐Ÿ’ฐ NAT Gateway Cost Impact: ${self.nat_gateway_monthly_cost}/month\n"
330
+ f"Annual Cost: ${self.nat_gateway_monthly_cost * 12:.0f}"
331
+ )
332
+
333
+ # Display configuration
334
+ self.rich_console.print_panel(
335
+ f"Creating NAT Gateway",
336
+ f"Subnet ID: {nat_config.subnet_id}\n"
337
+ f"Connectivity: {nat_config.connectivity_type}\n"
338
+ f"Monthly Cost: ${self.nat_gateway_monthly_cost}\n"
339
+ f"Region: {context.region}",
340
+ title="๐ŸŒ NAT Gateway Creation",
341
+ )
342
+
343
+ # Confirmation required for cost impact
344
+ if not self.confirm_operation(context, nat_config.subnet_id, "create_nat_gateway"):
345
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
346
+ return [result]
347
+
348
+ if context.dry_run:
349
+ result.mark_completed(OperationStatus.DRY_RUN)
350
+ result.response_data = {
351
+ "message": f"[DRY-RUN] Would create NAT Gateway in {nat_config.subnet_id}",
352
+ "estimated_monthly_cost": self.nat_gateway_monthly_cost,
353
+ }
354
+ logger.info(f"[DRY-RUN] NAT Gateway creation simulated for {nat_config.subnet_id}")
355
+ return [result]
356
+
357
+ # Create NAT Gateway
358
+ create_params = {"SubnetId": nat_config.subnet_id, "ConnectivityType": nat_config.connectivity_type}
359
+
360
+ # Add Elastic IP allocation if provided
361
+ if nat_config.allocation_id:
362
+ create_params["AllocationId"] = nat_config.allocation_id
363
+
364
+ # Add tags
365
+ if nat_config.tags:
366
+ create_params["TagSpecifications"] = [
367
+ {"ResourceType": "natgateway", "Tags": [{"Key": k, "Value": v} for k, v in nat_config.tags.items()]}
368
+ ]
369
+
370
+ response = self.execute_aws_call(ec2_client, "create_nat_gateway", **create_params)
371
+
372
+ nat_gateway_id = response["NatGateway"]["NatGatewayId"]
373
+ result.resource_id = nat_gateway_id
374
+
375
+ result.mark_completed(OperationStatus.SUCCESS)
376
+ result.response_data = {
377
+ "nat_gateway_id": nat_gateway_id,
378
+ "subnet_id": nat_config.subnet_id,
379
+ "state": response["NatGateway"]["State"],
380
+ "monthly_cost_estimate": self.nat_gateway_monthly_cost,
381
+ }
382
+
383
+ self.rich_console.print_success(
384
+ f"โœ… NAT Gateway created: {nat_gateway_id}\n๐Ÿ’ฐ Monthly cost: ${self.nat_gateway_monthly_cost}"
385
+ )
386
+ logger.info(f"NAT Gateway created: {nat_gateway_id} (${self.nat_gateway_monthly_cost}/month)")
387
+
388
+ except ClientError as e:
389
+ error_msg = f"Failed to create NAT Gateway: {e}"
390
+ result.mark_completed(OperationStatus.FAILED, error_msg)
391
+ self.rich_console.print_error(f"โŒ {error_msg}")
392
+ logger.error(error_msg)
393
+
394
+ return [result]
395
+
396
+ def _delete_nat_gateway(self, context: OperationContext, nat_gateway_id: str) -> List[OperationResult]:
397
+ """
398
+ Delete NAT Gateway with cost savings tracking.
399
+
400
+ Args:
401
+ context: Operation context
402
+ nat_gateway_id: NAT Gateway ID to delete
403
+
404
+ Returns:
405
+ List containing NAT Gateway deletion result
406
+ """
407
+ result = self.create_operation_result(context, "delete_nat_gateway", "nat-gateway", nat_gateway_id)
408
+
409
+ try:
410
+ ec2_client = self.get_client("ec2", context.region)
411
+
412
+ # Show cost savings from deletion
413
+ self.rich_console.print_panel(
414
+ f"Deleting NAT Gateway: {nat_gateway_id}",
415
+ f"๐Ÿ’ฐ Monthly Savings: ${self.nat_gateway_monthly_cost}\n"
416
+ f"Annual Savings: ${self.nat_gateway_monthly_cost * 12:.0f}\n"
417
+ f"Region: {context.region}",
418
+ title="๐Ÿ—‘๏ธ NAT Gateway Deletion",
419
+ )
420
+
421
+ if context.dry_run:
422
+ result.mark_completed(OperationStatus.DRY_RUN)
423
+ result.response_data = {
424
+ "message": f"[DRY-RUN] Would delete NAT Gateway {nat_gateway_id}",
425
+ "monthly_savings": self.nat_gateway_monthly_cost,
426
+ }
427
+ return [result]
428
+
429
+ # Delete NAT Gateway
430
+ response = self.execute_aws_call(ec2_client, "delete_nat_gateway", NatGatewayId=nat_gateway_id)
431
+
432
+ result.mark_completed(OperationStatus.SUCCESS)
433
+ result.response_data = {"nat_gateway_id": nat_gateway_id, "monthly_savings": self.nat_gateway_monthly_cost}
434
+
435
+ self.rich_console.print_success(
436
+ f"โœ… NAT Gateway deletion initiated: {nat_gateway_id}\n"
437
+ f"๐Ÿ’ฐ Monthly savings: ${self.nat_gateway_monthly_cost}"
438
+ )
439
+ logger.info(f"NAT Gateway deleted: {nat_gateway_id} (saving ${self.nat_gateway_monthly_cost}/month)")
440
+
441
+ except ClientError as e:
442
+ error_msg = f"Failed to delete NAT Gateway {nat_gateway_id}: {e}"
443
+ result.mark_completed(OperationStatus.FAILED, error_msg)
444
+ self.rich_console.print_error(f"โŒ {error_msg}")
445
+ logger.error(error_msg)
446
+
447
+ return [result]
448
+
449
+ def _describe_nat_gateways(self, context: OperationContext, vpc_id: Optional[str] = None) -> List[OperationResult]:
450
+ """
451
+ Describe NAT Gateways with cost analysis.
452
+
453
+ Args:
454
+ context: Operation context
455
+ vpc_id: Optional VPC ID filter
456
+
457
+ Returns:
458
+ List containing NAT Gateway description result
459
+ """
460
+ result = self.create_operation_result(context, "describe_nat_gateways", "nat-gateway", vpc_id or "all")
461
+
462
+ try:
463
+ ec2_client = self.get_client("ec2", context.region)
464
+
465
+ # Build filters
466
+ filters = []
467
+ if vpc_id:
468
+ filters.append({"Name": "vpc-id", "Values": [vpc_id]})
469
+
470
+ describe_params = {}
471
+ if filters:
472
+ describe_params["Filters"] = filters
473
+
474
+ response = self.execute_aws_call(ec2_client, "describe_nat_gateways", **describe_params)
475
+
476
+ nat_gateways = response.get("NatGateways", [])
477
+ total_monthly_cost = len(nat_gateways) * self.nat_gateway_monthly_cost
478
+
479
+ # Display NAT Gateway inventory with Rich formatting
480
+ if nat_gateways:
481
+ nat_data = []
482
+ for nat in nat_gateways:
483
+ nat_data.append(
484
+ [
485
+ nat["NatGatewayId"],
486
+ nat["VpcId"],
487
+ nat["SubnetId"],
488
+ nat["State"],
489
+ f"${self.nat_gateway_monthly_cost}/month",
490
+ ]
491
+ )
492
+
493
+ self.rich_console.print_table(
494
+ nat_data,
495
+ headers=["NAT Gateway ID", "VPC ID", "Subnet ID", "State", "Monthly Cost"],
496
+ title=f"๐ŸŒ NAT Gateways ({len(nat_gateways)} found)",
497
+ )
498
+
499
+ self.rich_console.print_info(
500
+ f"๐Ÿ’ฐ Total Monthly Cost: ${total_monthly_cost:.0f} (Annual: ${total_monthly_cost * 12:.0f})"
501
+ )
502
+ else:
503
+ self.rich_console.print_info("No NAT Gateways found")
504
+
505
+ result.mark_completed(OperationStatus.SUCCESS)
506
+ result.response_data = {
507
+ "nat_gateways": nat_gateways,
508
+ "count": len(nat_gateways),
509
+ "total_monthly_cost": total_monthly_cost,
510
+ }
511
+
512
+ except ClientError as e:
513
+ error_msg = f"Failed to describe NAT Gateways: {e}"
514
+ result.mark_completed(OperationStatus.FAILED, error_msg)
515
+ self.rich_console.print_error(f"โŒ {error_msg}")
516
+ logger.error(error_msg)
517
+
518
+ return [result]
519
+
520
+ def _analyze_nat_costs(self, context: OperationContext, vpc_id: Optional[str] = None) -> List[OperationResult]:
521
+ """
522
+ Analyze NAT Gateway costs and optimization opportunities.
523
+
524
+ Args:
525
+ context: Operation context
526
+ vpc_id: Optional VPC ID filter
527
+
528
+ Returns:
529
+ List containing cost analysis result
530
+ """
531
+ result = self.create_operation_result(context, "analyze_nat_costs", "nat-gateway", vpc_id or "all")
532
+
533
+ try:
534
+ # Get NAT Gateways
535
+ nat_results = self._describe_nat_gateways(context, vpc_id)
536
+ nat_data = nat_results[0].response_data
537
+
538
+ nat_gateways = nat_data["nat_gateways"]
539
+ total_cost = nat_data["total_monthly_cost"]
540
+
541
+ # Analyze optimization opportunities
542
+ recommendations = []
543
+
544
+ if len(nat_gateways) > 1:
545
+ potential_savings = (len(nat_gateways) - 1) * self.nat_gateway_monthly_cost
546
+ recommendations.append(
547
+ {
548
+ "type": "consolidation",
549
+ "description": f"Consider consolidating {len(nat_gateways)} NAT Gateways",
550
+ "potential_monthly_savings": potential_savings,
551
+ "potential_annual_savings": potential_savings * 12,
552
+ }
553
+ )
554
+
555
+ # Check for unused NAT Gateways (simplified heuristic)
556
+ unused_gateways = [ng for ng in nat_gateways if ng["State"] == "available"]
557
+ if unused_gateways:
558
+ unused_cost = len(unused_gateways) * self.nat_gateway_monthly_cost
559
+ recommendations.append(
560
+ {
561
+ "type": "unused_resources",
562
+ "description": f"Found {len(unused_gateways)} potentially unused NAT Gateways",
563
+ "potential_monthly_savings": unused_cost,
564
+ "potential_annual_savings": unused_cost * 12,
565
+ }
566
+ )
567
+
568
+ # Display cost analysis
569
+ self.rich_console.print_panel(
570
+ "NAT Gateway Cost Analysis",
571
+ f"Total NAT Gateways: {len(nat_gateways)}\n"
572
+ f"Current Monthly Cost: ${total_cost:.0f}\n"
573
+ f"Current Annual Cost: ${total_cost * 12:.0f}",
574
+ title="๐Ÿ’ฐ Cost Analysis",
575
+ )
576
+
577
+ if recommendations:
578
+ self.rich_console.print_warning("๐Ÿ’ก Cost Optimization Opportunities:")
579
+ for i, rec in enumerate(recommendations, 1):
580
+ self.rich_console.print_info(
581
+ f"{i}. {rec['description']}\n"
582
+ f" Monthly Savings: ${rec['potential_monthly_savings']:.0f}\n"
583
+ f" Annual Savings: ${rec['potential_annual_savings']:.0f}"
584
+ )
585
+ else:
586
+ self.rich_console.print_success("โœ… NAT Gateway configuration appears optimized")
587
+
588
+ result.mark_completed(OperationStatus.SUCCESS)
589
+ result.response_data = {
590
+ "total_nat_gateways": len(nat_gateways),
591
+ "current_monthly_cost": total_cost,
592
+ "current_annual_cost": total_cost * 12,
593
+ "optimization_recommendations": recommendations,
594
+ }
595
+
596
+ except Exception as e:
597
+ error_msg = f"Failed to analyze NAT costs: {e}"
598
+ result.mark_completed(OperationStatus.FAILED, error_msg)
599
+ self.rich_console.print_error(f"โŒ {error_msg}")
600
+ logger.error(error_msg)
601
+
602
+ return [result]
603
+
604
+ def _create_vpc_peering(
605
+ self, context: OperationContext, peering_config: VPCPeeringConfiguration
606
+ ) -> List[OperationResult]:
607
+ """
608
+ Create VPC peering connection for cross-VPC connectivity.
609
+
610
+ Args:
611
+ context: Operation context
612
+ peering_config: VPC peering configuration
613
+
614
+ Returns:
615
+ List containing VPC peering creation result
616
+ """
617
+ result = self.create_operation_result(
618
+ context, "create_vpc_peering", "vpc-peering", f"{peering_config.vpc_id}-{peering_config.peer_vpc_id}"
619
+ )
620
+
621
+ try:
622
+ ec2_client = self.get_client("ec2", context.region)
623
+
624
+ self.rich_console.print_panel(
625
+ "Creating VPC Peering Connection",
626
+ f"Source VPC: {peering_config.vpc_id}\n"
627
+ f"Peer VPC: {peering_config.peer_vpc_id}\n"
628
+ f"Peer Region: {peering_config.peer_region or 'Same region'}\n"
629
+ f"Peer Account: {peering_config.peer_owner_id or 'Same account'}",
630
+ title="๐Ÿ”— VPC Peering",
631
+ )
632
+
633
+ if context.dry_run:
634
+ result.mark_completed(OperationStatus.DRY_RUN)
635
+ result.response_data = {
636
+ "message": f"[DRY-RUN] Would create peering between {peering_config.vpc_id} and {peering_config.peer_vpc_id}"
637
+ }
638
+ return [result]
639
+
640
+ # Create peering connection
641
+ create_params = {"VpcId": peering_config.vpc_id, "PeerVpcId": peering_config.peer_vpc_id}
642
+
643
+ if peering_config.peer_region:
644
+ create_params["PeerRegion"] = peering_config.peer_region
645
+ if peering_config.peer_owner_id:
646
+ create_params["PeerOwnerId"] = peering_config.peer_owner_id
647
+ if peering_config.tags:
648
+ create_params["TagSpecifications"] = [
649
+ {
650
+ "ResourceType": "vpc-peering-connection",
651
+ "Tags": [{"Key": k, "Value": v} for k, v in peering_config.tags.items()],
652
+ }
653
+ ]
654
+
655
+ response = self.execute_aws_call(ec2_client, "create_vpc_peering_connection", **create_params)
656
+
657
+ peering_id = response["VpcPeeringConnection"]["VpcPeeringConnectionId"]
658
+ result.resource_id = peering_id
659
+
660
+ result.mark_completed(OperationStatus.SUCCESS)
661
+ result.response_data = {
662
+ "peering_connection_id": peering_id,
663
+ "requester_vpc_id": peering_config.vpc_id,
664
+ "accepter_vpc_id": peering_config.peer_vpc_id,
665
+ "status": response["VpcPeeringConnection"]["Status"]["Code"],
666
+ }
667
+
668
+ self.rich_console.print_success(f"โœ… VPC Peering Connection created: {peering_id}")
669
+ logger.info(f"VPC Peering created: {peering_id}")
670
+
671
+ except ClientError as e:
672
+ error_msg = f"Failed to create VPC peering: {e}"
673
+ result.mark_completed(OperationStatus.FAILED, error_msg)
674
+ self.rich_console.print_error(f"โŒ {error_msg}")
675
+ logger.error(error_msg)
676
+
677
+ return [result]
678
+
679
+ def _analyze_network_costs(self, context: OperationContext, vpc_id: Optional[str] = None) -> List[OperationResult]:
680
+ """
681
+ Comprehensive network cost analysis for VPC infrastructure.
682
+
683
+ Args:
684
+ context: Operation context
685
+ vpc_id: Optional VPC ID filter
686
+
687
+ Returns:
688
+ List containing network cost analysis result
689
+ """
690
+ result = self.create_operation_result(context, "analyze_network_costs", "vpc", vpc_id or "all")
691
+
692
+ try:
693
+ # Get NAT Gateway cost analysis
694
+ nat_results = self._analyze_nat_costs(context, vpc_id)
695
+ nat_data = nat_results[0].response_data
696
+
697
+ total_analysis = {
698
+ "nat_gateway_costs": {
699
+ "monthly": nat_data["current_monthly_cost"],
700
+ "annual": nat_data["current_annual_cost"],
701
+ },
702
+ "optimization_opportunities": nat_data["optimization_recommendations"],
703
+ "total_monthly_network_costs": nat_data["current_monthly_cost"], # Expandable for other network costs
704
+ "total_annual_network_costs": nat_data["current_annual_cost"],
705
+ }
706
+
707
+ # Display comprehensive cost analysis
708
+ self.rich_console.print_panel(
709
+ "Comprehensive Network Cost Analysis",
710
+ f"NAT Gateway Monthly Cost: ${total_analysis['nat_gateway_costs']['monthly']:.0f}\n"
711
+ f"NAT Gateway Annual Cost: ${total_analysis['nat_gateway_costs']['annual']:.0f}\n"
712
+ f"Optimization Opportunities: {len(total_analysis['optimization_opportunities'])}",
713
+ title="๐ŸŒ Network Infrastructure Costs",
714
+ )
715
+
716
+ result.mark_completed(OperationStatus.SUCCESS)
717
+ result.response_data = total_analysis
718
+
719
+ except Exception as e:
720
+ error_msg = f"Failed to analyze network costs: {e}"
721
+ result.mark_completed(OperationStatus.FAILED, error_msg)
722
+ self.rich_console.print_error(f"โŒ {error_msg}")
723
+ logger.error(error_msg)
724
+
725
+ return [result]
726
+
727
+ def _discover_unused_eips(
728
+ self, context: OperationContext, target_region: Optional[str] = None
729
+ ) -> List[OperationResult]:
730
+ """
731
+ MIGRATED FROM CLOUDOPS-AUTOMATION: Discover unused Elastic IPs across regions.
732
+
733
+ Implements the production-tested aws_list_unattached_elastic_ips() function
734
+ with enterprise enhancements for cost analysis and business impact.
735
+
736
+ Args:
737
+ context: Operation context
738
+ target_region: Optional single region filter
739
+
740
+ Returns:
741
+ List containing unused Elastic IP discovery result
742
+ """
743
+ result = self.create_operation_result(context, "discover_unused_eips", "elastic-ip", target_region or "all")
744
+
745
+ try:
746
+ # Get all regions for analysis
747
+ regions_to_scan = [target_region] if target_region else self._get_all_regions(context.region)
748
+
749
+ self.rich_console.print_panel(
750
+ "Discovering Unused Elastic IPs",
751
+ f"Regions to scan: {len(regions_to_scan)}\n"
752
+ f"Cost per unused EIP: ${self.elastic_ip_monthly_cost}/month\n"
753
+ f"Analysis scope: Multi-region comprehensive scan",
754
+ title="๐Ÿ” EIP Discovery",
755
+ )
756
+
757
+ unused_eips = []
758
+ total_regions_with_unused = 0
759
+
760
+ for region in regions_to_scan:
761
+ try:
762
+ ec2_client = self.get_client("ec2", region)
763
+
764
+ # CORE LOGIC FROM CLOUDOPS-AUTOMATION: describe_addresses()
765
+ all_eips = self.execute_aws_call(ec2_client, "describe_addresses")
766
+
767
+ region_unused_count = 0
768
+ for eip in all_eips["Addresses"]:
769
+ # CLOUDOPS-AUTOMATION LOGIC: No AssociationId means unused
770
+ if "AssociationId" not in eip:
771
+ eip_data = {
772
+ "public_ip": eip["PublicIp"],
773
+ "allocation_id": eip["AllocationId"],
774
+ "region": region,
775
+ "domain": eip.get("Domain", "standard"),
776
+ "monthly_cost": self.elastic_ip_monthly_cost,
777
+ "annual_cost": self.elastic_ip_monthly_cost * 12,
778
+ "tags": eip.get("Tags", []),
779
+ "instance_id": eip.get("InstanceId", "None"),
780
+ "network_interface_id": eip.get("NetworkInterfaceId", "None"),
781
+ }
782
+ unused_eips.append(eip_data)
783
+ region_unused_count += 1
784
+
785
+ if region_unused_count > 0:
786
+ total_regions_with_unused += 1
787
+ logger.info(f"Found {region_unused_count} unused EIPs in {region}")
788
+
789
+ except ClientError as e:
790
+ logger.warning(f"Could not scan region {region}: {e}")
791
+ continue
792
+
793
+ # Calculate business impact
794
+ total_monthly_cost = len(unused_eips) * self.elastic_ip_monthly_cost
795
+ total_annual_cost = total_monthly_cost * 12
796
+
797
+ # Display results with Rich formatting
798
+ if unused_eips:
799
+ # Show summary table
800
+ eip_data_for_display = []
801
+ for eip in unused_eips[:10]: # Show first 10
802
+ eip_data_for_display.append(
803
+ [
804
+ eip["public_ip"],
805
+ eip["allocation_id"],
806
+ eip["region"],
807
+ eip["domain"],
808
+ f"${eip['monthly_cost']:.2f}",
809
+ ]
810
+ )
811
+
812
+ self.rich_console.print_table(
813
+ eip_data_for_display,
814
+ headers=["Public IP", "Allocation ID", "Region", "Domain", "Monthly Cost"],
815
+ title=f"๐Ÿ” Unused Elastic IPs ({len(unused_eips)} found)",
816
+ )
817
+
818
+ if len(unused_eips) > 10:
819
+ self.rich_console.print_info(f"... and {len(unused_eips) - 10} more unused EIPs")
820
+
821
+ # Cost impact summary
822
+ self.rich_console.print_panel(
823
+ "Cost Impact Analysis",
824
+ f"Total unused EIPs: {len(unused_eips)}\n"
825
+ f"Monthly cost waste: ${total_monthly_cost:.2f}\n"
826
+ f"Annual savings opportunity: ${total_annual_cost:.2f}\n"
827
+ f"Regions affected: {total_regions_with_unused}",
828
+ title="๐Ÿ’ฐ Business Impact",
829
+ )
830
+ else:
831
+ self.rich_console.print_success("โœ… No unused Elastic IPs found - excellent optimization!")
832
+
833
+ result.mark_completed(OperationStatus.SUCCESS)
834
+ result.response_data = {
835
+ "unused_eips": unused_eips,
836
+ "total_unused": len(unused_eips),
837
+ "total_monthly_cost": total_monthly_cost,
838
+ "total_annual_cost": total_annual_cost,
839
+ "regions_scanned": len(regions_to_scan),
840
+ "regions_with_unused": total_regions_with_unused,
841
+ }
842
+
843
+ except Exception as e:
844
+ error_msg = f"Failed to discover unused Elastic IPs: {e}"
845
+ result.mark_completed(OperationStatus.FAILED, error_msg)
846
+ self.rich_console.print_error(f"โŒ {error_msg}")
847
+ logger.error(error_msg)
848
+
849
+ return [result]
850
+
851
+ def _release_elastic_ip(
852
+ self, context: OperationContext, allocation_id: str, target_region: Optional[str] = None
853
+ ) -> List[OperationResult]:
854
+ """
855
+ MIGRATED FROM CLOUDOPS-AUTOMATION: Release specific Elastic IP.
856
+
857
+ Implements the production-tested aws_release_elastic_ip() function
858
+ with enterprise safety controls and cost tracking.
859
+
860
+ Args:
861
+ context: Operation context
862
+ allocation_id: Allocation ID of EIP to release
863
+ target_region: Region containing the EIP
864
+
865
+ Returns:
866
+ List containing Elastic IP release result
867
+ """
868
+ result = self.create_operation_result(context, "release_elastic_ip", "elastic-ip", allocation_id)
869
+
870
+ try:
871
+ region = target_region or context.region
872
+ ec2_client = self.get_client("ec2", region)
873
+
874
+ # Show cost savings from release
875
+ self.rich_console.print_panel(
876
+ f"Releasing Elastic IP: {allocation_id}",
877
+ f"๐Ÿ’ฐ Monthly Savings: ${self.elastic_ip_monthly_cost}/month\n"
878
+ f"Annual Savings: ${self.elastic_ip_monthly_cost * 12:.0f}\n"
879
+ f"Region: {region}",
880
+ title="๐Ÿ—‘๏ธ EIP Release",
881
+ )
882
+
883
+ # Safety confirmation for production operations
884
+ if not context.dry_run:
885
+ if not self.confirm_operation(context, allocation_id, "release_elastic_ip"):
886
+ result.mark_completed(OperationStatus.CANCELLED, "Operation cancelled by user")
887
+ return [result]
888
+
889
+ if context.dry_run:
890
+ result.mark_completed(OperationStatus.DRY_RUN)
891
+ result.response_data = {
892
+ "message": f"[DRY-RUN] Would release EIP {allocation_id}",
893
+ "allocation_id": allocation_id,
894
+ "region": region,
895
+ "monthly_savings": self.elastic_ip_monthly_cost,
896
+ "annual_savings": self.elastic_ip_monthly_cost * 12,
897
+ }
898
+ return [result]
899
+
900
+ # CORE CLOUDOPS-AUTOMATION LOGIC: release_address()
901
+ response = self.execute_aws_call(ec2_client, "release_address", AllocationId=allocation_id)
902
+
903
+ result.mark_completed(OperationStatus.SUCCESS)
904
+ result.response_data = {
905
+ "allocation_id": allocation_id,
906
+ "region": region,
907
+ "monthly_savings": self.elastic_ip_monthly_cost,
908
+ "annual_savings": self.elastic_ip_monthly_cost * 12,
909
+ "aws_response": response,
910
+ }
911
+
912
+ self.rich_console.print_success(
913
+ f"โœ… Elastic IP released: {allocation_id}\n๐Ÿ’ฐ Monthly savings: ${self.elastic_ip_monthly_cost}"
914
+ )
915
+ logger.info(f"Elastic IP released: {allocation_id} (saving ${self.elastic_ip_monthly_cost}/month)")
916
+
917
+ except ClientError as e:
918
+ error_msg = f"Failed to release Elastic IP {allocation_id}: {e}"
919
+ result.mark_completed(OperationStatus.FAILED, error_msg)
920
+ self.rich_console.print_error(f"โŒ {error_msg}")
921
+ logger.error(error_msg)
922
+
923
+ return [result]
924
+
925
+ def _cleanup_unused_eips(
926
+ self, context: OperationContext, target_region: Optional[str] = None
927
+ ) -> List[OperationResult]:
928
+ """
929
+ Comprehensive cleanup of unused Elastic IPs (discover + release).
930
+
931
+ This is the main CLI command for EIP optimization, combining discovery
932
+ and release operations with comprehensive safety controls.
933
+
934
+ Args:
935
+ context: Operation context
936
+ target_region: Optional single region filter
937
+
938
+ Returns:
939
+ List containing cleanup operation result
940
+ """
941
+ result = self.create_operation_result(context, "cleanup_unused_eips", "elastic-ip", "comprehensive")
942
+
943
+ try:
944
+ self.rich_console.print_panel(
945
+ "Elastic IP Cleanup Operation",
946
+ f"Scope: {'Single region (' + target_region + ')' if target_region else 'Multi-region'}\n"
947
+ f"Safety Mode: {'DRY RUN' if context.dry_run else 'PRODUCTION'}\n"
948
+ f"Cost per EIP: ${self.elastic_ip_monthly_cost}/month",
949
+ title="๐Ÿงน EIP Cleanup",
950
+ )
951
+
952
+ # Step 1: Discover unused EIPs
953
+ self.rich_console.print_info("Step 1: Discovering unused Elastic IPs...")
954
+ discovery_results = self._discover_unused_eips(context, target_region)
955
+
956
+ if discovery_results[0].status == OperationStatus.FAILED:
957
+ result.mark_completed(OperationStatus.FAILED, "Failed to discover unused EIPs")
958
+ return [result]
959
+
960
+ unused_eips = discovery_results[0].response_data.get("unused_eips", [])
961
+
962
+ if not unused_eips:
963
+ self.rich_console.print_success("โœ… Cleanup complete - no unused EIPs found!")
964
+ result.mark_completed(OperationStatus.SUCCESS)
965
+ result.response_data = {"message": "No cleanup needed", "total_savings": 0}
966
+ return [result]
967
+
968
+ # Step 2: Batch release with safety controls
969
+ total_monthly_savings = len(unused_eips) * self.elastic_ip_monthly_cost
970
+
971
+ if not context.dry_run:
972
+ self.rich_console.print_warning(
973
+ f"โš ๏ธ BATCH RELEASE OPERATION\n"
974
+ f"EIPs to release: {len(unused_eips)}\n"
975
+ f"Monthly savings: ${total_monthly_savings:.2f}\n"
976
+ f"This action cannot be easily undone!"
977
+ )
978
+
979
+ if not self.confirm_operation(context, f"{len(unused_eips)} EIPs", "batch_cleanup"):
980
+ result.mark_completed(OperationStatus.CANCELLED, "Cleanup cancelled by user")
981
+ return [result]
982
+
983
+ # Process each EIP release
984
+ successful_releases = 0
985
+ failed_releases = 0
986
+ total_savings = 0
987
+
988
+ for eip in unused_eips:
989
+ try:
990
+ release_results = self._release_elastic_ip(context, eip["allocation_id"], eip["region"])
991
+
992
+ if release_results[0].status in [OperationStatus.SUCCESS, OperationStatus.DRY_RUN]:
993
+ successful_releases += 1
994
+ total_savings += eip["monthly_cost"]
995
+ else:
996
+ failed_releases += 1
997
+
998
+ except Exception as e:
999
+ logger.error(f"Failed to release EIP {eip['allocation_id']}: {e}")
1000
+ failed_releases += 1
1001
+
1002
+ # Summary
1003
+ self.rich_console.print_panel(
1004
+ "Cleanup Operation Summary",
1005
+ f"Successful releases: {successful_releases}\n"
1006
+ f"Failed releases: {failed_releases}\n"
1007
+ f"Total monthly savings: ${total_savings:.2f}\n"
1008
+ f"Annual savings impact: ${total_savings * 12:.0f}",
1009
+ title="๐ŸŽ‰ Cleanup Complete",
1010
+ )
1011
+
1012
+ result.mark_completed(OperationStatus.SUCCESS)
1013
+ result.response_data = {
1014
+ "discovery": discovery_results[0].response_data,
1015
+ "total_processed": len(unused_eips),
1016
+ "successful_releases": successful_releases,
1017
+ "failed_releases": failed_releases,
1018
+ "total_monthly_savings": total_savings,
1019
+ "total_annual_savings": total_savings * 12,
1020
+ }
1021
+
1022
+ except Exception as e:
1023
+ error_msg = f"Failed EIP cleanup operation: {e}"
1024
+ result.mark_completed(OperationStatus.FAILED, error_msg)
1025
+ self.rich_console.print_error(f"โŒ {error_msg}")
1026
+ logger.error(error_msg)
1027
+
1028
+ return [result]
1029
+
1030
+ def _get_all_regions(self, default_region: str) -> List[str]:
1031
+ """Get all AWS regions for comprehensive analysis"""
1032
+ try:
1033
+ ec2_client = self.get_client("ec2", default_region)
1034
+ response = self.execute_aws_call(ec2_client, "describe_regions")
1035
+ return [region["RegionName"] for region in response["Regions"]]
1036
+ except Exception as e:
1037
+ logger.warning(f"Could not get all regions, using defaults: {e}")
1038
+ return ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"]