David commited on
Commit
9e96290
Β·
1 Parent(s): 858dd9c

prod ready

Browse files
Files changed (10) hide show
  1. LICENSE +21 -0
  2. assets/header.png +0 -0
  3. assets/logo.svg +64 -0
  4. assets/logo_text.svg +37 -0
  5. assets/social.png +0 -0
  6. bin/d2 +0 -3
  7. ipmentor/core.py +0 -259
  8. ipmentor/tools.py +289 -82
  9. ipmentor/ui.py +57 -70
  10. requirements.txt +1 -2
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 David Romero
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
assets/header.png ADDED
assets/logo.svg ADDED
assets/logo_text.svg ADDED
assets/social.png ADDED
bin/d2 DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:470c99eedfc01adf3a9162fbc24e451dc0e5ea91b687d38ebe3ac6547e0cf412
3
- size 46341847
 
 
 
 
ipmentor/core.py DELETED
@@ -1,259 +0,0 @@
1
- """
2
- Core IPv4 networking functions for IPMentor.
3
- """
4
-
5
- import ipaddress
6
- import math
7
- from typing import Dict, List, Tuple
8
-
9
-
10
- def ip_to_binary(ip_str: str) -> str:
11
- """Convert IP address to binary format with dots."""
12
- try:
13
- ip = ipaddress.IPv4Address(ip_str)
14
- binary = format(int(ip), '032b')
15
- return f"{binary[:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:]}"
16
- except:
17
- return "Invalid IP"
18
-
19
-
20
- def binary_to_ip(binary_str: str) -> str:
21
- """Convert binary IP to decimal format."""
22
- try:
23
- binary_clean = binary_str.replace('.', '').replace(' ', '')
24
- if len(binary_clean) != 32 or not all(c in '01' for c in binary_clean):
25
- return "Invalid Binary"
26
- ip_int = int(binary_clean, 2)
27
- return str(ipaddress.IPv4Address(ip_int))
28
- except:
29
- return "Invalid Binary"
30
-
31
-
32
- def parse_subnet_mask(mask_str: str) -> Tuple[str, int]:
33
- """Parse subnet mask from various formats."""
34
- mask_str = mask_str.strip()
35
-
36
- if mask_str.startswith('/'):
37
- cidr = int(mask_str[1:])
38
- elif '.' in mask_str:
39
- mask_ip = ipaddress.IPv4Address(mask_str)
40
- cidr = bin(int(mask_ip)).count('1')
41
- else:
42
- cidr = int(mask_str)
43
-
44
- if not 0 <= cidr <= 32:
45
- raise ValueError("Invalid CIDR")
46
-
47
- mask_ip = ipaddress.IPv4Network(f"0.0.0.0/{cidr}").netmask
48
- return str(mask_ip), cidr
49
-
50
-
51
- def analyze_ip(ip: str, subnet_mask: str) -> Dict:
52
- """Analyze IP address with subnet mask."""
53
- try:
54
- # Handle binary IP
55
- if '.' in ip and all(c in '01.' for c in ip.replace('.', '')):
56
- ip = binary_to_ip(ip)
57
- if ip == "Invalid Binary":
58
- raise ValueError("Invalid binary IP")
59
-
60
- # Parse mask
61
- mask_decimal, cidr = parse_subnet_mask(subnet_mask)
62
-
63
- # Create network
64
- network = ipaddress.IPv4Network(f"{ip}/{cidr}", strict=False)
65
-
66
- # Calculate hosts
67
- if cidr < 31:
68
- total_hosts = 2 ** (32 - cidr) - 2
69
- first_host = str(network.network_address + 1)
70
- last_host = str(network.broadcast_address - 1)
71
- elif cidr == 31:
72
- total_hosts = 2
73
- first_host = str(network.network_address)
74
- last_host = str(network.broadcast_address)
75
- else:
76
- total_hosts = 1
77
- first_host = str(network.network_address)
78
- last_host = str(network.network_address)
79
-
80
- return {
81
- "ip_decimal": ip,
82
- "ip_binary": ip_to_binary(ip),
83
- "subnet_mask_decimal": mask_decimal,
84
- "subnet_mask_binary": ip_to_binary(mask_decimal),
85
- "subnet_mask_cidr": f"/{cidr}",
86
- "network_address": str(network.network_address),
87
- "broadcast_address": str(network.broadcast_address),
88
- "first_host": first_host,
89
- "last_host": last_host,
90
- "total_hosts": total_hosts
91
- }
92
-
93
- except Exception as e:
94
- return {"error": str(e)}
95
-
96
-
97
- def calculate_subnets(network: str, number: int, method: str, hosts_list: str = "") -> Dict:
98
- """Calculate subnets using different methods."""
99
- try:
100
- base_network = ipaddress.IPv4Network(network, strict=False)
101
- base_cidr = base_network.prefixlen
102
-
103
- if method == "max_subnets":
104
- # Calculate subnets needed
105
- bits_needed = math.ceil(math.log2(number))
106
- new_cidr = base_cidr + bits_needed
107
-
108
- if new_cidr > 32:
109
- raise ValueError("Too many subnets requested")
110
-
111
- subnets = list(base_network.subnets(new_prefix=new_cidr))
112
- hosts_per_subnet = 2 ** (32 - new_cidr) - 2 if new_cidr < 31 else (2 if new_cidr == 31 else 1)
113
-
114
- subnet_list = []
115
- for i, subnet in enumerate(subnets[:number]):
116
- subnet_list.append({
117
- "subnet": str(subnet),
118
- "network": str(subnet.network_address),
119
- "broadcast": str(subnet.broadcast_address),
120
- "first_host": str(subnet.network_address + 1) if hosts_per_subnet > 1 else str(subnet.network_address),
121
- "last_host": str(subnet.broadcast_address - 1) if hosts_per_subnet > 1 else str(subnet.broadcast_address),
122
- "hosts": hosts_per_subnet
123
- })
124
-
125
- return {
126
- "method": "Max Subnets",
127
- "subnets": subnet_list,
128
- "bits_borrowed": bits_needed,
129
- "hosts_per_subnet": hosts_per_subnet,
130
- "total_subnets": len(subnets)
131
- }
132
-
133
- elif method == "max_hosts_per_subnet":
134
- # Calculate CIDR for hosts
135
- if number <= 2:
136
- bits_for_hosts = 1 if number == 2 else 0
137
- else:
138
- bits_for_hosts = math.ceil(math.log2(number + 2))
139
-
140
- new_cidr = 32 - bits_for_hosts
141
-
142
- if new_cidr < base_cidr:
143
- raise ValueError("Too many hosts requested")
144
-
145
- subnets = list(base_network.subnets(new_prefix=new_cidr))
146
- actual_hosts = 2 ** bits_for_hosts - 2 if new_cidr < 31 else (2 if new_cidr == 31 else 1)
147
-
148
- subnet_list = []
149
- for subnet in subnets:
150
- subnet_list.append({
151
- "subnet": str(subnet),
152
- "network": str(subnet.network_address),
153
- "broadcast": str(subnet.broadcast_address),
154
- "first_host": str(subnet.network_address + 1) if actual_hosts > 1 else str(subnet.network_address),
155
- "last_host": str(subnet.broadcast_address - 1) if actual_hosts > 1 else str(subnet.broadcast_address),
156
- "hosts": actual_hosts
157
- })
158
-
159
- return {
160
- "method": "Max Hosts per Subnet",
161
- "subnets": subnet_list,
162
- "hosts_per_subnet": actual_hosts,
163
- "total_subnets": len(subnets)
164
- }
165
-
166
- elif method == "vlsm":
167
- # Parse hosts requirements
168
- hosts_requirements = [int(x.strip()) for x in hosts_list.split(',')]
169
-
170
- if len(hosts_requirements) != number:
171
- raise ValueError(f"Need exactly {number} host values")
172
-
173
- # Sort largest first for optimal allocation
174
- sorted_reqs = sorted(enumerate(hosts_requirements), key=lambda x: x[1], reverse=True)
175
-
176
- subnets = []
177
- available_networks = [base_network]
178
-
179
- for original_idx, hosts_needed in sorted_reqs:
180
- # Find required CIDR
181
- if hosts_needed == 1:
182
- required_cidr = 32
183
- elif hosts_needed == 2:
184
- required_cidr = 31
185
- else:
186
- # Calculate bits needed for hosts + network + broadcast
187
- bits_for_hosts = math.ceil(math.log2(hosts_needed + 2))
188
- required_cidr = 32 - bits_for_hosts
189
-
190
- # Find a suitable available network
191
- allocated = False
192
- for i, avail_net in enumerate(available_networks):
193
- # Check if we can fit the required subnet in this available network
194
- if required_cidr >= avail_net.prefixlen:
195
- # Allocate the first subnet of the required size
196
- try:
197
- allocated_subnet = list(avail_net.subnets(new_prefix=required_cidr))[0]
198
- except ValueError:
199
- # Cannot create subnet of this size
200
- continue
201
-
202
- # Calculate actual host capacity
203
- if required_cidr == 32:
204
- actual_hosts = 1
205
- first_host = str(allocated_subnet.network_address)
206
- last_host = str(allocated_subnet.network_address)
207
- elif required_cidr == 31:
208
- actual_hosts = 2
209
- first_host = str(allocated_subnet.network_address)
210
- last_host = str(allocated_subnet.broadcast_address)
211
- else:
212
- actual_hosts = 2 ** (32 - required_cidr) - 2
213
- first_host = str(allocated_subnet.network_address + 1)
214
- last_host = str(allocated_subnet.broadcast_address - 1)
215
-
216
- subnets.append({
217
- "subnet": str(allocated_subnet),
218
- "network": str(allocated_subnet.network_address),
219
- "broadcast": str(allocated_subnet.broadcast_address),
220
- "first_host": first_host,
221
- "last_host": last_host,
222
- "hosts": actual_hosts,
223
- "hosts_requested": hosts_needed,
224
- "original_order": original_idx + 1
225
- })
226
-
227
- # Remove the used network
228
- available_networks.pop(i)
229
-
230
- # Add remaining available spaces by splitting the original network
231
- remaining_subnets = []
232
- for subnet in avail_net.subnets(new_prefix=required_cidr):
233
- if subnet != allocated_subnet:
234
- remaining_subnets.append(subnet)
235
-
236
- # Add the remaining subnets back to available networks
237
- available_networks.extend(remaining_subnets)
238
-
239
- allocated = True
240
- break
241
-
242
- if not allocated:
243
- raise ValueError(f"Cannot allocate subnet for {hosts_needed} hosts")
244
-
245
- # Sort back to original order
246
- subnets.sort(key=lambda x: x["original_order"])
247
-
248
- return {
249
- "method": "VLSM",
250
- "subnets": subnets,
251
- "total_hosts_requested": sum(hosts_requirements),
252
- "total_hosts_allocated": sum(s["hosts"] for s in subnets)
253
- }
254
-
255
- else:
256
- raise ValueError("Invalid method")
257
-
258
- except Exception as e:
259
- return {"error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ipmentor/tools.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- MCP tools for IPMentor.
3
  """
4
 
5
  import json
@@ -8,9 +8,262 @@ import subprocess
8
  import tempfile
9
  import shutil
10
  import os
 
 
11
  from pathlib import Path
12
- from typing import List
13
- from .core import analyze_ip, calculate_subnets
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
 
16
  def ip_info(ip: str, subnet_mask: str) -> str:
@@ -150,29 +403,21 @@ def _style_d2_diagram(diagram: str) -> str:
150
  return "\n".join(styled)
151
 
152
 
153
- def _export_to_svg(d2_diagram: str, output_path: str | Path = "diagram.svg") -> Path:
154
- """Export D2 diagram to SVG using d2 CLI."""
155
- # Try to find d2 binary in different locations
156
- d2_binary = None
157
-
158
- # First try local binary relative to project root
159
- script_dir = Path(__file__).parent.parent # Go up from ipmentor/tools.py to project root
160
- local_d2 = script_dir / "bin" / "d2"
161
- if local_d2.exists() and local_d2.is_file():
162
- d2_binary = str(local_d2)
163
-
164
- # Fallback to system d2
165
- elif shutil.which("d2") is not None:
166
- d2_binary = "d2"
167
-
168
- if d2_binary is None:
169
  raise RuntimeError(
170
  "D2 executable not found.\n"
171
- "Expected at bin/d2 or in system PATH.\n"
172
- "Install from https://d2lang.com/tour/installation"
173
  )
174
 
 
175
  output_path = Path(output_path).expanduser().resolve()
 
 
 
 
176
 
177
  with tempfile.NamedTemporaryFile("w+", suffix=".d2", delete=False) as tmp:
178
  tmp.write(d2_diagram)
@@ -180,7 +425,7 @@ def _export_to_svg(d2_diagram: str, output_path: str | Path = "diagram.svg") ->
180
  tmp_path = Path(tmp.name)
181
 
182
  try:
183
- cmd = [d2_binary, str(tmp_path), str(output_path)]
184
  subprocess.run(cmd, check=True)
185
  finally:
186
  if tmp_path.exists():
@@ -189,78 +434,37 @@ def _export_to_svg(d2_diagram: str, output_path: str | Path = "diagram.svg") ->
189
  return output_path
190
 
191
 
192
- def generate_diagram(ip_network: str, hosts_list: str) -> str:
193
  """
194
- Generate a network diagram in SVG format for Gradio UI.
195
 
196
  Args:
197
  ip_network (str): Network IP with mask in CIDR format (e.g., "192.168.1.0/24")
198
  hosts_list (str): Comma-separated list of host counts per subnet (e.g., "50,20,10,5")
 
199
 
200
  Returns:
201
- str: Path to the generated SVG file for Gradio Image component
202
  """
203
  try:
204
  # Parse hosts list
205
  hosts_per_subnet = [int(h.strip()) for h in hosts_list.split(",") if h.strip()]
206
 
207
  if not hosts_per_subnet:
208
- raise ValueError("No valid host counts provided")
209
-
210
- # Generate basic diagram
211
- base_diagram = _generate_basic_d2_diagram(ip_network.strip(), hosts_per_subnet)
212
-
213
- # Add styling
214
- styled_diagram = _style_d2_diagram(base_diagram)
215
-
216
- # Export to SVG
217
- output_path = _export_to_svg(styled_diagram, "network_diagram.svg")
218
-
219
- return str(output_path)
220
-
221
- except Exception as e:
222
- # Create a simple error diagram for Gradio to display
223
- error_diagram = f"""
224
- Internet: Cloud {{
225
- style: {{
226
- font-color: red
227
- }}
228
- }}
229
- Error: Error occurred: {str(e)[:50]}... {{
230
- style: {{
231
- font-color: red
232
- font-size: 20
233
- }}
234
- }}
235
- Internet -> Error
236
- """
237
- try:
238
- error_path = _export_to_svg(error_diagram, "error_diagram.svg")
239
- return str(error_path)
240
- except Exception as svg_error:
241
- # Log both errors for debugging
242
- print(f"Original error: {e}", file=__import__('sys').stderr)
243
- print(f"SVG error: {svg_error}", file=__import__('sys').stderr)
244
- return None
245
-
246
-
247
- def generate_diagram_mcp(ip_network: str, hosts_list: str) -> str:
248
- """
249
- Generate a network diagram in SVG format for MCP API.
250
-
251
- Args:
252
- ip_network (str): Network IP with mask in CIDR format (e.g., "192.168.1.0/24")
253
- hosts_list (str): Comma-separated list of host counts per subnet (e.g., "50,20,10,5")
254
 
255
- Returns:
256
- str: Result information in JSON format including SVG path or error
257
- """
258
- try:
259
- # Parse hosts list
260
- hosts_per_subnet = [int(h.strip()) for h in hosts_list.split(",") if h.strip()]
 
261
 
262
- if not hosts_per_subnet:
263
- return json.dumps({"error": "No valid host counts provided"}, indent=2)
 
 
264
 
265
  # Generate basic diagram
266
  base_diagram = _generate_basic_d2_diagram(ip_network.strip(), hosts_per_subnet)
@@ -268,12 +472,15 @@ def generate_diagram_mcp(ip_network: str, hosts_list: str) -> str:
268
  # Add styling
269
  styled_diagram = _style_d2_diagram(base_diagram)
270
 
271
- # Export to SVG
272
- output_path = _export_to_svg(styled_diagram, "network_diagram.svg")
 
 
273
 
274
  result = {
275
  "success": True,
276
- "svg_path": str(output_path),
 
277
  "network": ip_network.strip(),
278
  "hosts_per_subnet": hosts_per_subnet,
279
  "d2_diagram": styled_diagram
 
1
  """
2
+ Tools for IPMentor.
3
  """
4
 
5
  import json
 
8
  import tempfile
9
  import shutil
10
  import os
11
+ import ipaddress
12
+ import math
13
  from pathlib import Path
14
+ from typing import List, Dict, Tuple
15
+
16
+
17
+ def ip_to_binary(ip_str: str) -> str:
18
+ """Convert IP address to binary format with dots."""
19
+ try:
20
+ ip = ipaddress.IPv4Address(ip_str)
21
+ binary = format(int(ip), '032b')
22
+ return f"{binary[:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:]}"
23
+ except:
24
+ return "Invalid IP"
25
+
26
+
27
+ def binary_to_ip(binary_str: str) -> str:
28
+ """Convert binary IP to decimal format."""
29
+ try:
30
+ binary_clean = binary_str.replace('.', '').replace(' ', '')
31
+ if len(binary_clean) != 32 or not all(c in '01' for c in binary_clean):
32
+ return "Invalid Binary"
33
+ ip_int = int(binary_clean, 2)
34
+ return str(ipaddress.IPv4Address(ip_int))
35
+ except:
36
+ return "Invalid Binary"
37
+
38
+
39
+ def parse_subnet_mask(mask_str: str) -> Tuple[str, int]:
40
+ """Parse subnet mask from various formats."""
41
+ mask_str = mask_str.strip()
42
+
43
+ if mask_str.startswith('/'):
44
+ cidr = int(mask_str[1:])
45
+ elif '.' in mask_str:
46
+ mask_ip = ipaddress.IPv4Address(mask_str)
47
+ cidr = bin(int(mask_ip)).count('1')
48
+ else:
49
+ cidr = int(mask_str)
50
+
51
+ if not 0 <= cidr <= 32:
52
+ raise ValueError("Invalid CIDR")
53
+
54
+ mask_ip = ipaddress.IPv4Network(f"0.0.0.0/{cidr}").netmask
55
+ return str(mask_ip), cidr
56
+
57
+
58
+ def analyze_ip(ip: str, subnet_mask: str) -> Dict:
59
+ """Analyze IP address with subnet mask."""
60
+ try:
61
+ # Handle binary IP
62
+ if '.' in ip and all(c in '01.' for c in ip.replace('.', '')):
63
+ ip = binary_to_ip(ip)
64
+ if ip == "Invalid Binary":
65
+ raise ValueError("Invalid binary IP")
66
+
67
+ # Parse mask
68
+ mask_decimal, cidr = parse_subnet_mask(subnet_mask)
69
+
70
+ # Create network
71
+ network = ipaddress.IPv4Network(f"{ip}/{cidr}", strict=False)
72
+
73
+ # Calculate hosts
74
+ if cidr < 31:
75
+ total_hosts = 2 ** (32 - cidr) - 2
76
+ first_host = str(network.network_address + 1)
77
+ last_host = str(network.broadcast_address - 1)
78
+ elif cidr == 31:
79
+ total_hosts = 2
80
+ first_host = str(network.network_address)
81
+ last_host = str(network.broadcast_address)
82
+ else:
83
+ total_hosts = 1
84
+ first_host = str(network.network_address)
85
+ last_host = str(network.network_address)
86
+
87
+ return {
88
+ "ip_decimal": ip,
89
+ "ip_binary": ip_to_binary(ip),
90
+ "subnet_mask_decimal": mask_decimal,
91
+ "subnet_mask_binary": ip_to_binary(mask_decimal),
92
+ "subnet_mask_cidr": f"/{cidr}",
93
+ "network_address": str(network.network_address),
94
+ "broadcast_address": str(network.broadcast_address),
95
+ "first_host": first_host,
96
+ "last_host": last_host,
97
+ "total_hosts": total_hosts
98
+ }
99
+
100
+ except Exception as e:
101
+ return {"error": str(e)}
102
+
103
+
104
+ def calculate_subnets(network: str, number: int, method: str, hosts_list: str = "") -> Dict:
105
+ """Calculate subnets using different methods."""
106
+ try:
107
+ base_network = ipaddress.IPv4Network(network, strict=False)
108
+ base_cidr = base_network.prefixlen
109
+
110
+ if method == "max_subnets":
111
+ # Calculate subnets needed
112
+ bits_needed = math.ceil(math.log2(number))
113
+ new_cidr = base_cidr + bits_needed
114
+
115
+ if new_cidr > 32:
116
+ raise ValueError("Too many subnets requested")
117
+
118
+ subnets = list(base_network.subnets(new_prefix=new_cidr))
119
+ hosts_per_subnet = 2 ** (32 - new_cidr) - 2 if new_cidr < 31 else (2 if new_cidr == 31 else 1)
120
+
121
+ subnet_list = []
122
+ for i, subnet in enumerate(subnets[:number]):
123
+ subnet_list.append({
124
+ "subnet": str(subnet),
125
+ "network": str(subnet.network_address),
126
+ "broadcast": str(subnet.broadcast_address),
127
+ "first_host": str(subnet.network_address + 1) if hosts_per_subnet > 1 else str(subnet.network_address),
128
+ "last_host": str(subnet.broadcast_address - 1) if hosts_per_subnet > 1 else str(subnet.broadcast_address),
129
+ "hosts": hosts_per_subnet
130
+ })
131
+
132
+ return {
133
+ "method": "Max Subnets",
134
+ "subnets": subnet_list,
135
+ "bits_borrowed": bits_needed,
136
+ "hosts_per_subnet": hosts_per_subnet,
137
+ "total_subnets": len(subnets)
138
+ }
139
+
140
+ elif method == "max_hosts_per_subnet":
141
+ # Calculate CIDR for hosts
142
+ if number <= 2:
143
+ bits_for_hosts = 1 if number == 2 else 0
144
+ else:
145
+ bits_for_hosts = math.ceil(math.log2(number + 2))
146
+
147
+ new_cidr = 32 - bits_for_hosts
148
+
149
+ if new_cidr < base_cidr:
150
+ raise ValueError("Too many hosts requested")
151
+
152
+ subnets = list(base_network.subnets(new_prefix=new_cidr))
153
+ actual_hosts = 2 ** bits_for_hosts - 2 if new_cidr < 31 else (2 if new_cidr == 31 else 1)
154
+
155
+ subnet_list = []
156
+ for subnet in subnets:
157
+ subnet_list.append({
158
+ "subnet": str(subnet),
159
+ "network": str(subnet.network_address),
160
+ "broadcast": str(subnet.broadcast_address),
161
+ "first_host": str(subnet.network_address + 1) if actual_hosts > 1 else str(subnet.network_address),
162
+ "last_host": str(subnet.broadcast_address - 1) if actual_hosts > 1 else str(subnet.broadcast_address),
163
+ "hosts": actual_hosts
164
+ })
165
+
166
+ return {
167
+ "method": "Max Hosts per Subnet",
168
+ "subnets": subnet_list,
169
+ "hosts_per_subnet": actual_hosts,
170
+ "total_subnets": len(subnets)
171
+ }
172
+
173
+ elif method == "vlsm":
174
+ # Parse hosts requirements
175
+ hosts_requirements = [int(x.strip()) for x in hosts_list.split(',')]
176
+
177
+ if len(hosts_requirements) != number:
178
+ raise ValueError(f"Need exactly {number} host values")
179
+
180
+ # Sort largest first for optimal allocation
181
+ sorted_reqs = sorted(enumerate(hosts_requirements), key=lambda x: x[1], reverse=True)
182
+
183
+ subnets = []
184
+ available_networks = [base_network]
185
+
186
+ for original_idx, hosts_needed in sorted_reqs:
187
+ # Find required CIDR
188
+ if hosts_needed == 1:
189
+ required_cidr = 32
190
+ elif hosts_needed == 2:
191
+ required_cidr = 31
192
+ else:
193
+ # Calculate bits needed for hosts + network + broadcast
194
+ bits_for_hosts = math.ceil(math.log2(hosts_needed + 2))
195
+ required_cidr = 32 - bits_for_hosts
196
+
197
+ # Find a suitable available network
198
+ allocated = False
199
+ for i, avail_net in enumerate(available_networks):
200
+ # Check if we can fit the required subnet in this available network
201
+ if required_cidr >= avail_net.prefixlen:
202
+ # Allocate the first subnet of the required size
203
+ try:
204
+ allocated_subnet = list(avail_net.subnets(new_prefix=required_cidr))[0]
205
+ except ValueError:
206
+ # Cannot create subnet of this size
207
+ continue
208
+
209
+ # Calculate actual host capacity
210
+ if required_cidr == 32:
211
+ actual_hosts = 1
212
+ first_host = str(allocated_subnet.network_address)
213
+ last_host = str(allocated_subnet.network_address)
214
+ elif required_cidr == 31:
215
+ actual_hosts = 2
216
+ first_host = str(allocated_subnet.network_address)
217
+ last_host = str(allocated_subnet.broadcast_address)
218
+ else:
219
+ actual_hosts = 2 ** (32 - required_cidr) - 2
220
+ first_host = str(allocated_subnet.network_address + 1)
221
+ last_host = str(allocated_subnet.broadcast_address - 1)
222
+
223
+ subnets.append({
224
+ "subnet": str(allocated_subnet),
225
+ "network": str(allocated_subnet.network_address),
226
+ "broadcast": str(allocated_subnet.broadcast_address),
227
+ "first_host": first_host,
228
+ "last_host": last_host,
229
+ "hosts": actual_hosts,
230
+ "hosts_requested": hosts_needed,
231
+ "original_order": original_idx + 1
232
+ })
233
+
234
+ # Remove the used network
235
+ available_networks.pop(i)
236
+
237
+ # Add remaining available spaces by splitting the original network
238
+ remaining_subnets = []
239
+ for subnet in avail_net.subnets(new_prefix=required_cidr):
240
+ if subnet != allocated_subnet:
241
+ remaining_subnets.append(subnet)
242
+
243
+ # Add the remaining subnets back to available networks
244
+ available_networks.extend(remaining_subnets)
245
+
246
+ allocated = True
247
+ break
248
+
249
+ if not allocated:
250
+ raise ValueError(f"Cannot allocate subnet for {hosts_needed} hosts")
251
+
252
+ # Sort back to original order
253
+ subnets.sort(key=lambda x: x["original_order"])
254
+
255
+ return {
256
+ "method": "VLSM",
257
+ "subnets": subnets,
258
+ "total_hosts_requested": sum(hosts_requirements),
259
+ "total_hosts_allocated": sum(s["hosts"] for s in subnets)
260
+ }
261
+
262
+ else:
263
+ raise ValueError("Invalid method")
264
+
265
+ except Exception as e:
266
+ return {"error": str(e)}
267
 
268
 
269
  def ip_info(ip: str, subnet_mask: str) -> str:
 
403
  return "\n".join(styled)
404
 
405
 
406
+ def _export_to_image(d2_diagram: str, output_path: str | Path = "diagram.png", format: str = "png") -> Path:
407
+ """Export D2 diagram to PNG or SVG using d2 CLI."""
408
+ if shutil.which("d2") is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  raise RuntimeError(
410
  "D2 executable not found.\n"
411
+ "Install it from https://d2lang.com/tour/installation "
412
+ "or with Homebrew / chocolatey / scoop."
413
  )
414
 
415
+ # Ensure output path has correct extension
416
  output_path = Path(output_path).expanduser().resolve()
417
+ if format == "svg" and not output_path.suffix == ".svg":
418
+ output_path = output_path.with_suffix(".svg")
419
+ elif format == "png" and not output_path.suffix == ".png":
420
+ output_path = output_path.with_suffix(".png")
421
 
422
  with tempfile.NamedTemporaryFile("w+", suffix=".d2", delete=False) as tmp:
423
  tmp.write(d2_diagram)
 
425
  tmp_path = Path(tmp.name)
426
 
427
  try:
428
+ cmd = ["d2", str(tmp_path), str(output_path)]
429
  subprocess.run(cmd, check=True)
430
  finally:
431
  if tmp_path.exists():
 
434
  return output_path
435
 
436
 
437
+ def generate_diagram(ip_network: str, hosts_list: str, use_svg: bool = False) -> str:
438
  """
439
+ Generate a network diagram in PNG or SVG format.
440
 
441
  Args:
442
  ip_network (str): Network IP with mask in CIDR format (e.g., "192.168.1.0/24")
443
  hosts_list (str): Comma-separated list of host counts per subnet (e.g., "50,20,10,5")
444
+ use_svg (bool): Whether to generate SVG format (default: PNG)
445
 
446
  Returns:
447
+ str: Result information in JSON format including image path or error
448
  """
449
  try:
450
  # Parse hosts list
451
  hosts_per_subnet = [int(h.strip()) for h in hosts_list.split(",") if h.strip()]
452
 
453
  if not hosts_per_subnet:
454
+ return json.dumps({"error": "No valid host counts provided"}, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
 
456
+ # Validate that the subnet distribution is possible using VLSM
457
+ validation_result = calculate_subnets(
458
+ ip_network.strip(),
459
+ len(hosts_per_subnet),
460
+ "vlsm",
461
+ hosts_list.strip()
462
+ )
463
 
464
+ if "error" in validation_result:
465
+ return json.dumps({
466
+ "error": f"Invalid subnet distribution: {validation_result['error']}"
467
+ }, indent=2)
468
 
469
  # Generate basic diagram
470
  base_diagram = _generate_basic_d2_diagram(ip_network.strip(), hosts_per_subnet)
 
472
  # Add styling
473
  styled_diagram = _style_d2_diagram(base_diagram)
474
 
475
+ # Export to image
476
+ format = "svg" if use_svg else "png"
477
+ filename = f"network_diagram.{format}"
478
+ output_path = _export_to_image(styled_diagram, filename, format)
479
 
480
  result = {
481
  "success": True,
482
+ "image_path": str(output_path),
483
+ "format": format,
484
  "network": ip_network.strip(),
485
  "hosts_per_subnet": hosts_per_subnet,
486
  "d2_diagram": styled_diagram
ipmentor/ui.py CHANGED
@@ -4,75 +4,36 @@ Gradio UI for IPMentor.
4
 
5
  import gradio as gr
6
  import json
7
- from .core import analyze_ip, calculate_subnets
8
- from .tools import generate_diagram, generate_diagram_mcp
9
- from .config import APP_NAME
10
 
11
-
12
-
13
-
14
- # MCP Tool Functions
15
- def ip_info(ip: str, subnet_mask: str) -> str:
16
  """
17
- Analyze a complete IPv4 address with its subnet mask.
18
 
19
  Args:
20
- ip (str): IP address in decimal (192.168.1.10) or binary format
21
- subnet_mask (str): Subnet mask in decimal (255.255.255.0), CIDR (/24), or number (24) format
 
22
 
23
  Returns:
24
- str: Complete IP and network information in JSON format
25
  """
26
- try:
27
- result = analyze_ip(ip.strip(), subnet_mask.strip())
28
- return json.dumps(result, indent=2)
29
- except Exception as e:
30
- return json.dumps({"error": str(e)}, indent=2)
31
-
32
-
33
- def subnet_calculator(network: str, number: str, division_type: str, hosts_per_subnet: str = "") -> str:
34
- """
35
- Calculate subnets using different division methods.
36
-
37
- Args:
38
- network (str): Main network in CIDR format (e.g., "192.168.1.0/24")
39
- number (str): Number for division calculation
40
- division_type (str): Division method - "max_subnets", "max_hosts_per_subnet", or "vlsm"
41
- hosts_per_subnet (str): Comma-separated host counts per subnet (VLSM only)
42
-
43
- Returns:
44
- str: Calculated subnet information in JSON format
45
- """
46
- try:
47
- number_int = int(number.strip())
48
- result = calculate_subnets(
49
- network.strip(),
50
- number_int,
51
- division_type.strip(),
52
- hosts_per_subnet.strip()
53
- )
54
- return json.dumps(result, indent=2)
55
- except Exception as e:
56
- return json.dumps({"error": str(e)}, indent=2)
57
-
58
-
59
- # MCP API function for diagram generation
60
- def generate_diagram_api(ip_network: str, hosts_list: str) -> str:
61
- """MCP API wrapper for diagram generation that returns JSON."""
62
- return generate_diagram_mcp(ip_network, hosts_list)
63
-
64
-
65
- # Gradio UI wrapper for diagram generation
66
- def generate_diagram_ui(ip_network: str, hosts_list: str):
67
- """Gradio UI wrapper for diagram generation that handles errors gracefully."""
68
  try:
69
  if not ip_network.strip() or not hosts_list.strip():
70
- return None
71
- result = generate_diagram(ip_network, hosts_list)
72
- return result
 
 
 
 
 
 
 
 
 
73
  except Exception as e:
74
- # Return None if there's any error - Gradio will handle this gracefully
75
- return None
76
 
77
 
78
  def create_interface():
@@ -98,7 +59,7 @@ def create_interface():
98
  gr.Textbox(label="Network", placeholder="192.168.1.0/24"),
99
  gr.Textbox(label="Number", placeholder="4"),
100
  gr.Dropdown(label="Division Type", choices=["max_subnets","max_hosts_per_subnet","vlsm"], value="max_subnets"),
101
- gr.Textbox(label="Hosts per Subnet", placeholder="100,50,25,10")
102
  ],
103
  outputs=gr.Textbox(label="Calculation Result"),
104
  title="Subnet Calculator",
@@ -106,22 +67,48 @@ def create_interface():
106
  )
107
 
108
  diagram_interface = gr.Interface(
109
- fn=generate_diagram_ui,
110
  api_name="generate_diagram",
111
  inputs=[
112
  gr.Textbox(label="Network", placeholder="192.168.1.0/24"),
113
- gr.Textbox(label="Hosts per Subnet", placeholder="50,20,10,5")
 
 
 
 
 
114
  ],
115
- outputs=gr.Image(label="Network Diagram", type="filepath"),
116
  title="Network Diagram Generator",
117
- description="Generate network diagrams with SVG output"
118
  )
119
 
120
- # Combine all the MCP tool interfaces
121
- combined_app = gr.TabbedInterface(
122
- [ip_interface, subnet_interface, diagram_interface],
123
- ["IP Info", "Subnet Calculator", "Network Diagram"],
124
- title=f"{APP_NAME} - MCP Tools"
125
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
  return combined_app
 
4
 
5
  import gradio as gr
6
  import json
7
+ from .tools import generate_diagram as generate_diagram_core, ip_info, subnet_calculator
 
 
8
 
9
+ def generate_diagram(ip_network: str, hosts_list: str, use_svg: bool = False):
 
 
 
 
10
  """
11
+ Generate a network diagram in PNG or SVG format.
12
 
13
  Args:
14
+ ip_network (str): Network IP with mask in CIDR format (e.g., "192.168.1.0/24")
15
+ hosts_list (str): Comma-separated list of host counts per subnet (e.g., "50,20,10,5")
16
+ use_svg (bool): Whether to generate SVG format (default: PNG)
17
 
18
  Returns:
19
+ tuple: (image_path, status_message) for Gradio outputs
20
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  try:
22
  if not ip_network.strip() or not hosts_list.strip():
23
+ return None, "❌ Error: Please provide both network and hosts list"
24
+
25
+ result_json = generate_diagram_core(ip_network.strip(), hosts_list.strip(), use_svg)
26
+ result = json.loads(result_json)
27
+
28
+ if "error" in result:
29
+ return None, f"❌ Error: {result['error']}"
30
+
31
+ format_type = "SVG" if use_svg else "PNG"
32
+ hosts_count = len(result.get("hosts_per_subnet", []))
33
+ return result.get("image_path"), f"βœ… Success: {format_type} diagram generated for {hosts_count} subnets"
34
+
35
  except Exception as e:
36
+ return None, f"❌ Error: {str(e)}"
 
37
 
38
 
39
  def create_interface():
 
59
  gr.Textbox(label="Network", placeholder="192.168.1.0/24"),
60
  gr.Textbox(label="Number", placeholder="4"),
61
  gr.Dropdown(label="Division Type", choices=["max_subnets","max_hosts_per_subnet","vlsm"], value="max_subnets"),
62
+ gr.Textbox(label="Hosts per Subnet", placeholder="100,50,25,10", value="")
63
  ],
