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