mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
159 lines
5.0 KiB
Python
159 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
import ipaddress
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .keys import generate_private_key, public_key_of
|
|
from .model import Mesh, MeshMember
|
|
|
|
if TYPE_CHECKING:
|
|
from .model import MeshSpec, Topology
|
|
|
|
HUB_OFFSET = 1
|
|
SPOKE_OFFSET = 10
|
|
|
|
|
|
def members_of(
|
|
spec: MeshSpec, topology: Topology, controller: str | None = None
|
|
) -> tuple[str | None, list[str]]:
|
|
spokes = [
|
|
host
|
|
for host in dict.fromkeys(topology.spokes.get(spec.name, ()))
|
|
if host != topology.hub
|
|
]
|
|
if controller and controller != topology.hub and controller not in spokes:
|
|
spokes.append(controller)
|
|
return topology.hub, sorted(spokes)
|
|
|
|
|
|
def primary_meshes(
|
|
specs: list[MeshSpec],
|
|
topology: Topology,
|
|
controller: str | None = None,
|
|
controller_mesh: str | None = None,
|
|
) -> dict[str, str]:
|
|
"""The first declared mesh each host belongs to.
|
|
|
|
Args:
|
|
specs: every declared mesh, in declaration order.
|
|
topology: who the inventory puts into each mesh.
|
|
controller: host admitted as an extra spoke of ``controller_mesh``.
|
|
controller_mesh: the mesh ``controller`` joins.
|
|
|
|
Returns:
|
|
hostname to the name of its primary mesh.
|
|
"""
|
|
primary: dict[str, str] = {}
|
|
for spec in specs:
|
|
hub, spokes = members_of(
|
|
spec, topology, controller if spec.name == controller_mesh else None
|
|
)
|
|
for host in [hub, *spokes] if hub else spokes:
|
|
primary.setdefault(host, spec.name)
|
|
return primary
|
|
|
|
|
|
def _address(subnet: str, offset: int) -> str:
|
|
network = ipaddress.ip_network(subnet, strict=True)
|
|
address = network.network_address + offset
|
|
if address not in network:
|
|
raise ValueError(f"offset {offset} falls outside {subnet}")
|
|
return str(address)
|
|
|
|
|
|
def _offset_of(subnet: str, address: str) -> int | None:
|
|
network = ipaddress.ip_network(subnet, strict=True)
|
|
try:
|
|
candidate = ipaddress.ip_address(address)
|
|
except ValueError:
|
|
return None
|
|
if candidate not in network:
|
|
return None
|
|
offset = int(candidate) - int(network.network_address)
|
|
return offset if offset > SPOKE_OFFSET else None
|
|
|
|
|
|
def _spoke_offsets(
|
|
subnet: str, spokes: list[str], stored: dict[str, str]
|
|
) -> dict[str, int]:
|
|
"""Hostname to its offset, keeping every address a spoke already holds.
|
|
|
|
Args:
|
|
subnet: the mesh subnet offsets are taken in.
|
|
spokes: every spoke of the mesh, in the order they were resolved.
|
|
stored: hostname to the address it already holds on this mesh.
|
|
"""
|
|
taken = {HUB_OFFSET}
|
|
offsets: dict[str, int] = {}
|
|
for host in spokes:
|
|
offset = _offset_of(subnet, stored.get(host, ""))
|
|
if offset is None or offset in taken:
|
|
continue
|
|
offsets[host] = offset
|
|
taken.add(offset)
|
|
|
|
cursor = SPOKE_OFFSET + 1
|
|
for host in spokes:
|
|
if host in offsets:
|
|
continue
|
|
while cursor in taken:
|
|
cursor += 1
|
|
offsets[host] = cursor
|
|
taken.add(cursor)
|
|
return offsets
|
|
|
|
|
|
def plan_mesh(
|
|
spec: MeshSpec,
|
|
topology: Topology,
|
|
existing_public_keys: dict[str, str] | None = None,
|
|
*,
|
|
rotate: bool = False,
|
|
controller: str | None = None,
|
|
endpoints: dict[str, str] | None = None,
|
|
addresses: dict[str, str] | None = None,
|
|
) -> Mesh:
|
|
"""Resolve ``spec`` into a mesh whose members are addressed and keyed.
|
|
|
|
Args:
|
|
spec: the mesh to resolve.
|
|
topology: who the inventory puts into each mesh.
|
|
existing_public_keys: hostname to already-issued public key.
|
|
rotate: mint a fresh keypair for every member.
|
|
controller: host to admit as an extra spoke.
|
|
endpoints: hostname to the underlay address its peers dial; a host
|
|
left out is dialled by its hostname.
|
|
addresses: hostname to the mesh address it already holds, kept across
|
|
a rotation.
|
|
"""
|
|
held = {} if rotate else dict(existing_public_keys or {})
|
|
underlay = dict(endpoints or {})
|
|
hub, spokes = members_of(spec, topology, controller)
|
|
offsets = _spoke_offsets(spec.subnet, spokes, dict(addresses or {}))
|
|
|
|
def member(host: str, offset: int, is_hub: bool) -> MeshMember:
|
|
address = _address(spec.subnet, offset)
|
|
endpoint = underlay.get(host, host)
|
|
if host in held:
|
|
return MeshMember(
|
|
host=host,
|
|
address=address,
|
|
private_key=None,
|
|
public_key=held[host],
|
|
is_hub=is_hub,
|
|
endpoint=endpoint,
|
|
)
|
|
private_key = generate_private_key()
|
|
return MeshMember(
|
|
host=host,
|
|
address=address,
|
|
private_key=private_key,
|
|
public_key=public_key_of(private_key),
|
|
is_hub=is_hub,
|
|
endpoint=endpoint,
|
|
)
|
|
|
|
members = [member(hub, HUB_OFFSET, True)] if hub else []
|
|
members.extend(member(host, offsets[host], False) for host in spokes)
|
|
return Mesh(spec=spec, members=tuple(members))
|