runbooks 0.9.9__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. runbooks/cfat/cloud_foundations_assessment.py +626 -0
  2. runbooks/cloudops/cost_optimizer.py +95 -33
  3. runbooks/common/aws_pricing.py +388 -0
  4. runbooks/common/aws_pricing_api.py +205 -0
  5. runbooks/common/aws_utils.py +2 -2
  6. runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
  7. runbooks/common/cross_account_manager.py +606 -0
  8. runbooks/common/enhanced_exception_handler.py +4 -0
  9. runbooks/common/env_utils.py +96 -0
  10. runbooks/common/mcp_integration.py +49 -2
  11. runbooks/common/organizations_client.py +579 -0
  12. runbooks/common/profile_utils.py +96 -2
  13. runbooks/finops/cost_optimizer.py +2 -1
  14. runbooks/finops/elastic_ip_optimizer.py +13 -9
  15. runbooks/finops/embedded_mcp_validator.py +31 -0
  16. runbooks/finops/enhanced_trend_visualization.py +3 -2
  17. runbooks/finops/markdown_exporter.py +217 -2
  18. runbooks/finops/nat_gateway_optimizer.py +57 -20
  19. runbooks/finops/vpc_cleanup_exporter.py +28 -26
  20. runbooks/finops/vpc_cleanup_optimizer.py +370 -16
  21. runbooks/inventory/__init__.py +10 -1
  22. runbooks/inventory/cloud_foundations_integration.py +409 -0
  23. runbooks/inventory/core/collector.py +1148 -88
  24. runbooks/inventory/discovery.md +389 -0
  25. runbooks/inventory/drift_detection_cli.py +327 -0
  26. runbooks/inventory/inventory_mcp_cli.py +171 -0
  27. runbooks/inventory/inventory_modules.py +4 -7
  28. runbooks/inventory/mcp_inventory_validator.py +2149 -0
  29. runbooks/inventory/mcp_vpc_validator.py +23 -6
  30. runbooks/inventory/organizations_discovery.py +91 -1
  31. runbooks/inventory/rich_inventory_display.py +129 -1
  32. runbooks/inventory/unified_validation_engine.py +1292 -0
  33. runbooks/inventory/verify_ec2_security_groups.py +3 -1
  34. runbooks/inventory/vpc_analyzer.py +825 -7
  35. runbooks/inventory/vpc_flow_analyzer.py +36 -42
  36. runbooks/main.py +654 -35
  37. runbooks/monitoring/performance_monitor.py +11 -7
  38. runbooks/operate/dynamodb_operations.py +6 -5
  39. runbooks/operate/ec2_operations.py +3 -2
  40. runbooks/operate/networking_cost_heatmap.py +4 -3
  41. runbooks/operate/s3_operations.py +13 -12
  42. runbooks/operate/vpc_operations.py +49 -1
  43. runbooks/remediation/base.py +1 -1
  44. runbooks/remediation/commvault_ec2_analysis.py +6 -1
  45. runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
  46. runbooks/remediation/rds_snapshot_list.py +5 -3
  47. runbooks/validation/__init__.py +21 -1
  48. runbooks/validation/comprehensive_2way_validator.py +1996 -0
  49. runbooks/validation/mcp_validator.py +904 -94
  50. runbooks/validation/terraform_citations_validator.py +363 -0
  51. runbooks/validation/terraform_drift_detector.py +1098 -0
  52. runbooks/vpc/cleanup_wrapper.py +231 -10
  53. runbooks/vpc/config.py +310 -62
  54. runbooks/vpc/cross_account_session.py +308 -0
  55. runbooks/vpc/heatmap_engine.py +96 -29
  56. runbooks/vpc/manager_interface.py +9 -9
  57. runbooks/vpc/mcp_no_eni_validator.py +1551 -0
  58. runbooks/vpc/networking_wrapper.py +14 -8
  59. runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
  60. runbooks/vpc/runbooks.security.report_generator.log +0 -0
  61. runbooks/vpc/runbooks.security.run_script.log +0 -0
  62. runbooks/vpc/runbooks.security.security_export.log +0 -0
  63. runbooks/vpc/tests/test_cost_engine.py +1 -1
  64. runbooks/vpc/unified_scenarios.py +73 -3
  65. runbooks/vpc/vpc_cleanup_integration.py +512 -78
  66. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/METADATA +94 -52
  67. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/RECORD +71 -49
  68. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/WHEEL +0 -0
  69. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/entry_points.txt +0 -0
  70. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/licenses/LICENSE +0 -0
  71. {runbooks-0.9.9.dist-info → runbooks-1.0.0.dist-info}/top_level.txt +0 -0
@@ -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: