feat(copyparty): add NixOS guest
This commit is contained in:
@@ -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`.
|
- `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`.
|
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.
|
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.
|
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.
|
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`.
|
- 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`.
|
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.
|
- Flake-managed Pi extension, prompt, and skill directories may still be written directly for throwaway development or local experiments.
|
||||||
|
|||||||
113
guests/copyparty.nix
Normal file
113
guests/copyparty.nix
Normal file
@@ -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 -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -81,6 +81,15 @@
|
|||||||
dataPath = "/pikachu/data/actualbudget";
|
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; [
|
environment.systemPackages = with pkgs; [
|
||||||
pciutils
|
pciutils
|
||||||
smartmontools
|
smartmontools
|
||||||
|
|||||||
179
modules/services/copyparty.nix
Normal file
179
modules/services/copyparty.nix
Normal file
@@ -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 <<EOF
|
||||||
|
[global]
|
||||||
|
p: ${toString cfg.port}
|
||||||
|
usernames
|
||||||
|
http-only
|
||||||
|
no-crt
|
||||||
|
no-thumb
|
||||||
|
no-snap
|
||||||
|
no-rescan
|
||||||
|
ses-db: ${cfg.stateDir}/sessions.db
|
||||||
|
chpw-db: ${cfg.stateDir}/chpw.json
|
||||||
|
shr-db: ${cfg.stateDir}/shares.db
|
||||||
|
|
||||||
|
[accounts]
|
||||||
|
${cfg.accountName}: $password
|
||||||
|
|
||||||
|
${renderedVolumes}
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.modules.services.copyparty = {
|
||||||
|
enable = lib.mkEnableOption "Copyparty";
|
||||||
|
|
||||||
|
package = lib.mkPackageOption pkgs "copyparty-min" { };
|
||||||
|
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.port;
|
||||||
|
default = 3923;
|
||||||
|
description = "Port Copyparty listens on.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "copyparty";
|
||||||
|
description = "User account that runs Copyparty.";
|
||||||
|
};
|
||||||
|
|
||||||
|
uid = lib.mkOption {
|
||||||
|
type = lib.types.ints.positive;
|
||||||
|
default = my.serviceUid "copyparty";
|
||||||
|
description = "Stable uid for the Copyparty service user.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "storage";
|
||||||
|
description = "Group account that owns Copyparty's writable state and served files.";
|
||||||
|
};
|
||||||
|
|
||||||
|
accountName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "alexion";
|
||||||
|
description = "Copyparty account name configured for password authentication.";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "Runtime file containing the Copyparty account password.";
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/var/lib/copyparty";
|
||||||
|
description = "Directory where Copyparty stores service state.";
|
||||||
|
};
|
||||||
|
|
||||||
|
volumes = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf volumeType;
|
||||||
|
default = { };
|
||||||
|
description = "Copyparty volumes keyed by their URL path.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = cfg.volumes != { };
|
||||||
|
message = "modules.services.copyparty.volumes must declare at least one served volume.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = cfg.uid;
|
||||||
|
group = cfg.group;
|
||||||
|
home = cfg.stateDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.copyparty = {
|
||||||
|
description = "Copyparty file server";
|
||||||
|
after = [ "network.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ReloadSignal = "SIGUSR1";
|
||||||
|
Type = "simple";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
WorkingDirectory = cfg.stateDir;
|
||||||
|
Environment = [
|
||||||
|
"HOME=${cfg.stateDir}"
|
||||||
|
"XDG_CONFIG_HOME=${cfg.stateDir}"
|
||||||
|
];
|
||||||
|
StateDirectory = "copyparty";
|
||||||
|
RuntimeDirectory = "copyparty";
|
||||||
|
RuntimeDirectoryMode = "0700";
|
||||||
|
LoadCredential = [ "password:${cfg.passwordFile}" ];
|
||||||
|
ExecStartPre = configScript;
|
||||||
|
ExecStart = "${lib.getExe cfg.package} -c /run/copyparty/copyparty.conf";
|
||||||
|
Restart = "on-failure";
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
ReadWritePaths = [ cfg.stateDir ] ++ map (volume: volume.path) (lib.attrValues cfg.volumes);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user