reverse-diagrams 1.3.4__py3-none-any.whl → 2.0.0__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.
- reverse_diagrams-2.0.0.dist-info/METADATA +706 -0
- reverse_diagrams-2.0.0.dist-info/RECORD +35 -0
- {reverse_diagrams-1.3.4.dist-info → reverse_diagrams-2.0.0.dist-info}/WHEEL +1 -1
- src/aws/client_manager.py +217 -0
- src/aws/describe_identity_store.py +8 -0
- src/aws/describe_organization.py +324 -445
- src/aws/describe_sso.py +170 -143
- src/aws/exceptions.py +26 -0
- src/banner/banner.py +43 -40
- src/config.py +153 -0
- src/models.py +242 -0
- src/plugins/__init__.py +12 -0
- src/plugins/base.py +292 -0
- src/plugins/builtin/__init__.py +12 -0
- src/plugins/builtin/ec2_plugin.py +228 -0
- src/plugins/builtin/identity_center_plugin.py +496 -0
- src/plugins/builtin/organizations_plugin.py +376 -0
- src/plugins/registry.py +126 -0
- src/reports/console_view.py +57 -19
- src/reports/save_results.py +210 -15
- src/reverse_diagrams.py +331 -38
- src/utils/__init__.py +1 -0
- src/utils/cache.py +274 -0
- src/utils/concurrent.py +361 -0
- src/utils/progress.py +257 -0
- src/version.py +1 -1
- reverse_diagrams-1.3.4.dist-info/METADATA +0 -247
- reverse_diagrams-1.3.4.dist-info/RECORD +0 -21
- src/reports/tes.py +0 -366
- {reverse_diagrams-1.3.4.dist-info → reverse_diagrams-2.0.0.dist-info}/entry_points.txt +0 -0
- {reverse_diagrams-1.3.4.dist-info → reverse_diagrams-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""EC2 service plugin for generating EC2 infrastructure diagrams."""
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Dict, Any, List
|
|
4
|
+
|
|
5
|
+
from src.plugins.base import AWSServicePlugin, PluginMetadata
|
|
6
|
+
from src.aws.client_manager import AWSClientManager
|
|
7
|
+
from src.models import DiagramConfig
|
|
8
|
+
from src.utils.concurrent import get_concurrent_processor
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EC2Plugin(AWSServicePlugin):
|
|
14
|
+
"""Plugin for AWS EC2 service diagram generation."""
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def metadata(self) -> PluginMetadata:
|
|
18
|
+
"""Get plugin metadata."""
|
|
19
|
+
return PluginMetadata(
|
|
20
|
+
name="ec2",
|
|
21
|
+
version="1.0.0",
|
|
22
|
+
description="Generate diagrams for EC2 instances, VPCs, and related resources",
|
|
23
|
+
author="Reverse Diagrams Team",
|
|
24
|
+
aws_services=["ec2"],
|
|
25
|
+
dependencies=[]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def collect_data(self, client_manager: AWSClientManager, region: str, **kwargs) -> Dict[str, Any]:
|
|
29
|
+
"""
|
|
30
|
+
Collect EC2 data from AWS.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
client_manager: AWS client manager
|
|
34
|
+
region: AWS region
|
|
35
|
+
**kwargs: Additional parameters
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dictionary containing EC2 data
|
|
39
|
+
"""
|
|
40
|
+
logger.debug(f"Collecting EC2 data from region {region}")
|
|
41
|
+
|
|
42
|
+
data = {
|
|
43
|
+
"region": region,
|
|
44
|
+
"vpcs": [],
|
|
45
|
+
"instances": [],
|
|
46
|
+
"security_groups": [],
|
|
47
|
+
"subnets": [],
|
|
48
|
+
"internet_gateways": [],
|
|
49
|
+
"route_tables": []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Collect VPCs
|
|
54
|
+
vpcs_response = client_manager.call_api("ec2", "describe_vpcs")
|
|
55
|
+
data["vpcs"] = vpcs_response.get("Vpcs", [])
|
|
56
|
+
logger.debug(f"Found {len(data['vpcs'])} VPCs")
|
|
57
|
+
|
|
58
|
+
# Collect EC2 instances
|
|
59
|
+
instances_response = client_manager.call_api("ec2", "describe_instances")
|
|
60
|
+
instances = []
|
|
61
|
+
for reservation in instances_response.get("Reservations", []):
|
|
62
|
+
instances.extend(reservation.get("Instances", []))
|
|
63
|
+
data["instances"] = instances
|
|
64
|
+
logger.debug(f"Found {len(data['instances'])} EC2 instances")
|
|
65
|
+
|
|
66
|
+
# Collect Security Groups
|
|
67
|
+
sg_response = client_manager.call_api("ec2", "describe_security_groups")
|
|
68
|
+
data["security_groups"] = sg_response.get("SecurityGroups", [])
|
|
69
|
+
logger.debug(f"Found {len(data['security_groups'])} security groups")
|
|
70
|
+
|
|
71
|
+
# Collect Subnets
|
|
72
|
+
subnets_response = client_manager.call_api("ec2", "describe_subnets")
|
|
73
|
+
data["subnets"] = subnets_response.get("Subnets", [])
|
|
74
|
+
logger.debug(f"Found {len(data['subnets'])} subnets")
|
|
75
|
+
|
|
76
|
+
# Collect Internet Gateways
|
|
77
|
+
igw_response = client_manager.call_api("ec2", "describe_internet_gateways")
|
|
78
|
+
data["internet_gateways"] = igw_response.get("InternetGateways", [])
|
|
79
|
+
logger.debug(f"Found {len(data['internet_gateways'])} internet gateways")
|
|
80
|
+
|
|
81
|
+
# Collect Route Tables
|
|
82
|
+
rt_response = client_manager.call_api("ec2", "describe_route_tables")
|
|
83
|
+
data["route_tables"] = rt_response.get("RouteTables", [])
|
|
84
|
+
logger.debug(f"Found {len(data['route_tables'])} route tables")
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Failed to collect EC2 data: {e}")
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
return data
|
|
91
|
+
|
|
92
|
+
def generate_diagram_code(self, data: Dict[str, Any], config: DiagramConfig) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Generate diagram code for EC2 resources.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
data: EC2 data collected from AWS
|
|
98
|
+
config: Diagram configuration
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Python code for generating EC2 diagram
|
|
102
|
+
"""
|
|
103
|
+
logger.debug("Generating EC2 diagram code")
|
|
104
|
+
|
|
105
|
+
code_lines = [
|
|
106
|
+
"from diagrams import Diagram, Cluster, Edge",
|
|
107
|
+
"from diagrams.aws.compute import EC2",
|
|
108
|
+
"from diagrams.aws.network import VPC, PrivateSubnet, PublicSubnet, InternetGateway, RouteTable",
|
|
109
|
+
"from diagrams.aws.security import SecurityGroup",
|
|
110
|
+
"",
|
|
111
|
+
f'with Diagram("{config.title}", show=False, direction="{config.direction}"):'
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# Group instances by VPC
|
|
115
|
+
vpc_instances = {}
|
|
116
|
+
for instance in data.get("instances", []):
|
|
117
|
+
vpc_id = instance.get("VpcId", "no-vpc")
|
|
118
|
+
if vpc_id not in vpc_instances:
|
|
119
|
+
vpc_instances[vpc_id] = []
|
|
120
|
+
vpc_instances[vpc_id].append(instance)
|
|
121
|
+
|
|
122
|
+
# Generate VPC clusters
|
|
123
|
+
for vpc in data.get("vpcs", []):
|
|
124
|
+
vpc_id = vpc["VpcId"]
|
|
125
|
+
vpc_name = self._get_resource_name(vpc)
|
|
126
|
+
|
|
127
|
+
code_lines.append(f' with Cluster("VPC: {vpc_name}"):')
|
|
128
|
+
|
|
129
|
+
# Add instances in this VPC
|
|
130
|
+
instances_in_vpc = vpc_instances.get(vpc_id, [])
|
|
131
|
+
for i, instance in enumerate(instances_in_vpc):
|
|
132
|
+
instance_name = self._get_resource_name(instance)
|
|
133
|
+
state = instance.get("State", {}).get("Name", "unknown")
|
|
134
|
+
|
|
135
|
+
code_lines.append(
|
|
136
|
+
f' instance_{i} = EC2("{instance_name}\\n{state}")'
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Add subnets in this VPC
|
|
140
|
+
vpc_subnets = [s for s in data.get("subnets", []) if s.get("VpcId") == vpc_id]
|
|
141
|
+
for subnet in vpc_subnets:
|
|
142
|
+
subnet_name = self._get_resource_name(subnet)
|
|
143
|
+
subnet_type = "Public" if self._is_public_subnet(subnet, data) else "Private"
|
|
144
|
+
subnet_class = "PublicSubnet" if subnet_type == "Public" else "PrivateSubnet"
|
|
145
|
+
|
|
146
|
+
code_lines.append(
|
|
147
|
+
f' {subnet_class}("{subnet_name}")'
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Add instances not in any VPC (EC2-Classic)
|
|
151
|
+
if "no-vpc" in vpc_instances:
|
|
152
|
+
code_lines.append(' with Cluster("EC2-Classic"):')
|
|
153
|
+
for i, instance in enumerate(vpc_instances["no-vpc"]):
|
|
154
|
+
instance_name = self._get_resource_name(instance)
|
|
155
|
+
state = instance.get("State", {}).get("Name", "unknown")
|
|
156
|
+
|
|
157
|
+
code_lines.append(
|
|
158
|
+
f' classic_instance_{i} = EC2("{instance_name}\\n{state}")'
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return "\n".join(code_lines)
|
|
162
|
+
|
|
163
|
+
def _get_resource_name(self, resource: Dict[str, Any]) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Get a human-readable name for a resource.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
resource: AWS resource dictionary
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Resource name
|
|
172
|
+
"""
|
|
173
|
+
# Try to get name from tags
|
|
174
|
+
tags = resource.get("Tags", [])
|
|
175
|
+
for tag in tags:
|
|
176
|
+
if tag.get("Key") == "Name":
|
|
177
|
+
return tag.get("Value", "Unnamed")
|
|
178
|
+
|
|
179
|
+
# Fallback to resource ID
|
|
180
|
+
for id_key in ["InstanceId", "VpcId", "SubnetId", "GroupId"]:
|
|
181
|
+
if id_key in resource:
|
|
182
|
+
return resource[id_key]
|
|
183
|
+
|
|
184
|
+
return "Unknown"
|
|
185
|
+
|
|
186
|
+
def _is_public_subnet(self, subnet: Dict[str, Any], data: Dict[str, Any]) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Determine if a subnet is public based on route tables.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
subnet: Subnet dictionary
|
|
192
|
+
data: Full EC2 data
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if subnet is public, False otherwise
|
|
196
|
+
"""
|
|
197
|
+
subnet_id = subnet.get("SubnetId")
|
|
198
|
+
|
|
199
|
+
# Check route tables for internet gateway routes
|
|
200
|
+
for rt in data.get("route_tables", []):
|
|
201
|
+
# Check if this route table is associated with the subnet
|
|
202
|
+
associations = rt.get("Associations", [])
|
|
203
|
+
subnet_associated = any(
|
|
204
|
+
assoc.get("SubnetId") == subnet_id for assoc in associations
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if subnet_associated:
|
|
208
|
+
# Check for internet gateway routes
|
|
209
|
+
routes = rt.get("Routes", [])
|
|
210
|
+
has_igw_route = any(
|
|
211
|
+
route.get("GatewayId", "").startswith("igw-")
|
|
212
|
+
for route in routes
|
|
213
|
+
)
|
|
214
|
+
if has_igw_route:
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
def get_required_permissions(self) -> List[str]:
|
|
220
|
+
"""Get required AWS permissions for EC2 plugin."""
|
|
221
|
+
return [
|
|
222
|
+
"ec2:DescribeVpcs",
|
|
223
|
+
"ec2:DescribeInstances",
|
|
224
|
+
"ec2:DescribeSecurityGroups",
|
|
225
|
+
"ec2:DescribeSubnets",
|
|
226
|
+
"ec2:DescribeInternetGateways",
|
|
227
|
+
"ec2:DescribeRouteTables"
|
|
228
|
+
]
|