reverse-diagrams 1.3.4__py3-none-any.whl → 1.3.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.
@@ -0,0 +1,376 @@
1
+ """AWS Organizations plugin for generating organizational structure diagrams."""
2
+ import logging
3
+ from typing import Dict, Any, List
4
+ from pathlib import Path
5
+
6
+ from src.plugins.base import AWSServicePlugin, PluginMetadata
7
+ from src.aws.client_manager import AWSClientManager
8
+ from src.models import DiagramConfig
9
+ from src.utils.concurrent import get_concurrent_processor
10
+ from src.utils.progress import get_progress_tracker
11
+ from src.dgms.graph_mapper import create_mapper, find_ou_name
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class OrganizationsPlugin(AWSServicePlugin):
17
+ """Plugin for AWS Organizations service diagram generation."""
18
+
19
+ @property
20
+ def metadata(self) -> PluginMetadata:
21
+ """Get plugin metadata."""
22
+ return PluginMetadata(
23
+ name="organizations",
24
+ version="1.0.0",
25
+ description="Generate diagrams for AWS Organizations structure, accounts, and OUs",
26
+ author="Reverse Diagrams Team",
27
+ aws_services=["organizations"],
28
+ dependencies=[]
29
+ )
30
+
31
+ def collect_data(self, client_manager: AWSClientManager, region: str, **kwargs) -> Dict[str, Any]:
32
+ """
33
+ Collect AWS Organizations data.
34
+
35
+ Args:
36
+ client_manager: AWS client manager
37
+ region: AWS region
38
+ **kwargs: Additional parameters
39
+
40
+ Returns:
41
+ Dictionary containing Organizations data
42
+ """
43
+ logger.debug(f"Collecting AWS Organizations data from region {region}")
44
+ progress = get_progress_tracker()
45
+
46
+ data = {
47
+ "region": region,
48
+ "organization": {},
49
+ "roots": [],
50
+ "organizational_units": [],
51
+ "accounts": [],
52
+ "organizations_complete": {}
53
+ }
54
+
55
+ try:
56
+ # Get organization details
57
+ progress.show_success("🏢 Getting Organization Info")
58
+ org_response = client_manager.call_api("organizations", "describe_organization")
59
+ data["organization"] = org_response.get("Organization", {})
60
+ logger.debug(f"Retrieved organization: {data['organization'].get('Id', 'Unknown')}")
61
+
62
+ # Get roots
63
+ roots_response = client_manager.call_api("organizations", "list_roots")
64
+ data["roots"] = roots_response.get("Roots", [])
65
+ logger.debug(f"Found {len(data['roots'])} organization roots")
66
+
67
+ if not data["roots"]:
68
+ raise ValueError("No organization roots found")
69
+
70
+ root_id = data["roots"][0]["Id"]
71
+
72
+ # Get organizational units recursively
73
+ progress.show_success("📋 Listing Organizational Units")
74
+ data["organizational_units"] = self._list_organizational_units_recursive(
75
+ client_manager, root_id, region
76
+ )
77
+ logger.debug(f"Found {len(data['organizational_units'])} organizational units")
78
+
79
+ # Get accounts with parent information
80
+ progress.show_success("👥 Getting Account Information")
81
+ data["accounts"] = self._list_accounts_with_parents(client_manager, region)
82
+ logger.debug(f"Found {len(data['accounts'])} accounts")
83
+
84
+ # Create complete organization map
85
+ data["organizations_complete"] = self._create_organization_complete_map(
86
+ root_id,
87
+ data["organization"],
88
+ data["organizational_units"],
89
+ data["accounts"]
90
+ )
91
+
92
+ progress.show_summary(
93
+ "Organization Summary",
94
+ [
95
+ f"Organization ID: {data['organization'].get('Id', 'Unknown')}",
96
+ f"Master Account: {data['organization'].get('MasterAccountId', 'Unknown')}",
97
+ f"Organizational Units: {len(data['organizational_units'])}",
98
+ f"Accounts: {len(data['accounts'])}"
99
+ ]
100
+ )
101
+
102
+ except Exception as e:
103
+ logger.error(f"Failed to collect Organizations data: {e}")
104
+ raise
105
+
106
+ return data
107
+
108
+ def _list_organizational_units_recursive(
109
+ self,
110
+ client_manager: AWSClientManager,
111
+ parent_id: str,
112
+ region: str,
113
+ depth: int = 0
114
+ ) -> List[Dict[str, Any]]:
115
+ """
116
+ List organizational units recursively.
117
+
118
+ Args:
119
+ client_manager: AWS client manager
120
+ parent_id: Parent ID to start from
121
+ region: AWS region
122
+ depth: Current recursion depth
123
+
124
+ Returns:
125
+ List of all organizational units
126
+ """
127
+ if depth > 10: # Prevent infinite recursion
128
+ logger.warning(f"Maximum recursion depth reached for parent {parent_id}")
129
+ return []
130
+
131
+ all_ous = []
132
+
133
+ try:
134
+ # Get OUs for current parent using pagination
135
+ ous = client_manager.paginate_api_call(
136
+ "organizations",
137
+ "list_organizational_units_for_parent",
138
+ "OrganizationalUnits",
139
+ ParentId=parent_id
140
+ )
141
+
142
+ for ou in ous:
143
+ # Add parent information
144
+ try:
145
+ parents_response = client_manager.call_api(
146
+ "organizations",
147
+ "list_parents",
148
+ ChildId=ou["Id"]
149
+ )
150
+ ou["Parents"] = parents_response.get("Parents", [])
151
+ except Exception as e:
152
+ logger.warning(f"Failed to get parents for OU {ou['Id']}: {e}")
153
+ ou["Parents"] = []
154
+
155
+ all_ous.append(ou)
156
+
157
+ # Recursively get child OUs
158
+ child_ous = self._list_organizational_units_recursive(
159
+ client_manager, ou["Id"], region, depth + 1
160
+ )
161
+ all_ous.extend(child_ous)
162
+
163
+ except Exception as e:
164
+ logger.warning(f"Failed to get OUs for parent {parent_id}: {e}")
165
+
166
+ return all_ous
167
+
168
+ def _list_accounts_with_parents(
169
+ self,
170
+ client_manager: AWSClientManager,
171
+ region: str
172
+ ) -> List[Dict[str, Any]]:
173
+ """
174
+ List all accounts with parent information.
175
+
176
+ Args:
177
+ client_manager: AWS client manager
178
+ region: AWS region
179
+
180
+ Returns:
181
+ List of accounts with parent information
182
+ """
183
+ progress = get_progress_tracker()
184
+
185
+ # Get all accounts using pagination
186
+ accounts = client_manager.paginate_api_call(
187
+ "organizations",
188
+ "list_accounts",
189
+ "Accounts"
190
+ )
191
+
192
+ logger.debug(f"Found {len(accounts)} accounts in organization")
193
+
194
+ # Add parent information for each account
195
+ indexed_accounts = []
196
+
197
+ with progress.track_operation(f"Getting parent info for {len(accounts)} accounts", total=len(accounts)) as task_id:
198
+ for i, account in enumerate(accounts):
199
+ try:
200
+ parents_response = client_manager.call_api(
201
+ "organizations",
202
+ "list_parents",
203
+ ChildId=account["Id"]
204
+ )
205
+
206
+ indexed_accounts.append({
207
+ "account": account["Id"],
208
+ "name": account["Name"],
209
+ "email": account.get("Email", ""),
210
+ "status": account.get("Status", "ACTIVE"),
211
+ "parents": parents_response.get("Parents", [])
212
+ })
213
+
214
+ progress.update_progress(task_id)
215
+
216
+ except Exception as e:
217
+ logger.warning(f"Failed to get parents for account {account['Id']}: {e}")
218
+ indexed_accounts.append({
219
+ "account": account["Id"],
220
+ "name": account["Name"],
221
+ "email": account.get("Email", ""),
222
+ "status": account.get("Status", "ACTIVE"),
223
+ "parents": []
224
+ })
225
+
226
+ return indexed_accounts
227
+
228
+ def _create_organization_complete_map(
229
+ self,
230
+ root_id: str,
231
+ organization: Dict[str, Any],
232
+ organizational_units: List[Dict[str, Any]],
233
+ accounts: List[Dict[str, Any]]
234
+ ) -> Dict[str, Any]:
235
+ """
236
+ Create a complete organization map with nested structure.
237
+
238
+ Args:
239
+ root_id: Organization root ID
240
+ organization: Organization details
241
+ organizational_units: List of OUs
242
+ accounts: List of accounts
243
+
244
+ Returns:
245
+ Complete organization structure
246
+ """
247
+ organizations_complete = {
248
+ "rootId": root_id,
249
+ "masterAccountId": organization.get("MasterAccountId", ""),
250
+ "noOutAccounts": [],
251
+ "organizationalUnits": {},
252
+ }
253
+
254
+ # Build OU hierarchy
255
+ for ou in organizational_units:
256
+ for parent in ou.get("Parents", []):
257
+ if parent["Type"] == "ROOT":
258
+ organizations_complete["organizationalUnits"][ou["Name"]] = {
259
+ "Id": ou["Id"],
260
+ "Name": ou["Name"],
261
+ "accounts": {},
262
+ "nestedOus": {},
263
+ }
264
+
265
+ # Add accounts to appropriate OUs or root
266
+ for account in accounts:
267
+ for parent in account.get("parents", []):
268
+ if parent["Type"] == "ROOT":
269
+ organizations_complete["noOutAccounts"].append({
270
+ "account": account["account"],
271
+ "name": account["name"]
272
+ })
273
+ elif parent["Type"] == "ORGANIZATIONAL_UNIT":
274
+ # Find the OU name
275
+ ou_name = find_ou_name(organizational_units, parent["Id"])
276
+ if ou_name and ou_name in organizations_complete["organizationalUnits"]:
277
+ organizations_complete["organizationalUnits"][ou_name]["accounts"][account["name"]] = {
278
+ "account": account["account"],
279
+ "name": account["name"]
280
+ }
281
+
282
+ return organizations_complete
283
+
284
+ def generate_diagram_code(self, data: Dict[str, Any], config: DiagramConfig) -> str:
285
+ """
286
+ Generate diagram code for AWS Organizations.
287
+
288
+ Args:
289
+ data: Organizations data collected from AWS
290
+ config: Diagram configuration
291
+
292
+ Returns:
293
+ Python code for generating Organizations diagram
294
+ """
295
+ logger.debug("Generating AWS Organizations diagram code")
296
+
297
+ from src.dgms.graph_template import graph_template
298
+
299
+ # Start with the template
300
+ code_lines = [
301
+ "from diagrams import Diagram, Cluster",
302
+ "from diagrams.aws.management import Organizations, OrganizationsAccount, OrganizationsOrganizationalUnit",
303
+ "",
304
+ f'with Diagram("{config.title}", show=False, direction="{config.direction}"):'
305
+ ]
306
+
307
+ # Add organization root
308
+ organization = data.get("organization", {})
309
+ roots = data.get("roots", [])
310
+ organizational_units = data.get("organizational_units", [])
311
+ accounts = data.get("accounts", [])
312
+
313
+ if roots:
314
+ root_id = roots[0]["Id"]
315
+ code_lines.extend([
316
+ " with Cluster('Organizations'):",
317
+ f" oo = Organizations('{organization.get('Id', 'Unknown')}\\n{organization.get('MasterAccountId', 'Unknown')}\\n{root_id}')"
318
+ ])
319
+
320
+ # Add organizational units
321
+ for ou in organizational_units:
322
+ ou_name_safe = self._format_name_for_code(ou["Name"])
323
+ code_lines.append(
324
+ f" ou_{ou_name_safe} = OrganizationsOrganizationalUnit(\"{ou['Id']}\\n{ou['Name']}\")"
325
+ )
326
+
327
+ # Add relationships
328
+ for parent in ou.get("Parents", []):
329
+ if parent["Type"] == "ROOT":
330
+ code_lines.append(f" oo >> ou_{ou_name_safe}")
331
+ elif parent["Type"] == "ORGANIZATIONAL_UNIT":
332
+ parent_name = find_ou_name(organizational_units, parent["Id"])
333
+ if parent_name:
334
+ parent_name_safe = self._format_name_for_code(parent_name)
335
+ code_lines.append(f" ou_{parent_name_safe} >> ou_{ou_name_safe}")
336
+
337
+ # Add accounts
338
+ for account in accounts:
339
+ account_name_safe = self._format_name_for_code(account["name"])
340
+ for parent in account.get("parents", []):
341
+ if parent["Type"] == "ROOT":
342
+ code_lines.append(
343
+ f" oo >> OrganizationsAccount(\"{account['account']}\\n{self._split_long_name(account['name'])}\")"
344
+ )
345
+ elif parent["Type"] == "ORGANIZATIONAL_UNIT":
346
+ ou_name = find_ou_name(organizational_units, parent["Id"])
347
+ if ou_name:
348
+ ou_name_safe = self._format_name_for_code(ou_name)
349
+ code_lines.append(
350
+ f" ou_{ou_name_safe} >> OrganizationsAccount(\"{account['account']}\\n{self._split_long_name(account['name'])}\")"
351
+ )
352
+
353
+ return "\n".join(code_lines)
354
+
355
+ def _format_name_for_code(self, name: str) -> str:
356
+ """Format name for use in Python code."""
357
+ import re
358
+ # Remove special characters and spaces
359
+ formatted = re.sub(r'[^\w]', '', name)
360
+ return formatted if formatted else "Unknown"
361
+
362
+ def _split_long_name(self, name: str) -> str:
363
+ """Split long names for better display."""
364
+ if len(name) > 17:
365
+ return name[:16] + "\\n" + name[16:]
366
+ return name
367
+
368
+ def get_required_permissions(self) -> List[str]:
369
+ """Get required AWS permissions for Organizations plugin."""
370
+ return [
371
+ "organizations:DescribeOrganization",
372
+ "organizations:ListRoots",
373
+ "organizations:ListOrganizationalUnitsForParent",
374
+ "organizations:ListAccounts",
375
+ "organizations:ListParents"
376
+ ]
@@ -0,0 +1,126 @@
1
+ """Plugin registry and discovery system."""
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import Type, List, Optional
5
+ import os
6
+
7
+ from .base import AWSServicePlugin, PluginManager, discover_plugins_in_directory
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Global plugin manager instance
12
+ _plugin_manager: Optional[PluginManager] = None
13
+
14
+
15
+ def get_plugin_manager() -> PluginManager:
16
+ """Get or create global plugin manager instance."""
17
+ global _plugin_manager
18
+ if _plugin_manager is None:
19
+ _plugin_manager = PluginManager()
20
+ # Auto-discover built-in plugins
21
+ discover_plugins()
22
+ return _plugin_manager
23
+
24
+
25
+ def register_plugin(plugin_class: Type[AWSServicePlugin]) -> None:
26
+ """
27
+ Register a plugin class with the global manager.
28
+
29
+ Args:
30
+ plugin_class: Plugin class to register
31
+ """
32
+ manager = get_plugin_manager()
33
+ manager.register_plugin(plugin_class)
34
+
35
+
36
+ def discover_plugins() -> List[Type[AWSServicePlugin]]:
37
+ """
38
+ Discover and register plugins from standard locations.
39
+
40
+ Returns:
41
+ List of discovered plugin classes
42
+ """
43
+ manager = get_plugin_manager()
44
+ all_discovered = []
45
+
46
+ # Standard plugin directories
47
+ plugin_directories = [
48
+ Path(__file__).parent / "builtin", # Built-in plugins
49
+ Path.home() / ".reverse_diagrams" / "plugins", # User plugins
50
+ Path.cwd() / "plugins", # Local plugins
51
+ ]
52
+
53
+ # Add plugins from environment variable
54
+ env_plugin_dirs = os.environ.get("REVERSE_DIAGRAMS_PLUGIN_DIRS", "")
55
+ if env_plugin_dirs:
56
+ for dir_path in env_plugin_dirs.split(":"):
57
+ if dir_path.strip():
58
+ plugin_directories.append(Path(dir_path.strip()))
59
+
60
+ # Discover plugins in each directory
61
+ for plugin_dir in plugin_directories:
62
+ try:
63
+ discovered = discover_plugins_in_directory(plugin_dir)
64
+ for plugin_class in discovered:
65
+ manager.register_plugin(plugin_class)
66
+ all_discovered.append(plugin_class)
67
+
68
+ except Exception as e:
69
+ logger.warning(f"Failed to discover plugins in {plugin_dir}: {e}")
70
+
71
+ logger.debug(f"Discovered and registered {len(all_discovered)} plugins")
72
+ return all_discovered
73
+
74
+
75
+ def list_available_plugins() -> List[str]:
76
+ """
77
+ List names of all available plugins.
78
+
79
+ Returns:
80
+ List of plugin names
81
+ """
82
+ manager = get_plugin_manager()
83
+ metadata_list = manager.list_available_plugins()
84
+ return [metadata.name for metadata in metadata_list]
85
+
86
+
87
+ def get_plugin_info(plugin_name: str) -> Optional[dict]:
88
+ """
89
+ Get information about a specific plugin.
90
+
91
+ Args:
92
+ plugin_name: Name of the plugin
93
+
94
+ Returns:
95
+ Plugin information dictionary or None if not found
96
+ """
97
+ manager = get_plugin_manager()
98
+
99
+ # Try to get from loaded plugins first
100
+ plugin = manager.get_plugin(plugin_name)
101
+ if plugin:
102
+ metadata = plugin.metadata
103
+ return {
104
+ "name": metadata.name,
105
+ "version": metadata.version,
106
+ "description": metadata.description,
107
+ "author": metadata.author,
108
+ "aws_services": metadata.aws_services,
109
+ "dependencies": metadata.dependencies,
110
+ "status": "loaded"
111
+ }
112
+
113
+ # Check available plugins
114
+ for metadata in manager.list_available_plugins():
115
+ if metadata.name == plugin_name:
116
+ return {
117
+ "name": metadata.name,
118
+ "version": metadata.version,
119
+ "description": metadata.description,
120
+ "author": metadata.author,
121
+ "aws_services": metadata.aws_services,
122
+ "dependencies": metadata.dependencies,
123
+ "status": "available"
124
+ }
125
+
126
+ return None
@@ -23,15 +23,38 @@ def get_members(group):
23
23
  """
24
24
  Get members name.
25
25
 
26
- :param group:
27
- :return:
26
+ :param group: Can be either a list of group dicts or a dict with numeric keys
27
+ :return: Dictionary mapping group names to member lists
28
28
  """
