Skip to content

Assemblies and roles

A backup has roles: one server, many clients. An assembly says which machine takes which role, with refs:

modules/inventory.nix
{ yen, ... }:
{
yen.host.nas.addr = "10.0.0.10";
yen.host.web.addr = "10.0.0.1";
yen.host.db.addr = "10.0.0.2";
yen.assembly.backup.roles = {
# (1)
server = [ yen.host.nas ]; # (2)
client = [
yen.host.web
yen.host.db
];
};
}

At (1), the assembly backup has roles. At (2), server lists one machine. client lists two.

Each machine spawns one member scope per (assembly, role) it takes part in. This kind file does it:

modules/kinds.nix
# An assembly is data: which machines take which role. Each machine spawns one `member` scope per role it has.
{ yen, lib, ... }:
let
inherit (yen.lib) same is;
membersOf =
m:
lib.listToAttrs (
lib.concatMap (
a:
lib.concatMap (
role:
lib.optional (lib.any (same m) a.roles.${role}) (
# (1)
lib.nameValuePair "${a.name}.${role}" {
assembly = a; # (2)
inherit role;
}
)
) (lib.attrNames a.roles)
) (lib.attrValues yen.assembly)
);
in
{
yen.kinds.fleet = {
spawns.host = _: yen.host;
sinks = {
out.reduce = yen.lib.reduce.merge;
exports.reduce = yen.lib.reduce.elements; # (3) what members publish, with the scope that published it
};
};
yen.kinds.assembly.fields.roles = yen.lib.attrs; # role name -> host refs
yen.kinds.host = {
fields.addr = yen.lib.str;
spawns.member = membersOf; # (4)
sinks.config = {
reduce = yen.lib.reduce.merge;
out =
{ host, config, ... }:
{
config.${host.name} = config;
};
};
};
yen.kinds.member = { }; # its elements go up to its machine
yen.fleet.main = { };
yen.flake = { out, ... }: out;
}

At (1), for each assembly and each of its roles, the machine looks for itself in the list of that role. At (2), the member holds a ref to its assembly. At (4), spawns.member creates the members. At (3), the sink exports collects what members publish. An aspect at a member scope runs once per assembly and role. An aspect at a machine that has a member runs once per machine. These are the scopes:

flowchart TD
  n6c4c773b6d["fleet main"]
  nbe39bbfa9b["host db"]
  n8252fa3265["member backup.client"]
  ncaf29b11ff["host nas"]
  n1d79234a62["member backup.server"]
  n0f3c4939ab["host web"]
  nab77973dae["member backup.client"]
  n6c4c773b6d --> nbe39bbfa9b
  nbe39bbfa9b --> n8252fa3265
  n6c4c773b6d --> ncaf29b11ff
  ncaf29b11ff --> n1d79234a62
  n6c4c773b6d --> n0f3c4939ab
  n0f3c4939ab --> nab77973dae

Clients publish their address to a fleet sink with elements. The engine sets from, so a member can only publish as itself. The server reads the elements and builds its allow list:

modules/backup.nix
# The backup: an aspect per role, at member scopes. Clients export their address; the server reads them.
{ yen, lib, ... }:
let
inherit (yen.lib) same;
role =
r:
yen.lib.all [
yen.kinds.member
(s: same s.assembly yen.assembly.backup && s.role == r) # (1)
];
nas.scopesOf = yen.host.nas; # a tree request: nas, with its data
me.nearest = yen.kinds.host; # a tree request: the host this member sits on
in
{
yen.aspects.backup-client = {
at = role "client";
exports = { host, ... }: host.addr; # (2) published by the member scope: the engine sets `from`
cycle =
{ tree, ... }:
{
tree = [
nas
me
];
config = [
{
backup.target = (lib.head (tree.select nas)).addr;
backup.name = (tree.select me).name;
}
];
};
};
yen.aspects.backup-server = {
at = role "server";
config =
{ exports, ... }:
{
backup.allow = lib.sort (a: b: a < b) (
map (e: e.value) (lib.filter (e: yen.lib.holds yen.kinds.member e.from) exports)
);
};
};
# once per machine that takes part in any role of the assembly
yen.aspects.backup-tools = {
at = yen.lib.below (s: yen.lib.holds yen.kinds.member s && same s.assembly yen.assembly.backup);
config.packages = [ "borgbackup" ];
};
}

At (1), an aspect is active only at members of the assembly backup with a given role. At (2), the client publishes its address with the key exports. The result:

result
{
db = {
backup = {
name = "db";
target = "10.0.0.10";
};
packages = [
"borgbackup"
];
};
nas = {
backup = {
allow = [
"10.0.0.1"
"10.0.0.2"
];
};
packages = [
"borgbackup"
];
};
web = {
backup = {
name = "web";
target = "10.0.0.10";
};
packages = [
"borgbackup"
];
};
}
ContributeCommunitySponsor