runbooks 0.9.9__py3-none-any.whl → 1.0.0__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 (71) hide show
  1. runbooks/cfat/cloud_foundations_assessment.py +626 -0
  2. runbooks/cloudops/cost_optimizer.py +95 -33
  3. runbooks/common/aws_pricing.py +388 -0
  4. runbooks/common/aws_pricing_api.py +205 -0
  5. runbooks/common/aws_utils.py +2 -2
  6. runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
  7. runbooks/common/cross_account_manager.py +606 -0
  8. runbooks/common/enhanced_exception_handler.py +4 -0
  9. runbooks/common/env_utils.py +96 -0
  10. runbooks/common/mcp_integration.py +49 -2
  11. runbooks/common/organizations_client.py +579 -0
  12. runbooks/common/profile_utils.py +96 -2
  13. runbooks/finops/cost_optimizer.py +2 -1
  14. runbooks/finops/elastic_ip_optimizer.py +13 -9
  15. runbooks/finops/embedded_mcp_validator.py +31 -0
  16. runbooks/finops/enhanced_trend_visualization.py +3 -2
  17. runbooks/finops/markdown_exporter.py +217 -2
  18. runbooks/finops/nat_gateway_optimizer.py +57 -20
  19. runbooks/finops/vpc_cleanup_exporter.py +28 -26
  20. runbooks/finops/vpc_cleanup_optimizer.py +370 -16
  21. runbooks/inventory/__init__.py +10 -1
  22. runbooks/inventory/cloud_foundations_integration.py +409 -0
  23. runbooks/inventory/core/collector.py +1148 -88
  24. runbooks/inventory/discovery.md +389 -0
  25. runbooks/inventory/drift_detection_cli.py +327 -0
  26. runbooks/inventory/inventory_mcp_cli.py +171 -0
  27. runbooks/inventory/inventory_modules.py +4 -7
  28. runbooks/inventory/mcp_inventory_validator.py +2149 -0
  29. runbooks/inventory/mcp_vpc_validator.py +23 -6
  30. runbooks/inventory/organizations_discovery.py +91 -1
  31. runbooks/inventory/rich_inventory_display.py +129 -1
  32. runbooks/inventory/unified_validation_engine.py +1292 -0
  33. runbooks/inventory/verify_ec2_security_groups.py +3 -1
  34. runbooks/inventory/vpc_analyzer.py +825 -7
  35. runbooks/inventory/vpc_flow_analyzer.py +36 -42
  36. runbooks/main.py +654 -35
  37. runbooks/monitoring/performance_monitor.py +11 -7
  38. runbooks/operate/dynamodb_operations.py +6 -5
  39. runbooks/operate/ec2_operations.py +3 -2
  40. runbooks/operate/networking_cost_heatmap.py +4 -3
  41. runbooks/operate/s3_operations.py +13 -12
  42. runbooks/operate/vpc_operations.py +49 -1
  43. runbooks/remediation/base.py +1 -1
  44. runbooks/remediation/commvault_ec2_analysis.py +6 -1
  45. runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
  46. runbooks/remediation/rds_snapshot_list.py +5 -3
  47. runbooks/validation/__init__.py +21 -1
  48. runbooks/validation/comprehensive_2way_validator.py +1996 -0
  49. runbooks/validation/mcp_validator.py +904 -94
  50. runbooks/validation/terraform_citations_validator.py +363 -0
  51. runbooks/validation/terraform_drift_detector.py +1098 -0
  52. runbooks/vpc/cleanup_wrapper.py +231 -10
  53. runbooks/vpc/config.py +310 -62
  54. runbooks/vpc/cross_account_session.py +308 -0
  55. runbooks/vpc/heatmap_engine.py +96 -29
  56. runbooks/vpc/manager_interface.py +9 -9
  57. runbooks/vpc/mcp_no_eni_validator.py +1551 -0
  58. runbooks/vpc/networking_wrapper.py +14 -8
  59. runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
  60. runbooks/vpc/runbooks.security.report_generator.log +0 -0
  61. runbooks/vpc/runbooks.security.run_script.log +0 -0
  62. runbooks/vpc/runbooks.security.security_export.log +0 -0
  63. runbooks/vpc/tests/test_cost_engine.py +1 -1
  64. runbooks/vpc/unified_scenarios.py +73 -3
  65. runbooks/vpc/vpc_cleanup_integration.py +512 -78
  66. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
  67. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/RECORD +71 -49
  68. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
  69. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
  70. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
  71. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,606 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enhanced Cross-Account Session Manager for CloudOps Runbooks Platform
