runbooks 0.7.6__py3-none-any.whl → 0.7.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +1 -1
- runbooks/base.py +5 -1
- runbooks/cfat/__init__.py +8 -4
- runbooks/cfat/assessment/collectors.py +171 -14
- runbooks/cfat/assessment/compliance.py +871 -0
- runbooks/cfat/assessment/runner.py +122 -11
- runbooks/cfat/models.py +6 -2
- runbooks/common/logger.py +14 -0
- runbooks/common/rich_utils.py +451 -0
- runbooks/enterprise/__init__.py +68 -0
- runbooks/enterprise/error_handling.py +411 -0
- runbooks/enterprise/logging.py +439 -0
- runbooks/enterprise/multi_tenant.py +583 -0
- runbooks/finops/README.md +468 -241
- runbooks/finops/__init__.py +39 -3
- runbooks/finops/cli.py +83 -18
- runbooks/finops/cross_validation.py +375 -0
- runbooks/finops/dashboard_runner.py +812 -164
- runbooks/finops/enhanced_dashboard_runner.py +525 -0
- runbooks/finops/finops_dashboard.py +1892 -0
- runbooks/finops/helpers.py +485 -51
- runbooks/finops/optimizer.py +823 -0
- runbooks/finops/tests/__init__.py +19 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
- runbooks/finops/tests/run_comprehensive_tests.py +421 -0
- runbooks/finops/tests/run_tests.py +305 -0
- runbooks/finops/tests/test_finops_dashboard.py +705 -0
- runbooks/finops/tests/test_integration.py +477 -0
- runbooks/finops/tests/test_performance.py +380 -0
- runbooks/finops/tests/test_performance_benchmarks.py +500 -0
- runbooks/finops/tests/test_reference_images_validation.py +867 -0
- runbooks/finops/tests/test_single_account_features.py +715 -0
- runbooks/finops/tests/validate_test_suite.py +220 -0
- runbooks/finops/types.py +1 -1
- runbooks/hitl/enhanced_workflow_engine.py +725 -0
- runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
- runbooks/inventory/collectors/aws_comprehensive.py +442 -0
- runbooks/inventory/collectors/enterprise_scale.py +281 -0
- runbooks/inventory/core/collector.py +172 -13
- runbooks/inventory/discovery.md +1 -1
- runbooks/inventory/list_ec2_instances.py +18 -20
- runbooks/inventory/list_ssm_parameters.py +31 -3
- runbooks/inventory/organizations_discovery.py +1269 -0
- runbooks/inventory/rich_inventory_display.py +393 -0
- runbooks/inventory/run_on_multi_accounts.py +35 -19
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/vpc_flow_analyzer.py +1030 -0
- runbooks/main.py +2215 -119
- runbooks/metrics/dora_metrics_engine.py +599 -0
- runbooks/operate/__init__.py +2 -2
- runbooks/operate/base.py +122 -10
- runbooks/operate/deployment_framework.py +1032 -0
- runbooks/operate/deployment_validator.py +853 -0
- runbooks/operate/dynamodb_operations.py +10 -6
- runbooks/operate/ec2_operations.py +319 -11
- runbooks/operate/executive_dashboard.py +779 -0
- runbooks/operate/mcp_integration.py +750 -0
- runbooks/operate/nat_gateway_operations.py +1120 -0
- runbooks/operate/networking_cost_heatmap.py +685 -0
- runbooks/operate/privatelink_operations.py +940 -0
- runbooks/operate/s3_operations.py +10 -6
- runbooks/operate/vpc_endpoints.py +644 -0
- runbooks/operate/vpc_operations.py +1038 -0
- runbooks/remediation/__init__.py +2 -2
- runbooks/remediation/acm_remediation.py +1 -1
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/cloudtrail_remediation.py +1 -1
- runbooks/remediation/cognito_remediation.py +1 -1
- runbooks/remediation/dynamodb_remediation.py +1 -1
- runbooks/remediation/ec2_remediation.py +1 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
- runbooks/remediation/kms_enable_key_rotation.py +1 -1
- runbooks/remediation/kms_remediation.py +1 -1
- runbooks/remediation/lambda_remediation.py +1 -1
- runbooks/remediation/multi_account.py +1 -1
- runbooks/remediation/rds_remediation.py +1 -1
- runbooks/remediation/s3_block_public_access.py +1 -1
- runbooks/remediation/s3_enable_access_logging.py +1 -1
- runbooks/remediation/s3_encryption.py +1 -1
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/remediation/vpc_remediation.py +475 -0
- runbooks/security/__init__.py +3 -1
- runbooks/security/compliance_automation.py +632 -0
- runbooks/security/report_generator.py +10 -0
- runbooks/security/run_script.py +31 -5
- runbooks/security/security_baseline_tester.py +169 -30
- runbooks/security/security_export.py +477 -0
- runbooks/validation/__init__.py +10 -0
- runbooks/validation/benchmark.py +484 -0
- runbooks/validation/cli.py +356 -0
- runbooks/validation/mcp_validator.py +768 -0
- runbooks/vpc/__init__.py +38 -0
- runbooks/vpc/config.py +212 -0
- runbooks/vpc/cost_engine.py +347 -0
- runbooks/vpc/heatmap_engine.py +605 -0
- runbooks/vpc/manager_interface.py +634 -0
- runbooks/vpc/networking_wrapper.py +1260 -0
- runbooks/vpc/rich_formatters.py +679 -0
- runbooks/vpc/tests/__init__.py +5 -0
- runbooks/vpc/tests/conftest.py +356 -0
- runbooks/vpc/tests/test_cli_integration.py +530 -0
- runbooks/vpc/tests/test_config.py +458 -0
- runbooks/vpc/tests/test_cost_engine.py +479 -0
- runbooks/vpc/tests/test_networking_wrapper.py +512 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
runbooks/vpc/__init__.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
"""
|
2
|
+
VPC Networking Operations Module
|
3
|
+
|
4
|
+
This module provides comprehensive VPC networking analysis and optimization capabilities
|
5
|
+
with support for both CLI and Jupyter notebook interfaces using Rich for beautiful outputs.
|
6
|
+
|
7
|
+
Key Components:
|
8
|
+
- VPCNetworkingWrapper: Main interface for all VPC operations
|
9
|
+
- VPCManagerInterface: Business-friendly interface for non-technical users
|
10
|
+
- NetworkingCostEngine: Cost analysis and optimization engine
|
11
|
+
- NetworkingCostHeatMapEngine: Heat map generation for cost visualization
|
12
|
+
- Rich formatters: Consistent, beautiful output formatting
|
13
|
+
|
14
|
+
Usage:
|
15
|
+
CLI: runbooks vpc analyze --profile aws-profile
|
16
|
+
Jupyter: from runbooks.vpc import VPCNetworkingWrapper
|
17
|
+
Manager Dashboard: from runbooks.vpc import VPCManagerInterface
|
18
|
+
"""
|
19
|
+
|
20
|
+
from .cost_engine import NetworkingCostEngine
|
21
|
+
from .heatmap_engine import NetworkingCostHeatMapEngine
|
22
|
+
from .networking_wrapper import VPCNetworkingWrapper
|
23
|
+
from .manager_interface import VPCManagerInterface, BusinessRecommendation, ManagerDashboardConfig
|
24
|
+
from .rich_formatters import display_cost_table, display_heatmap, display_optimization_recommendations
|
25
|
+
|
26
|
+
__all__ = [
|
27
|
+
"VPCNetworkingWrapper",
|
28
|
+
"VPCManagerInterface",
|
29
|
+
"BusinessRecommendation",
|
30
|
+
"ManagerDashboardConfig",
|
31
|
+
"NetworkingCostEngine",
|
32
|
+
"NetworkingCostHeatMapEngine",
|
33
|
+
"display_cost_table",
|
34
|
+
"display_heatmap",
|
35
|
+
"display_optimization_recommendations",
|
36
|
+
]
|
37
|
+
|
38
|
+
__version__ = "1.0.0"
|
runbooks/vpc/config.py
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
"""
|
2
|
+
VPC Networking Configuration Management
|
3
|
+
|
4
|
+
This module provides configurable parameters for VPC networking operations,
|
5
|
+
replacing hard-coded values with environment-aware configuration.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
from dataclasses import dataclass, field
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Any, Dict, List, Optional
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class AWSCostModel:
|
16
|
+
"""AWS Service Cost Model with configurable pricing"""
|
17
|
+
|
18
|
+
# NAT Gateway Pricing (configurable via environment)
|
19
|
+
nat_gateway_hourly: float = field(default_factory=lambda: float(os.getenv("AWS_NAT_GATEWAY_HOURLY", "0.045")))
|
20
|
+
nat_gateway_monthly: float = field(default_factory=lambda: float(os.getenv("AWS_NAT_GATEWAY_MONTHLY", "45.0")))
|
21
|
+
nat_gateway_data_processing: float = field(
|
22
|
+
default_factory=lambda: float(os.getenv("AWS_NAT_GATEWAY_DATA_PROCESSING", "0.045"))
|
23
|
+
)
|
24
|
+
|
25
|
+
# Transit Gateway Pricing
|
26
|
+
transit_gateway_hourly: float = field(
|
27
|
+
default_factory=lambda: float(os.getenv("AWS_TRANSIT_GATEWAY_HOURLY", "0.05"))
|
28
|
+
)
|
29
|
+
transit_gateway_monthly: float = field(
|
30
|
+
default_factory=lambda: float(os.getenv("AWS_TRANSIT_GATEWAY_MONTHLY", "36.50"))
|
31
|
+
)
|
32
|
+
transit_gateway_attachment: float = field(
|
33
|
+
default_factory=lambda: float(os.getenv("AWS_TRANSIT_GATEWAY_ATTACHMENT", "0.05"))
|
34
|
+
)
|
35
|
+
transit_gateway_data_processing: float = field(
|
36
|
+
default_factory=lambda: float(os.getenv("AWS_TRANSIT_GATEWAY_DATA_PROCESSING", "0.02"))
|
37
|
+
)
|
38
|
+
|
39
|
+
# VPC Endpoint Pricing
|
40
|
+
vpc_endpoint_interface_hourly: float = field(
|
41
|
+
default_factory=lambda: float(os.getenv("AWS_VPC_ENDPOINT_INTERFACE_HOURLY", "0.01"))
|
42
|
+
)
|
43
|
+
vpc_endpoint_interface_monthly: float = field(
|
44
|
+
default_factory=lambda: float(os.getenv("AWS_VPC_ENDPOINT_INTERFACE_MONTHLY", "10.0"))
|
45
|
+
)
|
46
|
+
vpc_endpoint_gateway: float = 0.0 # Always free
|
47
|
+
vpc_endpoint_data_processing: float = field(
|
48
|
+
default_factory=lambda: float(os.getenv("AWS_VPC_ENDPOINT_DATA_PROCESSING", "0.01"))
|
49
|
+
)
|
50
|
+
|
51
|
+
# Elastic IP Pricing
|
52
|
+
elastic_ip_idle_hourly: float = field(
|
53
|
+
default_factory=lambda: float(os.getenv("AWS_ELASTIC_IP_IDLE_HOURLY", "0.005"))
|
54
|
+
)
|
55
|
+
elastic_ip_idle_monthly: float = field(
|
56
|
+
default_factory=lambda: float(os.getenv("AWS_ELASTIC_IP_IDLE_MONTHLY", "3.60"))
|
57
|
+
)
|
58
|
+
elastic_ip_attached: float = 0.0 # Always free when attached
|
59
|
+
elastic_ip_remap: float = field(default_factory=lambda: float(os.getenv("AWS_ELASTIC_IP_REMAP", "0.10")))
|
60
|
+
|
61
|
+
# Data Transfer Pricing
|
62
|
+
data_transfer_inter_az: float = field(
|
63
|
+
default_factory=lambda: float(os.getenv("AWS_DATA_TRANSFER_INTER_AZ", "0.01"))
|
64
|
+
)
|
65
|
+
data_transfer_inter_region: float = field(
|
66
|
+
default_factory=lambda: float(os.getenv("AWS_DATA_TRANSFER_INTER_REGION", "0.02"))
|
67
|
+
)
|
68
|
+
data_transfer_internet_out: float = field(
|
69
|
+
default_factory=lambda: float(os.getenv("AWS_DATA_TRANSFER_INTERNET_OUT", "0.09"))
|
70
|
+
)
|
71
|
+
data_transfer_s3_same_region: float = 0.0 # Always free
|
72
|
+
|
73
|
+
|
74
|
+
@dataclass
|
75
|
+
class OptimizationThresholds:
|
76
|
+
"""Configurable thresholds for optimization recommendations"""
|
77
|
+
|
78
|
+
# Usage thresholds
|
79
|
+
idle_connection_threshold: int = field(default_factory=lambda: int(os.getenv("IDLE_CONNECTION_THRESHOLD", "10")))
|
80
|
+
low_usage_gb_threshold: float = field(default_factory=lambda: float(os.getenv("LOW_USAGE_GB_THRESHOLD", "100.0")))
|
81
|
+
low_connection_threshold: int = field(default_factory=lambda: int(os.getenv("LOW_CONNECTION_THRESHOLD", "100")))
|
82
|
+
|
83
|
+
# Cost thresholds
|
84
|
+
high_cost_threshold: float = field(default_factory=lambda: float(os.getenv("HIGH_COST_THRESHOLD", "100.0")))
|
85
|
+
critical_cost_threshold: float = field(default_factory=lambda: float(os.getenv("CRITICAL_COST_THRESHOLD", "500.0")))
|
86
|
+
|
87
|
+
# Optimization targets
|
88
|
+
target_reduction_percent: float = field(
|
89
|
+
default_factory=lambda: float(os.getenv("TARGET_REDUCTION_PERCENT", "30.0"))
|
90
|
+
)
|
91
|
+
|
92
|
+
# Enterprise approval thresholds (from user requirements)
|
93
|
+
cost_approval_threshold: float = field(
|
94
|
+
default_factory=lambda: float(os.getenv("COST_APPROVAL_THRESHOLD", "1000.0"))
|
95
|
+
) # $1000/month
|
96
|
+
performance_baseline_threshold: float = field(
|
97
|
+
default_factory=lambda: float(os.getenv("PERFORMANCE_BASELINE_THRESHOLD", "2.0"))
|
98
|
+
) # 2 seconds
|
99
|
+
|
100
|
+
|
101
|
+
@dataclass
|
102
|
+
class RegionalConfiguration:
|
103
|
+
"""Regional cost multipliers and configuration"""
|
104
|
+
|
105
|
+
# Default regions for analysis
|
106
|
+
default_regions: List[str] = field(
|
107
|
+
default_factory=lambda: [
|
108
|
+
"us-east-1",
|
109
|
+
"us-west-2",
|
110
|
+
"us-west-1",
|
111
|
+
"eu-west-1",
|
112
|
+
"eu-central-1",
|
113
|
+
"eu-west-2",
|
114
|
+
"ap-southeast-1",
|
115
|
+
"ap-southeast-2",
|
116
|
+
"ap-northeast-1",
|
117
|
+
]
|
118
|
+
)
|
119
|
+
|
120
|
+
# Regional cost multipliers (can be overridden by data from AWS Pricing API)
|
121
|
+
regional_multipliers: Dict[str, float] = field(
|
122
|
+
default_factory=lambda: {
|
123
|
+
"us-east-1": float(os.getenv("COST_MULTIPLIER_US_EAST_1", "1.5")),
|
124
|
+
"us-west-2": float(os.getenv("COST_MULTIPLIER_US_WEST_2", "1.3")),
|
125
|
+
"us-west-1": float(os.getenv("COST_MULTIPLIER_US_WEST_1", "0.8")),
|
126
|
+
"eu-west-1": float(os.getenv("COST_MULTIPLIER_EU_WEST_1", "1.2")),
|
127
|
+
"eu-central-1": float(os.getenv("COST_MULTIPLIER_EU_CENTRAL_1", "0.9")),
|
128
|
+
"eu-west-2": float(os.getenv("COST_MULTIPLIER_EU_WEST_2", "0.7")),
|
129
|
+
"ap-southeast-1": float(os.getenv("COST_MULTIPLIER_AP_SOUTHEAST_1", "1.0")),
|
130
|
+
"ap-southeast-2": float(os.getenv("COST_MULTIPLIER_AP_SOUTHEAST_2", "0.8")),
|
131
|
+
"ap-northeast-1": float(os.getenv("COST_MULTIPLIER_AP_NORTHEAST_1", "1.1")),
|
132
|
+
}
|
133
|
+
)
|
134
|
+
|
135
|
+
|
136
|
+
@dataclass
|
137
|
+
class VPCNetworkingConfig:
|
138
|
+
"""Main VPC Networking Configuration"""
|
139
|
+
|
140
|
+
# AWS Configuration
|
141
|
+
default_region: str = field(default_factory=lambda: os.getenv("AWS_DEFAULT_REGION", "us-east-1"))
|
142
|
+
|
143
|
+
# AWS Profiles
|
144
|
+
billing_profile: Optional[str] = field(default_factory=lambda: os.getenv("BILLING_PROFILE"))
|
145
|
+
centralized_ops_profile: Optional[str] = field(default_factory=lambda: os.getenv("CENTRALIZED_OPS_PROFILE"))
|
146
|
+
single_account_profile: Optional[str] = field(default_factory=lambda: os.getenv("SINGLE_ACCOUNT_PROFILE"))
|
147
|
+
management_profile: Optional[str] = field(default_factory=lambda: os.getenv("MANAGEMENT_PROFILE"))
|
148
|
+
|
149
|
+
# Analysis Configuration
|
150
|
+
default_analysis_days: int = field(default_factory=lambda: int(os.getenv("DEFAULT_ANALYSIS_DAYS", "30")))
|
151
|
+
forecast_days: int = field(default_factory=lambda: int(os.getenv("FORECAST_DAYS", "90")))
|
152
|
+
|
153
|
+
# Output Configuration
|
154
|
+
default_output_format: str = field(default_factory=lambda: os.getenv("OUTPUT_FORMAT", "rich"))
|
155
|
+
default_output_dir: Path = field(default_factory=lambda: Path(os.getenv("OUTPUT_DIR", "./exports")))
|
156
|
+
|
157
|
+
# Enterprise Configuration
|
158
|
+
enable_cost_approval_workflow: bool = field(
|
159
|
+
default_factory=lambda: os.getenv("ENABLE_COST_APPROVAL_WORKFLOW", "true").lower() == "true"
|
160
|
+
)
|
161
|
+
enable_mcp_validation: bool = field(
|
162
|
+
default_factory=lambda: os.getenv("ENABLE_MCP_VALIDATION", "false").lower() == "true"
|
163
|
+
)
|
164
|
+
|
165
|
+
# Component configurations
|
166
|
+
cost_model: AWSCostModel = field(default_factory=AWSCostModel)
|
167
|
+
thresholds: OptimizationThresholds = field(default_factory=OptimizationThresholds)
|
168
|
+
regional: RegionalConfiguration = field(default_factory=RegionalConfiguration)
|
169
|
+
|
170
|
+
def get_cost_approval_required(self, monthly_cost: float) -> bool:
|
171
|
+
"""Check if cost requires approval based on threshold"""
|
172
|
+
return self.enable_cost_approval_workflow and monthly_cost > self.thresholds.cost_approval_threshold
|
173
|
+
|
174
|
+
def get_performance_acceptable(self, execution_time: float) -> bool:
|
175
|
+
"""Check if performance meets baseline requirements"""
|
176
|
+
return execution_time <= self.thresholds.performance_baseline_threshold
|
177
|
+
|
178
|
+
def get_regional_multiplier(self, region: str) -> float:
|
179
|
+
"""Get cost multiplier for specific region"""
|
180
|
+
return self.regional.regional_multipliers.get(region, 1.0)
|
181
|
+
|
182
|
+
|
183
|
+
def load_config(config_file: Optional[str] = None) -> VPCNetworkingConfig:
|
184
|
+
"""
|
185
|
+
Load VPC networking configuration from environment and optional config file
|
186
|
+
|
187
|
+
Args:
|
188
|
+
config_file: Optional path to configuration file
|
189
|
+
|
190
|
+
Returns:
|
191
|
+
VPCNetworkingConfig instance
|
192
|
+
"""
|
193
|
+
# TODO: Add support for loading from JSON/YAML config file
|
194
|
+
# TODO: Add support for AWS Pricing API integration
|
195
|
+
|
196
|
+
config = VPCNetworkingConfig()
|
197
|
+
|
198
|
+
# Validate configuration only in production (not during testing)
|
199
|
+
is_testing = os.getenv("PYTEST_CURRENT_TEST") is not None or "pytest" in os.environ.get("_", "")
|
200
|
+
if not is_testing and config.enable_cost_approval_workflow and not config.billing_profile:
|
201
|
+
raise ValueError("BILLING_PROFILE required when cost approval workflow is enabled")
|
202
|
+
|
203
|
+
return config
|
204
|
+
|
205
|
+
|
206
|
+
# Global configuration instance (with testing environment detection)
|
207
|
+
default_config = None
|
208
|
+
try:
|
209
|
+
default_config = load_config()
|
210
|
+
except ValueError:
|
211
|
+
# Fallback configuration for testing or when validation fails
|
212
|
+
default_config = VPCNetworkingConfig(enable_cost_approval_workflow=False)
|
@@ -0,0 +1,347 @@
|
|
1
|
+
"""
|
2
|
+
Networking Cost Engine - Core cost analysis and calculation logic
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from dataclasses import dataclass, field
|
7
|
+
from datetime import datetime, timedelta
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple
|
9
|
+
|
10
|
+
import boto3
|
11
|
+
import numpy as np
|
12
|
+
from botocore.exceptions import ClientError
|
13
|
+
|
14
|
+
from .config import VPCNetworkingConfig, load_config
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class NetworkingCostEngine:
|
20
|
+
"""
|
21
|
+
Core engine for networking cost calculations and analysis
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self, session: Optional[boto3.Session] = None, config: Optional[VPCNetworkingConfig] = None):
|
25
|
+
"""
|
26
|
+
Initialize the cost engine
|
27
|
+
|
28
|
+
Args:
|
29
|
+
session: Boto3 session for AWS API calls
|
30
|
+
config: VPC networking configuration (uses default if None)
|
31
|
+
"""
|
32
|
+
self.session = session or boto3.Session()
|
33
|
+
self.config = config or load_config()
|
34
|
+
self.cost_model = self.config.cost_model
|
35
|
+
self._cost_explorer_client = None
|
36
|
+
self._cloudwatch_client = None
|
37
|
+
|
38
|
+
@property
|
39
|
+
def cost_explorer(self):
|
40
|
+
"""Lazy load Cost Explorer client"""
|
41
|
+
if not self._cost_explorer_client:
|
42
|
+
self._cost_explorer_client = self.session.client("ce", region_name="us-east-1")
|
43
|
+
return self._cost_explorer_client
|
44
|
+
|
45
|
+
@property
|
46
|
+
def cloudwatch(self):
|
47
|
+
"""Lazy load CloudWatch client"""
|
48
|
+
if not self._cloudwatch_client:
|
49
|
+
self._cloudwatch_client = self.session.client("cloudwatch")
|
50
|
+
return self._cloudwatch_client
|
51
|
+
|
52
|
+
def calculate_nat_gateway_cost(
|
53
|
+
self, nat_gateway_id: str, days: int = 30, include_data_processing: bool = True
|
54
|
+
) -> Dict[str, Any]:
|
55
|
+
"""
|
56
|
+
Calculate NAT Gateway costs
|
57
|
+
|
58
|
+
Args:
|
59
|
+
nat_gateway_id: NAT Gateway ID
|
60
|
+
days: Number of days to analyze
|
61
|
+
include_data_processing: Include data processing charges
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
Dictionary with cost breakdown
|
65
|
+
"""
|
66
|
+
cost_breakdown = {
|
67
|
+
"nat_gateway_id": nat_gateway_id,
|
68
|
+
"period_days": days,
|
69
|
+
"base_cost": 0.0,
|
70
|
+
"data_processing_cost": 0.0,
|
71
|
+
"total_cost": 0.0,
|
72
|
+
"daily_average": 0.0,
|
73
|
+
"monthly_projection": 0.0,
|
74
|
+
}
|
75
|
+
|
76
|
+
# Base cost calculation
|
77
|
+
cost_breakdown["base_cost"] = self.cost_model.nat_gateway_hourly * 24 * days
|
78
|
+
|
79
|
+
if include_data_processing:
|
80
|
+
try:
|
81
|
+
# Get data processing metrics from CloudWatch
|
82
|
+
end_time = datetime.now()
|
83
|
+
start_time = end_time - timedelta(days=days)
|
84
|
+
|
85
|
+
response = self.cloudwatch.get_metric_statistics(
|
86
|
+
Namespace="AWS/NATGateway",
|
87
|
+
MetricName="BytesOutToDestination",
|
88
|
+
Dimensions=[{"Name": "NatGatewayId", "Value": nat_gateway_id}],
|
89
|
+
StartTime=start_time,
|
90
|
+
EndTime=end_time,
|
91
|
+
Period=86400 * days,
|
92
|
+
Statistics=["Sum"],
|
93
|
+
)
|
94
|
+
|
95
|
+
if response["Datapoints"]:
|
96
|
+
total_bytes = sum([p["Sum"] for p in response["Datapoints"]])
|
97
|
+
total_gb = total_bytes / (1024**3)
|
98
|
+
cost_breakdown["data_processing_cost"] = total_gb * self.cost_model.nat_gateway_data_processing
|
99
|
+
except Exception as e:
|
100
|
+
logger.warning(f"Failed to get data processing metrics: {e}")
|
101
|
+
|
102
|
+
# Calculate totals
|
103
|
+
cost_breakdown["total_cost"] = cost_breakdown["base_cost"] + cost_breakdown["data_processing_cost"]
|
104
|
+
cost_breakdown["daily_average"] = cost_breakdown["total_cost"] / days
|
105
|
+
cost_breakdown["monthly_projection"] = cost_breakdown["daily_average"] * 30
|
106
|
+
|
107
|
+
return cost_breakdown
|
108
|
+
|
109
|
+
def calculate_vpc_endpoint_cost(
|
110
|
+
self, endpoint_type: str, availability_zones: int = 1, data_processed_gb: float = 0
|
111
|
+
) -> Dict[str, Any]:
|
112
|
+
"""
|
113
|
+
Calculate VPC Endpoint costs
|
114
|
+
|
115
|
+
Args:
|
116
|
+
endpoint_type: 'Interface' or 'Gateway'
|
117
|
+
availability_zones: Number of AZs for interface endpoints
|
118
|
+
data_processed_gb: Data processed in GB
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
Dictionary with cost breakdown
|
122
|
+
"""
|
123
|
+
cost_breakdown = {
|
124
|
+
"endpoint_type": endpoint_type,
|
125
|
+
"availability_zones": availability_zones,
|
126
|
+
"data_processed_gb": data_processed_gb,
|
127
|
+
"base_cost": 0.0,
|
128
|
+
"data_processing_cost": 0.0,
|
129
|
+
"total_monthly_cost": 0.0,
|
130
|
+
}
|
131
|
+
|
132
|
+
if endpoint_type == "Interface":
|
133
|
+
# Interface endpoints cost per AZ
|
134
|
+
cost_breakdown["base_cost"] = self.cost_model.vpc_endpoint_interface_monthly * availability_zones
|
135
|
+
cost_breakdown["data_processing_cost"] = data_processed_gb * self.cost_model.vpc_endpoint_data_processing
|
136
|
+
else:
|
137
|
+
# Gateway endpoints are free
|
138
|
+
cost_breakdown["base_cost"] = 0.0
|
139
|
+
cost_breakdown["data_processing_cost"] = 0.0
|
140
|
+
|
141
|
+
cost_breakdown["total_monthly_cost"] = cost_breakdown["base_cost"] + cost_breakdown["data_processing_cost"]
|
142
|
+
|
143
|
+
return cost_breakdown
|
144
|
+
|
145
|
+
def calculate_transit_gateway_cost(
|
146
|
+
self, attachments: int, data_processed_gb: float = 0, days: int = 30
|
147
|
+
) -> Dict[str, Any]:
|
148
|
+
"""
|
149
|
+
Calculate Transit Gateway costs
|
150
|
+
|
151
|
+
Args:
|
152
|
+
attachments: Number of attachments
|
153
|
+
data_processed_gb: Data processed in GB
|
154
|
+
days: Number of days
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
Dictionary with cost breakdown
|
158
|
+
"""
|
159
|
+
cost_breakdown = {
|
160
|
+
"attachments": attachments,
|
161
|
+
"data_processed_gb": data_processed_gb,
|
162
|
+
"base_cost": 0.0,
|
163
|
+
"attachment_cost": 0.0,
|
164
|
+
"data_processing_cost": 0.0,
|
165
|
+
"total_cost": 0.0,
|
166
|
+
"monthly_projection": 0.0,
|
167
|
+
}
|
168
|
+
|
169
|
+
# Base Transit Gateway cost
|
170
|
+
cost_breakdown["base_cost"] = self.cost_model.transit_gateway_hourly * 24 * days
|
171
|
+
|
172
|
+
# Attachment costs
|
173
|
+
cost_breakdown["attachment_cost"] = self.cost_model.transit_gateway_attachment * 24 * days * attachments
|
174
|
+
|
175
|
+
# Data processing costs
|
176
|
+
cost_breakdown["data_processing_cost"] = data_processed_gb * self.cost_model.transit_gateway_data_processing
|
177
|
+
|
178
|
+
# Calculate totals
|
179
|
+
cost_breakdown["total_cost"] = (
|
180
|
+
cost_breakdown["base_cost"] + cost_breakdown["attachment_cost"] + cost_breakdown["data_processing_cost"]
|
181
|
+
)
|
182
|
+
|
183
|
+
cost_breakdown["monthly_projection"] = cost_breakdown["total_cost"] / days * 30
|
184
|
+
|
185
|
+
return cost_breakdown
|
186
|
+
|
187
|
+
def calculate_elastic_ip_cost(self, idle_hours: int = 0, remaps: int = 0) -> Dict[str, Any]:
|
188
|
+
"""
|
189
|
+
Calculate Elastic IP costs
|
190
|
+
|
191
|
+
Args:
|
192
|
+
idle_hours: Hours the EIP was idle
|
193
|
+
remaps: Number of remaps
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
Dictionary with cost breakdown
|
197
|
+
"""
|
198
|
+
cost_breakdown = {
|
199
|
+
"idle_hours": idle_hours,
|
200
|
+
"remaps": remaps,
|
201
|
+
"idle_cost": idle_hours * self.cost_model.elastic_ip_idle_hourly,
|
202
|
+
"remap_cost": remaps * self.cost_model.elastic_ip_remap,
|
203
|
+
"total_cost": 0.0,
|
204
|
+
"monthly_projection": 0.0,
|
205
|
+
}
|
206
|
+
|
207
|
+
cost_breakdown["total_cost"] = cost_breakdown["idle_cost"] + cost_breakdown["remap_cost"]
|
208
|
+
|
209
|
+
# Project to monthly (assuming same pattern)
|
210
|
+
if idle_hours > 0:
|
211
|
+
days_analyzed = idle_hours / 24
|
212
|
+
cost_breakdown["monthly_projection"] = cost_breakdown["total_cost"] / days_analyzed * 30
|
213
|
+
else:
|
214
|
+
cost_breakdown["monthly_projection"] = cost_breakdown["total_cost"]
|
215
|
+
|
216
|
+
return cost_breakdown
|
217
|
+
|
218
|
+
def calculate_data_transfer_cost(
|
219
|
+
self, inter_az_gb: float = 0, inter_region_gb: float = 0, internet_out_gb: float = 0
|
220
|
+
) -> Dict[str, Any]:
|
221
|
+
"""
|
222
|
+
Calculate data transfer costs
|
223
|
+
|
224
|
+
Args:
|
225
|
+
inter_az_gb: Inter-AZ transfer in GB
|
226
|
+
inter_region_gb: Inter-region transfer in GB
|
227
|
+
internet_out_gb: Internet outbound transfer in GB
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
Dictionary with cost breakdown
|
231
|
+
"""
|
232
|
+
cost_breakdown = {
|
233
|
+
"inter_az_gb": inter_az_gb,
|
234
|
+
"inter_region_gb": inter_region_gb,
|
235
|
+
"internet_out_gb": internet_out_gb,
|
236
|
+
"inter_az_cost": inter_az_gb * self.cost_model.data_transfer_inter_az,
|
237
|
+
"inter_region_cost": inter_region_gb * self.cost_model.data_transfer_inter_region,
|
238
|
+
"internet_out_cost": internet_out_gb * self.cost_model.data_transfer_internet_out,
|
239
|
+
"total_cost": 0.0,
|
240
|
+
}
|
241
|
+
|
242
|
+
cost_breakdown["total_cost"] = (
|
243
|
+
cost_breakdown["inter_az_cost"] + cost_breakdown["inter_region_cost"] + cost_breakdown["internet_out_cost"]
|
244
|
+
)
|
245
|
+
|
246
|
+
return cost_breakdown
|
247
|
+
|
248
|
+
def get_actual_costs_from_cost_explorer(
|
249
|
+
self, service: str, start_date: str, end_date: str, granularity: str = "MONTHLY"
|
250
|
+
) -> Dict[str, Any]:
|
251
|
+
"""
|
252
|
+
Get actual costs from AWS Cost Explorer
|
253
|
+
|
254
|
+
Args:
|
255
|
+
service: AWS service name
|
256
|
+
start_date: Start date (YYYY-MM-DD)
|
257
|
+
end_date: End date (YYYY-MM-DD)
|
258
|
+
granularity: DAILY, MONTHLY, or HOURLY
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
Dictionary with actual cost data
|
262
|
+
"""
|
263
|
+
try:
|
264
|
+
response = self.cost_explorer.get_cost_and_usage(
|
265
|
+
TimePeriod={"Start": start_date, "End": end_date},
|
266
|
+
Granularity=granularity,
|
267
|
+
Metrics=["BlendedCost", "UnblendedCost"],
|
268
|
+
Filter={"Dimensions": {"Key": "SERVICE", "Values": [service]}},
|
269
|
+
)
|
270
|
+
|
271
|
+
cost_data = {
|
272
|
+
"service": service,
|
273
|
+
"period": f"{start_date} to {end_date}",
|
274
|
+
"granularity": granularity,
|
275
|
+
"total_cost": 0.0,
|
276
|
+
"results_by_time": [],
|
277
|
+
}
|
278
|
+
|
279
|
+
for result in response["ResultsByTime"]:
|
280
|
+
period_cost = float(result["Total"]["BlendedCost"]["Amount"])
|
281
|
+
cost_data["total_cost"] += period_cost
|
282
|
+
cost_data["results_by_time"].append(
|
283
|
+
{
|
284
|
+
"start": result["TimePeriod"]["Start"],
|
285
|
+
"end": result["TimePeriod"]["End"],
|
286
|
+
"cost": period_cost,
|
287
|
+
"unit": result["Total"]["BlendedCost"]["Unit"],
|
288
|
+
}
|
289
|
+
)
|
290
|
+
|
291
|
+
return cost_data
|
292
|
+
|
293
|
+
except Exception as e:
|
294
|
+
logger.error(f"Failed to get costs from Cost Explorer: {e}")
|
295
|
+
return {"service": service, "error": str(e), "total_cost": 0.0}
|
296
|
+
|
297
|
+
def estimate_optimization_savings(
|
298
|
+
self, current_costs: Dict[str, float], optimization_scenarios: List[Dict[str, Any]]
|
299
|
+
) -> Dict[str, Any]:
|
300
|
+
"""
|
301
|
+
Estimate savings from optimization scenarios
|
302
|
+
|
303
|
+
Args:
|
304
|
+
current_costs: Current cost breakdown by service
|
305
|
+
optimization_scenarios: List of optimization scenarios
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
Dictionary with savings estimates
|
309
|
+
"""
|
310
|
+
total_current = sum(current_costs.values())
|
311
|
+
|
312
|
+
savings_analysis = {
|
313
|
+
"current_monthly_cost": total_current,
|
314
|
+
"scenarios": [],
|
315
|
+
"recommended_scenario": None,
|
316
|
+
"maximum_savings": 0.0,
|
317
|
+
}
|
318
|
+
|
319
|
+
for scenario in optimization_scenarios:
|
320
|
+
scenario_savings = 0.0
|
321
|
+
optimized_costs = current_costs.copy()
|
322
|
+
|
323
|
+
# Apply optimization percentages
|
324
|
+
for service, reduction_pct in scenario.get("reductions", {}).items():
|
325
|
+
if service in optimized_costs:
|
326
|
+
savings = optimized_costs[service] * (reduction_pct / 100)
|
327
|
+
scenario_savings += savings
|
328
|
+
optimized_costs[service] -= savings
|
329
|
+
|
330
|
+
scenario_result = {
|
331
|
+
"name": scenario.get("name", "Unnamed"),
|
332
|
+
"description": scenario.get("description", ""),
|
333
|
+
"monthly_savings": scenario_savings,
|
334
|
+
"annual_savings": scenario_savings * 12,
|
335
|
+
"new_monthly_cost": total_current - scenario_savings,
|
336
|
+
"savings_percentage": (scenario_savings / total_current) * 100 if total_current > 0 else 0,
|
337
|
+
"risk_level": scenario.get("risk_level", "medium"),
|
338
|
+
"implementation_effort": scenario.get("effort", "medium"),
|
339
|
+
}
|
340
|
+
|
341
|
+
savings_analysis["scenarios"].append(scenario_result)
|
342
|
+
|
343
|
+
if scenario_savings > savings_analysis["maximum_savings"]:
|
344
|
+
savings_analysis["maximum_savings"] = scenario_savings
|
345
|
+
savings_analysis["recommended_scenario"] = scenario_result
|
346
|
+
|
347
|
+
return savings_analysis
|