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