Spaces:
Running
Running
Update ipmentor/tools.py
Browse files- ipmentor/tools.py +172 -1
ipmentor/tools.py
CHANGED
|
@@ -10,6 +10,7 @@ import shutil
|
|
| 10 |
import os
|
| 11 |
import ipaddress
|
| 12 |
import math
|
|
|
|
| 13 |
from pathlib import Path
|
| 14 |
from typing import List, Dict, Tuple
|
| 15 |
|
|
@@ -536,6 +537,176 @@ def generate_diagram(ip_network: str, hosts_list: str, use_svg: bool = False) ->
|
|
| 536 |
}
|
| 537 |
|
| 538 |
return json.dumps(result, indent=2)
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
except Exception as e:
|
| 541 |
return json.dumps({"error": str(e)}, indent=2)
|
|
|
|
| 10 |
import os
|
| 11 |
import ipaddress
|
| 12 |
import math
|
| 13 |
+
import random
|
| 14 |
from pathlib import Path
|
| 15 |
from typing import List, Dict, Tuple
|
| 16 |
|
|
|
|
| 537 |
}
|
| 538 |
|
| 539 |
return json.dumps(result, indent=2)
|
| 540 |
+
|
| 541 |
+
except Exception as e:
|
| 542 |
+
return json.dumps({"error": str(e)}, indent=2)
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def generate_subnetting_exercise(num_subnets: int, use_vlsm: bool = False) -> str:
|
| 546 |
+
"""
|
| 547 |
+
Generate a random subnetting exercise that is solvable.
|
| 548 |
+
|
| 549 |
+
Args:
|
| 550 |
+
num_subnets (int): Number of subnets required
|
| 551 |
+
use_vlsm (bool): If True, each subnet has different host requirements (VLSM)
|
| 552 |
+
If False, equal division of subnets
|
| 553 |
+
|
| 554 |
+
Returns:
|
| 555 |
+
str: Exercise specification in JSON format
|
| 556 |
+
"""
|
| 557 |
+
try:
|
| 558 |
+
if num_subnets < 1:
|
| 559 |
+
return json.dumps({"error": "Number of subnets must be at least 1"}, indent=2)
|
| 560 |
+
|
| 561 |
+
if num_subnets > 256:
|
| 562 |
+
return json.dumps({"error": "Number of subnets too large (max 256)"}, indent=2)
|
| 563 |
+
|
| 564 |
+
# RFC 1918 Private Address Spaces
|
| 565 |
+
private_ranges = [
|
| 566 |
+
("10.0.0.0", "10.255.255.255"),
|
| 567 |
+
("172.16.0.0", "172.31.255.255"),
|
| 568 |
+
("192.168.0.0", "192.168.255.255")
|
| 569 |
+
]
|
| 570 |
+
|
| 571 |
+
# Select a random private range
|
| 572 |
+
range_start, range_end = random.choice(private_ranges)
|
| 573 |
+
start_int = int(ipaddress.IPv4Address(range_start))
|
| 574 |
+
end_int = int(ipaddress.IPv4Address(range_end))
|
| 575 |
+
|
| 576 |
+
# Determine initial CIDR range
|
| 577 |
+
if use_vlsm:
|
| 578 |
+
# For VLSM: use networks between /16 (65536 hosts) and /24 (256 hosts)
|
| 579 |
+
# This provides good variety while keeping exercises manageable
|
| 580 |
+
initial_min_cidr = 16
|
| 581 |
+
initial_max_cidr = 24
|
| 582 |
+
else:
|
| 583 |
+
bits_needed = math.ceil(math.log2(num_subnets))
|
| 584 |
+
# For equal division: /16 minimum for variety
|
| 585 |
+
# /28 maximum (or less if more subnets needed) to avoid tiny networks
|
| 586 |
+
initial_min_cidr = 16
|
| 587 |
+
initial_max_cidr = min(28, 32 - bits_needed - 1)
|
| 588 |
+
|
| 589 |
+
if initial_min_cidr > initial_max_cidr:
|
| 590 |
+
return json.dumps({
|
| 591 |
+
"error": f"Cannot generate exercise: too many subnets requested ({num_subnets})"
|
| 592 |
+
}, indent=2)
|
| 593 |
+
|
| 594 |
+
# Start with random CIDR in the desired range
|
| 595 |
+
initial_cidr = random.randint(initial_min_cidr, initial_max_cidr)
|
| 596 |
+
|
| 597 |
+
# Intelligent retry: try increasing network sizes until we find a valid one
|
| 598 |
+
# Start from initial_cidr and go down to initial_min_cidr (larger networks)
|
| 599 |
+
for network_cidr in range(initial_cidr, initial_min_cidr - 1, -1):
|
| 600 |
+
# Generate random IP within the selected private range
|
| 601 |
+
block_size = 2 ** (32 - network_cidr)
|
| 602 |
+
|
| 603 |
+
# Calculate valid starting positions (must be aligned to block_size)
|
| 604 |
+
first_valid_block = ((start_int + block_size - 1) // block_size) * block_size
|
| 605 |
+
last_valid_block = (end_int // block_size) * block_size
|
| 606 |
+
|
| 607 |
+
if first_valid_block > last_valid_block:
|
| 608 |
+
network_int = first_valid_block if first_valid_block <= end_int else start_int
|
| 609 |
+
else:
|
| 610 |
+
num_blocks = (last_valid_block - first_valid_block) // block_size + 1
|
| 611 |
+
random_block = random.randint(0, num_blocks - 1)
|
| 612 |
+
network_int = first_valid_block + (random_block * block_size)
|
| 613 |
+
|
| 614 |
+
# Create the network
|
| 615 |
+
random_ip = str(ipaddress.IPv4Address(network_int))
|
| 616 |
+
selected_network = ipaddress.IPv4Network(f"{random_ip}/{network_cidr}", strict=False)
|
| 617 |
+
network_str = str(selected_network)
|
| 618 |
+
|
| 619 |
+
# Try to generate a valid exercise with this network
|
| 620 |
+
if use_vlsm:
|
| 621 |
+
# Try multiple host combinations for VLSM
|
| 622 |
+
# 50 attempts per network size balances success rate vs performance
|
| 623 |
+
max_host_attempts = 50
|
| 624 |
+
|
| 625 |
+
for attempt in range(max_host_attempts):
|
| 626 |
+
total_addresses = 2 ** (32 - network_cidr)
|
| 627 |
+
host_sizes = []
|
| 628 |
+
remaining_space = total_addresses
|
| 629 |
+
|
| 630 |
+
for i in range(num_subnets):
|
| 631 |
+
if i == num_subnets - 1:
|
| 632 |
+
# Last subnet: use up to half remaining space
|
| 633 |
+
# Cap at 1000 hosts to keep exercises reasonable
|
| 634 |
+
max_hosts = min(remaining_space // 2, 1000)
|
| 635 |
+
else:
|
| 636 |
+
# Distribute remaining space across remaining subnets
|
| 637 |
+
# Cap at 1000 hosts to avoid overly large subnets
|
| 638 |
+
max_hosts = min(remaining_space // (num_subnets - i + 1), 1000)
|
| 639 |
+
|
| 640 |
+
if max_hosts < 2:
|
| 641 |
+
max_hosts = 2
|
| 642 |
+
|
| 643 |
+
# Use power law (0.7) to bias toward smaller, more realistic subnets
|
| 644 |
+
# This creates more varied and interesting exercises
|
| 645 |
+
host_count = random.randint(2, max(2, int(max_hosts ** 0.7)))
|
| 646 |
+
host_sizes.append(host_count)
|
| 647 |
+
|
| 648 |
+
bits_for_hosts = math.ceil(math.log2(host_count + 2))
|
| 649 |
+
subnet_size = 2 ** bits_for_hosts
|
| 650 |
+
remaining_space -= subnet_size
|
| 651 |
+
|
| 652 |
+
# Validate with calculate_subnets
|
| 653 |
+
hosts_list = ",".join(str(h) for h in host_sizes)
|
| 654 |
+
validation = calculate_subnets(network_str, num_subnets, "vlsm", hosts_list)
|
| 655 |
+
|
| 656 |
+
if "error" not in validation:
|
| 657 |
+
# Success! Return the exercise
|
| 658 |
+
return json.dumps({
|
| 659 |
+
"network": str(selected_network.network_address),
|
| 660 |
+
"mask": f"/{network_cidr}",
|
| 661 |
+
"mask_decimal": str(selected_network.netmask),
|
| 662 |
+
"num_subnets": num_subnets,
|
| 663 |
+
"type": "VLSM",
|
| 664 |
+
"hosts_per_subnet": host_sizes,
|
| 665 |
+
"hosts_list": hosts_list
|
| 666 |
+
}, indent=2)
|
| 667 |
+
|
| 668 |
+
# If we couldn't find valid hosts with random generation, try fallback
|
| 669 |
+
# Only try fallback for /18 or larger (at least 16384 addresses)
|
| 670 |
+
if network_cidr >= 18:
|
| 671 |
+
# Use powers of 2 for host counts (2, 4, 8, 16, 32, 64...)
|
| 672 |
+
# Max power of 6 = 64 hosts, keeps exercises simple
|
| 673 |
+
max_power = min(6, 32 - network_cidr - 2)
|
| 674 |
+
if max_power >= 1:
|
| 675 |
+
host_sizes = [2 ** random.randint(1, max_power) for _ in range(num_subnets)]
|
| 676 |
+
host_sizes.sort(reverse=True) # Largest first for better VLSM allocation
|
| 677 |
+
hosts_list = ",".join(str(h) for h in host_sizes)
|
| 678 |
+
|
| 679 |
+
validation = calculate_subnets(network_str, num_subnets, "vlsm", hosts_list)
|
| 680 |
+
if "error" not in validation:
|
| 681 |
+
return json.dumps({
|
| 682 |
+
"network": str(selected_network.network_address),
|
| 683 |
+
"mask": f"/{network_cidr}",
|
| 684 |
+
"mask_decimal": str(selected_network.netmask),
|
| 685 |
+
"num_subnets": num_subnets,
|
| 686 |
+
"type": "VLSM",
|
| 687 |
+
"hosts_per_subnet": host_sizes,
|
| 688 |
+
"hosts_list": hosts_list
|
| 689 |
+
}, indent=2)
|
| 690 |
+
|
| 691 |
+
else:
|
| 692 |
+
# Equal division - validate directly
|
| 693 |
+
validation = calculate_subnets(network_str, num_subnets, "max_subnets", "")
|
| 694 |
+
|
| 695 |
+
if "error" not in validation:
|
| 696 |
+
# Success! Return the exercise
|
| 697 |
+
return json.dumps({
|
| 698 |
+
"network": str(selected_network.network_address),
|
| 699 |
+
"mask": f"/{network_cidr}",
|
| 700 |
+
"mask_decimal": str(selected_network.netmask),
|
| 701 |
+
"num_subnets": num_subnets,
|
| 702 |
+
"type": "Equal Division",
|
| 703 |
+
"hosts_per_subnet": validation["hosts_per_subnet"]
|
| 704 |
+
}, indent=2)
|
| 705 |
+
|
| 706 |
+
# If we exhausted all network sizes, return error
|
| 707 |
+
return json.dumps({
|
| 708 |
+
"error": f"Could not generate valid exercise after trying multiple network sizes"
|
| 709 |
+
}, indent=2)
|
| 710 |
+
|
| 711 |
except Exception as e:
|
| 712 |
return json.dumps({"error": str(e)}, indent=2)
|