davidlms commited on
Commit
dfee995
·
verified ·
1 Parent(s): a6eade9

Update ipmentor/tools.py

Browse files
Files changed (1) hide show
  1. 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)