Facts from other hosts
Some configuration of a host is about the other hosts: SSH knownHosts, WireGuard peers, Syncthing devices,
backup targets. The usual answers:
- A hand-written list of hosts in a shared file. It goes out of date when you add a host.
self.nixosConfigurations.<other>.configthroughspecialArgs. When two hosts read derived values from each other, the evaluation can recurse without end.- The collector aspect of the dendritic design guide: each host feature adds its own device to a shared NixOS module. It works, but every host gets every contribution, its own included.
In yen a host’s public facts are fields of its instance. Here are three hosts and their facts:
# Each host carries its public facts as fields. Every module can read every host: they are data, not NixOS configs.{ yen, lib, inputs, ...}:let str = yen.lib.str;in{ yen.kinds.fleet = { spawns.host = _: yen.host; sinks.out.reduce = yen.lib.reduce.merge; }; yen.kinds.host = { fields = { # (1) sshKey = str; wgKey = str; wgIp = str; }; sinks.nixos.out = { host, nixos, ... }: { nixosConfigurations.${host.name} = inputs.nixpkgs.lib.nixosSystem { modules = nixos; }; }; }; yen.fleet.main = { }; yen.flake = { out, ... }: out;
yen.host = { # (2) nas = { sshKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINas"; wgKey = "nas0wgPublicKey0000000000000000000000000000="; wgIp = "10.9.0.1"; }; desk = { sshKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDesk"; wgKey = "desk0wgPublicKey000000000000000000000000000="; wgIp = "10.9.0.2"; }; laptop = { sshKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILap"; wgKey = "laptop0wgPublicKey0000000000000000000000000="; wgIp = "10.9.0.3"; }; }; yen.aspects.machine = { at = yen.kinds.host; nixos = { host, ... }: { networking.hostName = host.name; boot.loader.grub.device = "nodev"; fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; nixpkgs.hostPlatform = "x86_64-linux"; system.stateVersion = "25.05"; }; };}(1) declares the facts as fields of the host kind. (2) gives their values for each host.
An aspect reads the other hosts through a tree request, a form of the cycle:
# known_hosts and WireGuard peers: every other host, from its fields. No hosts list to keep in sync.# Other hosts' data comes through a tree request, so each read is an edge `stage` can see.{ yen, lib, ... }:let all.instances = yen.kinds.host; # (1) a tree request: every host, with its data othersOf = tree: scope: lib.filter (h: !yen.lib.same h scope) (tree.select all); # every host but this onein{ yen.aspects.mesh = { # (2) at = yen.kinds.host; cycle = { tree, scope, ... }: { tree = [ all ]; # (3) nixos = [ { programs.ssh.knownHosts = lib.listToAttrs ( map (h: { name = h.name; value = { hostNames = [ h.name ]; publicKey = h.sshKey; }; }) (othersOf tree scope) ); networking.wireguard.interfaces.wg0 = { ips = [ "${scope.wgIp}/24" ]; privateKeyFile = "/var/lib/wireguard/private.key"; peers = map (h: { publicKey = h.wgKey; allowedIPs = [ "${h.wgIp}/32" ]; }) (othersOf tree scope); }; } ]; }; };}(1) is the tree request: every host, with its data. (2) is the mesh aspect. At (3) it asks the tree for
that request. The request is data, so nothing evaluates a NixOS system to read it, and
stage sees the read.
Facts that only some hosts have
Section titled “Facts that only some hosts have”A Syncthing device id exists only on hosts that run Syncthing. Each of them gives its id to the
syncthing-device sink of the fleet. A host has no such sink, so the id goes up to the fleet.
# A fact that only some hosts have: each Syncthing host gives its device id. The ids go up to the fleet;# every Syncthing host reads them back and adds the others as devices.{ yen, lib, ... }:{ yen.kinds.fleet.sinks.syncthing-device.reduce = yen.lib.reduce.elements; # (1) each id with the host that gave it
yen.aspects.syncthing = { at = [ yen.host.nas yen.host.desk ]; nixos = { host, syncthing-device, ... }: # (2) { services.syncthing = { enable = true; settings.devices = lib.listToAttrs ( map (e: lib.nameValuePair e.from.name { inherit (e.value) id; }) ( lib.filter (e: e.from.name != host.name) syncthing-device ) ); }; }; }; # the ids, in each host's own file in a real fleet yen.aspects.nas-id = { # (3) at = yen.host.nas; syncthing-device.id = "NAS1111-1111111-1111111-1111111-1111111-1111111-1111111-1111111"; }; yen.aspects.desk-id = { at = yen.host.desk; syncthing-device.id = "DESK222-2222222-2222222-2222222-2222222-2222222-2222222-2222222"; };}(1) is the sink of the fleet. Its reduce is elements, which keeps who gave each id. (2) is a Syncthing
host: it reads the sink as an arg, and leaves out its own id. (3) is one host that gives its id.