Files
Nomarchy/modules/nixos/services.nix
Bernardo Magri 964a5c98b7 feat(services): openrgb + restic toggles; close out the opt-in services item
Finishing the four remaining "opt-in services" candidates — but two of
them aren't service-shaped, so the package-vs-toggle principle applies:

  - Nextcloud client  → a bare tray app; already in the app-suite menu,
                        not a service.
  - WireGuard         → NetworkManager (on distro-wide) imports .conf
                        tunnels natively, so there's no daemon to "just
                        enable". Only the wg/wg-quick CLI is missing →
                        wireguard-tools added to the app-suite menu.

The two that ARE config-backed become nomarchy.services.* toggles:

  - openrgb → services.hardware.openrgb (daemon + device udev rules).
  - restic  → a small scaffolded surface (repository / passwordFile /
              paths) over services.restic.backups: daily timer, 7/4/6
              retention, --exclude-caches, repo auto-init. Asserts that
              repository + passwordFile are set when enabled. passwordFile
              is typed `str`, not `path`, so a flake-relative secret can't
              be copied into the world-readable store.

README option table and ROADMAP (services now "Seventeen shipped";
cloud-sync / networking / backup / devices areas updated) + the downstream
system.nix examples. Both branches eval-verified; the restic assertions
confirmed firing when repo/password are unset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:02:20 +01:00

279 lines
10 KiB
Nix

