runbooks 0.9.8__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.
- runbooks/__init__.py +1 -1
- runbooks/cfat/cloud_foundations_assessment.py +626 -0
- runbooks/cloudops/cost_optimizer.py +95 -33
- runbooks/common/aws_pricing.py +388 -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/enhanced_exception_handler.py +4 -0
- runbooks/common/env_utils.py +96 -0
- runbooks/common/mcp_integration.py +49 -2
- runbooks/common/organizations_client.py +579 -0
- runbooks/common/profile_utils.py +96 -2
- runbooks/common/rich_utils.py +3 -0
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/elastic_ip_optimizer.py +13 -9
- runbooks/finops/embedded_mcp_validator.py +31 -0
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/markdown_exporter.py +441 -0
- runbooks/finops/nat_gateway_optimizer.py +57 -20
- runbooks/finops/optimizer.py +2 -0
- runbooks/finops/single_dashboard.py +2 -2
- runbooks/finops/vpc_cleanup_exporter.py +330 -0
- runbooks/finops/vpc_cleanup_optimizer.py +895 -40
- runbooks/inventory/__init__.py +10 -1
- runbooks/inventory/cloud_foundations_integration.py +409 -0
- runbooks/inventory/core/collector.py +1148 -88
- runbooks/inventory/discovery.md +389 -0
- runbooks/inventory/drift_detection_cli.py +327 -0
- runbooks/inventory/inventory_mcp_cli.py +171 -0
- runbooks/inventory/inventory_modules.py +4 -7
- runbooks/inventory/mcp_inventory_validator.py +2149 -0
- runbooks/inventory/mcp_vpc_validator.py +23 -6
- runbooks/inventory/organizations_discovery.py +91 -1
- runbooks/inventory/rich_inventory_display.py +129 -1
- runbooks/inventory/unified_validation_engine.py +1292 -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 +969 -42
- runbooks/monitoring/performance_monitor.py +11 -7
- runbooks/operate/dynamodb_operations.py +6 -5
- runbooks/operate/ec2_operations.py +3 -2
- runbooks/operate/networking_cost_heatmap.py +4 -3
- runbooks/operate/s3_operations.py +13 -12
- runbooks/operate/vpc_operations.py +50 -2
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commvault_ec2_analysis.py +6 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
- runbooks/remediation/rds_snapshot_list.py +5 -3
- runbooks/validation/__init__.py +21 -1
- runbooks/validation/comprehensive_2way_validator.py +1996 -0
- runbooks/validation/mcp_validator.py +904 -94
- 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 +310 -62
- runbooks/vpc/cross_account_session.py +308 -0
- runbooks/vpc/heatmap_engine.py +96 -29
- runbooks/vpc/manager_interface.py +9 -9
- runbooks/vpc/mcp_no_eni_validator.py +1551 -0
- runbooks/vpc/networking_wrapper.py +14 -8
- runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/vpc/runbooks.security.report_generator.log +0 -0
- runbooks/vpc/runbooks.security.run_script.log +0 -0
- runbooks/vpc/runbooks.security.security_export.log +0 -0
- runbooks/vpc/tests/test_cost_engine.py +1 -1
- runbooks/vpc/unified_scenarios.py +3269 -0
- runbooks/vpc/vpc_cleanup_integration.py +516 -82
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/RECORD +75 -51
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.8.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
@@ -29,6 +29,8 @@ from dataclasses import dataclass
|
|
29
29
|
from datetime import datetime, timedelta
|
30
30
|
from pathlib import Path
|
31
31
|
from typing import Any, Dict, List, Optional, Tuple
|
32
|
+
import time
|
33
|
+
import asyncio
|
32
34
|
|
33
35
|
import boto3
|
34
36
|
from botocore.exceptions import ClientError
|
@@ -39,12 +41,15 @@ from rich.table import Table
|
|
39
41
|
from rich.tree import Tree
|
40
42
|
|
41
43
|
from runbooks.common.profile_utils import create_operational_session
|
44
|
+
from runbooks.common.cross_account_manager import EnhancedCrossAccountManager, CrossAccountSession
|
45
|
+
from runbooks.common.organizations_client import OrganizationAccount, get_unified_organizations_client
|
42
46
|
from runbooks.common.rich_utils import (
|
43
47
|
console,
|
44
48
|
print_header,
|
45
49
|
print_success,
|
46
50
|
print_error,
|
47
51
|
print_warning,
|
52
|
+
print_info,
|
48
53
|
create_table,
|
49
54
|
create_progress_bar,
|
50
55
|
format_cost
|
@@ -68,6 +73,8 @@ class VPCDiscoveryResult:
|
|
68
73
|
security_groups: List[Dict[str, Any]]
|
69
74
|
total_resources: int
|
70
75
|
discovery_timestamp: str
|
76
|
+
account_summary: Optional[Dict[str, Any]] = None # NEW: Multi-account metadata
|
77
|
+
landing_zone_metrics: Optional[Dict[str, Any]] = None # NEW: Landing zone analytics
|
71
78
|
|
72
79
|
|
73
80
|
@dataclass
|
@@ -98,21 +105,32 @@ class VPCAnalyzer:
|
|
98
105
|
profile: Optional[str] = None,
|
99
106
|
region: Optional[str] = "us-east-1",
|
100
107
|
console: Optional[Console] = None,
|
101
|
-
dry_run: bool = True
|
108
|
+
dry_run: bool = True,
|
109
|
+
excluded_accounts: Optional[List[str]] = None, # Enhanced: Decommissioned accounts filtering
|
110
|
+
enable_multi_account: bool = False, # Enhanced: Multi-Organization Landing Zone mode
|
111
|
+
max_workers: int = 10 # Enhanced: Parallel processing for 60-account operations
|
102
112
|
):
|
103
113
|
"""
|
104
|
-
Initialize VPC Analyzer with enterprise profile management
|
114
|
+
Initialize VPC Analyzer with enterprise profile management and 60-account Landing Zone support
|
105
115
|
|
106
116
|
Args:
|
107
117
|
profile: AWS profile name (3-tier priority: User > Environment > Default)
|
108
|
-
region: AWS region for analysis
|
109
|
-
console: Rich console instance
|
110
|
-
dry_run: Safety-first READ-ONLY analysis mode
|
118
|
+
region: AWS region for analysis (defaults to us-east-1)
|
119
|
+
console: Rich console instance for enterprise UX
|
120
|
+
dry_run: Safety-first READ-ONLY analysis mode (default: True)
|
121
|
+
excluded_accounts: List of decommissioned account IDs to exclude (default: ['294618320542'])
|
122
|
+
enable_multi_account: Enable Multi-Organization Landing Zone discovery mode
|
123
|
+
max_workers: Maximum parallel workers for 60-account operations (default: 10)
|
111
124
|
"""
|
112
125
|
self.profile = profile
|
113
126
|
self.region = region
|
114
127
|
self.console = console or Console()
|
115
128
|
self.dry_run = dry_run
|
129
|
+
self.max_workers = max_workers
|
130
|
+
|
131
|
+
# Decommissioned account filtering (default: account 294618320542)
|
132
|
+
self.excluded_accounts = excluded_accounts or ["294618320542"]
|
133
|
+
self.enable_multi_account = enable_multi_account
|
116
134
|
|
117
135
|
# Initialize AWS session using enterprise profile management
|
118
136
|
self.session = None
|
@@ -123,9 +141,378 @@ class VPCAnalyzer:
|
|
123
141
|
except Exception as e:
|
124
142
|
print_error(f"Failed to connect to AWS: {e}")
|
125
143
|
|
144
|
+
# NEW: Initialize Enhanced Cross-Account Manager for 60-account operations
|
145
|
+
self.cross_account_manager = None
|
146
|
+
if enable_multi_account:
|
147
|
+
self.cross_account_manager = EnhancedCrossAccountManager(
|
148
|
+
base_profile=profile,
|
149
|
+
max_workers=max_workers,
|
150
|
+
session_ttl_minutes=240 # 4-hour TTL for enterprise operations
|
151
|
+
)
|
152
|
+
print_info(f"🌐 Multi-Organization Landing Zone mode enabled for {max_workers} parallel accounts")
|
153
|
+
print_info(f"🚫 Excluded decommissioned accounts: {self.excluded_accounts}")
|
154
|
+
|
126
155
|
# Results storage
|
127
156
|
self.last_discovery = None
|
128
157
|
self.last_awso_analysis = None
|
158
|
+
self.landing_zone_sessions = [] # Enhanced: Store cross-account sessions
|
159
|
+
|
160
|
+
print_header(f"VPC Analyzer v0.9.9", "Multi-Organization Landing Zone Enhanced")
|
161
|
+
|
162
|
+
if self.enable_multi_account:
|
163
|
+
print_info(f"🎯 Target: 60-account Multi-Organization Landing Zone discovery")
|
164
|
+
print_info(f"⚡ Performance: <60s complete analysis with {max_workers} parallel workers")
|
165
|
+
print_info(f"🔒 Session TTL: 4-hour enterprise standard with auto-refresh")
|
166
|
+
|
167
|
+
def _filter_landing_zone_accounts(
|
168
|
+
self,
|
169
|
+
accounts: List[OrganizationAccount],
|
170
|
+
excluded_accounts: Optional[List[str]] = None
|
171
|
+
) -> List[OrganizationAccount]:
|
172
|
+
"""
|
173
|
+
Enhanced: Filter out decommissioned accounts from Landing Zone discovery
|
174
|
+
|
175
|
+
Args:
|
176
|
+
accounts: List of organization accounts
|
177
|
+
excluded_accounts: Additional accounts to exclude (merged with instance defaults)
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
Filtered list of active accounts for discovery
|
181
|
+
"""
|
182
|
+
exclusion_list = (excluded_accounts or []) + (self.excluded_accounts or [])
|
183
|
+
|
184
|
+
# Remove duplicates while preserving order
|
185
|
+
exclusion_list = list(dict.fromkeys(exclusion_list))
|
186
|
+
|
187
|
+
if not exclusion_list:
|
188
|
+
return accounts
|
189
|
+
|
190
|
+
filtered_accounts = []
|
191
|
+
excluded_count = 0
|
192
|
+
|
193
|
+
for account in accounts:
|
194
|
+
if account.account_id in exclusion_list:
|
195
|
+
excluded_count += 1
|
196
|
+
print_info(f"🚫 Excluded decommissioned account: {account.account_id} ({account.name or 'Unknown'})")
|
197
|
+
else:
|
198
|
+
filtered_accounts.append(account)
|
199
|
+
|
200
|
+
if excluded_count > 0:
|
201
|
+
print_warning(f"⚠️ Excluded {excluded_count} decommissioned accounts from discovery")
|
202
|
+
print_info(f"✅ Active accounts for discovery: {len(filtered_accounts)}")
|
203
|
+
|
204
|
+
return filtered_accounts
|
205
|
+
|
206
|
+
async def discover_multi_org_vpc_topology(
|
207
|
+
self,
|
208
|
+
target_accounts: int = 60,
|
209
|
+
landing_zone_structure: Optional[Dict] = None
|
210
|
+
) -> VPCDiscoveryResult:
|
211
|
+
"""
|
212
|
+
Enhanced: Discover VPC topology across Multi-Organization Landing Zone
|
213
|
+
|
214
|
+
This is the primary method for 60-account enterprise discovery operations.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
target_accounts: Expected number of accounts (default: 60)
|
218
|
+
landing_zone_structure: Optional Landing Zone structure metadata
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
VPCDiscoveryResult with comprehensive multi-account topology
|
222
|
+
"""
|
223
|
+
if not self.enable_multi_account or not self.cross_account_manager:
|
224
|
+
raise ValueError("Multi-account mode not enabled. Initialize with enable_multi_account=True")
|
225
|
+
|
226
|
+
print_header("Multi-Organization Landing Zone VPC Discovery", f"Target: {target_accounts} accounts")
|
227
|
+
start_time = time.time()
|
228
|
+
|
229
|
+
# Step 1: Discover and filter Landing Zone accounts
|
230
|
+
print_info("🏢 Step 1: Discovering Landing Zone accounts...")
|
231
|
+
try:
|
232
|
+
sessions = await self.cross_account_manager.create_cross_account_sessions_from_organization()
|
233
|
+
|
234
|
+
# Extract accounts from sessions and filter decommissioned
|
235
|
+
all_accounts = [
|
236
|
+
OrganizationAccount(
|
237
|
+
account_id=session.account_id,
|
238
|
+
name=session.account_name or session.account_id,
|
239
|
+
email="discovered@system",
|
240
|
+
status="ACTIVE" if session.status in ['success', 'cached'] else "INACTIVE",
|
241
|
+
joined_method="DISCOVERED"
|
242
|
+
)
|
243
|
+
for session in sessions
|
244
|
+
]
|
245
|
+
|
246
|
+
active_accounts = self._filter_landing_zone_accounts(all_accounts)
|
247
|
+
successful_sessions = self.cross_account_manager.get_successful_sessions(sessions)
|
248
|
+
|
249
|
+
print_success(f"✅ Landing Zone Discovery: {len(successful_sessions)}/{len(all_accounts)} accounts accessible")
|
250
|
+
|
251
|
+
except Exception as e:
|
252
|
+
print_error(f"❌ Failed to discover Landing Zone accounts: {e}")
|
253
|
+
raise
|
254
|
+
|
255
|
+
# Step 2: Parallel VPC topology discovery
|
256
|
+
print_info(f"🔍 Step 2: Parallel VPC discovery across {len(successful_sessions)} accounts...")
|
257
|
+
|
258
|
+
aggregated_results = await self._discover_vpc_topology_parallel(successful_sessions)
|
259
|
+
|
260
|
+
# Step 3: Generate comprehensive analytics
|
261
|
+
discovery_time = time.time() - start_time
|
262
|
+
|
263
|
+
landing_zone_metrics = {
|
264
|
+
'total_accounts_discovered': len(all_accounts),
|
265
|
+
'successful_sessions': len(successful_sessions),
|
266
|
+
'excluded_accounts': len(all_accounts) - len(active_accounts),
|
267
|
+
'discovery_time_seconds': discovery_time,
|
268
|
+
'performance_target_met': discovery_time < 60.0, # <60s target
|
269
|
+
'accounts_per_second': len(successful_sessions) / discovery_time if discovery_time > 0 else 0,
|
270
|
+
'session_ttl_hours': 4, # Enhanced: 4-hour TTL
|
271
|
+
'parallel_workers': self.max_workers
|
272
|
+
}
|
273
|
+
|
274
|
+
print_success(f"🎯 Multi-Organization Landing Zone Discovery Complete!")
|
275
|
+
print_info(f" 📊 Performance: {discovery_time:.1f}s for {len(successful_sessions)} accounts")
|
276
|
+
print_info(f" ⚡ Rate: {landing_zone_metrics['accounts_per_second']:.1f} accounts/second")
|
277
|
+
print_info(f" 🎯 Target met: {'✅ Yes' if landing_zone_metrics['performance_target_met'] else '❌ No'} (<60s)")
|
278
|
+
|
279
|
+
# Store sessions for future operations
|
280
|
+
self.landing_zone_sessions = successful_sessions
|
281
|
+
|
282
|
+
# Enhanced result with Landing Zone metadata
|
283
|
+
aggregated_results.landing_zone_metrics = landing_zone_metrics
|
284
|
+
aggregated_results.account_summary = {
|
285
|
+
'total_accounts': len(successful_sessions),
|
286
|
+
'excluded_accounts_list': self.excluded_accounts,
|
287
|
+
'discovery_timestamp': datetime.now().isoformat(),
|
288
|
+
'landing_zone_structure': landing_zone_structure
|
289
|
+
}
|
290
|
+
|
291
|
+
self.last_discovery = aggregated_results
|
292
|
+
return aggregated_results
|
293
|
+
|
294
|
+
async def _discover_vpc_topology_parallel(
|
295
|
+
self,
|
296
|
+
sessions: List[CrossAccountSession]
|
297
|
+
) -> VPCDiscoveryResult:
|
298
|
+
"""
|
299
|
+
Enhanced: Discover VPC topology across multiple accounts in parallel
|
300
|
+
|
301
|
+
Optimized for 60-account operations with <60s performance target.
|
302
|
+
|
303
|
+
Args:
|
304
|
+
sessions: List of successful cross-account sessions
|
305
|
+
|
306
|
+
Returns:
|
307
|
+
Aggregated VPCDiscoveryResult from all accounts
|
308
|
+
"""
|
309
|
+
if not sessions:
|
310
|
+
print_warning("⚠️ No successful sessions available for VPC discovery")
|
311
|
+
return self._create_empty_discovery_result()
|
312
|
+
|
313
|
+
# Initialize aggregated results
|
314
|
+
aggregated_vpcs = []
|
315
|
+
aggregated_nat_gateways = []
|
316
|
+
aggregated_vpc_endpoints = []
|
317
|
+
aggregated_internet_gateways = []
|
318
|
+
aggregated_route_tables = []
|
319
|
+
aggregated_subnets = []
|
320
|
+
aggregated_network_interfaces = []
|
321
|
+
aggregated_transit_gateway_attachments = []
|
322
|
+
aggregated_vpc_peering_connections = []
|
323
|
+
aggregated_security_groups = []
|
324
|
+
|
325
|
+
total_resources = 0
|
326
|
+
|
327
|
+
# Create progress tracking
|
328
|
+
with create_progress_bar() as progress:
|
329
|
+
task = progress.add_task(f"VPC discovery across {len(sessions)} accounts...", total=len(sessions))
|
330
|
+
|
331
|
+
# Process accounts in parallel batches
|
332
|
+
batch_size = min(self.max_workers, len(sessions))
|
333
|
+
|
334
|
+
for i in range(0, len(sessions), batch_size):
|
335
|
+
batch_sessions = sessions[i:i + batch_size]
|
336
|
+
batch_tasks = []
|
337
|
+
|
338
|
+
# Create async tasks for parallel processing
|
339
|
+
for session in batch_sessions:
|
340
|
+
task_coro = self._discover_single_account_vpc_topology(session)
|
341
|
+
batch_tasks.append(asyncio.create_task(task_coro))
|
342
|
+
|
343
|
+
# Wait for batch completion
|
344
|
+
batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
|
345
|
+
|
346
|
+
# Process batch results
|
347
|
+
for idx, result in enumerate(batch_results):
|
348
|
+
session = batch_sessions[idx]
|
349
|
+
|
350
|
+
if isinstance(result, Exception):
|
351
|
+
print_warning(f"⚠️ Account {session.account_id} discovery failed: {result}")
|
352
|
+
progress.advance(task)
|
353
|
+
continue
|
354
|
+
|
355
|
+
if result:
|
356
|
+
# Aggregate resources with account context
|
357
|
+
for vpc in result.vpcs:
|
358
|
+
vpc['source_account'] = session.account_id
|
359
|
+
vpc['source_account_name'] = session.account_name
|
360
|
+
aggregated_vpcs.extend(result.vpcs)
|
361
|
+
|
362
|
+
for nat_gw in result.nat_gateways:
|
363
|
+
nat_gw['source_account'] = session.account_id
|
364
|
+
nat_gw['source_account_name'] = session.account_name
|
365
|
+
aggregated_nat_gateways.extend(result.nat_gateways)
|
366
|
+
|
367
|
+
# Add account context to all resource types
|
368
|
+
for resource_list, aggregated_list in [
|
369
|
+
(result.vpc_endpoints, aggregated_vpc_endpoints),
|
370
|
+
(result.internet_gateways, aggregated_internet_gateways),
|
371
|
+
(result.route_tables, aggregated_route_tables),
|
372
|
+
(result.subnets, aggregated_subnets),
|
373
|
+
(result.network_interfaces, aggregated_network_interfaces),
|
374
|
+
(result.transit_gateway_attachments, aggregated_transit_gateway_attachments),
|
375
|
+
(result.vpc_peering_connections, aggregated_vpc_peering_connections),
|
376
|
+
(result.security_groups, aggregated_security_groups)
|
377
|
+
]:
|
378
|
+
for resource in resource_list:
|
379
|
+
resource['source_account'] = session.account_id
|
380
|
+
resource['source_account_name'] = session.account_name
|
381
|
+
aggregated_list.extend(resource_list)
|
382
|
+
|
383
|
+
total_resources += result.total_resources
|
384
|
+
|
385
|
+
progress.advance(task)
|
386
|
+
|
387
|
+
print_success(f"🎯 Parallel VPC discovery complete: {total_resources} total resources across {len(sessions)} accounts")
|
388
|
+
|
389
|
+
return VPCDiscoveryResult(
|
390
|
+
vpcs=aggregated_vpcs,
|
391
|
+
nat_gateways=aggregated_nat_gateways,
|
392
|
+
vpc_endpoints=aggregated_vpc_endpoints,
|
393
|
+
internet_gateways=aggregated_internet_gateways,
|
394
|
+
route_tables=aggregated_route_tables,
|
395
|
+
subnets=aggregated_subnets,
|
396
|
+
network_interfaces=aggregated_network_interfaces,
|
397
|
+
transit_gateway_attachments=aggregated_transit_gateway_attachments,
|
398
|
+
vpc_peering_connections=aggregated_vpc_peering_connections,
|
399
|
+
security_groups=aggregated_security_groups,
|
400
|
+
total_resources=total_resources,
|
401
|
+
discovery_timestamp=datetime.now().isoformat()
|
402
|
+
)
|
403
|
+
|
404
|
+
async def _discover_single_account_vpc_topology(
|
405
|
+
self,
|
406
|
+
session: CrossAccountSession
|
407
|
+
) -> Optional[VPCDiscoveryResult]:
|
408
|
+
"""
|
409
|
+
Discover VPC topology for a single account using cross-account session
|
410
|
+
|
411
|
+
Args:
|
412
|
+
session: CrossAccountSession with valid AWS credentials
|
413
|
+
|
414
|
+
Returns:
|
415
|
+
VPCDiscoveryResult for the account, or None if discovery fails
|
416
|
+
"""
|
417
|
+
if not session.session or session.status not in ['success', 'cached']:
|
418
|
+
return None
|
419
|
+
|
420
|
+
try:
|
421
|
+
# Create EC2 client with assumed role session
|
422
|
+
ec2_client = session.session.client('ec2', region_name=self.region)
|
423
|
+
|
424
|
+
# Discover VPC resources
|
425
|
+
vpcs = []
|
426
|
+
nat_gateways = []
|
427
|
+
vpc_endpoints = []
|
428
|
+
internet_gateways = []
|
429
|
+
route_tables = []
|
430
|
+
subnets = []
|
431
|
+
network_interfaces = []
|
432
|
+
transit_gateway_attachments = []
|
433
|
+
vpc_peering_connections = []
|
434
|
+
security_groups = []
|
435
|
+
|
436
|
+
# VPC Discovery
|
437
|
+
vpc_response = ec2_client.describe_vpcs()
|
438
|
+
for vpc in vpc_response['Vpcs']:
|
439
|
+
vpcs.append({
|
440
|
+
'VpcId': vpc['VpcId'],
|
441
|
+
'State': vpc['State'],
|
442
|
+
'CidrBlock': vpc['CidrBlock'],
|
443
|
+
'IsDefault': vpc.get('IsDefault', False),
|
444
|
+
'Tags': vpc.get('Tags', [])
|
445
|
+
})
|
446
|
+
|
447
|
+
# NAT Gateway Discovery
|
448
|
+
nat_response = ec2_client.describe_nat_gateways()
|
449
|
+
for nat_gw in nat_response['NatGateways']:
|
450
|
+
nat_gateways.append({
|
451
|
+
'NatGatewayId': nat_gw['NatGatewayId'],
|
452
|
+
'VpcId': nat_gw.get('VpcId'),
|
453
|
+
'State': nat_gw['State'],
|
454
|
+
'SubnetId': nat_gw.get('SubnetId'),
|
455
|
+
'Tags': nat_gw.get('Tags', [])
|
456
|
+
})
|
457
|
+
|
458
|
+
# Network Interfaces Discovery (for ENI gate analysis)
|
459
|
+
eni_response = ec2_client.describe_network_interfaces()
|
460
|
+
for eni in eni_response['NetworkInterfaces']:
|
461
|
+
network_interfaces.append({
|
462
|
+
'NetworkInterfaceId': eni['NetworkInterfaceId'],
|
463
|
+
'VpcId': eni.get('VpcId'),
|
464
|
+
'SubnetId': eni.get('SubnetId'),
|
465
|
+
'Status': eni['Status'],
|
466
|
+
'InterfaceType': eni.get('InterfaceType', 'interface'),
|
467
|
+
'Attachment': eni.get('Attachment', {}),
|
468
|
+
'Tags': eni.get('TagSet', [])
|
469
|
+
})
|
470
|
+
|
471
|
+
# Continue with other resource types as needed...
|
472
|
+
|
473
|
+
total_resources = (len(vpcs) + len(nat_gateways) + len(vpc_endpoints) +
|
474
|
+
len(internet_gateways) + len(route_tables) + len(subnets) +
|
475
|
+
len(network_interfaces) + len(transit_gateway_attachments) +
|
476
|
+
len(vpc_peering_connections) + len(security_groups))
|
477
|
+
|
478
|
+
return VPCDiscoveryResult(
|
479
|
+
vpcs=vpcs,
|
480
|
+
nat_gateways=nat_gateways,
|
481
|
+
vpc_endpoints=vpc_endpoints,
|
482
|
+
internet_gateways=internet_gateways,
|
483
|
+
route_tables=route_tables,
|
484
|
+
subnets=subnets,
|
485
|
+
network_interfaces=network_interfaces,
|
486
|
+
transit_gateway_attachments=transit_gateway_attachments,
|
487
|
+
vpc_peering_connections=vpc_peering_connections,
|
488
|
+
security_groups=security_groups,
|
489
|
+
total_resources=total_resources,
|
490
|
+
discovery_timestamp=datetime.now().isoformat()
|
491
|
+
)
|
492
|
+
|
493
|
+
except ClientError as e:
|
494
|
+
print_warning(f"⚠️ AWS API error for account {session.account_id}: {e}")
|
495
|
+
return None
|
496
|
+
except Exception as e:
|
497
|
+
print_error(f"❌ Unexpected error for account {session.account_id}: {e}")
|
498
|
+
return None
|
499
|
+
|
500
|
+
def _create_empty_discovery_result(self) -> VPCDiscoveryResult:
|
501
|
+
"""Create empty discovery result for error cases"""
|
502
|
+
return VPCDiscoveryResult(
|
503
|
+
vpcs=[],
|
504
|
+
nat_gateways=[],
|
505
|
+
vpc_endpoints=[],
|
506
|
+
internet_gateways=[],
|
507
|
+
route_tables=[],
|
508
|
+
subnets=[],
|
509
|
+
network_interfaces=[],
|
510
|
+
transit_gateway_attachments=[],
|
511
|
+
vpc_peering_connections=[],
|
512
|
+
security_groups=[],
|
513
|
+
total_resources=0,
|
514
|
+
discovery_timestamp=datetime.now().isoformat()
|
515
|
+
)
|
129
516
|
|
130
517
|
def discover_vpc_topology(self, vpc_ids: Optional[List[str]] = None) -> VPCDiscoveryResult:
|
131
518
|
"""
|
@@ -216,6 +603,388 @@ class VPCAnalyzer:
|
|
216
603
|
logger.error(f"VPC discovery error: {e}")
|
217
604
|
return self._empty_discovery_result()
|
218
605
|
|
606
|
+
async def discover_multi_org_vpc_topology(
|
607
|
+
self,
|
608
|
+
target_accounts: int = 60,
|
609
|
+
landing_zone_structure: Optional[Dict] = None
|
610
|
+
) -> VPCDiscoveryResult:
|
611
|
+
"""
|
612
|
+
NEW: Multi-Organization Landing Zone VPC discovery for 60-account operations
|
613
|
+
|
614
|
+
Optimized discovery across Landing Zone with decommissioned account filtering
|
615
|
+
and enhanced session management.
|
616
|
+
|
617
|
+
Args:
|
618
|
+
target_accounts: Target number of accounts to discover (60 for Landing Zone)
|
619
|
+
landing_zone_structure: Optional Landing Zone organizational structure
|
620
|
+
|
621
|
+
Returns:
|
622
|
+
VPCDiscoveryResult with comprehensive multi-account topology
|
623
|
+
"""
|
624
|
+
if not self.enable_multi_account or not self.cross_account_manager:
|
625
|
+
print_error("Multi-account mode not enabled. Initialize with enable_multi_account=True")
|
626
|
+
return self._empty_discovery_result()
|
627
|
+
|
628
|
+
print_header("Multi-Organization Landing Zone VPC Discovery", f"Target: {target_accounts} accounts")
|
629
|
+
|
630
|
+
start_time = time.time()
|
631
|
+
|
632
|
+
try:
|
633
|
+
# Step 1: Discover and filter Landing Zone accounts
|
634
|
+
print_info("🏢 Discovering Landing Zone organization accounts...")
|
635
|
+
accounts = await self._discover_landing_zone_accounts()
|
636
|
+
|
637
|
+
# Step 2: Filter decommissioned accounts
|
638
|
+
filtered_accounts = self._filter_landing_zone_accounts(accounts)
|
639
|
+
|
640
|
+
# Step 3: Create cross-account sessions
|
641
|
+
print_info(f"🔐 Creating cross-account sessions for {len(filtered_accounts)} accounts...")
|
642
|
+
sessions = await self.cross_account_manager.create_cross_account_sessions_from_accounts(filtered_accounts)
|
643
|
+
successful_sessions = self.cross_account_manager.get_successful_sessions(sessions)
|
644
|
+
|
645
|
+
self.landing_zone_sessions = successful_sessions
|
646
|
+
|
647
|
+
# Step 4: Parallel VPC discovery across all accounts
|
648
|
+
print_info(f"🔍 Discovering VPC topology across {len(successful_sessions)} accounts...")
|
649
|
+
multi_account_results = await self._discover_vpc_topology_parallel(successful_sessions)
|
650
|
+
|
651
|
+
# Step 5: Aggregate results and generate Landing Zone metrics
|
652
|
+
aggregated_result = self._aggregate_multi_account_results(multi_account_results, successful_sessions)
|
653
|
+
|
654
|
+
# Performance metrics
|
655
|
+
execution_time = time.time() - start_time
|
656
|
+
print_success(f"✅ Multi-Organization Landing Zone discovery complete in {execution_time:.1f}s")
|
657
|
+
print_info(f" 📈 {len(successful_sessions)}/{len(accounts)} accounts discovered")
|
658
|
+
print_info(f" 🏗️ {aggregated_result.total_resources} total VPC resources discovered")
|
659
|
+
|
660
|
+
return aggregated_result
|
661
|
+
|
662
|
+
except Exception as e:
|
663
|
+
print_error(f"Multi-Organization Landing Zone discovery failed: {e}")
|
664
|
+
logger.error(f"Landing Zone discovery error: {e}")
|
665
|
+
return self._empty_discovery_result()
|
666
|
+
|
667
|
+
async def _discover_landing_zone_accounts(self) -> List[OrganizationAccount]:
|
668
|
+
"""Discover accounts from Organizations API with Landing Zone context"""
|
669
|
+
orgs_client = get_unified_organizations_client(self.profile)
|
670
|
+
accounts = await orgs_client.get_organization_accounts()
|
671
|
+
|
672
|
+
if not accounts:
|
673
|
+
print_warning("No accounts discovered from Organizations API")
|
674
|
+
return []
|
675
|
+
|
676
|
+
print_info(f"🏢 Discovered {len(accounts)} total organization accounts")
|
677
|
+
return accounts
|
678
|
+
|
679
|
+
def _filter_landing_zone_accounts(self, accounts: List[OrganizationAccount]) -> List[OrganizationAccount]:
|
680
|
+
"""
|
681
|
+
Filter Landing Zone accounts with decommissioned account exclusion
|
682
|
+
|
683
|
+
Applies enterprise-grade filtering:
|
684
|
+
- Excludes decommissioned accounts (294618320542 by default)
|
685
|
+
- Filters to ACTIVE status accounts only
|
686
|
+
- Maintains Landing Zone organizational structure
|
687
|
+
"""
|
688
|
+
# Filter to active accounts only
|
689
|
+
active_accounts = [acc for acc in accounts if acc.status == 'ACTIVE']
|
690
|
+
|
691
|
+
# Filter out decommissioned accounts
|
692
|
+
filtered_accounts = [
|
693
|
+
acc for acc in active_accounts
|
694
|
+
if acc.account_id not in self.excluded_accounts
|
695
|
+
]
|
696
|
+
|
697
|
+
excluded_count = len(active_accounts) - len(filtered_accounts)
|
698
|
+
|
699
|
+
print_info(f"🎯 Landing Zone account filtering:")
|
700
|
+
print_info(f" • Total accounts: {len(accounts)}")
|
701
|
+
print_info(f" • Active accounts: {len(active_accounts)}")
|
702
|
+
print_info(f" • Excluded decommissioned: {excluded_count} ({self.excluded_accounts})")
|
703
|
+
print_info(f" • Ready for discovery: {len(filtered_accounts)}")
|
704
|
+
|
705
|
+
return filtered_accounts
|
706
|
+
|
707
|
+
async def _discover_vpc_topology_parallel(
|
708
|
+
self,
|
709
|
+
sessions: List[CrossAccountSession]
|
710
|
+
) -> List[Tuple[str, VPCDiscoveryResult]]:
|
711
|
+
"""
|
712
|
+
Parallel VPC discovery across multiple accounts optimized for 60-account Landing Zone
|
713
|
+
|
714
|
+
Performance optimized for <60s discovery across entire Landing Zone
|
715
|
+
"""
|
716
|
+
results = []
|
717
|
+
|
718
|
+
print_info(f"🚀 Starting parallel VPC discovery across {len(sessions)} accounts")
|
719
|
+
|
720
|
+
# Use asyncio.gather for concurrent execution
|
721
|
+
async def discover_account_vpc(session: CrossAccountSession) -> Tuple[str, VPCDiscoveryResult]:
|
722
|
+
try:
|
723
|
+
# Create account-specific VPC analyzer
|
724
|
+
account_analyzer = VPCAnalyzer(
|
725
|
+
profile=None, # Use session directly
|
726
|
+
region=self.region,
|
727
|
+
console=self.console,
|
728
|
+
dry_run=self.dry_run
|
729
|
+
)
|
730
|
+
account_analyzer.session = session.session
|
731
|
+
|
732
|
+
# Perform single-account VPC discovery
|
733
|
+
discovery_result = account_analyzer.discover_vpc_topology()
|
734
|
+
|
735
|
+
# Add account metadata to results
|
736
|
+
discovery_result.account_summary = {
|
737
|
+
'account_id': session.account_id,
|
738
|
+
'account_name': session.account_name,
|
739
|
+
'role_used': session.role_used,
|
740
|
+
'discovery_timestamp': discovery_result.discovery_timestamp
|
741
|
+
}
|
742
|
+
|
743
|
+
return session.account_id, discovery_result
|
744
|
+
|
745
|
+
except Exception as e:
|
746
|
+
print_warning(f"⚠️ VPC discovery failed for account {session.account_id}: {e}")
|
747
|
+
logger.warning(f"Account {session.account_id} VPC discovery error: {e}")
|
748
|
+
|
749
|
+
# Return empty result for failed account
|
750
|
+
empty_result = self._empty_discovery_result()
|
751
|
+
empty_result.account_summary = {
|
752
|
+
'account_id': session.account_id,
|
753
|
+
'account_name': session.account_name,
|
754
|
+
'error': str(e)
|
755
|
+
}
|
756
|
+
return session.account_id, empty_result
|
757
|
+
|
758
|
+
# Execute parallel discovery
|
759
|
+
with create_progress_bar() as progress:
|
760
|
+
task = progress.add_task("Discovering VPC topology across accounts...", total=len(sessions))
|
761
|
+
|
762
|
+
# Process accounts in batches to manage resource usage
|
763
|
+
batch_size = min(self.max_workers, len(sessions))
|
764
|
+
|
765
|
+
for i in range(0, len(sessions), batch_size):
|
766
|
+
batch_sessions = sessions[i:i + batch_size]
|
767
|
+
|
768
|
+
# Execute batch concurrently
|
769
|
+
batch_results = await asyncio.gather(*[
|
770
|
+
discover_account_vpc(session) for session in batch_sessions
|
771
|
+
], return_exceptions=True)
|
772
|
+
|
773
|
+
# Process batch results
|
774
|
+
for result in batch_results:
|
775
|
+
if isinstance(result, Exception):
|
776
|
+
print_warning(f"⚠️ Batch discovery error: {result}")
|
777
|
+
else:
|
778
|
+
results.append(result)
|
779
|
+
progress.advance(task)
|
780
|
+
|
781
|
+
print_success(f"✅ Parallel VPC discovery complete: {len(results)} accounts processed")
|
782
|
+
return results
|
783
|
+
|
784
|
+
def _aggregate_multi_account_results(
|
785
|
+
self,
|
786
|
+
multi_account_results: List[Tuple[str, VPCDiscoveryResult]],
|
787
|
+
sessions: List[CrossAccountSession]
|
788
|
+
) -> VPCDiscoveryResult:
|
789
|
+
"""
|
790
|
+
Aggregate multi-account VPC discovery results into comprehensive Landing Zone view
|
791
|
+
|
792
|
+
Creates unified view with Landing Zone metrics and account-level aggregation
|
793
|
+
"""
|
794
|
+
print_info("📊 Aggregating multi-account VPC results...")
|
795
|
+
|
796
|
+
# Initialize aggregation containers
|
797
|
+
all_vpcs = []
|
798
|
+
all_nat_gateways = []
|
799
|
+
all_vpc_endpoints = []
|
800
|
+
all_internet_gateways = []
|
801
|
+
all_route_tables = []
|
802
|
+
all_subnets = []
|
803
|
+
all_network_interfaces = []
|
804
|
+
all_tgw_attachments = []
|
805
|
+
all_vpc_peering = []
|
806
|
+
all_security_groups = []
|
807
|
+
|
808
|
+
account_summaries = []
|
809
|
+
total_resources_per_account = {}
|
810
|
+
|
811
|
+
# Aggregate resources from all accounts
|
812
|
+
for account_id, discovery_result in multi_account_results:
|
813
|
+
# Add account context to all resources
|
814
|
+
for vpc in discovery_result.vpcs:
|
815
|
+
vpc['AccountId'] = account_id
|
816
|
+
all_vpcs.append(vpc)
|
817
|
+
|
818
|
+
for nat in discovery_result.nat_gateways:
|
819
|
+
nat['AccountId'] = account_id
|
820
|
+
all_nat_gateways.append(nat)
|
821
|
+
|
822
|
+
for endpoint in discovery_result.vpc_endpoints:
|
823
|
+
endpoint['AccountId'] = account_id
|
824
|
+
all_vpc_endpoints.append(endpoint)
|
825
|
+
|
826
|
+
for igw in discovery_result.internet_gateways:
|
827
|
+
igw['AccountId'] = account_id
|
828
|
+
all_internet_gateways.append(igw)
|
829
|
+
|
830
|
+
for rt in discovery_result.route_tables:
|
831
|
+
rt['AccountId'] = account_id
|
832
|
+
all_route_tables.append(rt)
|
833
|
+
|
834
|
+
for subnet in discovery_result.subnets:
|
835
|
+
subnet['AccountId'] = account_id
|
836
|
+
all_subnets.append(subnet)
|
837
|
+
|
838
|
+
for eni in discovery_result.network_interfaces:
|
839
|
+
eni['AccountId'] = account_id
|
840
|
+
all_network_interfaces.append(eni)
|
841
|
+
|
842
|
+
for tgw in discovery_result.transit_gateway_attachments:
|
843
|
+
tgw['AccountId'] = account_id
|
844
|
+
all_tgw_attachments.append(tgw)
|
845
|
+
|
846
|
+
for peering in discovery_result.vpc_peering_connections:
|
847
|
+
peering['AccountId'] = account_id
|
848
|
+
all_vpc_peering.append(peering)
|
849
|
+
|
850
|
+
for sg in discovery_result.security_groups:
|
851
|
+
sg['AccountId'] = account_id
|
852
|
+
all_security_groups.append(sg)
|
853
|
+
|
854
|
+
# Track per-account metrics
|
855
|
+
total_resources_per_account[account_id] = discovery_result.total_resources
|
856
|
+
|
857
|
+
# Collect account summary
|
858
|
+
if discovery_result.account_summary:
|
859
|
+
account_summaries.append(discovery_result.account_summary)
|
860
|
+
|
861
|
+
# Calculate Landing Zone metrics
|
862
|
+
landing_zone_metrics = self._calculate_landing_zone_metrics(
|
863
|
+
total_resources_per_account, account_summaries, sessions
|
864
|
+
)
|
865
|
+
|
866
|
+
# Create aggregated result
|
867
|
+
total_resources = (
|
868
|
+
len(all_vpcs) + len(all_nat_gateways) + len(all_vpc_endpoints) +
|
869
|
+
len(all_internet_gateways) + len(all_route_tables) + len(all_subnets) +
|
870
|
+
len(all_network_interfaces) + len(all_tgw_attachments) +
|
871
|
+
len(all_vpc_peering) + len(all_security_groups)
|
872
|
+
)
|
873
|
+
|
874
|
+
aggregated_result = VPCDiscoveryResult(
|
875
|
+
vpcs=all_vpcs,
|
876
|
+
nat_gateways=all_nat_gateways,
|
877
|
+
vpc_endpoints=all_vpc_endpoints,
|
878
|
+
internet_gateways=all_internet_gateways,
|
879
|
+
route_tables=all_route_tables,
|
880
|
+
subnets=all_subnets,
|
881
|
+
network_interfaces=all_network_interfaces,
|
882
|
+
transit_gateway_attachments=all_tgw_attachments,
|
883
|
+
vpc_peering_connections=all_vpc_peering,
|
884
|
+
security_groups=all_security_groups,
|
885
|
+
total_resources=total_resources,
|
886
|
+
discovery_timestamp=datetime.now().isoformat(),
|
887
|
+
account_summary={'accounts_discovered': account_summaries},
|
888
|
+
landing_zone_metrics=landing_zone_metrics
|
889
|
+
)
|
890
|
+
|
891
|
+
# Display Landing Zone summary
|
892
|
+
self._display_landing_zone_summary(aggregated_result)
|
893
|
+
|
894
|
+
return aggregated_result
|
895
|
+
|
896
|
+
def _calculate_landing_zone_metrics(
|
897
|
+
self,
|
898
|
+
resources_per_account: Dict[str, int],
|
899
|
+
account_summaries: List[Dict],
|
900
|
+
sessions: List[CrossAccountSession]
|
901
|
+
) -> Dict[str, Any]:
|
902
|
+
"""Calculate comprehensive Landing Zone analytics"""
|
903
|
+
|
904
|
+
successful_accounts = len([s for s in sessions if s.status in ['success', 'cached']])
|
905
|
+
|
906
|
+
return {
|
907
|
+
'total_accounts_targeted': len(sessions),
|
908
|
+
'successful_discoveries': successful_accounts,
|
909
|
+
'failed_discoveries': len(sessions) - successful_accounts,
|
910
|
+
'discovery_success_rate': (successful_accounts / len(sessions) * 100) if sessions else 0,
|
911
|
+
'total_vpc_resources': sum(resources_per_account.values()),
|
912
|
+
'average_resources_per_account': (
|
913
|
+
sum(resources_per_account.values()) / len(resources_per_account)
|
914
|
+
if resources_per_account else 0
|
915
|
+
),
|
916
|
+
'accounts_with_resources': len([count for count in resources_per_account.values() if count > 0]),
|
917
|
+
'empty_accounts': len([count for count in resources_per_account.values() if count == 0]),
|
918
|
+
'decommissioned_accounts_excluded': len(self.excluded_accounts),
|
919
|
+
'excluded_account_list': self.excluded_accounts,
|
920
|
+
'session_manager_metrics': (
|
921
|
+
self.cross_account_manager.get_session_summary(sessions)
|
922
|
+
if self.cross_account_manager else {}
|
923
|
+
),
|
924
|
+
'generated_at': datetime.now().isoformat()
|
925
|
+
}
|
926
|
+
|
927
|
+
def _display_landing_zone_summary(self, result: VPCDiscoveryResult):
|
928
|
+
"""Display comprehensive Landing Zone summary with Rich formatting"""
|
929
|
+
|
930
|
+
# Landing Zone overview panel
|
931
|
+
metrics = result.landing_zone_metrics
|
932
|
+
|
933
|
+
summary_panel = Panel(
|
934
|
+
f"[bold green]Multi-Organization Landing Zone Discovery Complete[/bold green]\n\n"
|
935
|
+
f"🏢 Accounts Discovered: [bold cyan]{metrics['successful_discoveries']}/{metrics['total_accounts_targeted']}[/bold cyan] "
|
936
|
+
f"([bold yellow]{metrics['discovery_success_rate']:.1f}%[/bold yellow])\n"
|
937
|
+
f"🚫 Decommissioned Excluded: [bold red]{metrics['decommissioned_accounts_excluded']}[/bold red] "
|
938
|
+
f"({', '.join(metrics['excluded_account_list'])})\n"
|
939
|
+
f"📊 Total VPC Resources: [bold magenta]{metrics['total_vpc_resources']}[/bold magenta]\n"
|
940
|
+
f"📈 Avg Resources/Account: [bold blue]{metrics['average_resources_per_account']:.1f}[/bold blue]\n\n"
|
941
|
+
f"🏗️ Resource Breakdown:\n"
|
942
|
+
f" VPCs: [bold cyan]{len(result.vpcs)}[/bold cyan] | "
|
943
|
+
f"NAT Gateways: [bold yellow]{len(result.nat_gateways)}[/bold yellow] | "
|
944
|
+
f"Endpoints: [bold blue]{len(result.vpc_endpoints)}[/bold blue]\n"
|
945
|
+
f" IGWs: [bold green]{len(result.internet_gateways)}[/bold green] | "
|
946
|
+
f"Route Tables: [bold magenta]{len(result.route_tables)}[/bold magenta] | "
|
947
|
+
f"Subnets: [bold red]{len(result.subnets)}[/bold red]\n"
|
948
|
+
f" ENIs: [bold white]{len(result.network_interfaces)}[/bold white] | "
|
949
|
+
f"TGW Attachments: [bold orange]{len(result.transit_gateway_attachments)}[/bold orange] | "
|
950
|
+
f"Security Groups: [bold gray]{len(result.security_groups)}[/bold gray]",
|
951
|
+
title="🌐 Landing Zone VPC Discovery Summary",
|
952
|
+
style="bold blue"
|
953
|
+
)
|
954
|
+
|
955
|
+
self.console.print(summary_panel)
|
956
|
+
|
957
|
+
# Account distribution table
|
958
|
+
if metrics.get('session_manager_metrics'):
|
959
|
+
session_metrics = metrics['session_manager_metrics']
|
960
|
+
|
961
|
+
session_table = create_table(
|
962
|
+
title="🔐 Cross-Account Session Summary",
|
963
|
+
columns=[
|
964
|
+
{"header": "Metric", "style": "cyan"},
|
965
|
+
{"header": "Value", "style": "green"},
|
966
|
+
{"header": "Details", "style": "dim"}
|
967
|
+
]
|
968
|
+
)
|
969
|
+
|
970
|
+
session_table.add_row(
|
971
|
+
"Session Success Rate",
|
972
|
+
f"{(session_metrics['successful_sessions'] / session_metrics['total_sessions'] * 100):.1f}%",
|
973
|
+
f"{session_metrics['successful_sessions']}/{session_metrics['total_sessions']}"
|
974
|
+
)
|
975
|
+
session_table.add_row(
|
976
|
+
"Cache Performance",
|
977
|
+
f"{(session_metrics['metrics']['cache_hits'] / max(session_metrics['metrics']['cache_hits'] + session_metrics['metrics']['cache_misses'], 1) * 100):.1f}%",
|
978
|
+
f"{session_metrics['metrics']['cache_hits']} hits, {session_metrics['metrics']['cache_misses']} misses"
|
979
|
+
)
|
980
|
+
session_table.add_row(
|
981
|
+
"Session TTL",
|
982
|
+
f"{session_metrics['session_ttl_minutes']} minutes",
|
983
|
+
"4-hour enterprise standard"
|
984
|
+
)
|
985
|
+
|
986
|
+
self.console.print(session_table)
|
987
|
+
|
219
988
|
def analyze_awso_dependencies(self, discovery_result: Optional[VPCDiscoveryResult] = None) -> AWSOAnalysis:
|
220
989
|
"""
|
221
990
|
AWSO-05 specific dependency analysis for safe VPC cleanup
|
@@ -943,15 +1712,64 @@ class VPCAnalyzer:
|
|
943
1712
|
'CleanupReadiness': 'READY' if not analysis_data['eni_warnings'] else 'REQUIRES_WORKLOAD_MIGRATION'
|
944
1713
|
}
|
945
1714
|
|
1715
|
+
# NEW: Convenience methods for CLI integration
|
1716
|
+
def discover_landing_zone_vpc_topology(self) -> VPCDiscoveryResult:
|
1717
|
+
"""
|
1718
|
+
Convenience method for CLI integration - Multi-Organization Landing Zone discovery
|
1719
|
+
|
1720
|
+
Automatically enables multi-account mode and discovers VPC topology across
|
1721
|
+
60-account Landing Zone with decommissioned account filtering.
|
1722
|
+
|
1723
|
+
Returns:
|
1724
|
+
VPCDiscoveryResult with comprehensive Landing Zone topology
|
1725
|
+
"""
|
1726
|
+
if not self.enable_multi_account:
|
1727
|
+
# Auto-enable multi-account mode for Landing Zone discovery
|
1728
|
+
self.enable_multi_account = True
|
1729
|
+
self.cross_account_manager = EnhancedCrossAccountManager(
|
1730
|
+
base_profile=self.profile,
|
1731
|
+
max_workers=self.max_workers,
|
1732
|
+
session_ttl_minutes=240
|
1733
|
+
)
|
1734
|
+
print_info("🌐 Auto-enabled Multi-Organization Landing Zone mode")
|
1735
|
+
|
1736
|
+
# Use asyncio.run for CLI compatibility
|
1737
|
+
return asyncio.run(self.discover_multi_org_vpc_topology())
|
1738
|
+
|
1739
|
+
def get_landing_zone_session_summary(self) -> Optional[Dict[str, Any]]:
|
1740
|
+
"""Get comprehensive Landing Zone session summary for reporting"""
|
1741
|
+
if not self.landing_zone_sessions or not self.cross_account_manager:
|
1742
|
+
return None
|
1743
|
+
|
1744
|
+
return self.cross_account_manager.get_session_summary(self.landing_zone_sessions)
|
1745
|
+
|
1746
|
+
def refresh_landing_zone_sessions(self) -> bool:
|
1747
|
+
"""Refresh expired Landing Zone sessions for continued operations"""
|
1748
|
+
if not self.landing_zone_sessions or not self.cross_account_manager:
|
1749
|
+
print_warning("No Landing Zone sessions to refresh")
|
1750
|
+
return False
|
1751
|
+
|
1752
|
+
print_info("🔄 Refreshing Landing Zone sessions...")
|
1753
|
+
self.landing_zone_sessions = self.cross_account_manager.refresh_expired_sessions(
|
1754
|
+
self.landing_zone_sessions
|
1755
|
+
)
|
1756
|
+
|
1757
|
+
successful_sessions = len([s for s in self.landing_zone_sessions if s.status in ['success', 'cached']])
|
1758
|
+
print_success(f"✅ Session refresh complete: {successful_sessions} sessions ready")
|
1759
|
+
|
1760
|
+
return successful_sessions > 0
|
1761
|
+
|
946
1762
|
# Helper methods
|
947
1763
|
def _empty_discovery_result(self) -> VPCDiscoveryResult:
|
948
|
-
"""Return empty discovery result"""
|
1764
|
+
"""Return empty discovery result with Landing Zone structure"""
|
949
1765
|
return VPCDiscoveryResult(
|
950
1766
|
vpcs=[], nat_gateways=[], vpc_endpoints=[], internet_gateways=[],
|
951
1767
|
route_tables=[], subnets=[], network_interfaces=[],
|
952
1768
|
transit_gateway_attachments=[], vpc_peering_connections=[],
|
953
1769
|
security_groups=[], total_resources=0,
|
954
|
-
discovery_timestamp=datetime.now().isoformat()
|
1770
|
+
discovery_timestamp=datetime.now().isoformat(),
|
1771
|
+
account_summary=None,
|
1772
|
+
landing_zone_metrics=None
|
955
1773
|
)
|
956
1774
|
|
957
1775
|
def _empty_awso_analysis(self) -> AWSOAnalysis:
|