Skip to content

Bundles of the library

A bundle is a kit whose assemblies make more assemblies. One homelab assembly names the server and the nodes, and the base, wireguard, borg, monitoring and dns assemblies follow from those two roles. See Kits batteries for kits, assemblies and roles. You import the kits a bundle uses, then write one assembly:

modules/assemblies.nix
# A bundle is a kit whose assemblies make more assemblies: one `homelab` assembly names the server and the nodes,
# and the base, wireguard, borg, monitoring and dns assemblies follow from those two roles. Import the kits it uses.
{ yen, inputs, ... }:
let
inherit (yen) host kit;
k = inputs.yen.lib.kits;
shared = import ../shared.nix yen;
in
{
imports = [
# (1)
k.homelab
k.base
k.disko
k.facter
k.sshd
k.emergency-access
(k.wireguard { root = ../vars; })
k.borg
k.monitoring
k.dns
];
yen.host = {
lb.addr = "10.0.0.1";
web.addr = "10.0.0.2";
nas.addr = "10.0.0.3";
};
yen.assembly.lab = {
kit = kit.homelab; # (2)
roles = {
server = {
at = host.nas;
settings = shared // {
device = "/dev/sda /dev/sdb";
layout = yen.kit.disko.layouts.zfs-mirror; # (3)
};
};
node = {
# (4)
at = [
host.lb
host.web
];
settings = shared;
};
};
};
}
  • (1) imports the bundle homelab and the kits it uses. The parts of base and borg come with them: homelab lists only its own.
  • (2) is the kit of the assembly. lab is the only assembly the fleet writes.
  • (3) is a setting of the server role: one of the layouts of the kit disko. A role’s settings may also be a function of the machine, for a device per machine.
  • (4) is the role node. Its machines are lb and web.

The fleet’s words: hosts with an address, members of assemblies, and the sinks the library kits write to.

modules/fleet.nix
# The fleet's words: hosts with an address, members of assemblies, and the sinks the library kits write to.
{ yen, lib, ... }:
let
kits = yen.lib.batteries.kits;
inherit (yen.lib) opt holds;
attrs = yen.lib.attrs;
# the fleet's assemblies and the parts their kits add (a part lives under its assembly)
assemblies = yen.lib.batteries.kits.assemblies (lib.attrValues yen.assembly); # (1)
# tree requests: every assembly and host, with their data
assemblyTree.instances = yen.kinds.assembly;
hostTree.instances = yen.kinds.host;
machineOf = s: if holds yen.kinds.member s then s.parent else s;
in
{
yen.kinds.fleet = {
spawns.host = _: yen.host;
sinks = {
out.reduce = yen.lib.reduce.merge;
exports.reduce = kits.exports { isMember = holds yen.kinds.member; };
generators = {
reduce = yen.lib.batteries.vars.generators { inherit machineOf; };
out = yen.lib.batteries.vars.vars-plan {
from = yen.kinds.fleet.sinks.generators;
machines = lib.attrNames yen.host;
recipientsOf = _: [ "age1admin" ];
};
};
};
};
yen.kinds.host = {
fields.addr = yen.lib.str;
spawns.member = kits.members { inherit assemblies holds; }; # (2)
# the NixOS keys of a host, as data
sinks.disks.reduce = yen.lib.reduce.merge; # kit disko gives disks here
# (3) a kit's `os` keys (sshd's server) are the host's NixOS; without this sink they are dropped
sinks.os.nixos =
{ os, ... }:
{
imports = os;
};
sinks.nixos = {
reduce = yen.lib.reduce.merge;
out =
{ scope, sinks, ... }:
{
config.${scope.name} = sinks.nixos;
};
};
};
yen.kinds.member = { };
yen.kinds.kit.fields = {
roles = opt attrs { };
constraints = opt attrs { };
exports = opt (yen.lib.listOf yen.lib.str) [ ];
adds = opt (yen.lib.is builtins.isFunction) (_: { });
};
yen.kinds.assembly.fields = {
kit = yen.kinds.kit;
roles = opt attrs { };
};
# the fleet is read through tree requests, never as refs: an aspect reads instance data only through args
yen.aspects.contract = {
at = yen.kinds.fleet;
cycle =
{ tree, ... }:
{
tree = [
assemblyTree
hostTree
];
errors = [
(kits.contract {
# (4)
assemblies = yen.lib.batteries.kits.assemblies (tree.select assemblyTree);
machines = tree.select hostTree;
inherit holds;
})
];
};
};
yen.aspects.settings-typed = {
at = yen.kinds.member;
errors = kits.settings-typed; # (5)
};
# a generator that an export carries goes to the machines of its `deploy` predicate
yen.aspects.generators-from-exports = {
at = yen.kinds.host;
generators = yen.lib.batteries.vars.from-exports { inherit holds; }; # (6)
};
yen.fleet.main = { };
yen.flake = { out, ... }: out;
}
  • (1) expands the assemblies at every depth with the battery kits.assemblies. The parts of a bundle live under its assembly, keyed by a short name (base, wg), and are not names in the fleet.
  • (2) gives each host one member scope for each (assembly, role) it takes.
  • (3) is the nixos sink of the host. A kit’s os keys go into it.
  • (4) is kits.contract. It refuses two assemblies with one name. (5) checks the types of the settings.
  • (6) gives a generator that an export carries to the machines of its deploy predicate.

