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.
- 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/cloud_foundations_assessment.py +626 -0
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/cloudops/cost_optimizer.py +95 -33
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1353 -0
- runbooks/common/aws_pricing_api.py +205 -0
- runbooks/common/aws_utils.py +2 -2
- runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
- runbooks/common/cross_account_manager.py +606 -0
- runbooks/common/date_utils.py +115 -0
- runbooks/common/enhanced_exception_handler.py +14 -7
- runbooks/common/env_utils.py +96 -0
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/mcp_integration.py +49 -2
- runbooks/common/organizations_client.py +579 -0
- runbooks/common/profile_utils.py +127 -72
- runbooks/common/rich_utils.py +3 -3
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/elastic_ip_optimizer.py +13 -9
- runbooks/finops/embedded_mcp_validator.py +31 -0
- runbooks/finops/enhanced_trend_visualization.py +10 -4
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/markdown_exporter.py +217 -2
- runbooks/finops/nat_gateway_optimizer.py +76 -20
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_exporter.py +28 -26
- runbooks/finops/vpc_cleanup_optimizer.py +363 -16
- runbooks/inventory/__init__.py +10 -1
- runbooks/inventory/cloud_foundations_integration.py +409 -0
- runbooks/inventory/core/collector.py +1177 -94
- runbooks/inventory/discovery.md +339 -0
- runbooks/inventory/drift_detection_cli.py +327 -0
- runbooks/inventory/inventory_mcp_cli.py +171 -0
- runbooks/inventory/inventory_modules.py +6 -9
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/mcp_inventory_validator.py +2149 -0
- runbooks/inventory/mcp_vpc_validator.py +23 -6
- runbooks/inventory/organizations_discovery.py +104 -9
- runbooks/inventory/rich_inventory_display.py +129 -1
- runbooks/inventory/unified_validation_engine.py +1279 -0
- runbooks/inventory/verify_ec2_security_groups.py +3 -1
- runbooks/inventory/vpc_analyzer.py +825 -7
- runbooks/inventory/vpc_flow_analyzer.py +36 -42
- runbooks/main.py +708 -47
- runbooks/monitoring/performance_monitor.py +11 -7
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/dynamodb_operations.py +6 -5
- runbooks/operate/ec2_operations.py +3 -2
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +21 -16
- runbooks/operate/s3_operations.py +13 -12
- runbooks/operate/vpc_operations.py +100 -12
- runbooks/remediation/base.py +4 -2
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +68 -15
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/rds_snapshot_list.py +5 -3
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- 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/__init__.py +21 -1
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +2007 -0
- runbooks/validation/mcp_validator.py +965 -101
- runbooks/validation/terraform_citations_validator.py +363 -0
- runbooks/validation/terraform_drift_detector.py +1098 -0
- runbooks/vpc/cleanup_wrapper.py +231 -10
- runbooks/vpc/config.py +346 -73
- runbooks/vpc/cross_account_session.py +312 -0
- runbooks/vpc/heatmap_engine.py +115 -41
- runbooks/vpc/manager_interface.py +9 -9
- runbooks/vpc/mcp_no_eni_validator.py +1630 -0
- runbooks/vpc/networking_wrapper.py +14 -8
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +4 -2
- runbooks/vpc/unified_scenarios.py +73 -3
- runbooks/vpc/vpc_cleanup_integration.py +512 -78
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/METADATA +94 -52
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/RECORD +101 -81
- 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-0.9.9.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
runbooks/common/profile_utils.py
CHANGED
@@ -1,42 +1,50 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
Profile Management
|
3
|
+
Universal AWS Profile Management for CloudOps Runbooks Platform
|
4
4
|
|
5
|
-
This module provides
|
6
|
-
|
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
|
-
-
|
10
|
-
-
|
11
|
-
-
|
12
|
-
-
|
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.
|
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
|
-
|
37
|
+
Universal AWS profile selection that works with ANY AWS setup.
|
30
38
|
|
31
|
-
PRIORITY ORDER (
|
39
|
+
SIMPLE PRIORITY ORDER (Universal Compatibility):
|
32
40
|
1. User-specified profile (--profile parameter) - HIGHEST PRIORITY
|
33
|
-
2.
|
34
|
-
3.
|
41
|
+
2. AWS_PROFILE environment variable - STANDARD AWS CONVENTION
|
42
|
+
3. "default" profile - AWS STANDARD FALLBACK
|
35
43
|
|
36
|
-
|
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 (
|
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
|
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:
|
57
|
-
|
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:
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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 (
|
74
|
-
|
75
|
-
|
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
|
-
|
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 (
|
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:
|
104
|
-
|
105
|
-
|
106
|
-
|
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 (
|
116
|
-
return
|
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
|
122
|
-
|
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
|
145
|
+
boto3.Session: Session configured for AWS operations
|
129
146
|
"""
|
130
|
-
|
131
|
-
return boto3.Session(profile_name=
|
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
|
137
|
-
|
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
|
160
|
+
boto3.Session: Session configured for AWS operations
|
144
161
|
"""
|
145
|
-
|
146
|
-
return boto3.Session(profile_name=
|
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
|
152
|
-
|
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
|
175
|
+
boto3.Session: Session configured for AWS operations
|
159
176
|
"""
|
160
|
-
|
161
|
-
return boto3.Session(profile_name=
|
177
|
+
selected_profile = get_profile_for_operation("operational", profile)
|
178
|
+
return boto3.Session(profile_name=selected_profile)
|
162
179
|
|
163
180
|
|
164
|
-
def
|
181
|
+
def get_current_profile_info() -> Dict[str, Optional[str]]:
|
165
182
|
"""
|
166
|
-
Get current
|
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
|
187
|
+
Dict with current profile information
|
170
188
|
"""
|
171
189
|
return {
|
172
|
-
"
|
173
|
-
"
|
174
|
-
"
|
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
|
-
"
|
270
|
+
"get_current_profile_info",
|
217
271
|
"validate_profile_access",
|
272
|
+
"get_available_profiles_for_validation",
|
218
273
|
]
|
runbooks/common/rich_utils.py
CHANGED
@@ -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
|
-
'
|
362
|
-
'
|
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:
|
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 =
|
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
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
"
|
144
|
-
|
145
|
-
|
146
|
-
"
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
runbooks/finops/ebs_optimizer.py
CHANGED
@@ -166,15 +166,8 @@ class EBSOptimizer:
|
|
166
166
|
profile_name=get_profile_for_operation("operational", profile_name)
|
167
167
|
)
|
168
168
|
|
169
|
-
# EBS pricing
|
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 =
|
69
|
-
annual_cost: float =
|
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
|
122
|
-
|
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
|
-
|
365
|
-
|
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 =
|
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 =
|
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 (
|
347
|
-
|
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=
|
414
|
-
profile=
|
419
|
+
account_id=account_id,
|
420
|
+
profile=profile,
|
415
421
|
)
|
416
422
|
|
417
423
|
# Export to JSON (contract compliance)
|