Compare commits
8 Commits
dc74538cfe
...
d3d3987f86
| Author | SHA1 | Date | |
|---|---|---|---|
| d3d3987f86 | |||
| 8dde08bbcb | |||
| f5b854ca02 | |||
| 9f2957f4be | |||
| 98711c9575 | |||
| 3975548046 | |||
| ba3984d4f5 | |||
| cd721b5d6d |
@@ -4,13 +4,16 @@ One flake builds every machine I own.
|
||||
|
||||
## Repository Conventions
|
||||
|
||||
- Follow `docs/technical-writing.md` for comments, user-facing descriptions, commit prose, pull-request prose, and Markdown documentation.
|
||||
- Use `docs/technical-writing.md` and `docs/dictionary.md` while authoring any file.
|
||||
Apply them to comments, user-facing descriptions, commit prose, pull-request prose, and Markdown documentation.
|
||||
- Comments posted to the forge under the operator's account must end with `— Pi`.
|
||||
The sign-off identifies the agent as the author instead of the operator.
|
||||
- Commit messages follow Conventional Commits, defined in `docs/conventional-commits.md`.
|
||||
Use a scope for the changed module or host, such as `fish`, `nvim`, or `neogaia`.
|
||||
- Keep commit messages about the Git history, not the forge workflow.
|
||||
Do not reference issue or pull-request numbers unless the commit itself requires a stable external reference.
|
||||
- During pull-request development, add follow-up commits instead of amending existing commits.
|
||||
Preserve review history until the pull request is ready to merge.
|
||||
- Use `gitea-axi` for Gitea issues, pull requests, labels, reviews, comments, and milestones.
|
||||
It resolves this repository from the `origin` remote inside the checkout.
|
||||
|
||||
@@ -66,7 +69,7 @@ One flake builds every machine I own.
|
||||
- Guest modules are host-agnostic wiring, while host files own deployment-specific service configuration such as served volumes and host paths.
|
||||
For example, `guests/copyparty.nix` should expose options and translate them into guest mounts/service settings, while `hosts/pikachu/default.nix` chooses `/inbox`, `/media`, and their backing paths.
|
||||
Guest addresses and routes are host-level configuration because a guest module can be used on multiple hosts.
|
||||
- `self.lib.registry.collect { hosts = self.nixosConfigurations; }` reads only top-level host `config.registry` declarations.
|
||||
- The registry collector reads only top-level host `config.registry` declarations.
|
||||
Registry declarations inside guest interiors live under `config.containers.<guest>.config.registry`.
|
||||
The collected registry does not include those guest-interior declarations.
|
||||
Publish guest values from host-side code, or extend the collector to recurse into guests.
|
||||
|
||||
@@ -45,11 +45,15 @@ Use the simplest accurate word when a term is not listed.
|
||||
| operator | Agent tooling | The human driving the development process. Use when distinguishing the human from the agent acting on their behalf. |
|
||||
| guest | dotfiles | A systemd-nspawn container managed by this flake. Use for configured containers under `guests/`. |
|
||||
| host | dotfiles | A machine with a NixOS configuration built by this flake. |
|
||||
| module | dotfiles | In this repository, a self-contained configuration under `modules/`, usually scoped to one program, service, or subsystem. Use `Nix module` for the language-level module-system concept. |
|
||||
| module | dotfiles | In this repository, a self-contained configuration under `modules/`. Use for one program, service, or subsystem. |
|
||||
| registry | dotfiles | A typed configuration namespace whose entries can be collected across hosts and reused by other configuration. |
|
||||
| Conventional Commits | Git | A commit-message format defined in `docs/conventional-commits.md`. |
|
||||
| forge | Git | A web service that hosts Git repositories, pull requests, issues, and code review comments. Use when the exact provider does not matter. |
|
||||
| flake | Nix | A Nix project with pinned inputs and declared outputs. Use for the repository-level build unit defined by `flake.nix`. |
|
||||
| Nix module | Nix | A Nix language unit that declares or sets options through the Nix module system. |
|
||||
| assembly file | Nix | A Nix file under `flake/` that constructs flake outputs or arguments passed to NixOS modules. |
|
||||
| assembly function | Nix | A project function in `flake/` that creates modules, systems, or output values. |
|
||||
| library file | Nix | A Nix file that defines reusable project functions instead of a NixOS module or flake output. |
|
||||
| library function | Nix | A pure project function exposed under `util`. |
|
||||
| NixOS module | Nix | A Nix file evaluated by `nixosSystem` for a NixOS configuration. |
|
||||
| overlay | Nix | A function that modifies a Nix package set by adding or replacing packages. |
|
||||
| workspace | Window management | A named or numbered group of windows in a graphical session. Some desktop environments call this a virtual desktop. |
|
||||
|
||||
270
docs/nix.md
Normal file
270
docs/nix.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Nix Conventions
|
||||
|
||||
This document records local Nix style and design conventions.
|
||||
Use these conventions when changing this flake.
|
||||
Read this document with `docs/dictionary.md`.
|
||||
|
||||
## Names
|
||||
|
||||
### Reserve `lib` for `nixpkgs.lib`
|
||||
|
||||
Use `lib` only for the base `nixpkgs.lib` value.
|
||||
Do not use `lib` for project functions.
|
||||
Use `util` for library functions passed through module arguments.
|
||||
Use `flake.mk<Name>` for assembly functions used by flake outputs.
|
||||
|
||||
Avoid:
|
||||
|
||||
```nix
|
||||
specialArgs = {
|
||||
lib = util;
|
||||
};
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```nix
|
||||
specialArgs = {
|
||||
inherit util;
|
||||
};
|
||||
```
|
||||
|
||||
Then call library functions through `util` inside modules:
|
||||
|
||||
```nix
|
||||
{
|
||||
lib,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
{
|
||||
networking.networkmanager.unmanaged = [ "interface-name:${util.bridgeName 10}" ];
|
||||
}
|
||||
```
|
||||
|
||||
### Name project function namespaces by role
|
||||
|
||||
Put pure project functions under `util`.
|
||||
Put functions that create systems or output values under `flake`.
|
||||
This keeps data-only functions separate from functions that carry flake context.
|
||||
|
||||
Avoid:
|
||||
|
||||
```nix
|
||||
util.mkHosts { root = self + "/hosts"; }
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```nix
|
||||
flake.mkHosts { root = self + "/hosts"; }
|
||||
```
|
||||
|
||||
## File Placement
|
||||
|
||||
### Put files in the directory that owns their role
|
||||
|
||||
Choose the file location before writing a Nix file.
|
||||
Put assembly files under `flake/`.
|
||||
Put library files under `lib/`.
|
||||
Put modules under `modules/`.
|
||||
Put host configurations under `hosts/<name>/`.
|
||||
Put host-side guest wiring under `guests/`.
|
||||
Put registry definitions under `registry/`.
|
||||
Keep `flake.nix` focused on inputs and output wiring.
|
||||
Do not add root-level Nix files other than `flake.nix`.
|
||||
|
||||
A module is self-contained configuration under `modules/`.
|
||||
A module configures one program, service, or subsystem.
|
||||
Files under `hosts/`, `guests/`, and `modules/` return NixOS modules.
|
||||
|
||||
### Use `default.nix` for module directories with support files
|
||||
|
||||
Use `default.nix` as the primary module when a module directory owns support files.
|
||||
Keep sibling files for data or code read by that primary module.
|
||||
|
||||
Avoid:
|
||||
|
||||
```text
|
||||
modules/shell/fish/fish.nix
|
||||
modules/shell/fish/config.fish
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```text
|
||||
modules/shell/fish/default.nix
|
||||
modules/shell/fish/config.fish
|
||||
```
|
||||
|
||||
### Use non-module `default.nix` files only for directory assembly
|
||||
|
||||
Outside `modules/`, use `default.nix` to connect files in its directory.
|
||||
Do not define primary assembly functions in `default.nix`.
|
||||
Put primary assembly functions in named files.
|
||||
|
||||
Avoid:
|
||||
|
||||
```text
|
||||
flake/default.nix # defines mkHost
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```text
|
||||
flake/default.nix # imports ./host.nix
|
||||
flake/host.nix # defines mkHosts
|
||||
```
|
||||
|
||||
### Keep assembly files focused on one concept
|
||||
|
||||
Give each assembly file under `flake/` one top-level concept.
|
||||
Keep helper functions private to that file when no other file needs them.
|
||||
Export the constructor for the concept when another assembly file needs it.
|
||||
Name that constructor `mk<Concept>`.
|
||||
|
||||
Avoid:
|
||||
|
||||
```nix
|
||||
{
|
||||
inherit collectNixFiles mkHost mkHosts;
|
||||
}
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```nix
|
||||
{
|
||||
inherit mkHosts;
|
||||
}
|
||||
```
|
||||
|
||||
## Function Arguments
|
||||
|
||||
### Use closed interfaces for assembly files and library files
|
||||
|
||||
Use exact function argument patterns for assembly files and library files.
|
||||
The caller owns the argument set for these files.
|
||||
The imported file names each accepted attribute.
|
||||
The imported file rejects stale or accidental extra attributes.
|
||||
|
||||
Avoid:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
self,
|
||||
...
|
||||
}:
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
self,
|
||||
}:
|
||||
```
|
||||
|
||||
### Pass function arguments explicitly
|
||||
|
||||
Pass only the values an assembly file or library file needs.
|
||||
Bind `lib` to the base `nixpkgs.lib` value at the call site.
|
||||
Use `inherit` only for values that keep the same name.
|
||||
|
||||
Avoid:
|
||||
|
||||
```nix
|
||||
import ./flake inputs
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```nix
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
in
|
||||
import ./flake { inherit lib inputs self; }
|
||||
```
|
||||
|
||||
### Keep NixOS module interfaces open
|
||||
|
||||
Files under `hosts/`, `guests/`, and `modules/` return NixOS modules.
|
||||
`nixosSystem` owns their argument shape.
|
||||
Name each argument the module uses.
|
||||
Keep `...` so the module accepts standard arguments and special arguments.
|
||||
|
||||
Avoid:
|
||||
|
||||
```nix
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
registry,
|
||||
}:
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```nix
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
registry,
|
||||
...
|
||||
}:
|
||||
```
|
||||
|
||||
## `let` Bindings
|
||||
|
||||
### Use role-specific binding names
|
||||
|
||||
A `let` binding name must explain the value's role in the expression.
|
||||
Avoid names that describe only the value's type.
|
||||
|
||||
Avoid:
|
||||
|
||||
```nix
|
||||
let
|
||||
names = lib.attrNames hosts;
|
||||
in
|
||||
lib.genAttrs names mkHost
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```nix
|
||||
let
|
||||
hostNames = lib.attrNames hosts;
|
||||
in
|
||||
lib.genAttrs hostNames mkHost
|
||||
```
|
||||
|
||||
## Standard Library Calls
|
||||
|
||||
### Qualify standard library calls in flake assembly
|
||||
|
||||
Use qualified standard library calls inside assembly files.
|
||||
Use `lib.mapAttrs` instead of inheriting `mapAttrs` from `lib`.
|
||||
This keeps the source of each function visible.
|
||||
|
||||
Avoid:
|
||||
|
||||
```nix
|
||||
let
|
||||
inherit (lib) mapAttrs;
|
||||
in
|
||||
mapAttrs f attrs
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```nix
|
||||
lib.mapAttrs f attrs
|
||||
```
|
||||
|
||||
@@ -76,14 +76,13 @@
|
||||
{ self, nixpkgs, ... }@inputs:
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
my = import ./lib { inherit lib inputs self; };
|
||||
flake = import ./flake { inherit lib inputs self; };
|
||||
in
|
||||
{
|
||||
# Helper functions for discovering and building hosts.
|
||||
lib = my;
|
||||
|
||||
# Every host under hosts/ is discovered and built.
|
||||
nixosConfigurations = my.mkHosts (self + "/hosts");
|
||||
nixosConfigurations = flake.mkHosts {
|
||||
root = self + "/hosts";
|
||||
};
|
||||
|
||||
# A project shell for agent-local resources that should travel with this
|
||||
# checkout rather than the operator's global profile.
|
||||
|
||||
58
flake/default.nix
Normal file
58
flake/default.nix
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
lib,
|
||||
inputs,
|
||||
self,
|
||||
}:
|
||||
let
|
||||
# TODO: refactor the registry code
|
||||
registryAssembly = import ./registry.nix { inherit lib self; };
|
||||
|
||||
util = import ../lib { inherit lib; };
|
||||
mkModules = import ./module.nix { inherit util; };
|
||||
|
||||
modules = mkModules {
|
||||
root = self + "/modules";
|
||||
};
|
||||
|
||||
guestModules = guests.mkGuests {
|
||||
root = self + "/guests";
|
||||
};
|
||||
|
||||
#----------------------------------------------------------
|
||||
|
||||
collectedRegistry = registryAssembly.collect { hosts = self.nixosConfigurations; };
|
||||
|
||||
specialArgs = {
|
||||
inherit
|
||||
inputs
|
||||
self
|
||||
util
|
||||
;
|
||||
registry = collectedRegistry;
|
||||
};
|
||||
|
||||
guests = import ./guest.nix {
|
||||
inherit
|
||||
inputs
|
||||
lib
|
||||
specialArgs
|
||||
registryAssembly
|
||||
util
|
||||
;
|
||||
repositoryModules = modules;
|
||||
};
|
||||
|
||||
hosts = import ./host.nix {
|
||||
inherit
|
||||
inputs
|
||||
lib
|
||||
specialArgs
|
||||
guestModules
|
||||
registryAssembly
|
||||
;
|
||||
repositoryModules = modules;
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (hosts) mkHosts;
|
||||
}
|
||||
282
flake/guest.nix
Normal file
282
flake/guest.nix
Normal file
@@ -0,0 +1,282 @@
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
specialArgs,
|
||||
repositoryModules,
|
||||
registryAssembly,
|
||||
util,
|
||||
}:
|
||||
let
|
||||
mkGuest =
|
||||
name:
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.guests.${name};
|
||||
|
||||
directoryMounts = lib.filterAttrs (_: mount: mount.directory != null) cfg.mounts;
|
||||
|
||||
directoryScript = lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (
|
||||
_: mount:
|
||||
''
|
||||
${config.system.path}/bin/install -d \
|
||||
${lib.optionalString (mount.owner != null) "-o ${lib.escapeShellArg (toString mount.owner)}"} \
|
||||
${lib.optionalString (mount.group != null) "-g ${lib.escapeShellArg mount.group}"} \
|
||||
${lib.optionalString (mount.mode != null) "-m ${lib.escapeShellArg mount.mode}"} \
|
||||
${lib.escapeShellArg mount.directory}
|
||||
''
|
||||
) directoryMounts
|
||||
);
|
||||
in
|
||||
{
|
||||
options.guests.${name} = {
|
||||
enable = lib.mkEnableOption "the ${name} guest, run in its own nested container";
|
||||
vlan = lib.mkOption {
|
||||
type = lib.types.ints.between 1 4094;
|
||||
example = 10;
|
||||
description = ''
|
||||
The tagged VLAN this guest lives on. The guest attaches to its host's
|
||||
`br-vlan<id>` bridge for that VLAN. The id must be one of the host's
|
||||
`modules.network.vlans.ids`.
|
||||
'';
|
||||
};
|
||||
mac = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = util.deriveMac "${config.networking.hostName}.${name}";
|
||||
defaultText = lib.literalMD "a stable address derived from the host name and guest option path";
|
||||
example = "bc:24:11:00:00:01";
|
||||
description = ''
|
||||
The guest's MAC address on its VLAN, pinned inside the guest by its
|
||||
own networkd. Set it to reuse an existing address so a router's DHCP
|
||||
reservation keeps working. Left unset, a stable address is derived
|
||||
from the host name and guest option path in the locally-administered range.
|
||||
'';
|
||||
};
|
||||
address = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.23.20.126";
|
||||
description = ''
|
||||
Expected IP address assigned outside NixOS for this guest.
|
||||
Left null, this guest contributes no address to the address registry.
|
||||
'';
|
||||
};
|
||||
reverseProxy = {
|
||||
enable = lib.mkEnableOption "a reverse-proxy route for this guest";
|
||||
|
||||
route = lib.mkOption {
|
||||
type = registryAssembly.definitions.route.type;
|
||||
description = "Route registry entry published for this guest.";
|
||||
};
|
||||
};
|
||||
mounts = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
directory = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/srv/media";
|
||||
description = "Host directory bind-mounted into the guest.";
|
||||
};
|
||||
file = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/run/secrets/service-password";
|
||||
description = "Host file bind-mounted into the guest.";
|
||||
};
|
||||
readOnly = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Mount the host path read-only inside the guest.";
|
||||
};
|
||||
owner = lib.mkOption {
|
||||
type = lib.types.nullOr (
|
||||
lib.types.oneOf [
|
||||
lib.types.str
|
||||
lib.types.ints.positive
|
||||
]
|
||||
);
|
||||
default = null;
|
||||
description = "Owner assigned to the host directory before the guest starts.";
|
||||
};
|
||||
group = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "Group assigned to the host directory before the guest starts.";
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "0770";
|
||||
description = "Mode assigned to the host directory before the guest starts.";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"/data/media" = { directory = "/srv/media"; };
|
||||
"/data/config" = { directory = "/srv/config/jellyfin"; };
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Host paths bind-mounted into the guest, keyed by the path they appear
|
||||
at inside the guest, so a guest sees exactly the data it should at any
|
||||
granularity — a single folder, a whole pool, or a decrypted secret.
|
||||
Mounts are read-write unless `readOnly` is set.
|
||||
'';
|
||||
};
|
||||
interior = lib.mkOption {
|
||||
type = lib.types.deferredModule;
|
||||
default = { };
|
||||
description = "NixOS module imported into the guest interior.";
|
||||
};
|
||||
limits = {
|
||||
memory = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "2G";
|
||||
description = ''
|
||||
Cap on the guest's memory, applied to its unit as `MemoryMax`.
|
||||
Accepts systemd size suffixes such as `512M` or `2G`. Left null,
|
||||
the guest's memory is uncapped.
|
||||
'';
|
||||
};
|
||||
cpu = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "150%";
|
||||
description = ''
|
||||
Cap on the guest's CPU, applied to its unit as `CPUQuota`, where
|
||||
`100%` is one full core. Left null, the guest's CPU is uncapped.
|
||||
'';
|
||||
};
|
||||
tasksMax = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
example = 512;
|
||||
description = ''
|
||||
Cap on the number of processes and threads the guest may spawn,
|
||||
applied to its unit as `TasksMax`. Left null, the task count is
|
||||
uncapped.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions =
|
||||
let
|
||||
hasDirectorySettings = mount: mount.owner != null || mount.group != null || mount.mode != null;
|
||||
hasCompleteDirectorySettings = mount: mount.owner != null && mount.group != null && mount.mode != null;
|
||||
hasOneSource = mount: (mount.directory != null) != (mount.file != null);
|
||||
mountsWithInvalidSource = lib.attrNames (lib.filterAttrs (_: mount: !hasOneSource mount) cfg.mounts);
|
||||
fileMountsWithHostSettings = lib.attrNames (
|
||||
lib.filterAttrs (_: mount: mount.file != null && hasDirectorySettings mount) cfg.mounts
|
||||
);
|
||||
directoryMountsWithPartialHostSettings = lib.attrNames (
|
||||
lib.filterAttrs (_: mount: mount.directory != null && hasDirectorySettings mount && !hasCompleteDirectorySettings mount) cfg.mounts
|
||||
);
|
||||
in
|
||||
[
|
||||
{
|
||||
assertion = mountsWithInvalidSource == [ ];
|
||||
message = ''
|
||||
guests.${name} mounts ${lib.concatStringsSep ", " mountsWithInvalidSource} must set exactly one of `directory` or `file`.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = fileMountsWithHostSettings == [ ];
|
||||
message = ''
|
||||
guests.${name} file mounts ${lib.concatStringsSep ", " fileMountsWithHostSettings} must not set `owner`, `group`, or `mode`.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = directoryMountsWithPartialHostSettings == [ ];
|
||||
message = ''
|
||||
guests.${name} directory mounts ${lib.concatStringsSep ", " directoryMountsWithPartialHostSettings} must set `owner`, `group`, and `mode` together.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = lib.elem cfg.vlan config.modules.network.vlans.ids;
|
||||
message = ''
|
||||
guests.${name}.vlan = ${toString cfg.vlan} is not among its host's modules.network.vlans.ids (${lib.concatMapStringsSep ", " toString config.modules.network.vlans.ids}). Declare the VLAN on the host or correct the guest's placement.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
registry.address.${config.networking.hostName}.${name} =
|
||||
lib.mkIf (cfg.address != null) cfg.address;
|
||||
|
||||
registry.route.${config.networking.hostName}.${name} =
|
||||
lib.mkIf cfg.reverseProxy.enable cfg.reverseProxy.route;
|
||||
|
||||
systemd.services."container@${name}" =
|
||||
let
|
||||
bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (util.bridgeName cfg.vlan)}.device";
|
||||
in
|
||||
{
|
||||
after = [ bridgeDevice ];
|
||||
wants = [ bridgeDevice ];
|
||||
|
||||
serviceConfig = lib.filterAttrs (_: v: v != null) {
|
||||
MemoryMax = cfg.limits.memory;
|
||||
CPUQuota = cfg.limits.cpu;
|
||||
TasksMax = cfg.limits.tasksMax;
|
||||
};
|
||||
preStart = lib.mkIf (directoryMounts != { }) directoryScript;
|
||||
};
|
||||
|
||||
containers.${name} = {
|
||||
autoStart = true;
|
||||
privateNetwork = lib.mkDefault true;
|
||||
hostBridge = util.bridgeName cfg.vlan;
|
||||
privateUsers = lib.mkDefault "no";
|
||||
bindMounts = lib.mapAttrs (_: mount: {
|
||||
hostPath = if mount.directory != null then mount.directory else mount.file;
|
||||
isReadOnly = mount.readOnly;
|
||||
}) cfg.mounts;
|
||||
|
||||
inherit specialArgs;
|
||||
|
||||
config = {
|
||||
imports = [
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.stylix.nixosModules.stylix
|
||||
|
||||
repositoryModules
|
||||
registryAssembly.module
|
||||
cfg.interior
|
||||
|
||||
{
|
||||
modules.guest.enable = true;
|
||||
|
||||
networking.useNetworkd = true;
|
||||
networking.useHostResolvConf = false;
|
||||
|
||||
systemd.network.networks."20-eth0" = {
|
||||
matchConfig.Name = "eth0";
|
||||
linkConfig.MACAddress = cfg.mac;
|
||||
networkConfig.DHCP = "yes";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
mkGuests =
|
||||
{ root }:
|
||||
let
|
||||
files = util.collectNixFiles root;
|
||||
guestFiles = lib.filterAttrs (file: type: type == "regular" && lib.hasSuffix ".nix" file) (builtins.readDir root);
|
||||
names = map (file: lib.removeSuffix ".nix" file) (lib.attrNames guestFiles);
|
||||
in
|
||||
{
|
||||
imports = files ++ map mkGuest names;
|
||||
};
|
||||
}
|
||||
49
flake/host.nix
Normal file
49
flake/host.nix
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
specialArgs,
|
||||
repositoryModules,
|
||||
|
||||
guestModules,
|
||||
# TODO refactor registry
|
||||
registryAssembly,
|
||||
}:
|
||||
let
|
||||
mkHost =
|
||||
{
|
||||
root,
|
||||
name,
|
||||
}:
|
||||
lib.nixosSystem {
|
||||
inherit specialArgs;
|
||||
modules = [
|
||||
inputs.chaotic.nixosModules.default
|
||||
inputs.disko.nixosModules.disko
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.stylix.nixosModules.stylix
|
||||
|
||||
repositoryModules
|
||||
guestModules
|
||||
|
||||
# TODO: refactor
|
||||
registryAssembly.module (root + "/${name}")
|
||||
|
||||
{
|
||||
networking.hostName = name;
|
||||
modules.host.enable = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
mkHosts =
|
||||
{ root }:
|
||||
let
|
||||
directories = lib.filterAttrs (_name: type: type == "directory") (builtins.readDir root);
|
||||
names = lib.attrNames directories;
|
||||
in
|
||||
lib.genAttrs names (
|
||||
name:
|
||||
mkHost { inherit root name; }
|
||||
);
|
||||
}
|
||||
5
flake/module.nix
Normal file
5
flake/module.nix
Normal file
@@ -0,0 +1,5 @@
|
||||
{ util }:
|
||||
{ root }:
|
||||
{
|
||||
imports = util.collectNixFiles root;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
my,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -9,18 +8,6 @@ let
|
||||
dataDir = "/data";
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
(my.guest {
|
||||
name = "actualbudget";
|
||||
interior = {
|
||||
modules.services.actualbudget = {
|
||||
enable = true;
|
||||
inherit dataDir;
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
options.guests.actualbudget = {
|
||||
dataPath = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
@@ -39,7 +26,13 @@ in
|
||||
];
|
||||
|
||||
guests.actualbudget = {
|
||||
mounts.${dataDir}.hostPath = cfg.dataPath;
|
||||
interior = {
|
||||
modules.services.actualbudget = {
|
||||
enable = true;
|
||||
inherit dataDir;
|
||||
};
|
||||
};
|
||||
mounts.${dataDir}.directory = cfg.dataPath;
|
||||
limits = {
|
||||
memory = lib.mkDefault "2G";
|
||||
cpu = lib.mkDefault "200%";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
my,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -34,26 +33,6 @@ let
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
(my.guest {
|
||||
name = "copyparty";
|
||||
interior = {
|
||||
modules.services.copyparty = {
|
||||
enable = true;
|
||||
inherit (cfg) accountName stateDir;
|
||||
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||
reverseProxy = {
|
||||
enable = cfg.reverseProxy.enable;
|
||||
inherit (cfg.reverseProxy) trustedSources;
|
||||
};
|
||||
volumes = lib.mapAttrs (_urlPath: volume: {
|
||||
inherit (volume) path access flags;
|
||||
}) cfg.volumes;
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
options.guests.copyparty = {
|
||||
accountName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
@@ -106,19 +85,33 @@ in
|
||||
];
|
||||
|
||||
guests.copyparty = {
|
||||
secrets = [ cfg.passwordSecret ];
|
||||
interior.modules.services.copyparty = {
|
||||
enable = true;
|
||||
inherit (cfg) accountName stateDir;
|
||||
passwordFile = "/run/secrets/${cfg.passwordSecret}";
|
||||
reverseProxy = {
|
||||
enable = cfg.reverseProxy.enable;
|
||||
inherit (cfg.reverseProxy) trustedSources;
|
||||
};
|
||||
volumes = lib.mapAttrs (_urlPath: volume: {
|
||||
inherit (volume) path access flags;
|
||||
}) cfg.volumes;
|
||||
};
|
||||
|
||||
mounts =
|
||||
{
|
||||
${cfg.stateDir} = {
|
||||
hostPath = cfg.statePath;
|
||||
create = {
|
||||
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||
group = "storage";
|
||||
mode = "0770";
|
||||
};
|
||||
directory = cfg.statePath;
|
||||
owner = config.registry."stable-uid".${config.networking.hostName}.copyparty;
|
||||
group = "storage";
|
||||
mode = "0770";
|
||||
};
|
||||
${"/run/secrets/${cfg.passwordSecret}"} = {
|
||||
file = config.sops.secrets.${cfg.passwordSecret}.path;
|
||||
readOnly = true;
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs' (_urlPath: volume: lib.nameValuePair volume.path { inherit (volume) hostPath; }) cfg.volumes;
|
||||
// lib.mapAttrs' (_urlPath: volume: lib.nameValuePair volume.path { directory = volume.hostPath; }) cfg.volumes;
|
||||
limits = {
|
||||
memory = lib.mkDefault "2G";
|
||||
cpu = lib.mkDefault "200%";
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
args@{ my, ... }:
|
||||
# A sample guest whose interior runs an OCI container on Podman.
|
||||
# The image is pulled at runtime, so the guest builds with no build-time fetch.
|
||||
my.guest {
|
||||
name = "nesting-sample";
|
||||
interior = {
|
||||
virtualisation.oci-containers.containers.hello = {
|
||||
image = "docker.io/library/hello-world";
|
||||
};
|
||||
};
|
||||
} args
|
||||
@@ -1,16 +1,14 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
my,
|
||||
...
|
||||
}:
|
||||
let
|
||||
giteaSshBackend = "10.23.20.45:2022";
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
(my.guest {
|
||||
name = "reverse-proxy";
|
||||
config = lib.mkIf config.guests.reverse-proxy.enable {
|
||||
guests.reverse-proxy = {
|
||||
interior = {
|
||||
modules.services.reverse-proxy.enable = true;
|
||||
|
||||
@@ -66,11 +64,7 @@ in
|
||||
}
|
||||
'';
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
config = lib.mkIf config.guests.reverse-proxy.enable {
|
||||
guests.reverse-proxy = {
|
||||
limits = {
|
||||
memory = lib.mkDefault "1G";
|
||||
cpu = lib.mkDefault "100%";
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
args@{ my, ... }:
|
||||
# The tracer-bullet guest: the thinnest complete path from discovery to a
|
||||
# running nested container. Its interior is just the guest-base — the baseline
|
||||
# toolset and SSH access — so it proves the concept without carrying a service.
|
||||
my.guest { name = "sample"; } args
|
||||
@@ -40,26 +40,6 @@
|
||||
modules.network.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
|
||||
modules.network.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
|
||||
|
||||
# The walking-skeleton guest, enabled like any module: proves the guest path
|
||||
# end to end through this host's `nix flake check`.
|
||||
# Modest caps keep the skeleton guest from starving the laptop.
|
||||
guests.sample.enable = true;
|
||||
guests.sample.limits = {
|
||||
memory = "1G";
|
||||
cpu = "100%";
|
||||
tasksMax = 512;
|
||||
};
|
||||
|
||||
# The nesting guest, run with `nesting` on: proves an interior OCI container
|
||||
# on Podman builds end to end through this host's `nix flake check`.
|
||||
guests.nesting-sample.enable = true;
|
||||
guests.nesting-sample.nesting = true;
|
||||
guests.nesting-sample.limits = {
|
||||
memory = "1G";
|
||||
cpu = "100%";
|
||||
tasksMax = 512;
|
||||
};
|
||||
|
||||
modules.shell.herdr.enable = true;
|
||||
modules.agents.tools.gitea-axi.enable = true;
|
||||
modules.agents.pi.enable = true;
|
||||
|
||||
101
lib/default.nix
101
lib/default.nix
@@ -1,100 +1,9 @@
|
||||
{
|
||||
lib,
|
||||
inputs,
|
||||
self,
|
||||
}:
|
||||
{ lib }:
|
||||
let
|
||||
inherit (lib)
|
||||
attrNames
|
||||
filterAttrs
|
||||
genAttrs
|
||||
flatten
|
||||
hasSuffix
|
||||
mapAttrsToList
|
||||
;
|
||||
|
||||
registry = import ./registry.nix { inherit lib self; };
|
||||
|
||||
collectNixFiles =
|
||||
dir:
|
||||
flatten (
|
||||
mapAttrsToList (
|
||||
name: type:
|
||||
let
|
||||
path = dir + "/${name}";
|
||||
in
|
||||
if type == "directory" then
|
||||
collectNixFiles path
|
||||
else if type == "regular" && hasSuffix ".nix" name then
|
||||
[ path ]
|
||||
else
|
||||
[ ]
|
||||
) (builtins.readDir dir)
|
||||
);
|
||||
|
||||
moduleFiles = collectNixFiles (self + "/modules");
|
||||
guestFiles = collectNixFiles (self + "/guests");
|
||||
|
||||
specialArgs = {
|
||||
inherit inputs self;
|
||||
my = self.lib;
|
||||
registry = self.lib.registry.collect { hosts = self.nixosConfigurations; };
|
||||
};
|
||||
|
||||
bridgeName = id: "br-vlan${toString id}";
|
||||
|
||||
guests = import ./guests.nix {
|
||||
inherit
|
||||
inputs
|
||||
lib
|
||||
self
|
||||
specialArgs
|
||||
bridgeName
|
||||
moduleFiles
|
||||
registry
|
||||
;
|
||||
};
|
||||
|
||||
mkHost =
|
||||
{
|
||||
hostName,
|
||||
system ? "x86_64-linux",
|
||||
}:
|
||||
inputs.nixpkgs.lib.nixosSystem {
|
||||
inherit system specialArgs;
|
||||
modules =
|
||||
moduleFiles
|
||||
++ guestFiles
|
||||
++ [
|
||||
inputs.chaotic.nixosModules.default
|
||||
inputs.disko.nixosModules.disko
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.stylix.nixosModules.stylix
|
||||
registry.module
|
||||
(self + "/hosts/${hostName}")
|
||||
{
|
||||
networking.hostName = hostName;
|
||||
modules.host.enable = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
mkHosts =
|
||||
hostsDir:
|
||||
let
|
||||
hostNames = attrNames (filterAttrs (_name: type: type == "directory") (builtins.readDir hostsDir));
|
||||
in
|
||||
genAttrs hostNames (hostName: mkHost { inherit hostName; });
|
||||
files = import ./files.nix { inherit lib; };
|
||||
network = import ./network.nix { inherit lib; };
|
||||
in
|
||||
{
|
||||
inherit
|
||||
collectNixFiles
|
||||
mkHost
|
||||
mkHosts
|
||||
bridgeName
|
||||
registry
|
||||
;
|
||||
|
||||
inherit (guests) deriveMac guest;
|
||||
|
||||
inherit (files) collectNixFiles;
|
||||
inherit (network) bridgeName deriveMac;
|
||||
}
|
||||
|
||||
22
lib/files.nix
Normal file
22
lib/files.nix
Normal file
@@ -0,0 +1,22 @@
|
||||
{ lib }:
|
||||
let
|
||||
collectNixFiles =
|
||||
dir:
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: type:
|
||||
let
|
||||
path = dir + "/${name}";
|
||||
in
|
||||
if type == "directory" then
|
||||
collectNixFiles path
|
||||
else if type == "regular" && lib.hasSuffix ".nix" name then
|
||||
[ path ]
|
||||
else
|
||||
[ ]
|
||||
) (builtins.readDir dir)
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit collectNixFiles;
|
||||
}
|
||||
370
lib/guests.nix
370
lib/guests.nix
@@ -1,370 +0,0 @@
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
self,
|
||||
specialArgs,
|
||||
bridgeName,
|
||||
moduleFiles,
|
||||
registry,
|
||||
}:
|
||||
let
|
||||
inherit (lib) escapeShellArg mapAttrsToList;
|
||||
|
||||
deriveMac =
|
||||
name:
|
||||
let
|
||||
hash = builtins.hashString "sha256" name;
|
||||
octet = i: builtins.substring (i * 2) 2 hash;
|
||||
in
|
||||
lib.concatStringsSep ":" ([ "02" ] ++ map octet [ 0 1 2 3 4 ]);
|
||||
|
||||
guest =
|
||||
{
|
||||
name,
|
||||
interior ? { },
|
||||
}:
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
optionPath = [ "guests" ] ++ lib.splitString "." name;
|
||||
cfg = lib.getAttrFromPath optionPath config;
|
||||
machineName = lib.replaceStrings [ "." ] [ "-" ] name;
|
||||
|
||||
networked = cfg.vlan != null;
|
||||
|
||||
userMounts = lib.mapAttrs (_guestPath: m: {
|
||||
inherit (m) hostPath;
|
||||
isReadOnly = m.readOnly;
|
||||
}) cfg.mounts;
|
||||
|
||||
secretMounts = lib.listToAttrs (
|
||||
map (
|
||||
name:
|
||||
let
|
||||
path = config.sops.secrets.${name}.path;
|
||||
in
|
||||
lib.nameValuePair path {
|
||||
hostPath = path;
|
||||
isReadOnly = true;
|
||||
}
|
||||
) cfg.secrets
|
||||
);
|
||||
|
||||
mountCollisions = lib.attrNames (builtins.intersectAttrs userMounts secretMounts);
|
||||
|
||||
limitConfig = lib.filterAttrs (_: v: v != null) {
|
||||
MemoryMax = cfg.limits.memory;
|
||||
CPUQuota = cfg.limits.cpu;
|
||||
TasksMax = cfg.limits.tasksMax;
|
||||
};
|
||||
|
||||
createdMounts = lib.filterAttrs (_guestPath: m: m.create != null) cfg.mounts;
|
||||
mountCreateScript = lib.concatStringsSep "\n" (
|
||||
mapAttrsToList (
|
||||
_guestPath: m:
|
||||
let
|
||||
create = m.create;
|
||||
in
|
||||
''
|
||||
${config.system.path}/bin/install -d -o ${escapeShellArg (toString create.owner)} -g ${escapeShellArg create.group} -m ${escapeShellArg create.mode} ${escapeShellArg m.hostPath}
|
||||
''
|
||||
) createdMounts
|
||||
);
|
||||
|
||||
guestNet =
|
||||
{ lib, ... }:
|
||||
{
|
||||
config = lib.mkIf networked {
|
||||
networking.useNetworkd = true;
|
||||
networking.useHostResolvConf = false;
|
||||
|
||||
systemd.network.networks."20-eth0" = {
|
||||
matchConfig.Name = "eth0";
|
||||
linkConfig.MACAddress = cfg.mac;
|
||||
networkConfig.DHCP = "yes";
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
options = lib.setAttrByPath optionPath {
|
||||
enable = lib.mkEnableOption "the ${name} guest, run in its own nested container";
|
||||
backend = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"container"
|
||||
"microvm"
|
||||
];
|
||||
default = "container";
|
||||
description = ''
|
||||
How the guest is realized. `container` runs the guest as a
|
||||
systemd-nspawn nested container. `microvm` is reserved for a future
|
||||
hard-isolation backend and is not built yet.
|
||||
'';
|
||||
};
|
||||
vlan = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.ints.between 1 4094);
|
||||
default = null;
|
||||
example = 10;
|
||||
description = ''
|
||||
The tagged VLAN this guest lives on. The guest attaches to its host's
|
||||
`br-vlan<id>` bridge for that VLAN. Left null, the guest keeps a
|
||||
private network with no bridge attachment. The id must be one of the
|
||||
host's `modules.network.vlans.ids`.
|
||||
'';
|
||||
};
|
||||
mac = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = deriveMac name;
|
||||
defaultText = lib.literalMD "a stable address derived from the guest's namespace path";
|
||||
example = "bc:24:11:00:00:01";
|
||||
description = ''
|
||||
The guest's MAC address on its VLAN, pinned inside the guest by its
|
||||
own networkd. Set it to reuse an existing address so a router's DHCP
|
||||
reservation keeps working. Left unset, a stable address is derived
|
||||
from the guest's namespace path in the locally-administered range.
|
||||
'';
|
||||
};
|
||||
address = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "10.23.20.126";
|
||||
description = ''
|
||||
Expected IP address assigned outside NixOS for this guest.
|
||||
Left null, this guest contributes no address to the address registry.
|
||||
'';
|
||||
};
|
||||
reverseProxy = {
|
||||
enable = lib.mkEnableOption "a reverse-proxy route for this guest";
|
||||
|
||||
route = lib.mkOption {
|
||||
type = self.lib.registry.definitions.route.type;
|
||||
description = "Route registry entry published for this guest.";
|
||||
};
|
||||
};
|
||||
mounts = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
hostPath = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "/srv/media";
|
||||
description = "The path on the host bind-mounted into the guest.";
|
||||
};
|
||||
readOnly = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Mount the path read-only. Read-write by default, since a
|
||||
service must write to the pool data it owns.
|
||||
'';
|
||||
};
|
||||
create = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.submodule {
|
||||
options = {
|
||||
type = lib.mkOption {
|
||||
type = lib.types.enum [ "directory" ];
|
||||
default = "directory";
|
||||
description = "Host path type to create before the guest starts.";
|
||||
};
|
||||
owner = lib.mkOption {
|
||||
type = lib.types.oneOf [
|
||||
lib.types.str
|
||||
lib.types.ints.positive
|
||||
];
|
||||
description = "Owner assigned to the created host path.";
|
||||
};
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Group assigned to the created host path.";
|
||||
};
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "0770";
|
||||
description = "Mode assigned to the created host path.";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
description = ''
|
||||
Opt-in host path creation before the guest starts. Existing
|
||||
paths are left enabled unless this is set, so served storage
|
||||
roots can still be required to exist explicitly.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"/data/media" = { hostPath = "/srv/media"; };
|
||||
"/data/config" = {
|
||||
hostPath = "/srv/config/jellyfin";
|
||||
readOnly = true;
|
||||
};
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Host paths bind-mounted into the guest, keyed by the path they appear
|
||||
at inside the guest, so a guest sees exactly the data it should at any
|
||||
granularity — a single folder or a whole pool. Each mount is
|
||||
read-write unless `readOnly` is set.
|
||||
'';
|
||||
};
|
||||
secrets = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "jellyfin-api-key" ];
|
||||
description = ''
|
||||
Names of the secrets this guest needs. The host is the sole
|
||||
decryptor: it decrypts each named secret from its own sops files and
|
||||
bind-mounts the plaintext file into the guest read-only at
|
||||
`/run/secrets/<name>`, the same path it would occupy on a host, so a
|
||||
service reads its credentials at a predictable location. The guest
|
||||
names the files it wants and receives exactly those. It holds no age
|
||||
key and decrypts nothing itself. Ownership carries across unchanged,
|
||||
since the container maps ids one to one, so a secret owned by a uid on
|
||||
the host is owned by that same uid inside the guest.
|
||||
'';
|
||||
};
|
||||
limits = {
|
||||
memory = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "2G";
|
||||
description = ''
|
||||
Cap on the guest's memory, applied to its unit as `MemoryMax`.
|
||||
Accepts systemd size suffixes such as `512M` or `2G`. Left null,
|
||||
the guest's memory is uncapped.
|
||||
'';
|
||||
};
|
||||
cpu = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "150%";
|
||||
description = ''
|
||||
Cap on the guest's CPU, applied to its unit as `CPUQuota`, where
|
||||
`100%` is one full core. Left null, the guest's CPU is uncapped.
|
||||
'';
|
||||
};
|
||||
tasksMax = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.ints.positive;
|
||||
default = null;
|
||||
example = 512;
|
||||
description = ''
|
||||
Cap on the number of processes and threads the guest may spawn,
|
||||
applied to its unit as `TasksMax`. Left null, the task count is
|
||||
uncapped.
|
||||
'';
|
||||
};
|
||||
};
|
||||
nesting = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Grant the guest's interior the prerequisites to run Podman or other
|
||||
OCI containers of its own. Off by default, so a guest cannot nest
|
||||
containers. On, the guest's container gains the network-administration
|
||||
capability its container runtime uses to build bridges and firewall
|
||||
rules, along with the tun and fuse device nodes such a runtime reaches
|
||||
for, so the interior's `virtualisation.oci-containers` works with
|
||||
Podman as its default runtime.
|
||||
'';
|
||||
};
|
||||
autoStart = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Start the guest at boot. On by default. Disabled, the guest stays
|
||||
defined and can be started on demand, but does not come up at boot.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
sops.secrets = lib.genAttrs cfg.secrets (_: { });
|
||||
|
||||
registry.address.${config.networking.hostName}.${machineName} = lib.mkIf (cfg.address != null) cfg.address;
|
||||
|
||||
registry.route.${config.networking.hostName}.${machineName} = lib.mkIf cfg.reverseProxy.enable cfg.reverseProxy.route;
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = mountCollisions == [ ];
|
||||
message = ''
|
||||
guests.${name} maps a mount at ${lib.concatStringsSep ", " mountCollisions}, colliding with a secret bind-mounted at the same path. Rename the mount or the secret so each in-guest path is used once.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = cfg.backend == "container";
|
||||
message = ''
|
||||
guests.${name}.backend = "${cfg.backend}" is not implemented. Only the "container" backend is built; "microvm" is reserved for future work.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = !networked || lib.elem cfg.vlan config.modules.network.vlans.ids;
|
||||
message = ''
|
||||
guests.${name}.vlan = ${toString cfg.vlan} is not among its host's modules.network.vlans.ids (${lib.concatMapStringsSep ", " toString config.modules.network.vlans.ids}). Declare the VLAN on the host or correct the guest's placement.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
systemd.services."container@${machineName}" = lib.mkIf (cfg.backend == "container") (
|
||||
lib.mkMerge [
|
||||
{
|
||||
serviceConfig = limitConfig;
|
||||
preStart = lib.mkIf (createdMounts != { }) mountCreateScript;
|
||||
}
|
||||
(lib.mkIf networked (
|
||||
let
|
||||
bridgeDevice = "sys-subsystem-net-devices-${lib.replaceStrings [ "-" ] [ "\\x2d" ] (bridgeName cfg.vlan)}.device";
|
||||
in
|
||||
{
|
||||
after = [ bridgeDevice ];
|
||||
wants = [ bridgeDevice ];
|
||||
}
|
||||
))
|
||||
]
|
||||
);
|
||||
|
||||
containers.${machineName} = lib.mkIf (cfg.backend == "container") {
|
||||
autoStart = cfg.autoStart;
|
||||
privateNetwork = lib.mkDefault true;
|
||||
hostBridge = lib.mkIf networked (bridgeName cfg.vlan);
|
||||
privateUsers = lib.mkDefault "no";
|
||||
additionalCapabilities = lib.optionals cfg.nesting [ "CAP_NET_ADMIN" ];
|
||||
allowedDevices = lib.optionals cfg.nesting [
|
||||
{
|
||||
node = "/dev/net/tun";
|
||||
modifier = "rwm";
|
||||
}
|
||||
{
|
||||
node = "/dev/fuse";
|
||||
modifier = "rwm";
|
||||
}
|
||||
];
|
||||
|
||||
bindMounts = userMounts // secretMounts;
|
||||
|
||||
inherit specialArgs;
|
||||
|
||||
config = {
|
||||
imports =
|
||||
moduleFiles
|
||||
++ [
|
||||
registry.module
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.stylix.nixosModules.stylix
|
||||
{
|
||||
modules.guest.enable = true;
|
||||
}
|
||||
guestNet
|
||||
interior
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit deriveMac guest;
|
||||
}
|
||||
12
lib/network.nix
Normal file
12
lib/network.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
{ lib }:
|
||||
{
|
||||
bridgeName = id: "br-vlan${toString id}";
|
||||
|
||||
deriveMac =
|
||||
name:
|
||||
let
|
||||
hash = builtins.hashString "sha256" name;
|
||||
octet = i: builtins.substring (i * 2) 2 hash;
|
||||
in
|
||||
lib.concatStringsSep ":" ([ "02" ] ++ map octet [ 0 1 2 3 4 ]);
|
||||
}
|
||||
@@ -64,7 +64,6 @@ in
|
||||
useUserPackages = true;
|
||||
extraSpecialArgs = {
|
||||
inherit inputs;
|
||||
my = inputs.self.lib;
|
||||
};
|
||||
users.${user.name} = {
|
||||
home.username = user.name;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
my,
|
||||
util,
|
||||
...
|
||||
}:
|
||||
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
||||
let
|
||||
cfg = config.modules.network.vlans;
|
||||
|
||||
inherit (my) bridgeName;
|
||||
inherit (util) bridgeName;
|
||||
|
||||
vlanName = id: "${cfg.trunk}.${toString id}";
|
||||
in
|
||||
|
||||
Reference in New Issue
Block a user