# Opt-in services & integrations — off by default, one toggle each, enabled
# in system.nix (see the commented examples in the downstream template).
# These are system-side: their own `nomarchy.services.*` namespace, distinct
# from the distro's core `nomarchy.system.*` toggles. `username` (from
# specialArgs) is read lazily, only where a service needs it.
{ config, lib, pkgs, ... }@args:
let
cfg = config.nomarchy.services;
in
{
options.nomarchy.services = {
tailscale.enable = lib.mkEnableOption ''
Tailscale, the mesh VPN ships the daemon; authenticate once with
`sudo tailscale up`'';
syncthing.enable = lib.mkEnableOption ''
Syncthing continuous file sync, running as the login user add
folders and devices in the web UI at http://127.0.0.1:8384'';
podman.enable = lib.mkEnableOption ''
Podman for rootless containers, with `docker` aliased to it and DNS on
the default network; the login user gets a subuid/subgid range'';
flatpak.enable = lib.mkEnableOption ''
Flatpak, with the Flathub remote added system-wide install apps with
`flatpak install flathub <app>`'';
pika.enable = lib.mkEnableOption ''
Pika Backup, a friendly GUI for scheduled Borg backups (the app;
configure repositories and schedules inside it)'';
steam.enable = lib.mkEnableOption ''
Steam (Valve's game store/launcher) via programs.steam which wires up
the 32-bit graphics libraries, controller udev rules and Remote-Play
firewall ports a bare package can't. Unfree'';
libvirt.enable = lib.mkEnableOption ''
libvirt/KVM virtualisation with the virt-manager GUI runs the
libvirtd daemon and adds the login user to the `libvirtd` group, so VMs
can be managed without root'';
obs.enable = lib.mkEnableOption ''
OBS Studio for screen recording and streaming, plus a v4l2loopback
virtual camera so an OBS scene can be selected as a webcam in Zoom,
Teams or a browser (screen capture itself already works over the
desktop's PipeWire portal)'';
docker.enable = lib.mkEnableOption ''
Docker (rootful) with the login user in the `docker` group. Mutually
exclusive with the podman toggle's docker-compat (both provide the
`docker` command) enable only one'';
kdeconnect.enable = lib.mkEnableOption ''
KDE Connect for phone integration shared clipboard, notifications and
file transfer with a paired phone; opens the firewall ports it needs'';
gamemode.enable = lib.mkEnableOption ''
Feral GameMode a daemon games ask to apply a performance CPU governor
and IO/nice priorities while running (via `gamemoderun`)'';
adb.enable = lib.mkEnableOption ''
the Android platform tools (adb/fastboot) for flashing/debugging phones.
systemd 258 applies the device uaccess udev rules automatically, so the
tools alone are enough no group dance'';
wireshark.enable = lib.mkEnableOption ''
Wireshark (the Qt GUI) for packet capture, with the login user in the
`wireshark` group so capture works without root'';
ollama.enable = lib.mkEnableOption ''
Ollama, a local LLM runtime serving an API on 127.0.0.1:11434 (pair it
with the lmstudio/alpaca GUIs). CPU by default set
`services.ollama.acceleration` natively for GPU offload'';
printing.enable = lib.mkEnableOption ''
CUPS printing with Avahi/mDNS, so network printers are auto-discovered
(add vendor drivers via `services.printing.drivers`)'';
openrgb.enable = lib.mkEnableOption ''
the OpenRGB daemon and GUI for controlling RGB lighting on peripherals
and (where supported) motherboards installs the udev rules for device
access. Some motherboard controllers also need `hardware.i2c.enable`'';
restic = {
enable = lib.mkEnableOption ''
a scheduled (daily) restic backup headless, complementing the Pika
GUI. Backs up `paths` with sane retention (7 daily / 4 weekly /
6 monthly); needs a `repository` and a `passwordFile` (below)'';
repository = lib.mkOption {
type = lib.types.str;
default = "";
example = "/mnt/backup/restic";
description = ''
The restic repository as a path or URL e.g. `/mnt/backup/restic`,
`sftp:user@host:/srv/restic`, `b2:bucket:/path`. Required when
`restic.enable` is set.
'';
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/etc/nomarchy/restic-password";
description = ''
Path to a file holding the repository password. Give an absolute
path that lives ON THE MACHINE not a `./file` in the flake, which
would copy the secret into the world-readable Nix store. Required
when `restic.enable` is set.
'';
};
paths = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "/home" ];
description = "Paths to include in the backup.";
};
};
};
config = lib.mkMerge [
(lib.mkIf cfg.tailscale.enable {
services.tailscale.enable = true;
})
(lib.mkIf cfg.syncthing.enable {
services.syncthing = {
enable = true;
user = args.username;
# Manage folders/devices in the GUI; don't let the (empty) declarative
# config wipe them on a rebuild.
overrideDevices = false;
overrideFolders = false;
guiAddress = "127.0.0.1:8384";
};
})
(lib.mkIf cfg.podman.enable {
virtualisation.podman = {
enable = true;
dockerCompat = lib.mkDefault true; # `docker` → podman (off if you run real docker)
defaultNetwork.settings.dns_enabled = true; # DNS for rootless containers
};
# Rootless podman needs a subuid/subgid range for the user.
users.users.${args.username}.autoSubUidGidRange = lib.mkDefault true;
})
(lib.mkIf cfg.flatpak.enable {
services.flatpak.enable = true;
# Flatpak ships no remotes; add Flathub system-wide. Idempotent
# (--if-not-exists), and it reads the .flatpakrepo over the network, so
# it waits for connectivity and simply retries next boot if offline.
systemd.services.nomarchy-flathub = {
description = "Add the Flathub Flatpak remote";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
path = [ pkgs.flatpak ];
serviceConfig.Type = "oneshot";
script = ''
flatpak remote-add --system --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
'';
};
})
(lib.mkIf cfg.pika.enable {
environment.systemPackages = [ pkgs.pika-backup ];
})
(lib.mkIf cfg.steam.enable {
# The Steam package alone misses the system bits a game needs: the
# 32-bit graphics stack, controller udev rules, Remote-Play ports.
programs.steam.enable = true;
})
(lib.mkIf cfg.libvirt.enable {
virtualisation.libvirtd.enable = true;
programs.virt-manager.enable = true;
# Group membership lets the user reach the system libvirtd socket
# (manage VMs) without sudo. Merges with the user's other extraGroups.
users.users.${args.username}.extraGroups = [ "libvirtd" ];
})
(lib.mkIf cfg.obs.enable {
environment.systemPackages = [ pkgs.obs-studio ];
# Virtual camera: OBS streams a scene into a v4l2 loopback device that
# Zoom/Teams/browsers then pick up as a webcam. exclusive_caps=1 is the
# bit that makes those apps actually list the device.
boot.extraModulePackages = [ config.boot.kernelPackages.v4l2loopback ];
boot.kernelModules = [ "v4l2loopback" ];
boot.extraModprobeConfig = ''
options v4l2loopback devices=1 video_nr=10 card_label="OBS Virtual Camera" exclusive_caps=1
'';
})
(lib.mkIf cfg.docker.enable {
virtualisation.docker.enable = true;
users.users.${args.username}.extraGroups = [ "docker" ];
# Podman's docker-compat and real Docker both claim the `docker`
# command — guard against enabling both (unless you turn that off).
assertions = [{
assertion = !(cfg.podman.enable && config.virtualisation.podman.dockerCompat);
message = ''
nomarchy.services: docker and podman both provide the `docker`
command. Enable only one, or set
`virtualisation.podman.dockerCompat = false`.
'';
}];
})
(lib.mkIf cfg.kdeconnect.enable {
programs.kdeconnect.enable = true; # installs the app and opens its ports
})
(lib.mkIf cfg.gamemode.enable {
programs.gamemode.enable = true;
})
(lib.mkIf cfg.adb.enable {
# `programs.adb` was removed — systemd ≥258 handles the device uaccess
# udev rules, so the tools package is all that's needed now.
environment.systemPackages = [ pkgs.android-tools ];
})
(lib.mkIf cfg.wireshark.enable {
programs.wireshark = {
enable = true;
package = pkgs.wireshark; # the Qt GUI (the module defaults to the CLI)
};
users.users.${args.username}.extraGroups = [ "wireshark" ];
})
(lib.mkIf cfg.ollama.enable {
services.ollama.enable = true;
})
(lib.mkIf cfg.printing.enable {
services.printing.enable = true;
# mDNS so network printers show up without manual setup.
services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = true;
};
})
(lib.mkIf cfg.openrgb.enable {
services.hardware.openrgb.enable = true;
})
(lib.mkIf cfg.restic.enable {
assertions = [
{
assertion = cfg.restic.repository != "";
message = "nomarchy.services.restic.repository must be set when restic is enabled.";
}
{
assertion = cfg.restic.passwordFile != null;
message = "nomarchy.services.restic.passwordFile must be set when restic is enabled.";
}
];
services.restic.backups.nomarchy = {
repository = cfg.restic.repository;
passwordFile = cfg.restic.passwordFile;
paths = cfg.restic.paths;
initialize = true; # create the repo on first run if it doesn't exist
extraBackupArgs = [ "--exclude-caches" ];
pruneOpts = [ "--keep-daily 7" "--keep-weekly 4" "--keep-monthly 6" ];
timerConfig = {
OnCalendar = "daily";
Persistent = true; # catch up a missed run after downtime
};
};
})
];
}