diff --git a/AGENTS.md b/AGENTS.md index adcd3a8..c8bc775 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,10 +78,10 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla - `home.sessionVariables` do **not** reach the Hyprland session, since UWSM does not source `hm-session-vars.sh`. The cursor is therefore set through Hyprland's own `env = KEY,VALUE` in `modules/desktop/hyprland/hyprland.nix`, sourced from `config.stylix.cursor`. Bibata ships XCursor format only (no `hyprcursor/` dir), rendered through Hyprland's XCursor fallback, so `XCURSOR_*` and `HYPRCURSOR_*` naming the same theme are both safe. -- `neogaia`, the repo's only host, is a wifi laptop with a btrfs root and no ZFS pools, so it cannot honestly carry `modules.network`, `modules.zfs`, or a networked/pool-mounted guest. +- `neogaia` is a wifi laptop with a btrfs root and no ZFS pools, so it cannot honestly carry `modules.network`, `modules.zfs`, or a networked/pool-mounted guest. Enabling networkd takes over its DNS, its CachyOS `zfs-kernel` build is marked broken, and it has no bridge or pool to attach to. Verify these against it ad hoc through `nixosConfigurations.neogaia.extendModules` (forcing a ZFS-capable `boot.kernelPackages` for the zfs case) plus `nix eval` of the derived values, never by committing the enablement. - A committed guest therefore leaves `vlan`, `mounts`, and `secrets` unset, and the standing enablement waits for the first wired server host with real storage. + Committed networked and pool-mounted guests belong on a wired storage host such as `pikachu`, not on `neogaia`. - Herdr key names for shifted punctuation are not interchangeable with the physical base key plus `shift`. The tab rename binding must use the produced literal, such as `prefix+<`, rather than `prefix+shift+comma`. - Flake-managed Pi extension, prompt, and skill directories may still be written directly for throwaway development or local experiments. diff --git a/guests/copyparty.nix b/guests/copyparty.nix new file mode 100644 index 0000000..c5ec3f4 --- /dev/null +++ b/guests/copyparty.nix @@ -0,0 +1,113 @@ +{ + config, + lib, + my, + ... +}: +let + cfg = config.guests.copyparty; + accountName = "alexion"; + passwordSecret = "services/copyparty/users/alexion/password"; + serviceUid = my.serviceUid "copyparty"; + stateDir = "/var/lib/copyparty"; + inboxDir = "/srv/inbox"; + mediaDir = "/srv/media"; +in +{ + imports = [ + (my.guest { + name = "copyparty"; + interior = { + modules.services.copyparty = { + enable = true; + inherit accountName stateDir; + passwordFile = "/run/secrets/${passwordSecret}"; + volumes = { + "/inbox" = { + path = inboxDir; + access.A = [ accountName ]; + flags = [ + "d2d" + "d2t" + "dthumb" + ]; + }; + "/media" = { + path = mediaDir; + access.A = [ accountName ]; + flags = [ + "d2d" + "d2t" + "dthumb" + ]; + }; + }; + }; + }; + }) + ]; + + options.guests.copyparty = { + statePath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/srv/copyparty"; + description = "Host path bind-mounted as Copyparty's state directory."; + }; + + inboxPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/srv/inbox"; + description = "Host path bind-mounted as Copyparty's inbox volume."; + }; + + mediaPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/srv/media"; + description = "Host path bind-mounted as Copyparty's media volume."; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.statePath != null; + message = "guests.copyparty.statePath must point at the host directory that stores Copyparty state."; + } + { + assertion = cfg.inboxPath != null; + message = "guests.copyparty.inboxPath must point at the host directory served as /inbox."; + } + { + assertion = cfg.mediaPath != null; + message = "guests.copyparty.mediaPath must point at the host directory served as /media."; + } + ]; + + guests.copyparty = { + secrets = [ passwordSecret ]; + mounts = { + ${stateDir}.hostPath = cfg.statePath; + ${inboxDir}.hostPath = cfg.inboxPath; + ${mediaDir}.hostPath = cfg.mediaPath; + }; + limits = { + memory = lib.mkDefault "2G"; + cpu = lib.mkDefault "200%"; + tasksMax = lib.mkDefault 512; + }; + }; + + sops.secrets.${passwordSecret} = { + owner = toString serviceUid; + group = "storage"; + mode = "0440"; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.statePath} 0770 ${toString serviceUid} storage -" + ]; + }; +} diff --git a/hosts/pikachu/default.nix b/hosts/pikachu/default.nix index 43adb6f..f05b6a0 100644 --- a/hosts/pikachu/default.nix +++ b/hosts/pikachu/default.nix @@ -81,6 +81,15 @@ dataPath = "/pikachu/data/actualbudget"; }; + guests.copyparty = { + enable = true; + vlan = 20; + mac = "02:23:20:39:23:01"; + statePath = "/pikachu/data/copyparty"; + inboxPath = "/pikachu/__inbox"; + mediaPath = "/pikachu/media"; + }; + environment.systemPackages = with pkgs; [ pciutils smartmontools diff --git a/modules/services/copyparty.nix b/modules/services/copyparty.nix new file mode 100644 index 0000000..481f562 --- /dev/null +++ b/modules/services/copyparty.nix @@ -0,0 +1,179 @@ +{ + config, + lib, + my, + pkgs, + ... +}: +let + cfg = config.modules.services.copyparty; + + volumeType = lib.types.submodule { + options = { + path = lib.mkOption { + type = lib.types.str; + description = "Filesystem path served by this volume."; + }; + + access = lib.mkOption { + type = lib.types.attrsOf (lib.types.listOf lib.types.str); + default = { }; + example = { + A = [ "alexion" ]; + }; + description = "Copyparty access map from permission string to account names."; + }; + + flags = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "dthumb" ]; + description = "Copyparty volume flags."; + }; + }; + }; + + renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}"; + + renderFlags = flags: lib.concatMapStringsSep "\n" (flag: " ${flag}") flags; + + renderVolume = urlPath: volume: '' + [${urlPath}] + ${volume.path} + accs: + ${lib.concatStringsSep "\n" (lib.mapAttrsToList renderAccess volume.access)} + ${lib.optionalString (volume.flags != [ ]) '' + flags: + ${renderFlags volume.flags} + ''} + ''; + + renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes); + + configScript = pkgs.writeShellScript "copyparty-config" '' + set -eu + + umask 077 + password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")" + + cat > /run/copyparty/copyparty.conf <