David commited on
Commit
0071b55
·
1 Parent(s): 3670007

Add app files

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .DS_Store
app.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ IPMentor - IPv4 Network Analysis and Subnetting Tutor
4
+ Hugging Face Space Entry Point
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ from ipmentor.ui import create_interface
10
+
11
+ # Configure logging
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ def main():
20
+ """Main application entry point for Hugging Face Space."""
21
+ logger.info("Starting IPMentor v1.0.0 for Hugging Face Space")
22
+
23
+ # Create Gradio app
24
+ app = create_interface()
25
+
26
+ # Launch configuration for Hugging Face Space
27
+ port = int(os.getenv("GRADIO_SERVER_PORT", 7860))
28
+ launch_config = {
29
+ "server_name": "0.0.0.0",
30
+ "server_port": port, # Use environment port or default
31
+ "share": False,
32
+ "show_error": True,
33
+ "mcp_server": True, # Enable MCP for Space
34
+ "quiet": False
35
+ }
36
+
37
+ logger.info(f"🌐 Web Interface: Starting on port {port}")
38
+ logger.info("🤖 MCP Server: Enabled for Hugging Face Space")
39
+
40
+ try:
41
+ app.launch(**launch_config)
42
+ except Exception as e:
43
+ logger.error(f"❌ Error launching app: {e}")
44
+ raise
45
+
46
+ if __name__ == "__main__":
47
+ main()
assets/cloud.svg ADDED
assets/host.svg ADDED
assets/router.svg ADDED
assets/switch.svg ADDED
ipmentor/__init__.py ADDED
File without changes
ipmentor/config.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple configuration for IPMentor.
3
+ """
4
+
5
+ import os
6
+ from dotenv import load_dotenv
7
+
8
+ load_dotenv()
9
+
10
+ # Basic settings
11
+ HOST = os.getenv("HOST", "0.0.0.0")
12
+ PORT = int(os.getenv("PORT", 7861))
13
+ DEBUG = os.getenv("DEBUG", "false").lower() == "true"
14
+ MCP_ENABLED = os.getenv("MCP_ENABLED", "true").lower() == "true"
15
+
16
+ # App info
17
+ APP_NAME = "IPMentor"
18
+ VERSION = "1.0.0"
19
+ DESCRIPTION = "IPv4 network analysis and subnetting tutor"
ipmentor/core.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/main.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ IPMentor - IPv4 Network Analysis and Subnetting Tutor
3
+ """
4
+
5
+ import logging
6
+ from .ui import create_interface
7
+ from .config import HOST, PORT, DEBUG, MCP_ENABLED, APP_NAME, VERSION
8
+
9
+ # Simple logging setup
10
+ logging.basicConfig(
11
+ level=logging.INFO if not DEBUG else logging.DEBUG,
12
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def main():
19
+ """Main application entry point."""
20
+ logger.info(f"Starting {APP_NAME} v{VERSION}")
21
+
22
+ # Create Gradio app
23
+ app = create_interface()
24
+
25
+ # Launch configuration
26
+ launch_config = {
27
+ "server_name": HOST,
28
+ "server_port": PORT,
29
+ "share": False,
30
+ "show_error": DEBUG,
31
+ "mcp_server": MCP_ENABLED,
32
+ "quiet": not DEBUG
33
+ }
34
+
35
+ logger.info(f"🌐 Web Interface: http://{HOST}:{PORT}")
36
+ if MCP_ENABLED:
37
+ logger.info(f"🤖 MCP Server: http://{HOST}:{PORT}/gradio_api/mcp/sse")
38
+
39
+ try:
40
+ app.launch(**launch_config)
41
+ except KeyboardInterrupt:
42
+ logger.info("👋 Shutting down...")
43
+ except Exception as e:
44
+ logger.error(f"❌ Error: {e}")
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main()
ipmentor/tools.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP tools for IPMentor.
3
+ """
4
+
5
+ import json
6
+ import re
7
+ 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:
17
+ """
18
+ Analyze a complete IPv4 address with its subnet mask.
19
+
20
+ Args:
21
+ ip (str): IP address in decimal (192.168.1.10) or binary format
22
+ subnet_mask (str): Subnet mask in decimal (255.255.255.0), CIDR (/24), or number (24) format
23
+
24
+ Returns:
25
+ str: Complete IP and network information in JSON format
26
+ """
27
+ try:
28
+ result = analyze_ip(ip.strip(), subnet_mask.strip())
29
+ return json.dumps(result, indent=2)
30
+ except Exception as e:
31
+ return json.dumps({"error": str(e)}, indent=2)
32
+
33
+
34
+ def subnet_calculator(network: str, number: str, division_type: str, hosts_per_subnet: str = "") -> str:
35
+ """
36
+ Calculate subnets using different division methods.
37
+
38
+ Args:
39
+ network (str): Main network in CIDR format (e.g., "192.168.1.0/24")
40
+ number (str): Number for division calculation
41
+ division_type (str): Division method - "max_subnets", "max_hosts_per_subnet", or "vlsm"
42
+ hosts_per_subnet (str): Comma-separated host counts per subnet (VLSM only)
43
+
44
+ Returns:
45
+ str: Calculated subnet information in JSON format
46
+ """
47
+ try:
48
+ number_int = int(number.strip())
49
+ result = calculate_subnets(
50
+ network.strip(),
51
+ number_int,
52
+ division_type.strip(),
53
+ hosts_per_subnet.strip()
54
+ )
55
+ return json.dumps(result, indent=2)
56
+ except Exception as e:
57
+ return json.dumps({"error": str(e)}, indent=2)
58
+
59
+
60
+ # Diagram generation constants and functions
61
+ ICON_MAP = {
62
+ "Cloud": "https://davidlms.github.io/ipmentor/assets/cloud.svg",
63
+ "Router": "https://davidlms.github.io/ipmentor/assets/router.svg",
64
+ "Switch": "https://davidlms.github.io/ipmentor/assets/switch.svg",
65
+ "Hosts": "https://davidlms.github.io/ipmentor/assets/host.svg",
66
+ }
67
+
68
+ EDGE_STYLE_BASE = (
69
+ ' style: {\n'
70
+ ' stroke: "#FFA201"\n'
71
+ ' stroke-width: 3\n'
72
+ )
73
+
74
+
75
+ def _generate_basic_d2_diagram(network_ip: str, hosts_per_subnet: List[int]) -> str:
76
+ """Generate basic D2 diagram without styling."""
77
+ lines = [
78
+ "Internet: Cloud",
79
+ "Router: Router",
80
+ "Switch_0: Switch",
81
+ "",
82
+ "Internet -> Router",
83
+ f'Router -> Switch_0: "{network_ip}"',
84
+ ""
85
+ ]
86
+ for idx, hosts in enumerate(hosts_per_subnet, start=1):
87
+ lines += [
88
+ f"Switch_{idx}: Switch",
89
+ f"Host_{idx}: Hosts",
90
+ f"Switch_0 -> Switch_{idx}",
91
+ f'Switch_{idx} -> Host_{idx}: "{hosts} hosts"',
92
+ ""
93
+ ]
94
+ if lines[-1] == "":
95
+ lines.pop()
96
+ return "\n".join(lines)
97
+
98
+
99
+ def _style_d2_diagram(diagram: str) -> str:
100
+ """Add styling and icons to D2 diagram."""
101
+ styled = []
102
+ for line in diagram.splitlines():
103
+
104
+ if not line.strip():
105
+ styled.append("")
106
+ continue
107
+
108
+ # Nodes
109
+ node_match = re.match(r'^(\w+(?:_\d+)?):\s*(Cloud|Router|Switch|Hosts)\s*$', line.strip())
110
+ if node_match:
111
+ name, kind = node_match.groups()
112
+ styled.append(
113
+ f"{name}: {kind} {{\n"
114
+ " style: {\n"
115
+ " font-color: transparent\n"
116
+ " }\n"
117
+ " shape: image\n"
118
+ f" icon: {ICON_MAP[kind]}\n"
119
+ "}"
120
+ )
121
+ continue
122
+
123
+ # Edges with labels
124
+ edge_with_label_match = re.match(r'^(\w+(?:_\d+)?) -> (\w+(?:_\d+)?):\s*"([^"]+)"$', line.strip())
125
+ if edge_with_label_match:
126
+ src, dst, label = edge_with_label_match.groups()
127
+ styled.append(
128
+ f'{src} -> {dst}: "{label}" {{\n'
129
+ f"{EDGE_STYLE_BASE}"
130
+ " font-size: 30\n"
131
+ " }\n"
132
+ "}"
133
+ )
134
+ continue
135
+
136
+ # Edges without labels
137
+ edge_match = re.match(r'^(\w+(?:_\d+)?) -> (\w+(?:_\d+)?)$', line.strip())
138
+ if edge_match:
139
+ src, dst = edge_match.groups()
140
+ styled.append(
141
+ f"{src} -> {dst}: {{\n"
142
+ f"{EDGE_STYLE_BASE}"
143
+ " }\n"
144
+ "}"
145
+ )
146
+ continue
147
+
148
+ styled.append(line)
149
+
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
+ if shutil.which("d2") is None:
156
+ raise RuntimeError(
157
+ "D2 executable not found.\n"
158
+ "Install it from https://d2lang.com/tour/installation "
159
+ "or with Homebrew / chocolatey / scoop."
160
+ )
161
+
162
+ output_path = Path(output_path).expanduser().resolve()
163
+
164
+ with tempfile.NamedTemporaryFile("w+", suffix=".d2", delete=False) as tmp:
165
+ tmp.write(d2_diagram)
166
+ tmp.flush()
167
+ tmp_path = Path(tmp.name)
168
+
169
+ try:
170
+ cmd = ["d2", str(tmp_path), str(output_path)]
171
+ subprocess.run(cmd, check=True)
172
+ finally:
173
+ if tmp_path.exists():
174
+ os.remove(tmp_path)
175
+
176
+ return output_path
177
+
178
+
179
+ def generate_diagram(ip_network: str, hosts_list: str) -> str:
180
+ """
181
+ Generate a network diagram in SVG format for Gradio UI.
182
+
183
+ Args:
184
+ ip_network (str): Network IP with mask in CIDR format (e.g., "192.168.1.0/24")
185
+ hosts_list (str): Comma-separated list of host counts per subnet (e.g., "50,20,10,5")
186
+
187
+ Returns:
188
+ str: Path to the generated SVG file for Gradio Image component
189
+ """
190
+ try:
191
+ # Parse hosts list
192
+ hosts_per_subnet = [int(h.strip()) for h in hosts_list.split(",") if h.strip()]
193
+
194
+ if not hosts_per_subnet:
195
+ raise ValueError("No valid host counts provided")
196
+
197
+ # Generate basic diagram
198
+ base_diagram = _generate_basic_d2_diagram(ip_network.strip(), hosts_per_subnet)
199
+
200
+ # Add styling
201
+ styled_diagram = _style_d2_diagram(base_diagram)
202
+
203
+ # Export to SVG
204
+ output_path = _export_to_svg(styled_diagram, "network_diagram.svg")
205
+
206
+ return str(output_path)
207
+
208
+ except Exception as e:
209
+ # Create a simple error diagram for Gradio to display
210
+ error_diagram = f"""
211
+ Internet: Cloud {{
212
+ style: {{
213
+ font-color: red
214
+ }}
215
+ }}
216
+ Error: Error occurred: {str(e)[:50]}... {{
217
+ style: {{
218
+ font-color: red
219
+ font-size: 20
220
+ }}
221
+ }}
222
+ Internet -> Error
223
+ """
224
+ try:
225
+ error_path = _export_to_svg(error_diagram, "error_diagram.svg")
226
+ return str(error_path)
227
+ except:
228
+ # If even error diagram fails, return None
229
+ return None
230
+
231
+
232
+ def generate_diagram_mcp(ip_network: str, hosts_list: str) -> str:
233
+ """
234
+ Generate a network diagram in SVG format for MCP API.
235
+
236
+ Args:
237
+ ip_network (str): Network IP with mask in CIDR format (e.g., "192.168.1.0/24")
238
+ hosts_list (str): Comma-separated list of host counts per subnet (e.g., "50,20,10,5")
239
+
240
+ Returns:
241
+ str: Result information in JSON format including SVG path or error
242
+ """
243
+ try:
244
+ # Parse hosts list
245
+ hosts_per_subnet = [int(h.strip()) for h in hosts_list.split(",") if h.strip()]
246
+
247
+ if not hosts_per_subnet:
248
+ return json.dumps({"error": "No valid host counts provided"}, indent=2)
249
+
250
+ # Generate basic diagram
251
+ base_diagram = _generate_basic_d2_diagram(ip_network.strip(), hosts_per_subnet)
252
+
253
+ # Add styling
254
+ styled_diagram = _style_d2_diagram(base_diagram)
255
+
256
+ # Export to SVG
257
+ output_path = _export_to_svg(styled_diagram, "network_diagram.svg")
258
+
259
+ result = {
260
+ "success": True,
261
+ "svg_path": str(output_path),
262
+ "network": ip_network.strip(),
263
+ "hosts_per_subnet": hosts_per_subnet,
264
+ "d2_diagram": styled_diagram
265
+ }
266
+
267
+ return json.dumps(result, indent=2)
268
+
269
+ except Exception as e:
270
+ return json.dumps({"error": str(e)}, indent=2)
ipmentor/ui.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio UI for IPMentor.
3
+ """
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():
79
+ """Create the Gradio interface."""
80
+
81
+ # Create separate interfaces for MCP tools only
82
+ ip_interface = gr.Interface(
83
+ fn=ip_info,
84
+ api_name="ip_info",
85
+ inputs=[
86
+ gr.Textbox(label="IP Address", placeholder="192.168.1.10"),
87
+ gr.Textbox(label="Subnet Mask", placeholder="/24 or 255.255.255.0")
88
+ ],
89
+ outputs=gr.Textbox(label="Analysis Result"),
90
+ title="IP Info",
91
+ description="Analyze IPv4 addresses with subnet masks"
92
+ )
93
+
94
+ subnet_interface = gr.Interface(
95
+ fn=subnet_calculator,
96
+ api_name="subnet_calculator",
97
+ inputs=[
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",
105
+ description="Calculate subnets using different methods"
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
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio[mcp]>=4.44.0
2
+ python-dotenv>=1.0.0