runbooks 0.9.9__py3-none-any.whl → 1.0.1__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/cfat/WEIGHT_CONFIG_README.md +368 -0
  3. runbooks/cfat/app.ts +27 -19
  4. runbooks/cfat/assessment/runner.py +6 -5
  5. runbooks/cfat/cloud_foundations_assessment.py +626 -0
  6. runbooks/cfat/tests/test_weight_configuration.ts +449 -0
  7. runbooks/cfat/weight_config.ts +574 -0
  8. runbooks/cloudops/cost_optimizer.py +95 -33
  9. runbooks/common/__init__.py +26 -9
  10. runbooks/common/aws_pricing.py +1353 -0
  11. runbooks/common/aws_pricing_api.py +205 -0
  12. runbooks/common/aws_utils.py +2 -2
  13. runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
  14. runbooks/common/cross_account_manager.py +606 -0
  15. runbooks/common/date_utils.py +115 -0
  16. runbooks/common/enhanced_exception_handler.py +14 -7
  17. runbooks/common/env_utils.py +96 -0
  18. runbooks/common/mcp_cost_explorer_integration.py +5 -4
  19. runbooks/common/mcp_integration.py +49 -2
  20. runbooks/common/organizations_client.py +579 -0
  21. runbooks/common/profile_utils.py +127 -72
  22. runbooks/common/rich_utils.py +3 -3
  23. runbooks/finops/cost_optimizer.py +2 -1
  24. runbooks/finops/dashboard_runner.py +47 -28
  25. runbooks/finops/ebs_optimizer.py +56 -9
  26. runbooks/finops/elastic_ip_optimizer.py +13 -9
  27. runbooks/finops/embedded_mcp_validator.py +31 -0
  28. runbooks/finops/enhanced_trend_visualization.py +10 -4
  29. runbooks/finops/finops_dashboard.py +6 -5
  30. runbooks/finops/iam_guidance.py +6 -1
  31. runbooks/finops/markdown_exporter.py +217 -2
  32. runbooks/finops/nat_gateway_optimizer.py +76 -20
  33. runbooks/finops/tests/test_integration.py +3 -1
  34. runbooks/finops/vpc_cleanup_exporter.py +28 -26
  35. runbooks/finops/vpc_cleanup_optimizer.py +363 -16
  36. runbooks/inventory/__init__.py +10 -1
  37. runbooks/inventory/cloud_foundations_integration.py +409 -0
  38. runbooks/inventory/core/collector.py +1177 -94
  39. runbooks/inventory/discovery.md +339 -0
  40. runbooks/inventory/drift_detection_cli.py +327 -0
  41. runbooks/inventory/inventory_mcp_cli.py +171 -0
  42. runbooks/inventory/inventory_modules.py +6 -9
  43. runbooks/inventory/list_ec2_instances.py +3 -3
  44. runbooks/inventory/mcp_inventory_validator.py +2149 -0
  45. runbooks/inventory/mcp_vpc_validator.py +23 -6
  46. runbooks/inventory/organizations_discovery.py +104 -9
  47. runbooks/inventory/rich_inventory_display.py +129 -1
  48. runbooks/inventory/unified_validation_engine.py +1279 -0
  49. runbooks/inventory/verify_ec2_security_groups.py +3 -1
  50. runbooks/inventory/vpc_analyzer.py +825 -7
  51. runbooks/inventory/vpc_flow_analyzer.py +36 -42
  52. runbooks/main.py +708 -47
  53. runbooks/monitoring/performance_monitor.py +11 -7
  54. runbooks/operate/base.py +9 -6
  55. runbooks/operate/deployment_framework.py +5 -4
  56. runbooks/operate/deployment_validator.py +6 -5
  57. runbooks/operate/dynamodb_operations.py +6 -5
  58. runbooks/operate/ec2_operations.py +3 -2
  59. runbooks/operate/mcp_integration.py +6 -5
  60. runbooks/operate/networking_cost_heatmap.py +21 -16
  61. runbooks/operate/s3_operations.py +13 -12
  62. runbooks/operate/vpc_operations.py +100 -12
  63. runbooks/remediation/base.py +4 -2
  64. runbooks/remediation/commons.py +5 -5
  65. runbooks/remediation/commvault_ec2_analysis.py +68 -15
  66. runbooks/remediation/config/accounts_example.json +31 -0
  67. runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
  68. runbooks/remediation/multi_account.py +120 -7
  69. runbooks/remediation/rds_snapshot_list.py +5 -3
  70. runbooks/remediation/remediation_cli.py +710 -0
  71. runbooks/remediation/universal_account_discovery.py +377 -0
  72. runbooks/security/compliance_automation_engine.py +99 -20
  73. runbooks/security/config/__init__.py +24 -0
  74. runbooks/security/config/compliance_config.py +255 -0
  75. runbooks/security/config/compliance_weights_example.json +22 -0
  76. runbooks/security/config_template_generator.py +500 -0
  77. runbooks/security/security_cli.py +377 -0
  78. runbooks/validation/__init__.py +21 -1
  79. runbooks/validation/cli.py +8 -7
  80. runbooks/validation/comprehensive_2way_validator.py +2007 -0
  81. runbooks/validation/mcp_validator.py +965 -101
  82. runbooks/validation/terraform_citations_validator.py +363 -0
  83. runbooks/validation/terraform_drift_detector.py +1098 -0
  84. runbooks/vpc/cleanup_wrapper.py +231 -10
  85. runbooks/vpc/config.py +346 -73
  86. runbooks/vpc/cross_account_session.py +312 -0
  87. runbooks/vpc/heatmap_engine.py +115 -41
  88. runbooks/vpc/manager_interface.py +9 -9
  89. runbooks/vpc/mcp_no_eni_validator.py +1630 -0
  90. runbooks/vpc/networking_wrapper.py +14 -8
  91. runbooks/vpc/runbooks_adapter.py +33 -12
  92. runbooks/vpc/tests/conftest.py +4 -2
  93. runbooks/vpc/tests/test_cost_engine.py +4 -2
  94. runbooks/vpc/unified_scenarios.py +73 -3
  95. runbooks/vpc/vpc_cleanup_integration.py +512 -78
  96. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/METADATA +94 -52
  97. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/RECORD +101 -81
  98. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  99. runbooks/finops/runbooks.security.report_generator.log +0 -0
  100. runbooks/finops/runbooks.security.run_script.log +0 -0
  101. runbooks/finops/runbooks.security.security_export.log +0 -0
  102. runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
  103. runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
  104. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  105. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  106. runbooks/inventory/runbooks.security.run_script.log +0 -0
  107. runbooks/inventory/runbooks.security.security_export.log +0 -0
  108. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
  109. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,42 +1,50 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Profile Management Utilities for CloudOps Runbooks Platform
