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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/WEIGHT_CONFIG_README.md +368 -0
  3. runbooks/cfat/app.ts +27 -19
  4. runbooks/cfat/assessment/runner.py +6 -5
  5. runbooks/cfat/cloud_foundations_assessment.py +626 -0
  6. runbooks/cfat/tests/test_weight_configuration.ts +449 -0
  7. runbooks/cfat/weight_config.ts +574 -0
  8. runbooks/cloudops/cost_optimizer.py +95 -33
  9. runbooks/common/__init__.py +26 -9
  10. runbooks/common/aws_pricing.py +1353 -0
  11. runbooks/common/aws_pricing_api.py +205 -0
  12. runbooks/common/aws_utils.py +2 -2
  13. runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
  14. runbooks/common/cross_account_manager.py +606 -0
  15. runbooks/common/date_utils.py +115 -0
  16. runbooks/common/enhanced_exception_handler.py +14 -7
  17. runbooks/common/env_utils.py +96 -0
  18. runbooks/common/mcp_cost_explorer_integration.py +5 -4
  19. runbooks/common/mcp_integration.py +49 -2
  20. runbooks/common/organizations_client.py +579 -0
  21. runbooks/common/profile_utils.py +127 -72
  22. runbooks/common/rich_utils.py +3 -3
  23. runbooks/finops/cost_optimizer.py +2 -1
  24. runbooks/finops/dashboard_runner.py +47 -28
  25. runbooks/finops/ebs_optimizer.py +56 -9
  26. runbooks/finops/elastic_ip_optimizer.py +13 -9
  27. runbooks/finops/embedded_mcp_validator.py +31 -0
  28. runbooks/finops/enhanced_trend_visualization.py +10 -4
  29. runbooks/finops/finops_dashboard.py +6 -5
  30. runbooks/finops/iam_guidance.py +6 -1
  31. runbooks/finops/markdown_exporter.py +217 -2
  32. runbooks/finops/nat_gateway_optimizer.py +76 -20
  33. runbooks/finops/tests/test_integration.py +3 -1
  34. runbooks/finops/vpc_cleanup_exporter.py +28 -26
  35. runbooks/finops/vpc_cleanup_optimizer.py +363 -16
  36. runbooks/inventory/__init__.py +10 -1
  37. runbooks/inventory/cloud_foundations_integration.py +409 -0
  38. runbooks/inventory/core/collector.py +1177 -94
  39. runbooks/inventory/discovery.md +339 -0
  40. runbooks/inventory/drift_detection_cli.py +327 -0
  41. runbooks/inventory/inventory_mcp_cli.py +171 -0
  42. runbooks/inventory/inventory_modules.py +6 -9
  43. runbooks/inventory/list_ec2_instances.py +3 -3
  44. runbooks/inventory/mcp_inventory_validator.py +2149 -0
  45. runbooks/inventory/mcp_vpc_validator.py +23 -6
  46. runbooks/inventory/organizations_discovery.py +104 -9
  47. runbooks/inventory/rich_inventory_display.py +129 -1
  48. runbooks/inventory/unified_validation_engine.py +1279 -0
  49. runbooks/inventory/verify_ec2_security_groups.py +3 -1
  50. runbooks/inventory/vpc_analyzer.py +825 -7
  51. runbooks/inventory/vpc_flow_analyzer.py +36 -42
  52. runbooks/main.py +708 -47
  53. runbooks/monitoring/performance_monitor.py +11 -7
  54. runbooks/operate/base.py +9 -6
  55. runbooks/operate/deployment_framework.py +5 -4
  56. runbooks/operate/deployment_validator.py +6 -5
  57. runbooks/operate/dynamodb_operations.py +6 -5
  58. runbooks/operate/ec2_operations.py +3 -2
  59. runbooks/operate/mcp_integration.py +6 -5
  60. runbooks/operate/networking_cost_heatmap.py +21 -16
  61. runbooks/operate/s3_operations.py +13 -12
  62. runbooks/operate/vpc_operations.py +100 -12
  63. runbooks/remediation/base.py +4 -2
  64. runbooks/remediation/commons.py +5 -5
  65. runbooks/remediation/commvault_ec2_analysis.py +68 -15
  66. runbooks/remediation/config/accounts_example.json +31 -0
  67. runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
  68. runbooks/remediation/multi_account.py +120 -7
  69. runbooks/remediation/rds_snapshot_list.py +5 -3
  70. runbooks/remediation/remediation_cli.py +710 -0
  71. runbooks/remediation/universal_account_discovery.py +377 -0
  72. runbooks/security/compliance_automation_engine.py +99 -20
  73. runbooks/security/config/__init__.py +24 -0
  74. runbooks/security/config/compliance_config.py +255 -0
  75. runbooks/security/config/compliance_weights_example.json +22 -0
  76. runbooks/security/config_template_generator.py +500 -0
  77. runbooks/security/security_cli.py +377 -0
  78. runbooks/validation/__init__.py +21 -1
  79. runbooks/validation/cli.py +8 -7
  80. runbooks/validation/comprehensive_2way_validator.py +2007 -0
  81. runbooks/validation/mcp_validator.py +965 -101
  82. runbooks/validation/terraform_citations_validator.py +363 -0
  83. runbooks/validation/terraform_drift_detector.py +1098 -0
  84. runbooks/vpc/cleanup_wrapper.py +231 -10
  85. runbooks/vpc/config.py +346 -73
  86. runbooks/vpc/cross_account_session.py +312 -0
  87. runbooks/vpc/heatmap_engine.py +115 -41
  88. runbooks/vpc/manager_interface.py +9 -9
  89. runbooks/vpc/mcp_no_eni_validator.py +1630 -0
  90. runbooks/vpc/networking_wrapper.py +14 -8
  91. runbooks/vpc/runbooks_adapter.py +33 -12
  92. runbooks/vpc/tests/conftest.py +4 -2
  93. runbooks/vpc/tests/test_cost_engine.py +4 -2
  94. runbooks/vpc/unified_scenarios.py +73 -3
  95. runbooks/vpc/vpc_cleanup_integration.py +512 -78
  96. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/METADATA +94 -52
  97. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/RECORD +101 -81
  98. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  99. runbooks/finops/runbooks.security.report_generator.log +0 -0
  100. runbooks/finops/runbooks.security.run_script.log +0 -0
  101. runbooks/finops/runbooks.security.security_export.log +0 -0
  102. runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
  103. runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
  104. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  105. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  106. runbooks/inventory/runbooks.security.run_script.log +0 -0
  107. runbooks/inventory/runbooks.security.security_export.log +0 -0
  108. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
  109. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,579 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unified Organizations API Client for CloudOps Runbooks Platform
