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.
- runbooks/__init__.py +1 -1
- runbooks/base.py +5 -1
- runbooks/cfat/__init__.py +8 -4
- runbooks/cfat/assessment/collectors.py +171 -14
- runbooks/cfat/assessment/compliance.py +871 -0
- runbooks/cfat/assessment/runner.py +122 -11
- runbooks/cfat/models.py +6 -2
- runbooks/common/logger.py +14 -0
- runbooks/common/rich_utils.py +451 -0
- runbooks/enterprise/__init__.py +68 -0
- runbooks/enterprise/error_handling.py +411 -0
- runbooks/enterprise/logging.py +439 -0
- runbooks/enterprise/multi_tenant.py +583 -0
- runbooks/finops/README.md +468 -241
- runbooks/finops/__init__.py +39 -3
- runbooks/finops/cli.py +83 -18
- runbooks/finops/cross_validation.py +375 -0
- runbooks/finops/dashboard_runner.py +812 -164
- runbooks/finops/enhanced_dashboard_runner.py +525 -0
- runbooks/finops/finops_dashboard.py +1892 -0
- runbooks/finops/helpers.py +485 -51
- runbooks/finops/optimizer.py +823 -0
- runbooks/finops/tests/__init__.py +19 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
- runbooks/finops/tests/run_comprehensive_tests.py +421 -0
- runbooks/finops/tests/run_tests.py +305 -0
- runbooks/finops/tests/test_finops_dashboard.py +705 -0
- runbooks/finops/tests/test_integration.py +477 -0
- runbooks/finops/tests/test_performance.py +380 -0
- runbooks/finops/tests/test_performance_benchmarks.py +500 -0
- runbooks/finops/tests/test_reference_images_validation.py +867 -0
- runbooks/finops/tests/test_single_account_features.py +715 -0
- runbooks/finops/tests/validate_test_suite.py +220 -0
- runbooks/finops/types.py +1 -1
- runbooks/hitl/enhanced_workflow_engine.py +725 -0
- runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
- runbooks/inventory/collectors/aws_comprehensive.py +442 -0
- runbooks/inventory/collectors/enterprise_scale.py +281 -0
- runbooks/inventory/core/collector.py +172 -13
- runbooks/inventory/discovery.md +1 -1
- runbooks/inventory/list_ec2_instances.py +18 -20
- runbooks/inventory/list_ssm_parameters.py +31 -3
- runbooks/inventory/organizations_discovery.py +1269 -0
- runbooks/inventory/rich_inventory_display.py +393 -0
- runbooks/inventory/run_on_multi_accounts.py +35 -19
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/vpc_flow_analyzer.py +1030 -0
- runbooks/main.py +2215 -119
- runbooks/metrics/dora_metrics_engine.py +599 -0
- runbooks/operate/__init__.py +2 -2
- runbooks/operate/base.py +122 -10
- runbooks/operate/deployment_framework.py +1032 -0
- runbooks/operate/deployment_validator.py +853 -0
- runbooks/operate/dynamodb_operations.py +10 -6
- runbooks/operate/ec2_operations.py +319 -11
- runbooks/operate/executive_dashboard.py +779 -0
- runbooks/operate/mcp_integration.py +750 -0
- runbooks/operate/nat_gateway_operations.py +1120 -0
- runbooks/operate/networking_cost_heatmap.py +685 -0
- runbooks/operate/privatelink_operations.py +940 -0
- runbooks/operate/s3_operations.py +10 -6
- runbooks/operate/vpc_endpoints.py +644 -0
- runbooks/operate/vpc_operations.py +1038 -0
- runbooks/remediation/__init__.py +2 -2
- runbooks/remediation/acm_remediation.py +1 -1
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/cloudtrail_remediation.py +1 -1
- runbooks/remediation/cognito_remediation.py +1 -1
- runbooks/remediation/dynamodb_remediation.py +1 -1
- runbooks/remediation/ec2_remediation.py +1 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
- runbooks/remediation/kms_enable_key_rotation.py +1 -1
- runbooks/remediation/kms_remediation.py +1 -1
- runbooks/remediation/lambda_remediation.py +1 -1
- runbooks/remediation/multi_account.py +1 -1
- runbooks/remediation/rds_remediation.py +1 -1
- runbooks/remediation/s3_block_public_access.py +1 -1
- runbooks/remediation/s3_enable_access_logging.py +1 -1
- runbooks/remediation/s3_encryption.py +1 -1
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/remediation/vpc_remediation.py +475 -0
- runbooks/security/__init__.py +3 -1
- runbooks/security/compliance_automation.py +632 -0
- runbooks/security/report_generator.py +10 -0
- runbooks/security/run_script.py +31 -5
- runbooks/security/security_baseline_tester.py +169 -30
- runbooks/security/security_export.py +477 -0
- runbooks/validation/__init__.py +10 -0
- runbooks/validation/benchmark.py +484 -0
- runbooks/validation/cli.py +356 -0
- runbooks/validation/mcp_validator.py +768 -0
- runbooks/vpc/__init__.py +38 -0
- runbooks/vpc/config.py +212 -0
- runbooks/vpc/cost_engine.py +347 -0
- runbooks/vpc/heatmap_engine.py +605 -0
- runbooks/vpc/manager_interface.py +634 -0
- runbooks/vpc/networking_wrapper.py +1260 -0
- runbooks/vpc/rich_formatters.py +679 -0
- runbooks/vpc/tests/__init__.py +5 -0
- runbooks/vpc/tests/conftest.py +356 -0
- runbooks/vpc/tests/test_cli_integration.py +530 -0
- runbooks/vpc/tests/test_config.py +458 -0
- runbooks/vpc/tests/test_cost_engine.py +479 -0
- runbooks/vpc/tests/test_networking_wrapper.py +512 -0
- {runbooks-0.7.6.dist-info โ runbooks-0.7.9.dist-info}/METADATA +40 -12
- {runbooks-0.7.6.dist-info โ runbooks-0.7.9.dist-info}/RECORD +111 -50
- {runbooks-0.7.6.dist-info โ runbooks-0.7.9.dist-info}/WHEEL +0 -0
- {runbooks-0.7.6.dist-info โ runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
- {runbooks-0.7.6.dist-info โ runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
- {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"]
|