3
+ Universal AWS Profile Management for CloudOps Runbooks Platform
4
4
 
5
- This module provides centralized AWS profile management with enterprise-grade
6
- three-tier priority system extracted from proven FinOps success patterns.
5
+ This module provides truly universal AWS profile management that works with ANY AWS setup:
6
+ - Single account setups
7
+ - Multi-account setups
8
+ - Any profile naming convention
9
+ - No specific environment variable requirements
7
10
 
8
11
  Features:
9
- - Three-tier priority: User > Environment > Default
10
- - Multi-profile enterprise architecture support
11
- - Consistent session creation across all modules
12
- - Rich CLI integration for user feedback
13
- - Profile validation and error handling
12
+ - Universal compatibility: User --profile AWS_PROFILE → "default"
13
+ - Works with ANY AWS profile names (not just specific test profiles)
14
+ - No hardcoded environment variable assumptions
15
+ - Simple, reliable profile selection for all users
14
16
 
15
17
  Author: CloudOps Runbooks Team
16
- Version: 0.9.0
18
+ Version: 1.0.0 - Universal Compatibility
17
19
  """
18
20
 
19
21
  import os
22
+ import time
20
23
  from typing import Dict, Optional
21
24
 
22
25
  import boto3
23
26
 
24
27
  from runbooks.common.rich_utils import console
25
28
 
29
+ # Profile cache to reduce duplicate calls (performance optimization)
30
+ _profile_cache = {}
31
+ _cache_timestamp = None
32
+ _cache_ttl = 300 # 5 minutes cache TTL
33
+
26
34
 
27
35
  def get_profile_for_operation(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
28
36
  """
29
- Get the appropriate AWS profile based on operation type using proven three-tier priority system.
37
+ Universal AWS profile selection that works with ANY AWS setup.
30
38
 
31
- PRIORITY ORDER (Enterprise Success Pattern):
39
+ SIMPLE PRIORITY ORDER (Universal Compatibility):
32
40
  1. User-specified profile (--profile parameter) - HIGHEST PRIORITY
33
- 2. Environment variables for specialized operations - FALLBACK ONLY
34
- 3. Default profile - LAST RESORT
41
+ 2. AWS_PROFILE environment variable - STANDARD AWS CONVENTION
42
+ 3. "default" profile - AWS STANDARD FALLBACK
35
43
 
36
- This pattern extracted from FinOps module achieving 99.9996% accuracy and 280% ROI.
44
+ Works with ANY profile names and ANY AWS setup - no specific environment variable requirements.
37
45
 
38
46
  Args:
39
- operation_type: Type of operation ('billing', 'management', 'operational')
47
+ operation_type: Type of operation (informational only, not used for profile selection)
40
48
  user_specified_profile: Profile specified by user via --profile parameter
41
49
 
42
50
  Returns:
@@ -45,43 +53,59 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
45
53
  Raises:
46
54
  SystemExit: If user-specified profile not found in AWS config
