runbooks 0.9.6__py3-none-any.whl → 0.9.8__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/_platform/__init__.py +19 -0
- runbooks/_platform/core/runbooks_wrapper.py +478 -0
- runbooks/cloudops/cost_optimizer.py +330 -0
- runbooks/cloudops/interfaces.py +3 -3
- runbooks/common/mcp_integration.py +174 -0
- runbooks/common/performance_monitor.py +4 -4
- runbooks/enterprise/__init__.py +18 -10
- runbooks/enterprise/security.py +708 -0
- runbooks/finops/README.md +1 -1
- runbooks/finops/automation_core.py +643 -0
- runbooks/finops/business_cases.py +414 -16
- runbooks/finops/cli.py +23 -0
- runbooks/finops/compute_cost_optimizer.py +865 -0
- runbooks/finops/ebs_cost_optimizer.py +718 -0
- runbooks/finops/ebs_optimizer.py +909 -0
- runbooks/finops/elastic_ip_optimizer.py +675 -0
- runbooks/finops/embedded_mcp_validator.py +330 -14
- runbooks/finops/enhanced_dashboard_runner.py +2 -1
- runbooks/finops/enterprise_wrappers.py +827 -0
- runbooks/finops/finops_dashboard.py +322 -11
- runbooks/finops/legacy_migration.py +730 -0
- runbooks/finops/nat_gateway_optimizer.py +1160 -0
- runbooks/finops/network_cost_optimizer.py +1387 -0
- runbooks/finops/notebook_utils.py +596 -0
- runbooks/finops/reservation_optimizer.py +956 -0
- runbooks/finops/single_dashboard.py +16 -16
- runbooks/finops/validation_framework.py +753 -0
- runbooks/finops/vpc_cleanup_optimizer.py +817 -0
- runbooks/finops/workspaces_analyzer.py +1 -1
- runbooks/inventory/__init__.py +7 -0
- runbooks/inventory/collectors/aws_networking.py +357 -6
- runbooks/inventory/mcp_vpc_validator.py +1091 -0
- runbooks/inventory/vpc_analyzer.py +1107 -0
- runbooks/inventory/vpc_architecture_validator.py +939 -0
- runbooks/inventory/vpc_dependency_analyzer.py +845 -0
- runbooks/main.py +487 -40
- runbooks/operate/vpc_operations.py +1485 -16
- runbooks/remediation/commvault_ec2_analysis.py +1 -1
- runbooks/remediation/dynamodb_optimize.py +2 -2
- runbooks/remediation/rds_instance_list.py +1 -1
- runbooks/remediation/rds_snapshot_list.py +1 -1
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/security/compliance_automation.py +2 -2
- runbooks/vpc/__init__.py +12 -0
- runbooks/vpc/cleanup_wrapper.py +757 -0
- runbooks/vpc/cost_engine.py +527 -3
- runbooks/vpc/networking_wrapper.py +29 -29
- runbooks/vpc/runbooks_adapter.py +479 -0
- runbooks/vpc/tests/test_config.py +2 -2
- runbooks/vpc/vpc_cleanup_integration.py +2629 -0
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/METADATA +1 -1
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/RECORD +57 -34
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/WHEEL +0 -0
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1387 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Network Cost Optimization Engine - Enterprise FinOps Network Analysis Platform
|
4
|
+
Strategic Business Focus: Multi-service network cost optimization for Manager, Financial, and CTO stakeholders
|
5
|
+
|
6
|
+
Strategic Achievement: Consolidation of 5+ network optimization notebooks targeting $2.4M-$7.3M annual savings
|
7
|
+
Business Impact: Comprehensive network cost analysis with NAT Gateway, Elastic IP, Load Balancer, and VPC optimization
|
8
|
+
Technical Foundation: Enterprise-grade network topology analysis with CloudWatch metrics integration
|
9
|
+
|
10
|
+
This module provides comprehensive network cost optimization analysis following proven FinOps patterns:
|
11
|
+
- Multi-region NAT Gateway discovery and usage analysis with CloudWatch metrics
|
12
|
+
- Elastic IP resource efficiency analysis with DNS dependency checking
|
13
|
+
- Load Balancer cost optimization (ALB, NLB, CLB) with traffic analysis
|
14
|
+
- VPC Transit Gateway cost optimization for inter-VPC connectivity
|
15
|
+
- Data transfer cost analysis and optimization recommendations
|
16
|
+
- Cross-AZ and cross-region data transfer optimization strategies
|
17
|
+
|
18
|
+
Strategic Alignment:
|
19
|
+
- "Do one thing and do it well": Network cost optimization specialization across all network services
|
20
|
+
- "Move Fast, But Not So Fast We Crash": Safety-first analysis with dependency mapping
|
21
|
+
- Enterprise FAANG SDLC: Evidence-based network optimization with comprehensive audit trails
|
22
|
+
- Universal $132K Cost Optimization Methodology: Manager scenarios prioritized over generic patterns
|
23
|
+
"""
|
24
|
+
|
25
|
+
import asyncio
|
26
|
+
import logging
|
27
|
+
import time
|
28
|
+
from datetime import datetime, timedelta
|
29
|
+
from typing import Any, Dict, List, Optional, Tuple
|
30
|
+
from dataclasses import dataclass
|
31
|
+
from enum import Enum
|
32
|
+
|
33
|
+
import boto3
|
34
|
+
import click
|
35
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
36
|
+
from pydantic import BaseModel, Field
|
37
|
+
|
38
|
+
from ..common.rich_utils import (
|
39
|
+
console, print_header, print_success, print_error, print_warning, print_info,
|
40
|
+
create_table, create_progress_bar, format_cost, create_panel, STATUS_INDICATORS
|
41
|
+
)
|
42
|
+
from .embedded_mcp_validator import EmbeddedMCPValidator
|
43
|
+
from ..common.profile_utils import get_profile_for_operation
|
44
|
+
|
45
|
+
logger = logging.getLogger(__name__)
|
46
|
+
|
47
|
+
|
48
|
+
class NetworkService(str, Enum):
|
49
|
+
"""Network services for cost optimization."""
|
50
|
+
NAT_GATEWAY = "nat_gateway"
|
51
|
+
ELASTIC_IP = "elastic_ip"
|
52
|
+
LOAD_BALANCER = "load_balancer"
|
53
|
+
TRANSIT_GATEWAY = "transit_gateway"
|
54
|
+
VPC_ENDPOINT = "vpc_endpoint"
|
55
|
+
|
56
|
+
|
57
|
+
class LoadBalancerType(str, Enum):
|
58
|
+
"""Load balancer types."""
|
59
|
+
APPLICATION = "application" # ALB
|
60
|
+
NETWORK = "network" # NLB
|
61
|
+
CLASSIC = "classic" # CLB
|
62
|
+
GATEWAY = "gateway" # GWLB
|
63
|
+
|
64
|
+
|
65
|
+
class NetworkResourceDetails(BaseModel):
|
66
|
+
"""Network resource details from AWS APIs."""
|
67
|
+
resource_id: str
|
68
|
+
resource_type: str
|
69
|
+
service: NetworkService
|
70
|
+
region: str
|
71
|
+
availability_zone: Optional[str] = None
|
72
|
+
vpc_id: Optional[str] = None
|
73
|
+
subnet_id: Optional[str] = None
|
74
|
+
state: str = "available"
|
75
|
+
create_time: Optional[datetime] = None
|
76
|
+
|
77
|
+
# Network-specific attributes
|
78
|
+
public_ip: Optional[str] = None
|
79
|
+
private_ip: Optional[str] = None
|
80
|
+
dns_name: Optional[str] = None
|
81
|
+
load_balancer_type: Optional[LoadBalancerType] = None
|
82
|
+
target_count: int = 0
|
83
|
+
|
84
|
+
# Cost attributes
|
85
|
+
hourly_cost: float = 0.0
|
86
|
+
data_processing_cost: float = 0.0 # Per GB
|
87
|
+
monthly_cost: float = 0.0
|
88
|
+
annual_cost: float = 0.0
|
89
|
+
|
90
|
+
tags: Dict[str, str] = Field(default_factory=dict)
|
91
|
+
|
92
|
+
# Usage and dependency attributes
|
93
|
+
has_dependencies: bool = False
|
94
|
+
dependency_score: float = 0.0
|
95
|
+
safety_checks: Dict[str, bool] = Field(default_factory=dict)
|
96
|
+
|
97
|
+
|
98
|
+
class NetworkUsageMetrics(BaseModel):
|
99
|
+
"""Network resource usage metrics from CloudWatch."""
|
100
|
+
resource_id: str
|
101
|
+
region: str
|
102
|
+
service: NetworkService
|
103
|
+
|
104
|
+
# Common network metrics
|
105
|
+
active_connections: float = 0.0
|
106
|
+
bytes_processed: float = 0.0
|
107
|
+
request_count: float = 0.0
|
108
|
+
|
109
|
+
# NAT Gateway specific
|
110
|
+
bytes_in_from_destination: float = 0.0
|
111
|
+
bytes_out_to_destination: float = 0.0
|
112
|
+
packet_drop_count: float = 0.0
|
113
|
+
|
114
|
+
# Load Balancer specific
|
115
|
+
target_response_time: float = 0.0
|
116
|
+
healthy_targets: int = 0
|
117
|
+
unhealthy_targets: int = 0
|
118
|
+
|
119
|
+
# Analysis results
|
120
|
+
analysis_period_days: int = 7
|
121
|
+
is_used: bool = True
|
122
|
+
usage_score: float = 0.0 # 0-100
|
123
|
+
is_underutilized: bool = False
|
124
|
+
|
125
|
+
|
126
|
+
class NetworkOptimizationResult(BaseModel):
|
127
|
+
"""Network resource optimization analysis results."""
|
128
|
+
resource_id: str
|
129
|
+
region: str
|
130
|
+
service: NetworkService
|
131
|
+
resource_type: str
|
132
|
+
current_state: str
|
133
|
+
usage_metrics: Optional[NetworkUsageMetrics] = None
|
134
|
+
|
135
|
+
# Cost analysis
|
136
|
+
current_monthly_cost: float = 0.0
|
137
|
+
current_annual_cost: float = 0.0
|
138
|
+
data_processing_monthly_cost: float = 0.0
|
139
|
+
data_processing_annual_cost: float = 0.0
|
140
|
+
|
141
|
+
# Optimization strategies
|
142
|
+
optimization_recommendation: str = "retain" # retain, decommission, rightsize, consolidate
|
143
|
+
risk_level: str = "low" # low, medium, high
|
144
|
+
business_impact: str = "minimal"
|
145
|
+
|
146
|
+
# Savings potential
|
147
|
+
infrastructure_monthly_savings: float = 0.0
|
148
|
+
infrastructure_annual_savings: float = 0.0
|
149
|
+
data_transfer_monthly_savings: float = 0.0
|
150
|
+
data_transfer_annual_savings: float = 0.0
|
151
|
+
total_monthly_savings: float = 0.0
|
152
|
+
total_annual_savings: float = 0.0
|
153
|
+
|
154
|
+
# Dependencies and safety
|
155
|
+
route_table_dependencies: List[str] = Field(default_factory=list)
|
156
|
+
dns_dependencies: List[str] = Field(default_factory=list)
|
157
|
+
application_dependencies: List[str] = Field(default_factory=list)
|
158
|
+
dependency_risk_score: float = 0.0
|
159
|
+
|
160
|
+
# Alternative solutions
|
161
|
+
alternative_solution: Optional[str] = None
|
162
|
+
alternative_monthly_cost: float = 0.0
|
163
|
+
alternative_annual_cost: float = 0.0
|
164
|
+
|
165
|
+
|
166
|
+
class NetworkCostOptimizerResults(BaseModel):
|
167
|
+
"""Complete network cost optimization analysis results."""
|
168
|
+
analyzed_services: List[NetworkService] = Field(default_factory=list)
|
169
|
+
analyzed_regions: List[str] = Field(default_factory=list)
|
170
|
+
|
171
|
+
# Resource summary
|
172
|
+
total_network_resources: int = 0
|
173
|
+
nat_gateways: int = 0
|
174
|
+
elastic_ips: int = 0
|
175
|
+
load_balancers: int = 0
|
176
|
+
transit_gateways: int = 0
|
177
|
+
vpc_endpoints: int = 0
|
178
|
+
|
179
|
+
# Cost summary
|
180
|
+
total_monthly_infrastructure_cost: float = 0.0
|
181
|
+
total_annual_infrastructure_cost: float = 0.0
|
182
|
+
total_monthly_data_processing_cost: float = 0.0
|
183
|
+
total_annual_data_processing_cost: float = 0.0
|
184
|
+
total_monthly_cost: float = 0.0
|
185
|
+
total_annual_cost: float = 0.0
|
186
|
+
|
187
|
+
# Savings breakdown
|
188
|
+
infrastructure_monthly_savings: float = 0.0
|
189
|
+
infrastructure_annual_savings: float = 0.0
|
190
|
+
data_transfer_monthly_savings: float = 0.0
|
191
|
+
data_transfer_annual_savings: float = 0.0
|
192
|
+
total_monthly_savings: float = 0.0
|
193
|
+
total_annual_savings: float = 0.0
|
194
|
+
|
195
|
+
# Optimization results
|
196
|
+
optimization_results: List[NetworkOptimizationResult] = Field(default_factory=list)
|
197
|
+
|
198
|
+
execution_time_seconds: float = 0.0
|
199
|
+
mcp_validation_accuracy: float = 0.0
|
200
|
+
analysis_timestamp: datetime = Field(default_factory=datetime.now)
|
201
|
+
|
202
|
+
|
203
|
+
class NetworkCostOptimizer:
|
204
|
+
"""
|
205
|
+
Network Cost Optimization Engine - Enterprise FinOps Network Analysis Platform
|
206
|
+
|
207
|
+
Following $132,720+ methodology with proven FinOps patterns targeting $2.4M-$7.3M annual savings:
|
208
|
+
- Multi-service network resource discovery and analysis
|
209
|
+
- CloudWatch metrics integration for usage validation and rightsizing
|
210
|
+
- Comprehensive dependency analysis for safe optimization
|
211
|
+
- Data transfer cost analysis and optimization strategies
|
212
|
+
- Cost calculation with MCP validation (≥99.5% accuracy)
|
213
|
+
- Evidence generation for Manager/Financial/CTO executive reporting
|
214
|
+
- Business-focused network optimization strategy for enterprise presentation
|
215
|
+
"""
|
216
|
+
|
217
|
+
def __init__(self, profile_name: Optional[str] = None, regions: Optional[List[str]] = None):
|
218
|
+
"""Initialize network cost optimizer with enterprise profile support."""
|
219
|
+
self.profile_name = profile_name
|
220
|
+
self.regions = regions or ['us-east-1', 'us-west-2', 'eu-west-1']
|
221
|
+
|
222
|
+
# Initialize AWS session with profile priority system
|
223
|
+
self.session = boto3.Session(
|
224
|
+
profile_name=get_profile_for_operation("operational", profile_name)
|
225
|
+
)
|
226
|
+
|
227
|
+
# Network service pricing (per hour, as of 2024)
|
228
|
+
self.network_pricing = {
|
229
|
+
NetworkService.NAT_GATEWAY: {
|
230
|
+
'hourly_cost': 0.045, # $0.045/hour
|
231
|
+
'data_processing_cost': 0.045 # $0.045/GB
|
232
|
+
},
|
233
|
+
NetworkService.ELASTIC_IP: {
|
234
|
+
'monthly_cost_unattached': 3.65 # $3.65/month if unattached
|
235
|
+
},
|
236
|
+
NetworkService.LOAD_BALANCER: {
|
237
|
+
LoadBalancerType.APPLICATION: {
|
238
|
+
'hourly_cost': 0.0225, # $0.0225/hour
|
239
|
+
'lcu_cost': 0.008 # $0.008/LCU hour
|
240
|
+
},
|
241
|
+
LoadBalancerType.NETWORK: {
|
242
|
+
'hourly_cost': 0.0225, # $0.0225/hour
|
243
|
+
'nlcu_cost': 0.006 # $0.006/NLCU hour
|
244
|
+
},
|
245
|
+
LoadBalancerType.CLASSIC: {
|
246
|
+
'hourly_cost': 0.025, # $0.025/hour
|
247
|
+
'data_cost': 0.008 # $0.008/GB
|
248
|
+
}
|
249
|
+
},
|
250
|
+
NetworkService.TRANSIT_GATEWAY: {
|
251
|
+
'hourly_cost': 0.05, # $0.05/hour attachment
|
252
|
+
'data_processing_cost': 0.02 # $0.02/GB
|
253
|
+
},
|
254
|
+
NetworkService.VPC_ENDPOINT: {
|
255
|
+
'hourly_cost': 0.01, # $0.01/hour per AZ
|
256
|
+
'data_processing_cost': 0.01 # $0.01/GB
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
# Usage thresholds for optimization recommendations
|
261
|
+
self.low_usage_threshold_connections = 10 # Active connections per day
|
262
|
+
self.low_usage_threshold_bytes = 1_000_000 # 1MB per day
|
263
|
+
self.analysis_period_days = 14 # CloudWatch analysis period
|
264
|
+
|
265
|
+
async def analyze_network_costs(self, services: List[NetworkService] = None, dry_run: bool = True) -> NetworkCostOptimizerResults:
|
266
|
+
"""
|
267
|
+
Comprehensive network cost optimization analysis.
|
268
|
+
|
269
|
+
Args:
|
270
|
+
services: List of network services to analyze (None = all services)
|
271
|
+
dry_run: Safety mode - READ-ONLY analysis only
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
Complete analysis results with optimization recommendations
|
275
|
+
"""
|
276
|
+
print_header("Network Cost Optimization Engine", "Enterprise Multi-Service Network Analysis Platform v1.0")
|
277
|
+
|
278
|
+
if not dry_run:
|
279
|
+
print_warning("⚠️ Dry-run disabled - This optimizer is READ-ONLY analysis only")
|
280
|
+
print_info("All network operations require manual execution after review")
|
281
|
+
|
282
|
+
analysis_start_time = time.time()
|
283
|
+
services_to_analyze = services or [
|
284
|
+
NetworkService.NAT_GATEWAY,
|
285
|
+
NetworkService.ELASTIC_IP,
|
286
|
+
NetworkService.LOAD_BALANCER,
|
287
|
+
NetworkService.TRANSIT_GATEWAY,
|
288
|
+
NetworkService.VPC_ENDPOINT
|
289
|
+
]
|
290
|
+
|
291
|
+
try:
|
292
|
+
with create_progress_bar() as progress:
|
293
|
+
# Step 1: Multi-service network resource discovery
|
294
|
+
discovery_task = progress.add_task("Discovering network resources...",
|
295
|
+
total=len(services_to_analyze) * len(self.regions))
|
296
|
+
network_resources = await self._discover_network_resources_multi_service(services_to_analyze, progress, discovery_task)
|
297
|
+
|
298
|
+
if not network_resources:
|
299
|
+
print_warning("No network resources found in specified regions")
|
300
|
+
return NetworkCostOptimizerResults(
|
301
|
+
analyzed_services=services_to_analyze,
|
302
|
+
analyzed_regions=self.regions,
|
303
|
+
analysis_timestamp=datetime.now(),
|
304
|
+
execution_time_seconds=time.time() - analysis_start_time
|
305
|
+
)
|
306
|
+
|
307
|
+
# Step 2: Usage metrics analysis via CloudWatch
|
308
|
+
metrics_task = progress.add_task("Analyzing usage metrics...", total=len(network_resources))
|
309
|
+
usage_metrics = await self._analyze_network_usage_metrics(network_resources, progress, metrics_task)
|
310
|
+
|
311
|
+
# Step 3: Dependency analysis for safety assessment
|
312
|
+
dependency_task = progress.add_task("Analyzing dependencies...", total=len(network_resources))
|
313
|
+
dependency_analysis = await self._analyze_network_dependencies(network_resources, progress, dependency_task)
|
314
|
+
|
315
|
+
# Step 4: Cost calculation and pricing analysis
|
316
|
+
costing_task = progress.add_task("Calculating costs...", total=len(network_resources))
|
317
|
+
cost_analysis = await self._calculate_network_costs(network_resources, usage_metrics, progress, costing_task)
|
318
|
+
|
319
|
+
# Step 5: Comprehensive optimization analysis
|
320
|
+
optimization_task = progress.add_task("Calculating optimization potential...", total=len(network_resources))
|
321
|
+
optimization_results = await self._calculate_network_optimization_recommendations(
|
322
|
+
network_resources, usage_metrics, dependency_analysis, cost_analysis, progress, optimization_task
|
323
|
+
)
|
324
|
+
|
325
|
+
# Step 6: MCP validation
|
326
|
+
validation_task = progress.add_task("MCP validation...", total=1)
|
327
|
+
mcp_accuracy = await self._validate_with_mcp(optimization_results, progress, validation_task)
|
328
|
+
|
329
|
+
# Compile comprehensive results
|
330
|
+
results = self._compile_results(network_resources, optimization_results, mcp_accuracy, analysis_start_time, services_to_analyze)
|
331
|
+
|
332
|
+
# Display executive summary
|
333
|
+
self._display_executive_summary(results)
|
334
|
+
|
335
|
+
return results
|
336
|
+
|
337
|
+
except Exception as e:
|
338
|
+
print_error(f"Network cost optimization analysis failed: {e}")
|
339
|
+
logger.error(f"Network analysis error: {e}", exc_info=True)
|
340
|
+
raise
|
341
|
+
|
342
|
+
async def _discover_network_resources_multi_service(self, services: List[NetworkService], progress, task_id) -> List[NetworkResourceDetails]:
|
343
|
+
"""Discover network resources across multiple services and regions."""
|
344
|
+
network_resources = []
|
345
|
+
|
346
|
+
for service in services:
|
347
|
+
for region in self.regions:
|
348
|
+
try:
|
349
|
+
if service == NetworkService.NAT_GATEWAY:
|
350
|
+
resources = await self._discover_nat_gateways(region)
|
351
|
+
network_resources.extend(resources)
|
352
|
+
elif service == NetworkService.ELASTIC_IP:
|
353
|
+
resources = await self._discover_elastic_ips(region)
|
354
|
+
network_resources.extend(resources)
|
355
|
+
elif service == NetworkService.LOAD_BALANCER:
|
356
|
+
resources = await self._discover_load_balancers(region)
|
357
|
+
network_resources.extend(resources)
|
358
|
+
elif service == NetworkService.TRANSIT_GATEWAY:
|
359
|
+
resources = await self._discover_transit_gateways(region)
|
360
|
+
network_resources.extend(resources)
|
361
|
+
elif service == NetworkService.VPC_ENDPOINT:
|
362
|
+
resources = await self._discover_vpc_endpoints(region)
|
363
|
+
network_resources.extend(resources)
|
364
|
+
|
365
|
+
service_resources = [r for r in network_resources if r.region == region and r.service == service]
|
366
|
+
print_info(f"Service {service.value} in {region}: {len(service_resources)} resources discovered")
|
367
|
+
|
368
|
+
except ClientError as e:
|
369
|
+
print_warning(f"Service {service.value} in {region}: Access denied - {e.response['Error']['Code']}")
|
370
|
+
except Exception as e:
|
371
|
+
print_error(f"Service {service.value} in {region}: Discovery error - {str(e)}")
|
372
|
+
|
373
|
+
progress.advance(task_id)
|
374
|
+
|
375
|
+
return network_resources
|
376
|
+
|
377
|
+
async def _discover_nat_gateways(self, region: str) -> List[NetworkResourceDetails]:
|
378
|
+
"""Discover NAT Gateways for cost analysis."""
|
379
|
+
resources = []
|
380
|
+
|
381
|
+
try:
|
382
|
+
ec2_client = self.session.client('ec2', region_name=region)
|
383
|
+
|
384
|
+
response = ec2_client.describe_nat_gateways()
|
385
|
+
for nat_gateway in response.get('NatGateways', []):
|
386
|
+
# Skip deleted NAT Gateways
|
387
|
+
if nat_gateway.get('State') == 'deleted':
|
388
|
+
continue
|
389
|
+
|
390
|
+
tags = {tag['Key']: tag['Value'] for tag in nat_gateway.get('Tags', [])}
|
391
|
+
|
392
|
+
# Get NAT Gateway addresses
|
393
|
+
public_ip = None
|
394
|
+
private_ip = None
|
395
|
+
for address in nat_gateway.get('NatGatewayAddresses', []):
|
396
|
+
if address.get('PublicIp'):
|
397
|
+
public_ip = address['PublicIp']
|
398
|
+
if address.get('PrivateIp'):
|
399
|
+
private_ip = address['PrivateIp']
|
400
|
+
|
401
|
+
pricing = self.network_pricing[NetworkService.NAT_GATEWAY]
|
402
|
+
hourly_cost = pricing['hourly_cost']
|
403
|
+
monthly_cost = hourly_cost * 24 * 30.44
|
404
|
+
annual_cost = hourly_cost * 24 * 365
|
405
|
+
|
406
|
+
resources.append(NetworkResourceDetails(
|
407
|
+
resource_id=nat_gateway['NatGatewayId'],
|
408
|
+
resource_type='NAT Gateway',
|
409
|
+
service=NetworkService.NAT_GATEWAY,
|
410
|
+
region=region,
|
411
|
+
availability_zone=nat_gateway.get('SubnetId'), # Subnet implies AZ
|
412
|
+
vpc_id=nat_gateway.get('VpcId'),
|
413
|
+
subnet_id=nat_gateway.get('SubnetId'),
|
414
|
+
state=nat_gateway.get('State'),
|
415
|
+
create_time=nat_gateway.get('CreateTime'),
|
416
|
+
public_ip=public_ip,
|
417
|
+
private_ip=private_ip,
|
418
|
+
hourly_cost=hourly_cost,
|
419
|
+
data_processing_cost=pricing['data_processing_cost'],
|
420
|
+
monthly_cost=monthly_cost,
|
421
|
+
annual_cost=annual_cost,
|
422
|
+
tags=tags
|
423
|
+
))
|
424
|
+
|
425
|
+
except Exception as e:
|
426
|
+
logger.warning(f"NAT Gateway discovery failed in {region}: {e}")
|
427
|
+
|
428
|
+
return resources
|
429
|
+
|
430
|
+
async def _discover_elastic_ips(self, region: str) -> List[NetworkResourceDetails]:
|
431
|
+
"""Discover Elastic IPs for cost analysis."""
|
432
|
+
resources = []
|
433
|
+
|
434
|
+
try:
|
435
|
+
ec2_client = self.session.client('ec2', region_name=region)
|
436
|
+
|
437
|
+
response = ec2_client.describe_addresses()
|
438
|
+
for eip in response.get('Addresses', []):
|
439
|
+
tags = {tag['Key']: tag['Value'] for tag in eip.get('Tags', [])}
|
440
|
+
|
441
|
+
# Check if EIP is attached
|
442
|
+
is_attached = bool(eip.get('InstanceId') or eip.get('NetworkInterfaceId'))
|
443
|
+
|
444
|
+
# Only unattached EIPs have costs
|
445
|
+
monthly_cost = 0.0 if is_attached else self.network_pricing[NetworkService.ELASTIC_IP]['monthly_cost_unattached']
|
446
|
+
annual_cost = monthly_cost * 12
|
447
|
+
|
448
|
+
resources.append(NetworkResourceDetails(
|
449
|
+
resource_id=eip['AllocationId'],
|
450
|
+
resource_type='Elastic IP',
|
451
|
+
service=NetworkService.ELASTIC_IP,
|
452
|
+
region=region,
|
453
|
+
state='attached' if is_attached else 'unattached',
|
454
|
+
public_ip=eip.get('PublicIp'),
|
455
|
+
private_ip=eip.get('PrivateIpAddress'),
|
456
|
+
monthly_cost=monthly_cost,
|
457
|
+
annual_cost=annual_cost,
|
458
|
+
tags=tags,
|
459
|
+
has_dependencies=is_attached
|
460
|
+
))
|
461
|
+
|
462
|
+
except Exception as e:
|
463
|
+
logger.warning(f"Elastic IP discovery failed in {region}: {e}")
|
464
|
+
|
465
|
+
return resources
|
466
|
+
|
467
|
+
async def _discover_load_balancers(self, region: str) -> List[NetworkResourceDetails]:
|
468
|
+
"""Discover Load Balancers (ALB, NLB, CLB) for cost analysis."""
|
469
|
+
resources = []
|
470
|
+
|
471
|
+
try:
|
472
|
+
# Application and Network Load Balancers (ELBv2)
|
473
|
+
elbv2_client = self.session.client('elbv2', region_name=region)
|
474
|
+
|
475
|
+
response = elbv2_client.describe_load_balancers()
|
476
|
+
for lb in response.get('LoadBalancers', []):
|
477
|
+
# Skip provisioning or failed load balancers
|
478
|
+
if lb.get('State', {}).get('Code') not in ['active', 'idle']:
|
479
|
+
continue
|
480
|
+
|
481
|
+
lb_type = LoadBalancerType.APPLICATION if lb.get('Type') == 'application' else LoadBalancerType.NETWORK
|
482
|
+
|
483
|
+
# Get target count
|
484
|
+
target_count = 0
|
485
|
+
try:
|
486
|
+
target_groups_response = elbv2_client.describe_target_groups(LoadBalancerArn=lb['LoadBalancerArn'])
|
487
|
+
for tg in target_groups_response.get('TargetGroups', []):
|
488
|
+
targets_response = elbv2_client.describe_target_health(TargetGroupArn=tg['TargetGroupArn'])
|
489
|
+
target_count += len(targets_response.get('TargetHealthDescriptions', []))
|
490
|
+
except Exception:
|
491
|
+
pass # Target count is optional
|
492
|
+
|
493
|
+
# Get pricing
|
494
|
+
pricing = self.network_pricing[NetworkService.LOAD_BALANCER][lb_type]
|
495
|
+
hourly_cost = pricing['hourly_cost']
|
496
|
+
monthly_cost = hourly_cost * 24 * 30.44
|
497
|
+
annual_cost = hourly_cost * 24 * 365
|
498
|
+
|
499
|
+
resources.append(NetworkResourceDetails(
|
500
|
+
resource_id=lb['LoadBalancerArn'].split('/')[-3] + '/' + lb['LoadBalancerArn'].split('/')[-2] + '/' + lb['LoadBalancerArn'].split('/')[-1],
|
501
|
+
resource_type=f'{lb_type.value.title()} Load Balancer',
|
502
|
+
service=NetworkService.LOAD_BALANCER,
|
503
|
+
region=region,
|
504
|
+
vpc_id=lb.get('VpcId'),
|
505
|
+
state=lb.get('State', {}).get('Code', 'unknown'),
|
506
|
+
create_time=lb.get('CreatedTime'),
|
507
|
+
dns_name=lb.get('DNSName'),
|
508
|
+
load_balancer_type=lb_type,
|
509
|
+
target_count=target_count,
|
510
|
+
hourly_cost=hourly_cost,
|
511
|
+
monthly_cost=monthly_cost,
|
512
|
+
annual_cost=annual_cost,
|
513
|
+
has_dependencies=target_count > 0
|
514
|
+
))
|
515
|
+
|
516
|
+
# Classic Load Balancers (ELB)
|
517
|
+
elb_client = self.session.client('elb', region_name=region)
|
518
|
+
|
519
|
+
response = elb_client.describe_load_balancers()
|
520
|
+
for lb in response.get('LoadBalancerDescriptions', []):
|
521
|
+
# Get instance count
|
522
|
+
instance_count = len(lb.get('Instances', []))
|
523
|
+
|
524
|
+
pricing = self.network_pricing[NetworkService.LOAD_BALANCER][LoadBalancerType.CLASSIC]
|
525
|
+
hourly_cost = pricing['hourly_cost']
|
526
|
+
monthly_cost = hourly_cost * 24 * 30.44
|
527
|
+
annual_cost = hourly_cost * 24 * 365
|
528
|
+
|
529
|
+
resources.append(NetworkResourceDetails(
|
530
|
+
resource_id=lb['LoadBalancerName'],
|
531
|
+
resource_type='Classic Load Balancer',
|
532
|
+
service=NetworkService.LOAD_BALANCER,
|
533
|
+
region=region,
|
534
|
+
vpc_id=lb.get('VPCId'),
|
535
|
+
state='active', # CLBs don't have explicit state
|
536
|
+
create_time=lb.get('CreatedTime'),
|
537
|
+
dns_name=lb.get('DNSName'),
|
538
|
+
load_balancer_type=LoadBalancerType.CLASSIC,
|
539
|
+
target_count=instance_count,
|
540
|
+
hourly_cost=hourly_cost,
|
541
|
+
monthly_cost=monthly_cost,
|
542
|
+
annual_cost=annual_cost,
|
543
|
+
has_dependencies=instance_count > 0
|
544
|
+
))
|
545
|
+
|
546
|
+
except Exception as e:
|
547
|
+
logger.warning(f"Load Balancer discovery failed in {region}: {e}")
|
548
|
+
|
549
|
+
return resources
|
550
|
+
|
551
|
+
async def _discover_transit_gateways(self, region: str) -> List[NetworkResourceDetails]:
|
552
|
+
"""Discover Transit Gateways for cost analysis."""
|
553
|
+
resources = []
|
554
|
+
|
555
|
+
try:
|
556
|
+
ec2_client = self.session.client('ec2', region_name=region)
|
557
|
+
|
558
|
+
response = ec2_client.describe_transit_gateways()
|
559
|
+
for tgw in response.get('TransitGateways', []):
|
560
|
+
# Skip deleted TGWs
|
561
|
+
if tgw.get('State') == 'deleted':
|
562
|
+
continue
|
563
|
+
|
564
|
+
tags = {tag['Key']: tag['Value'] for tag in tgw.get('Tags', [])}
|
565
|
+
|
566
|
+
# Get attachment count for dependency analysis
|
567
|
+
attachments_response = ec2_client.describe_transit_gateway_attachments(
|
568
|
+
Filters=[{'Name': 'transit-gateway-id', 'Values': [tgw['TransitGatewayId']]}]
|
569
|
+
)
|
570
|
+
attachment_count = len(attachments_response.get('TransitGatewayAttachments', []))
|
571
|
+
|
572
|
+
pricing = self.network_pricing[NetworkService.TRANSIT_GATEWAY]
|
573
|
+
hourly_cost = pricing['hourly_cost'] * attachment_count # Cost per attachment
|
574
|
+
monthly_cost = hourly_cost * 24 * 30.44
|
575
|
+
annual_cost = hourly_cost * 24 * 365
|
576
|
+
|
577
|
+
resources.append(NetworkResourceDetails(
|
578
|
+
resource_id=tgw['TransitGatewayId'],
|
579
|
+
resource_type='Transit Gateway',
|
580
|
+
service=NetworkService.TRANSIT_GATEWAY,
|
581
|
+
region=region,
|
582
|
+
state=tgw.get('State'),
|
583
|
+
hourly_cost=hourly_cost,
|
584
|
+
data_processing_cost=pricing['data_processing_cost'],
|
585
|
+
monthly_cost=monthly_cost,
|
586
|
+
annual_cost=annual_cost,
|
587
|
+
tags=tags,
|
588
|
+
has_dependencies=attachment_count > 0,
|
589
|
+
dependency_score=min(1.0, attachment_count / 10.0) # Normalize to 0-1
|
590
|
+
))
|
591
|
+
|
592
|
+
except Exception as e:
|
593
|
+
logger.warning(f"Transit Gateway discovery failed in {region}: {e}")
|
594
|
+
|
595
|
+
return resources
|
596
|
+
|
597
|
+
async def _discover_vpc_endpoints(self, region: str) -> List[NetworkResourceDetails]:
|
598
|
+
"""Discover VPC Endpoints for cost analysis."""
|
599
|
+
resources = []
|
600
|
+
|
601
|
+
try:
|
602
|
+
ec2_client = self.session.client('ec2', region_name=region)
|
603
|
+
|
604
|
+
response = ec2_client.describe_vpc_endpoints()
|
605
|
+
for vpce in response.get('VpcEndpoints', []):
|
606
|
+
# Skip deleted endpoints
|
607
|
+
if vpce.get('State') in ['deleted', 'deleting']:
|
608
|
+
continue
|
609
|
+
|
610
|
+
tags = {tag['Key']: tag['Value'] for tag in vpce.get('Tags', [])}
|
611
|
+
|
612
|
+
# VPC Endpoint pricing varies by type (Interface vs Gateway)
|
613
|
+
endpoint_type = vpce.get('VpcEndpointType', 'Interface')
|
614
|
+
|
615
|
+
if endpoint_type == 'Gateway':
|
616
|
+
# Gateway endpoints are free
|
617
|
+
hourly_cost = 0.0
|
618
|
+
data_processing_cost = 0.0
|
619
|
+
else:
|
620
|
+
# Interface endpoints charge per AZ
|
621
|
+
az_count = len(vpce.get('SubnetIds', []))
|
622
|
+
pricing = self.network_pricing[NetworkService.VPC_ENDPOINT]
|
623
|
+
hourly_cost = pricing['hourly_cost'] * az_count
|
624
|
+
data_processing_cost = pricing['data_processing_cost']
|
625
|
+
|
626
|
+
monthly_cost = hourly_cost * 24 * 30.44
|
627
|
+
annual_cost = hourly_cost * 24 * 365
|
628
|
+
|
629
|
+
resources.append(NetworkResourceDetails(
|
630
|
+
resource_id=vpce['VpcEndpointId'],
|
631
|
+
resource_type=f'{endpoint_type} VPC Endpoint',
|
632
|
+
service=NetworkService.VPC_ENDPOINT,
|
633
|
+
region=region,
|
634
|
+
vpc_id=vpce.get('VpcId'),
|
635
|
+
state=vpce.get('State'),
|
636
|
+
create_time=vpce.get('CreationTimestamp'),
|
637
|
+
hourly_cost=hourly_cost,
|
638
|
+
data_processing_cost=data_processing_cost,
|
639
|
+
monthly_cost=monthly_cost,
|
640
|
+
annual_cost=annual_cost,
|
641
|
+
tags=tags,
|
642
|
+
has_dependencies=True # VPC Endpoints always have VPC dependencies
|
643
|
+
))
|
644
|
+
|
645
|
+
except Exception as e:
|
646
|
+
logger.warning(f"VPC Endpoint discovery failed in {region}: {e}")
|
647
|
+
|
648
|
+
return resources
|
649
|
+
|
650
|
+
async def _analyze_network_usage_metrics(self, resources: List[NetworkResourceDetails], progress, task_id) -> Dict[str, NetworkUsageMetrics]:
|
651
|
+
"""Analyze network resource usage metrics via CloudWatch."""
|
652
|
+
usage_metrics = {}
|
653
|
+
end_time = datetime.utcnow()
|
654
|
+
start_time = end_time - timedelta(days=self.analysis_period_days)
|
655
|
+
|
656
|
+
for resource in resources:
|
657
|
+
try:
|
658
|
+
cloudwatch = self.session.client('cloudwatch', region_name=resource.region)
|
659
|
+
|
660
|
+
if resource.service == NetworkService.NAT_GATEWAY:
|
661
|
+
metrics = await self._get_nat_gateway_metrics(cloudwatch, resource.resource_id, start_time, end_time)
|
662
|
+
elif resource.service == NetworkService.LOAD_BALANCER:
|
663
|
+
metrics = await self._get_load_balancer_metrics(cloudwatch, resource, start_time, end_time)
|
664
|
+
elif resource.service == NetworkService.TRANSIT_GATEWAY:
|
665
|
+
metrics = await self._get_transit_gateway_metrics(cloudwatch, resource.resource_id, start_time, end_time)
|
666
|
+
else:
|
667
|
+
# For Elastic IPs and VPC Endpoints, create default metrics
|
668
|
+
metrics = NetworkUsageMetrics(
|
669
|
+
resource_id=resource.resource_id,
|
670
|
+
region=resource.region,
|
671
|
+
service=resource.service,
|
672
|
+
analysis_period_days=self.analysis_period_days,
|
673
|
+
usage_score=50.0 # Neutral score
|
674
|
+
)
|
675
|
+
|
676
|
+
usage_metrics[resource.resource_id] = metrics
|
677
|
+
|
678
|
+
except Exception as e:
|
679
|
+
print_warning(f"Usage metrics unavailable for {resource.resource_id}: {str(e)}")
|
680
|
+
# Create default metrics
|
681
|
+
usage_metrics[resource.resource_id] = NetworkUsageMetrics(
|
682
|
+
resource_id=resource.resource_id,
|
683
|
+
region=resource.region,
|
684
|
+
service=resource.service,
|
685
|
+
analysis_period_days=self.analysis_period_days,
|
686
|
+
usage_score=50.0 # Conservative score
|
687
|
+
)
|
688
|
+
|
689
|
+
progress.advance(task_id)
|
690
|
+
|
691
|
+
return usage_metrics
|
692
|
+
|
693
|
+
async def _get_nat_gateway_metrics(self, cloudwatch, nat_gateway_id: str, start_time: datetime, end_time: datetime) -> NetworkUsageMetrics:
|
694
|
+
"""Get NAT Gateway metrics from CloudWatch."""
|
695
|
+
try:
|
696
|
+
# Get active connections
|
697
|
+
connections_response = cloudwatch.get_metric_statistics(
|
698
|
+
Namespace='AWS/NATGateway',
|
699
|
+
MetricName='ActiveConnectionCount',
|
700
|
+
Dimensions=[{'Name': 'NatGatewayId', 'Value': nat_gateway_id}],
|
701
|
+
StartTime=start_time,
|
702
|
+
EndTime=end_time,
|
703
|
+
Period=86400, # Daily data points
|
704
|
+
Statistics=['Average']
|
705
|
+
)
|
706
|
+
|
707
|
+
# Get bytes processed
|
708
|
+
bytes_response = cloudwatch.get_metric_statistics(
|
709
|
+
Namespace='AWS/NATGateway',
|
710
|
+
MetricName='BytesInFromDestination',
|
711
|
+
Dimensions=[{'Name': 'NatGatewayId', 'Value': nat_gateway_id}],
|
712
|
+
StartTime=start_time,
|
713
|
+
EndTime=end_time,
|
714
|
+
Period=86400,
|
715
|
+
Statistics=['Sum']
|
716
|
+
)
|
717
|
+
|
718
|
+
active_connections = sum(dp['Average'] for dp in connections_response.get('Datapoints', []))
|
719
|
+
bytes_processed = sum(dp['Sum'] for dp in bytes_response.get('Datapoints', []))
|
720
|
+
|
721
|
+
# Determine if NAT Gateway is being used
|
722
|
+
is_used = active_connections > self.low_usage_threshold_connections or bytes_processed > self.low_usage_threshold_bytes
|
723
|
+
usage_score = min(100, (active_connections / self.low_usage_threshold_connections) * 50 + (bytes_processed / self.low_usage_threshold_bytes) * 50)
|
724
|
+
|
725
|
+
return NetworkUsageMetrics(
|
726
|
+
resource_id=nat_gateway_id,
|
727
|
+
region=cloudwatch.meta.region_name,
|
728
|
+
service=NetworkService.NAT_GATEWAY,
|
729
|
+
active_connections=active_connections,
|
730
|
+
bytes_processed=bytes_processed,
|
731
|
+
analysis_period_days=self.analysis_period_days,
|
732
|
+
is_used=is_used,
|
733
|
+
usage_score=usage_score,
|
734
|
+
is_underutilized=not is_used
|
735
|
+
)
|
736
|
+
|
737
|
+
except Exception as e:
|
738
|
+
logger.warning(f"NAT Gateway metrics unavailable for {nat_gateway_id}: {e}")
|
739
|
+
return NetworkUsageMetrics(
|
740
|
+
resource_id=nat_gateway_id,
|
741
|
+
region=cloudwatch.meta.region_name,
|
742
|
+
service=NetworkService.NAT_GATEWAY,
|
743
|
+
analysis_period_days=self.analysis_period_days,
|
744
|
+
usage_score=50.0
|
745
|
+
)
|
746
|
+
|
747
|
+
async def _get_load_balancer_metrics(self, cloudwatch, resource: NetworkResourceDetails, start_time: datetime, end_time: datetime) -> NetworkUsageMetrics:
|
748
|
+
"""Get Load Balancer metrics from CloudWatch."""
|
749
|
+
try:
|
750
|
+
if resource.load_balancer_type in [LoadBalancerType.APPLICATION, LoadBalancerType.NETWORK]:
|
751
|
+
namespace = 'AWS/ApplicationELB' if resource.load_balancer_type == LoadBalancerType.APPLICATION else 'AWS/NetworkELB'
|
752
|
+
dimension_name = 'LoadBalancer'
|
753
|
+
dimension_value = resource.resource_id
|
754
|
+
else: # Classic Load Balancer
|
755
|
+
namespace = 'AWS/ELB'
|
756
|
+
dimension_name = 'LoadBalancerName'
|
757
|
+
dimension_value = resource.resource_id
|
758
|
+
|
759
|
+
# Get request count
|
760
|
+
request_response = cloudwatch.get_metric_statistics(
|
761
|
+
Namespace=namespace,
|
762
|
+
MetricName='RequestCount',
|
763
|
+
Dimensions=[{
|
764
|
+
'Name': dimension_name,
|
765
|
+
'Value': dimension_value
|
766
|
+
}],
|
767
|
+
StartTime=start_time,
|
768
|
+
EndTime=end_time,
|
769
|
+
Period=86400,
|
770
|
+
Statistics=['Sum']
|
771
|
+
)
|
772
|
+
|
773
|
+
request_count = sum(dp['Sum'] for dp in request_response.get('Datapoints', []))
|
774
|
+
|
775
|
+
# Calculate usage score
|
776
|
+
usage_score = min(100, (request_count / (1000 * self.analysis_period_days)) * 100) # 1000 requests per day baseline
|
777
|
+
is_used = request_count > 100 * self.analysis_period_days # 100 requests per day minimum
|
778
|
+
|
779
|
+
return NetworkUsageMetrics(
|
780
|
+
resource_id=resource.resource_id,
|
781
|
+
region=resource.region,
|
782
|
+
service=NetworkService.LOAD_BALANCER,
|
783
|
+
request_count=request_count,
|
784
|
+
analysis_period_days=self.analysis_period_days,
|
785
|
+
is_used=is_used,
|
786
|
+
usage_score=usage_score,
|
787
|
+
is_underutilized=not is_used,
|
788
|
+
healthy_targets=resource.target_count
|
789
|
+
)
|
790
|
+
|
791
|
+
except Exception as e:
|
792
|
+
logger.warning(f"Load Balancer metrics unavailable for {resource.resource_id}: {e}")
|
793
|
+
return NetworkUsageMetrics(
|
794
|
+
resource_id=resource.resource_id,
|
795
|
+
region=resource.region,
|
796
|
+
service=NetworkService.LOAD_BALANCER,
|
797
|
+
analysis_period_days=self.analysis_period_days,
|
798
|
+
usage_score=50.0,
|
799
|
+
healthy_targets=resource.target_count
|
800
|
+
)
|
801
|
+
|
802
|
+
async def _get_transit_gateway_metrics(self, cloudwatch, tgw_id: str, start_time: datetime, end_time: datetime) -> NetworkUsageMetrics:
|
803
|
+
"""Get Transit Gateway metrics from CloudWatch."""
|
804
|
+
try:
|
805
|
+
# Get bytes transferred
|
806
|
+
bytes_response = cloudwatch.get_metric_statistics(
|
807
|
+
Namespace='AWS/TransitGateway',
|
808
|
+
MetricName='BytesIn',
|
809
|
+
Dimensions=[{'Name': 'TransitGateway', 'Value': tgw_id}],
|
810
|
+
StartTime=start_time,
|
811
|
+
EndTime=end_time,
|
812
|
+
Period=86400,
|
813
|
+
Statistics=['Sum']
|
814
|
+
)
|
815
|
+
|
816
|
+
bytes_transferred = sum(dp['Sum'] for dp in bytes_response.get('Datapoints', []))
|
817
|
+
usage_score = min(100, (bytes_transferred / (10_000_000 * self.analysis_period_days)) * 100) # 10MB per day baseline
|
818
|
+
is_used = bytes_transferred > 1_000_000 * self.analysis_period_days # 1MB per day minimum
|
819
|
+
|
820
|
+
return NetworkUsageMetrics(
|
821
|
+
resource_id=tgw_id,
|
822
|
+
region=cloudwatch.meta.region_name,
|
823
|
+
service=NetworkService.TRANSIT_GATEWAY,
|
824
|
+
bytes_processed=bytes_transferred,
|
825
|
+
analysis_period_days=self.analysis_period_days,
|
826
|
+
is_used=is_used,
|
827
|
+
usage_score=usage_score,
|
828
|
+
is_underutilized=not is_used
|
829
|
+
)
|
830
|
+
|
831
|
+
except Exception as e:
|
832
|
+
logger.warning(f"Transit Gateway metrics unavailable for {tgw_id}: {e}")
|
833
|
+
return NetworkUsageMetrics(
|
834
|
+
resource_id=tgw_id,
|
835
|
+
region=cloudwatch.meta.region_name,
|
836
|
+
service=NetworkService.TRANSIT_GATEWAY,
|
837
|
+
analysis_period_days=self.analysis_period_days,
|
838
|
+
usage_score=50.0
|
839
|
+
)
|
840
|
+
|
841
|
+
async def _analyze_network_dependencies(self, resources: List[NetworkResourceDetails], progress, task_id) -> Dict[str, Dict[str, Any]]:
|
842
|
+
"""Analyze network resource dependencies for safe optimization."""
|
843
|
+
dependencies = {}
|
844
|
+
|
845
|
+
for resource in resources:
|
846
|
+
try:
|
847
|
+
resource_dependencies = {
|
848
|
+
'route_tables': [],
|
849
|
+
'dns_records': [],
|
850
|
+
'applications': [],
|
851
|
+
'dependency_score': 0.0
|
852
|
+
}
|
853
|
+
|
854
|
+
if resource.service == NetworkService.NAT_GATEWAY:
|
855
|
+
# Check route tables that reference this NAT Gateway
|
856
|
+
route_tables = await self._get_nat_gateway_route_dependencies(resource)
|
857
|
+
resource_dependencies['route_tables'] = route_tables
|
858
|
+
resource_dependencies['dependency_score'] = min(1.0, len(route_tables) / 5.0)
|
859
|
+
|
860
|
+
elif resource.service == NetworkService.ELASTIC_IP:
|
861
|
+
# Check if EIP is referenced in DNS or applications
|
862
|
+
dns_records = await self._get_elastic_ip_dns_dependencies(resource)
|
863
|
+
resource_dependencies['dns_records'] = dns_records
|
864
|
+
resource_dependencies['dependency_score'] = 0.8 if resource.has_dependencies else 0.1
|
865
|
+
|
866
|
+
elif resource.service == NetworkService.LOAD_BALANCER:
|
867
|
+
# Load balancers with targets have high dependency scores
|
868
|
+
resource_dependencies['applications'] = [f"Target count: {resource.target_count}"]
|
869
|
+
resource_dependencies['dependency_score'] = min(1.0, resource.target_count / 10.0) if resource.target_count else 0.0
|
870
|
+
|
871
|
+
else:
|
872
|
+
# Default dependency analysis
|
873
|
+
resource_dependencies['dependency_score'] = 0.5 if resource.has_dependencies else 0.0
|
874
|
+
|
875
|
+
dependencies[resource.resource_id] = resource_dependencies
|
876
|
+
|
877
|
+
except Exception as e:
|
878
|
+
print_warning(f"Dependency analysis failed for {resource.resource_id}: {str(e)}")
|
879
|
+
dependencies[resource.resource_id] = {'dependency_score': 0.5}
|
880
|
+
|
881
|
+
progress.advance(task_id)
|
882
|
+
|
883
|
+
return dependencies
|
884
|
+
|
885
|
+
async def _get_nat_gateway_route_dependencies(self, resource: NetworkResourceDetails) -> List[str]:
|
886
|
+
"""Get route tables that depend on this NAT Gateway."""
|
887
|
+
route_tables = []
|
888
|
+
|
889
|
+
try:
|
890
|
+
ec2_client = self.session.client('ec2', region_name=resource.region)
|
891
|
+
|
892
|
+
response = ec2_client.describe_route_tables(
|
893
|
+
Filters=[
|
894
|
+
{
|
895
|
+
'Name': 'route.nat-gateway-id',
|
896
|
+
'Values': [resource.resource_id]
|
897
|
+
}
|
898
|
+
]
|
899
|
+
)
|
900
|
+
|
901
|
+
route_tables = [rt['RouteTableId'] for rt in response.get('RouteTables', [])]
|
902
|
+
|
903
|
+
except Exception as e:
|
904
|
+
logger.warning(f"Route table dependency check failed for NAT Gateway {resource.resource_id}: {e}")
|
905
|
+
|
906
|
+
return route_tables
|
907
|
+
|
908
|
+
async def _get_elastic_ip_dns_dependencies(self, resource: NetworkResourceDetails) -> List[str]:
|
909
|
+
"""Get DNS records that might reference this Elastic IP."""
|
910
|
+
dns_records = []
|
911
|
+
|
912
|
+
# This would require integration with Route 53 or external DNS systems
|
913
|
+
# For now, return empty list - could be enhanced with Route 53 API calls
|
914
|
+
|
915
|
+
return dns_records
|
916
|
+
|
917
|
+
async def _calculate_network_costs(self, resources: List[NetworkResourceDetails],
|
918
|
+
usage_metrics: Dict[str, NetworkUsageMetrics],
|
919
|
+
progress, task_id) -> Dict[str, Dict[str, float]]:
|
920
|
+
"""Calculate comprehensive network costs including data processing."""
|
921
|
+
cost_analysis = {}
|
922
|
+
|
923
|
+
for resource in resources:
|
924
|
+
try:
|
925
|
+
metrics = usage_metrics.get(resource.resource_id)
|
926
|
+
|
927
|
+
# Base infrastructure cost is already calculated
|
928
|
+
infrastructure_cost = {
|
929
|
+
'monthly': resource.monthly_cost,
|
930
|
+
'annual': resource.annual_cost
|
931
|
+
}
|
932
|
+
|
933
|
+
# Calculate data processing costs if applicable
|
934
|
+
data_processing_cost = {
|
935
|
+
'monthly': 0.0,
|
936
|
+
'annual': 0.0
|
937
|
+
}
|
938
|
+
|
939
|
+
if hasattr(resource, 'data_processing_cost') and resource.data_processing_cost > 0 and metrics:
|
940
|
+
# Estimate monthly data processing based on metrics
|
941
|
+
if resource.service == NetworkService.NAT_GATEWAY and metrics.bytes_processed > 0:
|
942
|
+
monthly_gb = (metrics.bytes_processed / self.analysis_period_days) * 30.44 / (1024**3)
|
943
|
+
data_processing_cost['monthly'] = monthly_gb * resource.data_processing_cost
|
944
|
+
data_processing_cost['annual'] = data_processing_cost['monthly'] * 12
|
945
|
+
|
946
|
+
elif resource.service == NetworkService.TRANSIT_GATEWAY and metrics.bytes_processed > 0:
|
947
|
+
monthly_gb = (metrics.bytes_processed / self.analysis_period_days) * 30.44 / (1024**3)
|
948
|
+
data_processing_cost['monthly'] = monthly_gb * resource.data_processing_cost
|
949
|
+
data_processing_cost['annual'] = data_processing_cost['monthly'] * 12
|
950
|
+
|
951
|
+
cost_analysis[resource.resource_id] = {
|
952
|
+
'infrastructure': infrastructure_cost,
|
953
|
+
'data_processing': data_processing_cost,
|
954
|
+
'total_monthly': infrastructure_cost['monthly'] + data_processing_cost['monthly'],
|
955
|
+
'total_annual': infrastructure_cost['annual'] + data_processing_cost['annual']
|
956
|
+
}
|
957
|
+
|
958
|
+
except Exception as e:
|
959
|
+
print_warning(f"Cost calculation failed for {resource.resource_id}: {str(e)}")
|
960
|
+
cost_analysis[resource.resource_id] = {
|
961
|
+
'infrastructure': {'monthly': 0.0, 'annual': 0.0},
|
962
|
+
'data_processing': {'monthly': 0.0, 'annual': 0.0},
|
963
|
+
'total_monthly': 0.0,
|
964
|
+
'total_annual': 0.0
|
965
|
+
}
|
966
|
+
|
967
|
+
progress.advance(task_id)
|
968
|
+
|
969
|
+
return cost_analysis
|
970
|
+
|
971
|
+
async def _calculate_network_optimization_recommendations(self,
|
972
|
+
resources: List[NetworkResourceDetails],
|
973
|
+
usage_metrics: Dict[str, NetworkUsageMetrics],
|
974
|
+
dependencies: Dict[str, Dict[str, Any]],
|
975
|
+
cost_analysis: Dict[str, Dict[str, float]],
|
976
|
+
progress, task_id) -> List[NetworkOptimizationResult]:
|
977
|
+
"""Calculate comprehensive network optimization recommendations and potential savings."""
|
978
|
+
optimization_results = []
|
979
|
+
|
980
|
+
for resource in resources:
|
981
|
+
try:
|
982
|
+
metrics = usage_metrics.get(resource.resource_id)
|
983
|
+
deps = dependencies.get(resource.resource_id, {})
|
984
|
+
costs = cost_analysis.get(resource.resource_id, {})
|
985
|
+
|
986
|
+
# Initialize optimization analysis
|
987
|
+
recommendation = "retain" # Default
|
988
|
+
risk_level = "low"
|
989
|
+
business_impact = "minimal"
|
990
|
+
|
991
|
+
infrastructure_savings = 0.0
|
992
|
+
data_transfer_savings = 0.0
|
993
|
+
total_monthly_savings = 0.0
|
994
|
+
|
995
|
+
# Service-specific optimization logic
|
996
|
+
if resource.service == NetworkService.NAT_GATEWAY:
|
997
|
+
if metrics and not metrics.is_used:
|
998
|
+
recommendation = "decommission"
|
999
|
+
risk_level = "medium" if len(deps.get('route_tables', [])) > 0 else "low"
|
1000
|
+
business_impact = "cost_elimination"
|
1001
|
+
infrastructure_savings = costs.get('infrastructure', {}).get('monthly', 0.0)
|
1002
|
+
data_transfer_savings = costs.get('data_processing', {}).get('monthly', 0.0)
|
1003
|
+
|
1004
|
+
elif resource.service == NetworkService.ELASTIC_IP:
|
1005
|
+
if resource.state == 'unattached':
|
1006
|
+
recommendation = "release"
|
1007
|
+
risk_level = "low" if not deps.get('dns_records') else "medium"
|
1008
|
+
business_impact = "cost_elimination"
|
1009
|
+
infrastructure_savings = costs.get('infrastructure', {}).get('monthly', 0.0)
|
1010
|
+
|
1011
|
+
elif resource.service == NetworkService.LOAD_BALANCER:
|
1012
|
+
if metrics and not metrics.is_used and resource.target_count == 0:
|
1013
|
+
recommendation = "decommission"
|
1014
|
+
risk_level = "low"
|
1015
|
+
business_impact = "cost_elimination"
|
1016
|
+
infrastructure_savings = costs.get('infrastructure', {}).get('monthly', 0.0)
|
1017
|
+
elif metrics and metrics.is_underutilized:
|
1018
|
+
recommendation = "consolidate"
|
1019
|
+
risk_level = "medium"
|
1020
|
+
business_impact = "consolidation_opportunity"
|
1021
|
+
infrastructure_savings = costs.get('infrastructure', {}).get('monthly', 0.0) * 0.5 # 50% savings estimate
|
1022
|
+
|
1023
|
+
elif resource.service == NetworkService.TRANSIT_GATEWAY:
|
1024
|
+
if metrics and not metrics.is_used:
|
1025
|
+
recommendation = "decommission"
|
1026
|
+
risk_level = "high" # TGWs typically have complex dependencies
|
1027
|
+
business_impact = "infrastructure_simplification"
|
1028
|
+
infrastructure_savings = costs.get('infrastructure', {}).get('monthly', 0.0)
|
1029
|
+
data_transfer_savings = costs.get('data_processing', {}).get('monthly', 0.0)
|
1030
|
+
|
1031
|
+
elif resource.service == NetworkService.VPC_ENDPOINT:
|
1032
|
+
if resource.resource_type == 'Interface VPC Endpoint':
|
1033
|
+
# Interface endpoints could potentially be replaced with NAT Gateway for some use cases
|
1034
|
+
recommendation = "evaluate_alternatives"
|
1035
|
+
risk_level = "medium"
|
1036
|
+
business_impact = "architecture_optimization"
|
1037
|
+
|
1038
|
+
# Calculate total savings
|
1039
|
+
total_monthly_savings = infrastructure_savings + data_transfer_savings
|
1040
|
+
|
1041
|
+
# Adjust risk level based on dependency score
|
1042
|
+
dependency_risk = deps.get('dependency_score', 0.0)
|
1043
|
+
if dependency_risk > 0.7:
|
1044
|
+
risk_level = "high"
|
1045
|
+
elif dependency_risk > 0.3 and risk_level == "low":
|
1046
|
+
risk_level = "medium"
|
1047
|
+
|
1048
|
+
optimization_results.append(NetworkOptimizationResult(
|
1049
|
+
resource_id=resource.resource_id,
|
1050
|
+
region=resource.region,
|
1051
|
+
service=resource.service,
|
1052
|
+
resource_type=resource.resource_type,
|
1053
|
+
current_state=resource.state,
|
1054
|
+
usage_metrics=metrics,
|
1055
|
+
current_monthly_cost=costs.get('total_monthly', 0.0),
|
1056
|
+
current_annual_cost=costs.get('total_annual', 0.0),
|
1057
|
+
data_processing_monthly_cost=costs.get('data_processing', {}).get('monthly', 0.0),
|
1058
|
+
data_processing_annual_cost=costs.get('data_processing', {}).get('annual', 0.0),
|
1059
|
+
optimization_recommendation=recommendation,
|
1060
|
+
risk_level=risk_level,
|
1061
|
+
business_impact=business_impact,
|
1062
|
+
infrastructure_monthly_savings=infrastructure_savings,
|
1063
|
+
infrastructure_annual_savings=infrastructure_savings * 12,
|
1064
|
+
data_transfer_monthly_savings=data_transfer_savings,
|
1065
|
+
data_transfer_annual_savings=data_transfer_savings * 12,
|
1066
|
+
total_monthly_savings=total_monthly_savings,
|
1067
|
+
total_annual_savings=total_monthly_savings * 12,
|
1068
|
+
route_table_dependencies=deps.get('route_tables', []),
|
1069
|
+
dns_dependencies=deps.get('dns_records', []),
|
1070
|
+
application_dependencies=deps.get('applications', []),
|
1071
|
+
dependency_risk_score=dependency_risk
|
1072
|
+
))
|
1073
|
+
|
1074
|
+
except Exception as e:
|
1075
|
+
print_error(f"Network optimization calculation failed for {resource.resource_id}: {str(e)}")
|
1076
|
+
|
1077
|
+
progress.advance(task_id)
|
1078
|
+
|
1079
|
+
return optimization_results
|
1080
|
+
|
1081
|
+
async def _validate_with_mcp(self, optimization_results: List[NetworkOptimizationResult],
|
1082
|
+
progress, task_id) -> float:
|
1083
|
+
"""Validate network optimization results with embedded MCP validator."""
|
1084
|
+
try:
|
1085
|
+
# Prepare validation data in FinOps format
|
1086
|
+
validation_data = {
|
1087
|
+
'total_annual_cost': sum(result.current_annual_cost for result in optimization_results),
|
1088
|
+
'potential_annual_savings': sum(result.total_annual_savings for result in optimization_results),
|
1089
|
+
'resources_analyzed': len(optimization_results),
|
1090
|
+
'services_analyzed': list(set(result.service.value for result in optimization_results)),
|
1091
|
+
'analysis_timestamp': datetime.now().isoformat()
|
1092
|
+
}
|
1093
|
+
|
1094
|
+
# Initialize MCP validator if profile is available
|
1095
|
+
if self.profile_name:
|
1096
|
+
mcp_validator = EmbeddedMCPValidator([self.profile_name])
|
1097
|
+
validation_results = await mcp_validator.validate_cost_data_async(validation_data)
|
1098
|
+
accuracy = validation_results.get('total_accuracy', 0.0)
|
1099
|
+
|
1100
|
+
if accuracy >= 99.5:
|
1101
|
+
print_success(f"MCP Validation: {accuracy:.1f}% accuracy achieved (target: ≥99.5%)")
|
1102
|
+
else:
|
1103
|
+
print_warning(f"MCP Validation: {accuracy:.1f}% accuracy (target: ≥99.5%)")
|
1104
|
+
|
1105
|
+
progress.advance(task_id)
|
1106
|
+
return accuracy
|
1107
|
+
else:
|
1108
|
+
print_info("MCP validation skipped - no profile specified")
|
1109
|
+
progress.advance(task_id)
|
1110
|
+
return 0.0
|
1111
|
+
|
1112
|
+
except Exception as e:
|
1113
|
+
print_warning(f"MCP validation failed: {str(e)}")
|
1114
|
+
progress.advance(task_id)
|
1115
|
+
return 0.0
|
1116
|
+
|
1117
|
+
def _compile_results(self, resources: List[NetworkResourceDetails],
|
1118
|
+
optimization_results: List[NetworkOptimizationResult],
|
1119
|
+
mcp_accuracy: float, analysis_start_time: float,
|
1120
|
+
services_analyzed: List[NetworkService]) -> NetworkCostOptimizerResults:
|
1121
|
+
"""Compile comprehensive network cost optimization results."""
|
1122
|
+
|
1123
|
+
# Count resources by service type
|
1124
|
+
nat_gateways = len([r for r in resources if r.service == NetworkService.NAT_GATEWAY])
|
1125
|
+
elastic_ips = len([r for r in resources if r.service == NetworkService.ELASTIC_IP])
|
1126
|
+
load_balancers = len([r for r in resources if r.service == NetworkService.LOAD_BALANCER])
|
1127
|
+
transit_gateways = len([r for r in resources if r.service == NetworkService.TRANSIT_GATEWAY])
|
1128
|
+
vpc_endpoints = len([r for r in resources if r.service == NetworkService.VPC_ENDPOINT])
|
1129
|
+
|
1130
|
+
# Calculate cost breakdowns
|
1131
|
+
total_monthly_cost = sum(result.current_monthly_cost for result in optimization_results)
|
1132
|
+
total_annual_cost = total_monthly_cost * 12
|
1133
|
+
|
1134
|
+
total_monthly_infrastructure_cost = sum(r.monthly_cost for r in resources)
|
1135
|
+
total_annual_infrastructure_cost = total_monthly_infrastructure_cost * 12
|
1136
|
+
|
1137
|
+
total_monthly_data_processing_cost = sum(result.data_processing_monthly_cost for result in optimization_results)
|
1138
|
+
total_annual_data_processing_cost = total_monthly_data_processing_cost * 12
|
1139
|
+
|
1140
|
+
# Calculate savings
|
1141
|
+
infrastructure_monthly_savings = sum(result.infrastructure_monthly_savings for result in optimization_results)
|
1142
|
+
data_transfer_monthly_savings = sum(result.data_transfer_monthly_savings for result in optimization_results)
|
1143
|
+
total_monthly_savings = sum(result.total_monthly_savings for result in optimization_results)
|
1144
|
+
|
1145
|
+
return NetworkCostOptimizerResults(
|
1146
|
+
analyzed_services=services_analyzed,
|
1147
|
+
analyzed_regions=self.regions,
|
1148
|
+
total_network_resources=len(resources),
|
1149
|
+
nat_gateways=nat_gateways,
|
1150
|
+
elastic_ips=elastic_ips,
|
1151
|
+
load_balancers=load_balancers,
|
1152
|
+
transit_gateways=transit_gateways,
|
1153
|
+
vpc_endpoints=vpc_endpoints,
|
1154
|
+
total_monthly_infrastructure_cost=total_monthly_infrastructure_cost,
|
1155
|
+
total_annual_infrastructure_cost=total_annual_infrastructure_cost,
|
1156
|
+
total_monthly_data_processing_cost=total_monthly_data_processing_cost,
|
1157
|
+
total_annual_data_processing_cost=total_annual_data_processing_cost,
|
1158
|
+
total_monthly_cost=total_monthly_cost,
|
1159
|
+
total_annual_cost=total_annual_cost,
|
1160
|
+
infrastructure_monthly_savings=infrastructure_monthly_savings,
|
1161
|
+
infrastructure_annual_savings=infrastructure_monthly_savings * 12,
|
1162
|
+
data_transfer_monthly_savings=data_transfer_monthly_savings,
|
1163
|
+
data_transfer_annual_savings=data_transfer_monthly_savings * 12,
|
1164
|
+
total_monthly_savings=total_monthly_savings,
|
1165
|
+
total_annual_savings=total_monthly_savings * 12,
|
1166
|
+
optimization_results=optimization_results,
|
1167
|
+
execution_time_seconds=time.time() - analysis_start_time,
|
1168
|
+
mcp_validation_accuracy=mcp_accuracy,
|
1169
|
+
analysis_timestamp=datetime.now()
|
1170
|
+
)
|
1171
|
+
|
1172
|
+
def _display_executive_summary(self, results: NetworkCostOptimizerResults) -> None:
|
1173
|
+
"""Display executive summary with Rich CLI formatting."""
|
1174
|
+
|
1175
|
+
# Executive Summary Panel
|
1176
|
+
summary_content = f"""
|
1177
|
+
🌐 Network Infrastructure Analysis
|
1178
|
+
|
1179
|
+
📊 Total Network Resources: {results.total_network_resources}
|
1180
|
+
• NAT Gateways: {results.nat_gateways}
|
1181
|
+
• Elastic IPs: {results.elastic_ips}
|
1182
|
+
• Load Balancers: {results.load_balancers}
|
1183
|
+
• Transit Gateways: {results.transit_gateways}
|
1184
|
+
• VPC Endpoints: {results.vpc_endpoints}
|
1185
|
+
|
1186
|
+
💰 Current Network Costs:
|
1187
|
+
• Infrastructure: {format_cost(results.total_annual_infrastructure_cost)} annually
|
1188
|
+
• Data Processing: {format_cost(results.total_annual_data_processing_cost)} annually
|
1189
|
+
• Total: {format_cost(results.total_annual_cost)} annually
|
1190
|
+
|
1191
|
+
📈 Optimization Potential:
|
1192
|
+
• Infrastructure Savings: {format_cost(results.infrastructure_annual_savings)}
|
1193
|
+
• Data Transfer Savings: {format_cost(results.data_transfer_annual_savings)}
|
1194
|
+
• Total Savings: {format_cost(results.total_annual_savings)}
|
1195
|
+
|
1196
|
+
🌍 Regions: {', '.join(results.analyzed_regions)}
|
1197
|
+
⚡ Analysis Time: {results.execution_time_seconds:.2f}s
|
1198
|
+
✅ MCP Accuracy: {results.mcp_validation_accuracy:.1f}%
|
1199
|
+
"""
|
1200
|
+
|
1201
|
+
console.print(create_panel(
|
1202
|
+
summary_content.strip(),
|
1203
|
+
title="🏆 Network Cost Optimization Executive Summary",
|
1204
|
+
border_style="green"
|
1205
|
+
))
|
1206
|
+
|
1207
|
+
# Detailed Results Table
|
1208
|
+
table = create_table(
|
1209
|
+
title="Network Resource Optimization Recommendations"
|
1210
|
+
)
|
1211
|
+
|
1212
|
+
table.add_column("Resource ID", style="cyan", no_wrap=True)
|
1213
|
+
table.add_column("Service", style="dim")
|
1214
|
+
table.add_column("Type", justify="center")
|
1215
|
+
table.add_column("Region", justify="center")
|
1216
|
+
table.add_column("Current Cost", justify="right", style="red")
|
1217
|
+
table.add_column("Potential Savings", justify="right", style="green")
|
1218
|
+
table.add_column("Recommendation", justify="center")
|
1219
|
+
table.add_column("Risk", justify="center")
|
1220
|
+
|
1221
|
+
# Sort by potential savings (descending)
|
1222
|
+
sorted_results = sorted(
|
1223
|
+
results.optimization_results,
|
1224
|
+
key=lambda x: x.total_annual_savings,
|
1225
|
+
reverse=True
|
1226
|
+
)
|
1227
|
+
|
1228
|
+
# Show top 20 results
|
1229
|
+
display_results = sorted_results[:20]
|
1230
|
+
|
1231
|
+
for result in display_results:
|
1232
|
+
# Status indicators for recommendations
|
1233
|
+
rec_color = {
|
1234
|
+
"decommission": "red",
|
1235
|
+
"release": "red",
|
1236
|
+
"consolidate": "yellow",
|
1237
|
+
"evaluate_alternatives": "blue",
|
1238
|
+
"retain": "green"
|
1239
|
+
}.get(result.optimization_recommendation, "white")
|
1240
|
+
|
1241
|
+
risk_indicator = {
|
1242
|
+
"low": "🟢",
|
1243
|
+
"medium": "🟡",
|
1244
|
+
"high": "🔴"
|
1245
|
+
}.get(result.risk_level, "⚪")
|
1246
|
+
|
1247
|
+
service_icon = {
|
1248
|
+
NetworkService.NAT_GATEWAY: "🔀",
|
1249
|
+
NetworkService.ELASTIC_IP: "🌐",
|
1250
|
+
NetworkService.LOAD_BALANCER: "⚖️",
|
1251
|
+
NetworkService.TRANSIT_GATEWAY: "🚇",
|
1252
|
+
NetworkService.VPC_ENDPOINT: "🔗"
|
1253
|
+
}.get(result.service, "📡")
|
1254
|
+
|
1255
|
+
table.add_row(
|
1256
|
+
result.resource_id[-12:], # Show last 12 chars
|
1257
|
+
f"{service_icon} {result.service.value.replace('_', ' ').title()}",
|
1258
|
+
result.resource_type,
|
1259
|
+
result.region,
|
1260
|
+
format_cost(result.current_annual_cost),
|
1261
|
+
format_cost(result.total_annual_savings) if result.total_annual_savings > 0 else "-",
|
1262
|
+
f"[{rec_color}]{result.optimization_recommendation.replace('_', ' ').title()}[/]",
|
1263
|
+
f"{risk_indicator} {result.risk_level.title()}"
|
1264
|
+
)
|
1265
|
+
|
1266
|
+
if len(sorted_results) > 20:
|
1267
|
+
table.add_row(
|
1268
|
+
"...", "...", "...", "...", "...", "...",
|
1269
|
+
f"[dim]+{len(sorted_results) - 20} more resources[/]", "..."
|
1270
|
+
)
|
1271
|
+
|
1272
|
+
console.print(table)
|
1273
|
+
|
1274
|
+
# Service-specific breakdown if we have multiple services
|
1275
|
+
if len(results.analyzed_services) > 1:
|
1276
|
+
service_breakdown = {}
|
1277
|
+
for result in results.optimization_results:
|
1278
|
+
service = result.service
|
1279
|
+
if service not in service_breakdown:
|
1280
|
+
service_breakdown[service] = {
|
1281
|
+
'count': 0,
|
1282
|
+
'total_cost': 0.0,
|
1283
|
+
'total_savings': 0.0
|
1284
|
+
}
|
1285
|
+
service_breakdown[service]['count'] += 1
|
1286
|
+
service_breakdown[service]['total_cost'] += result.current_annual_cost
|
1287
|
+
service_breakdown[service]['total_savings'] += result.total_annual_savings
|
1288
|
+
|
1289
|
+
breakdown_content = []
|
1290
|
+
for service, data in service_breakdown.items():
|
1291
|
+
service_name = service.value.replace('_', ' ').title()
|
1292
|
+
breakdown_content.append(
|
1293
|
+
f"• {service_name}: {data['count']} resources | "
|
1294
|
+
f"{format_cost(data['total_cost'])} cost | "
|
1295
|
+
f"{format_cost(data['total_savings'])} savings"
|
1296
|
+
)
|
1297
|
+
|
1298
|
+
console.print(create_panel(
|
1299
|
+
"\n".join(breakdown_content),
|
1300
|
+
title="📊 Service-Level Cost Breakdown",
|
1301
|
+
border_style="blue"
|
1302
|
+
))
|
1303
|
+
|
1304
|
+
|
1305
|
+
# CLI Integration for enterprise runbooks commands
|
1306
|
+
@click.command()
|
1307
|
+
@click.option('--profile', help='AWS profile name (3-tier priority: User > Environment > Default)')
|
1308
|
+
@click.option('--regions', multiple=True, help='AWS regions to analyze (space-separated)')
|
1309
|
+
@click.option('--services', multiple=True,
|
1310
|
+
type=click.Choice(['nat_gateway', 'elastic_ip', 'load_balancer', 'transit_gateway', 'vpc_endpoint']),
|
1311
|
+
help='Network services to analyze')
|
1312
|
+
@click.option('--dry-run/--no-dry-run', default=True, help='Execute in dry-run mode (READ-ONLY analysis)')
|
1313
|
+
@click.option('--usage-threshold-days', type=int, default=14,
|
1314
|
+
help='CloudWatch analysis period in days')
|
1315
|
+
def network_optimizer(profile, regions, services, dry_run, usage_threshold_days):
|
1316
|
+
"""
|
1317
|
+
Network Cost Optimizer - Enterprise Multi-Service Network Analysis
|
1318
|
+
|
1319
|
+
Comprehensive network cost optimization across AWS services:
|
1320
|
+
• NAT Gateway usage analysis with CloudWatch metrics integration
|
1321
|
+
• Elastic IP resource efficiency analysis with DNS dependency checking
|
1322
|
+
• Load Balancer optimization (ALB, NLB, CLB) with traffic analysis
|
1323
|
+
• Transit Gateway cost optimization with attachment analysis
|
1324
|
+
• VPC Endpoint cost-benefit analysis and alternative recommendations
|
1325
|
+
|
1326
|
+
Part of $132,720+ annual savings methodology targeting $2.4M-$7.3M network optimization.
|
1327
|
+
|
1328
|
+
SAFETY: READ-ONLY analysis only - no resource modifications.
|
1329
|
+
|
1330
|
+
Examples:
|
1331
|
+
runbooks finops network --analyze
|
1332
|
+
runbooks finops network --services nat_gateway elastic_ip --regions us-east-1 us-west-2
|
1333
|
+
runbooks finops network --usage-threshold-days 30
|
1334
|
+
"""
|
1335
|
+
try:
|
1336
|
+
# Convert services to NetworkService enum
|
1337
|
+
service_enums = []
|
1338
|
+
if services:
|
1339
|
+
service_map = {
|
1340
|
+
'nat_gateway': NetworkService.NAT_GATEWAY,
|
1341
|
+
'elastic_ip': NetworkService.ELASTIC_IP,
|
1342
|
+
'load_balancer': NetworkService.LOAD_BALANCER,
|
1343
|
+
'transit_gateway': NetworkService.TRANSIT_GATEWAY,
|
1344
|
+
'vpc_endpoint': NetworkService.VPC_ENDPOINT
|
1345
|
+
}
|
1346
|
+
service_enums = [service_map[s] for s in services]
|
1347
|
+
|
1348
|
+
# Initialize optimizer
|
1349
|
+
optimizer = NetworkCostOptimizer(
|
1350
|
+
profile_name=profile,
|
1351
|
+
regions=list(regions) if regions else None
|
1352
|
+
)
|
1353
|
+
|
1354
|
+
# Override analysis period if specified
|
1355
|
+
if usage_threshold_days != 14:
|
1356
|
+
optimizer.analysis_period_days = usage_threshold_days
|
1357
|
+
|
1358
|
+
# Execute comprehensive analysis
|
1359
|
+
results = asyncio.run(optimizer.analyze_network_costs(
|
1360
|
+
services=service_enums if service_enums else None,
|
1361
|
+
dry_run=dry_run
|
1362
|
+
))
|
1363
|
+
|
1364
|
+
# Display final success message
|
1365
|
+
if results.total_annual_savings > 0:
|
1366
|
+
savings_breakdown = []
|
1367
|
+
if results.infrastructure_annual_savings > 0:
|
1368
|
+
savings_breakdown.append(f"Infrastructure: {format_cost(results.infrastructure_annual_savings)}")
|
1369
|
+
if results.data_transfer_annual_savings > 0:
|
1370
|
+
savings_breakdown.append(f"Data Transfer: {format_cost(results.data_transfer_annual_savings)}")
|
1371
|
+
|
1372
|
+
print_success(f"Analysis complete: {format_cost(results.total_annual_savings)} potential annual savings")
|
1373
|
+
print_info(f"Cost breakdown: {' | '.join(savings_breakdown)}")
|
1374
|
+
print_info(f"Services analyzed: {', '.join([s.value.replace('_', ' ').title() for s in results.analyzed_services])}")
|
1375
|
+
else:
|
1376
|
+
print_info("Analysis complete: All network resources are optimally configured")
|
1377
|
+
|
1378
|
+
except KeyboardInterrupt:
|
1379
|
+
print_warning("Analysis interrupted by user")
|
1380
|
+
raise click.Abort()
|
1381
|
+
except Exception as e:
|
1382
|
+
print_error(f"Network cost optimization analysis failed: {str(e)}")
|
1383
|
+
raise click.Abort()
|
1384
|
+
|
1385
|
+
|
1386
|
+
if __name__ == '__main__':
|
1387
|
+
network_optimizer()
|