runbooks 1.1.3__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/WEIGHT_CONFIG_README.md +1 -1
- runbooks/cfat/assessment/compliance.py +8 -8
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cfat/models.py +6 -2
- runbooks/cfat/tests/__init__.py +6 -1
- runbooks/cli/__init__.py +13 -0
- runbooks/cli/commands/cfat.py +274 -0
- runbooks/cli/commands/finops.py +1164 -0
- runbooks/cli/commands/inventory.py +379 -0
- runbooks/cli/commands/operate.py +239 -0
- runbooks/cli/commands/security.py +248 -0
- runbooks/cli/commands/validation.py +825 -0
- runbooks/cli/commands/vpc.py +310 -0
- runbooks/cli/registry.py +107 -0
- runbooks/cloudops/__init__.py +23 -30
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +549 -547
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +226 -227
- 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 +179 -215
- 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 +341 -0
- runbooks/common/aws_utils.py +75 -80
- runbooks/common/business_logic.py +127 -105
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
- runbooks/common/cross_account_manager.py +198 -205
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +235 -0
- 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 +478 -495
- 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 +176 -194
- runbooks/common/patterns.py +204 -0
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +248 -39
- runbooks/common/rich_utils.py +643 -92
- 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 +29 -33
- 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 +488 -622
- 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 +40 -37
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +230 -292
- 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 +338 -175
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1513 -482
- 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 +25 -29
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +77 -78
- runbooks/finops/scenarios.py +1278 -439
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/tests/test_finops_dashboard.py +3 -3
- runbooks/finops/tests/test_reference_images_validation.py +2 -2
- runbooks/finops/tests/test_single_account_features.py +17 -17
- runbooks/finops/tests/validate_test_suite.py +1 -1
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +263 -269
- runbooks/finops/vpc_cleanup_exporter.py +191 -146
- runbooks/finops/vpc_cleanup_optimizer.py +593 -575
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/hitl/enhanced_workflow_engine.py +1 -1
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/README.md +3 -3
- runbooks/inventory/Tests/common_test_data.py +30 -30
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +28 -11
- runbooks/inventory/collectors/aws_networking.py +111 -101
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/discovery.md +2 -2
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/find_ec2_security_groups.py +1 -1
- 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 +56 -52
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +733 -696
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +3 -3
- runbooks/main.py +152 -9147
- 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/metrics/dora_metrics_engine.py +2 -2
- 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/mcp_integration.py +1 -1
- runbooks/operate/networking_cost_heatmap.py +33 -10
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/operate/vpc_operations.py +648 -618
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +71 -67
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +91 -65
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +49 -44
- 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/integration_test_enterprise_security.py +5 -3
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/run_script.py +1 -1
- 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/mcp_reliability_engine.py +6 -6
- 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 +51 -48
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +754 -708
- 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 +190 -162
- runbooks/vpc/mcp_no_eni_validator.py +681 -640
- 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 +1302 -1129
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
- 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 -956
- 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.3.dist-info/METADATA +0 -799
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -16,361 +16,355 @@ from functools import lru_cache
|
|
16
16
|
from datetime import datetime, timedelta
|
17
17
|
import os
|
18
18
|
|
19
|
+
|
19
20
|
class AWSPricingAPI:
|
20
21
|
"""Real-time AWS Pricing API integration - ZERO hardcoded values."""
|
21
|
-
|
22
|
+
|
22
23
|
def __init__(self, profile: Optional[str] = None):
|
23
24
|
"""Initialize with AWS Pricing API client."""
|
24
25
|
session = boto3.Session(profile_name=profile) if profile else boto3.Session()
|
25
|
-
self.pricing_client = session.client(
|
26
|
-
self.ce_client = session.client(
|
26
|
+
self.pricing_client = session.client("pricing", region_name="us-east-1")
|
27
|
+
self.ce_client = session.client("ce") # Cost Explorer for real costs
|
27
28
|
self._cache = {}
|
28
29
|
self._cache_expiry = {}
|
29
|
-
|
30
|
+
|
30
31
|
@lru_cache(maxsize=128)
|
31
|
-
def get_ebs_gp3_cost_per_gb(self, region: str =
|
32
|
+
def get_ebs_gp3_cost_per_gb(self, region: str = "us-east-1") -> float:
|
32
33
|
"""Get real-time EBS GP3 cost per GB per month from AWS Pricing API."""
|
33
34
|
try:
|
34
35
|
response = self.pricing_client.get_products(
|
35
|
-
ServiceCode=
|
36
|
+
ServiceCode="AmazonEC2",
|
36
37
|
Filters=[
|
37
|
-
{
|
38
|
-
{
|
39
|
-
{
|
40
|
-
{
|
41
|
-
{
|
38
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage"},
|
39
|
+
{"Type": "TERM_MATCH", "Field": "volumeType", "Value": "General Purpose"},
|
40
|
+
{"Type": "TERM_MATCH", "Field": "storageMedia", "Value": "SSD-backed"},
|
41
|
+
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "gp3"},
|
42
|
+
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
|
42
43
|
],
|
43
|
-
MaxResults=1
|
44
|
+
MaxResults=1,
|
44
45
|
)
|
45
|
-
|
46
|
-
if response[
|
47
|
-
price_data = json.loads(response[
|
48
|
-
on_demand = price_data[
|
46
|
+
|
47
|
+
if response["PriceList"]:
|
48
|
+
price_data = json.loads(response["PriceList"][0])
|
49
|
+
on_demand = price_data["terms"]["OnDemand"]
|
49
50
|
for term in on_demand.values():
|
50
|
-
for price_dimension in term[
|
51
|
-
if
|
52
|
-
return float(price_dimension[
|
53
|
-
|
51
|
+
for price_dimension in term["priceDimensions"].values():
|
52
|
+
if "GB-month" in price_dimension.get("unit", ""):
|
53
|
+
return float(price_dimension["pricePerUnit"]["USD"])
|
54
|
+
|
54
55
|
# Fallback to Cost Explorer actual costs if Pricing API fails
|
55
|
-
return self._get_from_cost_explorer(
|
56
|
-
|
56
|
+
return self._get_from_cost_explorer("EBS", "gp3")
|
57
|
+
|
57
58
|
except Exception as e:
|
58
59
|
# Use Cost Explorer as ultimate fallback
|
59
|
-
return self._get_from_cost_explorer(
|
60
|
-
|
60
|
+
return self._get_from_cost_explorer("EBS", "gp3")
|
61
|
+
|
61
62
|
@lru_cache(maxsize=128)
|
62
|
-
def get_ebs_gp2_cost_per_gb(self, region: str =
|
63
|
+
def get_ebs_gp2_cost_per_gb(self, region: str = "us-east-1") -> float:
|
63
64
|
"""Get real-time EBS GP2 cost per GB per month from AWS Pricing API."""
|
64
65
|
try:
|
65
66
|
response = self.pricing_client.get_products(
|
66
|
-
ServiceCode=
|
67
|
+
ServiceCode="AmazonEC2",
|
67
68
|
Filters=[
|
68
|
-
{
|
69
|
-
{
|
70
|
-
{
|
71
|
-
{
|
69
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage"},
|
70
|
+
{"Type": "TERM_MATCH", "Field": "volumeType", "Value": "General Purpose"},
|
71
|
+
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "gp2"},
|
72
|
+
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
|
72
73
|
],
|
73
|
-
MaxResults=1
|
74
|
+
MaxResults=1,
|
74
75
|
)
|
75
|
-
|
76
|
-
if response[
|
77
|
-
price_data = json.loads(response[
|
78
|
-
on_demand = price_data[
|
76
|
+
|
77
|
+
if response["PriceList"]:
|
78
|
+
price_data = json.loads(response["PriceList"][0])
|
79
|
+
on_demand = price_data["terms"]["OnDemand"]
|
79
80
|
for term in on_demand.values():
|
80
|
-
for price_dimension in term[
|
81
|
-
if
|
82
|
-
return float(price_dimension[
|
83
|
-
|
84
|
-
return self._get_from_cost_explorer(
|
85
|
-
|
81
|
+
for price_dimension in term["priceDimensions"].values():
|
82
|
+
if "GB-month" in price_dimension.get("unit", ""):
|
83
|
+
return float(price_dimension["pricePerUnit"]["USD"])
|
84
|
+
|
85
|
+
return self._get_from_cost_explorer("EBS", "gp2")
|
86
|
+
|
86
87
|
except Exception:
|
87
|
-
return self._get_from_cost_explorer(
|
88
|
-
|
88
|
+
return self._get_from_cost_explorer("EBS", "gp2")
|
89
|
+
|
89
90
|
@lru_cache(maxsize=128)
|
90
|
-
def get_rds_snapshot_cost_per_gb(self, region: str =
|
91
|
+
def get_rds_snapshot_cost_per_gb(self, region: str = "us-east-1") -> float:
|
91
92
|
"""Get real-time RDS snapshot cost per GB per month from AWS Pricing API."""
|
92
93
|
try:
|
93
94
|
response = self.pricing_client.get_products(
|
94
|
-
ServiceCode=
|
95
|
+
ServiceCode="AmazonRDS",
|
95
96
|
Filters=[
|
96
|
-
{
|
97
|
-
{
|
97
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage Snapshot"},
|
98
|
+
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
|
98
99
|
],
|
99
|
-
MaxResults=1
|
100
|
+
MaxResults=1,
|
100
101
|
)
|
101
|
-
|
102
|
-
if response[
|
103
|
-
price_data = json.loads(response[
|
104
|
-
on_demand = price_data[
|
102
|
+
|
103
|
+
if response["PriceList"]:
|
104
|
+
price_data = json.loads(response["PriceList"][0])
|
105
|
+
on_demand = price_data["terms"]["OnDemand"]
|
105
106
|
for term in on_demand.values():
|
106
|
-
for price_dimension in term[
|
107
|
-
if
|
108
|
-
return float(price_dimension[
|
109
|
-
|
110
|
-
return self._get_from_cost_explorer(
|
111
|
-
|
107
|
+
for price_dimension in term["priceDimensions"].values():
|
108
|
+
if "GB-month" in price_dimension.get("unit", ""):
|
109
|
+
return float(price_dimension["pricePerUnit"]["USD"])
|
110
|
+
|
111
|
+
return self._get_from_cost_explorer("RDS", "Snapshot")
|
112
|
+
|
112
113
|
except Exception:
|
113
|
-
return self._get_from_cost_explorer(
|
114
|
-
|
114
|
+
return self._get_from_cost_explorer("RDS", "Snapshot")
|
115
|
+
|
115
116
|
@lru_cache(maxsize=128)
|
116
|
-
def get_nat_gateway_monthly_cost(self, region: str =
|
117
|
+
def get_nat_gateway_monthly_cost(self, region: str = "us-east-1") -> float:
|
117
118
|
"""Get real-time NAT Gateway monthly cost from AWS Pricing API with enterprise regional fallback."""
|
118
|
-
|
119
|
+
|
119
120
|
# Enterprise Regional Fallback Strategy
|
120
|
-
fallback_regions = [
|
121
|
+
fallback_regions = ["us-east-1", "us-west-2", "eu-west-1"]
|
121
122
|
if region not in fallback_regions:
|
122
123
|
fallback_regions.insert(0, region)
|
123
|
-
|
124
|
+
|
124
125
|
last_error = None
|
125
|
-
|
126
|
+
|
126
127
|
for attempt_region in fallback_regions:
|
127
128
|
try:
|
128
129
|
# Try AWS Pricing API for this region
|
129
130
|
response = self.pricing_client.get_products(
|
130
|
-
ServiceCode=
|
131
|
+
ServiceCode="AmazonVPC",
|
131
132
|
Filters=[
|
132
|
-
{
|
133
|
-
{
|
133
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "NAT Gateway"},
|
134
|
+
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(attempt_region)},
|
134
135
|
],
|
135
|
-
MaxResults=1
|
136
|
+
MaxResults=1,
|
136
137
|
)
|
137
|
-
|
138
|
-
if response[
|
139
|
-
price_data = json.loads(response[
|
140
|
-
on_demand = price_data[
|
138
|
+
|
139
|
+
if response["PriceList"]:
|
140
|
+
price_data = json.loads(response["PriceList"][0])
|
141
|
+
on_demand = price_data["terms"]["OnDemand"]
|
141
142
|
for term in on_demand.values():
|
142
|
-
for price_dimension in term[
|
143
|
-
if
|
144
|
-
hourly_rate = float(price_dimension[
|
143
|
+
for price_dimension in term["priceDimensions"].values():
|
144
|
+
if "Hrs" in price_dimension.get("unit", ""):
|
145
|
+
hourly_rate = float(price_dimension["pricePerUnit"]["USD"])
|
145
146
|
monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
|
146
147
|
print(f"✅ NAT Gateway pricing: ${monthly_cost:.2f}/month from {attempt_region}")
|
147
148
|
return monthly_cost
|
148
|
-
|
149
|
+
|
149
150
|
# Try Cost Explorer for this region
|
150
|
-
ce_cost = self._get_from_cost_explorer(
|
151
|
+
ce_cost = self._get_from_cost_explorer("VPC", "NAT Gateway", attempt_region)
|
151
152
|
if ce_cost > 0:
|
152
153
|
print(f"✅ NAT Gateway pricing: ${ce_cost:.2f}/month from Cost Explorer")
|
153
154
|
return ce_cost
|
154
|
-
|
155
|
+
|
155
156
|
except Exception as e:
|
156
157
|
last_error = e
|
157
158
|
print(f"⚠️ Pricing API failed for region {attempt_region}: {e}")
|
158
159
|
continue
|
159
|
-
|
160
|
+
|
160
161
|
# Enterprise fallback with graceful degradation
|
161
|
-
return self._get_enterprise_fallback_pricing(
|
162
|
-
|
162
|
+
return self._get_enterprise_fallback_pricing("nat_gateway", region, last_error)
|
163
|
+
|
163
164
|
def _get_from_cost_explorer(self, service: str, resource_type: str, region: str = None) -> float:
|
164
165
|
"""Get actual costs from Cost Explorer as ultimate source of truth."""
|
165
166
|
try:
|
166
167
|
end_date = datetime.now()
|
167
168
|
start_date = end_date - timedelta(days=30)
|
168
|
-
|
169
|
+
|
169
170
|
# Build filter with optional region
|
170
|
-
filter_conditions = [
|
171
|
-
|
172
|
-
]
|
173
|
-
|
171
|
+
filter_conditions = [{"Dimensions": {"Key": "SERVICE", "Values": [f"Amazon {service}"]}}]
|
172
|
+
|
174
173
|
if region:
|
175
|
-
filter_conditions.append({
|
176
|
-
|
177
|
-
})
|
178
|
-
|
174
|
+
filter_conditions.append({"Dimensions": {"Key": "REGION", "Values": [region]}})
|
175
|
+
|
179
176
|
# Add resource type filter if it helps
|
180
177
|
if resource_type != service:
|
181
|
-
filter_conditions.append({
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
cost_filter = {'And': filter_conditions} if len(filter_conditions) > 1 else filter_conditions[0]
|
186
|
-
|
178
|
+
filter_conditions.append({"Dimensions": {"Key": "USAGE_TYPE_GROUP", "Values": [resource_type]}})
|
179
|
+
|
180
|
+
cost_filter = {"And": filter_conditions} if len(filter_conditions) > 1 else filter_conditions[0]
|
181
|
+
|
187
182
|
response = self.ce_client.get_cost_and_usage(
|
188
|
-
TimePeriod={
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
Granularity='MONTHLY',
|
193
|
-
Metrics=['UnblendedCost'],
|
194
|
-
Filter=cost_filter
|
183
|
+
TimePeriod={"Start": start_date.strftime("%Y-%m-%d"), "End": end_date.strftime("%Y-%m-%d")},
|
184
|
+
Granularity="MONTHLY",
|
185
|
+
Metrics=["UnblendedCost"],
|
186
|
+
Filter=cost_filter,
|
195
187
|
)
|
196
|
-
|
197
|
-
if response[
|
198
|
-
total_cost = float(response[
|
188
|
+
|
189
|
+
if response["ResultsByTime"] and response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"]:
|
190
|
+
total_cost = float(response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"])
|
199
191
|
if total_cost > 0:
|
200
192
|
# Calculate per-unit cost based on usage
|
201
193
|
return self._calculate_unit_cost(total_cost, service, resource_type)
|
202
|
-
|
194
|
+
|
203
195
|
return 0.0 # No cost data found
|
204
|
-
|
196
|
+
|
205
197
|
except Exception as e:
|
206
198
|
print(f"⚠️ Cost Explorer query failed: {e}")
|
207
199
|
return 0.0
|
208
|
-
|
200
|
+
|
209
201
|
def _calculate_unit_cost(self, total_cost: float, service: str, resource_type: str) -> float:
|
210
202
|
"""Calculate per-unit cost from total cost and usage metrics."""
|
211
203
|
# This would query CloudWatch for usage metrics and calculate unit cost
|
212
204
|
# For now, returning calculated estimates based on typical usage patterns
|
213
205
|
usage_multipliers = {
|
214
|
-
|
215
|
-
|
216
|
-
|
206
|
+
"EBS": {"gp3": 1000, "gp2": 1200}, # Typical GB usage
|
207
|
+
"RDS": {"Snapshot": 5000}, # Typical snapshot GB
|
208
|
+
"VPC": {"NAT Gateway": 1}, # Per gateway
|
217
209
|
}
|
218
|
-
|
210
|
+
|
219
211
|
divisor = usage_multipliers.get(service, {}).get(resource_type, 1000)
|
220
212
|
return total_cost / divisor
|
221
|
-
|
213
|
+
|
222
214
|
def _get_enterprise_fallback_pricing(self, resource_type: str, region: str, last_error: Exception = None) -> float:
|
223
215
|
"""Enterprise-compliant fallback pricing with graceful degradation."""
|
224
|
-
|
216
|
+
|
225
217
|
# Check for enterprise configuration override
|
226
218
|
override_env = f"AWS_PRICING_OVERRIDE_{resource_type.upper()}_MONTHLY"
|
227
219
|
override_value = os.getenv(override_env)
|
228
220
|
if override_value:
|
229
221
|
print(f"💼 Using enterprise pricing override: ${override_value}/month")
|
230
222
|
return float(override_value)
|
231
|
-
|
223
|
+
|
232
224
|
# Check if running in compliance-mode or analysis can proceed with warnings
|
233
225
|
compliance_mode = os.getenv("AWS_PRICING_STRICT_COMPLIANCE", "false").lower() == "true"
|
234
|
-
|
226
|
+
|
235
227
|
if compliance_mode:
|
236
228
|
# Strict compliance: block operation
|
237
|
-
error_msg =
|
238
|
-
|
229
|
+
error_msg = (
|
230
|
+
f"ENTERPRISE VIOLATION: Cannot proceed without dynamic {resource_type} pricing. "
|
231
|
+
f"Last error: {last_error}. Set {override_env} or enable fallback pricing."
|
232
|
+
)
|
239
233
|
print(f"🚫 {error_msg}")
|
240
234
|
raise ValueError(error_msg)
|
241
235
|
else:
|
242
236
|
# Graceful degradation: allow analysis with standard AWS rates (documented approach)
|
243
237
|
standard_rates = {
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
238
|
+
"nat_gateway": 32.40, # AWS standard us-east-1 rate: $0.045/hour * 24 * 30
|
239
|
+
"transit_gateway": 36.00, # AWS standard us-east-1 rate: $0.05/hour * 24 * 30
|
240
|
+
"vpc_endpoint_interface": 7.20, # AWS standard us-east-1 rate: $0.01/hour * 24 * 30
|
241
|
+
"elastic_ip_idle": 3.60, # AWS standard us-east-1 rate: $0.005/hour * 24 * 30
|
248
242
|
}
|
249
|
-
|
243
|
+
|
250
244
|
if resource_type in standard_rates:
|
251
245
|
fallback_cost = standard_rates[resource_type]
|
252
246
|
print(f"⚠️ FALLBACK PRICING: Using standard AWS rate ${fallback_cost}/month for {resource_type}")
|
253
247
|
print(f" ℹ️ To fix: Check IAM permissions for pricing:GetProducts and ce:GetCostAndUsage")
|
254
248
|
print(f" ℹ️ Or set {override_env} for enterprise override")
|
255
249
|
return fallback_cost
|
256
|
-
|
257
|
-
# Last resort: query MCP servers for validation
|
250
|
+
|
251
|
+
# Last resort: query MCP servers for validation
|
258
252
|
return self._query_mcp_servers(resource_type, region, last_error)
|
259
|
-
|
253
|
+
|
260
254
|
@lru_cache(maxsize=128)
|
261
|
-
def get_vpc_endpoint_monthly_cost(self, region: str =
|
255
|
+
def get_vpc_endpoint_monthly_cost(self, region: str = "us-east-1") -> float:
|
262
256
|
"""Get real-time VPC Endpoint monthly cost from AWS Pricing API."""
|
263
257
|
try:
|
264
258
|
response = self.pricing_client.get_products(
|
265
|
-
ServiceCode=
|
259
|
+
ServiceCode="AmazonVPC",
|
266
260
|
Filters=[
|
267
|
-
{
|
268
|
-
{
|
261
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "VpcEndpoint"},
|
262
|
+
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
|
269
263
|
],
|
270
|
-
MaxResults=1
|
264
|
+
MaxResults=1,
|
271
265
|
)
|
272
|
-
|
273
|
-
if response[
|
274
|
-
price_data = json.loads(response[
|
275
|
-
on_demand = price_data[
|
266
|
+
|
267
|
+
if response["PriceList"]:
|
268
|
+
price_data = json.loads(response["PriceList"][0])
|
269
|
+
on_demand = price_data["terms"]["OnDemand"]
|
276
270
|
for term in on_demand.values():
|
277
|
-
for price_dimension in term[
|
278
|
-
if
|
279
|
-
hourly_rate = float(price_dimension[
|
271
|
+
for price_dimension in term["priceDimensions"].values():
|
272
|
+
if "Hrs" in price_dimension.get("unit", ""):
|
273
|
+
hourly_rate = float(price_dimension["pricePerUnit"]["USD"])
|
280
274
|
monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
|
281
275
|
return monthly_cost
|
282
|
-
|
276
|
+
|
283
277
|
# Fallback to Cost Explorer
|
284
|
-
return self._get_from_cost_explorer(
|
285
|
-
|
278
|
+
return self._get_from_cost_explorer("VPC", "VpcEndpoint", region)
|
279
|
+
|
286
280
|
except Exception as e:
|
287
|
-
return self._get_from_cost_explorer(
|
281
|
+
return self._get_from_cost_explorer("VPC", "VpcEndpoint", region)
|
288
282
|
|
289
283
|
@lru_cache(maxsize=128)
|
290
|
-
def get_transit_gateway_monthly_cost(self, region: str =
|
284
|
+
def get_transit_gateway_monthly_cost(self, region: str = "us-east-1") -> float:
|
291
285
|
"""Get real-time Transit Gateway monthly cost from AWS Pricing API."""
|
292
286
|
try:
|
293
287
|
response = self.pricing_client.get_products(
|
294
|
-
ServiceCode=
|
288
|
+
ServiceCode="AmazonVPC",
|
295
289
|
Filters=[
|
296
|
-
{
|
297
|
-
{
|
290
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Transit Gateway"},
|
291
|
+
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
|
298
292
|
],
|
299
|
-
MaxResults=1
|
293
|
+
MaxResults=1,
|
300
294
|
)
|
301
|
-
|
302
|
-
if response[
|
303
|
-
price_data = json.loads(response[
|
304
|
-
on_demand = price_data[
|
295
|
+
|
296
|
+
if response["PriceList"]:
|
297
|
+
price_data = json.loads(response["PriceList"][0])
|
298
|
+
on_demand = price_data["terms"]["OnDemand"]
|
305
299
|
for term in on_demand.values():
|
306
|
-
for price_dimension in term[
|
307
|
-
if
|
308
|
-
hourly_rate = float(price_dimension[
|
300
|
+
for price_dimension in term["priceDimensions"].values():
|
301
|
+
if "Hrs" in price_dimension.get("unit", ""):
|
302
|
+
hourly_rate = float(price_dimension["pricePerUnit"]["USD"])
|
309
303
|
monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
|
310
304
|
return monthly_cost
|
311
|
-
|
305
|
+
|
312
306
|
# Fallback to Cost Explorer
|
313
|
-
return self._get_from_cost_explorer(
|
314
|
-
|
307
|
+
return self._get_from_cost_explorer("VPC", "Transit Gateway", region)
|
308
|
+
|
315
309
|
except Exception as e:
|
316
|
-
return self._get_from_cost_explorer(
|
310
|
+
return self._get_from_cost_explorer("VPC", "Transit Gateway", region)
|
317
311
|
|
318
312
|
@lru_cache(maxsize=128)
|
319
|
-
def get_elastic_ip_monthly_cost(self, region: str =
|
313
|
+
def get_elastic_ip_monthly_cost(self, region: str = "us-east-1") -> float:
|
320
314
|
"""Get real-time Elastic IP monthly cost from AWS Pricing API."""
|
321
315
|
try:
|
322
316
|
response = self.pricing_client.get_products(
|
323
|
-
ServiceCode=
|
317
|
+
ServiceCode="AmazonEC2",
|
324
318
|
Filters=[
|
325
|
-
{
|
326
|
-
{
|
319
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "IP Address"},
|
320
|
+
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
|
327
321
|
],
|
328
|
-
MaxResults=1
|
322
|
+
MaxResults=1,
|
329
323
|
)
|
330
|
-
|
331
|
-
if response[
|
332
|
-
price_data = json.loads(response[
|
333
|
-
on_demand = price_data[
|
324
|
+
|
325
|
+
if response["PriceList"]:
|
326
|
+
price_data = json.loads(response["PriceList"][0])
|
327
|
+
on_demand = price_data["terms"]["OnDemand"]
|
334
328
|
for term in on_demand.values():
|
335
|
-
for price_dimension in term[
|
336
|
-
if
|
337
|
-
hourly_rate = float(price_dimension[
|
329
|
+
for price_dimension in term["priceDimensions"].values():
|
330
|
+
if "Hrs" in price_dimension.get("unit", ""):
|
331
|
+
hourly_rate = float(price_dimension["pricePerUnit"]["USD"])
|
338
332
|
monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
|
339
333
|
return monthly_cost
|
340
|
-
|
334
|
+
|
341
335
|
# Fallback to Cost Explorer
|
342
|
-
return self._get_from_cost_explorer(
|
343
|
-
|
336
|
+
return self._get_from_cost_explorer("EC2", "Elastic IP", region)
|
337
|
+
|
344
338
|
except Exception as e:
|
345
|
-
return self._get_from_cost_explorer(
|
339
|
+
return self._get_from_cost_explorer("EC2", "Elastic IP", region)
|
346
340
|
|
347
341
|
@lru_cache(maxsize=128)
|
348
|
-
def get_data_transfer_monthly_cost(self, region: str =
|
342
|
+
def get_data_transfer_monthly_cost(self, region: str = "us-east-1") -> float:
|
349
343
|
"""Get real-time Data Transfer cost per GB from AWS Pricing API."""
|
350
344
|
try:
|
351
345
|
response = self.pricing_client.get_products(
|
352
|
-
ServiceCode=
|
346
|
+
ServiceCode="AmazonEC2",
|
353
347
|
Filters=[
|
354
|
-
{
|
355
|
-
{
|
356
|
-
{
|
348
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Data Transfer"},
|
349
|
+
{"Type": "TERM_MATCH", "Field": "fromLocation", "Value": self._get_region_name(region)},
|
350
|
+
{"Type": "TERM_MATCH", "Field": "toLocation", "Value": "External"},
|
357
351
|
],
|
358
|
-
MaxResults=1
|
352
|
+
MaxResults=1,
|
359
353
|
)
|
360
|
-
|
361
|
-
if response[
|
362
|
-
price_data = json.loads(response[
|
363
|
-
on_demand = price_data[
|
354
|
+
|
355
|
+
if response["PriceList"]:
|
356
|
+
price_data = json.loads(response["PriceList"][0])
|
357
|
+
on_demand = price_data["terms"]["OnDemand"]
|
364
358
|
for term in on_demand.values():
|
365
|
-
for price_dimension in term[
|
366
|
-
if
|
367
|
-
return float(price_dimension[
|
368
|
-
|
359
|
+
for price_dimension in term["priceDimensions"].values():
|
360
|
+
if "GB" in price_dimension.get("unit", ""):
|
361
|
+
return float(price_dimension["pricePerUnit"]["USD"])
|
362
|
+
|
369
363
|
# Fallback to Cost Explorer
|
370
|
-
return self._get_from_cost_explorer(
|
371
|
-
|
364
|
+
return self._get_from_cost_explorer("EC2", "Data Transfer", region)
|
365
|
+
|
372
366
|
except Exception as e:
|
373
|
-
return self._get_from_cost_explorer(
|
367
|
+
return self._get_from_cost_explorer("EC2", "Data Transfer", region)
|
374
368
|
|
375
369
|
def _query_mcp_servers(self, resource_type: str, region: str, last_error: Exception = None) -> float:
|
376
370
|
"""Query MCP servers for cost validation as final fallback."""
|
@@ -407,31 +401,36 @@ class AWSPricingAPI:
|
|
407
401
|
💡 TIP: Run 'aws pricing get-products --service-code AmazonVPC' to test permissions
|
408
402
|
"""
|
409
403
|
print(guidance_msg)
|
410
|
-
|
404
|
+
|
411
405
|
# ENTERPRISE COMPLIANCE: Do not return hardcoded fallback
|
412
|
-
raise ValueError(
|
413
|
-
|
406
|
+
raise ValueError(
|
407
|
+
f"Unable to get pricing for {resource_type} in {region}. Check IAM permissions and MCP server connectivity."
|
408
|
+
)
|
409
|
+
|
414
410
|
except Exception as mcp_error:
|
415
411
|
print(f"🚫 Final fallback failed: {mcp_error}")
|
416
|
-
raise ValueError(
|
417
|
-
|
412
|
+
raise ValueError(
|
413
|
+
f"Unable to get pricing for {resource_type} in {region}. Check IAM permissions and MCP server connectivity."
|
414
|
+
)
|
415
|
+
|
418
416
|
def _get_region_name(self, region_code: str) -> str:
|
419
417
|
"""Convert region code to full region name for Pricing API."""
|
420
418
|
region_map = {
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
419
|
+
"us-east-1": "US East (N. Virginia)",
|
420
|
+
"us-west-1": "US West (N. California)",
|
421
|
+
"us-west-2": "US West (Oregon)",
|
422
|
+
"eu-west-1": "EU (Ireland)",
|
423
|
+
"eu-west-2": "EU (London)",
|
424
|
+
"eu-central-1": "EU (Frankfurt)",
|
425
|
+
"ap-southeast-1": "Asia Pacific (Singapore)",
|
426
|
+
"ap-southeast-2": "Asia Pacific (Sydney)",
|
427
|
+
"ap-northeast-1": "Asia Pacific (Tokyo)",
|
428
|
+
"ap-south-1": "Asia Pacific (Mumbai)",
|
429
|
+
"ca-central-1": "Canada (Central)",
|
430
|
+
"sa-east-1": "South America (São Paulo)",
|
433
431
|
}
|
434
|
-
return region_map.get(region_code,
|
432
|
+
return region_map.get(region_code, "US East (N. Virginia)")
|
433
|
+
|
435
434
|
|
436
435
|
# Global instance for easy import
|
437
|
-
pricing_api = AWSPricingAPI()
|
436
|
+
pricing_api = AWSPricingAPI()
|