runbooks 1.1.1__py3-none-any.whl → 1.1.3__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 (39) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/assessment/collectors.py +3 -2
  3. runbooks/cloudops/cost_optimizer.py +235 -83
  4. runbooks/cloudops/models.py +8 -2
  5. runbooks/common/aws_pricing.py +12 -0
  6. runbooks/common/business_logic.py +1 -1
  7. runbooks/common/profile_utils.py +213 -310
  8. runbooks/common/rich_utils.py +15 -21
  9. runbooks/finops/README.md +3 -3
  10. runbooks/finops/__init__.py +13 -5
  11. runbooks/finops/business_case_config.py +5 -5
  12. runbooks/finops/cli.py +170 -95
  13. runbooks/finops/cost_optimizer.py +2 -1
  14. runbooks/finops/cost_processor.py +69 -22
  15. runbooks/finops/dashboard_router.py +3 -3
  16. runbooks/finops/dashboard_runner.py +3 -4
  17. runbooks/finops/embedded_mcp_validator.py +101 -23
  18. runbooks/finops/enhanced_progress.py +213 -0
  19. runbooks/finops/finops_scenarios.py +90 -16
  20. runbooks/finops/markdown_exporter.py +4 -2
  21. runbooks/finops/multi_dashboard.py +1 -1
  22. runbooks/finops/nat_gateway_optimizer.py +85 -57
  23. runbooks/finops/rds_snapshot_optimizer.py +1389 -0
  24. runbooks/finops/scenario_cli_integration.py +212 -22
  25. runbooks/finops/scenarios.py +41 -25
  26. runbooks/finops/single_dashboard.py +68 -9
  27. runbooks/finops/tests/run_tests.py +5 -3
  28. runbooks/finops/vpc_cleanup_optimizer.py +1 -1
  29. runbooks/finops/workspaces_analyzer.py +40 -16
  30. runbooks/inventory/list_rds_snapshots_aggregator.py +745 -0
  31. runbooks/main.py +393 -61
  32. runbooks/operate/executive_dashboard.py +4 -3
  33. runbooks/remediation/rds_snapshot_list.py +13 -0
  34. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/METADATA +234 -40
  35. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/RECORD +39 -37
  36. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/WHEEL +0 -0
  37. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/entry_points.txt +0 -0
  38. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/licenses/LICENSE +0 -0
  39. {runbooks-1.1.1.dist-info → runbooks-1.1.3.dist-info}/top_level.txt +0 -0
@@ -1,92 +1,80 @@
1
- #!/usr/bin/env python3
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 botocore.exceptions import ProfileNotFound, NoCredentialsError
29
-
30
- from runbooks.common.rich_utils import console, print_error, print_success, print_info, print_warning
31
-
32
- # Profile cache to reduce duplicate calls (performance optimization)
33
- _profile_cache = {}
34
- _cache_timestamp = None
35
- _cache_ttl = 300 # 5 minutes cache TTL
36
-
37
-
38
- def get_profile_for_operation(operation_type: str, user_specified_profile: Optional[Union[str, Tuple[str, ...], List[str]]] = None) -> str:
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
- Universal AWS profile selection that works with ANY AWS setup.
30
+ Enhanced profile resolution with intelligent caching and enterprise logging optimization.
41
31
 
42
- SIMPLE PRIORITY ORDER (Universal Compatibility):
32
+ Priority Order:
43
33
  1. User-specified profile (--profile parameter) - HIGHEST PRIORITY
44
- 2. AWS_PROFILE environment variable - STANDARD AWS CONVENTION
45
- 3. "default" profile - AWS STANDARD FALLBACK
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 (informational only, not used for profile selection)
51
- user_specified_profile: Profile specified by user via --profile parameter (handles both str and tuple)
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
- str: Profile name to use for the operation
43
+ Profile name to use for the operation
55
44
 
56
- Raises:
57
- SystemExit: If user-specified profile not found in AWS config
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
- # SAFETY NET: Handle tuple profiles (Click multiple=True parameter issue)
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
- if (_cache_timestamp and
73
- current_time - _cache_timestamp < _cache_ttl and
74
- cache_key in _profile_cache):
75
- return _profile_cache[cache_key]
76
-
77
- # Clear cache if TTL expired
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
- console.log(f"[green]Using user-specified profile: {user_specified_profile}[/]")
88
- # Cache the result to reduce duplicate calls
89
- _profile_cache[cache_key] = user_specified_profile
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
- console.log(f"[dim cyan]Using AWS_PROFILE environment variable: {aws_profile}[/]")
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: Default profile (AWS standard fallback)
105
- default_profile = "default"
106
- console.log(f"[yellow]Using default AWS profile: {default_profile}[/]")
107
- # Cache the result to reduce duplicate calls
108
- _profile_cache[cache_key] = default_profile
109
- return default_profile
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
- def resolve_profile_for_operation_silent(operation_type: str, user_specified_profile: Optional[str] = None) -> str:
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
- Universal AWS profile resolution without logging (for display purposes).
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
- operation_type: Type of operation (informational only, not used for profile selection)
119
- user_specified_profile: Profile specified by user via --profile parameter
122
+ profile_name: AWS profile name to validate
123
+ operation_description: Optional description of the operation (for logging)
120
124
 
121
125
  Returns:
