runbooks 1.1.0__py3-none-any.whl → 1.1.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/assessment/collectors.py +3 -2
- runbooks/cloudops/cost_optimizer.py +77 -61
- runbooks/cloudops/models.py +8 -2
- runbooks/common/aws_pricing.py +12 -0
- runbooks/common/profile_utils.py +213 -310
- runbooks/common/rich_utils.py +10 -16
- runbooks/finops/__init__.py +13 -5
- runbooks/finops/business_case_config.py +5 -5
- runbooks/finops/cli.py +24 -15
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/cost_processor.py +69 -22
- runbooks/finops/dashboard_router.py +3 -3
- runbooks/finops/dashboard_runner.py +3 -4
- runbooks/finops/enhanced_progress.py +213 -0
- runbooks/finops/markdown_exporter.py +4 -2
- runbooks/finops/multi_dashboard.py +1 -1
- runbooks/finops/nat_gateway_optimizer.py +85 -57
- runbooks/finops/scenario_cli_integration.py +212 -22
- runbooks/finops/scenarios.py +41 -25
- runbooks/finops/single_dashboard.py +68 -9
- runbooks/finops/tests/run_tests.py +5 -3
- runbooks/finops/workspaces_analyzer.py +10 -4
- runbooks/main.py +86 -25
- runbooks/operate/executive_dashboard.py +4 -3
- runbooks/remediation/rds_snapshot_list.py +13 -0
- runbooks/utils/version_validator.py +1 -1
- {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/METADATA +234 -40
- {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/RECORD +33 -33
- {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/WHEEL +0 -0
- {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.0.dist-info → runbooks-1.1.2.dist-info}/top_level.txt +0 -0
runbooks/common/profile_utils.py
CHANGED
@@ -1,92 +1,80 @@
|
|
1
|
-
|
2
|
-
"""
|
3
|
-
Universal AWS Profile Management for CloudOps Runbooks Platform
|
4
|
-
|
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
|
10
|
-
|
11
|
-
Features:
|
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
|
16
|
-
- Enhanced profile validation with enterprise error handling
|
17
|
-
|
18
|
-
Author: CloudOps Runbooks Team
|
19
|
-
Version: 1.1.0 - Enhanced Profile Validation
|
20
|
-
"""
|
21
|
-
|
1
|
+
# Profile utilities for multi-account AWS operations with enterprise caching
|
22
2
|
import os
|
23
3
|
import time
|
24
|
-
from functools import wraps
|
25
|
-
from typing import Dict, Optional, Union, List, Tuple, Callable
|
26
|
-
|
27
4
|
import boto3
|
28
|
-
from
|
29
|
-
|
30
|
-
from
|
31
|
-
|
32
|
-
#
|
33
|
-
_profile_cache = {}
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
5
|
+
from typing import Optional, List, Dict, Any
|
6
|
+
from runbooks.common.rich_utils import console
|
7
|
+
from datetime import datetime
|
8
|
+
|
9
|
+
# Enhanced caching system for enterprise performance
|
10
|
+
_profile_cache: Dict[str, str] = {}
|
11
|
+
_validation_cache: Dict[str, bool] = {} # Cache for profile validation results
|
12
|
+
_cache_timestamp: Optional[float] = None
|
13
|
+
_cache_ttl: int = 300 # 5 minutes TTL for enterprise session management
|
14
|
+
_session_id: Optional[str] = None # Track session consistency
|
15
|
+
_session_cache: Dict[str, boto3.Session] = {} # Cache AWS sessions
|
16
|
+
|
17
|
+
def _get_session_id() -> str:
|
18
|
+
"""Generate consistent session ID for cache scoping"""
|
19
|
+
global _session_id
|
20
|
+
if _session_id is None:
|
21
|
+
_session_id = f"session_{int(time.time())}"
|
22
|
+
return _session_id
|
23
|
+
|
24
|
+
def get_profile_for_operation(
|
25
|
+
operation_type: str,
|
26
|
+
user_specified_profile: Optional[str] = None,
|
27
|
+
profiles: Optional[List[str]] = None
|
28
|
+
) -> str:
|
39
29
|
"""
|
40
|
-
|
30
|
+
Enhanced profile resolution with intelligent caching and enterprise logging optimization.
|
41
31
|
|
42
|
-
|
32
|
+
Priority Order:
|
43
33
|
1. User-specified profile (--profile parameter) - HIGHEST PRIORITY
|
44
|
-
2.
|
45
|
-
3.
|
46
|
-
|
47
|
-
Works with ANY profile names and ANY AWS setup - no specific environment variable requirements.
|
34
|
+
2. Environment variable mapping (per operation type)
|
35
|
+
3. Default profile fallback
|
48
36
|
|
49
37
|
Args:
|
50
|
-
operation_type: Type of operation (
|
51
|
-
user_specified_profile:
|
38
|
+
operation_type: Type of operation (billing, management, operational)
|
39
|
+
user_specified_profile: User-provided profile via --profile parameter
|
40
|
+
profiles: List of profiles for multi-account operations
|
52
41
|
|
53
42
|
Returns:
|
54
|
-
|
43
|
+
Profile name to use for the operation
|
55
44
|
|
56
|
-
|
57
|
-
|
45
|
+
Caching Strategy:
|
46
|
+
- Cache profile resolution to prevent redundant AWS API calls
|
47
|
+
- Session-scoped caching with 5-minute TTL
|
48
|
+
- Only log profile selection once per session to reduce noise
|
58
49
|
"""
|
59
|
-
|
60
|
-
# This prevents errors like: Profile '('profile-name',)' not found
|
61
|
-
if isinstance(user_specified_profile, (tuple, list)) and user_specified_profile:
|
62
|
-
user_specified_profile = user_specified_profile[0] # Take first profile from tuple/list
|
63
|
-
elif isinstance(user_specified_profile, (tuple, list)) and not user_specified_profile:
|
64
|
-
user_specified_profile = None # Empty tuple/list becomes None
|
65
|
-
|
66
|
-
global _profile_cache, _cache_timestamp
|
67
|
-
|
68
|
-
# Check cache first to reduce duplicate calls (performance optimization)
|
69
|
-
cache_key = f"{operation_type}:{user_specified_profile or 'None'}"
|
50
|
+
global _cache_timestamp
|
70
51
|
current_time = time.time()
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
52
|
+
|
53
|
+
# Create profile-specific cache key (same profile for all operations)
|
54
|
+
profile_cache_key = f"{_get_session_id()}:{user_specified_profile or 'default'}"
|
55
|
+
|
56
|
+
# Return cached result if still valid and within TTL
|
57
|
+
if (profile_cache_key in _profile_cache and
|
58
|
+
_cache_timestamp and
|
59
|
+
current_time - _cache_timestamp < _cache_ttl):
|
60
|
+
return _profile_cache[profile_cache_key]
|
61
|
+
|
62
|
+
# Update cache timestamp only when cache is actually refreshed
|
78
63
|
if not _cache_timestamp or current_time - _cache_timestamp >= _cache_ttl:
|
79
64
|
_profile_cache.clear()
|
65
|
+
_validation_cache.clear()
|
80
66
|
_cache_timestamp = current_time
|
81
|
-
|
67
|
+
|
82
68
|
available_profiles = boto3.Session().available_profiles
|
83
69
|
|
84
70
|
# PRIORITY 1: User-specified profile ALWAYS takes precedence
|
85
71
|
if user_specified_profile and user_specified_profile != "default":
|
86
72
|
if user_specified_profile in available_profiles:
|
87
|
-
|
88
|
-
|
89
|
-
|
73
|
+
# SESSION-AWARE LOGGING: Only log when cache miss occurs
|
74
|
+
if profile_cache_key not in _profile_cache:
|
75
|
+
console.log(f"[green]Using user-specified profile: {user_specified_profile}[/]")
|
76
|
+
# Cache the result to prevent duplicate logging
|
77
|
+
_profile_cache[profile_cache_key] = user_specified_profile
|
90
78
|
return user_specified_profile
|
91
79
|
else:
|
92
80
|
console.log(f"[red]Error: Profile '{user_specified_profile}' not found in AWS config[/]")
|
@@ -96,312 +84,227 @@ def get_profile_for_operation(operation_type: str, user_specified_profile: Optio
|
|
96
84
|
# PRIORITY 2: AWS_PROFILE environment variable (standard AWS convention)
|
97
85
|
aws_profile = os.getenv("AWS_PROFILE")
|
98
86
|
if aws_profile and aws_profile in available_profiles:
|
99
|
-
|
100
|
-
# Cache the result to reduce duplicate calls
|
101
|
-
_profile_cache[cache_key] = aws_profile
|
87
|
+
_profile_cache[profile_cache_key] = aws_profile
|
102
88
|
return aws_profile
|
103
89
|
|
104
|
-
# PRIORITY 3:
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
90
|
+
# PRIORITY 3: Operation-specific environment variables
|
91
|
+
profile_map = {
|
92
|
+
"billing": os.getenv("BILLING_PROFILE"),
|
93
|
+
"management": os.getenv("MANAGEMENT_PROFILE"),
|
94
|
+
"operational": os.getenv("CENTRALISED_OPS_PROFILE"),
|
95
|
+
}
|
111
96
|
|
112
|
-
|
97
|
+
env_profile = profile_map.get(operation_type)
|
98
|
+
if env_profile and env_profile in available_profiles:
|
99
|
+
_profile_cache[profile_cache_key] = env_profile
|
100
|
+
return env_profile
|
101
|
+
|
102
|
+
# PRIORITY 4: Default profile fallback
|
103
|
+
if "default" in available_profiles:
|
104
|
+
_profile_cache[profile_cache_key] = "default"
|
105
|
+
return "default"
|
106
|
+
elif available_profiles:
|
107
|
+
# Use first available profile if no default
|
108
|
+
first_profile = available_profiles[0]
|
109
|
+
console.log(f"[yellow]Warning: No default profile found, using: {first_profile}[/]")
|
110
|
+
_profile_cache[profile_cache_key] = first_profile
|
111
|
+
return first_profile
|
112
|
+
else:
|
113
|
+
console.log("[red]Error: No AWS profiles configured[/]")
|
114
|
+
console.log("[yellow]Please run: aws configure sso or aws configure[/]")
|
115
|
+
raise SystemExit(1)
|
116
|
+
|
117
|
+
def validate_profile_access(profile_name: str, operation_description: str = "") -> bool:
|
113
118
|
"""
|
114
|
-
|
115
|
-
Uses the same universal logic as get_profile_for_operation but without console output.
|
119
|
+
Validate that the specified profile has proper AWS access with caching.
|
116
120
|
|
117
121
|
Args:
|
118
|
-
|
119
|
-
|
122
|
+
profile_name: AWS profile name to validate
|
123
|
+
operation_description: Optional description of the operation (for logging)
|
120
124
|
|
121
125
|
Returns:
|
122
|
-
|
123
|
-
|
124
|
-
Raises:
|
125
|
-
SystemExit: If user-specified profile not found in AWS config
|
126
|
+
True if profile has access, False otherwise
|
126
127
|
"""
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
if user_specified_profile in available_profiles:
|
132
|
-
return user_specified_profile
|
133
|
-
else:
|
134
|
-
# Don't fall back - user explicitly chose this profile
|
135
|
-
raise SystemExit(1)
|
128
|
+
# Check cache first to avoid redundant validations
|
129
|
+
global _cache_timestamp
|
130
|
+
current_time = time.time()
|
131
|
+
cache_key = f"validation:{profile_name}"
|
136
132
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
return
|
133
|
+
if (cache_key in _validation_cache and
|
134
|
+
_cache_timestamp and
|
135
|
+
current_time - _cache_timestamp < _cache_ttl):
|
136
|
+
return _validation_cache[cache_key]
|
141
137
|
|
142
|
-
|
143
|
-
|
138
|
+
try:
|
139
|
+
session = boto3.Session(profile_name=profile_name)
|
140
|
+
sts_client = session.client('sts')
|
141
|
+
sts_client.get_caller_identity()
|
144
142
|
|
143
|
+
# Cache successful validation
|
144
|
+
_validation_cache[cache_key] = True
|
145
|
+
return True
|
146
|
+
except Exception as e:
|
147
|
+
# Cache failed validation for shorter time to allow retry
|
148
|
+
_validation_cache[cache_key] = False
|
149
|
+
console.log(f"[yellow]Profile {profile_name} validation failed: {e}[/]")
|
150
|
+
return False
|
145
151
|
|
146
|
-
def
|
152
|
+
def get_account_id_from_profile(profile_name: str) -> Optional[str]:
|
147
153
|
"""
|
148
|
-
|
149
|
-
Works with ANY AWS profile configuration.
|
154
|
+
Extract account ID from AWS profile.
|
150
155
|
|
151
156
|
Args:
|
152
|
-
|
157
|
+
profile_name: AWS profile name
|
153
158
|
|
154
159
|
Returns:
|
155
|
-
|
160
|
+
Account ID if available, None otherwise
|
156
161
|
"""
|
157
|
-
|
158
|
-
|
159
|
-
|
162
|
+
try:
|
163
|
+
session = boto3.Session(profile_name=profile_name)
|
164
|
+
sts_client = session.client('sts')
|
165
|
+
response = sts_client.get_caller_identity()
|
166
|
+
return response.get('Account')
|
167
|
+
except Exception:
|
168
|
+
return None
|
160
169
|
|
161
|
-
def
|
170
|
+
def create_cost_session(profile_name: Optional[str] = None) -> boto3.Session:
|
162
171
|
"""
|
163
|
-
Create
|
164
|
-
Works with ANY AWS profile configuration.
|
172
|
+
Create AWS session optimized for cost operations (Cost Explorer).
|
165
173
|
|
166
174
|
Args:
|
167
|
-
|
175
|
+
profile_name: AWS profile name for cost operations
|
168
176
|
|
169
177
|
Returns:
|
170
|
-
boto3
|
178
|
+
Configured boto3 Session for cost operations
|
171
179
|
"""
|
172
|
-
|
173
|
-
return boto3.Session(profile_name=selected_profile)
|
180
|
+
cost_profile = get_profile_for_operation("billing", profile_name)
|
174
181
|
|
182
|
+
# Use cached session if available
|
183
|
+
session_key = f"cost:{cost_profile}"
|
184
|
+
if session_key in _session_cache:
|
185
|
+
return _session_cache[session_key]
|
175
186
|
|
176
|
-
|
187
|
+
session = boto3.Session(profile_name=cost_profile)
|
188
|
+
_session_cache[session_key] = session
|
189
|
+
return session
|
190
|
+
|
191
|
+
def create_management_session(profile_name: Optional[str] = None) -> boto3.Session:
|
177
192
|
"""
|
178
|
-
Create
|
179
|
-
Works with ANY AWS profile configuration.
|
193
|
+
Create AWS session optimized for management operations (Organizations).
|
180
194
|
|
181
195
|
Args:
|
182
|
-
|
196
|
+
profile_name: AWS profile name for management operations
|
183
197
|
|
184
198
|
Returns:
|
185
|
-
boto3
|
199
|
+
Configured boto3 Session for management operations
|
186
200
|
"""
|
187
|
-
|
188
|
-
|
201
|
+
mgmt_profile = get_profile_for_operation("management", profile_name)
|
202
|
+
|
203
|
+
# Use cached session if available
|
204
|
+
session_key = f"management:{mgmt_profile}"
|
205
|
+
if session_key in _session_cache:
|
206
|
+
return _session_cache[session_key]
|
189
207
|
|
208
|
+
session = boto3.Session(profile_name=mgmt_profile)
|
209
|
+
_session_cache[session_key] = session
|
210
|
+
return session
|
190
211
|
|
191
|
-
def
|
212
|
+
def create_operational_session(profile_name: Optional[str] = None) -> boto3.Session:
|
192
213
|
"""
|
193
|
-
|
194
|
-
|
214
|
+
Create AWS session optimized for operational tasks (EC2, S3, etc).
|
215
|
+
|
216
|
+
Args:
|
217
|
+
profile_name: AWS profile name for operational tasks
|
195
218
|
|
196
219
|
Returns:
|
197
|
-
|
220
|
+
Configured boto3 Session for operational tasks
|
198
221
|
"""
|
199
|
-
|
200
|
-
"aws_profile": os.getenv("AWS_PROFILE"),
|
201
|
-
"default_profile": "default",
|
202
|
-
"available_profiles": boto3.Session().available_profiles
|
203
|
-
}
|
222
|
+
ops_profile = get_profile_for_operation("operational", profile_name)
|
204
223
|
|
224
|
+
# Use cached session if available
|
225
|
+
session_key = f"operational:{ops_profile}"
|
226
|
+
if session_key in _session_cache:
|
227
|
+
return _session_cache[session_key]
|
205
228
|
|
206
|
-
|
229
|
+
session = boto3.Session(profile_name=ops_profile)
|
230
|
+
_session_cache[session_key] = session
|
231
|
+
return session
|
232
|
+
|
233
|
+
def get_current_profile_info(profile_name: Optional[str] = None) -> Dict[str, Any]:
|
207
234
|
"""
|
208
|
-
|
235
|
+
Get current profile information including account ID and region.
|
209
236
|
|
210
237
|
Args:
|
211
|
-
profile_name: AWS profile name to
|
212
|
-
operation_type: Type of operation for context
|
238
|
+
profile_name: AWS profile name to get info for
|
213
239
|
|
214
240
|
Returns:
|
215
|
-
|
241
|
+
Dictionary containing profile information
|
216
242
|
"""
|
217
243
|
try:
|
218
|
-
available_profiles = boto3.Session().available_profiles
|
219
|
-
if profile_name not in available_profiles:
|
220
|
-
console.log(f"[red]Profile '{profile_name}' not found in AWS config[/]")
|
221
|
-
return False
|
222
|
-
|
223
|
-
# Test session creation
|
224
244
|
session = boto3.Session(profile_name=profile_name)
|
225
|
-
sts_client = session.client(
|
245
|
+
sts_client = session.client('sts')
|
226
246
|
identity = sts_client.get_caller_identity()
|
227
247
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
return False
|
235
|
-
|
236
|
-
|
237
|
-
def get_available_profiles_for_validation() -> list:
|
238
|
-
"""
|
239
|
-
Get available AWS profiles for validation - truly universal approach.
|
240
|
-
|
241
|
-
Returns all configured AWS profiles for validation without ANY hardcoded assumptions.
|
242
|
-
Works with any AWS setup: single account, multi-account, any profile naming convention.
|
243
|
-
|
244
|
-
Returns:
|
245
|
-
list: Available AWS profile names for validation
|
246
|
-
"""
|
247
|
-
try:
|
248
|
-
# Get all available profiles from AWS CLI configuration
|
249
|
-
available_profiles = boto3.Session().available_profiles
|
250
|
-
|
251
|
-
# Start with AWS_PROFILE if set
|
252
|
-
validation_profiles = []
|
253
|
-
aws_profile = os.getenv("AWS_PROFILE")
|
254
|
-
if aws_profile and aws_profile in available_profiles:
|
255
|
-
validation_profiles.append(aws_profile)
|
256
|
-
|
257
|
-
# Add all other available profiles (universal approach)
|
258
|
-
for profile in available_profiles:
|
259
|
-
if profile not in validation_profiles:
|
260
|
-
validation_profiles.append(profile)
|
261
|
-
|
262
|
-
# Ensure we have at least one profile to test
|
263
|
-
if not validation_profiles:
|
264
|
-
validation_profiles = ['default']
|
265
|
-
|
266
|
-
return validation_profiles
|
267
|
-
|
248
|
+
return {
|
249
|
+
'profile_name': profile_name or 'default',
|
250
|
+
'account_id': identity.get('Account'),
|
251
|
+
'user_arn': identity.get('Arn'),
|
252
|
+
'region': session.region_name or 'us-east-1'
|
253
|
+
}
|
268
254
|
except Exception as e:
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
255
|
+
return {
|
256
|
+
'profile_name': profile_name or 'default',
|
257
|
+
'error': str(e),
|
258
|
+
'account_id': None,
|
259
|
+
'user_arn': None,
|
260
|
+
'region': None
|
261
|
+
}
|
262
|
+
|
263
|
+
def resolve_profile_for_operation_silent(
|
264
|
+
operation_type: str,
|
265
|
+
user_specified_profile: Optional[str] = None
|
266
|
+
) -> str:
|
274
267
|
"""
|
275
|
-
|
276
|
-
|
277
|
-
Enhances existing profile management with enterprise validation capabilities.
|
268
|
+
Silent version of profile resolution without logging.
|
278
269
|
|
279
270
|
Args:
|
280
|
-
operation_type: Type of operation
|
281
|
-
|
282
|
-
Usage:
|
283
|
-
@validate_profile_access_decorator(operation_type="finops")
|
284
|
-
def my_cost_analysis(profile=None, **kwargs):
|
285
|
-
# Your operation code here
|
286
|
-
"""
|
287
|
-
def decorator(f: Callable) -> Callable:
|
288
|
-
@wraps(f)
|
289
|
-
def wrapper(*args, **kwargs):
|
290
|
-
# Extract profile from kwargs
|
291
|
-
profile = kwargs.get('profile')
|
292
|
-
|
293
|
-
# Get the profile that would be used
|
294
|
-
selected_profile = get_profile_for_operation(operation_type, profile)
|
295
|
-
|
296
|
-
# Validate profile access
|
297
|
-
try:
|
298
|
-
session = boto3.Session(profile_name=selected_profile)
|
299
|
-
sts = session.client('sts')
|
300
|
-
identity = sts.get_caller_identity()
|
301
|
-
|
302
|
-
print_success(f"Profile validation successful: {selected_profile}")
|
303
|
-
print_info(f"Account: {identity.get('Account', 'Unknown')}")
|
304
|
-
|
305
|
-
# Store validated profile in kwargs for operation
|
306
|
-
kwargs['_validated_profile'] = selected_profile
|
307
|
-
kwargs['_account_id'] = identity.get('Account')
|
308
|
-
|
309
|
-
except ProfileNotFound:
|
310
|
-
print_error(f"AWS profile not found: {selected_profile}")
|
311
|
-
print_info("Available profiles:")
|
312
|
-
for p in boto3.Session().available_profiles:
|
313
|
-
print_info(f" • {p}")
|
314
|
-
raise SystemExit(1)
|
315
|
-
|
316
|
-
except NoCredentialsError:
|
317
|
-
print_error("No AWS credentials configured")
|
318
|
-
print_info("Configure credentials with: [bold green]aws configure[/]")
|
319
|
-
print_info("Or use SSO login: [bold green]aws sso login --profile your-profile[/]")
|
320
|
-
raise SystemExit(1)
|
321
|
-
|
322
|
-
except Exception as e:
|
323
|
-
print_error(f"Profile validation failed: {selected_profile}")
|
324
|
-
print_warning(f"Error: {str(e)}")
|
325
|
-
|
326
|
-
# Provide operation-specific recommendations
|
327
|
-
if operation_type == "finops" and "Access" in str(e):
|
328
|
-
print_info("Cost operations may require billing permissions")
|
329
|
-
print_info("Try: [bold green]--profile BILLING_PROFILE[/]")
|
330
|
-
elif operation_type == "inventory" and "Access" in str(e):
|
331
|
-
print_info("Inventory operations may require organizations permissions")
|
332
|
-
print_info("Try: [bold green]--profile MANAGEMENT_PROFILE[/]")
|
333
|
-
elif operation_type == "operate" and "Access" in str(e):
|
334
|
-
print_info("Resource operations may require operational permissions")
|
335
|
-
print_info("Try: [bold green]--profile CENTRALISED_OPS_PROFILE[/]")
|
336
|
-
|
337
|
-
raise SystemExit(1)
|
338
|
-
|
339
|
-
return f(*args, **kwargs)
|
340
|
-
return wrapper
|
341
|
-
return decorator
|
342
|
-
|
343
|
-
|
344
|
-
def quick_profile_check(profile: Optional[str] = None, quiet: bool = False) -> bool:
|
345
|
-
"""
|
346
|
-
Quick profile accessibility check without raising exceptions.
|
347
|
-
|
348
|
-
Args:
|
349
|
-
profile: Profile to check (None for auto-detection)
|
350
|
-
quiet: Suppress output messages
|
271
|
+
operation_type: Type of operation (billing, management, operational)
|
272
|
+
user_specified_profile: User-provided profile via --profile parameter
|
351
273
|
|
352
274
|
Returns:
|
353
|
-
|
275
|
+
Profile name to use for the operation
|
354
276
|
"""
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
sts = session.client('sts')
|
359
|
-
sts.get_caller_identity()
|
277
|
+
# Skip all logging and caching, just return the profile
|
278
|
+
if user_specified_profile and user_specified_profile != "default":
|
279
|
+
return user_specified_profile
|
360
280
|
|
361
|
-
|
362
|
-
|
363
|
-
|
281
|
+
# Check environment variables
|
282
|
+
profile_map = {
|
283
|
+
"billing": os.getenv("BILLING_PROFILE"),
|
284
|
+
"management": os.getenv("MANAGEMENT_PROFILE"),
|
285
|
+
"operational": os.getenv("CENTRALISED_OPS_PROFILE"),
|
286
|
+
}
|
364
287
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
return False
|
288
|
+
env_profile = profile_map.get(operation_type)
|
289
|
+
if env_profile:
|
290
|
+
return env_profile
|
369
291
|
|
292
|
+
return user_specified_profile or "default"
|
370
293
|
|
371
|
-
def
|
294
|
+
def list_available_profiles() -> List[str]:
|
372
295
|
"""
|
373
|
-
Get
|
374
|
-
|
375
|
-
Args:
|
376
|
-
operation_type: Type of operation (finops, inventory, operate, security)
|
296
|
+
Get list of all available AWS profiles.
|
377
297
|
|
378
298
|
Returns:
|
379
|
-
List of
|
299
|
+
List of available profile names
|
380
300
|
"""
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
return recommendations.get(operation_type, ["AWS_PROFILE", "MANAGEMENT_PROFILE"])
|
392
|
-
|
393
|
-
|
394
|
-
# Export all public functions including new validation enhancements
|
395
|
-
__all__ = [
|
396
|
-
"get_profile_for_operation",
|
397
|
-
"resolve_profile_for_operation_silent",
|
398
|
-
"create_cost_session",
|
399
|
-
"create_management_session",
|
400
|
-
"create_operational_session",
|
401
|
-
"get_current_profile_info",
|
402
|
-
"validate_profile_access",
|
403
|
-
"get_available_profiles_for_validation",
|
404
|
-
"validate_profile_access_decorator",
|
405
|
-
"quick_profile_check",
|
406
|
-
"get_profile_recommendations",
|
407
|
-
]
|
301
|
+
return boto3.Session().available_profiles
|
302
|
+
|
303
|
+
def clear_profile_cache() -> None:
|
304
|
+
"""Clear the profile cache for testing or troubleshooting."""
|
305
|
+
global _profile_cache, _validation_cache, _session_cache, _cache_timestamp, _session_id
|
306
|
+
_profile_cache.clear()
|
307
|
+
_validation_cache.clear()
|
308
|
+
_session_cache.clear()
|
309
|
+
_cache_timestamp = None
|
310
|
+
_session_id = None
|