runbooks 0.7.6__py3-none-any.whl → 0.7.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/base.py +5 -1
  3. runbooks/cfat/__init__.py +8 -4
  4. runbooks/cfat/assessment/collectors.py +171 -14
  5. runbooks/cfat/assessment/compliance.py +871 -0
  6. runbooks/cfat/assessment/runner.py +122 -11
  7. runbooks/cfat/models.py +6 -2
  8. runbooks/common/logger.py +14 -0
  9. runbooks/common/rich_utils.py +451 -0
  10. runbooks/enterprise/__init__.py +68 -0
  11. runbooks/enterprise/error_handling.py +411 -0
  12. runbooks/enterprise/logging.py +439 -0
  13. runbooks/enterprise/multi_tenant.py +583 -0
  14. runbooks/finops/README.md +468 -241
  15. runbooks/finops/__init__.py +39 -3
  16. runbooks/finops/cli.py +83 -18
  17. runbooks/finops/cross_validation.py +375 -0
  18. runbooks/finops/dashboard_runner.py +812 -164
  19. runbooks/finops/enhanced_dashboard_runner.py +525 -0
  20. runbooks/finops/finops_dashboard.py +1892 -0
  21. runbooks/finops/helpers.py +485 -51
  22. runbooks/finops/optimizer.py +823 -0
  23. runbooks/finops/tests/__init__.py +19 -0
  24. runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
  25. runbooks/finops/tests/run_comprehensive_tests.py +421 -0
  26. runbooks/finops/tests/run_tests.py +305 -0
  27. runbooks/finops/tests/test_finops_dashboard.py +705 -0
  28. runbooks/finops/tests/test_integration.py +477 -0
  29. runbooks/finops/tests/test_performance.py +380 -0
  30. runbooks/finops/tests/test_performance_benchmarks.py +500 -0
  31. runbooks/finops/tests/test_reference_images_validation.py +867 -0
  32. runbooks/finops/tests/test_single_account_features.py +715 -0
  33. runbooks/finops/tests/validate_test_suite.py +220 -0
  34. runbooks/finops/types.py +1 -1
  35. runbooks/hitl/enhanced_workflow_engine.py +725 -0
  36. runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
  37. runbooks/inventory/collectors/aws_comprehensive.py +442 -0
  38. runbooks/inventory/collectors/enterprise_scale.py +281 -0
  39. runbooks/inventory/core/collector.py +172 -13
  40. runbooks/inventory/discovery.md +1 -1
  41. runbooks/inventory/list_ec2_instances.py +18 -20
  42. runbooks/inventory/list_ssm_parameters.py +31 -3
  43. runbooks/inventory/organizations_discovery.py +1269 -0
  44. runbooks/inventory/rich_inventory_display.py +393 -0
  45. runbooks/inventory/run_on_multi_accounts.py +35 -19
  46. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  47. runbooks/inventory/runbooks.security.run_script.log +0 -0
  48. runbooks/inventory/vpc_flow_analyzer.py +1030 -0
  49. runbooks/main.py +2215 -119
  50. runbooks/metrics/dora_metrics_engine.py +599 -0
  51. runbooks/operate/__init__.py +2 -2
  52. runbooks/operate/base.py +122 -10
  53. runbooks/operate/deployment_framework.py +1032 -0
  54. runbooks/operate/deployment_validator.py +853 -0
  55. runbooks/operate/dynamodb_operations.py +10 -6
  56. runbooks/operate/ec2_operations.py +319 -11
  57. runbooks/operate/executive_dashboard.py +779 -0
  58. runbooks/operate/mcp_integration.py +750 -0
  59. runbooks/operate/nat_gateway_operations.py +1120 -0
  60. runbooks/operate/networking_cost_heatmap.py +685 -0
  61. runbooks/operate/privatelink_operations.py +940 -0
  62. runbooks/operate/s3_operations.py +10 -6
  63. runbooks/operate/vpc_endpoints.py +644 -0
  64. runbooks/operate/vpc_operations.py +1038 -0
  65. runbooks/remediation/__init__.py +2 -2
  66. runbooks/remediation/acm_remediation.py +1 -1
  67. runbooks/remediation/base.py +1 -1
  68. runbooks/remediation/cloudtrail_remediation.py +1 -1
  69. runbooks/remediation/cognito_remediation.py +1 -1
  70. runbooks/remediation/dynamodb_remediation.py +1 -1
  71. runbooks/remediation/ec2_remediation.py +1 -1
  72. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  73. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  74. runbooks/remediation/kms_remediation.py +1 -1
  75. runbooks/remediation/lambda_remediation.py +1 -1
  76. runbooks/remediation/multi_account.py +1 -1
  77. runbooks/remediation/rds_remediation.py +1 -1
  78. runbooks/remediation/s3_block_public_access.py +1 -1
  79. runbooks/remediation/s3_enable_access_logging.py +1 -1
  80. runbooks/remediation/s3_encryption.py +1 -1
  81. runbooks/remediation/s3_remediation.py +1 -1
  82. runbooks/remediation/vpc_remediation.py +475 -0
  83. runbooks/security/__init__.py +3 -1
  84. runbooks/security/compliance_automation.py +632 -0
  85. runbooks/security/report_generator.py +10 -0
  86. runbooks/security/run_script.py +31 -5
  87. runbooks/security/security_baseline_tester.py +169 -30
  88. runbooks/security/security_export.py +477 -0
  89. runbooks/validation/__init__.py +10 -0
  90. runbooks/validation/benchmark.py +484 -0
  91. runbooks/validation/cli.py +356 -0
  92. runbooks/validation/mcp_validator.py +768 -0
  93. runbooks/vpc/__init__.py +38 -0
  94. runbooks/vpc/config.py +212 -0
  95. runbooks/vpc/cost_engine.py +347 -0
  96. runbooks/vpc/heatmap_engine.py +605 -0
  97. runbooks/vpc/manager_interface.py +634 -0
  98. runbooks/vpc/networking_wrapper.py +1260 -0
  99. runbooks/vpc/rich_formatters.py +679 -0
  100. runbooks/vpc/tests/__init__.py +5 -0
  101. runbooks/vpc/tests/conftest.py +356 -0
  102. runbooks/vpc/tests/test_cli_integration.py +530 -0
  103. runbooks/vpc/tests/test_config.py +458 -0
  104. runbooks/vpc/tests/test_cost_engine.py +479 -0
  105. runbooks/vpc/tests/test_networking_wrapper.py +512 -0
  106. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
  107. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
  108. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
  109. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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