47
55
  """
56
+ global _profile_cache, _cache_timestamp
57
+
58
+ # Check cache first to reduce duplicate calls (performance optimization)
59
+ cache_key = f"{operation_type}:{user_specified_profile or 'None'}"
60
+ current_time = time.time()
61
+
62
+ if (_cache_timestamp and
63
+ current_time - _cache_timestamp < _cache_ttl and
64
+ cache_key in _profile_cache):
65
+ return _profile_cache[cache_key]
66
+
67
+ # Clear cache if TTL expired
68
+ if not _cache_timestamp or current_time - _cache_timestamp >= _cache_ttl:
69
+ _profile_cache.clear()
70
+ _cache_timestamp = current_time
71
+
48
72
  available_profiles = boto3.Session().available_profiles
49
73
 
50
74
  # PRIORITY 1: User-specified profile ALWAYS takes precedence
51
75
  if user_specified_profile and user_specified_profile != "default":
52
76
  if user_specified_profile in available_profiles:
53
- console.log(f"[green]Using user-specified profile for {operation_type}: {user_specified_profile}[/]")
77
+ console.log(f"[green]Using user-specified profile: {user_specified_profile}[/]")
78
+ # Cache the result to reduce duplicate calls
79
+ _profile_cache[cache_key] = user_specified_profile
54
80
  return user_specified_profile
55
81
  else:
56
- console.log(f"[red]Error: User-specified profile '{user_specified_profile}' not found in AWS config[/]")
57
- # Don't fall back - user explicitly chose this profile
82
+ console.log(f"[red]Error: Profile '{user_specified_profile}' not found in AWS config[/]")
83
+ console.log(f"[yellow]Available profiles: {', '.join(available_profiles)}[/]")
58
84
  raise SystemExit(1)
59
85
 
60
- # PRIORITY 2: Environment variables (only when no user input)
61
- profile_map = {
62
- "billing": os.getenv("AWS_BILLING_PROFILE") or os.getenv("BILLING_PROFILE"),
63
- "management": os.getenv("AWS_MANAGEMENT_PROFILE") or os.getenv("MANAGEMENT_PROFILE"),
64
- "operational": os.getenv("AWS_CENTRALISED_OPS_PROFILE") or os.getenv("CENTRALISED_OPS_PROFILE"),
65
- "single_account": os.getenv("AWS_SINGLE_ACCOUNT_PROFILE") or os.getenv("SINGLE_AWS_PROFILE"),
66
- }
67
-
68
- env_profile = profile_map.get(operation_type)
69
- if env_profile and env_profile in available_profiles:
70
- console.log(f"[dim cyan]Using {operation_type} profile from environment: {env_profile}[/]")
71
- return env_profile
86
+ # PRIORITY 2: AWS_PROFILE environment variable (standard AWS convention)
87
+ aws_profile = os.getenv("AWS_PROFILE")
88
+ if aws_profile and aws_profile in available_profiles:
89
+ console.log(f"[dim cyan]Using AWS_PROFILE environment variable: {aws_profile}[/]")
90
+ # Cache the result to reduce duplicate calls
91
+ _profile_cache[cache_key] = aws_profile
92
+ return aws_profile
72
93
 
73
- # PRIORITY 3: Default profile (last resort)
74
- console.log(f"[yellow]No {operation_type} profile found, using default: {user_specified_profile or 'default'}[/]")
75
- return user_specified_profile or "default"
94
+ # PRIORITY 3: Default profile (AWS standard fallback)
95
+ default_profile = "default"
96
+ console.log(f"[yellow]Using default AWS profile: {default_profile}[/]")
97
+ # Cache the result to reduce duplicate calls
98
+ _profile_cache[cache_key] = default_profile
99
+ return default_profile
76
100
 
77
101
 
78
102
  def resolve_profile_for_operation_silent(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
79
103
  """
80
- Resolve AWS profile for operation type without logging (for display purposes).
81
- Uses the same logic as get_profile_for_operation but without console output.
104
+ Universal AWS profile resolution without logging (for display purposes).
105
+ Uses the same universal logic as get_profile_for_operation but without console output.
82
106
 
83
107
  Args:
84
- operation_type: Type of operation ('billing', 'management', 'operational')
108
+ operation_type: Type of operation (informational only, not used for profile selection)
85
109
  user_specified_profile: Profile specified by user via --profile parameter
86
110
 
87
111
  Returns:
