runbooks 1.1.3__py3-none-any.whl → 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
- runbooks/cfat/assessment/compliance.py +8 -8
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cfat/models.py +6 -2
- runbooks/cfat/tests/__init__.py +6 -1
- runbooks/cli/__init__.py +13 -0
- runbooks/cli/commands/cfat.py +274 -0
- runbooks/cli/commands/finops.py +1164 -0
- runbooks/cli/commands/inventory.py +379 -0
- runbooks/cli/commands/operate.py +239 -0
- runbooks/cli/commands/security.py +248 -0
- runbooks/cli/commands/validation.py +825 -0
- runbooks/cli/commands/vpc.py +310 -0
- runbooks/cli/registry.py +107 -0
- runbooks/cloudops/__init__.py +23 -30
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +549 -547
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +226 -227
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +179 -215
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +341 -0
- runbooks/common/aws_utils.py +75 -80
- runbooks/common/business_logic.py +127 -105
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
- runbooks/common/cross_account_manager.py +198 -205
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +235 -0
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +478 -495
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +176 -194
- runbooks/common/patterns.py +204 -0
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +248 -39
- runbooks/common/rich_utils.py +643 -92
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +29 -33
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +488 -622
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +40 -37
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +230 -292
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +338 -175
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1513 -482
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +25 -29
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +77 -78
- runbooks/finops/scenarios.py +1278 -439
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/tests/test_finops_dashboard.py +3 -3
- runbooks/finops/tests/test_reference_images_validation.py +2 -2
- runbooks/finops/tests/test_single_account_features.py +17 -17
- runbooks/finops/tests/validate_test_suite.py +1 -1
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +263 -269
- runbooks/finops/vpc_cleanup_exporter.py +191 -146
- runbooks/finops/vpc_cleanup_optimizer.py +593 -575
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/hitl/enhanced_workflow_engine.py +1 -1
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/README.md +3 -3
- runbooks/inventory/Tests/common_test_data.py +30 -30
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +28 -11
- runbooks/inventory/collectors/aws_networking.py +111 -101
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/discovery.md +2 -2
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/find_ec2_security_groups.py +1 -1
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +56 -52
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +733 -696
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +3 -3
- runbooks/main.py +152 -9147
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/metrics/dora_metrics_engine.py +2 -2
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/mcp_integration.py +1 -1
- runbooks/operate/networking_cost_heatmap.py +33 -10
- runbooks/operate/privatelink_operations.py +1 -1
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_endpoints.py +1 -1
- runbooks/operate/vpc_operations.py +648 -618
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +71 -67
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +91 -65
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +49 -44
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/integration_test_enterprise_security.py +5 -3
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/run_script.py +1 -1
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/mcp_reliability_engine.py +6 -6
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +51 -48
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +754 -708
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +190 -162
- runbooks/vpc/mcp_no_eni_validator.py +681 -640
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1302 -1129
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -956
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.3.dist-info/METADATA +0 -799
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@ Features:
|
|
14
14
|
- Thread-safe operations with concurrent access support
|
15
15
|
|
16
16
|
Author: CloudOps Runbooks Team
|
17
|
-
Version:
|
17
|
+
Version: latest version
|
18
18
|
"""
|
19
19
|
|
20
20
|
import asyncio
|
@@ -40,21 +40,23 @@ from runbooks.common.rich_utils import (
|
|
40
40
|
|
41
41
|
# Global Organizations cache shared across all instances and modules
|
42
42
|
_GLOBAL_ORGS_CACHE = {
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
43
|
+
"data": None,
|
44
|
+
"accounts": None,
|
45
|
+
"organizational_units": None,
|
46
|
+
"timestamp": None,
|
47
|
+
"ttl_minutes": 30,
|
48
48
|
}
|
49
49
|
|
50
50
|
# Thread lock for cache operations
|
51
51
|
import threading
|
52
|
+
|
52
53
|
_cache_lock = threading.Lock()
|
53
54
|
|
54
55
|
|
55
56
|
@dataclass
|
56
57
|
class OrganizationAccount:
|
57
58
|
"""Standard organization account representation across all modules"""
|
59
|
+
|
58
60
|
account_id: str
|
59
61
|
name: str
|
60
62
|
email: str
|
@@ -77,6 +79,7 @@ class OrganizationAccount:
|
|
77
79
|
@dataclass
|
78
80
|
class OrganizationalUnit:
|
79
81
|
"""Standard organizational unit representation"""
|
82
|
+
|
80
83
|
ou_id: str
|
81
84
|
name: str
|
82
85
|
parent_id: Optional[str] = None
|
@@ -93,20 +96,15 @@ class OrganizationalUnit:
|
|
93
96
|
class UnifiedOrganizationsClient:
|
94
97
|
"""
|
95
98
|
Unified Organizations API client consolidating patterns from all modules.
|
96
|
-
|
99
|
+
|
97
100
|
This client provides a single interface for Organizations API operations
|
98
101
|
with global caching, error handling, and performance optimization.
|
99
102
|
"""
|
100
103
|
|
101
|
-
def __init__(
|
102
|
-
self,
|
103
|
-
management_profile: Optional[str] = None,
|
104
|
-
cache_ttl_minutes: int = 30,
|
105
|
-
max_workers: int = 50
|
106
|
-
):
|
104
|
+
def __init__(self, management_profile: Optional[str] = None, cache_ttl_minutes: int = 30, max_workers: int = 50):
|
107
105
|
"""
|
108
106
|
Initialize unified Organizations client.
|
109
|
-
|
107
|
+
|
110
108
|
Args:
|
111
109
|
management_profile: AWS profile with Organizations access
|
112
110
|
cache_ttl_minutes: Cache TTL in minutes (default: 30)
|
@@ -115,18 +113,18 @@ class UnifiedOrganizationsClient:
|
|
115
113
|
self.management_profile = management_profile
|
116
114
|
self.cache_ttl_minutes = cache_ttl_minutes
|
117
115
|
self.max_workers = max_workers
|
118
|
-
|
116
|
+
|
119
117
|
# Initialize session
|
120
118
|
self.session = None
|
121
119
|
self.client = None
|
122
|
-
|
120
|
+
|
123
121
|
# Performance metrics
|
124
122
|
self.metrics = {
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
123
|
+
"api_calls_made": 0,
|
124
|
+
"cache_hits": 0,
|
125
|
+
"cache_misses": 0,
|
126
|
+
"errors_encountered": 0,
|
127
|
+
"last_refresh": None,
|
130
128
|
}
|
131
129
|
|
132
130
|
def _initialize_client(self) -> bool:
|
@@ -138,28 +136,28 @@ class UnifiedOrganizationsClient:
|
|
138
136
|
# Use profile resolution from existing patterns
|
139
137
|
profile = get_profile_for_operation("management", None)
|
140
138
|
self.session = boto3.Session(profile_name=profile)
|
141
|
-
|
139
|
+
|
142
140
|
# Organizations is a global service - always use us-east-1
|
143
|
-
self.client = self.session.client(
|
144
|
-
|
141
|
+
self.client = self.session.client("organizations", region_name="us-east-1")
|
142
|
+
|
145
143
|
# Test connectivity
|
146
144
|
self.client.describe_organization()
|
147
145
|
return True
|
148
|
-
|
146
|
+
|
149
147
|
except ClientError as e:
|
150
|
-
error_code = e.response.get(
|
151
|
-
if error_code ==
|
148
|
+
error_code = e.response.get("Error", {}).get("Code", "")
|
149
|
+
if error_code == "AccessDeniedException":
|
152
150
|
print_warning(f"Organizations access denied for profile '{self.management_profile}'")
|
153
|
-
elif error_code ==
|
151
|
+
elif error_code == "AWSOrganizationsNotInUseException":
|
154
152
|
print_info("AWS Organizations not enabled for this account")
|
155
153
|
else:
|
156
154
|
print_warning(f"Organizations API error: {error_code}")
|
157
155
|
return False
|
158
|
-
|
156
|
+
|
159
157
|
except NoCredentialsError:
|
160
158
|
print_warning("AWS credentials not available for Organizations API")
|
161
159
|
return False
|
162
|
-
|
160
|
+
|
163
161
|
except Exception as e:
|
164
162
|
print_error(f"Failed to initialize Organizations client: {e}")
|
165
163
|
return False
|
@@ -167,39 +165,37 @@ class UnifiedOrganizationsClient:
|
|
167
165
|
def _is_cache_valid(self) -> bool:
|
168
166
|
"""Check if global cache is still valid"""
|
169
167
|
with _cache_lock:
|
170
|
-
if not _GLOBAL_ORGS_CACHE[
|
168
|
+
if not _GLOBAL_ORGS_CACHE["timestamp"]:
|
171
169
|
return False
|
172
|
-
|
173
|
-
cache_age_minutes = (
|
174
|
-
|
175
|
-
).total_seconds() / 60
|
176
|
-
|
170
|
+
|
171
|
+
cache_age_minutes = (datetime.now(timezone.utc) - _GLOBAL_ORGS_CACHE["timestamp"]).total_seconds() / 60
|
172
|
+
|
177
173
|
return cache_age_minutes < self.cache_ttl_minutes
|
178
174
|
|
179
175
|
def _get_cached_data(self, data_type: str) -> Optional[any]:
|
180
176
|
"""Get specific cached data type"""
|
181
177
|
if self._is_cache_valid():
|
182
178
|
with _cache_lock:
|
183
|
-
self.metrics[
|
184
|
-
if data_type ==
|
185
|
-
return _GLOBAL_ORGS_CACHE.get(
|
186
|
-
elif data_type ==
|
187
|
-
return _GLOBAL_ORGS_CACHE.get(
|
188
|
-
elif data_type ==
|
189
|
-
return _GLOBAL_ORGS_CACHE.get(
|
190
|
-
|
191
|
-
self.metrics[
|
179
|
+
self.metrics["cache_hits"] += 1
|
180
|
+
if data_type == "accounts":
|
181
|
+
return _GLOBAL_ORGS_CACHE.get("accounts")
|
182
|
+
elif data_type == "organizational_units":
|
183
|
+
return _GLOBAL_ORGS_CACHE.get("organizational_units")
|
184
|
+
elif data_type == "complete":
|
185
|
+
return _GLOBAL_ORGS_CACHE.get("data")
|
186
|
+
|
187
|
+
self.metrics["cache_misses"] += 1
|
192
188
|
return None
|
193
189
|
|
194
190
|
def _set_cached_data(self, accounts: List[OrganizationAccount], ous: List[OrganizationalUnit], complete_data: Dict):
|
195
191
|
"""Set cached data with thread safety"""
|
196
192
|
with _cache_lock:
|
197
|
-
_GLOBAL_ORGS_CACHE[
|
198
|
-
_GLOBAL_ORGS_CACHE[
|
199
|
-
_GLOBAL_ORGS_CACHE[
|
200
|
-
_GLOBAL_ORGS_CACHE[
|
201
|
-
self.metrics[
|
202
|
-
|
193
|
+
_GLOBAL_ORGS_CACHE["accounts"] = accounts
|
194
|
+
_GLOBAL_ORGS_CACHE["organizational_units"] = ous
|
195
|
+
_GLOBAL_ORGS_CACHE["data"] = complete_data
|
196
|
+
_GLOBAL_ORGS_CACHE["timestamp"] = datetime.now(timezone.utc)
|
197
|
+
self.metrics["last_refresh"] = datetime.now(timezone.utc)
|
198
|
+
|
203
199
|
accounts_count = len(accounts) if accounts else 0
|
204
200
|
ous_count = len(ous) if ous else 0
|
205
201
|
print_success(f"✅ Organizations cache updated: {accounts_count} accounts, {ous_count} OUs")
|
@@ -207,15 +203,15 @@ class UnifiedOrganizationsClient:
|
|
207
203
|
async def get_organization_accounts(self, include_tags: bool = False) -> List[OrganizationAccount]:
|
208
204
|
"""
|
209
205
|
Get all organization accounts with caching support.
|
210
|
-
|
206
|
+
|
211
207
|
Args:
|
212
208
|
include_tags: Whether to include account tags (slower but more comprehensive)
|
213
|
-
|
209
|
+
|
214
210
|
Returns:
|
215
211
|
List of OrganizationAccount objects
|
216
212
|
"""
|
217
213
|
# Check cache first
|
218
|
-
cached_accounts = self._get_cached_data(
|
214
|
+
cached_accounts = self._get_cached_data("accounts")
|
219
215
|
if cached_accounts:
|
220
216
|
print_info(f"🚀 Using cached account data ({len(cached_accounts)} accounts)")
|
221
217
|
return cached_accounts
|
@@ -233,36 +229,32 @@ class UnifiedOrganizationsClient:
|
|
233
229
|
task = progress.add_task("Discovering accounts...", total=None)
|
234
230
|
|
235
231
|
# Get accounts using paginator for large organizations
|
236
|
-
paginator = self.client.get_paginator(
|
237
|
-
|
232
|
+
paginator = self.client.get_paginator("list_accounts")
|
233
|
+
|
238
234
|
for page in paginator.paginate():
|
239
|
-
for account_data in page[
|
235
|
+
for account_data in page["Accounts"]:
|
240
236
|
account = OrganizationAccount(
|
241
|
-
account_id=account_data[
|
242
|
-
name=account_data[
|
243
|
-
email=account_data[
|
244
|
-
status=account_data[
|
245
|
-
joined_method=account_data[
|
246
|
-
joined_timestamp=account_data[
|
237
|
+
account_id=account_data["Id"],
|
238
|
+
name=account_data["Name"],
|
239
|
+
email=account_data["Email"],
|
240
|
+
status=account_data["Status"],
|
241
|
+
joined_method=account_data["JoinedMethod"],
|
242
|
+
joined_timestamp=account_data["JoinedTimestamp"],
|
247
243
|
)
|
248
244
|
|
249
245
|
# Get account tags if requested
|
250
246
|
if include_tags:
|
251
247
|
try:
|
252
|
-
tags_response = self.client.list_tags_for_resource(
|
253
|
-
|
254
|
-
|
255
|
-
account.tags = {
|
256
|
-
tag['Key']: tag['Value'] for tag in tags_response['Tags']
|
257
|
-
}
|
258
|
-
self.metrics['api_calls_made'] += 1
|
248
|
+
tags_response = self.client.list_tags_for_resource(ResourceId=account.account_id)
|
249
|
+
account.tags = {tag["Key"]: tag["Value"] for tag in tags_response["Tags"]}
|
250
|
+
self.metrics["api_calls_made"] += 1
|
259
251
|
except ClientError:
|
260
252
|
# Tags may not be accessible for all accounts
|
261
253
|
account.tags = {}
|
262
254
|
|
263
255
|
accounts.append(account)
|
264
|
-
|
265
|
-
self.metrics[
|
256
|
+
|
257
|
+
self.metrics["api_calls_made"] += 1
|
266
258
|
progress.update(task, description=f"Found {len(accounts)} accounts...")
|
267
259
|
|
268
260
|
# Map accounts to OUs
|
@@ -272,19 +264,19 @@ class UnifiedOrganizationsClient:
|
|
272
264
|
return accounts
|
273
265
|
|
274
266
|
except Exception as e:
|
275
|
-
self.metrics[
|
267
|
+
self.metrics["errors_encountered"] += 1
|
276
268
|
print_error(f"Failed to discover organization accounts: {e}")
|
277
269
|
return []
|
278
270
|
|
279
271
|
async def get_organizational_units(self) -> List[OrganizationalUnit]:
|
280
272
|
"""
|
281
273
|
Get all organizational units with caching support.
|
282
|
-
|
274
|
+
|
283
275
|
Returns:
|
284
276
|
List of OrganizationalUnit objects
|
285
277
|
"""
|
286
278
|
# Check cache first
|
287
|
-
cached_ous = self._get_cached_data(
|
279
|
+
cached_ous = self._get_cached_data("organizational_units")
|
288
280
|
if cached_ous:
|
289
281
|
print_info(f"🚀 Using cached OU data ({len(cached_ous)} OUs)")
|
290
282
|
return cached_ous
|
@@ -300,47 +292,43 @@ class UnifiedOrganizationsClient:
|
|
300
292
|
try:
|
301
293
|
# Get root OU
|
302
294
|
roots_response = self.client.list_roots()
|
303
|
-
if not roots_response.get(
|
295
|
+
if not roots_response.get("Roots"):
|
304
296
|
print_warning("No root organizational units found")
|
305
297
|
return []
|
306
298
|
|
307
|
-
root_id = roots_response[
|
308
|
-
self.metrics[
|
299
|
+
root_id = roots_response["Roots"][0]["Id"]
|
300
|
+
self.metrics["api_calls_made"] += 1
|
309
301
|
|
310
302
|
# Recursively discover all OUs
|
311
303
|
await self._discover_ou_recursive(root_id, all_ous)
|
312
|
-
|
304
|
+
|
313
305
|
print_success(f"✅ Discovered {len(all_ous)} organizational units")
|
314
306
|
return all_ous
|
315
307
|
|
316
308
|
except Exception as e:
|
317
|
-
self.metrics[
|
309
|
+
self.metrics["errors_encountered"] += 1
|
318
310
|
print_error(f"Failed to discover organizational units: {e}")
|
319
311
|
return []
|
320
312
|
|
321
313
|
async def _discover_ou_recursive(self, parent_id: str, ou_list: List[OrganizationalUnit]):
|
322
314
|
"""Recursively discover organizational units"""
|
323
315
|
try:
|
324
|
-
paginator = self.client.get_paginator(
|
325
|
-
|
316
|
+
paginator = self.client.get_paginator("list_organizational_units_for_parent")
|
317
|
+
|
326
318
|
for page in paginator.paginate(ParentId=parent_id):
|
327
|
-
for ou_data in page[
|
328
|
-
ou = OrganizationalUnit(
|
329
|
-
|
330
|
-
name=ou_data['Name'],
|
331
|
-
parent_id=parent_id
|
332
|
-
)
|
333
|
-
|
319
|
+
for ou_data in page["OrganizationalUnits"]:
|
320
|
+
ou = OrganizationalUnit(ou_id=ou_data["Id"], name=ou_data["Name"], parent_id=parent_id)
|
321
|
+
|
334
322
|
ou_list.append(ou)
|
335
|
-
|
323
|
+
|
336
324
|
# Recursively discover child OUs
|
337
325
|
await self._discover_ou_recursive(ou.ou_id, ou_list)
|
338
|
-
|
339
|
-
self.metrics[
|
326
|
+
|
327
|
+
self.metrics["api_calls_made"] += 1
|
340
328
|
|
341
329
|
except ClientError as e:
|
342
330
|
print_warning(f"Failed to discover OU children for {parent_id}: {e}")
|
343
|
-
self.metrics[
|
331
|
+
self.metrics["errors_encountered"] += 1
|
344
332
|
|
345
333
|
async def _map_accounts_to_ous(self, accounts: List[OrganizationAccount]):
|
346
334
|
"""Map accounts to their organizational units"""
|
@@ -348,182 +336,180 @@ class UnifiedOrganizationsClient:
|
|
348
336
|
return
|
349
337
|
|
350
338
|
print_info("🗺️ Mapping accounts to organizational units...")
|
351
|
-
|
339
|
+
|
352
340
|
with create_progress_bar() as progress:
|
353
341
|
task = progress.add_task("Mapping accounts to OUs...", total=len(accounts))
|
354
|
-
|
342
|
+
|
355
343
|
for account in accounts:
|
356
344
|
try:
|
357
345
|
parents_response = self.client.list_parents(ChildId=account.account_id)
|
358
|
-
|
359
|
-
if parents_response[
|
360
|
-
parent = parents_response[
|
361
|
-
account.parent_id = parent[
|
362
|
-
|
346
|
+
|
347
|
+
if parents_response["Parents"]:
|
348
|
+
parent = parents_response["Parents"][0]
|
349
|
+
account.parent_id = parent["Id"]
|
350
|
+
|
363
351
|
# Get OU name if parent is an OU
|
364
|
-
if parent[
|
352
|
+
if parent["Type"] == "ORGANIZATIONAL_UNIT":
|
365
353
|
try:
|
366
354
|
ou_response = self.client.describe_organizational_unit(
|
367
|
-
OrganizationalUnitId=parent[
|
355
|
+
OrganizationalUnitId=parent["Id"]
|
368
356
|
)
|
369
|
-
account.organizational_unit = ou_response[
|
370
|
-
self.metrics[
|
357
|
+
account.organizational_unit = ou_response["OrganizationalUnit"]["Name"]
|
358
|
+
self.metrics["api_calls_made"] += 1
|
371
359
|
except ClientError:
|
372
360
|
account.organizational_unit = f"OU-{parent['Id']}"
|
373
|
-
|
374
|
-
self.metrics[
|
375
|
-
|
361
|
+
|
362
|
+
self.metrics["api_calls_made"] += 1
|
363
|
+
|
376
364
|
except ClientError:
|
377
365
|
# Continue with other accounts
|
378
|
-
self.metrics[
|
379
|
-
|
366
|
+
self.metrics["errors_encountered"] += 1
|
367
|
+
|
380
368
|
progress.advance(task)
|
381
369
|
|
382
370
|
async def get_complete_organization_structure(self, include_tags: bool = False) -> Dict:
|
383
371
|
"""
|
384
372
|
Get complete organization structure with caching.
|
385
|
-
|
373
|
+
|
386
374
|
This method provides compatibility with existing inventory module patterns.
|
387
|
-
|
375
|
+
|
388
376
|
Args:
|
389
377
|
include_tags: Whether to include account tags
|
390
|
-
|
378
|
+
|
391
379
|
Returns:
|
392
380
|
Complete organization structure dictionary
|
393
381
|
"""
|
394
382
|
# Check for complete cached data
|
395
|
-
cached_data = self._get_cached_data(
|
383
|
+
cached_data = self._get_cached_data("complete")
|
396
384
|
if cached_data:
|
397
385
|
print_info("🚀 Using cached complete organization structure")
|
398
386
|
return cached_data
|
399
387
|
|
400
388
|
print_info("🏢 Discovering complete organization structure...")
|
401
|
-
|
389
|
+
|
402
390
|
# Get accounts and OUs
|
403
391
|
accounts = await self.get_organization_accounts(include_tags=include_tags)
|
404
392
|
ous = await self.get_organizational_units()
|
405
|
-
|
393
|
+
|
406
394
|
# Get organization info
|
407
395
|
org_info = await self._get_organization_info()
|
408
|
-
|
396
|
+
|
409
397
|
# Build complete structure
|
410
398
|
complete_data = {
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
399
|
+
"status": "completed",
|
400
|
+
"discovery_type": "unified_organizations_api",
|
401
|
+
"organization_info": org_info,
|
402
|
+
"accounts": {
|
403
|
+
"total_accounts": len(accounts),
|
404
|
+
"active_accounts": len([a for a in accounts if a.status == "ACTIVE"]),
|
405
|
+
"discovered_accounts": [a.to_dict() for a in accounts],
|
406
|
+
"discovery_method": "organizations_api",
|
419
407
|
},
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
408
|
+
"organizational_units": {
|
409
|
+
"total_ous": len(ous),
|
410
|
+
"organizational_units": [asdict(ou) for ou in ous],
|
411
|
+
"discovery_method": "organizations_api",
|
424
412
|
},
|
425
|
-
|
426
|
-
|
413
|
+
"metrics": self.metrics.copy(),
|
414
|
+
"timestamp": datetime.now().isoformat(),
|
427
415
|
}
|
428
|
-
|
416
|
+
|
429
417
|
# Cache the complete structure
|
430
418
|
self._set_cached_data(accounts, ous, complete_data)
|
431
|
-
|
419
|
+
|
432
420
|
return complete_data
|
433
421
|
|
434
422
|
async def _get_organization_info(self) -> Dict:
|
435
423
|
"""Get high-level organization information"""
|
436
424
|
if not self.client:
|
437
425
|
return {
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
426
|
+
"organization_id": "unavailable",
|
427
|
+
"master_account_id": "unavailable",
|
428
|
+
"master_account_email": "unavailable",
|
429
|
+
"feature_set": "unavailable",
|
430
|
+
"available_policy_types": [],
|
431
|
+
"discovery_method": "unavailable",
|
444
432
|
}
|
445
433
|
|
446
434
|
try:
|
447
435
|
org_response = self.client.describe_organization()
|
448
|
-
org = org_response[
|
449
|
-
self.metrics[
|
450
|
-
|
436
|
+
org = org_response["Organization"]
|
437
|
+
self.metrics["api_calls_made"] += 1
|
438
|
+
|
451
439
|
return {
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
440
|
+
"organization_id": org["Id"],
|
441
|
+
"master_account_id": org["MasterAccountId"],
|
442
|
+
"master_account_email": org["MasterAccountEmail"],
|
443
|
+
"feature_set": org["FeatureSet"],
|
444
|
+
"available_policy_types": [pt["Type"] for pt in org.get("AvailablePolicyTypes", [])],
|
445
|
+
"discovery_method": "organizations_api",
|
458
446
|
}
|
459
|
-
|
447
|
+
|
460
448
|
except ClientError as e:
|
461
449
|
print_warning(f"Failed to get organization info: {e}")
|
462
450
|
return {
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
451
|
+
"organization_id": "error",
|
452
|
+
"master_account_id": "error",
|
453
|
+
"master_account_email": "error",
|
454
|
+
"feature_set": "error",
|
455
|
+
"available_policy_types": [],
|
456
|
+
"discovery_method": "failed",
|
457
|
+
"error": str(e),
|
470
458
|
}
|
471
459
|
|
472
460
|
def get_account_name_mapping(self) -> Dict[str, str]:
|
473
461
|
"""
|
474
462
|
Get account ID to name mapping for compatibility with FinOps module.
|
475
|
-
|
463
|
+
|
476
464
|
Returns:
|
477
465
|
Dictionary mapping account IDs to account names
|
478
466
|
"""
|
479
|
-
cached_accounts = self._get_cached_data(
|
467
|
+
cached_accounts = self._get_cached_data("accounts")
|
480
468
|
if not cached_accounts:
|
481
469
|
# Try to refresh cache
|
482
470
|
import asyncio
|
471
|
+
|
483
472
|
try:
|
484
|
-
cached_accounts = asyncio.get_event_loop().run_until_complete(
|
485
|
-
self.get_organization_accounts()
|
486
|
-
)
|
473
|
+
cached_accounts = asyncio.get_event_loop().run_until_complete(self.get_organization_accounts())
|
487
474
|
except:
|
488
475
|
return {}
|
489
|
-
|
476
|
+
|
490
477
|
return {account.account_id: account.name for account in cached_accounts}
|
491
478
|
|
492
479
|
def invalidate_cache(self):
|
493
480
|
"""Manually invalidate the global cache"""
|
494
481
|
with _cache_lock:
|
495
|
-
_GLOBAL_ORGS_CACHE[
|
496
|
-
_GLOBAL_ORGS_CACHE[
|
497
|
-
_GLOBAL_ORGS_CACHE[
|
498
|
-
_GLOBAL_ORGS_CACHE[
|
499
|
-
|
482
|
+
_GLOBAL_ORGS_CACHE["data"] = None
|
483
|
+
_GLOBAL_ORGS_CACHE["accounts"] = None
|
484
|
+
_GLOBAL_ORGS_CACHE["organizational_units"] = None
|
485
|
+
_GLOBAL_ORGS_CACHE["timestamp"] = None
|
486
|
+
|
500
487
|
print_info("🗑️ Organizations cache invalidated")
|
501
488
|
|
502
489
|
def get_cache_status(self) -> Dict:
|
503
490
|
"""Get cache status and metrics"""
|
504
491
|
with _cache_lock:
|
505
492
|
return {
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
493
|
+
"cache_valid": self._is_cache_valid(),
|
494
|
+
"cache_timestamp": _GLOBAL_ORGS_CACHE.get("timestamp"),
|
495
|
+
"ttl_minutes": self.cache_ttl_minutes,
|
496
|
+
"metrics": self.metrics.copy(),
|
497
|
+
"accounts_cached": len(_GLOBAL_ORGS_CACHE.get("accounts", [])),
|
498
|
+
"ous_cached": len(_GLOBAL_ORGS_CACHE.get("organizational_units", [])),
|
512
499
|
}
|
513
500
|
|
514
501
|
|
515
502
|
# Factory functions for easy integration with existing modules
|
516
503
|
def get_unified_organizations_client(
|
517
|
-
management_profile: Optional[str] = None,
|
518
|
-
cache_ttl_minutes: int = 30
|
504
|
+
management_profile: Optional[str] = None, cache_ttl_minutes: int = 30
|
519
505
|
) -> UnifiedOrganizationsClient:
|
520
506
|
"""
|
521
507
|
Factory function to get unified Organizations client.
|
522
|
-
|
508
|
+
|
523
509
|
Args:
|
524
510
|
management_profile: AWS profile with Organizations access
|
525
511
|
cache_ttl_minutes: Cache TTL in minutes
|
526
|
-
|
512
|
+
|
527
513
|
Returns:
|
528
514
|
UnifiedOrganizationsClient instance
|
529
515
|
"""
|
@@ -531,16 +517,15 @@ def get_unified_organizations_client(
|
|
531
517
|
|
532
518
|
|
533
519
|
async def get_organization_accounts(
|
534
|
-
management_profile: Optional[str] = None,
|
535
|
-
include_tags: bool = False
|
520
|
+
management_profile: Optional[str] = None, include_tags: bool = False
|
536
521
|
) -> List[OrganizationAccount]:
|
537
522
|
"""
|
538
523
|
Convenience function to get organization accounts.
|
539
|
-
|
524
|
+
|
540
525
|
Args:
|
541
526
|
management_profile: AWS profile with Organizations access
|
542
527
|
include_tags: Whether to include account tags
|
543
|
-
|
528
|
+
|
544
529
|
Returns:
|
545
530
|
List of OrganizationAccount objects
|
546
531
|
"""
|
@@ -548,19 +533,16 @@ async def get_organization_accounts(
|
|
548
533
|
return await client.get_organization_accounts(include_tags)
|
549
534
|
|
550
535
|
|
551
|
-
async def get_organization_structure(
|
552
|
-
management_profile: Optional[str] = None,
|
553
|
-
include_tags: bool = False
|
554
|
-
) -> Dict:
|
536
|
+
async def get_organization_structure(management_profile: Optional[str] = None, include_tags: bool = False) -> Dict:
|
555
537
|
"""
|
556
538
|
Convenience function to get complete organization structure.
|
557
|
-
|
539
|
+
|
558
540
|
This function provides backward compatibility with existing inventory module.
|
559
|
-
|
541
|
+
|
560
542
|
Args:
|
561
543
|
management_profile: AWS profile with Organizations access
|
562
544
|
include_tags: Whether to include account tags
|
563
|
-
|
545
|
+
|
564
546
|
Returns:
|
565
547
|
Complete organization structure dictionary
|
566
548
|
"""
|
@@ -570,10 +552,10 @@ async def get_organization_structure(
|
|
570
552
|
|
571
553
|
# Export public interface
|
572
554
|
__all__ = [
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
]
|
555
|
+
"UnifiedOrganizationsClient",
|
556
|
+
"OrganizationAccount",
|
557
|
+
"OrganizationalUnit",
|
558
|
+
"get_unified_organizations_client",
|
559
|
+
"get_organization_accounts",
|
560
|
+
"get_organization_structure",
|
561
|
+
]
|