29
29
  members_groups = {}
30
- for m in group:
31
- members_groups[group[m]["group_name"]] = []
32
- for mm in group[m]["members"]:
33
- members_groups[group[m]["group_name"]].append(mm["MemberId"]["UserName"])
34
-
30
+
31
+ # Handle new format (list of dicts with group_id, group_name, members)
32
+ if isinstance(group, list):
33
+ for g in group:
34
+ group_name = g.get("group_name", "Unknown Group")
35
+ members_groups[group_name] = []
36
+ for member in g.get("members", []):
37
+ # Handle both old and new member formats
38
+ if isinstance(member, dict):
39
+ if "MemberId" in member and "UserName" in member["MemberId"]:
40
+ members_groups[group_name].append(member["MemberId"]["UserName"])
41
+ elif "UserName" in member:
42
+ members_groups[group_name].append(member["UserName"])
43
+
44
+ # Handle old format (dict with numeric keys)
45
+ elif isinstance(group, dict):
46
+ for key in group:
47
+ g = group[key]
48
+ if isinstance(g, dict) and "group_name" in g:
49
+ group_name = g["group_name"]
50
+ members_groups[group_name] = []
51
+ for member in g.get("members", []):
52
+ if isinstance(member, dict):
53
+ if "MemberId" in member and "UserName" in member["MemberId"]:
54
+ members_groups[group_name].append(member["MemberId"]["UserName"])
55
+ elif "UserName" in member:
56
+ members_groups[group_name].append(member["UserName"])
57
+
35
58
  return members_groups
