runbooks 1.0.0__py3-none-any.whl → 1.0.2__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/cfat/WEIGHT_CONFIG_README.md +368 -0
- runbooks/cfat/app.ts +27 -19
- runbooks/cfat/assessment/runner.py +6 -5
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/cloudops/models.py +20 -14
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1070 -105
- runbooks/common/aws_pricing_api.py +276 -44
- runbooks/common/date_utils.py +115 -0
- runbooks/common/dry_run_examples.py +587 -0
- runbooks/common/dry_run_framework.py +520 -0
- runbooks/common/enhanced_exception_handler.py +10 -7
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/memory_optimization.py +533 -0
- runbooks/common/performance_optimization_engine.py +1153 -0
- runbooks/common/profile_utils.py +86 -118
- runbooks/common/rich_utils.py +3 -3
- runbooks/common/sre_performance_suite.py +574 -0
- runbooks/finops/business_case_config.py +314 -0
- runbooks/finops/cost_processor.py +19 -4
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_cost_optimizer.py +1 -1
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/embedded_mcp_validator.py +642 -36
- runbooks/finops/enhanced_trend_visualization.py +7 -2
- runbooks/finops/executive_export.py +789 -0
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/finops_scenarios.py +34 -27
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/nat_gateway_optimizer.py +46 -27
- runbooks/finops/notebook_utils.py +1 -1
- runbooks/finops/schemas.py +73 -58
- runbooks/finops/single_dashboard.py +20 -4
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_exporter.py +2 -1
- runbooks/finops/vpc_cleanup_optimizer.py +22 -29
- runbooks/inventory/core/collector.py +51 -28
- runbooks/inventory/discovery.md +197 -247
- runbooks/inventory/inventory_modules.py +2 -2
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/models/account.py +5 -3
- runbooks/inventory/models/inventory.py +1 -1
- runbooks/inventory/models/resource.py +5 -3
- runbooks/inventory/organizations_discovery.py +102 -13
- runbooks/inventory/unified_validation_engine.py +2 -15
- runbooks/main.py +255 -92
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +17 -13
- runbooks/operate/vpc_operations.py +82 -13
- runbooks/remediation/base.py +3 -1
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +66 -18
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/security/compliance_automation_engine.py +99 -20
- runbooks/security/config/__init__.py +24 -0
- runbooks/security/config/compliance_config.py +255 -0
- runbooks/security/config/compliance_weights_example.json +22 -0
- runbooks/security/config_template_generator.py +500 -0
- runbooks/security/security_cli.py +377 -0
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +26 -15
- runbooks/validation/mcp_validator.py +62 -8
- runbooks/vpc/config.py +49 -15
- runbooks/vpc/cross_account_session.py +5 -1
- runbooks/vpc/heatmap_engine.py +438 -59
- runbooks/vpc/mcp_no_eni_validator.py +115 -36
- runbooks/vpc/performance_optimized_analyzer.py +546 -0
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +3 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/METADATA +1 -1
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/RECORD +85 -79
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
- runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/vpc/runbooks.security.report_generator.log +0 -0
- runbooks/vpc/runbooks.security.run_script.log +0 -0
- runbooks/vpc/runbooks.security.security_export.log +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/WHEEL +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/entry_points.txt +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/top_level.txt +0 -0
@@ -114,37 +114,76 @@ class AWSPricingAPI:
|
|
114
114
|
|
115
115
|
@lru_cache(maxsize=128)
|
116
116
|
def get_nat_gateway_monthly_cost(self, region: str = 'us-east-1') -> float:
|
117
|
-
"""Get real-time NAT Gateway monthly cost from AWS Pricing API."""
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
117
|
+
"""Get real-time NAT Gateway monthly cost from AWS Pricing API with enterprise regional fallback."""
|
118
|
+
|
119
|
+
# Enterprise Regional Fallback Strategy
|
120
|
+
fallback_regions = ['us-east-1', 'us-west-2', 'eu-west-1']
|
121
|
+
if region not in fallback_regions:
|
122
|
+
fallback_regions.insert(0, region)
|
123
|
+
|
124
|
+
last_error = None
|
125
|
+
|
126
|
+
for attempt_region in fallback_regions:
|
127
|
+
try:
|
128
|
+
# Try AWS Pricing API for this region
|
129
|
+
response = self.pricing_client.get_products(
|
130
|
+
ServiceCode='AmazonVPC',
|
131
|
+
Filters=[
|
132
|
+
{'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'NAT Gateway'},
|
133
|
+
{'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(attempt_region)}
|
134
|
+
],
|
135
|
+
MaxResults=1
|
136
|
+
)
|
137
|
+
|
138
|
+
if response['PriceList']:
|
139
|
+
price_data = json.loads(response['PriceList'][0])
|
140
|
+
on_demand = price_data['terms']['OnDemand']
|
141
|
+
for term in on_demand.values():
|
142
|
+
for price_dimension in term['priceDimensions'].values():
|
143
|
+
if 'Hrs' in price_dimension.get('unit', ''):
|
144
|
+
hourly_rate = float(price_dimension['pricePerUnit']['USD'])
|
145
|
+
monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
|
146
|
+
print(f"✅ NAT Gateway pricing: ${monthly_cost:.2f}/month from {attempt_region}")
|
147
|
+
return monthly_cost
|
148
|
+
|
149
|
+
# Try Cost Explorer for this region
|
150
|
+
ce_cost = self._get_from_cost_explorer('VPC', 'NAT Gateway', attempt_region)
|
151
|
+
if ce_cost > 0:
|
152
|
+
print(f"✅ NAT Gateway pricing: ${ce_cost:.2f}/month from Cost Explorer")
|
153
|
+
return ce_cost
|
154
|
+
|
155
|
+
except Exception as e:
|
156
|
+
last_error = e
|
157
|
+
print(f"⚠️ Pricing API failed for region {attempt_region}: {e}")
|
158
|
+
continue
|
159
|
+
|
160
|
+
# Enterprise fallback with graceful degradation
|
161
|
+
return self._get_enterprise_fallback_pricing('nat_gateway', region, last_error)
|
141
162
|
|
142
|
-
def _get_from_cost_explorer(self, service: str, resource_type: str) -> float:
|
163
|
+
def _get_from_cost_explorer(self, service: str, resource_type: str, region: str = None) -> float:
|
143
164
|
"""Get actual costs from Cost Explorer as ultimate source of truth."""
|
144
165
|
try:
|
145
166
|
end_date = datetime.now()
|
146
167
|
start_date = end_date - timedelta(days=30)
|
147
168
|
|
169
|
+
# Build filter with optional region
|
170
|
+
filter_conditions = [
|
171
|
+
{'Dimensions': {'Key': 'SERVICE', 'Values': [f'Amazon {service}']}}
|
172
|
+
]
|
173
|
+
|
174
|
+
if region:
|
175
|
+
filter_conditions.append({
|
176
|
+
'Dimensions': {'Key': 'REGION', 'Values': [region]}
|
177
|
+
})
|
178
|
+
|
179
|
+
# Add resource type filter if it helps
|
180
|
+
if resource_type != service:
|
181
|
+
filter_conditions.append({
|
182
|
+
'Dimensions': {'Key': 'USAGE_TYPE_GROUP', 'Values': [resource_type]}
|
183
|
+
})
|
184
|
+
|
185
|
+
cost_filter = {'And': filter_conditions} if len(filter_conditions) > 1 else filter_conditions[0]
|
186
|
+
|
148
187
|
response = self.ce_client.get_cost_and_usage(
|
149
188
|
TimePeriod={
|
150
189
|
'Start': start_date.strftime('%Y-%m-%d'),
|
@@ -152,24 +191,20 @@ class AWSPricingAPI:
|
|
152
191
|
},
|
153
192
|
Granularity='MONTHLY',
|
154
193
|
Metrics=['UnblendedCost'],
|
155
|
-
Filter=
|
156
|
-
'And': [
|
157
|
-
{'Dimensions': {'Key': 'SERVICE', 'Values': [f'Amazon {service}']}},
|
158
|
-
{'Tags': {'Key': 'ResourceType', 'Values': [resource_type]}}
|
159
|
-
]
|
160
|
-
}
|
194
|
+
Filter=cost_filter
|
161
195
|
)
|
162
196
|
|
163
|
-
if response['ResultsByTime']:
|
197
|
+
if response['ResultsByTime'] and response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount']:
|
164
198
|
total_cost = float(response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])
|
165
|
-
|
166
|
-
|
199
|
+
if total_cost > 0:
|
200
|
+
# Calculate per-unit cost based on usage
|
201
|
+
return self._calculate_unit_cost(total_cost, service, resource_type)
|
167
202
|
|
168
|
-
#
|
169
|
-
return self._query_mcp_servers(service, resource_type)
|
203
|
+
return 0.0 # No cost data found
|
170
204
|
|
171
|
-
except Exception:
|
172
|
-
|
205
|
+
except Exception as e:
|
206
|
+
print(f"⚠️ Cost Explorer query failed: {e}")
|
207
|
+
return 0.0
|
173
208
|
|
174
209
|
def _calculate_unit_cost(self, total_cost: float, service: str, resource_type: str) -> float:
|
175
210
|
"""Calculate per-unit cost from total cost and usage metrics."""
|
@@ -184,20 +219,217 @@ class AWSPricingAPI:
|
|
184
219
|
divisor = usage_multipliers.get(service, {}).get(resource_type, 1000)
|
185
220
|
return total_cost / divisor
|
186
221
|
|
187
|
-
def
|
188
|
-
"""
|
189
|
-
|
190
|
-
#
|
191
|
-
|
222
|
+
def _get_enterprise_fallback_pricing(self, resource_type: str, region: str, last_error: Exception = None) -> float:
|
223
|
+
"""Enterprise-compliant fallback pricing with graceful degradation."""
|
224
|
+
|
225
|
+
# Check for enterprise configuration override
|
226
|
+
override_env = f"AWS_PRICING_OVERRIDE_{resource_type.upper()}_MONTHLY"
|
227
|
+
override_value = os.getenv(override_env)
|
228
|
+
if override_value:
|
229
|
+
print(f"💼 Using enterprise pricing override: ${override_value}/month")
|
230
|
+
return float(override_value)
|
231
|
+
|
232
|
+
# Check if running in compliance-mode or analysis can proceed with warnings
|
233
|
+
compliance_mode = os.getenv("AWS_PRICING_STRICT_COMPLIANCE", "false").lower() == "true"
|
234
|
+
|
235
|
+
if compliance_mode:
|
236
|
+
# Strict compliance: block operation
|
237
|
+
error_msg = f"ENTERPRISE VIOLATION: Cannot proceed without dynamic {resource_type} pricing. " \
|
238
|
+
f"Last error: {last_error}. Set {override_env} or enable fallback pricing."
|
239
|
+
print(f"🚫 {error_msg}")
|
240
|
+
raise ValueError(error_msg)
|
241
|
+
else:
|
242
|
+
# Graceful degradation: allow analysis with standard AWS rates (documented approach)
|
243
|
+
standard_rates = {
|
244
|
+
'nat_gateway': 32.40, # AWS standard us-east-1 rate: $0.045/hour * 24 * 30
|
245
|
+
'transit_gateway': 36.00, # AWS standard us-east-1 rate: $0.05/hour * 24 * 30
|
246
|
+
'vpc_endpoint_interface': 7.20, # AWS standard us-east-1 rate: $0.01/hour * 24 * 30
|
247
|
+
'elastic_ip_idle': 3.60, # AWS standard us-east-1 rate: $0.005/hour * 24 * 30
|
248
|
+
}
|
249
|
+
|
250
|
+
if resource_type in standard_rates:
|
251
|
+
fallback_cost = standard_rates[resource_type]
|
252
|
+
print(f"⚠️ FALLBACK PRICING: Using standard AWS rate ${fallback_cost}/month for {resource_type}")
|
253
|
+
print(f" ℹ️ To fix: Check IAM permissions for pricing:GetProducts and ce:GetCostAndUsage")
|
254
|
+
print(f" ℹ️ Or set {override_env} for enterprise override")
|
255
|
+
return fallback_cost
|
256
|
+
|
257
|
+
# Last resort: query MCP servers for validation
|
258
|
+
return self._query_mcp_servers(resource_type, region, last_error)
|
259
|
+
|
260
|
+
@lru_cache(maxsize=128)
|
261
|
+
def get_vpc_endpoint_monthly_cost(self, region: str = 'us-east-1') -> float:
|
262
|
+
"""Get real-time VPC Endpoint monthly cost from AWS Pricing API."""
|
263
|
+
try:
|
264
|
+
response = self.pricing_client.get_products(
|
265
|
+
ServiceCode='AmazonVPC',
|
266
|
+
Filters=[
|
267
|
+
{'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'VpcEndpoint'},
|
268
|
+
{'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(region)}
|
269
|
+
],
|
270
|
+
MaxResults=1
|
271
|
+
)
|
272
|
+
|
273
|
+
if response['PriceList']:
|
274
|
+
price_data = json.loads(response['PriceList'][0])
|
275
|
+
on_demand = price_data['terms']['OnDemand']
|
276
|
+
for term in on_demand.values():
|
277
|
+
for price_dimension in term['priceDimensions'].values():
|
278
|
+
if 'Hrs' in price_dimension.get('unit', ''):
|
279
|
+
hourly_rate = float(price_dimension['pricePerUnit']['USD'])
|
280
|
+
monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
|
281
|
+
return monthly_cost
|
282
|
+
|
283
|
+
# Fallback to Cost Explorer
|
284
|
+
return self._get_from_cost_explorer('VPC', 'VpcEndpoint', region)
|
285
|
+
|
286
|
+
except Exception as e:
|
287
|
+
return self._get_from_cost_explorer('VPC', 'VpcEndpoint', region)
|
288
|
+
|
289
|
+
@lru_cache(maxsize=128)
|
290
|
+
def get_transit_gateway_monthly_cost(self, region: str = 'us-east-1') -> float:
|
291
|
+
"""Get real-time Transit Gateway monthly cost from AWS Pricing API."""
|
292
|
+
try:
|
293
|
+
response = self.pricing_client.get_products(
|
294
|
+
ServiceCode='AmazonVPC',
|
295
|
+
Filters=[
|
296
|
+
{'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'Transit Gateway'},
|
297
|
+
{'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(region)}
|
298
|
+
],
|
299
|
+
MaxResults=1
|
300
|
+
)
|
301
|
+
|
302
|
+
if response['PriceList']:
|
303
|
+
price_data = json.loads(response['PriceList'][0])
|
304
|
+
on_demand = price_data['terms']['OnDemand']
|
305
|
+
for term in on_demand.values():
|
306
|
+
for price_dimension in term['priceDimensions'].values():
|
307
|
+
if 'Hrs' in price_dimension.get('unit', ''):
|
308
|
+
hourly_rate = float(price_dimension['pricePerUnit']['USD'])
|
309
|
+
monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
|
310
|
+
return monthly_cost
|
311
|
+
|
312
|
+
# Fallback to Cost Explorer
|
313
|
+
return self._get_from_cost_explorer('VPC', 'Transit Gateway', region)
|
314
|
+
|
315
|
+
except Exception as e:
|
316
|
+
return self._get_from_cost_explorer('VPC', 'Transit Gateway', region)
|
317
|
+
|
318
|
+
@lru_cache(maxsize=128)
|
319
|
+
def get_elastic_ip_monthly_cost(self, region: str = 'us-east-1') -> float:
|
320
|
+
"""Get real-time Elastic IP monthly cost from AWS Pricing API."""
|
321
|
+
try:
|
322
|
+
response = self.pricing_client.get_products(
|
323
|
+
ServiceCode='AmazonEC2',
|
324
|
+
Filters=[
|
325
|
+
{'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'IP Address'},
|
326
|
+
{'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(region)}
|
327
|
+
],
|
328
|
+
MaxResults=1
|
329
|
+
)
|
330
|
+
|
331
|
+
if response['PriceList']:
|
332
|
+
price_data = json.loads(response['PriceList'][0])
|
333
|
+
on_demand = price_data['terms']['OnDemand']
|
334
|
+
for term in on_demand.values():
|
335
|
+
for price_dimension in term['priceDimensions'].values():
|
336
|
+
if 'Hrs' in price_dimension.get('unit', ''):
|
337
|
+
hourly_rate = float(price_dimension['pricePerUnit']['USD'])
|
338
|
+
monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
|
339
|
+
return monthly_cost
|
340
|
+
|
341
|
+
# Fallback to Cost Explorer
|
342
|
+
return self._get_from_cost_explorer('EC2', 'Elastic IP', region)
|
343
|
+
|
344
|
+
except Exception as e:
|
345
|
+
return self._get_from_cost_explorer('EC2', 'Elastic IP', region)
|
346
|
+
|
347
|
+
@lru_cache(maxsize=128)
|
348
|
+
def get_data_transfer_monthly_cost(self, region: str = 'us-east-1') -> float:
|
349
|
+
"""Get real-time Data Transfer cost per GB from AWS Pricing API."""
|
350
|
+
try:
|
351
|
+
response = self.pricing_client.get_products(
|
352
|
+
ServiceCode='AmazonEC2',
|
353
|
+
Filters=[
|
354
|
+
{'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'Data Transfer'},
|
355
|
+
{'Type': 'TERM_MATCH', 'Field': 'fromLocation', 'Value': self._get_region_name(region)},
|
356
|
+
{'Type': 'TERM_MATCH', 'Field': 'toLocation', 'Value': 'External'}
|
357
|
+
],
|
358
|
+
MaxResults=1
|
359
|
+
)
|
360
|
+
|
361
|
+
if response['PriceList']:
|
362
|
+
price_data = json.loads(response['PriceList'][0])
|
363
|
+
on_demand = price_data['terms']['OnDemand']
|
364
|
+
for term in on_demand.values():
|
365
|
+
for price_dimension in term['priceDimensions'].values():
|
366
|
+
if 'GB' in price_dimension.get('unit', ''):
|
367
|
+
return float(price_dimension['pricePerUnit']['USD'])
|
368
|
+
|
369
|
+
# Fallback to Cost Explorer
|
370
|
+
return self._get_from_cost_explorer('EC2', 'Data Transfer', region)
|
371
|
+
|
372
|
+
except Exception as e:
|
373
|
+
return self._get_from_cost_explorer('EC2', 'Data Transfer', region)
|
374
|
+
|
375
|
+
def _query_mcp_servers(self, resource_type: str, region: str, last_error: Exception = None) -> float:
|
376
|
+
"""Query MCP servers for cost validation as final fallback."""
|
377
|
+
try:
|
378
|
+
# This would integrate with MCP servers for real-time validation
|
379
|
+
# For now, provide guidance for resolution
|
380
|
+
guidance_msg = f"""
|
381
|
+
🔧 VPC PRICING RESOLUTION REQUIRED:
|
382
|
+
|
383
|
+
Resource: {resource_type}
|
384
|
+
Region: {region}
|
385
|
+
Last Error: {last_error}
|
386
|
+
|
387
|
+
📋 RESOLUTION OPTIONS:
|
388
|
+
|
389
|
+
1. IAM Permissions (Most Common Fix):
|
390
|
+
Add these policies to your AWS profile:
|
391
|
+
- pricing:GetProducts
|
392
|
+
- ce:GetCostAndUsage
|
393
|
+
- ce:GetDimensionValues
|
394
|
+
|
395
|
+
2. Enterprise Override:
|
396
|
+
export AWS_PRICING_OVERRIDE_{resource_type.upper()}_MONTHLY=32.40
|
397
|
+
|
398
|
+
3. Enable Fallback Mode:
|
399
|
+
export AWS_PRICING_STRICT_COMPLIANCE=false
|
400
|
+
|
401
|
+
4. Alternative Region:
|
402
|
+
Try with --region us-east-1 (best Pricing API support)
|
403
|
+
|
404
|
+
5. MCP Server Integration:
|
405
|
+
Ensure MCP servers are accessible and operational
|
406
|
+
|
407
|
+
💡 TIP: Run 'aws pricing get-products --service-code AmazonVPC' to test permissions
|
408
|
+
"""
|
409
|
+
print(guidance_msg)
|
410
|
+
|
411
|
+
# ENTERPRISE COMPLIANCE: Do not return hardcoded fallback
|
412
|
+
raise ValueError(f"Unable to get pricing for {resource_type} in {region}. Check IAM permissions and MCP server connectivity.")
|
413
|
+
|
414
|
+
except Exception as mcp_error:
|
415
|
+
print(f"🚫 Final fallback failed: {mcp_error}")
|
416
|
+
raise ValueError(f"Unable to get pricing for {resource_type} in {region}. Check IAM permissions and MCP server connectivity.")
|
192
417
|
|
193
418
|
def _get_region_name(self, region_code: str) -> str:
|
194
419
|
"""Convert region code to full region name for Pricing API."""
|
195
420
|
region_map = {
|
196
421
|
'us-east-1': 'US East (N. Virginia)',
|
422
|
+
'us-west-1': 'US West (N. California)',
|
197
423
|
'us-west-2': 'US West (Oregon)',
|
198
424
|
'eu-west-1': 'EU (Ireland)',
|
425
|
+
'eu-west-2': 'EU (London)',
|
426
|
+
'eu-central-1': 'EU (Frankfurt)',
|
199
427
|
'ap-southeast-1': 'Asia Pacific (Singapore)',
|
200
|
-
|
428
|
+
'ap-southeast-2': 'Asia Pacific (Sydney)',
|
429
|
+
'ap-northeast-1': 'Asia Pacific (Tokyo)',
|
430
|
+
'ap-south-1': 'Asia Pacific (Mumbai)',
|
431
|
+
'ca-central-1': 'Canada (Central)',
|
432
|
+
'sa-east-1': 'South America (São Paulo)',
|
201
433
|
}
|
202
434
|
return region_map.get(region_code, 'US East (N. Virginia)')
|
203
435
|
|
@@ -0,0 +1,115 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Dynamic Date Utilities for CloudOps Runbooks Platform
|
4
|
+
|
5
|
+
Replaces hardcoded 2024 dates with dynamic date generation following manager's
|
6
|
+
"No hardcoded values" requirement. Supports current month/year calculations
|
7
|
+
for all test data and AWS API time period generation.
|
8
|
+
|
9
|
+
Strategic Alignment: "Do one thing and do it well" - Focused date utility
|
10
|
+
KISS Principle: Simple, reusable date functions for all modules
|
11
|
+
"""
|
12
|
+
|
13
|
+
from datetime import datetime, timedelta
|
14
|
+
from typing import Dict, Tuple
|
15
|
+
|
16
|
+
|
17
|
+
def get_current_year() -> int:
|
18
|
+
"""Get current year dynamically."""
|
19
|
+
return datetime.now().year
|
20
|
+
|
21
|
+
|
22
|
+
def get_current_month_period() -> Dict[str, str]:
|
23
|
+
"""
|
24
|
+
Generate current month's start and end dates for AWS API calls.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Dict with 'Start' and 'End' keys in YYYY-MM-DD format
|
28
|
+
"""
|
29
|
+
now = datetime.now()
|
30
|
+
start_date = now.replace(day=1).strftime('%Y-%m-%d')
|
31
|
+
|
32
|
+
# Get last day of current month
|
33
|
+
if now.month == 12:
|
34
|
+
next_month = now.replace(year=now.year + 1, month=1, day=1)
|
35
|
+
else:
|
36
|
+
next_month = now.replace(month=now.month + 1, day=1)
|
37
|
+
|
38
|
+
end_date = (next_month - timedelta(days=1)).strftime('%Y-%m-%d')
|
39
|
+
|
40
|
+
return {
|
41
|
+
'Start': start_date,
|
42
|
+
'End': end_date
|
43
|
+
}
|
44
|
+
|
45
|
+
|
46
|
+
def get_previous_month_period() -> Dict[str, str]:
|
47
|
+
"""
|
48
|
+
Generate previous month's start and end dates for AWS API calls.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
Dict with 'Start' and 'End' keys in YYYY-MM-DD format
|
52
|
+
"""
|
53
|
+
now = datetime.now()
|
54
|
+
|
55
|
+
# Get first day of previous month
|
56
|
+
if now.month == 1:
|
57
|
+
prev_month = now.replace(year=now.year - 1, month=12, day=1)
|
58
|
+
else:
|
59
|
+
prev_month = now.replace(month=now.month - 1, day=1)
|
60
|
+
|
61
|
+
start_date = prev_month.strftime('%Y-%m-%d')
|
62
|
+
|
63
|
+
# Get last day of previous month
|
64
|
+
end_date = (now.replace(day=1) - timedelta(days=1)).strftime('%Y-%m-%d')
|
65
|
+
|
66
|
+
return {
|
67
|
+
'Start': start_date,
|
68
|
+
'End': end_date
|
69
|
+
}
|
70
|
+
|
71
|
+
|
72
|
+
def get_test_date_period(days_back: int = 30) -> Dict[str, str]:
|
73
|
+
"""
|
74
|
+
Generate test date periods for consistent test data.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
days_back: Number of days back from today for start date
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Dict with 'Start' and 'End' keys in YYYY-MM-DD format
|
81
|
+
"""
|
82
|
+
end_date = datetime.now().strftime('%Y-%m-%d')
|
83
|
+
start_date = (datetime.now() - timedelta(days=days_back)).strftime('%Y-%m-%d')
|
84
|
+
|
85
|
+
return {
|
86
|
+
'Start': start_date,
|
87
|
+
'End': end_date
|
88
|
+
}
|
89
|
+
|
90
|
+
|
91
|
+
def get_aws_cli_example_period() -> Tuple[str, str]:
|
92
|
+
"""
|
93
|
+
Generate example date period for AWS CLI documentation.
|
94
|
+
Uses yesterday and today to ensure valid time range.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Tuple of (start_date, end_date) in YYYY-MM-DD format
|
98
|
+
"""
|
99
|
+
today = datetime.now()
|
100
|
+
yesterday = today - timedelta(days=1)
|
101
|
+
|
102
|
+
return (
|
103
|
+
yesterday.strftime('%Y-%m-%d'),
|
104
|
+
today.strftime('%Y-%m-%d')
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
def get_collection_timestamp() -> str:
|
109
|
+
"""
|
110
|
+
Generate collection timestamp for test data.
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
ISO format timestamp string
|
114
|
+
"""
|
115
|
+
return datetime.now().isoformat()
|