64
  outputs=gr.Textbox(label="Calculation Result"),
65
  title="Subnet Calculator",
 
67
  )
68
 
69
  diagram_interface = gr.Interface(
70
+ fn=generate_diagram,
71
  api_name="generate_diagram",
72
  inputs=[
73
  gr.Textbox(label="Network", placeholder="192.168.1.0/24"),
74
+ gr.Textbox(label="Hosts per Subnet", placeholder="50,20,10,5"),
75
+ gr.Checkbox(label="Generate as SVG", value=False)
76
+ ],
77
+ outputs=[
78
+ gr.Image(label="Network Diagram", type="filepath"),
79
+ gr.Textbox(label="Status", lines=2, interactive=False)
80
  ],
 
81
  title="Network Diagram Generator",
82
+ description="Generate network diagrams (PNG by default, SVG optional)"
83
  )
84
 
85
+ # Create main interface with custom header and description
86
+ with gr.Blocks() as combined_app:
87
+ # Header with logo
88
+ gr.Image("assets/header.png", show_label=False, interactive=False, container=False, height=120)
89
+
90
+ # Description
91
+ gr.Markdown("""
92
+ **IPMentor** is a comprehensive IPv4 networking toolkit that provides three powerful tools:
93
+
94
+ - **IP Info**: Analyze IPv4 addresses with subnet masks, supporting decimal, binary, and CIDR formats
95
+ - **Subnet Calculator**: Calculate subnets using different methods (max subnets, max hosts per subnet, and VLSM)
96
+ - **Network Diagram**: Generate visual network diagrams with automatic subnet validation
97
+
98
+ <a href="https://github.com/DavidLMS/ipmentor" target="_blank">Project on GitHub</a>
99
+
100
+ <a href="https://huggingface.co/spaces/DavidLMS/ipmentor-demo" target="_blank">Client demo</a>
101
+
102
+ Choose a tab below to get started with your networking calculations and visualizations.
103
+ """)
104
+
105
+ # Tabbed interface
106
+ with gr.Tabs():
107
+ with gr.Tab("IP Info"):
108
+ ip_interface.render()
109
+ with gr.Tab("Subnet Calculator"):
110
+ subnet_interface.render()
111
+ with gr.Tab("Network Diagram"):
112
+ diagram_interface.render()
113
 
114
  return combined_app
requirements.txt CHANGED
@@ -1,3 +1,2 @@
1
  gradio[mcp]>=4.44.0
2
- python-dotenv>=1.0.0
3
- playwright
 
1
  gradio[mcp]>=4.44.0
2
+ python-dotenv>=1.0.0