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
@@ -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
|
+
]
|