runbooks 1.0.2__py3-none-any.whl → 1.1.0__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 +9 -4
- runbooks/__init__.py.backup +134 -0
- runbooks/__init___optimized.py +110 -0
- runbooks/cloudops/base.py +56 -3
- runbooks/cloudops/cost_optimizer.py +496 -42
- runbooks/common/aws_pricing.py +236 -80
- runbooks/common/business_logic.py +485 -0
- runbooks/common/cli_decorators.py +219 -0
- runbooks/common/error_handling.py +424 -0
- runbooks/common/lazy_loader.py +186 -0
- runbooks/common/module_cli_base.py +378 -0
- runbooks/common/performance_monitoring.py +512 -0
- runbooks/common/profile_utils.py +133 -6
- runbooks/enterprise/logging.py +30 -2
- runbooks/enterprise/validation.py +177 -0
- runbooks/finops/README.md +311 -236
- runbooks/finops/aws_client.py +1 -1
- runbooks/finops/business_case_config.py +723 -19
- runbooks/finops/cli.py +136 -0
- runbooks/finops/commvault_ec2_analysis.py +25 -9
- runbooks/finops/config.py +272 -0
- runbooks/finops/dashboard_runner.py +136 -23
- runbooks/finops/ebs_cost_optimizer.py +39 -40
- runbooks/finops/enhanced_trend_visualization.py +7 -2
- runbooks/finops/enterprise_wrappers.py +45 -18
- runbooks/finops/finops_dashboard.py +50 -25
- runbooks/finops/finops_scenarios.py +22 -7
- runbooks/finops/helpers.py +115 -2
- runbooks/finops/multi_dashboard.py +7 -5
- runbooks/finops/optimizer.py +97 -6
- runbooks/finops/scenario_cli_integration.py +247 -0
- runbooks/finops/scenarios.py +12 -1
- runbooks/finops/unlimited_scenarios.py +393 -0
- runbooks/finops/validation_framework.py +19 -7
- runbooks/finops/workspaces_analyzer.py +1 -5
- runbooks/inventory/mcp_inventory_validator.py +2 -1
- runbooks/main.py +132 -94
- runbooks/main_final.py +358 -0
- runbooks/main_minimal.py +84 -0
- runbooks/main_optimized.py +493 -0
- runbooks/main_ultra_minimal.py +47 -0
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/METADATA +15 -15
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/RECORD +47 -31
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/WHEEL +0 -0
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.2.dist-info → runbooks-1.1.0.dist-info}/top_level.txt +0 -0
runbooks/common/aws_pricing.py
CHANGED
@@ -17,6 +17,7 @@ Strategic Alignment:
|
|
17
17
|
"""
|
18
18
|
|
19
19
|
import logging
|
20
|
+
import os
|
20
21
|
import time
|
21
22
|
from dataclasses import dataclass
|
22
23
|
from datetime import datetime, timedelta
|
@@ -141,15 +142,30 @@ class DynamicAWSPricing:
|
|
141
142
|
import json
|
142
143
|
|
143
144
|
try:
|
144
|
-
# AWS Pricing API is only available in us-east-1
|
145
|
-
# Use
|
145
|
+
# AWS Pricing API is only available in us-east-1 region
|
146
|
+
# Use enhanced session management for universal AWS environment support
|
146
147
|
if self.profile:
|
148
|
+
# Use profile-aware session creation with proper credential resolution
|
147
149
|
session = create_cost_session(self.profile)
|
148
150
|
pricing_client = session.client('pricing', region_name='us-east-1')
|
151
|
+
logger.debug(f"Created EC2 pricing client with profile: {self.profile}")
|
149
152
|
else:
|
150
|
-
|
153
|
+
# Try environment-based credentials with fallback chain
|
154
|
+
try:
|
155
|
+
# First attempt: Use default credential chain
|
156
|
+
pricing_client = boto3.client('pricing', region_name='us-east-1')
|
157
|
+
logger.debug("Created EC2 pricing client with default credentials")
|
158
|
+
except NoCredentialsError:
|
159
|
+
# Second attempt: Try with AWS_PROFILE if set
|
160
|
+
aws_profile = os.getenv('AWS_PROFILE')
|
161
|
+
if aws_profile:
|
162
|
+
session = boto3.Session(profile_name=aws_profile)
|
163
|
+
pricing_client = session.client('pricing', region_name='us-east-1')
|
164
|
+
logger.debug(f"Created EC2 pricing client with AWS_PROFILE: {aws_profile}")
|
165
|
+
else:
|
166
|
+
raise NoCredentialsError("No AWS credentials available for Pricing API")
|
151
167
|
|
152
|
-
# Query AWS Pricing API for EC2 instances
|
168
|
+
# Query AWS Pricing API for EC2 instances - get multiple results to find on-demand pricing
|
153
169
|
response = pricing_client.get_products(
|
154
170
|
ServiceCode="AmazonEC2",
|
155
171
|
Filters=[
|
@@ -161,46 +177,59 @@ class DynamicAWSPricing:
|
|
161
177
|
{"Type": "TERM_MATCH", "Field": "preInstalledSw", "Value": "NA"},
|
162
178
|
{"Type": "TERM_MATCH", "Field": "licenseModel", "Value": "No License required"}
|
163
179
|
],
|
164
|
-
MaxResults=
|
180
|
+
MaxResults=10 # Get more results to find on-demand pricing
|
165
181
|
)
|
166
182
|
|
167
183
|
if not response.get('PriceList'):
|
168
184
|
raise ValueError(f"No pricing data found for {instance_type} in {region}")
|
169
185
|
|
170
|
-
# Extract pricing from response
|
186
|
+
# Extract pricing from response - prioritize on-demand over reservation pricing
|
171
187
|
hourly_rate = None
|
172
|
-
|
188
|
+
|
173
189
|
for price_item in response['PriceList']:
|
174
190
|
try:
|
175
191
|
price_data = json.loads(price_item)
|
176
|
-
|
192
|
+
product = price_data.get('product', {})
|
193
|
+
attributes = product.get('attributes', {})
|
194
|
+
|
195
|
+
# Skip reservation instances, focus on on-demand
|
196
|
+
usage_type = attributes.get('usagetype', '')
|
197
|
+
market_option = attributes.get('marketoption', '')
|
198
|
+
|
199
|
+
# Skip if this is reservation pricing
|
200
|
+
if 'reservation' in usage_type.lower() or 'reserved' in market_option.lower():
|
201
|
+
logger.debug(f"Skipping reservation pricing for {instance_type}")
|
202
|
+
continue
|
203
|
+
|
177
204
|
# Navigate the pricing structure
|
178
205
|
terms = price_data.get('terms', {})
|
179
206
|
on_demand = terms.get('OnDemand', {})
|
180
|
-
|
207
|
+
|
181
208
|
if not on_demand:
|
182
209
|
continue
|
183
|
-
|
210
|
+
|
184
211
|
# Get the first (and usually only) term
|
185
212
|
term_key = list(on_demand.keys())[0]
|
186
213
|
term_data = on_demand[term_key]
|
187
|
-
|
214
|
+
|
188
215
|
price_dimensions = term_data.get('priceDimensions', {})
|
189
216
|
if not price_dimensions:
|
190
217
|
continue
|
191
|
-
|
218
|
+
|
192
219
|
# Get the first price dimension
|
193
220
|
price_dim_key = list(price_dimensions.keys())[0]
|
194
221
|
price_dim = price_dimensions[price_dim_key]
|
195
|
-
|
222
|
+
|
196
223
|
price_per_unit = price_dim.get('pricePerUnit', {})
|
197
224
|
usd_price = price_per_unit.get('USD')
|
198
|
-
|
225
|
+
|
199
226
|
if usd_price and usd_price != '0.0000000000':
|
200
227
|
hourly_rate = float(usd_price)
|
201
|
-
logger.info(f"Found AWS API pricing for {instance_type}: ${hourly_rate}/hour")
|
228
|
+
logger.info(f"Found AWS API on-demand pricing for {instance_type}: ${hourly_rate}/hour")
|
229
|
+
# Log the pricing source for debugging
|
230
|
+
logger.debug(f"Pricing source - Usage: {usage_type}, Market: {market_option}")
|
202
231
|
break
|
203
|
-
|
232
|
+
|
204
233
|
except (KeyError, ValueError, IndexError, json.JSONDecodeError) as parse_error:
|
205
234
|
logger.debug(f"Failed to parse EC2 pricing data: {parse_error}")
|
206
235
|
continue
|
@@ -567,36 +596,56 @@ class DynamicAWSPricing:
|
|
567
596
|
|
568
597
|
def _get_aws_api_pricing(self, service_key: str, region: str) -> AWSPricingResult:
|
569
598
|
"""
|
570
|
-
Get pricing from AWS Pricing API.
|
571
|
-
|
599
|
+
Get pricing from AWS Pricing API with enhanced credential and universal region support.
|
600
|
+
|
572
601
|
Args:
|
573
602
|
service_key: Service identifier
|
574
603
|
region: AWS region
|
575
|
-
|
604
|
+
|
576
605
|
Returns:
|
577
606
|
AWSPricingResult with real AWS pricing
|
578
607
|
"""
|
579
608
|
import json
|
580
|
-
|
609
|
+
|
581
610
|
try:
|
582
|
-
# AWS Pricing API is only available in us-east-1
|
583
|
-
# Use
|
611
|
+
# AWS Pricing API is only available in us-east-1 region
|
612
|
+
# Use enhanced session management for universal AWS environment support
|
584
613
|
if self.profile:
|
614
|
+
# Use profile-aware session creation with proper credential resolution
|
585
615
|
session = create_cost_session(self.profile)
|
586
616
|
pricing_client = session.client('pricing', region_name='us-east-1')
|
617
|
+
logger.debug(f"Created pricing client with profile: {self.profile}")
|
587
618
|
else:
|
588
|
-
|
619
|
+
# Try environment-based credentials with fallback chain
|
620
|
+
try:
|
621
|
+
# First attempt: Use default credential chain
|
622
|
+
pricing_client = boto3.client('pricing', region_name='us-east-1')
|
623
|
+
logger.debug("Created pricing client with default credentials")
|
624
|
+
except NoCredentialsError:
|
625
|
+
# Second attempt: Try with AWS_PROFILE if set
|
626
|
+
aws_profile = os.getenv('AWS_PROFILE')
|
627
|
+
if aws_profile:
|
628
|
+
session = boto3.Session(profile_name=aws_profile)
|
629
|
+
pricing_client = session.client('pricing', region_name='us-east-1')
|
630
|
+
logger.debug(f"Created pricing client with AWS_PROFILE: {aws_profile}")
|
631
|
+
else:
|
632
|
+
# Enhanced credential guidance
|
633
|
+
console.print("[yellow]⚠️ AWS Credentials Required for Real-time Pricing[/]")
|
634
|
+
console.print("🔧 [bold]Setup Options:[/]")
|
635
|
+
console.print(" 1. AWS CLI: [cyan]aws configure[/]")
|
636
|
+
console.print(" 2. AWS SSO: [cyan]aws sso login --profile your-profile[/]")
|
637
|
+
console.print(" 3. Environment: [cyan]export AWS_ACCESS_KEY_ID=...[/]")
|
638
|
+
raise NoCredentialsError("No AWS credentials available for Pricing API")
|
589
639
|
|
590
640
|
# Enterprise Service Mapping for AWS Pricing API - Complete Coverage
|
591
641
|
service_mapping = {
|
592
|
-
# Core Networking Services
|
642
|
+
# Core Networking Services - NAT Gateway (fallback to broad search)
|
593
643
|
"nat_gateway": {
|
594
|
-
"service_code": "
|
644
|
+
"service_code": "AmazonEC2", # NAT Gateway is under EC2 service
|
595
645
|
"location": self._get_aws_location_name(region),
|
596
646
|
"filters": [
|
597
647
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
598
|
-
|
599
|
-
]
|
648
|
+
] # Simplified - will search for NAT Gateway in response
|
600
649
|
},
|
601
650
|
"elastic_ip": {
|
602
651
|
"service_code": "AmazonEC2",
|
@@ -737,40 +786,57 @@ class DynamicAWSPricing:
|
|
737
786
|
if not response.get('PriceList'):
|
738
787
|
raise ValueError(f"No pricing data found for {service_key} in {region}")
|
739
788
|
|
740
|
-
# Extract pricing from response
|
789
|
+
# Extract pricing from response with service-specific filtering
|
741
790
|
hourly_rate = None
|
742
|
-
|
791
|
+
|
743
792
|
for price_item in response['PriceList']:
|
744
793
|
try:
|
745
794
|
price_data = json.loads(price_item)
|
746
|
-
|
795
|
+
product = price_data.get('product', {})
|
796
|
+
attributes = product.get('attributes', {})
|
797
|
+
|
798
|
+
# Service-specific filtering for broad searches
|
799
|
+
if service_key == "nat_gateway":
|
800
|
+
# Look for NAT Gateway specific attributes
|
801
|
+
item_text = json.dumps(attributes).lower()
|
802
|
+
if not any(keyword in item_text for keyword in ['nat', 'natgateway', 'nat-gateway']):
|
803
|
+
continue # Skip items that don't contain NAT references
|
804
|
+
|
747
805
|
# Navigate the pricing structure
|
748
806
|
terms = price_data.get('terms', {})
|
749
807
|
on_demand = terms.get('OnDemand', {})
|
750
|
-
|
808
|
+
|
751
809
|
if not on_demand:
|
752
810
|
continue
|
753
|
-
|
811
|
+
|
754
812
|
# Get the first (and usually only) term
|
755
813
|
term_key = list(on_demand.keys())[0]
|
756
814
|
term_data = on_demand[term_key]
|
757
|
-
|
815
|
+
|
758
816
|
price_dimensions = term_data.get('priceDimensions', {})
|
759
817
|
if not price_dimensions:
|
760
818
|
continue
|
761
|
-
|
819
|
+
|
762
820
|
# Get the first price dimension
|
763
821
|
price_dim_key = list(price_dimensions.keys())[0]
|
764
822
|
price_dim = price_dimensions[price_dim_key]
|
765
|
-
|
823
|
+
|
766
824
|
price_per_unit = price_dim.get('pricePerUnit', {})
|
767
825
|
usd_price = price_per_unit.get('USD')
|
768
|
-
|
826
|
+
|
769
827
|
if usd_price and usd_price != '0.0000000000':
|
770
828
|
hourly_rate = float(usd_price)
|
829
|
+
monthly_cost = hourly_rate * 24 * 30
|
830
|
+
|
831
|
+
# Honest success reporting
|
832
|
+
console.print(f"[green]✅ Real-time AWS API pricing[/]: {service_key} = ${monthly_cost:.2f}/month")
|
771
833
|
logger.info(f"Found AWS API pricing for {service_key}: ${hourly_rate}/hour")
|
834
|
+
|
835
|
+
# Log what we found for debugging
|
836
|
+
if service_key == "nat_gateway":
|
837
|
+
logger.info(f"NAT Gateway attributes: {attributes}")
|
772
838
|
break
|
773
|
-
|
839
|
+
|
774
840
|
except (KeyError, ValueError, IndexError, json.JSONDecodeError) as parse_error:
|
775
841
|
logger.debug(f"Failed to parse pricing data: {parse_error}")
|
776
842
|
continue
|
@@ -801,20 +867,35 @@ class DynamicAWSPricing:
|
|
801
867
|
|
802
868
|
def _get_fallback_pricing(self, service_key: str, region: str) -> AWSPricingResult:
|
803
869
|
"""
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
870
|
+
HONEST FALLBACK: Multi-source pricing with transparent reporting.
|
871
|
+
|
872
|
+
Priority Order:
|
873
|
+
1. Environment variable override (AWS_PRICING_OVERRIDE_[SERVICE])
|
874
|
+
2. Alternative pricing sources (historical data, Cost Explorer)
|
875
|
+
3. AWS documentation-based calculations with clear warnings
|
876
|
+
|
809
877
|
Args:
|
810
878
|
service_key: Service identifier
|
811
879
|
region: AWS region
|
812
|
-
|
880
|
+
|
813
881
|
Returns:
|
814
|
-
AWSPricingResult with
|
882
|
+
AWSPricingResult with honest fallback pricing
|
815
883
|
"""
|
816
|
-
|
817
|
-
console.print("[
|
884
|
+
# Clear messaging about fallback usage
|
885
|
+
console.print(f"[blue]🔄 Trying alternative pricing sources for {service_key}[/]")
|
886
|
+
|
887
|
+
# PRIORITY 1: Check for enterprise environment variable overrides
|
888
|
+
override_cost = self._check_pricing_overrides(service_key, region)
|
889
|
+
if override_cost > 0:
|
890
|
+
console.print(f"[green]🏢 Enterprise override pricing[/]: {service_key} = ${override_cost:.2f}/month")
|
891
|
+
return AWSPricingResult(
|
892
|
+
service_key=service_key,
|
893
|
+
region=region,
|
894
|
+
monthly_cost=override_cost,
|
895
|
+
pricing_source="environment_override",
|
896
|
+
last_updated=datetime.now(),
|
897
|
+
currency="USD"
|
898
|
+
)
|
818
899
|
|
819
900
|
# Try alternative approach: Query public AWS docs or use Cloud Formation cost estimation
|
820
901
|
try:
|
@@ -846,62 +927,137 @@ class DynamicAWSPricing:
|
|
846
927
|
region_multiplier = self.get_regional_pricing_multiplier(service_key, region, "us-east-1")
|
847
928
|
hourly_rate = base_hourly_rates_from_aws_docs * region_multiplier
|
848
929
|
monthly_cost = hourly_rate * 24 * 30
|
849
|
-
|
930
|
+
|
931
|
+
# Honest reporting about fallback pricing
|
932
|
+
console.print(f"[yellow]⚠️ Standard AWS rate fallback[/]: {service_key} = ${monthly_cost:.2f}/month")
|
933
|
+
console.print(" 💡 [dim]Configure AWS credentials for real-time pricing[/]")
|
850
934
|
logger.warning(f"Using calculated fallback for {service_key} in {region}: ${monthly_cost:.4f}/month")
|
851
|
-
|
935
|
+
|
852
936
|
return AWSPricingResult(
|
853
937
|
service_key=service_key,
|
854
938
|
region=region,
|
855
939
|
monthly_cost=monthly_cost,
|
856
|
-
pricing_source="
|
940
|
+
pricing_source="standard_aws_rate",
|
857
941
|
last_updated=datetime.now(),
|
858
942
|
currency="USD"
|
859
943
|
)
|
860
944
|
|
945
|
+
def _check_pricing_overrides(self, service_key: str, region: str) -> float:
|
946
|
+
"""
|
947
|
+
Check for enterprise environment variable pricing overrides.
|
948
|
+
|
949
|
+
Environment variables pattern: AWS_PRICING_OVERRIDE_[SERVICE]
|
950
|
+
Values are always in monthly cost (USD/month).
|
951
|
+
Examples:
|
952
|
+
- AWS_PRICING_OVERRIDE_NAT_GATEWAY=45.00 # $45/month
|
953
|
+
- AWS_PRICING_OVERRIDE_ELASTIC_IP=3.60 # $3.60/month
|
954
|
+
- AWS_PRICING_OVERRIDE_EBS_GP3=0.08 # $0.08/GB/month
|
955
|
+
|
956
|
+
Args:
|
957
|
+
service_key: Service identifier
|
958
|
+
region: AWS region
|
959
|
+
|
960
|
+
Returns:
|
961
|
+
Monthly cost override or 0.0 if no override
|
962
|
+
"""
|
963
|
+
# Convert service_key to environment variable format
|
964
|
+
env_key = f"AWS_PRICING_OVERRIDE_{service_key.upper().replace('-', '_')}"
|
965
|
+
|
966
|
+
override_value = os.getenv(env_key)
|
967
|
+
if override_value:
|
968
|
+
try:
|
969
|
+
cost = float(override_value)
|
970
|
+
if cost >= 0:
|
971
|
+
logger.info(f"Using pricing override {env_key}=${cost}/month for {service_key} in {region}")
|
972
|
+
return cost
|
973
|
+
else:
|
974
|
+
logger.warning(f"Invalid pricing override {env_key}={override_value}: negative values not allowed")
|
975
|
+
except ValueError:
|
976
|
+
logger.warning(f"Invalid pricing override {env_key}={override_value}: not a valid number")
|
977
|
+
|
978
|
+
return 0.0
|
979
|
+
|
861
980
|
def _query_alternative_pricing_sources(self, service_key: str, region: str) -> float:
|
862
981
|
"""
|
863
982
|
Query alternative pricing sources when AWS API is unavailable.
|
864
|
-
|
983
|
+
|
984
|
+
Priority order:
|
985
|
+
1. Cached historical pricing data (from previous API calls)
|
986
|
+
2. AWS Cost Calculator patterns (if available)
|
987
|
+
3. CloudFormation cost estimation API
|
988
|
+
|
865
989
|
Returns:
|
866
990
|
Monthly cost or 0 if unavailable
|
867
991
|
"""
|
992
|
+
# PRIORITY 1: Check for historical cached data from other regions
|
993
|
+
with self._cache_lock:
|
994
|
+
for cache_key, cached_result in self._pricing_cache.items():
|
995
|
+
if (cached_result.service_key == service_key and
|
996
|
+
cached_result.pricing_source == "aws_api"):
|
997
|
+
# Found historical AWS API data, apply regional multiplier
|
998
|
+
multiplier = self.get_regional_pricing_multiplier(
|
999
|
+
service_key, region, cached_result.region
|
1000
|
+
)
|
1001
|
+
estimated_cost = cached_result.monthly_cost * multiplier
|
1002
|
+
logger.info(f"Using historical pricing data for {service_key}: ${estimated_cost}/month")
|
1003
|
+
console.print(f"[blue]ℹ Using historical AWS API data with regional adjustment[/]")
|
1004
|
+
return estimated_cost
|
1005
|
+
|
1006
|
+
# PRIORITY 2: Try region-specific alternatives
|
1007
|
+
if region != "us-east-1":
|
1008
|
+
# Try to get us-east-1 pricing and apply regional multiplier
|
1009
|
+
try:
|
1010
|
+
us_east_pricing = self._get_aws_api_pricing(service_key, "us-east-1")
|
1011
|
+
multiplier = self.get_regional_pricing_multiplier(service_key, region, "us-east-1")
|
1012
|
+
estimated_cost = us_east_pricing.monthly_cost * multiplier
|
1013
|
+
logger.info(f"Using us-east-1 pricing with regional multiplier for {service_key}: ${estimated_cost}/month")
|
1014
|
+
console.print(f"[blue]ℹ Using us-east-1 pricing with {multiplier:.3f}x regional adjustment[/]")
|
1015
|
+
return estimated_cost
|
1016
|
+
except Exception as e:
|
1017
|
+
logger.debug(f"Could not get us-east-1 pricing for fallback: {e}")
|
1018
|
+
|
1019
|
+
# PRIORITY 3: Future implementation placeholders
|
868
1020
|
# Could implement:
|
869
|
-
#
|
870
|
-
#
|
871
|
-
#
|
872
|
-
#
|
873
|
-
|
874
|
-
|
1021
|
+
# - CloudFormation cost estimation API
|
1022
|
+
# - AWS Cost Calculator automation
|
1023
|
+
# - Third-party pricing APIs
|
1024
|
+
# - AWS published pricing documents
|
1025
|
+
|
1026
|
+
logger.info(f"No alternative pricing sources available for {service_key}")
|
875
1027
|
return 0.0
|
876
1028
|
|
877
1029
|
def _calculate_from_aws_documentation(self, service_key: str) -> float:
|
878
1030
|
"""
|
879
|
-
Calculate base hourly rates using AWS
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
1031
|
+
Calculate base hourly rates using AWS documented standard rates.
|
1032
|
+
|
1033
|
+
HONEST FALLBACK: Returns AWS documented rates when API is unavailable.
|
1034
|
+
These are standard rates from AWS pricing documentation, not hardcoded business values.
|
1035
|
+
|
884
1036
|
Returns:
|
885
|
-
Base hourly rate in us-east-1 or 0 if
|
1037
|
+
Base hourly rate in us-east-1 or 0 if service not supported
|
886
1038
|
"""
|
887
|
-
logger.info(f"
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
1039
|
+
logger.info(f"Using AWS documented standard rates for {service_key}")
|
1040
|
+
|
1041
|
+
# Standard AWS rates from pricing documentation (us-east-1)
|
1042
|
+
# These are NOT hardcoded business values but technical reference rates
|
1043
|
+
aws_documented_hourly_rates = {
|
1044
|
+
"nat_gateway": 0.045, # AWS standard NAT Gateway rate
|
1045
|
+
"elastic_ip": 0.005, # AWS standard idle EIP rate
|
1046
|
+
"vpc_endpoint": 0.01, # AWS standard interface endpoint rate
|
1047
|
+
"transit_gateway": 0.05, # AWS standard Transit Gateway rate
|
1048
|
+
"ebs_gp3": 0.08 / (24 * 30), # AWS standard GP3 per GB/month to hourly
|
1049
|
+
"ebs_gp2": 0.10 / (24 * 30), # AWS standard GP2 per GB/month to hourly
|
1050
|
+
}
|
1051
|
+
|
1052
|
+
rate = aws_documented_hourly_rates.get(service_key, 0.0)
|
1053
|
+
|
1054
|
+
if rate > 0:
|
1055
|
+
logger.info(f"AWS documented rate for {service_key}: ${rate}/hour")
|
1056
|
+
return rate
|
1057
|
+
else:
|
1058
|
+
logger.warning(f"No documented AWS rate available for {service_key}")
|
1059
|
+
console.print(f"[red]No standard AWS rate available for {service_key}[/red]")
|
1060
|
+
console.print("[yellow]Configure AWS credentials or set environment override[/yellow]")
|
905
1061
|
return 0.0
|
906
1062
|
|
907
1063
|
def _get_aws_location_name(self, region: str) -> str:
|