runbooks 0.9.9__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +1 -1
- runbooks/cfat/WEIGHT_CONFIG_README.md +368 -0
- runbooks/cfat/app.ts +27 -19
- runbooks/cfat/assessment/runner.py +6 -5
- runbooks/cfat/cloud_foundations_assessment.py +626 -0
- runbooks/cfat/tests/test_weight_configuration.ts +449 -0
- runbooks/cfat/weight_config.ts +574 -0
- runbooks/cloudops/cost_optimizer.py +95 -33
- runbooks/common/__init__.py +26 -9
- runbooks/common/aws_pricing.py +1353 -0
- runbooks/common/aws_pricing_api.py +205 -0
- runbooks/common/aws_utils.py +2 -2
- runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
- runbooks/common/cross_account_manager.py +606 -0
- runbooks/common/date_utils.py +115 -0
- runbooks/common/enhanced_exception_handler.py +14 -7
- runbooks/common/env_utils.py +96 -0
- runbooks/common/mcp_cost_explorer_integration.py +5 -4
- runbooks/common/mcp_integration.py +49 -2
- runbooks/common/organizations_client.py +579 -0
- runbooks/common/profile_utils.py +127 -72
- runbooks/common/rich_utils.py +3 -3
- runbooks/finops/cost_optimizer.py +2 -1
- runbooks/finops/dashboard_runner.py +47 -28
- runbooks/finops/ebs_optimizer.py +56 -9
- runbooks/finops/elastic_ip_optimizer.py +13 -9
- runbooks/finops/embedded_mcp_validator.py +31 -0
- runbooks/finops/enhanced_trend_visualization.py +10 -4
- runbooks/finops/finops_dashboard.py +6 -5
- runbooks/finops/iam_guidance.py +6 -1
- runbooks/finops/markdown_exporter.py +217 -2
- runbooks/finops/nat_gateway_optimizer.py +76 -20
- runbooks/finops/tests/test_integration.py +3 -1
- runbooks/finops/vpc_cleanup_exporter.py +28 -26
- runbooks/finops/vpc_cleanup_optimizer.py +363 -16
- runbooks/inventory/__init__.py +10 -1
- runbooks/inventory/cloud_foundations_integration.py +409 -0
- runbooks/inventory/core/collector.py +1177 -94
- runbooks/inventory/discovery.md +339 -0
- runbooks/inventory/drift_detection_cli.py +327 -0
- runbooks/inventory/inventory_mcp_cli.py +171 -0
- runbooks/inventory/inventory_modules.py +6 -9
- runbooks/inventory/list_ec2_instances.py +3 -3
- runbooks/inventory/mcp_inventory_validator.py +2149 -0
- runbooks/inventory/mcp_vpc_validator.py +23 -6
- runbooks/inventory/organizations_discovery.py +104 -9
- runbooks/inventory/rich_inventory_display.py +129 -1
- runbooks/inventory/unified_validation_engine.py +1279 -0
- runbooks/inventory/verify_ec2_security_groups.py +3 -1
- runbooks/inventory/vpc_analyzer.py +825 -7
- runbooks/inventory/vpc_flow_analyzer.py +36 -42
- runbooks/main.py +708 -47
- runbooks/monitoring/performance_monitor.py +11 -7
- runbooks/operate/base.py +9 -6
- runbooks/operate/deployment_framework.py +5 -4
- runbooks/operate/deployment_validator.py +6 -5
- runbooks/operate/dynamodb_operations.py +6 -5
- runbooks/operate/ec2_operations.py +3 -2
- runbooks/operate/mcp_integration.py +6 -5
- runbooks/operate/networking_cost_heatmap.py +21 -16
- runbooks/operate/s3_operations.py +13 -12
- runbooks/operate/vpc_operations.py +100 -12
- runbooks/remediation/base.py +4 -2
- runbooks/remediation/commons.py +5 -5
- runbooks/remediation/commvault_ec2_analysis.py +68 -15
- runbooks/remediation/config/accounts_example.json +31 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
- runbooks/remediation/multi_account.py +120 -7
- runbooks/remediation/rds_snapshot_list.py +5 -3
- runbooks/remediation/remediation_cli.py +710 -0
- runbooks/remediation/universal_account_discovery.py +377 -0
- runbooks/security/compliance_automation_engine.py +99 -20
- runbooks/security/config/__init__.py +24 -0
- runbooks/security/config/compliance_config.py +255 -0
- runbooks/security/config/compliance_weights_example.json +22 -0
- runbooks/security/config_template_generator.py +500 -0
- runbooks/security/security_cli.py +377 -0
- runbooks/validation/__init__.py +21 -1
- runbooks/validation/cli.py +8 -7
- runbooks/validation/comprehensive_2way_validator.py +2007 -0
- runbooks/validation/mcp_validator.py +965 -101
- runbooks/validation/terraform_citations_validator.py +363 -0
- runbooks/validation/terraform_drift_detector.py +1098 -0
- runbooks/vpc/cleanup_wrapper.py +231 -10
- runbooks/vpc/config.py +346 -73
- runbooks/vpc/cross_account_session.py +312 -0
- runbooks/vpc/heatmap_engine.py +115 -41
- runbooks/vpc/manager_interface.py +9 -9
- runbooks/vpc/mcp_no_eni_validator.py +1630 -0
- runbooks/vpc/networking_wrapper.py +14 -8
- runbooks/vpc/runbooks_adapter.py +33 -12
- runbooks/vpc/tests/conftest.py +4 -2
- runbooks/vpc/tests/test_cost_engine.py +4 -2
- runbooks/vpc/unified_scenarios.py +73 -3
- runbooks/vpc/vpc_cleanup_integration.py +512 -78
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/METADATA +94 -52
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/RECORD +101 -81
- runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/finops/runbooks.security.report_generator.log +0 -0
- runbooks/finops/runbooks.security.run_script.log +0 -0
- runbooks/finops/runbooks.security.security_export.log +0 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
- runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
- runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/runbooks.security.security_export.log +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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
|
+
]
|