nixos/services.mosquitto: remove with lib;

This commit is contained in:
Felix Buehler 2024-08-24 22:05:53 +02:00
parent 36eaab4720
commit 481df957a6

View File

@ -1,39 +1,36 @@
{ config, lib, pkgs, ...}:
with lib;
let
cfg = config.services.mosquitto;
# note that mosquitto config parsing is very simplistic as of may 2021.
# often times they'll e.g. strtok() a line, check the first two tokens, and ignore the rest.
# there's no escaping available either, so we have to prevent any being necessary.
str = types.strMatching "[^\r\n]*" // {
str = lib.types.strMatching "[^\r\n]*" // {
description = "single-line string";
};
path = types.addCheck types.path (p: str.check "${p}");
configKey = types.strMatching "[^\r\n\t ]+";
optionType = with types; oneOf [ str path bool int ] // {
path = lib.types.addCheck lib.types.path (p: str.check "${p}");
configKey = lib.types.strMatching "[^\r\n\t ]+";
optionType = with lib.types; oneOf [ str path bool int ] // {
description = "string, path, bool, or integer";
};
optionToString = v:
if isBool v then boolToString v
if lib.isBool v then lib.boolToString v
else if path.check v then "${v}"
else toString v;
assertKeysValid = prefix: valid: config:
mapAttrsToList
lib.mapAttrsToList
(n: _: {
assertion = valid ? ${n};
message = "Invalid config key ${prefix}.${n}.";
})
config;
formatFreeform = { prefix ? "" }: mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
formatFreeform = { prefix ? "" }: lib.mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
userOptions = with types; submodule {
userOptions = with lib.types; submodule {
options = {
password = mkOption {
password = lib.mkOption {
type = uniq (nullOr str);
default = null;
description = ''
@ -41,7 +38,7 @@ let
'';
};
passwordFile = mkOption {
passwordFile = lib.mkOption {
type = uniq (nullOr path);
example = "/path/to/file";
default = null;
@ -54,7 +51,7 @@ let
'';
};
hashedPassword = mkOption {
hashedPassword = lib.mkOption {
type = uniq (nullOr str);
default = null;
description = ''
@ -66,7 +63,7 @@ let
'';
};
hashedPasswordFile = mkOption {
hashedPasswordFile = lib.mkOption {
type = uniq (nullOr path);
example = "/path/to/file";
default = null;
@ -82,7 +79,7 @@ let
'';
};
acl = mkOption {
acl = lib.mkOption {
type = listOf str;
example = [ "read A/B" "readwrite A/#" ];
default = [];
@ -94,15 +91,15 @@ let
};
userAsserts = prefix: users:
mapAttrsToList
lib.mapAttrsToList
(n: _: {
assertion = builtins.match "[^:\r\n]+" n != null;
message = "Invalid user name ${n} in ${prefix}";
})
users
++ mapAttrsToList
++ lib.mapAttrsToList
(n: u: {
assertion = count (s: s != null) [
assertion = lib.count (s: s != null) [
u.password u.passwordFile u.hashedPassword u.hashedPasswordFile
] <= 1;
message = "Cannot set more than one password option for user ${n} in ${prefix}";
@ -112,26 +109,26 @@ let
userScope = prefix: index: "${prefix}-user-${toString index}";
credentialID = prefix: credential: "${prefix}-${credential}";
toScopedUsers = listenerScope: users: pipe users [
attrNames
(imap0 (index: user: nameValuePair user
toScopedUsers = listenerScope: users: lib.pipe users [
lib.attrNames
(lib.imap0 (index: user: lib.nameValuePair user
(users.${user} // { scope = userScope listenerScope index; })
))
listToAttrs
lib.listToAttrs
];
userCredentials = user: credentials: pipe credentials [
(filter (credential: user.${credential} != null))
userCredentials = user: credentials: lib.pipe credentials [
(lib.filter (credential: user.${credential} != null))
(map (credential: "${credentialID user.scope credential}:${user.${credential}}"))
];
usersCredentials = listenerScope: users: credentials: pipe users [
usersCredentials = listenerScope: users: credentials: lib.pipe users [
(toScopedUsers listenerScope)
(mapAttrsToList (_: user: userCredentials user credentials))
concatLists
(lib.mapAttrsToList (_: user: userCredentials user credentials))
lib.concatLists
];
systemdCredentials = listeners: listenerCredentials: pipe listeners [
(imap0 (index: listener: listenerCredentials (listenerScope index) listener))
concatLists
systemdCredentials = listeners: listenerCredentials: lib.pipe listeners [
(lib.imap0 (index: listener: listenerCredentials (listenerScope index) listener))
lib.concatLists
];
makePasswordFile = listenerScope: users: path:
@ -139,12 +136,12 @@ let
makeLines = store: file: let
scopedUsers = toScopedUsers listenerScope users;
in
mapAttrsToList
(name: user: ''addLine ${escapeShellArg name} "''$(systemd-creds cat ${credentialID user.scope store})"'')
(filterAttrs (_: user: user.${store} != null) scopedUsers)
++ mapAttrsToList
(name: user: ''addFile ${escapeShellArg name} "''${CREDENTIALS_DIRECTORY}/${credentialID user.scope file}"'')
(filterAttrs (_: user: user.${file} != null) scopedUsers);
lib.mapAttrsToList
(name: user: ''addLine ${lib.escapeShellArg name} "''$(systemd-creds cat ${credentialID user.scope store})"'')
(lib.filterAttrs (_: user: user.${store} != null) scopedUsers)
++ lib.mapAttrsToList
(name: user: ''addFile ${lib.escapeShellArg name} "''${CREDENTIALS_DIRECTORY}/${credentialID user.scope file}"'')
(lib.filterAttrs (_: user: user.${file} != null) scopedUsers);
plainLines = makeLines "password" "passwordFile";
hashedLines = makeLines "hashedPassword" "hashedPasswordFile";
in
@ -154,7 +151,7 @@ let
set -eu
file=${escapeShellArg path}
file=${lib.escapeShellArg path}
rm -f "$file"
touch "$file"
@ -170,23 +167,23 @@ let
echo "$1:$(cat "$2")" >> "$file"
}
''
+ concatStringsSep "\n"
+ lib.concatStringsSep "\n"
(plainLines
++ optional (plainLines != []) ''
++ lib.optional (plainLines != []) ''
${cfg.package}/bin/mosquitto_passwd -U "$file"
''
++ hashedLines));
authPluginOptions = with types; submodule {
authPluginOptions = with lib.types; submodule {
options = {
plugin = mkOption {
plugin = lib.mkOption {
type = path;
description = ''
Plugin path to load, should be a `.so` file.
'';
};
denySpecialChars = mkOption {
denySpecialChars = lib.mkOption {
type = bool;
description = ''
Automatically disallow all clients using `#`
@ -195,7 +192,7 @@ let
default = true;
};
options = mkOption {
options = lib.mkOption {
type = attrsOf optionType;
description = ''
Options for the auth plugin. Each key turns into a `auth_opt_*`
@ -207,7 +204,7 @@ let
};
authAsserts = prefix: auth:
mapAttrsToList
lib.mapAttrsToList
(n: _: {
assertion = configKey.check n;
message = "Invalid auth plugin key ${prefix}.${n}";
@ -253,9 +250,9 @@ let
use_username_as_clientid = 1;
};
listenerOptions = with types; submodule {
listenerOptions = with lib.types; submodule {
options = {
port = mkOption {
port = lib.mkOption {
type = port;
description = ''
Port to listen on. Must be set to 0 to listen on a unix domain socket.
@ -263,7 +260,7 @@ let
default = 1883;
};
address = mkOption {
address = lib.mkOption {
type = nullOr str;
description = ''
Address to listen on. Listen on `0.0.0.0`/`::`
@ -272,7 +269,7 @@ let
default = null;
};
authPlugins = mkOption {
authPlugins = lib.mkOption {
type = listOf authPluginOptions;
description = ''
Authentication plugin to attach to this listener.
@ -282,7 +279,7 @@ let
default = [];
};
users = mkOption {
users = lib.mkOption {
type = attrsOf userOptions;
example = { john = { password = "123456"; acl = [ "readwrite john/#" ]; }; };
description = ''
@ -291,7 +288,7 @@ let
default = {};
};
omitPasswordAuth = mkOption {
omitPasswordAuth = lib.mkOption {
type = bool;
description = ''
Omits password checking, allowing anyone to log in with any user name unless
@ -300,7 +297,7 @@ let
default = false;
};
acl = mkOption {
acl = lib.mkOption {
type = listOf str;
description = ''
Additional ACL items to prepend to the generated ACL file.
@ -309,7 +306,7 @@ let
default = [];
};
settings = mkOption {
settings = lib.mkOption {
type = submodule {
freeformType = attrsOf optionType;
};
@ -324,7 +321,7 @@ let
listenerAsserts = prefix: listener:
assertKeysValid "${prefix}.settings" freeformListenerKeys listener.settings
++ userAsserts prefix listener.users
++ imap0
++ lib.imap0
(i: v: authAsserts "${prefix}.authPlugins.${toString i}" v)
listener.authPlugins;
@ -333,9 +330,9 @@ let
"listener ${toString listener.port} ${toString listener.address}"
"acl_file /etc/mosquitto/acl-${toString idx}.conf"
]
++ optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
++ lib.optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
++ formatFreeform {} listener.settings
++ concatMap formatAuthPlugin listener.authPlugins;
++ lib.concatMap formatAuthPlugin listener.authPlugins;
freeformBridgeKeys = {
bridge_alpn = 1;
@ -373,19 +370,19 @@ let
try_private = 1;
};
bridgeOptions = with types; submodule {
bridgeOptions = with lib.types; submodule {
options = {
addresses = mkOption {
addresses = lib.mkOption {
type = listOf (submodule {
options = {
address = mkOption {
address = lib.mkOption {
type = str;
description = ''
Address of the remote MQTT broker.
'';
};
port = mkOption {
port = lib.mkOption {
type = port;
description = ''
Port of the remote MQTT broker.
@ -400,7 +397,7 @@ let
'';
};
topics = mkOption {
topics = lib.mkOption {
type = listOf str;
description = ''
Topic patterns to be shared between the two brokers.
@ -411,7 +408,7 @@ let
example = [ "# both 2 local/topic/ remote/topic/" ];
};
settings = mkOption {
settings = lib.mkOption {
type = submodule {
freeformType = attrsOf optionType;
};
@ -426,14 +423,14 @@ let
bridgeAsserts = prefix: bridge:
assertKeysValid "${prefix}.settings" freeformBridgeKeys bridge.settings
++ [ {
assertion = length bridge.addresses > 0;
assertion = lib.length bridge.addresses > 0;
message = "Bridge ${prefix} needs remote broker addresses";
} ];
formatBridge = name: bridge:
[
"connection ${name}"
"addresses ${concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
"addresses ${lib.concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
]
++ map (t: "topic ${t}") bridge.topics
++ formatFreeform {} bridge.settings;
@ -468,12 +465,12 @@ let
websockets_log_level = 1;
};
globalOptions = with types; {
enable = mkEnableOption "the MQTT Mosquitto broker";
globalOptions = with lib.types; {
enable = lib.mkEnableOption "the MQTT Mosquitto broker";
package = mkPackageOption pkgs "mosquitto" { };
package = lib.mkPackageOption pkgs "mosquitto" { };
bridges = mkOption {
bridges = lib.mkOption {
type = attrsOf bridgeOptions;
default = {};
description = ''
@ -481,7 +478,7 @@ let
'';
};
listeners = mkOption {
listeners = lib.mkOption {
type = listOf listenerOptions;
default = [];
description = ''
@ -489,7 +486,7 @@ let
'';
};
includeDirs = mkOption {
includeDirs = lib.mkOption {
type = listOf path;
description = ''
Directories to be scanned for further config files to include.
@ -500,7 +497,7 @@ let
default = [];
};
logDest = mkOption {
logDest = lib.mkOption {
type = listOf (either path (enum [ "stdout" "stderr" "syslog" "topic" "dlt" ]));
description = ''
Destinations to send log messages to.
@ -508,7 +505,7 @@ let
default = [ "stderr" ];
};
logType = mkOption {
logType = lib.mkOption {
type = listOf (enum [ "debug" "error" "warning" "notice" "information"
"subscribe" "unsubscribe" "websockets" "none" "all" ]);
description = ''
@ -517,7 +514,7 @@ let
default = [];
};
persistence = mkOption {
persistence = lib.mkOption {
type = bool;
description = ''
Enable persistent storage of subscriptions and messages.
@ -525,15 +522,15 @@ let
default = true;
};
dataDir = mkOption {
dataDir = lib.mkOption {
default = "/var/lib/mosquitto";
type = types.path;
type = lib.types.path;
description = ''
The data directory.
'';
};
settings = mkOption {
settings = lib.mkOption {
type = submodule {
freeformType = attrsOf optionType;
};
@ -545,10 +542,10 @@ let
};
globalAsserts = prefix: cfg:
flatten [
lib.flatten [
(assertKeysValid "${prefix}.settings" freeformGlobalKeys cfg.settings)
(imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
(mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
(lib.imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
(lib.mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
];
formatGlobal = cfg:
@ -561,12 +558,12 @@ let
cfg.logDest
++ map (t: "log_type ${t}") cfg.logType
++ formatFreeform {} cfg.settings
++ concatLists (imap0 formatListener cfg.listeners)
++ concatLists (mapAttrsToList formatBridge cfg.bridges)
++ lib.concatLists (lib.imap0 formatListener cfg.listeners)
++ lib.concatLists (lib.mapAttrsToList formatBridge cfg.bridges)
++ map (d: "include_dir ${d}") cfg.includeDirs;
configFile = pkgs.writeText "mosquitto.conf"
(concatStringsSep "\n" (formatGlobal cfg));
(lib.concatStringsSep "\n" (formatGlobal cfg));
in
@ -578,7 +575,7 @@ in
###### Implementation
config = mkIf cfg.enable {
config = lib.mkIf cfg.enable {
assertions = globalAsserts "services.mosquitto" cfg;
@ -633,13 +630,13 @@ in
ReadWritePaths = [
cfg.dataDir
"/tmp" # mosquitto_passwd creates files in /tmp before moving them
] ++ filter path.check cfg.logDest;
] ++ lib.filter path.check cfg.logDest;
ReadOnlyPaths =
map (p: "${p}")
(cfg.includeDirs
++ filter
++ lib.filter
(v: v != null)
(flatten [
(lib.flatten [
(map
(l: [
(l.settings.psk_file or null)
@ -652,7 +649,7 @@ in
(l.settings.keyfile or null)
])
cfg.listeners)
(mapAttrsToList
(lib.mapAttrsToList
(_: b: [
(b.settings.bridge_cafile or null)
(b.settings.bridge_capath or null)
@ -680,26 +677,26 @@ in
UMask = "0077";
};
preStart =
concatStringsSep
lib.concatStringsSep
"\n"
(imap0
(lib.imap0
(idx: listener: makePasswordFile (listenerScope idx) listener.users "${cfg.dataDir}/passwd-${toString idx}")
cfg.listeners);
};
environment.etc = listToAttrs (
imap0
environment.etc = lib.listToAttrs (
lib.imap0
(idx: listener: {
name = "mosquitto/acl-${toString idx}.conf";
value = {
user = config.users.users.mosquitto.name;
group = config.users.users.mosquitto.group;
mode = "0400";
text = (concatStringsSep
text = (lib.concatStringsSep
"\n"
(flatten [
(lib.flatten [
listener.acl
(mapAttrsToList
(lib.mapAttrsToList
(n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl)
listener.users)
]));