4
+
5
+ This module consolidates Organizations API patterns from inventory, finops, and vpc modules
6
+ into a unified, cached, high-performance client following enterprise standards.
7
+
8
+ Features:
9
+ - Global caching with 30-minute TTL (extracted from inventory module)
10
+ - 4-profile enterprise architecture support
11
+ - Rich CLI integration with progress indicators
12
+ - Comprehensive error handling and graceful degradation
13
+ - Performance optimization for 61-account enterprise scale
14
+ - Thread-safe operations with concurrent access support
15
+
16
+ Author: CloudOps Runbooks Team
17
+ Version: 0.9.1
18
+ """
19
+
20
+ import asyncio
21
+ import functools
22
+ import time
23
+ from concurrent.futures import ThreadPoolExecutor, as_completed
24
+ from dataclasses import asdict, dataclass
25
+ from datetime import datetime, timezone
26
+ from typing import Dict, List, Optional, Set, Tuple
27
+
28
+ import boto3
29
+ from botocore.exceptions import ClientError, NoCredentialsError
30
+
31
+ from runbooks.common.profile_utils import create_management_session, get_profile_for_operation
32
+ from runbooks.common.rich_utils import (
33
+ console,
34
+ create_progress_bar,
35
+ print_error,
36
+ print_info,
37
+ print_success,
38
+ print_warning,
39
+ )
40
+
41
+ # Global Organizations cache shared across all instances and modules
42
+ _GLOBAL_ORGS_CACHE = {
43
+ 'data': None,
44
+ 'accounts': None,
45
+ 'organizational_units': None,
46
+ 'timestamp': None,
47
+ 'ttl_minutes': 30
48
+ }
49
+
50
+ # Thread lock for cache operations
51
+ import threading
52
+ _cache_lock = threading.Lock()
53
+
54
+
55
+ @dataclass
56
+ class OrganizationAccount:
57
+ """Standard organization account representation across all modules"""
58
+ account_id: str
59
+ name: str
60
+ email: str
61
+ status: str
62
+ joined_method: str
63
+ joined_timestamp: Optional[datetime] = None
64
+ parent_id: Optional[str] = None
65
+ organizational_unit: Optional[str] = None
66
+ tags: Dict[str, str] = None
67
+
68
+ def __post_init__(self):
69
+ if self.tags is None:
70
+ self.tags = {}
71
+
72
+ def to_dict(self) -> Dict[str, any]:
73
+ """Convert to dictionary for compatibility with existing modules"""
74
+ return asdict(self)
75
+
76
+
77
+ @dataclass
78
+ class OrganizationalUnit:
79
+ """Standard organizational unit representation"""
80
+ ou_id: str
81
+ name: str
82
+ parent_id: Optional[str] = None
83
+ accounts: List[str] = None
84
+ child_ous: List[str] = None
85
+
86
+ def __post_init__(self):
87
+ if self.accounts is None:
88
+ self.accounts = []
89
+ if self.child_ous is None:
90
+ self.child_ous = []
91
+
92
+
93
+ class UnifiedOrganizationsClient:
94
+ """
95
+ Unified Organizations API client consolidating patterns from all modules.
96
+
97
+ This client provides a single interface for Organizations API operations
98
+ with global caching, error handling, and performance optimization.
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ management_profile: Optional[str] = None,
104
+ cache_ttl_minutes: int = 30,
105
+ max_workers: int = 50
106
+ ):
107
+ """
108
+ Initialize unified Organizations client.
109
+
110
+ Args:
111
+ management_profile: AWS profile with Organizations access
112
+ cache_ttl_minutes: Cache TTL in minutes (default: 30)
113
+ max_workers: Maximum workers for parallel operations
114
+ """
115
+ self.management_profile = management_profile
116
+ self.cache_ttl_minutes = cache_ttl_minutes
117
+ self.max_workers = max_workers
118
+
119
+ # Initialize session
120
+ self.session = None
121
+ self.client = None
122
+
123
+ # Performance metrics
124
+ self.metrics = {
125
+ 'api_calls_made': 0,
126
+ 'cache_hits': 0,
127
+ 'cache_misses': 0,
128
+ 'errors_encountered': 0,
129
+ 'last_refresh': None,
130
+ }
131
+
132
+ def _initialize_client(self) -> bool:
133
+ """Initialize Organizations client with error handling"""
134
+ try:
135
+ if self.management_profile:
136
+ self.session = create_management_session(self.management_profile)
137
+ else:
138
+ # Use profile resolution from existing patterns
139
+ profile = get_profile_for_operation("management", None)
140
+ self.session = boto3.Session(profile_name=profile)
141
+
142
+ # Organizations is a global service - always use us-east-1
143
+ self.client = self.session.client('organizations', region_name='us-east-1')
144
+
145
+ # Test connectivity
146
+ self.client.describe_organization()
147
+ return True
148
+
149
+ except ClientError as e:
150
+ error_code = e.response.get('Error', {}).get('Code', '')
151
+ if error_code == 'AccessDeniedException':
152
+ print_warning(f"Organizations access denied for profile '{self.management_profile}'")
153
+ elif error_code == 'AWSOrganizationsNotInUseException':
154
+ print_info("AWS Organizations not enabled for this account")
155
+ else:
156
+ print_warning(f"Organizations API error: {error_code}")
157
+ return False
158
+
159
+ except NoCredentialsError:
160
+ print_warning("AWS credentials not available for Organizations API")
161
+ return False
162
+
163
+ except Exception as e:
164
+ print_error(f"Failed to initialize Organizations client: {e}")
165
+ return False
166
+
167
+ def _is_cache_valid(self) -> bool:
168
+ """Check if global cache is still valid"""
169
+ with _cache_lock:
170
+ if not _GLOBAL_ORGS_CACHE['timestamp']:
171
+ return False
172
+
173
+ cache_age_minutes = (
174
+ datetime.now(timezone.utc) - _GLOBAL_ORGS_CACHE['timestamp']
175
+ ).total_seconds() / 60
176
+
177
+ return cache_age_minutes < self.cache_ttl_minutes
178
+
179
+ def _get_cached_data(self, data_type: str) -> Optional[any]:
180
+ """Get specific cached data type"""
181
+ if self._is_cache_valid():
182
+ with _cache_lock:
183
+ self.metrics['cache_hits'] += 1
184
+ if data_type == 'accounts':
185
+ return _GLOBAL_ORGS_CACHE.get('accounts')
186
+ elif data_type == 'organizational_units':
187
+ return _GLOBAL_ORGS_CACHE.get('organizational_units')
188
+ elif data_type == 'complete':
189
+ return _GLOBAL_ORGS_CACHE.get('data')
190
+
191
+ self.metrics['cache_misses'] += 1
192
+ return None
193
+
194
+ def _set_cached_data(self, accounts: List[OrganizationAccount], ous: List[OrganizationalUnit], complete_data: Dict):
195
+ """Set cached data with thread safety"""
196
+ with _cache_lock:
197
+ _GLOBAL_ORGS_CACHE['accounts'] = accounts
198
+ _GLOBAL_ORGS_CACHE['organizational_units'] = ous
199
+ _GLOBAL_ORGS_CACHE['data'] = complete_data
200
+ _GLOBAL_ORGS_CACHE['timestamp'] = datetime.now(timezone.utc)
201
+ self.metrics['last_refresh'] = datetime.now(timezone.utc)
202
+
203
+ accounts_count = len(accounts) if accounts else 0
204
+ ous_count = len(ous) if ous else 0
205
+ print_success(f"✅ Organizations cache updated: {accounts_count} accounts, {ous_count} OUs")
206
+
207
+ async def get_organization_accounts(self, include_tags: bool = False) -> List[OrganizationAccount]:
208
+ """
209
+ Get all organization accounts with caching support.
210
+
211
+ Args:
212
+ include_tags: Whether to include account tags (slower but more comprehensive)
213
+
214
+ Returns:
215
+ List of OrganizationAccount objects
216
+ """
217
+ # Check cache first
218
+ cached_accounts = self._get_cached_data('accounts')
219
+ if cached_accounts:
220
+ print_info(f"🚀 Using cached account data ({len(cached_accounts)} accounts)")
221
+ return cached_accounts
222
+
223
+ # Initialize client if needed
224
+ if not self.client and not self._initialize_client():
225
+ print_warning("Organizations client unavailable - returning empty account list")
226
+ return []
227
+
228
+ print_info("🔍 Discovering organization accounts...")
229
+ accounts = []
230
+
231
+ try:
232
+ with create_progress_bar() as progress:
233
+ task = progress.add_task("Discovering accounts...", total=None)
234
+
235
+ # Get accounts using paginator for large organizations
236
+ paginator = self.client.get_paginator('list_accounts')
237
+
238
+ for page in paginator.paginate():
239
+ for account_data in page['Accounts']:
240
+ account = OrganizationAccount(
241
+ account_id=account_data['Id'],
242
+ name=account_data['Name'],
243
+ email=account_data['Email'],
244
+ status=account_data['Status'],
245
+ joined_method=account_data['JoinedMethod'],
246
+ joined_timestamp=account_data['JoinedTimestamp'],
247
+ )
248
+
249
+ # Get account tags if requested
250
+ if include_tags:
251
+ try:
252
+ tags_response = self.client.list_tags_for_resource(
253
+ ResourceId=account.account_id
254
+ )
255
+ account.tags = {
256
+ tag['Key']: tag['Value'] for tag in tags_response['Tags']
257
+ }
258
+ self.metrics['api_calls_made'] += 1
259
+ except ClientError:
260
+ # Tags may not be accessible for all accounts
261
+ account.tags = {}
262
+
263
+ accounts.append(account)
264
+
265
+ self.metrics['api_calls_made'] += 1
266
+ progress.update(task, description=f"Found {len(accounts)} accounts...")
267
+
268
+ # Map accounts to OUs
269
+ await self._map_accounts_to_ous(accounts)
270
+
271
+ print_success(f"✅ Discovered {len(accounts)} organization accounts")
272
+ return accounts
273
+
274
+ except Exception as e:
275
+ self.metrics['errors_encountered'] += 1
276
+ print_error(f"Failed to discover organization accounts: {e}")
277
+ return []
278
+
279
+ async def get_organizational_units(self) -> List[OrganizationalUnit]:
280
+ """
281
+ Get all organizational units with caching support.
282
+
283
+ Returns:
284
+ List of OrganizationalUnit objects
285
+ """
286
+ # Check cache first
287
+ cached_ous = self._get_cached_data('organizational_units')
288
+ if cached_ous:
289
+ print_info(f"🚀 Using cached OU data ({len(cached_ous)} OUs)")
290
+ return cached_ous
291
+
292
+ # Initialize client if needed
293
+ if not self.client and not self._initialize_client():
294
+ print_warning("Organizations client unavailable - returning empty OU list")
295
+ return []
296
+
297
+ print_info("🏗️ Discovering organizational units...")
298
+ all_ous = []
299
+
300
+ try:
301
+ # Get root OU
302
+ roots_response = self.client.list_roots()
303
+ if not roots_response.get('Roots'):
304
+ print_warning("No root organizational units found")
305
+ return []
306
+
307
+ root_id = roots_response['Roots'][0]['Id']
308
+ self.metrics['api_calls_made'] += 1
309
+
310
+ # Recursively discover all OUs
311
+ await self._discover_ou_recursive(root_id, all_ous)
312
+
313
+ print_success(f"✅ Discovered {len(all_ous)} organizational units")
314
+ return all_ous
315
+
316
+ except Exception as e:
317
+ self.metrics['errors_encountered'] += 1
318
+ print_error(f"Failed to discover organizational units: {e}")
319
+ return []
320
+
321
+ async def _discover_ou_recursive(self, parent_id: str, ou_list: List[OrganizationalUnit]):
322
+ """Recursively discover organizational units"""
323
+ try:
324
+ paginator = self.client.get_paginator('list_organizational_units_for_parent')
325
+
326
+ for page in paginator.paginate(ParentId=parent_id):
327
+ for ou_data in page['OrganizationalUnits']:
328
+ ou = OrganizationalUnit(
329
+ ou_id=ou_data['Id'],
330
+ name=ou_data['Name'],
331
+ parent_id=parent_id
332
+ )
333
+
334
+ ou_list.append(ou)
335
+
336
+ # Recursively discover child OUs
337
+ await self._discover_ou_recursive(ou.ou_id, ou_list)
338
+
339
+ self.metrics['api_calls_made'] += 1
340
+
341
+ except ClientError as e:
342
+ print_warning(f"Failed to discover OU children for {parent_id}: {e}")
343
+ self.metrics['errors_encountered'] += 1
344
+
345
+ async def _map_accounts_to_ous(self, accounts: List[OrganizationAccount]):
346
+ """Map accounts to their organizational units"""
347
+ if not self.client:
348
+ return
349
+
350
+ print_info("🗺️ Mapping accounts to organizational units...")
351
+
352
+ with create_progress_bar() as progress:
353
+ task = progress.add_task("Mapping accounts to OUs...", total=len(accounts))
354
+
355
+ for account in accounts:
356
+ try:
357
+ parents_response = self.client.list_parents(ChildId=account.account_id)
358
+
359
+ if parents_response['Parents']:
360
+ parent = parents_response['Parents'][0]
361
+ account.parent_id = parent['Id']
362
+
363
+ # Get OU name if parent is an OU
364
+ if parent['Type'] == 'ORGANIZATIONAL_UNIT':
365
+ try:
366
+ ou_response = self.client.describe_organizational_unit(
367
+ OrganizationalUnitId=parent['Id']
368
+ )
369
+ account.organizational_unit = ou_response['OrganizationalUnit']['Name']
370
+ self.metrics['api_calls_made'] += 1
371
+ except ClientError:
372
+ account.organizational_unit = f"OU-{parent['Id']}"
373
+
374
+ self.metrics['api_calls_made'] += 1
375
+
376
+ except ClientError:
377
+ # Continue with other accounts
378
+ self.metrics['errors_encountered'] += 1
379
+
380
+ progress.advance(task)
381
+
382
+ async def get_complete_organization_structure(self, include_tags: bool = False) -> Dict:
383
+ """
384
+ Get complete organization structure with caching.
385
+
386
+ This method provides compatibility with existing inventory module patterns.
387
+
388
+ Args:
389
+ include_tags: Whether to include account tags
390
+
391
+ Returns:
392
+ Complete organization structure dictionary
393
+ """
394
+ # Check for complete cached data
395
+ cached_data = self._get_cached_data('complete')
396
+ if cached_data:
397
+ print_info("🚀 Using cached complete organization structure")
398
+ return cached_data
399
+
400
+ print_info("🏢 Discovering complete organization structure...")
401
+
402
+ # Get accounts and OUs
403
+ accounts = await self.get_organization_accounts(include_tags=include_tags)
404
+ ous = await self.get_organizational_units()
405
+
406
+ # Get organization info
407
+ org_info = await self._get_organization_info()
408
+
409
+ # Build complete structure
410
+ complete_data = {
411
+ 'status': 'completed',
412
+ 'discovery_type': 'unified_organizations_api',
413
+ 'organization_info': org_info,
414
+ 'accounts': {
415
+ 'total_accounts': len(accounts),
416
+ 'active_accounts': len([a for a in accounts if a.status == 'ACTIVE']),
417
+ 'discovered_accounts': [a.to_dict() for a in accounts],
418
+ 'discovery_method': 'organizations_api',
419
+ },
420
+ 'organizational_units': {
421
+ 'total_ous': len(ous),
422
+ 'organizational_units': [asdict(ou) for ou in ous],
423
+ 'discovery_method': 'organizations_api',
424
+ },
425
+ 'metrics': self.metrics.copy(),
426
+ 'timestamp': datetime.now().isoformat(),
427
+ }
428
+
429
+ # Cache the complete structure
430
+ self._set_cached_data(accounts, ous, complete_data)
431
+
432
+ return complete_data
433
+
434
+ async def _get_organization_info(self) -> Dict:
435
+ """Get high-level organization information"""
436
+ if not self.client:
437
+ return {
438
+ 'organization_id': 'unavailable',
439
+ 'master_account_id': 'unavailable',
440
+ 'master_account_email': 'unavailable',
441
+ 'feature_set': 'unavailable',
442
+ 'available_policy_types': [],
443
+ 'discovery_method': 'unavailable',
444
+ }
445
+
446
+ try:
447
+ org_response = self.client.describe_organization()
448
+ org = org_response['Organization']
449
+ self.metrics['api_calls_made'] += 1
450
+
451
+ return {
452
+ 'organization_id': org['Id'],
453
+ 'master_account_id': org['MasterAccountId'],
454
+ 'master_account_email': org['MasterAccountEmail'],
455
+ 'feature_set': org['FeatureSet'],
456
+ 'available_policy_types': [pt['Type'] for pt in org.get('AvailablePolicyTypes', [])],
457
+ 'discovery_method': 'organizations_api',
458
+ }
459
+
460
+ except ClientError as e:
461
+ print_warning(f"Failed to get organization info: {e}")
462
+ return {
463
+ 'organization_id': 'error',
464
+ 'master_account_id': 'error',
465
+ 'master_account_email': 'error',
466
+ 'feature_set': 'error',
467
+ 'available_policy_types': [],
468
+ 'discovery_method': 'failed',
469
+ 'error': str(e),
470
+ }
471
+
472
+ def get_account_name_mapping(self) -> Dict[str, str]:
473
+ """
474
+ Get account ID to name mapping for compatibility with FinOps module.
475
+
476
+ Returns:
477
+ Dictionary mapping account IDs to account names
478
+ """
479
+ cached_accounts = self._get_cached_data('accounts')
480
+ if not cached_accounts:
481
+ # Try to refresh cache
482
+ import asyncio
483
+ try:
484
+ cached_accounts = asyncio.get_event_loop().run_until_complete(
485
+ self.get_organization_accounts()
486
+ )
487
+ except:
488
+ return {}
489
+
490
+ return {account.account_id: account.name for account in cached_accounts}
491
+
492
+ def invalidate_cache(self):
493
+ """Manually invalidate the global cache"""
494
+ with _cache_lock:
495
+ _GLOBAL_ORGS_CACHE['data'] = None
496
+ _GLOBAL_ORGS_CACHE['accounts'] = None
497
+ _GLOBAL_ORGS_CACHE['organizational_units'] = None
498
+ _GLOBAL_ORGS_CACHE['timestamp'] = None
499
+
500
+ print_info("🗑️ Organizations cache invalidated")
501
+
502
+ def get_cache_status(self) -> Dict:
503
+ """Get cache status and metrics"""
504
+ with _cache_lock:
505
+ return {
506
+ 'cache_valid': self._is_cache_valid(),
507
+ 'cache_timestamp': _GLOBAL_ORGS_CACHE.get('timestamp'),
508
+ 'ttl_minutes': self.cache_ttl_minutes,
509
+ 'metrics': self.metrics.copy(),
510
+ 'accounts_cached': len(_GLOBAL_ORGS_CACHE.get('accounts', [])),
511
+ 'ous_cached': len(_GLOBAL_ORGS_CACHE.get('organizational_units', [])),
512
+ }
513
+
514
+
515
+ # Factory functions for easy integration with existing modules
516
+ def get_unified_organizations_client(
517
+ management_profile: Optional[str] = None,
518
+ cache_ttl_minutes: int = 30
519
+ ) -> UnifiedOrganizationsClient:
520
+ """
521
+ Factory function to get unified Organizations client.
522
+
523
+ Args:
524
+ management_profile: AWS profile with Organizations access
525
+ cache_ttl_minutes: Cache TTL in minutes
526
+
527
+ Returns:
528
+ UnifiedOrganizationsClient instance
529
+ """
530
+ return UnifiedOrganizationsClient(management_profile, cache_ttl_minutes)
531
+
532
+
533
+ async def get_organization_accounts(
534
+ management_profile: Optional[str] = None,
535
+ include_tags: bool = False
536
+ ) -> List[OrganizationAccount]:
537
+ """
538
+ Convenience function to get organization accounts.
539
+
540
+ Args:
541
+ management_profile: AWS profile with Organizations access
542
+ include_tags: Whether to include account tags
543
+
544
+ Returns:
545
+ List of OrganizationAccount objects
546
+ """
547
+ client = get_unified_organizations_client(management_profile)
548
+ return await client.get_organization_accounts(include_tags)
549
+
550
+
551
+ async def get_organization_structure(
552
+ management_profile: Optional[str] = None,
553
+ include_tags: bool = False
554
+ ) -> Dict:
555
+ """
556
+ Convenience function to get complete organization structure.
557
+
558
+ This function provides backward compatibility with existing inventory module.
559
+
560
+ Args:
561
+ management_profile: AWS profile with Organizations access
562
+ include_tags: Whether to include account tags
563
+
564
+ Returns:
565
+ Complete organization structure dictionary
566
+ """
567
+ client = get_unified_organizations_client(management_profile)
568
+ return await client.get_complete_organization_structure(include_tags)
569
+
570
+
571
+ # Export public interface
572
+ __all__ = [
573
+ 'UnifiedOrganizationsClient',
574
+ 'OrganizationAccount',
575
+ 'OrganizationalUnit',
576
+ 'get_unified_organizations_client',
577
+ 'get_organization_accounts',
578
+ 'get_organization_structure',
579
+ ]