Files
computer-playbook/cli/contributing/network/address/suggest/__main__.py
T
kevinveenbirkenbachandClaude Opus 5 5137465c6f style: delete every comment the comments-valid policy rejects
3529 invalid comments in 528 files, grandfathered in while the linter
only saw the working-tree diff. Removal ran through the linter's own
collectors, so detection and deletion cannot drift: a comment whose
opener line is blank before it and whose closer line is blank after it
loses all its lines, anything else loses only the comment region, and a
line left holding a bare delimiter goes with it.

Five files keep their comments behind a nocheck marker instead. Four are
upstream configs whose inline docs are upstream's own (mautrix, systemd
journald, nextcloud-oidc-login, MediaWiki). In littlejs the
commented-out entries ARE the deactivation mechanism for an example
tile, so deleting them would delete configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:22:02 +02:00

186 lines
5.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Suggest free per-role subnets based on the live role tree.
Walks every ``roles/*/meta/networks.yml``, collects occupied
``local.subnet`` assignments, picks the smallest CIDR prefix that
fits the requested client count, and proposes free sub-blocks gap-first.
Usage:
cli contributing network address suggest --clients 14 --count 2
cli contributing network address suggest --clients 254 --count 1
cli contributing network address suggest --clients 14 --block 192.168.101.0/24
"""
from __future__ import annotations
import argparse
import ipaddress
import sys
from typing import TYPE_CHECKING
from utils.meta.scan import iter_subnets
if TYPE_CHECKING:
from collections.abc import Iterable
_PREFIX_THRESHOLDS: tuple[tuple[int, int], ...] = (
(2, 30),
(6, 29),
(14, 28),
(30, 27),
(62, 26),
(126, 25),
(254, 24),
(510, 23),
(1022, 22),
(2046, 21),
(4094, 20),
)
def smallest_prefix(clients: int) -> int:
if clients < 1:
raise SystemExit("--clients must be >= 1")
for max_clients, prefix in _PREFIX_THRESHOLDS:
if clients <= max_clients:
return prefix
raise SystemExit(
f"--clients {clients} exceeds the largest supported prefix; "
f"pass --block <cidr> to bootstrap a new umbrella block manually."
)
def umbrella_blocks_for(prefix: int) -> list[ipaddress.IPv4Network]:
"""Return the set of /24 (or coarser) umbrella blocks already in use for
sub-allocations of the requested prefix length.
"""
occupied = [net for _r, net in iter_subnets() if net.prefixlen == prefix]
if not occupied:
return []
umbrellas: set[ipaddress.IPv4Network] = set()
for net in occupied:
umbrella_prefix = min(prefix, 24)
umbrellas.add(net.supernet(new_prefix=umbrella_prefix))
return sorted(umbrellas, key=lambda n: int(n.network_address))
def gap_first_subnets(
block: ipaddress.IPv4Network, prefix: int, occupied: Iterable[ipaddress.IPv4Network]
) -> Iterable[ipaddress.IPv4Network]:
occupied_set = {net for net in occupied if block.supernet_of(net)}
for sub in block.subnets(new_prefix=prefix):
if sub in occupied_set:
continue
yield sub
def suggest_subnets(
clients: int,
count: int,
explicit_block: ipaddress.IPv4Network | None,
) -> tuple[list[ipaddress.IPv4Network], int]:
prefix = smallest_prefix(clients)
occupied = sorted(
{net for _r, net in iter_subnets() if net.prefixlen == prefix},
key=lambda n: int(n.network_address),
)
if explicit_block is not None:
if explicit_block.prefixlen > prefix:
raise SystemExit(
f"--block {explicit_block} prefix /{explicit_block.prefixlen} "
f"is smaller than the required /{prefix}."
)
candidate_blocks = [explicit_block]
else:
candidate_blocks = umbrella_blocks_for(prefix)
if not candidate_blocks:
raise SystemExit(
f"no umbrella block established for prefix /{prefix}; pass "
f"--block <cidr> to bootstrap a new one manually."
)
suggestions: list[ipaddress.IPv4Network] = []
gaps_filled = 0
for block in candidate_blocks:
for sub in gap_first_subnets(block, prefix, occupied):
suggestions.append(sub)
gaps_filled += 1
occupied.append(sub)
if len(suggestions) >= count:
break
if len(suggestions) >= count:
break
if len(suggestions) < count:
raise SystemExit(
f"available umbrella blocks {[str(b) for b in candidate_blocks]} "
f"cannot satisfy --count {count} subnet(s) at /{prefix}."
)
return suggestions, gaps_filled
def capacity_for(network: ipaddress.IPv4Network) -> int:
if network.prefixlen >= 31:
return 0
return network.num_addresses - 2
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
prog="cli contributing network address suggest",
description=(
"Suggest free per-role IPv4 subnets based on the live role tree. "
"Picks the smallest CIDR prefix that fits --clients, then "
"proposes free sub-blocks gap-first within established umbrella "
"blocks (or --block when given)."
),
)
parser.add_argument(
"--clients",
type=int,
required=True,
help="minimum number of usable client IPs the subnet must fit.",
)
parser.add_argument(
"--count",
type=int,
default=1,
help="how many free subnets to return (default: 1).",
)
parser.add_argument(
"--block",
help=(
"force suggestions inside a specific umbrella block, e.g. "
"'192.168.101.0/24'."
),
)
args = parser.parse_args(argv)
if args.count < 1:
raise SystemExit("--count must be >= 1")
explicit_block: ipaddress.IPv4Network | None = None
if args.block:
try:
explicit_block = ipaddress.IPv4Network(args.block)
except (ipaddress.AddressValueError, ValueError) as exc:
raise SystemExit(f"invalid --block {args.block!r}: {exc}") from exc
suggestions, gaps = suggest_subnets(args.clients, args.count, explicit_block)
for sub in suggestions:
print(str(sub))
sys.stderr.write(
f"# clients={args.clients} prefix=/{suggestions[0].prefixlen} "
f"capacity={capacity_for(suggestions[0])} returned={len(suggestions)} "
f"gaps={gaps}\n"
)
return 0
if __name__ == "__main__":
sys.exit(main())