@@ -100,78 +124,72 @@ def resolve_profile_for_operation_silent(operation_type: str, user_specified_pro
100
124
  # Don't fall back - user explicitly chose this profile
101
125
  raise SystemExit(1)
102
126
 
103
- # PRIORITY 2: Environment variables (only when no user input)
104
- profile_map = {
105
- "billing": os.getenv("AWS_BILLING_PROFILE") or os.getenv("BILLING_PROFILE"),
106
- "management": os.getenv("AWS_MANAGEMENT_PROFILE") or os.getenv("MANAGEMENT_PROFILE"),
107
- "operational": os.getenv("AWS_CENTRALISED_OPS_PROFILE") or os.getenv("CENTRALISED_OPS_PROFILE"),
108
- "single_account": os.getenv("AWS_SINGLE_ACCOUNT_PROFILE") or os.getenv("SINGLE_AWS_PROFILE"),
109
- }
110
-
111
- env_profile = profile_map.get(operation_type)
112
- if env_profile and env_profile in available_profiles:
113
- return env_profile
127
+ # PRIORITY 2: AWS_PROFILE environment variable (standard AWS convention)
128
+ aws_profile = os.getenv("AWS_PROFILE")
129
+ if aws_profile and aws_profile in available_profiles:
130
+ return aws_profile
114
131
 
115
- # PRIORITY 3: Default profile (last resort)
116
- return user_specified_profile or "default"
132
+ # PRIORITY 3: Default profile (AWS standard fallback)
133
+ return "default"
117
134
 
118
135
 
119
136
  def create_cost_session(profile: Optional[str] = None) -> boto3.Session:
120
137
  """
121
- Create a boto3 session specifically for cost operations.
122
- User-specified profile takes priority over BILLING_PROFILE environment variable.
138
+ Create a boto3 session for cost operations with universal profile support.
139
+ Works with ANY AWS profile configuration.
123
140
 
124
141
  Args:
125
142
  profile: User-specified profile (from --profile parameter)
126
143
 
127
144
  Returns:
128
- boto3.Session: Session configured for cost operations
145
+ boto3.Session: Session configured for AWS operations
129
146
  """
130
- cost_profile = get_profile_for_operation("billing", profile)
131
- return boto3.Session(profile_name=cost_profile)
147
+ selected_profile = get_profile_for_operation("cost", profile)
148
+ return boto3.Session(profile_name=selected_profile)
132
149
 
133
150
 
134
151
  def create_management_session(profile: Optional[str] = None) -> boto3.Session:
135
152
  """
136
- Create a boto3 session specifically for management operations.
137
- User-specified profile takes priority over MANAGEMENT_PROFILE environment variable.
153
+ Create a boto3 session for management operations with universal profile support.
154
+ Works with ANY AWS profile configuration.
138
155
 
139
156
  Args:
140
157
  profile: User-specified profile (from --profile parameter)
141
158
 
142
159
  Returns:
143
- boto3.Session: Session configured for management operations
160
+ boto3.Session: Session configured for AWS operations
144
161
  """
145
- mgmt_profile = get_profile_for_operation("management", profile)
146
- return boto3.Session(profile_name=mgmt_profile)
162
+ selected_profile = get_profile_for_operation("management", profile)
163
+ return boto3.Session(profile_name=selected_profile)
147
164
 
148
165
 
149
166
  def create_operational_session(profile: Optional[str] = None) -> boto3.Session:
150
167
  """
151
- Create a boto3 session specifically for operational tasks.
152
- User-specified profile takes priority over CENTRALISED_OPS_PROFILE environment variable.
168
+ Create a boto3 session for operational tasks with universal profile support.
169
+ Works with ANY AWS profile configuration.
153
170
 
154
171
  Args:
155
172
  profile: User-specified profile (from --profile parameter)
156
173
 
157
174
  Returns:
158
- boto3.Session: Session configured for operational tasks
175
+ boto3.Session: Session configured for AWS operations
159
176
  """
160
- ops_profile = get_profile_for_operation("operational", profile)
161
- return boto3.Session(profile_name=ops_profile)
177
+ selected_profile = get_profile_for_operation("operational", profile)
178
+ return boto3.Session(profile_name=selected_profile)
162
179
 
163
180
 
164
- def get_enterprise_profile_mapping() -> Dict[str, Optional[str]]:
181
+ def get_current_profile_info() -> Dict[str, Optional[str]]:
165
182
  """
166
- Get current enterprise profile mapping from environment variables.
183
+ Get current AWS profile information using universal approach.
184
+ Works with ANY AWS setup without hardcoded environment variable assumptions.
167
185
 
168
186
  Returns:
169
- Dict mapping operation types to their environment profile values
187
+ Dict with current profile information
170
188
  """
171
189
  return {
172
- "billing": os.getenv("BILLING_PROFILE"),
173
- "management": os.getenv("MANAGEMENT_PROFILE"),
174
- "operational": os.getenv("CENTRALISED_OPS_PROFILE"),
190
+ "aws_profile": os.getenv("AWS_PROFILE"),
191
+ "default_profile": "default",
192
+ "available_profiles": boto3.Session().available_profiles
175
193
  }
176
194
 
177
195
 
@@ -206,13 +224,50 @@ def validate_profile_access(profile_name: str, operation_type: str = "general")
206
224
  return False
207
225
 
208
226
 
227
+ def get_available_profiles_for_validation() -> list:
228
+ """
229
+ Get available AWS profiles for validation - truly universal approach.
230
+
231
+ Returns all configured AWS profiles for validation without ANY hardcoded assumptions.
232
+ Works with any AWS setup: single account, multi-account, any profile naming convention.
233
+
234
+ Returns:
235
+ list: Available AWS profile names for validation
236
+ """
237
+ try:
238
+ # Get all available profiles from AWS CLI configuration
239
+ available_profiles = boto3.Session().available_profiles
240
+
241
+ # Start with AWS_PROFILE if set
242
+ validation_profiles = []
243
+ aws_profile = os.getenv("AWS_PROFILE")
244
+ if aws_profile and aws_profile in available_profiles:
245
+ validation_profiles.append(aws_profile)
246
+
247
+ # Add all other available profiles (universal approach)
248
+ for profile in available_profiles:
249
+ if profile not in validation_profiles:
250
+ validation_profiles.append(profile)
251
+
252
+ # Ensure we have at least one profile to test
253
+ if not validation_profiles:
254
+ validation_profiles = ['default']
255
+
256
+ return validation_profiles
257
+
258
+ except Exception as e:
259
+ console.log(f"[yellow]Warning: Could not detect AWS profiles: {e}[/]")
260
+ return ['default'] # Fallback to default profile
261
+
262
+
209
263
  # Export all public functions
210
264
  __all__ = [
211
265
  "get_profile_for_operation",
212
266
  "resolve_profile_for_operation_silent",
213
- "create_cost_session",
267
+ "create_cost_session",
214
268
  "create_management_session",
215
269
  "create_operational_session",
216
- "get_enterprise_profile_mapping",
270
+ "get_current_profile_info",
217
271
  "validate_profile_access",
272
+ "get_available_profiles_for_validation",
218
273
  ]
@@ -358,8 +358,8 @@ def create_display_profile_name(profile_name: str, max_length: int = 25, context
358
358
  meaningful information for identification. Full names remain available for AWS API calls.
359
359
 
360
360
  Examples:
361
- 'ams-admin-Billing-ReadOnlyAccess-909135376185' → 'ams-admin-Billing-9091...'
362
- 'ams-centralised-ops-ReadOnlyAccess-335083429030' → 'ams-centralised-ops-3350...'
361
+ 'your-admin-Billing-ReadOnlyAccess-123456789012' → 'your-admin-Billing-1234...'
362
+ 'your-centralised-ops-ReadOnlyAccess-987654321098' → 'your-centralised-ops-9876...'
363
363
  'short-profile' → 'short-profile' (no truncation needed)
364
364
 
365
365
  Args:
@@ -398,7 +398,7 @@ def create_display_profile_name(profile_name: str, max_length: int = 25, context
398
398
 
399
399
  # Strategy 1: Keep meaningful prefix + account ID suffix
400
400
  if len(parts) >= 4 and parts[-1].isdigit():
401
- # Enterprise pattern: ams-admin-Billing-ReadOnlyAccess-909135376185
401
+ # Enterprise pattern: your-admin-Billing-ReadOnlyAccess-123456789012
402
402
  account_id = parts[-1]
403
403
  prefix_parts = parts[:-2] # Skip permissions part for brevity
404
404
 
@@ -25,6 +25,7 @@ from ..common.rich_utils import (
25
25
  console, print_header, print_success, print_error, print_warning,
26
26
  create_table, create_progress_bar, format_cost
27
27
  )
28
+ from ..common.aws_pricing import DynamicAWSPricing
28
29
 
29
30
  @dataclass
30
31
  class IdleInstance:
@@ -55,7 +56,7 @@ class UnusedNATGateway:
55
56
  region: str
56
57
  vpc_id: str = ""
57
58
  state: str = ""
58
- estimated_monthly_cost: float = 45.0 # ~$45/month per NAT Gateway
59
+ estimated_monthly_cost: float = 0.0 # Calculated dynamically using AWS pricing
59
60
  creation_date: Optional[str] = None
60
61
  tags: Dict[str, str] = Field(default_factory=dict)
61
62
 
@@ -132,38 +132,57 @@ def estimate_resource_costs(session: boto3.Session, regions: List[str]) -> Dict[
132
132
  }
133
133
 
134
134
  try:
135
- # EC2 Instance cost estimation with performance optimization
135
+ # EC2 Instance cost estimation using dynamic AWS pricing
136
136
  profile_name = session.profile_name if hasattr(session, "profile_name") else None
137
137
  ec2_data = ec2_summary(session, regions, profile_name)
138
+
139
+ from ..common.aws_pricing import get_ec2_monthly_cost, get_aws_pricing_engine
140
+ from ..common.rich_utils import console
141
+
138
142
  for instance_type, count in ec2_data.items():
139
143
  if count > 0:
140
- # Estimate monthly cost based on instance type
141
- # Using approximate AWS pricing (simplified model)
142
- hourly_rates = {
143
- "t3.nano": 0.0052,
144
- "t3.micro": 0.0104,
145
- "t3.small": 0.0208,
146
- "t3.medium": 0.0416,
147
- "t3.large": 0.0832,
148
- "t3.xlarge": 0.1664,
149
- "t2.nano": 0.0058,
150
- "t2.micro": 0.0116,
151
- "t2.small": 0.023,
152
- "m5.large": 0.096,
153
- "m5.xlarge": 0.192,
154
- "m5.2xlarge": 0.384,
155
- "c5.large": 0.085,
156
- "c5.xlarge": 0.17,
157
- "c5.2xlarge": 0.34,
158
- "r5.large": 0.126,
159
- "r5.xlarge": 0.252,
160
- "r5.2xlarge": 0.504,
161
- }
162
-
163
- base_type = instance_type.lower()
164
- hourly_rate = hourly_rates.get(base_type, 0.05) # Default rate
165
- monthly_cost = hourly_rate * 24 * 30 * count # Hours * days * instances
166
- estimated_costs["EC2-Instance"] += monthly_cost
144
+ try:
145
+ # Use dynamic AWS pricing - NO hardcoded values
146
+ # Assume primary region for cost estimation
147
+ primary_region = regions[0] if regions else "us-east-1"
148
+ monthly_cost_per_instance = get_ec2_monthly_cost(instance_type, primary_region)
149
+ total_monthly_cost = monthly_cost_per_instance * count
150
+ estimated_costs["EC2-Instance"] += total_monthly_cost
151
+
152
+ console.print(
153
+ f"[dim]Dynamic pricing: {count}x {instance_type} = "
154
+ f"${total_monthly_cost:.2f}/month[/]"
155
+ )
156
+
157
+ except Exception as e:
158
+ console.print(
159
+ f"[yellow]⚠ Warning: Could not get dynamic pricing for {instance_type}: {e}[/yellow]"
160
+ )
161
+
162
+ try:
163
+ # Use fallback pricing engine with AWS patterns
164
+ pricing_engine = get_aws_pricing_engine(enable_fallback=True)
165
+ primary_region = regions[0] if regions else "us-east-1"
166
+ result = pricing_engine.get_ec2_instance_pricing(instance_type, primary_region)
167
+ total_monthly_cost = result.monthly_cost * count
168
+ estimated_costs["EC2-Instance"] += total_monthly_cost
169
+
170
+ console.print(
171
+ f"[dim]Fallback pricing: {count}x {instance_type} = "
172
+ f"${total_monthly_cost:.2f}/month[/]"
173
+ )
174
+
175
+ except Exception as fallback_error:
176
+ console.print(
177
+ f"[red]⚠ ERROR: All pricing methods failed for {instance_type}: {fallback_error}[/red]"
178
+ )
179
+ console.print(
180
+ f"[red]Skipping cost estimation for {count}x {instance_type}[/red]"
181
+ )
182
+ logger.error(
183
+ f"ENTERPRISE VIOLATION: Cannot estimate cost for {instance_type} "
184
+ f"without hardcoded values. Instance type skipped."
185
+ )
167
186
 
168
187
  # Add some EC2-Other costs (EBS, snapshots, etc.)
169
188
  estimated_costs["EC2-Other"] = estimated_costs["EC2-Instance"] * 0.3
@@ -166,15 +166,8 @@ class EBSOptimizer:
166
166
  profile_name=get_profile_for_operation("operational", profile_name)
167
167
  )
168
168
 
169
- # EBS pricing (per GB per month, as of 2024)
170
- self.ebs_pricing = {
171
- 'gp2': 0.10, # $0.10/GB/month
172
- 'gp3': 0.08, # $0.08/GB/month (20% cheaper than GP2)
173
- 'io1': 0.125, # $0.125/GB/month
174
- 'io2': 0.125, # $0.125/GB/month
175
- 'st1': 0.045, # $0.045/GB/month
176
- 'sc1': 0.025, # $0.025/GB/month
177
- }
169
+ # EBS pricing using dynamic AWS pricing engine for universal compatibility
170
+ self.ebs_pricing = self._initialize_dynamic_ebs_pricing()
178
171
 
179
172
  # GP3 conversion savings percentage
180
173
  self.gp3_savings_percentage = 0.20 # 20% savings GP2→GP3
@@ -184,6 +177,60 @@ class EBSOptimizer:
184
177
  self.low_usage_threshold_bytes = 1_000_000 # 1MB per day
185
178
  self.analysis_period_days = 7
186
179
 
180
+ def _initialize_dynamic_ebs_pricing(self) -> Dict[str, float]:
181
+ """Initialize dynamic EBS pricing using AWS pricing engine for universal compatibility."""
182
+ try:
183
+ from ..common.aws_pricing import get_service_monthly_cost
184
+
185
+ # Get dynamic pricing for common EBS volume types in us-east-1 (base region)
186
+ base_region = "us-east-1"
187
+
188
+ return {
189
+ 'gp2': get_service_monthly_cost("ebs_gp2", base_region, self.profile_name),
190
+ 'gp3': get_service_monthly_cost("ebs_gp3", base_region, self.profile_name),
191
+ 'io1': get_service_monthly_cost("ebs_io1", base_region, self.profile_name),
192
+ 'io2': get_service_monthly_cost("ebs_io2", base_region, self.profile_name),
193
+ 'st1': get_service_monthly_cost("ebs_st1", base_region, self.profile_name),
194
+ 'sc1': get_service_monthly_cost("ebs_sc1", base_region, self.profile_name),
195
+ }
196
+ except Exception as e:
197
+ print_warning(f"Dynamic EBS pricing initialization failed: {e}")
198
+ print_warning("Attempting AWS Pricing API fallback with universal profile support")
199
+
200
+ try:
201
+ from ..common.aws_pricing import get_aws_pricing_engine
202
+
203
+ # Use AWS Pricing API with profile support for universal compatibility
204
+ pricing_engine = get_aws_pricing_engine(profile=self.profile_name, enable_fallback=True)
205
+
206
+ # Get actual AWS pricing instead of hardcoded values
207
+ gp2_pricing = pricing_engine.get_ebs_pricing("gp2", "us-east-1")
208
+ gp3_pricing = pricing_engine.get_ebs_pricing("gp3", "us-east-1")
209
+ io1_pricing = pricing_engine.get_ebs_pricing("io1", "us-east-1")
210
+ io2_pricing = pricing_engine.get_ebs_pricing("io2", "us-east-1")
211
+ st1_pricing = pricing_engine.get_ebs_pricing("st1", "us-east-1")
212
+ sc1_pricing = pricing_engine.get_ebs_pricing("sc1", "us-east-1")
213
+
214
+ return {
215
+ 'gp2': gp2_pricing.monthly_cost_per_gb,
216
+ 'gp3': gp3_pricing.monthly_cost_per_gb,
217
+ 'io1': io1_pricing.monthly_cost_per_gb,
218
+ 'io2': io2_pricing.monthly_cost_per_gb,
219
+ 'st1': st1_pricing.monthly_cost_per_gb,
220
+ 'sc1': sc1_pricing.monthly_cost_per_gb,
221
+ }
222
+
223
+ except Exception as pricing_error:
224
+ print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine EBS pricing without AWS API access: {pricing_error}")
225
+ print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
226
+
227
+ # Return error state instead of hardcoded values to maintain enterprise compliance
228
+ raise RuntimeError(
229
+ "Universal compatibility mode requires dynamic AWS pricing API access. "
230
+ "Please ensure your AWS profile has pricing:GetProducts permissions or configure "
231
+ "appropriate billing/management profile access."
232
+ )
233
+
187
234
  async def analyze_ebs_volumes(self, dry_run: bool = True) -> EBSOptimizerResults:
188
235
  """
189
236
  Comprehensive EBS volume cost optimization analysis.
@@ -36,6 +36,7 @@ from ..common.rich_utils import (
36
36
  console, print_header, print_success, print_error, print_warning, print_info,
37
37
  create_table, create_progress_bar, format_cost, create_panel, STATUS_INDICATORS
38
38
  )
39
+ from ..common.aws_pricing import get_service_monthly_cost, calculate_annual_cost
39
40
  from .embedded_mcp_validator import EmbeddedMCPValidator
40
41
  from ..common.profile_utils import get_profile_for_operation
41
42
 
@@ -65,8 +66,8 @@ class ElasticIPOptimizationResult(BaseModel):
65
66
  domain: str
66
67
  is_attached: bool
67
68
  instance_id: Optional[str] = None
68
- monthly_cost: float = 3.65 # $3.65/month for unattached EIPs
69
- annual_cost: float = 43.80 # $43.80/year for unattached EIPs
69
+ monthly_cost: float = 0.0 # Calculated dynamically per region
70
+ annual_cost: float = 0.0 # Calculated dynamically (monthly * 12)
70
71
  optimization_recommendation: str = "retain" # retain, release
71
72
  risk_level: str = "low" # low, medium, high
72
73
  business_impact: str = "minimal"
@@ -118,8 +119,8 @@ class ElasticIPOptimizer:
118
119
  profile_name=get_profile_for_operation("operational", profile_name)
119
120
  )
120
121
 
121
- # Elastic IP pricing (per month, as of 2024)
122
- self.elastic_ip_monthly_cost = 3.65 # $3.65/month per unattached EIP
122
+ # Dynamic Elastic IP pricing - Enterprise compliance (no hardcoded values)
123
+ # Pricing will be calculated dynamically per region using AWS Pricing API
123
124
 
124
125
  # All AWS regions for comprehensive discovery
125
126
  self.all_regions = [
@@ -360,9 +361,12 @@ class ElasticIPOptimizer:
360
361
  try:
361
362
  dns_refs = dns_dependencies.get(elastic_ip.allocation_id, [])
362
363
 
363
- # Calculate current costs (only unattached EIPs are charged)
364
- monthly_cost = self.elastic_ip_monthly_cost if not elastic_ip.is_attached else 0.0
365
- annual_cost = monthly_cost * 12
364
+ # Calculate current costs (only unattached EIPs are charged) - Dynamic pricing
365
+ if elastic_ip.is_attached:
366
+ monthly_cost = 0.0 # Attached EIPs are free
367
+ else:
368
+ monthly_cost = get_service_monthly_cost("elastic_ip", elastic_ip.region)
369
+ annual_cost = calculate_annual_cost(monthly_cost)
366
370
 
367
371
  # Determine optimization recommendation
368
372
  recommendation = "retain" # Default: keep the Elastic IP
@@ -384,14 +388,14 @@ class ElasticIPOptimizer:
384
388
  recommendation = "release"
385
389
  risk_level = "low"
386
390
  business_impact = "none"
387
- potential_monthly_savings = self.elastic_ip_monthly_cost
391
+ potential_monthly_savings = monthly_cost
388
392
  safety_checks["safe_to_release"] = True
389
393
  else:
390
394
  # Unattached but has DNS references - investigate before release
391
395
  recommendation = "investigate"
392
396
  risk_level = "medium"
393
397
  business_impact = "potential"
394
- potential_monthly_savings = self.elastic_ip_monthly_cost * 0.8 # Conservative estimate
398
+ potential_monthly_savings = monthly_cost * 0.8 # Conservative estimate
395
399
  elif elastic_ip.is_attached:
396
400
  # Attached EIPs are retained (no cost for attached EIPs)
397
401
  recommendation = "retain"
@@ -126,6 +126,37 @@ class EmbeddedMCPValidator:
126
126
  self._finalize_validation_results(validation_results)
127
127
  return validation_results
128
128
 
129
+ def validate_cost_data_sync(self, runbooks_data: Dict[str, Any]) -> Dict[str, Any]:
130
+ """
131
+ Synchronous wrapper for MCP validation - for compatibility with test scripts.
132
+
133
+ Args:
134
+ runbooks_data: Cost data from runbooks FinOps analysis
135
+
136
+ Returns:
137
+ Validation results with accuracy metrics
138
+ """
139
+ # Use asyncio to run the async validation method
140
+ try:
141
+ loop = asyncio.get_event_loop()
142
+ except RuntimeError:
143
+ loop = asyncio.new_event_loop()
144
+ asyncio.set_event_loop(loop)
145
+
146
+ try:
147
+ return loop.run_until_complete(self.validate_cost_data_async(runbooks_data))
148
+ except Exception as e:
149
+ print_error(f"Synchronous MCP validation failed: {e}")
150
+ return {
151
+ "validation_timestamp": datetime.now().isoformat(),
152
+ "profiles_validated": 0,
153
+ "total_accuracy": 0.0,
154
+ "passed_validation": False,
155
+ "profile_results": [],
156
+ "validation_method": "embedded_mcp_direct_aws_api_sync",
157
+ "error": str(e)
158
+ }
159
+
129
160
  def _validate_profile_sync(self, profile: str, session: boto3.Session, runbooks_data: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
130
161
  """Synchronous wrapper for profile validation (for parallel execution)."""
131
162
  try:
@@ -343,8 +343,9 @@ def create_resource_based_trend_estimate(session, months: int = 6) -> List[Tuple
343
343
  current_date = datetime.now()
344
344
  trend_data = []
345
345
 
346
- # Base resource cost estimation (simplified model)
347
- base_monthly_cost = 850.0 # Starting baseline
346
+ # Base resource cost estimation (dynamic model)
347
+ # Calculate baseline from actual service usage patterns
348
+ base_monthly_cost = 0.0 # Will be calculated from actual usage data or dynamic pricing
348
349
 
349
350
  # Simulate realistic cost variations over 6 months
350
351
  # Based on typical AWS usage patterns
@@ -408,10 +409,15 @@ if __name__ == "__main__":
408
409
  console.print("[bold bright_cyan]🚀 CloudOps Runbooks - Enhanced Trend Analysis[/]")
409
410
  console.print("[dim]QA Testing Specialist Implementation - Reference Image Compliance[/]")
410
411
 
412
+ import os
413
+ # Use environment-driven values for universal compatibility
414
+ account_id = os.getenv("AWS_ACCOUNT_ID", "123456789012")
415
+ profile = os.getenv("SINGLE_AWS_PROFILE", "default-single-profile")
416
+
411
417
  visualizer.create_enhanced_trend_display(
412
418
  monthly_costs=trend_data,
413
- account_id="499201730520",
414
- profile="ams-shared-services-non-prod-ReadOnlyAccess-499201730520",
419
+ account_id=account_id,
420
+ profile=profile,
415
421
  )
416
422
 
417
423
  # Export to JSON (contract compliance)