Commit Graph
9 Commits
Author SHA1 Message Date
evangelostsak cc38fe6232 refactor: drop rationale and narration from cli/administration/inventory/mesh/plan.py 2026-10-16 19:10:00 +02:00
evangelostsak acef74a150 feat(mesh-cli): plan meshes without a hub
With no hub every joined host is a member, addressed from the same offsets a
spoke takes, and each host is primary on the first declared mesh it joined.
2026-10-16 03:40:15 +02:00
evangelostsak be55cebab8 refactor(mesh): plan against the resolved topology
members_of, primary_meshes and plan_mesh take the Topology instead of raw
inventory groups. The spokes come out deduplicated, sorted and without the
hub, as before.
2026-10-14 15:03:40 +02:00
evangelostsak 4321d3a342 feat(mesh): resolve each host's primary mesh
A host's primary mesh is the first declared mesh it belongs to. It is the one
interface a spoke of several meshes may route the pool through, and the
address the other planes reach the host on.
2026-10-14 11:23:22 +02:00
evangelostsak 379dface8c docs(mesh-cli): reduce the planner docstrings to summary and Args
The prose explaining why offsets are reused and how the hub keeps index
zero is recorded in requirement 037. A reader of plan.py needs the signature,
not the history.
2026-10-05 12:43:23 +02:00
evangelostsak dc14e8a3a8 fix(mesh): keep every member on the address it already holds
The offset came from the spoke's position in a sorted list, so a host that
sorts earlier renumbered every member behind it. `wg syncconf` carries keys
and peers onto a live interface and never its address, so a renumbered member
keeps answering on the old address while the peers and the inventory have
moved to the new one -- and the address it gave up can be handed to a new
member while the old owner still holds it.

The planner now reuses the addresses already on disk and allocates only an
unseen member, from the lowest free offset. A value outside the subnet, a
duplicate, and the hub's own offset are refused, so a mirrored host_vars
cannot hand a spoke the hub's address. Rotation keeps addresses: it replaces
identities, not addresses.

The routing assertions re-planned the mesh and compared the result with
itself, so a writer that stored the mesh subnet in place of the routed range
would have passed; they now read the entry off disk, which is what the role
reads.
2026-10-02 00:54:20 +02:00
evangelostsak b0697c08d6 feat(mesh): resolve each member's endpoint when planning
Defaults to the inventory name, which is what peers already dial wherever
those names resolve for every member.
2026-09-27 02:03:05 +02:00
evangelostsak 435d839ac5 feat(mesh): admit the Ansible controller as a spoke
Named rather than resolved from a group: the controller is not a swarm member
and has no role group, and inventing one would put a non-role name into
group_names where every service and placement lookup would special-case it.
2026-09-26 01:27:07 +02:00
evangelostsak dae11dd57e feat(inventory): add cross-host credential generation
Every credential generator in this package is single-host scoped.
credentials_generator shells out once per (app_id, host_vars_file) pair, and
that subprocess sees one host's inventory with no knowledge of any other. A
mesh credential breaks the assumption: the value one host receives is derived
from a secret minted for a different host, because a peer holds the public half
of a key whose private half lives elsewhere.

This package is the missing writer that sees the whole host set at once, so a
mesh is written complete or not at all. It is specified as cross-host
credential generation rather than as a WireGuard utility -- the application id
is a parameter and a mesh is a named hub-and-spoke grouping of inventory hosts,
so a later consumer needing paired secrets reuses it unmodified.

Planning is a pure function of the inventory, the spec and the public keys that
already exist, so mesh completeness, key pairing and rotation are provable
without a deploy. Keys are minted in-process with Curve25519 in the encoding
WireGuard expects, so the controller needs no wg binary to plan a mesh it never
joins, and a public key is always derived from its private half rather than
stored, because a stored pair can drift into a mesh that looks configured and
never handshakes.

Group membership is read across every inventory file it is spread over: the
swarm lab keeps its backup host in a sibling backup.yml, so a mesh spanning
both planes is only complete when both files have been read.

Vault encryption is salted, so a member that already holds a key is left
untouched rather than re-encrypted; otherwise every run would rewrite the file
and no deploy could be a no-op.
2026-09-18 09:06:52 +02:00