lib.ips
1import json 2import random 3from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Interface, IPv6Network 4 5import msgpack 6from netaddr import EUI 7 8 9class _AttrContainer: 10 """Dynamic container for named attributes of one type (see the subclasses). 11 12 Serialises to ``{name: str(value)}`` (dict, JSON, msgpack) and back. 13 """ 14 _item_type = object 15 _type_name = "object" 16 17 def __setattr__(self, name, value): 18 if not isinstance(value, self._item_type): 19 raise TypeError(f"{name}: expected {self._type_name}, got {type(value).__name__}") 20 super().__setattr__(name, value) 21 22 # ---------------- dict ---------------- 23 24 def to_dict(self): 25 return {k: str(v) for k, v in self.__dict__.items()} 26 27 @classmethod 28 def from_dict(cls, d): 29 obj = cls() 30 for k, v in d.items(): 31 setattr(obj, k, cls._item_type(v)) 32 return obj 33 34 # ---------------- JSON ---------------- 35 36 def to_json(self): 37 return json.dumps(self.to_dict()) 38 39 @classmethod 40 def from_json(cls, s): 41 return cls.from_dict(json.loads(s)) 42 43 # ---------------- msgpack ---------------- 44 45 def pack(self): 46 return msgpack.packb(self.to_dict(), use_bin_type=True) 47 48 @classmethod 49 def unpack(cls, blob): 50 return cls.from_dict(msgpack.unpackb(blob, raw=False)) 51 52 53class IPv4Addresses(_AttrContainer): 54 """Dynamic container for named IPv4Interface attributes, with no boilerplate. 55 56 Usage: 57 ips = IPv4Addresses() 58 ips.ip1 = IPv4Interface("192.168.1.1/24") 59 ips.ip2 = IPv4Interface("192.168.1.2/24") 60 """ 61 _item_type = IPv4Interface 62 _type_name = "IPv4Interface" 63 64 65class IPv4Networks(_AttrContainer): 66 """Dynamic container for named IPv4Network attributes, with no boilerplate. 67 68 Usage: 69 nets = IPv4Networks() 70 nets.lan = IPv4Network("192.168.1.0/24") 71 nets.mgmt = IPv4Network("10.0.0.0/8") 72 """ 73 _item_type = IPv4Network 74 _type_name = "IPv4Network" 75 76 77class IPv6Addresses(_AttrContainer): 78 """Dynamic container for named IPv6Interface attributes (the IPv6 twin of IPv4Addresses). 79 80 Usage: 81 ips6 = IPv6Addresses() 82 ips6.router = IPv6Interface("2001:db8:1::1/64") 83 """ 84 _item_type = IPv6Interface 85 _type_name = "IPv6Interface" 86 87 88class IPv6Networks(_AttrContainer): 89 """Dynamic container for named IPv6Network attributes (the IPv6 twin of IPv4Networks). 90 91 Usage: 92 nets6 = IPv6Networks() 93 nets6.lan = IPv6Network("2001:db8:1::/64") 94 """ 95 _item_type = IPv6Network 96 _type_name = "IPv6Network" 97 98 99_PRIVATE_NETWORKS = [ 100 IPv4Network("10.0.0.0/8"), 101 IPv4Network("172.16.0.0/12"), 102 IPv4Network("192.168.0.0/16"), 103] 104_PRIVATE_NETWORKS_V6 = [IPv6Network("fd00::/8")] # RFC 4193 unique local addresses 105_GLOBAL_UNICAST_V6 = IPv6Network("2000::/3") # default search space of random_ipv6networks 106_PRIVATE_BY_VERSION = {4: _PRIVATE_NETWORKS, 6: _PRIVATE_NETWORKS_V6} 107 108 109def _pick_one(mask, from_network, exclude, from_private_network): 110 """Pick a single random /mask network of the family of *from_network*. 111 112 Internal helper for random_ipv4networks / random_ipv6networks. 113 """ 114 max_prefixlen = from_network.max_prefixlen 115 if not 0 <= mask <= max_prefixlen: 116 raise ValueError(f"mask /{mask} out of range for IPv{from_network.version} (0-{max_prefixlen})") 117 block_size = 1 << (max_prefixlen - mask) 118 net_cls = type(from_network) # not ip_network((int, mask)): it would guess the family from the int 119 # networks of the other family never overlap: drop them 120 exclude = [ex for ex in exclude if ex.version == from_network.version] 121 122 def make(abs_idx): 123 return net_cls((abs_idx * block_size, mask)) 124 125 def available(net): 126 return not any(net.overlaps(ex) for ex in exclude) 127 128 def indices_in(a, b): 129 first = (a + block_size - 1) // block_size 130 last = (b + 1) // block_size - 1 131 return range(first, last + 1) if first <= last else range(0, 0) 132 133 flo = int(from_network.network_address) 134 fhi = int(from_network.broadcast_address) 135 if from_private_network: 136 boxes = [] 137 for p in _PRIVATE_BY_VERSION[from_network.version]: 138 lo = max(flo, int(p.network_address)) 139 hi = min(fhi, int(p.broadcast_address)) 140 if lo <= hi: 141 boxes.append((lo, hi)) 142 else: 143 boxes = [(flo, fhi)] 144 145 # r.stop - r.start, not len(r): len() overflows for ranges longer than 2**63 (IPv6) 146 spans = [(r.start, r.stop - r.start) for lo, hi in boxes for r in [indices_in(lo, hi)] if r] 147 total = sum(count for _, count in spans) 148 149 if total == 0: 150 raise ValueError(f"No /{mask} network fits in the search space") 151 152 def pick_abs_idx(): 153 if from_private_network and len(spans) > 1: 154 # Pick uniformly across private ranges (not weighted by range size) 155 start, count = random.choice(spans) 156 return start + random.randrange(count) 157 n = random.randrange(total) 158 for start, count in spans: 159 if n < count: 160 return start + n 161 n -= count 162 return spans[-1][0] # unreachable: n < total guarantees a match above 163 164 if total <= 65536: 165 candidates = [make(i) for start, count in spans 166 for i in range(start, start + count) 167 if available(make(i))] 168 if not candidates: 169 raise ValueError(f"No available /{mask} network with the given constraints") 170 return random.choice(candidates) 171 172 for _ in range(1000): 173 candidate = make(pick_abs_idx()) 174 if available(candidate): 175 return candidate 176 177 raise ValueError(f"No available /{mask} network found after 1000 attempts") 178 179 180def _random_networks(masks, from_network, exclude, from_private_network): 181 if isinstance(masks, int): 182 masks = [masks] 183 184 working_exclude = list(exclude) if exclude else [] 185 result = [] 186 for mask in masks: 187 net = _pick_one(mask, from_network, working_exclude, from_private_network) 188 result.append(net) 189 working_exclude.append(net) 190 return result 191 192 193def _check_version(func_name, network, version): 194 if getattr(network, 'version', None) != version: 195 raise ValueError(f"{func_name}: expected an IPv{version} network, got {type(network).__name__}") 196 197 198def random_ipv4networks( 199 masks, 200 from_network=IPv4Network("0.0.0.0/0"), 201 exclude=None, 202 from_private_network=False, 203): 204 """Return a list of disjoint random IPv4Networks. 205 206 `masks` is an int or a list of ints; one network is returned per mask, 207 all mutually disjoint and not overlapping any network in `exclude`. 208 Optionally restricted to RFC-1918 private ranges with `from_private_network=True`. 209 210 `from` being a reserved keyword, the parameter is named `from_network`. 211 212 Raises ValueError if any network cannot be allocated. 213 """ 214 _check_version("random_ipv4networks", from_network, 4) 215 return _random_networks(masks, from_network, exclude, from_private_network) 216 217 218def random_ipv6networks( 219 masks, 220 from_network=None, 221 exclude=None, 222 from_private_network=False, 223): 224 """Return a list of disjoint random IPv6Networks (the IPv6 twin of random_ipv4networks). 225 226 `masks` is an int or a list of ints (prefix lengths, e.g. 64); one network is returned 227 per mask, all mutually disjoint and not overlapping any network in `exclude`. 228 229 `from_network=None` (default) means the global unicast space ``2000::/3``, or ``::/0`` 230 when `from_private_network=True`: the search space is then the unique local range 231 ``fd00::/8`` (RFC 4193). Use ``from_network=IPv6Network('2001:db8::/32')`` for 232 documentation prefixes. 233 234 Raises ValueError if any network cannot be allocated. 235 """ 236 if from_network is None: 237 from_network = IPv6Network("::/0") if from_private_network else _GLOBAL_UNICAST_V6 238 _check_version("random_ipv6networks", from_network, 6) 239 return _random_networks(masks, from_network, exclude, from_private_network) 240 241 242def _host_offsets(network): 243 """Offsets of *network* usable as host addresses. 244 245 IPv4: the network and broadcast addresses are skipped up to /30; /31 (RFC 3021) and /32 246 use every address. IPv6: offset 0 (the subnet-router anycast address, RFC 4291) is 247 skipped up to /126; /127 (RFC 6164) and /128 use every address. 248 """ 249 total = network.num_addresses 250 if network.version == 4: 251 return range(1, total - 1) if network.prefixlen <= 30 else range(total) 252 return range(1, total) if network.prefixlen <= 126 else range(total) 253 254 255def _iface_cls(network): 256 return IPv4Interface if network.version == 4 else IPv6Interface 257 258 259def _random_addresses(network, n, exclude_ips, exclude_nets): 260 ex_ips = set(exclude_ips) if exclude_ips else set() 261 ex_nets = list(exclude_nets) if exclude_nets else [] 262 263 base = int(network.network_address) 264 total = network.num_addresses 265 iface_cls = _iface_cls(network) 266 267 def make(i): 268 return iface_cls((base + i, network.prefixlen)) 269 270 def available(ip): 271 return ip not in ex_ips and not any(ip in net for net in ex_nets) 272 273 host_range = _host_offsets(network) 274 275 # Small space: enumerate all candidates, then sample. 276 if total <= 65536: 277 candidates = [make(i) for i in host_range if available(make(i))] 278 if len(candidates) < n: 279 raise ValueError( 280 f"Not enough available addresses: need {n}, found {len(candidates)}" 281 ) 282 return random.sample(candidates, n) 283 284 # Large space: pick one at a time, accumulating into a working exclude set. 285 working_ex = set(ex_ips) 286 result = [] 287 for _ in range(n): 288 for _ in range(1000): 289 candidate = make(random.randrange(host_range.start, host_range.stop)) 290 if candidate not in working_ex and not any(candidate in net for net in ex_nets): 291 result.append(candidate) 292 working_ex.add(candidate) 293 break 294 else: 295 raise ValueError( 296 f"Could not find enough available addresses (got {len(result)}/{n})" 297 ) 298 return result 299 300 301def random_ipv4s(network, n=1, exclude_ips=None, exclude_nets=None): 302 """Return a list of n distinct random IPv4Interface within `network`, 303 excluding any address in `exclude_ips` or covered by any network in `exclude_nets`. 304 305 The network address and the broadcast address are never returned (prefix <= /30). 306 Note: `exclude_ips` is compared with the prefix, so list IPv4Interface values 307 (as returned by this function), not bare IPv4Address values. 308 309 Raises ValueError if fewer than n addresses are available. 310 """ 311 _check_version("random_ipv4s", network, 4) 312 return _random_addresses(network, n, exclude_ips, exclude_nets) 313 314 315def random_ipv6s(network, n=1, exclude_ips=None, exclude_nets=None): 316 """Return a list of n distinct random IPv6Interface within `network` (the IPv6 twin 317 of random_ipv4s), excluding any address in `exclude_ips` or covered by any network 318 in `exclude_nets`. 319 320 Offset 0 (the subnet-router anycast address) is never returned up to /126; /127 and 321 /128 use every address. 322 323 Raises ValueError if fewer than n addresses are available. 324 """ 325 _check_version("random_ipv6s", network, 6) 326 return _random_addresses(network, n, exclude_ips, exclude_nets) 327 328 329def _random_addresses_with_range(network, gap, n, exclude_ips, exclude_nets): 330 """Return a list of n + 2*k distinct random interfaces within `network`. 331 332 `gap` is either an int or a list of ints. ``k = 1`` when gap is an int, 333 ``k = len(gap)`` otherwise. 334 335 The returned list has the form: 336 [ip_min1, ip_max1, ip_min2, ip_max2, ..., ip_mink, ip_maxk, ip1, ..., ipn] 337 338 Guarantees: 339 - int(ip_max_i.ip) - int(ip_min_i.ip) == gap[i] (or gap when gap is an int) 340 - ip_max_i < ip_min_{i+1} (ranges are strictly ordered, non-overlapping) 341 - ip1 .. ipn are outside every range [ip_min_i, ip_max_i] 342 343 Raises ValueError if the constraints cannot be satisfied. 344 """ 345 gaps = [gap] if isinstance(gap, int) else list(gap) 346 k = len(gaps) 347 348 ex_ips = set(exclude_ips) if exclude_ips else set() 349 ex_nets = list(exclude_nets) if exclude_nets else [] 350 351 base = int(network.network_address) 352 total = network.num_addresses 353 prefixlen = network.prefixlen 354 iface_cls = _iface_cls(network) 355 356 def make(i): 357 return iface_cls((base + i, prefixlen)) 358 359 def is_excluded(ip): 360 return ip in ex_ips or any(ip in net for net in ex_nets) 361 362 # Minimum space: k ranges placed back-to-back with 1-address gaps between them. 363 # Minimum last offset: sum(gaps) + k - 1; need total >= sum(gaps) + k. 364 min_needed = sum(gaps) + k 365 if total < min_needed: 366 raise ValueError( 367 f"Network {network} is too small to fit {k} range(s) with gaps {gaps}" 368 ) 369 370 # suffix_sums[i] = sum(gaps[i:]) 371 suffix_sums = [0] * (k + 1) 372 for i in range(k - 1, -1, -1): 373 suffix_sums[i] = suffix_sums[i + 1] + gaps[i] 374 375 def hi_for(i): 376 """Max start offset for range i that still leaves room for ranges i+1..k-1.""" 377 return total - 1 - suffix_sums[i] - (k - 1 - i) 378 379 SMALL = 65536 380 381 def try_place_ranges(): 382 """Try one left-to-right placement. Returns [(start, end), ...] or None.""" 383 range_offsets = [] 384 lo = 0 385 for i in range(k): 386 h = hi_for(i) 387 if h < lo: 388 return None 389 g = gaps[i] 390 space = h - lo + 1 391 if space <= SMALL: 392 candidates = [ 393 j 394 for j in range(lo, h + 1) 395 if not is_excluded(make(j)) and not is_excluded(make(j + g)) 396 ] 397 if not candidates: 398 return None 399 s = random.choice(candidates) 400 else: 401 s = None 402 for _ in range(1000): 403 c = random.randint(lo, h) 404 if not is_excluded(make(c)) and not is_excluded(make(c + g)): 405 s = c 406 break 407 if s is None: 408 return None 409 range_offsets.append((s, s + g)) 410 lo = s + g + 1 411 return range_offsets 412 413 range_offsets = None 414 for _ in range(1000): 415 range_offsets = try_place_ranges() 416 if range_offsets is not None: 417 break 418 if range_offsets is None: 419 raise ValueError( 420 f"Could not place {k} range(s) with gaps {gaps} with the given exclusions" 421 ) 422 423 def in_any_range(offset): 424 return any(s <= offset <= e for s, e in range_offsets) 425 426 def extra_available(offset): 427 ip = make(offset) 428 return ( 429 ip not in ex_ips 430 and not in_any_range(offset) 431 and not any(ip in net for net in ex_nets) 432 ) 433 434 if total <= SMALL: 435 extra_candidates = [make(i) for i in range(total) if extra_available(i)] 436 if len(extra_candidates) < n: 437 raise ValueError( 438 f"Not enough addresses outside the range(s) for {n} extra IPs: " 439 f"found {len(extra_candidates)}" 440 ) 441 extras = random.sample(extra_candidates, n) 442 else: 443 chosen = set() 444 extras = [] 445 for _ in range(n): 446 for _ in range(1000): 447 offset = random.randrange(total) 448 candidate = make(offset) 449 if extra_available(offset) and candidate not in chosen: 450 extras.append(candidate) 451 chosen.add(candidate) 452 break 453 else: 454 raise ValueError( 455 f"Could not find enough extra addresses (got {len(extras)}/{n})" 456 ) 457 458 result = [] 459 for s, e in range_offsets: 460 result.append(make(s)) 461 result.append(make(e)) 462 result.extend(extras) 463 return result 464 465 466def random_ipv4s_with_range(network, gap, n=1, exclude_ips=None, exclude_nets=None): 467 """Return a list of n + 2*k distinct random IPv4Interface within `network`. 468 469 `gap` is either an int or a list of ints. ``k = 1`` when gap is an int, 470 ``k = len(gap)`` otherwise. 471 472 The returned list has the form: 473 [ip_min1, ip_max1, ip_min2, ip_max2, ..., ip_mink, ip_maxk, ip1, ..., ipn] 474 475 Guarantees: 476 - int(ip_max_i.ip) - int(ip_min_i.ip) == gap[i] (or gap when gap is an int) 477 - ip_max_i < ip_min_{i+1} (ranges are strictly ordered, non-overlapping) 478 - ip1 .. ipn are outside every range [ip_min_i, ip_max_i] 479 480 Unlike random_ipv4s, every offset of the network is eligible (including the first 481 and the last address). 482 483 Raises ValueError if the constraints cannot be satisfied. 484 """ 485 _check_version("random_ipv4s_with_range", network, 4) 486 return _random_addresses_with_range(network, gap, n, exclude_ips, exclude_nets) 487 488 489def random_ipv6s_with_range(network, gap, n=1, exclude_ips=None, exclude_nets=None): 490 """IPv6 twin of random_ipv4s_with_range: n + 2*k distinct random IPv6Interface within 491 `network`, the first 2*k forming k ordered ranges of the given gap(s), the last n 492 outside those ranges. Every offset of the network is eligible. 493 494 Raises ValueError if the constraints cannot be satisfied. 495 """ 496 _check_version("random_ipv6s_with_range", network, 6) 497 return _random_addresses_with_range(network, gap, n, exclude_ips, exclude_nets) 498 499 500def _assign_from_topology(machine_nets, nets, ips, picker, nets_label): 501 """Fill the container *ips* from the networks of *nets* for every machine of *machine_nets*.""" 502 assigned = {} # {net_name: list[interface]} — used addresses per network, to avoid duplicates 503 for machine, net_names in machine_nets.items(): 504 for net_name in net_names: 505 try: 506 network = getattr(nets, net_name) 507 except AttributeError: 508 raise AttributeError( 509 f"random_ips_from_topology: data.{nets_label} has no network {net_name!r}") from None 510 ip = picker(network, 1, exclude_ips=assigned.get(net_name))[0] 511 assigned.setdefault(net_name, []).append(ip) 512 attr = machine if len(net_names) == 1 else f"{machine}_{net_name}" 513 setattr(ips, attr, ip) 514 515 516def random_ips_from_topology(data, topology, ipv4=True, ipv6=False): 517 """Assign random IPs to data.ips (from data.nets) and/or data.ips6 (from data.nets6) 518 based on a NetScheme0 topology. 519 520 topology: {net_name: [machine, ...] or {machine: iface_spec, ...}} 521 (the _topology class attribute format of NetScheme0) 522 523 For each machine m: 524 - belongs to exactly one network netX → data.ips.m (in data.nets.netX) 525 - belongs to multiple networks → data.ips.m_netX for each netX 526 527 `ipv4=True` (default) fills data.ips from data.nets as always; `ipv6=True` also fills 528 data.ips6 from data.nets6 with the same naming rule (use `ipv4=False, ipv6=True` for an 529 IPv6-only lab). Every network of *topology* must exist in the containers used; 530 AttributeError otherwise. 531 """ 532 if not ipv4 and not ipv6: 533 raise ValueError("random_ips_from_topology: at least one of ipv4/ipv6 must be True") 534 # Build inverse map: machine -> [net_name, ...] (preserve insertion order) 535 machine_nets = {} 536 for net_name, machines in topology.items(): 537 names = machines.keys() if isinstance(machines, dict) else machines 538 for m in names: 539 machine_nets.setdefault(m, []).append(net_name) 540 541 if ipv4: 542 _assign_from_topology(machine_nets, data.nets, data.ips, random_ipv4s, 'nets') 543 if ipv6: 544 _assign_from_topology(machine_nets, data.nets6, data.ips6, random_ipv6s, 'nets6') 545 546 547def random_mac_address(prefix=None, n=1): 548 """Return a list of n distinct random EUI MAC addresses. 549 550 `prefix` is an optional colon- or dash-separated hex string specifying the 551 leading bytes (e.g. ``"00:1A:2B"`` for a 3-byte OUI prefix). 552 Remaining bytes are chosen at random. 553 554 Raises ValueError if n distinct addresses cannot be generated. 555 """ 556 if prefix is not None: 557 sep = "-" if "-" in prefix else ":" 558 prefix_bytes = bytes(int(x, 16) for x in prefix.split(sep)) 559 else: 560 prefix_bytes = b"" 561 562 suffix_len = 6 - len(prefix_bytes) 563 if suffix_len < 0: 564 raise ValueError( 565 f"Prefix too long: {prefix!r} ({len(prefix_bytes)} bytes, max 6)" 566 ) 567 568 total = 256**suffix_len 569 if n > total: 570 raise ValueError( 571 f"Cannot generate {n} distinct MACs with {suffix_len} random bytes (max {total})" 572 ) 573 574 def make(suffix_bytes): 575 all_bytes = prefix_bytes + suffix_bytes 576 if prefix is None: 577 all_bytes = bytes([all_bytes[0] & 0xFE]) + all_bytes[1:] 578 return EUI(":".join(f"{b:02x}" for b in all_bytes)) 579 580 if total <= 65536: 581 candidates = [make(i.to_bytes(suffix_len, "big")) for i in range(total)] 582 return random.sample(candidates, n) 583 584 seen = set() 585 result = [] 586 for _ in range(n): 587 for _ in range(1000): 588 suffix = random.randbytes(suffix_len) 589 if suffix not in seen: 590 seen.add(suffix) 591 result.append(make(suffix)) 592 break 593 else: 594 raise ValueError( 595 f"Could not generate {n} distinct MACs after 1000 attempts" 596 ) 597 return result
54class IPv4Addresses(_AttrContainer): 55 """Dynamic container for named IPv4Interface attributes, with no boilerplate. 56 57 Usage: 58 ips = IPv4Addresses() 59 ips.ip1 = IPv4Interface("192.168.1.1/24") 60 ips.ip2 = IPv4Interface("192.168.1.2/24") 61 """ 62 _item_type = IPv4Interface 63 _type_name = "IPv4Interface"
Dynamic container for named IPv4Interface attributes, with no boilerplate.
Usage:
ips = IPv4Addresses() ips.ip1 = IPv4Interface("192.168.1.1/24") ips.ip2 = IPv4Interface("192.168.1.2/24")
66class IPv4Networks(_AttrContainer): 67 """Dynamic container for named IPv4Network attributes, with no boilerplate. 68 69 Usage: 70 nets = IPv4Networks() 71 nets.lan = IPv4Network("192.168.1.0/24") 72 nets.mgmt = IPv4Network("10.0.0.0/8") 73 """ 74 _item_type = IPv4Network 75 _type_name = "IPv4Network"
Dynamic container for named IPv4Network attributes, with no boilerplate.
Usage:
nets = IPv4Networks() nets.lan = IPv4Network("192.168.1.0/24") nets.mgmt = IPv4Network("10.0.0.0/8")
78class IPv6Addresses(_AttrContainer): 79 """Dynamic container for named IPv6Interface attributes (the IPv6 twin of IPv4Addresses). 80 81 Usage: 82 ips6 = IPv6Addresses() 83 ips6.router = IPv6Interface("2001:db8:1::1/64") 84 """ 85 _item_type = IPv6Interface 86 _type_name = "IPv6Interface"
Dynamic container for named IPv6Interface attributes (the IPv6 twin of IPv4Addresses).
Usage:
ips6 = IPv6Addresses() ips6.router = IPv6Interface("2001:db8:1::1/64")
89class IPv6Networks(_AttrContainer): 90 """Dynamic container for named IPv6Network attributes (the IPv6 twin of IPv4Networks). 91 92 Usage: 93 nets6 = IPv6Networks() 94 nets6.lan = IPv6Network("2001:db8:1::/64") 95 """ 96 _item_type = IPv6Network 97 _type_name = "IPv6Network"
Dynamic container for named IPv6Network attributes (the IPv6 twin of IPv4Networks).
Usage:
nets6 = IPv6Networks() nets6.lan = IPv6Network("2001:db8:1::/64")
199def random_ipv4networks( 200 masks, 201 from_network=IPv4Network("0.0.0.0/0"), 202 exclude=None, 203 from_private_network=False, 204): 205 """Return a list of disjoint random IPv4Networks. 206 207 `masks` is an int or a list of ints; one network is returned per mask, 208 all mutually disjoint and not overlapping any network in `exclude`. 209 Optionally restricted to RFC-1918 private ranges with `from_private_network=True`. 210 211 `from` being a reserved keyword, the parameter is named `from_network`. 212 213 Raises ValueError if any network cannot be allocated. 214 """ 215 _check_version("random_ipv4networks", from_network, 4) 216 return _random_networks(masks, from_network, exclude, from_private_network)
Return a list of disjoint random IPv4Networks.
masks is an int or a list of ints; one network is returned per mask,
all mutually disjoint and not overlapping any network in exclude.
Optionally restricted to RFC-1918 private ranges with from_private_network=True.
from being a reserved keyword, the parameter is named from_network.
Raises ValueError if any network cannot be allocated.
219def random_ipv6networks( 220 masks, 221 from_network=None, 222 exclude=None, 223 from_private_network=False, 224): 225 """Return a list of disjoint random IPv6Networks (the IPv6 twin of random_ipv4networks). 226 227 `masks` is an int or a list of ints (prefix lengths, e.g. 64); one network is returned 228 per mask, all mutually disjoint and not overlapping any network in `exclude`. 229 230 `from_network=None` (default) means the global unicast space ``2000::/3``, or ``::/0`` 231 when `from_private_network=True`: the search space is then the unique local range 232 ``fd00::/8`` (RFC 4193). Use ``from_network=IPv6Network('2001:db8::/32')`` for 233 documentation prefixes. 234 235 Raises ValueError if any network cannot be allocated. 236 """ 237 if from_network is None: 238 from_network = IPv6Network("::/0") if from_private_network else _GLOBAL_UNICAST_V6 239 _check_version("random_ipv6networks", from_network, 6) 240 return _random_networks(masks, from_network, exclude, from_private_network)
Return a list of disjoint random IPv6Networks (the IPv6 twin of random_ipv4networks).
masks is an int or a list of ints (prefix lengths, e.g. 64); one network is returned
per mask, all mutually disjoint and not overlapping any network in exclude.
from_network=None (default) means the global unicast space 2000::/3, or ::/0
when from_private_network=True: the search space is then the unique local range
fd00::/8 (RFC 4193). Use from_network=IPv6Network('2001:db8::/32') for
documentation prefixes.
Raises ValueError if any network cannot be allocated.
302def random_ipv4s(network, n=1, exclude_ips=None, exclude_nets=None): 303 """Return a list of n distinct random IPv4Interface within `network`, 304 excluding any address in `exclude_ips` or covered by any network in `exclude_nets`. 305 306 The network address and the broadcast address are never returned (prefix <= /30). 307 Note: `exclude_ips` is compared with the prefix, so list IPv4Interface values 308 (as returned by this function), not bare IPv4Address values. 309 310 Raises ValueError if fewer than n addresses are available. 311 """ 312 _check_version("random_ipv4s", network, 4) 313 return _random_addresses(network, n, exclude_ips, exclude_nets)
Return a list of n distinct random IPv4Interface within network,
excluding any address in exclude_ips or covered by any network in exclude_nets.
The network address and the broadcast address are never returned (prefix <= /30).
Note: exclude_ips is compared with the prefix, so list IPv4Interface values
(as returned by this function), not bare IPv4Address values.
Raises ValueError if fewer than n addresses are available.
316def random_ipv6s(network, n=1, exclude_ips=None, exclude_nets=None): 317 """Return a list of n distinct random IPv6Interface within `network` (the IPv6 twin 318 of random_ipv4s), excluding any address in `exclude_ips` or covered by any network 319 in `exclude_nets`. 320 321 Offset 0 (the subnet-router anycast address) is never returned up to /126; /127 and 322 /128 use every address. 323 324 Raises ValueError if fewer than n addresses are available. 325 """ 326 _check_version("random_ipv6s", network, 6) 327 return _random_addresses(network, n, exclude_ips, exclude_nets)
Return a list of n distinct random IPv6Interface within network (the IPv6 twin
of random_ipv4s), excluding any address in exclude_ips or covered by any network
in exclude_nets.
Offset 0 (the subnet-router anycast address) is never returned up to /126; /127 and /128 use every address.
Raises ValueError if fewer than n addresses are available.
467def random_ipv4s_with_range(network, gap, n=1, exclude_ips=None, exclude_nets=None): 468 """Return a list of n + 2*k distinct random IPv4Interface within `network`. 469 470 `gap` is either an int or a list of ints. ``k = 1`` when gap is an int, 471 ``k = len(gap)`` otherwise. 472 473 The returned list has the form: 474 [ip_min1, ip_max1, ip_min2, ip_max2, ..., ip_mink, ip_maxk, ip1, ..., ipn] 475 476 Guarantees: 477 - int(ip_max_i.ip) - int(ip_min_i.ip) == gap[i] (or gap when gap is an int) 478 - ip_max_i < ip_min_{i+1} (ranges are strictly ordered, non-overlapping) 479 - ip1 .. ipn are outside every range [ip_min_i, ip_max_i] 480 481 Unlike random_ipv4s, every offset of the network is eligible (including the first 482 and the last address). 483 484 Raises ValueError if the constraints cannot be satisfied. 485 """ 486 _check_version("random_ipv4s_with_range", network, 4) 487 return _random_addresses_with_range(network, gap, n, exclude_ips, exclude_nets)
Return a list of n + 2*k distinct random IPv4Interface within network.
gap is either an int or a list of ints. k = 1 when gap is an int,
k = len(gap) otherwise.
The returned list has the form:
[ip_min1, ip_max1, ip_min2, ip_max2, ..., ip_mink, ip_maxk, ip1, ..., ipn]
Guarantees:
- int(ip_max_i.ip) - int(ip_min_i.ip) == gap[i] (or gap when gap is an int)
- ip_max_i < ip_min_{i+1} (ranges are strictly ordered, non-overlapping)
- ip1 .. ipn are outside every range [ip_min_i, ip_max_i]
Unlike random_ipv4s, every offset of the network is eligible (including the first and the last address).
Raises ValueError if the constraints cannot be satisfied.
490def random_ipv6s_with_range(network, gap, n=1, exclude_ips=None, exclude_nets=None): 491 """IPv6 twin of random_ipv4s_with_range: n + 2*k distinct random IPv6Interface within 492 `network`, the first 2*k forming k ordered ranges of the given gap(s), the last n 493 outside those ranges. Every offset of the network is eligible. 494 495 Raises ValueError if the constraints cannot be satisfied. 496 """ 497 _check_version("random_ipv6s_with_range", network, 6) 498 return _random_addresses_with_range(network, gap, n, exclude_ips, exclude_nets)
IPv6 twin of random_ipv4s_with_range: n + 2k distinct random IPv6Interface within
network, the first 2k forming k ordered ranges of the given gap(s), the last n
outside those ranges. Every offset of the network is eligible.
Raises ValueError if the constraints cannot be satisfied.
517def random_ips_from_topology(data, topology, ipv4=True, ipv6=False): 518 """Assign random IPs to data.ips (from data.nets) and/or data.ips6 (from data.nets6) 519 based on a NetScheme0 topology. 520 521 topology: {net_name: [machine, ...] or {machine: iface_spec, ...}} 522 (the _topology class attribute format of NetScheme0) 523 524 For each machine m: 525 - belongs to exactly one network netX → data.ips.m (in data.nets.netX) 526 - belongs to multiple networks → data.ips.m_netX for each netX 527 528 `ipv4=True` (default) fills data.ips from data.nets as always; `ipv6=True` also fills 529 data.ips6 from data.nets6 with the same naming rule (use `ipv4=False, ipv6=True` for an 530 IPv6-only lab). Every network of *topology* must exist in the containers used; 531 AttributeError otherwise. 532 """ 533 if not ipv4 and not ipv6: 534 raise ValueError("random_ips_from_topology: at least one of ipv4/ipv6 must be True") 535 # Build inverse map: machine -> [net_name, ...] (preserve insertion order) 536 machine_nets = {} 537 for net_name, machines in topology.items(): 538 names = machines.keys() if isinstance(machines, dict) else machines 539 for m in names: 540 machine_nets.setdefault(m, []).append(net_name) 541 542 if ipv4: 543 _assign_from_topology(machine_nets, data.nets, data.ips, random_ipv4s, 'nets') 544 if ipv6: 545 _assign_from_topology(machine_nets, data.nets6, data.ips6, random_ipv6s, 'nets6')
Assign random IPs to data.ips (from data.nets) and/or data.ips6 (from data.nets6) based on a NetScheme0 topology.
topology: {net_name: [machine, ...] or {machine: iface_spec, ...}} (the _topology class attribute format of NetScheme0)
For each machine m:
- belongs to exactly one network netX → data.ips.m (in data.nets.netX)
- belongs to multiple networks → data.ips.m_netX for each netX
ipv4=True (default) fills data.ips from data.nets as always; ipv6=True also fills
data.ips6 from data.nets6 with the same naming rule (use ipv4=False, ipv6=True for an
IPv6-only lab). Every network of topology must exist in the containers used;
AttributeError otherwise.
548def random_mac_address(prefix=None, n=1): 549 """Return a list of n distinct random EUI MAC addresses. 550 551 `prefix` is an optional colon- or dash-separated hex string specifying the 552 leading bytes (e.g. ``"00:1A:2B"`` for a 3-byte OUI prefix). 553 Remaining bytes are chosen at random. 554 555 Raises ValueError if n distinct addresses cannot be generated. 556 """ 557 if prefix is not None: 558 sep = "-" if "-" in prefix else ":" 559 prefix_bytes = bytes(int(x, 16) for x in prefix.split(sep)) 560 else: 561 prefix_bytes = b"" 562 563 suffix_len = 6 - len(prefix_bytes) 564 if suffix_len < 0: 565 raise ValueError( 566 f"Prefix too long: {prefix!r} ({len(prefix_bytes)} bytes, max 6)" 567 ) 568 569 total = 256**suffix_len 570 if n > total: 571 raise ValueError( 572 f"Cannot generate {n} distinct MACs with {suffix_len} random bytes (max {total})" 573 ) 574 575 def make(suffix_bytes): 576 all_bytes = prefix_bytes + suffix_bytes 577 if prefix is None: 578 all_bytes = bytes([all_bytes[0] & 0xFE]) + all_bytes[1:] 579 return EUI(":".join(f"{b:02x}" for b in all_bytes)) 580 581 if total <= 65536: 582 candidates = [make(i.to_bytes(suffix_len, "big")) for i in range(total)] 583 return random.sample(candidates, n) 584 585 seen = set() 586 result = [] 587 for _ in range(n): 588 for _ in range(1000): 589 suffix = random.randbytes(suffix_len) 590 if suffix not in seen: 591 seen.add(suffix) 592 result.append(make(suffix)) 593 break 594 else: 595 raise ValueError( 596 f"Could not generate {n} distinct MACs after 1000 attempts" 597 ) 598 return result
Return a list of n distinct random EUI MAC addresses.
prefix is an optional colon- or dash-separated hex string specifying the
leading bytes (e.g. "00:1A:2B" for a 3-byte OUI prefix).
Remaining bytes are chosen at random.
Raises ValueError if n distinct addresses cannot be generated.