4
+
5
+ This module consolidates cross-account session patterns from VPC and other modules
6
+ into a unified, high-performance manager optimized for 61-account enterprise operations.
7
+
8
+ Features:
9
+ - STS AssumeRole patterns with multiple role fallbacks
10
+ - Session caching and reuse for performance optimization
11
+ - Parallel session creation using ThreadPoolExecutor
12
+ - Integration with unified Organizations client
13
+ - Rich CLI progress indicators and error reporting
14
+ - Comprehensive session validation and metadata tracking
15
+
16
+ Author: CloudOps Runbooks Team
17
+ Version: 0.9.1
18
+ """
19
+
20
+ import threading
21
+ import time
22
+ from concurrent.futures import ThreadPoolExecutor, as_completed
23
+ from dataclasses import dataclass
24
+ from typing import Dict, List, Optional, Tuple
25
+
26
+ import boto3
27
+ from botocore.exceptions import ClientError, NoCredentialsError
28
+
29
+ from runbooks.common.organizations_client import OrganizationAccount, get_unified_organizations_client
30
+ from runbooks.common.profile_utils import create_management_session, get_profile_for_operation
31
+ from runbooks.common.rich_utils import (
32
+ console,
33
+ create_progress_bar,
34
+ print_error,
35
+ print_info,
36
+ print_success,
37
+ print_warning,
38
+ )
39
+
40
+ # Global session cache for performance optimization
41
+ _SESSION_CACHE = {}
42
+ _cache_lock = threading.Lock()
43
+
44
+
45
+ @dataclass
46
+ class CrossAccountSession:
47
+ """Enhanced cross-account session with comprehensive metadata and refresh capabilities"""
48
+ account_id: str
49
+ account_name: Optional[str]
50
+ session: Optional[boto3.Session]
51
+ status: str # 'success', 'failed', 'error', 'cached'
52
+ role_used: Optional[str] = None
53
+ assumed_role_arn: Optional[str] = None
54
+ session_expires: Optional[float] = None # Unix timestamp
55
+ error_message: Optional[str] = None
56
+ creation_timestamp: Optional[float] = None
57
+ last_refresh_timestamp: Optional[float] = None # Enhanced: Track refresh cycles
58
+ refresh_count: int = 0 # Enhanced: Count refresh operations
59
+ next_refresh_time: Optional[float] = None # Enhanced: Calculated refresh time
60
+
61
+ def __post_init__(self):
62
+ if self.creation_timestamp is None:
63
+ self.creation_timestamp = time.time()
64
+
65
+ def is_expired(self, session_ttl_minutes: int = 240) -> bool:
66
+ """Check if session is expired based on TTL (enhanced default: 4-hour)"""
67
+ if not self.session_expires:
68
+ # If no explicit expiry, use creation time + TTL
69
+ return (time.time() - self.creation_timestamp) > (session_ttl_minutes * 60)
70
+
71
+ return time.time() > self.session_expires
72
+
73
+ def needs_refresh(self, session_ttl_minutes: int = 240, auto_refresh_threshold: float = 0.9) -> bool:
74
+ """Enhanced: Check if session needs preemptive refresh"""
75
+ if not self.session_expires:
76
+ # Use creation time + TTL for calculation
77
+ ttl_seconds = session_ttl_minutes * 60
78
+ refresh_time = self.creation_timestamp + (ttl_seconds * auto_refresh_threshold)
79
+ return time.time() >= refresh_time
80
+
81
+ # Use explicit expiry time
82
+ refresh_time = self.session_expires - ((session_ttl_minutes * 60) * (1 - auto_refresh_threshold))
83
+ return time.time() >= refresh_time
84
+
85
+ def calculate_next_refresh(self, session_ttl_minutes: int = 240, auto_refresh_threshold: float = 0.9):
86
+ """Enhanced: Calculate next refresh time"""
87
+ if self.session_expires:
88
+ ttl_seconds = self.session_expires - time.time()
89
+ else:
90
+ ttl_seconds = session_ttl_minutes * 60
91
+
92
+ self.next_refresh_time = time.time() + (ttl_seconds * auto_refresh_threshold)
93
+
94
+ def to_dict(self) -> Dict:
95
+ """Convert to dictionary for serialization (excluding session object)"""
96
+ data = self.__dict__.copy()
97
+ data.pop('session', None) # Remove session object for serialization
98
+ return data
99
+
100
+
101
+ class EnhancedCrossAccountManager:
102
+ """
103
+ Enhanced cross-account session manager for enterprise 61-account operations.
104
+
105
+ This manager provides optimized cross-account access using:
106
+ - STS AssumeRole with multiple role pattern fallbacks
107
+ - Session caching and reuse for performance
108
+ - Parallel session creation for speed
109
+ - Integration with Organizations API for account discovery
110
+ - Rich progress indicators and comprehensive error handling
111
+ """
112
+
113
+ # Standard role patterns for cross-account access
114
+ STANDARD_ROLE_PATTERNS = [
115
+ "OrganizationAccountAccessRole", # AWS Organizations default
116
+ "AWSControlTowerExecution", # AWS Control Tower
117
+ "OrganizationAccountAccess", # Alternative naming
118
+ "CrossAccountAccessRole", # Custom pattern
119
+ "ReadOnlyAccess", # Fallback for read-only operations
120
+ ]
121
+
122
+ def __init__(
123
+ self,
124
+ base_profile: Optional[str] = None,
125
+ role_patterns: Optional[List[str]] = None,
126
+ max_workers: int = 10,
127
+ session_ttl_minutes: int = 240, # Enhanced: 4-hour TTL for enterprise operations
128
+ enable_session_cache: bool = True,
129
+ auto_refresh_threshold: float = 0.9, # Auto-refresh at 90% of TTL (216 minutes)
130
+ enable_preemptive_refresh: bool = True # Preemptive session refresh capability
131
+ ):
132
+ """
133
+ Initialize enhanced cross-account session manager.
134
+
135
+ Args:
136
+ base_profile: Base profile for assuming roles
137
+ role_patterns: Custom role patterns to try (defaults to STANDARD_ROLE_PATTERNS)
138
+ max_workers: Maximum parallel workers for session creation
139
+ session_ttl_minutes: Session TTL in minutes (enhanced default: 240 minutes / 4 hours)
140
+ enable_session_cache: Whether to enable session caching
141
+ auto_refresh_threshold: Fraction of TTL at which to trigger refresh (0.9 = 90%)
142
+ enable_preemptive_refresh: Enable background session refresh before expiration
143
+ """
144
+ self.base_profile = base_profile
145
+ self.role_patterns = role_patterns or self.STANDARD_ROLE_PATTERNS.copy()
146
+ self.max_workers = max_workers
147
+ self.session_ttl_minutes = session_ttl_minutes
148
+ self.enable_session_cache = enable_session_cache
149
+ self.auto_refresh_threshold = auto_refresh_threshold
150
+ self.enable_preemptive_refresh = enable_preemptive_refresh
151
+
152
+ # Initialize base session for role assumptions
153
+ if base_profile:
154
+ self.base_session = create_management_session(base_profile)
155
+ else:
156
+ # Use profile resolution for management operations
157
+ management_profile = get_profile_for_operation("management", None)
158
+ self.base_session = boto3.Session(profile_name=management_profile)
159
+
160
+ # Performance metrics
161
+ self.metrics = {
162
+ 'sessions_created': 0,
163
+ 'sessions_cached': 0,
164
+ 'sessions_failed': 0,
165
+ 'cache_hits': 0,
166
+ 'cache_misses': 0,
167
+ 'total_api_calls': 0,
168
+ }
169
+
170
+ print_info(f"🔐 Enhanced cross-account manager initialized")
171
+ print_info(f" Role patterns: {len(self.role_patterns)} configured")
172
+ print_info(f" Session caching: {'enabled' if enable_session_cache else 'disabled'}")
173
+ print_info(f" Session TTL: {session_ttl_minutes} minutes (4-hour enterprise standard)")
174
+ print_info(f" Auto-refresh: {'enabled' if enable_preemptive_refresh else 'disabled'} at {auto_refresh_threshold:.0%} TTL")
175
+
176
+ def _get_cached_session(self, account_id: str) -> Optional[CrossAccountSession]:
177
+ """Get cached session if valid and not expired"""
178
+ if not self.enable_session_cache:
179
+ return None
180
+
181
+ with _cache_lock:
182
+ cached_session = _SESSION_CACHE.get(account_id)
183
+ if cached_session and not cached_session.is_expired(self.session_ttl_minutes):
184
+ self.metrics['cache_hits'] += 1
185
+ return cached_session
186
+ elif cached_session:
187
+ # Remove expired session from cache
188
+ del _SESSION_CACHE[account_id]
189
+
190
+ self.metrics['cache_misses'] += 1
191
+ return None
192
+
193
+ def _cache_session(self, session: CrossAccountSession):
194
+ """Cache session for reuse"""
195
+ if not self.enable_session_cache or session.status != 'success':
196
+ return
197
+
198
+ with _cache_lock:
199
+ _SESSION_CACHE[session.account_id] = session
200
+
201
+ print_info(f"💾 Cached session for account {session.account_id}")
202
+
203
+ async def create_cross_account_sessions_from_accounts(
204
+ self,
205
+ accounts: List[OrganizationAccount]
206
+ ) -> List[CrossAccountSession]:
207
+ """
208
+ Create cross-account sessions from OrganizationAccount objects.
209
+
210
+ Args:
211
+ accounts: List of OrganizationAccount objects
212
+
213
+ Returns:
214
+ List of CrossAccountSession objects
215
+ """
216
+ # Filter active accounts
217
+ active_accounts = [acc for acc in accounts if acc.status == 'ACTIVE']
218
+
219
+ print_info(f"🌐 Creating cross-account sessions for {len(active_accounts)} active accounts")
220
+
221
+ return await self._create_sessions_parallel(active_accounts)
222
+
223
+ async def create_cross_account_sessions_from_organization(
224
+ self,
225
+ management_profile: Optional[str] = None
226
+ ) -> List[CrossAccountSession]:
227
+ """
228
+ Create cross-account sessions by discovering accounts from Organizations API.
229
+
230
+ Args:
231
+ management_profile: Profile for Organizations API access
232
+
233
+ Returns:
234
+ List of CrossAccountSession objects
235
+ """
236
+ print_info("🏢 Discovering accounts from Organizations API...")
237
+
238
+ # Use unified Organizations client to discover accounts
239
+ orgs_client = get_unified_organizations_client(management_profile or self.base_profile)
240
+ accounts = await orgs_client.get_organization_accounts()
241
+
242
+ if not accounts:
243
+ print_warning("No accounts discovered from Organizations API")
244
+ return []
245
+
246
+ return await self.create_cross_account_sessions_from_accounts(accounts)
247
+
248
+ async def _create_sessions_parallel(
249
+ self,
250
+ accounts: List[OrganizationAccount]
251
+ ) -> List[CrossAccountSession]:
252
+ """Create sessions in parallel for performance"""
253
+
254
+ sessions = []
255
+
256
+ with create_progress_bar() as progress:
257
+ task = progress.add_task("Creating cross-account sessions...", total=len(accounts))
258
+
259
+ # Use ThreadPoolExecutor for parallel session creation
260
+ with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
261
+ future_to_account = {
262
+ executor.submit(self._create_single_session, account): account
263
+ for account in accounts
264
+ }
265
+
266
+ for future in as_completed(future_to_account):
267
+ account = future_to_account[future]
268
+ try:
269
+ session = future.result()
270
+ sessions.append(session)
271
+
272
+ # Update progress with status
273
+ if session.status == 'success':
274
+ self.metrics['sessions_created'] += 1
275
+ elif session.status == 'cached':
276
+ self.metrics['sessions_cached'] += 1
277
+ else:
278
+ self.metrics['sessions_failed'] += 1
279
+
280
+ progress.advance(task)
281
+
282
+ except Exception as e:
283
+ print_error(f"❌ Unexpected error creating session for {account.account_id}: {e}")
284
+ sessions.append(CrossAccountSession(
285
+ account_id=account.account_id,
286
+ account_name=account.name,
287
+ session=None,
288
+ status='error',
289
+ error_message=str(e)
290
+ ))
291
+ progress.advance(task)
292
+
293
+ # Summary
294
+ successful = len([s for s in sessions if s.status in ['success', 'cached']])
295
+ failed = len([s for s in sessions if s.status in ['failed', 'error']])
296
+
297
+ print_success(f"✅ Session creation complete: {successful} successful, {failed} failed")
298
+
299
+ return sessions
300
+
301
+ def _create_single_session(self, account: OrganizationAccount) -> CrossAccountSession:
302
+ """
303
+ Create a single cross-account session with caching and role pattern fallback.
304
+
305
+ This is the core implementation handling caching, role patterns, and error handling.
306
+ """
307
+ # Check cache first
308
+ cached_session = self._get_cached_session(account.account_id)
309
+ if cached_session:
310
+ print_info(f"💾 Using cached session for {account.account_id}")
311
+ cached_session.status = 'cached' # Mark as cached for metrics
312
+ return cached_session
313
+
314
+ # Try each role pattern
315
+ for role_name in self.role_patterns:
316
+ try:
317
+ session = self._assume_role_and_create_session(
318
+ account.account_id,
319
+ account.name,
320
+ role_name
321
+ )
322
+
323
+ if session.status == 'success':
324
+ # Cache successful session
325
+ self._cache_session(session)
326
+ return session
327
+
328
+ except ClientError as e:
329
+ error_code = e.response.get('Error', {}).get('Code', '')
330
+
331
+ # Continue to next role pattern for certain errors
332
+ if error_code in ['AccessDenied', 'NoSuchEntity']:
333
+ continue
334
+ else:
335
+ # For other errors, return failure
336
+ return CrossAccountSession(
337
+ account_id=account.account_id,
338
+ account_name=account.name,
339
+ session=None,
340
+ status='failed',
341
+ error_message=f"AWS API error: {error_code}"
342
+ )
343
+
344
+ except Exception as e:
345
+ # For unexpected errors, continue to next role pattern
346
+ continue
347
+
348
+ # If no role patterns worked
349
+ return CrossAccountSession(
350
+ account_id=account.account_id,
351
+ account_name=account.name,
352
+ session=None,
353
+ status='failed',
354
+ role_used=None,
355
+ error_message=f"Unable to assume any role pattern: {', '.join(self.role_patterns)}"
356
+ )
357
+
358
+ def _assume_role_and_create_session(
359
+ self,
360
+ account_id: str,
361
+ account_name: Optional[str],
362
+ role_name: str
363
+ ) -> CrossAccountSession:
364
+ """Assume role and create session with validation"""
365
+
366
+ role_arn = f"arn:aws:iam::{account_id}:role/{role_name}"
367
+ session_name = f"CloudOpsRunbooks-{account_id[:12]}-{int(time.time())}"
368
+
369
+ try:
370
+ # Step 1: Assume role using base session
371
+ sts_client = self.base_session.client('sts')
372
+ assume_role_response = sts_client.assume_role(
373
+ RoleArn=role_arn,
374
+ RoleSessionName=session_name,
375
+ DurationSeconds=3600 # 1 hour (default)
376
+ )
377
+
378
+ credentials = assume_role_response['Credentials']
379
+ expiration = credentials['Expiration'].timestamp()
380
+
381
+ self.metrics['total_api_calls'] += 1
382
+
383
+ # Step 2: Create session with assumed role credentials
384
+ assumed_session = boto3.Session(
385
+ aws_access_key_id=credentials['AccessKeyId'],
386
+ aws_secret_access_key=credentials['SecretAccessKey'],
387
+ aws_session_token=credentials['SessionToken']
388
+ )
389
+
390
+ # Step 3: Validate session with STS call
391
+ assumed_sts = assumed_session.client('sts')
392
+ identity = assumed_sts.get_caller_identity()
393
+ self.metrics['total_api_calls'] += 1
394
+
395
+ # Verify we're in the correct account
396
+ if identity['Account'] != account_id:
397
+ return CrossAccountSession(
398
+ account_id=account_id,
399
+ account_name=account_name,
400
+ session=None,
401
+ status='failed',
402
+ error_message=f"Role assumption returned wrong account: {identity['Account']}"
403
+ )
404
+
405
+ return CrossAccountSession(
406
+ account_id=account_id,
407
+ account_name=account_name,
408
+ session=assumed_session,
409
+ status='success',
410
+ role_used=role_name,
411
+ assumed_role_arn=role_arn,
412
+ session_expires=expiration
413
+ )
414
+
415
+ except ClientError as e:
416
+ error_code = e.response.get('Error', {}).get('Code', '')
417
+ return CrossAccountSession(
418
+ account_id=account_id,
419
+ account_name=account_name,
420
+ session=None,
421
+ status='failed',
422
+ error_message=f"Failed to assume {role_name}: {error_code}"
423
+ )
424
+
425
+ def get_successful_sessions(self, sessions: List[CrossAccountSession]) -> List[CrossAccountSession]:
426
+ """Get only successful sessions for operations"""
427
+ successful = [s for s in sessions if s.status in ['success', 'cached']]
428
+ print_info(f"🎯 {len(successful)}/{len(sessions)} sessions ready for cross-account operations")
429
+ return successful
430
+
431
+ def get_session_by_account_id(
432
+ self,
433
+ sessions: List[CrossAccountSession],
434
+ account_id: str
435
+ ) -> Optional[CrossAccountSession]:
436
+ """Get session for specific account ID"""
437
+ for session in sessions:
438
+ if session.account_id == account_id and session.status in ['success', 'cached']:
439
+ return session
440
+ return None
441
+
442
+ def refresh_expired_sessions(self, sessions: List[CrossAccountSession]) -> List[CrossAccountSession]:
443
+ """Enhanced: Refresh expired sessions with preemptive refresh support"""
444
+ refreshed_sessions = []
445
+
446
+ for session in sessions:
447
+ should_refresh = False
448
+ refresh_reason = ""
449
+
450
+ if session.status in ['success', 'cached']:
451
+ if session.is_expired(self.session_ttl_minutes):
452
+ should_refresh = True
453
+ refresh_reason = "expired"
454
+ elif (self.enable_preemptive_refresh and
455
+ session.needs_refresh(self.session_ttl_minutes, self.auto_refresh_threshold)):
456
+ should_refresh = True
457
+ refresh_reason = "preemptive"
458
+
459
+ if should_refresh:
460
+ print_info(f"🔄 Refreshing {refresh_reason} session for {session.account_id}")
461
+
462
+ # Create new session
463
+ account = OrganizationAccount(
464
+ account_id=session.account_id,
465
+ name=session.account_name or session.account_id,
466
+ email="refresh@system",
467
+ status="ACTIVE",
468
+ joined_method="REFRESH"
469
+ )
470
+
471
+ new_session = self._create_single_session(account)
472
+
473
+ # Enhanced: Copy refresh metadata
474
+ if new_session.status == 'success':
475
+ new_session.refresh_count = session.refresh_count + 1
476
+ new_session.last_refresh_timestamp = time.time()
477
+ new_session.calculate_next_refresh(self.session_ttl_minutes, self.auto_refresh_threshold)
478
+ print_info(f"✅ Session refreshed successfully (refresh #{new_session.refresh_count})")
479
+
480
+ refreshed_sessions.append(new_session)
481
+ else:
482
+ refreshed_sessions.append(session)
483
+
484
+ return refreshed_sessions
485
+
486
+ def get_session_summary(self, sessions: List[CrossAccountSession]) -> Dict:
487
+ """Enhanced: Get comprehensive session summary with refresh metrics"""
488
+ refresh_stats = {
489
+ 'sessions_needing_refresh': len([s for s in sessions if s.status in ['success', 'cached']
490
+ and s.needs_refresh(self.session_ttl_minutes, self.auto_refresh_threshold)]),
491
+ 'refreshed_sessions': len([s for s in sessions if s.refresh_count > 0]),
492
+ 'total_refresh_operations': sum(s.refresh_count for s in sessions),
493
+ 'sessions_with_next_refresh_time': len([s for s in sessions if s.next_refresh_time is not None])
494
+ }
495
+
496
+ return {
497
+ 'total_sessions': len(sessions),
498
+ 'successful_sessions': len([s for s in sessions if s.status == 'success']),
499
+ 'cached_sessions': len([s for s in sessions if s.status == 'cached']),
500
+ 'failed_sessions': len([s for s in sessions if s.status == 'failed']),
501
+ 'error_sessions': len([s for s in sessions if s.status == 'error']),
502
+ 'metrics': self.metrics.copy(),
503
+ 'refresh_metrics': refresh_stats, # Enhanced: Refresh statistics
504
+ 'role_patterns_configured': len(self.role_patterns),
505
+ 'session_ttl_minutes': self.session_ttl_minutes,
506
+ 'cache_enabled': self.enable_session_cache,
507
+ 'preemptive_refresh_enabled': self.enable_preemptive_refresh, # Enhanced
508
+ 'auto_refresh_threshold': self.auto_refresh_threshold, # Enhanced
509
+ }
510
+
511
+ def clear_session_cache(self):
512
+ """Clear the global session cache"""
513
+ with _cache_lock:
514
+ cache_size = len(_SESSION_CACHE)
515
+ _SESSION_CACHE.clear()
516
+
517
+ print_info(f"🗑️ Cleared {cache_size} cached sessions")
518
+
519
+ def get_cache_statistics(self) -> Dict:
520
+ """Get cache statistics"""
521
+ with _cache_lock:
522
+ cache_size = len(_SESSION_CACHE)
523
+ expired_count = sum(1 for s in _SESSION_CACHE.values()
524
+ if s.is_expired(self.session_ttl_minutes))
525
+
526
+ return {
527
+ 'cache_size': cache_size,
528
+ 'expired_sessions': expired_count,
529
+ 'cache_hits': self.metrics['cache_hits'],
530
+ 'cache_misses': self.metrics['cache_misses'],
531
+ 'hit_rate': (
532
+ self.metrics['cache_hits'] /
533
+ (self.metrics['cache_hits'] + self.metrics['cache_misses'])
534
+ if (self.metrics['cache_hits'] + self.metrics['cache_misses']) > 0 else 0
535
+ )
536
+ }
537
+
538
+
539
+ # Convenience functions for easy integration
540
+
541
+ async def create_cross_account_sessions(
542
+ base_profile: Optional[str] = None,
543
+ management_profile: Optional[str] = None,
544
+ role_patterns: Optional[List[str]] = None,
545
+ max_workers: int = 10
546
+ ) -> List[CrossAccountSession]:
547
+ """
548
+ Convenience function to create cross-account sessions from Organizations API.
549
+
550
+ Args:
551
+ base_profile: Base profile for assuming roles
552
+ management_profile: Profile for Organizations API access
553
+ role_patterns: Custom role patterns to try
554
+ max_workers: Maximum parallel workers
555
+
556
+ Returns:
557
+ List of CrossAccountSession objects
558
+ """
559
+ manager = EnhancedCrossAccountManager(
560
+ base_profile=base_profile,
561
+ role_patterns=role_patterns,
562
+ max_workers=max_workers
563
+ )
564
+
565
+ return await manager.create_cross_account_sessions_from_organization(management_profile)
566
+
567
+
568
+ def convert_sessions_to_profiles_compatibility(
569
+ sessions: List[CrossAccountSession]
570
+ ) -> Tuple[List[str], Dict[str, str]]:
571
+ """
572
+ Convert sessions to profile format for compatibility with existing VPC module.
573
+
574
+ This function provides backward compatibility for modules expecting profile names.
575
+ Note: This is a bridge function - modules should migrate to use sessions directly.
576
+
577
+ Returns:
578
+ Tuple of (profile_list, account_metadata) for compatibility
579
+ """
580
+ successful_sessions = [s for s in sessions if s.status in ['success', 'cached']]
581
+
582
+ # Create temporary profile identifiers (session-based)
583
+ profile_list = [f"session:{s.account_id}" for s in successful_sessions]
584
+
585
+ # Create account metadata
586
+ account_metadata = {
587
+ s.account_id: {
588
+ 'id': s.account_id,
589
+ 'name': s.account_name or s.account_id,
590
+ 'profile_identifier': f"session:{s.account_id}",
591
+ 'role_used': s.role_used,
592
+ 'session_available': True
593
+ }
594
+ for s in successful_sessions
595
+ }
596
+
597
+ return profile_list, account_metadata
598
+
599
+
600
+ # Export public interface
601
+ __all__ = [
602
+ 'EnhancedCrossAccountManager',
603
+ 'CrossAccountSession',
604
+ 'create_cross_account_sessions',
605
+ 'convert_sessions_to_profiles_compatibility',
606
+ ]
@@ -323,6 +323,10 @@ class EnterpriseExceptionHandler:
323
323
  Returns:
324
324
  Enhanced error if performance target significantly exceeded, None otherwise
325
325
  """
326
+ # Defensive check for None values
327
+ if execution_time is None or performance_target is None or performance_target == 0:
328
+ return None
329
+
326
330
  performance_ratio = execution_time / performance_target
327
331
 
328
332
  # Only create error if performance significantly exceeded (>150% of target)