nixos/syncplay: add missing options

Exposes all currently available command-line arguments that were
missing, including some that were impossible to use with the catch-all
option `extraArgs` alone, requiring changes to other parts of the
system.
Those are now all self-contained in the module.
The service now uses systemd's `DynamicUsers`.
This commit is contained in:
Assistant 2024-08-31 04:11:56 -04:00
parent e6c09b2d57
commit 8119ec6478
2 changed files with 216 additions and 31 deletions

View File

@ -368,6 +368,9 @@
- The `antennas` package and the `services.antennas` module have been - The `antennas` package and the `services.antennas` module have been
removed as they only work with `tvheadend` (see above). removed as they only work with `tvheadend` (see above).
- The `services.syncplay` module now exposes all currently available command-line arguments for `syncplay-server` as options, as well as a `useACMEHost` option for easy TLS setup.
The systemd service now uses `DynamicUser`/`StateDirectory` and the `user` and `group` options have been deprecated.
## Other Notable Changes {#sec-release-24.11-notable-changes} ## Other Notable Changes {#sec-release-24.11-notable-changes}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

View File

@ -7,18 +7,41 @@ let
cmdArgs = cmdArgs =
[ "--port" cfg.port ] [ "--port" cfg.port ]
++ optionals (cfg.isolateRooms) [ "--isolate-rooms" ]
++ optionals (!cfg.ready) [ "--disable-ready" ]
++ optionals (!cfg.chat) [ "--disable-chat" ]
++ optionals (cfg.salt != null) [ "--salt" cfg.salt ] ++ optionals (cfg.salt != null) [ "--salt" cfg.salt ]
++ optionals (cfg.motdFile != null) [ "--motd-file" cfg.motdFile ]
++ optionals (cfg.roomsDBFile != null) [ "--rooms-db-file" cfg.roomsDBFile ]
++ optionals (cfg.permanentRoomsFile != null) [ "--permanent-rooms-file" cfg.permanentRoomsFile ]
++ [ "--max-chat-message-length" cfg.maxChatMessageLength ]
++ [ "--max-username-length" cfg.maxUsernameLength ]
++ optionals (cfg.statsDBFile != null) [ "--stats-db-file" cfg.statsDBFile ]
++ optionals (cfg.certDir != null) [ "--tls" cfg.certDir ] ++ optionals (cfg.certDir != null) [ "--tls" cfg.certDir ]
++ optionals cfg.ipv4Only [ "--ipv4-only" ]
++ optionals cfg.ipv6Only [ "--ipv6-only" ]
++ optionals (cfg.interfaceIpv4 != "") [ "--interface-ipv4" cfg.interfaceIpv4 ]
++ optionals (cfg.interfaceIpv6 != "") [ "--interface-ipv6" cfg.interfaceIpv6 ]
++ cfg.extraArgs; ++ cfg.extraArgs;
useACMEHostDir = optionalString (cfg.useACMEHost != null) config.security.acme.certs.${cfg.useACMEHost}.directory;
in in
{ {
imports = [
(mkRemovedOptionModule [ "services" "syncplay" "user" ]
"The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality.")
(mkRemovedOptionModule [ "services" "syncplay" "group" ]
"The syncplay service now uses DynamicUser, override the systemd unit settings if you need the old functionality.")
];
options = { options = {
services.syncplay = { services.syncplay = {
enable = mkOption { enable = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = "If enabled, start the Syncplay server."; description = ''
If enabled, start the Syncplay server.
'';
}; };
port = mkOption { port = mkOption {
@ -29,6 +52,39 @@ in
''; '';
}; };
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to the file that contains the server password. If
`null`, the server doesn't require a password.
'';
};
isolateRooms = mkOption {
type = types.bool;
default = false;
description = ''
Enable room isolation.
'';
};
ready = mkOption {
type = types.bool;
default = true;
description = ''
Check readiness of users.
'';
};
chat = mkOption {
type = types.bool;
default = true;
description = ''
Chat with users in the same room.
'';
};
salt = mkOption { salt = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
@ -37,7 +93,7 @@ in
instance to still work when the server is restarted. The salt will be instance to still work when the server is restarted. The salt will be
readable in the nix store and the processlist. If this is not readable in the nix store and the processlist. If this is not
intended use `saltFile` instead. Mutually exclusive with intended use `saltFile` instead. Mutually exclusive with
<option>services.syncplay.saltFile</option>. {option}`services.syncplay.saltFile`.
''; '';
}; };
@ -49,7 +105,83 @@ in
operator passwords generated by this server instance to still work operator passwords generated by this server instance to still work
when the server is restarted. `null`, the server doesn't load the when the server is restarted. `null`, the server doesn't load the
salt from a file. Mutually exclusive with salt from a file. Mutually exclusive with
<option>services.syncplay.salt</option>. {option}`services.syncplay.salt`.
'';
};
motd = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Text to display when users join. The motd will be readable in the nix store
and the processlist. If this is not intended use `motdFile` instead.
Will be overriden by {option}`services.syncplay.motdFile`.
'';
};
motdFile = mkOption {
type = types.nullOr types.str;
default = if cfg.motd != null then (builtins.toFile "motd" cfg.motd) else null;
defaultText = literalExpression ''if services.syncplay.motd != null then (builtins.toFile "motd" services.syncplay.motd) else null'';
description = ''
Path to text to display when users join.
Will override {option}`services.syncplay.motd`.
'';
};
roomsDBFile = mkOption {
type = types.nullOr types.str;
default = null;
example = "rooms.db";
description = ''
Path to SQLite database file to store room states.
Relative to the working directory provided by systemd.
'';
};
permanentRooms = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
List of rooms that will be listed even if the room is empty.
Will be overriden by {option}`services.syncplay.permanentRoomsFile`.
'';
};
permanentRoomsFile = mkOption {
type = types.nullOr types.str;
default = if cfg.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" cfg.permanentRooms)) else null;
defaultText = literalExpression ''if services.syncplay.permanentRooms != [ ] then (builtins.toFile "perm" (builtins.concatStringsSep "\n" services.syncplay.permanentRooms)) else null'';
description = ''
File with list of rooms that will be listed even if the room is empty,
newline delimited.
Will override {option}`services.syncplay.permanentRooms`.
'';
};
maxChatMessageLength = mkOption {
type = types.ints.unsigned;
default = 150;
description = ''
Maximum number of characters in a chat message.
'';
};
maxUsernameLength = mkOption {
type = types.ints.unsigned;
default = 16;
description = ''
Maximum number of characters in a username.
'';
};
statsDBFile = mkOption {
type = types.nullOr types.str;
default = null;
example = "stats.db";
description = ''
Path to SQLite database file to store stats.
Relative to the working directory provided by systemd.
''; '';
}; };
@ -62,6 +194,49 @@ in
''; '';
}; };
useACMEHost = mkOption {
type = types.nullOr types.str;
default = null;
example = "syncplay.example.com";
description = ''
If set, use NixOS-generated ACME certificate with the specified name for TLS.
Note that it requires {option}`security.acme` to be setup, e.g., credentials provided if using DNS-01 validation.
'';
};
ipv4Only = mkOption {
type = types.bool;
default = false;
description = ''
Listen only on IPv4 when strting the server.
'';
};
ipv6Only = mkOption {
type = types.bool;
default = false;
description = ''
Listen only on IPv6 when strting the server.
'';
};
interfaceIpv4 = mkOption {
type = types.str;
default = "";
description = ''
The IP address to bind to for IPv4. Leaving it empty defaults to using all.
'';
};
interfaceIpv6 = mkOption {
type = types.str;
default = "";
description = ''
The IP address to bind to for IPv6. Leaving it empty defaults to using all.
'';
};
extraArgs = mkOption { extraArgs = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = [ ]; default = [ ];
@ -70,28 +245,12 @@ in
''; '';
}; };
user = mkOption { package = mkOption {
type = types.str; type = types.package;
default = "nobody"; default = pkgs.syncplay-nogui;
defaultText = literalExpression "pkgs.syncplay-nogui";
description = '' description = ''
User to use when running Syncplay. Package to use for syncplay.
'';
};
group = mkOption {
type = types.str;
default = "nogroup";
description = ''
Group to use when running Syncplay.
'';
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to the file that contains the server password. If
`null`, the server doesn't require a password.
''; '';
}; };
}; };
@ -103,7 +262,24 @@ in
assertion = cfg.salt == null || cfg.saltFile == null; assertion = cfg.salt == null || cfg.saltFile == null;
message = "services.syncplay.salt and services.syncplay.saltFile are mutually exclusive."; message = "services.syncplay.salt and services.syncplay.saltFile are mutually exclusive.";
} }
{
assertion = cfg.certDir == null || cfg.useACMEHost == null;
message = "services.syncplay.certDir and services.syncplay.useACMEHost are mutually exclusive.";
}
{
assertion = !cfg.ipv4Only || !cfg.ipv6Only;
message = "services.syncplay.ipv4Only and services.syncplay.ipv6Only are mutually exclusive.";
}
]; ];
warnings = optional (cfg.interfaceIpv4 != "" && cfg.ipv6Only) "You have specified services.syncplay.interfaceIpv4 but IPv4 is disabled by services.syncplay.ipv6Only."
++ optional (cfg.interfaceIpv6 != "" && cfg.ipv4Only) "You have specified services.syncplay.interfaceIpv6 but IPv6 is disabled by services.syncplay.ipv4Only.";
security.acme.certs = mkIf (cfg.useACMEHost != null) {
"${cfg.useACMEHost}".reloadServices = [ "syncplay.service" ];
};
networking.firewall.allowedTCPPorts = [ cfg.port ];
systemd.services.syncplay = { systemd.services.syncplay = {
description = "Syncplay Service"; description = "Syncplay Service";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
@ -111,20 +287,26 @@ in
after = [ "network-online.target" ]; after = [ "network-online.target" ];
serviceConfig = { serviceConfig = {
User = cfg.user; DynamicUser = true;
Group = cfg.group; StateDirectory = "syncplay";
LoadCredential = lib.optional (cfg.passwordFile != null) "password:${cfg.passwordFile}" WorkingDirectory = "%S/syncplay";
++ lib.optional (cfg.saltFile != null) "salt:${cfg.saltFile}"; LoadCredential = optional (cfg.passwordFile != null) "password:${cfg.passwordFile}"
++ optional (cfg.saltFile != null) "salt:${cfg.saltFile}"
++ optionals (cfg.useACMEHost != null) [
"cert.pem:${useACMEHostDir}/cert.pem"
"privkey.pem:${useACMEHostDir}/key.pem"
"chain.pem:${useACMEHostDir}/chain.pem"
];
}; };
script = '' script = ''
${lib.optionalString (cfg.passwordFile != null) '' ${optionalString (cfg.passwordFile != null) ''
export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password") export SYNCPLAY_PASSWORD=$(cat "''${CREDENTIALS_DIRECTORY}/password")
''} ''}
${lib.optionalString (cfg.saltFile != null) '' ${optionalString (cfg.saltFile != null) ''
export SYNCPLAY_SALT=$(cat "''${CREDENTIALS_DIRECTORY}/salt") export SYNCPLAY_SALT=$(cat "''${CREDENTIALS_DIRECTORY}/salt")
''} ''}
exec ${pkgs.syncplay-nogui}/bin/syncplay-server ${escapeShellArgs cmdArgs} exec ${cfg.package}/bin/syncplay-server ${escapeShellArgs cmdArgs} ${optionalString (cfg.useACMEHost != null) "--tls $CREDENTIALS_DIRECTORY"}
''; '';
}; };
}; };