The members of each machine (a part is named by its path joined by -: lab-base-disko is part disko of part base of lab):

result
{
lb = [
"lab-base-disko.default"
"lab-base-emergency.default"
"lab-base-facter.default"
"lab-base-sshd.server"
"lab-base.default"
"lab-borg.client"
"lab-monitoring.exporter"
"lab-wg.peer"
"lab.node"
];
nas = [
"lab-base-disko.default"
"lab-base-emergency.default"
"lab-base-facter.default"
"lab-base-sshd.server"
"lab-base.default"
"lab-borg-sshd.server"
"lab-borg.server"
"lab-dns.default"
"lab-monitoring.server"
"lab-wg.controller"
"lab.server"
];
web = [
"lab-base-disko.default"
"lab-base-emergency.default"
"lab-base-facter.default"
"lab-base-sshd.server"
"lab-base.default"
"lab-borg.client"
"lab-monitoring.exporter"
"lab-wg.peer"
"lab.node"
];
}

What the members export:

result
[
"sshd:lab-base-sshd:server:lb"
"monitoring:lab-monitoring:exporter:lb"
"wireguard:lab-wg:peer:lb"
"sshd:lab-base-sshd:server:nas"
"sshd:lab-borg-sshd:server:nas"
"borg:lab-borg:server:nas"
"wireguard:lab-wg:controller:nas"
"sshd:lab-base-sshd:server:web"
"monitoring:lab-monitoring:exporter:web"
"wireguard:lab-wg:peer:web"
]

homelab allows exactly one server. Two:

variants/two-servers.nix
# a homelab with two servers: exactly one is allowed
{ yen, ... }:
{
yen.assembly.lab2 = {
kit = yen.kit.homelab;
roles = {
server = {
at = [
yen.host.nas
yen.host.lb
];
settings = import ../shared.nix yen;
};
node = {
at = yen.host.web;
settings = import ../shared.nix yen;
};
};
};
}
result
[
"merge conflict at services.prometheus.scrapeConfigs"
"wireguard: lb has no wg-lab2-wg.pub public key"
"wireguard: nas has no wg-lab2-wg.pub public key"
"wireguard: web has no wg-lab2-wg.pub public key"
"kits: assembly lab2 role server: 2 machines, at most 1"
"kits: assembly lab2-borg role server: 2 machines, at most 1"
"kits: assembly lab2-monitoring role server: 2 machines, at most 1"
"kits: assembly lab2-wg role controller: 2 machines, at most 1"
]

And none:

variants/no-server.nix
# a homelab with nodes and no machine for the server role
{ yen, ... }:
{
yen.assembly.lab0 = {
kit = yen.kit.homelab;
roles = {
server = {
at = _: false;
settings = import ../shared.nix yen;
};
node = {
at = yen.host.web;
settings = import ../shared.nix yen;
};
};
};
}
result
[
"wireguard: web has no wg-lab0-wg.pub public key"
"kits: assembly lab0 role server: 0 machines, at least 1"
"kits: assembly lab0-borg role server: 0 machines, at least 1"
"kits: assembly lab0-borg-sshd role server: 0 machines, at least 1"
"kits: assembly lab0-monitoring role server: 0 machines, at least 1"
"kits: assembly lab0-wg role controller: 0 machines, at least 1"
]
Bundle Roles Adds
base default with device, layout (one of yen.kit.disko.layouts), authorizedKeys, hardware disko, facter, emergency-access, and sshd as server, on the same machines. No users: a user needs a name per machine
homelab server (exactly 1), node; the settings of base and prefix base on both roles; wireguard (server controller, nodes peers); borg (server backs up); monitoring (server scrapes); dns on the server

The templates kits-homelab and kits-base are working examples.

ContributeCommunitySponsor