122
- str: Profile name to use for the operation
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
- available_profiles = boto3.Session().available_profiles
128
-
129
- # PRIORITY 1: User-specified profile ALWAYS takes precedence
130
- if user_specified_profile and user_specified_profile != "default":
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
- # PRIORITY 2: AWS_PROFILE environment variable (standard AWS convention)
138
- aws_profile = os.getenv("AWS_PROFILE")
139
- if aws_profile and aws_profile in available_profiles:
140
- return aws_profile
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
- # PRIORITY 3: Default profile (AWS standard fallback)
143
- return "default"
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 create_cost_session(profile: Optional[str] = None) -> boto3.Session:
152
+ def get_account_id_from_profile(profile_name: str) -> Optional[str]:
147
153
  """
148
- Create a boto3 session for cost operations with universal profile support.
149
- Works with ANY AWS profile configuration.
154
+ Extract account ID from AWS profile.
150
155
 
151
156
  Args:
152
- profile: User-specified profile (from --profile parameter)
157
+ profile_name: AWS profile name
153
158
 
154
159
  Returns:
155
- boto3.Session: Session configured for AWS operations
160
+ Account ID if available, None otherwise
156
161
  """
157
- selected_profile = get_profile_for_operation("cost", profile)
158
- return boto3.Session(profile_name=selected_profile)
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 create_management_session(profile: Optional[str] = None) -> boto3.Session:
170
+ def create_cost_session(profile_name: Optional[str] = None) -> boto3.Session:
162
171
  """
163
- Create a boto3 session for management operations with universal profile support.
164
- Works with ANY AWS profile configuration.
172
+ Create AWS session optimized for cost operations (Cost Explorer).
165
173
 
166
174
  Args:
167
- profile: User-specified profile (from --profile parameter)
175
+ profile_name: AWS profile name for cost operations
168
176
 
169
177
  Returns:
170
- boto3.Session: Session configured for AWS operations
178
+ Configured boto3 Session for cost operations
171
179
  """
172
- selected_profile = get_profile_for_operation("management", profile)
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
- def create_operational_session(profile: Optional[str] = None) -> boto3.Session:
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 a boto3 session for operational tasks with universal profile support.
179
- Works with ANY AWS profile configuration.
193
+ Create AWS session optimized for management operations (Organizations).
180
194
 
181
195
  Args:
182
- profile: User-specified profile (from --profile parameter)
196
+ profile_name: AWS profile name for management operations
183
197
 
184
198
  Returns:
185
- boto3.Session: Session configured for AWS operations
199
+ Configured boto3 Session for management operations
186
200
  """
187
- selected_profile = get_profile_for_operation("operational", profile)
188
- return boto3.Session(profile_name=selected_profile)
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 get_current_profile_info() -> Dict[str, Optional[str]]:
212
+ def create_operational_session(profile_name: Optional[str] = None) -> boto3.Session:
192
213
  """
193
- Get current AWS profile information using universal approach.
194
- Works with ANY AWS setup without hardcoded environment variable assumptions.
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
- Dict with current profile information
220
+ Configured boto3 Session for operational tasks
198
221
  """
199
- return {
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
- def validate_profile_access(profile_name: str, operation_type: str = "general") -> bool:
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
- Validate that profile exists and is accessible.
235
+ Get current profile information including account ID and region.
209
236
 
210
237
  Args:
211
- profile_name: AWS profile name to validate
212
- operation_type: Type of operation for context
238
+ profile_name: AWS profile name to get info for
213
239
 
214
240
  Returns:
215
- bool: True if profile is valid and accessible
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("sts")
245
+ sts_client = session.client('sts')
226
246
  identity = sts_client.get_caller_identity()
227
247
 
228
- console.log(f"[green]Profile '{profile_name}' validated for {operation_type} operations[/]")
229
- console.log(f"[dim]Account: {identity.get('Account')}, User: {identity.get('UserId', 'Unknown')}[/]")
230
- return True
231
-
232
- except Exception as e:
233
- console.log(f"[red]Profile '{profile_name}' validation failed: {str(e)}[/]")
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
- console.log(f"[yellow]Warning: Could not detect AWS profiles: {e}[/]")
270
- return ['default'] # Fallback to default profile
271
-
272
-
273
- def validate_profile_access_decorator(operation_type: str = "general"):
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
- Decorator to validate profile has required permissions before executing operation.
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 for context and recommendations
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
- True if profile is accessible, False otherwise
275
+ Profile name to use for the operation
354
276
  """
355
- try:
356
- selected_profile = get_profile_for_operation("general", profile)
357
- session = boto3.Session(profile_name=selected_profile)
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
- if not quiet:
362
- print_success(f"Profile {selected_profile} is accessible")
363
- return True
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
- except Exception as e:
366
- if not quiet:
367
- print_warning(f"Profile accessibility check failed: {str(e)}")
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 get_profile_recommendations(operation_type: str) -> List[str]:
294
+ def list_available_profiles() -> List[str]:
372
295
  """
373
- Get profile recommendations for specific operation types.
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 recommended profile names (environment variable names)
299
+ List of available profile names
380
300
  """
381
- recommendations = {
382
- "finops": ["BILLING_PROFILE", "MANAGEMENT_PROFILE"],
383
- "cost": ["BILLING_PROFILE", "MANAGEMENT_PROFILE"],
384
- "inventory": ["MANAGEMENT_PROFILE", "CENTRALISED_OPS_PROFILE"],
385
- "operate": ["CENTRALISED_OPS_PROFILE", "MANAGEMENT_PROFILE"],
386
- "security": ["MANAGEMENT_PROFILE", "SECURITY_PROFILE"],
387
- "cfat": ["MANAGEMENT_PROFILE"],
388
- "vpc": ["CENTRALISED_OPS_PROFILE", "MANAGEMENT_PROFILE"]
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