runbooks 0.7.0__py3-none-any.whl → 0.7.6__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 +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/__init__.py +1 -1
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/collectors/__init__.py +8 -0
- runbooks/inventory/collectors/aws_management.py +791 -0
- runbooks/inventory/collectors/aws_networking.py +3 -3
- runbooks/main.py +3389 -782
- runbooks/operate/__init__.py +207 -0
- runbooks/operate/base.py +311 -0
- runbooks/operate/cloudformation_operations.py +619 -0
- runbooks/operate/cloudwatch_operations.py +496 -0
- runbooks/operate/dynamodb_operations.py +812 -0
- runbooks/operate/ec2_operations.py +926 -0
- runbooks/operate/iam_operations.py +569 -0
- runbooks/operate/s3_operations.py +1211 -0
- runbooks/operate/tagging_operations.py +655 -0
- runbooks/remediation/CLAUDE.md +100 -0
- runbooks/remediation/DOME9.md +218 -0
- runbooks/remediation/README.md +26 -0
- runbooks/remediation/Tests/__init__.py +0 -0
- runbooks/remediation/Tests/update_policy.py +74 -0
- runbooks/remediation/__init__.py +95 -0
- runbooks/remediation/acm_cert_expired_unused.py +98 -0
- runbooks/remediation/acm_remediation.py +875 -0
- runbooks/remediation/api_gateway_list.py +167 -0
- runbooks/remediation/base.py +643 -0
- runbooks/remediation/cloudtrail_remediation.py +908 -0
- runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
- runbooks/remediation/cognito_active_users.py +78 -0
- runbooks/remediation/cognito_remediation.py +856 -0
- runbooks/remediation/cognito_user_password_reset.py +163 -0
- runbooks/remediation/commons.py +455 -0
- runbooks/remediation/dynamodb_optimize.py +155 -0
- runbooks/remediation/dynamodb_remediation.py +744 -0
- runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
- runbooks/remediation/ec2_public_ips.py +134 -0
- runbooks/remediation/ec2_remediation.py +892 -0
- runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
- runbooks/remediation/ec2_unused_security_groups.py +202 -0
- runbooks/remediation/kms_enable_key_rotation.py +651 -0
- runbooks/remediation/kms_remediation.py +717 -0
- runbooks/remediation/lambda_list.py +243 -0
- runbooks/remediation/lambda_remediation.py +971 -0
- runbooks/remediation/multi_account.py +569 -0
- runbooks/remediation/rds_instance_list.py +199 -0
- runbooks/remediation/rds_remediation.py +873 -0
- runbooks/remediation/rds_snapshot_list.py +192 -0
- runbooks/remediation/requirements.txt +118 -0
- runbooks/remediation/s3_block_public_access.py +159 -0
- runbooks/remediation/s3_bucket_public_access.py +143 -0
- runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
- runbooks/remediation/s3_downloader.py +215 -0
- runbooks/remediation/s3_enable_access_logging.py +562 -0
- runbooks/remediation/s3_encryption.py +526 -0
- runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
- runbooks/remediation/s3_list.py +141 -0
- runbooks/remediation/s3_object_search.py +201 -0
- runbooks/remediation/s3_remediation.py +816 -0
- runbooks/remediation/scan_for_phrase.py +425 -0
- runbooks/remediation/workspaces_list.py +220 -0
- runbooks/security/__init__.py +9 -10
- runbooks/security/security_baseline_tester.py +4 -2
- runbooks-0.7.6.dist-info/METADATA +608 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
- jupyter-agent/.env +0 -2
- jupyter-agent/.env.template +0 -2
- jupyter-agent/.gitattributes +0 -35
- jupyter-agent/.gradio/certificate.pem +0 -31
- jupyter-agent/README.md +0 -16
- jupyter-agent/__main__.log +0 -8
- jupyter-agent/app.py +0 -256
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +0 -154
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +0 -123
- jupyter-agent/requirements.txt +0 -9
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
- jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
- jupyter-agent/utils.py +0 -409
- runbooks/aws/__init__.py +0 -58
- runbooks/aws/dynamodb_operations.py +0 -231
- runbooks/aws/ec2_copy_image_cross-region.py +0 -195
- runbooks/aws/ec2_describe_instances.py +0 -202
- runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
- runbooks/aws/ec2_run_instances.py +0 -213
- runbooks/aws/ec2_start_stop_instances.py +0 -212
- runbooks/aws/ec2_terminate_instances.py +0 -143
- runbooks/aws/ec2_unused_eips.py +0 -196
- runbooks/aws/ec2_unused_volumes.py +0 -188
- runbooks/aws/s3_create_bucket.py +0 -142
- runbooks/aws/s3_list_buckets.py +0 -152
- runbooks/aws/s3_list_objects.py +0 -156
- runbooks/aws/s3_object_operations.py +0 -183
- runbooks/aws/tagging_lambda_handler.py +0 -183
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +0 -1526
- runbooks/inventory/delete_s3_buckets_objects.py +0 -169
- runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
- runbooks/inventory/update_aws_actions.py +0 -173
- runbooks/inventory/update_cfn_stacksets.py +0 -1215
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
- runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
- runbooks/inventory/update_s3_public_access_block.py +0 -539
- runbooks/organizations/__init__.py +0 -12
- runbooks/organizations/manager.py +0 -374
- runbooks-0.7.0.dist-info/METADATA +0 -375
- /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/setup.py +0 -0
- /runbooks/inventory/{tests → Tests}/src.py +0 -0
- /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
- /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
- /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
- /runbooks/{aws → operate}/tags.json +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,374 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Organizational Unit (OU) management for AWS Organizations.
|
3
|
-
|
4
|
-
This module provides capabilities for setting up and managing
|
5
|
-
AWS Organizations structure following Cloud Foundations best practices.
|
6
|
-
"""
|
7
|
-
|
8
|
-
from pathlib import Path
|
9
|
-
from typing import Any, Dict, List, Optional, Union
|
10
|
-
|
11
|
-
import yaml
|
12
|
-
from loguru import logger
|
13
|
-
|
14
|
-
from runbooks.base import CloudFoundationsBase, ProgressTracker
|
15
|
-
from runbooks.config import RunbooksConfig
|
16
|
-
|
17
|
-
|
18
|
-
class OUManager(CloudFoundationsBase):
|
19
|
-
"""
|
20
|
-
Manager for AWS Organizations OU structure.
|
21
|
-
|
22
|
-
Provides capabilities to create and manage organizational unit
|
23
|
-
structures based on Cloud Foundations templates.
|
24
|
-
"""
|
25
|
-
|
26
|
-
def __init__(
|
27
|
-
self, profile: Optional[str] = None, region: Optional[str] = None, config: Optional[RunbooksConfig] = None
|
28
|
-
):
|
29
|
-
"""Initialize OU manager."""
|
30
|
-
super().__init__(profile, region, config)
|
31
|
-
self._org_client = None
|
32
|
-
|
33
|
-
@property
|
34
|
-
def org_client(self):
|
35
|
-
"""Get AWS Organizations client."""
|
36
|
-
if self._org_client is None:
|
37
|
-
self._org_client = self.get_client("organizations")
|
38
|
-
return self._org_client
|
39
|
-
|
40
|
-
def get_template_structure(self, template: str) -> Dict[str, Any]:
|
41
|
-
"""
|
42
|
-
Get predefined OU structure template.
|
43
|
-
|
44
|
-
Args:
|
45
|
-
template: Template name ('standard', 'security', 'custom')
|
46
|
-
|
47
|
-
Returns:
|
48
|
-
OU structure definition
|
49
|
-
"""
|
50
|
-
templates = {
|
51
|
-
"standard": {
|
52
|
-
"name": "Standard OU Structure",
|
53
|
-
"description": "Standard Cloud Foundations OU structure",
|
54
|
-
"organizational_units": [
|
55
|
-
{
|
56
|
-
"name": "Core",
|
57
|
-
"description": "Core organizational units for foundational services",
|
58
|
-
"children": [
|
59
|
-
{
|
60
|
-
"name": "Log Archive",
|
61
|
-
"description": "Centralized logging account",
|
62
|
-
"policies": ["LogArchivePolicy"],
|
63
|
-
},
|
64
|
-
{
|
65
|
-
"name": "Audit",
|
66
|
-
"description": "Security and compliance auditing",
|
67
|
-
"policies": ["AuditPolicy"],
|
68
|
-
},
|
69
|
-
{
|
70
|
-
"name": "Shared Services",
|
71
|
-
"description": "Shared infrastructure services",
|
72
|
-
"policies": ["SharedServicesPolicy"],
|
73
|
-
},
|
74
|
-
],
|
75
|
-
},
|
76
|
-
{
|
77
|
-
"name": "Production",
|
78
|
-
"description": "Production workload accounts",
|
79
|
-
"children": [
|
80
|
-
{
|
81
|
-
"name": "Prod-WebApps",
|
82
|
-
"description": "Production web applications",
|
83
|
-
"policies": ["ProductionPolicy"],
|
84
|
-
},
|
85
|
-
{
|
86
|
-
"name": "Prod-Data",
|
87
|
-
"description": "Production data services",
|
88
|
-
"policies": ["ProductionPolicy", "DataPolicy"],
|
89
|
-
},
|
90
|
-
],
|
91
|
-
},
|
92
|
-
{
|
93
|
-
"name": "Non-Production",
|
94
|
-
"description": "Development and testing accounts",
|
95
|
-
"children": [
|
96
|
-
{
|
97
|
-
"name": "Development",
|
98
|
-
"description": "Development environments",
|
99
|
-
"policies": ["DevelopmentPolicy"],
|
100
|
-
},
|
101
|
-
{
|
102
|
-
"name": "Testing",
|
103
|
-
"description": "Testing and staging environments",
|
104
|
-
"policies": ["TestingPolicy"],
|
105
|
-
},
|
106
|
-
],
|
107
|
-
},
|
108
|
-
],
|
109
|
-
},
|
110
|
-
"security": {
|
111
|
-
"name": "Security-Focused OU Structure",
|
112
|
-
"description": "Enhanced security OU structure with additional controls",
|
113
|
-
"organizational_units": [
|
114
|
-
{
|
115
|
-
"name": "Security",
|
116
|
-
"description": "Security and compliance organizational unit",
|
117
|
-
"children": [
|
118
|
-
{
|
119
|
-
"name": "Security-Prod",
|
120
|
-
"description": "Production security tools",
|
121
|
-
"policies": ["SecurityProdPolicy"],
|
122
|
-
},
|
123
|
-
{
|
124
|
-
"name": "Security-NonProd",
|
125
|
-
"description": "Non-production security tools",
|
126
|
-
"policies": ["SecurityNonProdPolicy"],
|
127
|
-
},
|
128
|
-
{
|
129
|
-
"name": "Log Archive",
|
130
|
-
"description": "Centralized security logging",
|
131
|
-
"policies": ["LogArchivePolicy", "SecurityLogPolicy"],
|
132
|
-
},
|
133
|
-
{
|
134
|
-
"name": "Audit",
|
135
|
-
"description": "Security auditing and compliance",
|
136
|
-
"policies": ["AuditPolicy", "CompliancePolicy"],
|
137
|
-
},
|
138
|
-
],
|
139
|
-
},
|
140
|
-
{
|
141
|
-
"name": "Workloads",
|
142
|
-
"description": "Application workload accounts",
|
143
|
-
"children": [
|
144
|
-
{
|
145
|
-
"name": "Prod-HighSecurity",
|
146
|
-
"description": "High security production workloads",
|
147
|
-
"policies": ["HighSecurityPolicy", "ProductionPolicy"],
|
148
|
-
},
|
149
|
-
{
|
150
|
-
"name": "Prod-Standard",
|
151
|
-
"description": "Standard production workloads",
|
152
|
-
"policies": ["StandardSecurityPolicy", "ProductionPolicy"],
|
153
|
-
},
|
154
|
-
{
|
155
|
-
"name": "NonProd",
|
156
|
-
"description": "Non-production workloads",
|
157
|
-
"policies": ["NonProdPolicy"],
|
158
|
-
},
|
159
|
-
],
|
160
|
-
},
|
161
|
-
],
|
162
|
-
},
|
163
|
-
}
|
164
|
-
|
165
|
-
if template not in templates:
|
166
|
-
raise ValueError(f"Unknown template: {template}. Available: {list(templates.keys())}")
|
167
|
-
|
168
|
-
logger.info(f"Using OU structure template: {template}")
|
169
|
-
return templates[template]
|
170
|
-
|
171
|
-
def load_structure_from_file(self, file_path: Union[str, Path]) -> Dict[str, Any]:
|
172
|
-
"""
|
173
|
-
Load OU structure from YAML file.
|
174
|
-
|
175
|
-
Args:
|
176
|
-
file_path: Path to YAML structure file
|
177
|
-
|
178
|
-
Returns:
|
179
|
-
OU structure definition
|
180
|
-
"""
|
181
|
-
config_path = Path(file_path)
|
182
|
-
|
183
|
-
if not config_path.exists():
|
184
|
-
raise FileNotFoundError(f"Structure file not found: {config_path}")
|
185
|
-
|
186
|
-
try:
|
187
|
-
with open(config_path, "r", encoding="utf-8") as f:
|
188
|
-
structure = yaml.safe_load(f)
|
189
|
-
|
190
|
-
logger.info(f"Loaded OU structure from: {config_path}")
|
191
|
-
return structure
|
192
|
-
|
193
|
-
except Exception as e:
|
194
|
-
logger.error(f"Failed to load structure file: {e}")
|
195
|
-
raise
|
196
|
-
|
197
|
-
def create_ou_structure(self, structure: Dict[str, Any]) -> Dict[str, Any]:
|
198
|
-
"""
|
199
|
-
Create OU structure in AWS Organizations.
|
200
|
-
|
201
|
-
Args:
|
202
|
-
structure: OU structure definition
|
203
|
-
|
204
|
-
Returns:
|
205
|
-
Creation results with OU IDs and status
|
206
|
-
"""
|
207
|
-
logger.info(f"Creating OU structure: {structure.get('name', 'Unnamed')}")
|
208
|
-
|
209
|
-
try:
|
210
|
-
# Get organization root
|
211
|
-
root_id = self._get_organization_root()
|
212
|
-
|
213
|
-
# Create OUs
|
214
|
-
results = {"structure_name": structure.get("name"), "root_id": root_id, "created_ous": [], "errors": []}
|
215
|
-
|
216
|
-
organizational_units = structure.get("organizational_units", [])
|
217
|
-
progress = ProgressTracker(len(organizational_units), "Creating organizational units")
|
218
|
-
|
219
|
-
for ou_def in organizational_units:
|
220
|
-
try:
|
221
|
-
ou_result = self._create_ou_recursive(ou_def, root_id)
|
222
|
-
results["created_ous"].append(ou_result)
|
223
|
-
progress.update(status=f"Created {ou_def['name']}")
|
224
|
-
|
225
|
-
except Exception as e:
|
226
|
-
error_msg = f"Failed to create OU {ou_def['name']}: {e}"
|
227
|
-
logger.error(error_msg)
|
228
|
-
results["errors"].append(error_msg)
|
229
|
-
progress.update(status=f"Failed {ou_def['name']}")
|
230
|
-
|
231
|
-
progress.complete()
|
232
|
-
|
233
|
-
logger.info(f"OU structure creation completed. Created {len(results['created_ous'])} OUs")
|
234
|
-
return results
|
235
|
-
|
236
|
-
except Exception as e:
|
237
|
-
logger.error(f"OU structure creation failed: {e}")
|
238
|
-
raise
|
239
|
-
|
240
|
-
def _get_organization_root(self) -> str:
|
241
|
-
"""Get the organization root ID."""
|
242
|
-
try:
|
243
|
-
response = self._make_aws_call(self.org_client.list_roots)
|
244
|
-
|
245
|
-
if not response.get("Roots"):
|
246
|
-
raise Exception("No organization roots found")
|
247
|
-
|
248
|
-
root_id = response["Roots"][0]["Id"]
|
249
|
-
logger.debug(f"Found organization root: {root_id}")
|
250
|
-
return root_id
|
251
|
-
|
252
|
-
except Exception as e:
|
253
|
-
logger.error(f"Failed to get organization root: {e}")
|
254
|
-
raise
|
255
|
-
|
256
|
-
def _create_ou_recursive(self, ou_def: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
|
257
|
-
"""
|
258
|
-
Recursively create OU and its children.
|
259
|
-
|
260
|
-
Args:
|
261
|
-
ou_def: OU definition
|
262
|
-
parent_id: Parent OU ID
|
263
|
-
|
264
|
-
Returns:
|
265
|
-
Creation result with OU details
|
266
|
-
"""
|
267
|
-
ou_name = ou_def["name"]
|
268
|
-
ou_description = ou_def.get("description", "")
|
269
|
-
|
270
|
-
logger.info(f"Creating OU: {ou_name} under parent: {parent_id}")
|
271
|
-
|
272
|
-
# Check if OU already exists
|
273
|
-
existing_ou = self._find_existing_ou(ou_name, parent_id)
|
274
|
-
if existing_ou:
|
275
|
-
logger.info(f"OU {ou_name} already exists: {existing_ou['Id']}")
|
276
|
-
ou_id = existing_ou["Id"]
|
277
|
-
else:
|
278
|
-
# Create the OU
|
279
|
-
response = self._make_aws_call(self.org_client.create_organizational_unit, ParentId=parent_id, Name=ou_name)
|
280
|
-
|
281
|
-
ou_id = response["OrganizationalUnit"]["Id"]
|
282
|
-
logger.info(f"Created OU {ou_name}: {ou_id}")
|
283
|
-
|
284
|
-
result = {"name": ou_name, "id": ou_id, "parent_id": parent_id, "description": ou_description, "children": []}
|
285
|
-
|
286
|
-
# Create child OUs
|
287
|
-
children = ou_def.get("children", [])
|
288
|
-
for child_def in children:
|
289
|
-
try:
|
290
|
-
child_result = self._create_ou_recursive(child_def, ou_id)
|
291
|
-
result["children"].append(child_result)
|
292
|
-
except Exception as e:
|
293
|
-
logger.error(f"Failed to create child OU {child_def.get('name', 'Unknown')}: {e}")
|
294
|
-
|
295
|
-
return result
|
296
|
-
|
297
|
-
def _find_existing_ou(self, ou_name: str, parent_id: str) -> Optional[Dict[str, Any]]:
|
298
|
-
"""Find existing OU by name under a parent."""
|
299
|
-
try:
|
300
|
-
response = self._make_aws_call(self.org_client.list_organizational_units_for_parent, ParentId=parent_id)
|
301
|
-
|
302
|
-
for ou in response.get("OrganizationalUnits", []):
|
303
|
-
if ou["Name"] == ou_name:
|
304
|
-
return ou
|
305
|
-
|
306
|
-
return None
|
307
|
-
|
308
|
-
except Exception as e:
|
309
|
-
logger.warning(f"Error checking for existing OU {ou_name}: {e}")
|
310
|
-
return None
|
311
|
-
|
312
|
-
def list_organizational_units(self) -> List[Dict[str, Any]]:
|
313
|
-
"""List all organizational units in the organization."""
|
314
|
-
try:
|
315
|
-
root_id = self._get_organization_root()
|
316
|
-
all_ous = []
|
317
|
-
|
318
|
-
def collect_ous(parent_id: str, level: int = 0):
|
319
|
-
response = self._make_aws_call(self.org_client.list_organizational_units_for_parent, ParentId=parent_id)
|
320
|
-
|
321
|
-
for ou in response.get("OrganizationalUnits", []):
|
322
|
-
ou["Level"] = level
|
323
|
-
ou["ParentId"] = parent_id
|
324
|
-
all_ous.append(ou)
|
325
|
-
|
326
|
-
# Recursively collect child OUs
|
327
|
-
collect_ous(ou["Id"], level + 1)
|
328
|
-
|
329
|
-
collect_ous(root_id)
|
330
|
-
|
331
|
-
logger.info(f"Found {len(all_ous)} organizational units")
|
332
|
-
return all_ous
|
333
|
-
|
334
|
-
except Exception as e:
|
335
|
-
logger.error(f"Failed to list organizational units: {e}")
|
336
|
-
raise
|
337
|
-
|
338
|
-
def delete_ou(self, ou_id: str) -> bool:
|
339
|
-
"""
|
340
|
-
Delete an organizational unit.
|
341
|
-
|
342
|
-
Args:
|
343
|
-
ou_id: OU ID to delete
|
344
|
-
|
345
|
-
Returns:
|
346
|
-
True if successful
|
347
|
-
"""
|
348
|
-
try:
|
349
|
-
# Check if OU has any accounts
|
350
|
-
accounts_response = self._make_aws_call(self.org_client.list_accounts_for_parent, ParentId=ou_id)
|
351
|
-
|
352
|
-
if accounts_response.get("Accounts"):
|
353
|
-
raise Exception(f"Cannot delete OU {ou_id}: it contains accounts")
|
354
|
-
|
355
|
-
# Check if OU has child OUs
|
356
|
-
ous_response = self._make_aws_call(self.org_client.list_organizational_units_for_parent, ParentId=ou_id)
|
357
|
-
|
358
|
-
if ous_response.get("OrganizationalUnits"):
|
359
|
-
raise Exception(f"Cannot delete OU {ou_id}: it contains child OUs")
|
360
|
-
|
361
|
-
# Delete the OU
|
362
|
-
self._make_aws_call(self.org_client.delete_organizational_unit, OrganizationalUnitId=ou_id)
|
363
|
-
|
364
|
-
logger.info(f"Deleted OU: {ou_id}")
|
365
|
-
return True
|
366
|
-
|
367
|
-
except Exception as e:
|
368
|
-
logger.error(f"Failed to delete OU {ou_id}: {e}")
|
369
|
-
raise
|
370
|
-
|
371
|
-
def run(self):
|
372
|
-
"""Implementation of abstract base method."""
|
373
|
-
# Default operation: list current OU structure
|
374
|
-
return self.list_organizational_units()
|