36
59
 
37
60
 
@@ -53,17 +76,17 @@ def create_group_console_view(groups):
53
76
  """
54
77
  Create tree.
55
78
 
56
- :param groups:
79
+ :param groups: Can be either a list of group dicts or a dict with numeric keys
57
80
  :return:
58
81
  """
59
82
  members = get_members(groups)
60
83
  console = Console()
61
84
  c = [
62
85
  Panel(
63
- f"[b][green]{group}[/b]\n[yellow]{pretty_members(members[group])}",
86
+ f"[b][green]{group_name}[/b]\n[yellow]{pretty_members(members[group_name])}",
64
87
  expand=True,
65
88
  )
66
- for group in groups
89
+ for group_name in members.keys()
67
90
  ]
68
91
  console.print(Columns(c))
69
92
 
@@ -227,12 +250,27 @@ def watch_on_demand(
227
250
  :param args:
228
251
  :return:
229
252
  """
230
- if args.watch_graph_organization:
231
- # create_console_view(file_path=f"{diagrams_path}/json/organizations.json")
232
- print("Not available jet")
233
- if args.watch_graph_accounts_assignments:
234
- assign = load_json(args.watch_graph_accounts_assignments)
235
- create_account_assignments_view(assign=assign)
236
- if args.watch_graph_identity:
237
- c = load_json(args.watch_graph_identity)
238
- create_group_console_view(groups=c)
253
+ try:
254
+ if args.watch_graph_organization:
255
+ # create_console_view(file_path=f"{diagrams_path}/json/organizations.json")
256
+ print("Not available yet")
257
+ if args.watch_graph_accounts_assignments:
258
+ assign = load_json(args.watch_graph_accounts_assignments)
259
+ create_account_assignments_view(assign=assign)
260
+ if args.watch_graph_identity:
261
+ c = load_json(args.watch_graph_identity)
262
+ create_group_console_view(groups=c)
263
+ except FileNotFoundError as e:
264
+ print(f"❌ Error: File not found - {e}")
265
+ print("💡 Make sure you've generated the diagrams first using:")
266
+ print(" reverse_diagrams -o -i -p <profile> -r <region>")
267
+ except json.JSONDecodeError as e:
268
+ print(f"❌ Error: Invalid JSON file - {e}")
269
+ print("💡 The file may be corrupted. Try regenerating it.")
270
+ except KeyError as e:
271
+ print(f"❌ Error: Missing expected data in file - {e}")
272
+ print("💡 The file format may be outdated. Try regenerating it.")
273
+ except Exception as e:
274
+ print(f"❌ Unexpected error: {e}")
275
+ import traceback
276
+ traceback.print_exc()