mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 20:26:33 +00:00
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>
186 lines
5.6 KiB
Python
Executable File
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())
|