nixos/ups: add {users,upsmon,upsd} config options

This commit is contained in:
Majiir Paktu 2023-01-21 22:07:54 -05:00
parent 09002e9d23
commit c1793ff623
2 changed files with 360 additions and 13 deletions

View file

@ -24,6 +24,8 @@ In addition to numerous new and upgraded packages, this release has the followin
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
- The `power.ups` module now generates `upsd.conf`, `upsd.users` and `upsmon.conf` automatically from a set of new configuration options. This breaks compatibility with existing `power.ups` setups where these files were created manually. Back up these files before upgrading NixOS.
- `mkosi` was updated to v19. Parts of the user interface have changed. Consult the
[release notes](https://github.com/systemd/mkosi/releases/tag/v19) for a list of changes.

View file

@ -6,6 +6,82 @@ with lib;
let
cfg = config.power.ups;
defaultPort = 3493;
nutFormat = {
type = with lib.types; let
singleAtom = nullOr (oneOf [
bool
int
float
str
]) // {
description = "atom (null, bool, int, float or string)";
};
in attrsOf (oneOf [
singleAtom
(listOf (nonEmptyListOf singleAtom))
]);
generate = name: value:
let
normalizedValue =
lib.mapAttrs (key: val:
if lib.isList val
then forEach val (elem: if lib.isList elem then elem else [elem])
else
if val == null
then []
else [[val]]
) value;
mkValueString = concatMapStringsSep " " (v:
let str = generators.mkValueStringDefault {} v;
in
# Quote the value if it has spaces and isn't already quoted.
if (hasInfix " " str) && !(hasPrefix "\"" str && hasSuffix "\"" str)
then "\"${str}\""
else str
);
in pkgs.writeText name (lib.generators.toKeyValue {
mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " ";
listsAsDuplicateKeys = true;
} normalizedValue);
};
installSecrets = source: target: secrets:
pkgs.writeShellScript "installSecrets.sh" ''
install -m0600 -D ${source} "${target}"
${concatLines (forEach secrets (name: ''
${pkgs.replace-secret}/bin/replace-secret \
'@${name}@' \
"$CREDENTIALS_DIRECTORY/${name}" \
"${target}"
''))}
chmod u-w "${target}"
'';
upsmonConf = nutFormat.generate "upsmon.conf" cfg.upsmon.settings;
upsdUsers = pkgs.writeText "upsd.users" (let
# This looks like INI, but it's not quite because the
# 'upsmon' option lacks a '='. See: man upsd.users
userConfig = name: user: concatStringsSep "\n " (concatLists [
[
"[${name}]"
"password = \"@upsdusers_password_${name}@\""
]
(optional (user.upsmon != null) "upsmon ${user.upsmon}")
(forEach user.actions (action: "actions = ${action}"))
(forEach user.instcmds (instcmd: "instcmds = ${instcmd}"))
]);
in concatStringsSep "\n\n" (mapAttrsToList userConfig cfg.users));
upsOptions = {name, config, ...}:
{
@ -93,6 +169,213 @@ let
};
};
listenOptions = {
options = {
address = mkOption {
type = types.str;
description = lib.mdDoc ''
Address of the interface for `upsd` to listen on.
See `man upsd.conf` for details.
'';
};
port = mkOption {
type = types.port;
default = defaultPort;
description = lib.mdDoc ''
TCP port for `upsd` to listen on.
See `man upsd.conf` for details.
'';
};
};
};
upsdOptions = {
options = {
enable = mkOption {
type = types.bool;
defaultText = literalMD "`true` if `mode` is one of `standalone`, `netserver`";
description = mdDoc "Whether to enable `upsd`.";
};
listen = mkOption {
type = with types; listOf (submodule listenOptions);
default = [];
example = [
{
address = "192.168.50.1";
}
{
address = "::1";
port = 5923;
}
];
description = lib.mdDoc ''
Address of the interface for `upsd` to listen on.
See `man upsd` for details`.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = lib.mdDoc ''
Additional lines to add to `upsd.conf`.
'';
};
};
config = {
enable = mkDefault (elem cfg.mode [ "standalone" "netserver" ]);
};
};
monitorOptions = { name, config, ... }: {
options = {
system = mkOption {
type = types.str;
default = name;
description = lib.mdDoc ''
Identifier of the UPS to monitor, in this form: `<upsname>[@<hostname>[:<port>]]`
See `upsmon.conf` for details.
'';
};
powerValue = mkOption {
type = types.int;
default = 1;
description = lib.mdDoc ''
Number of power supplies that the UPS feeds on this system.
See `upsmon.conf` for details.
'';
};
user = mkOption {
type = types.str;
description = lib.mdDoc ''
Username from `upsd.users` for accessing this UPS.
See `upsmon.conf` for details.
'';
};
passwordFile = mkOption {
type = types.str;
defaultText = literalMD "power.ups.users.\${user}.passwordFile";
description = lib.mdDoc ''
The full path to a file containing the password from
`upsd.users` for accessing this UPS. The password file
is read on service start.
See `upsmon.conf` for details.
'';
};
type = mkOption {
type = types.str;
default = "master";
description = lib.mdDoc ''
The relationship with `upsd`.
See `upsmon.conf` for details.
'';
};
};
config = {
passwordFile = mkDefault cfg.users.${config.user}.passwordFile;
};
};
upsmonOptions = {
options = {
enable = mkOption {
type = types.bool;
defaultText = literalMD "`true` if `mode` is one of `standalone`, `netserver`, `netclient`";
description = mdDoc "Whether to enable `upsmon`.";
};
monitor = mkOption {
type = with types; attrsOf (submodule monitorOptions);
default = {};
description = lib.mdDoc ''
Set of UPS to monitor. See `man upsmon.conf` for details.
'';
};
settings = mkOption {
type = nutFormat.type;
default = {};
defaultText = literalMD ''
{
MINSUPPLIES = 1;
RUN_AS_USER = "root";
NOTIFYCMD = "''${pkgs.nut}/bin/upssched";
SHUTDOWNCMD = "''${pkgs.systemd}/bin/shutdown now";
}
'';
description = mdDoc "Additional settings to add to `upsmon.conf`.";
example = literalMD ''
{
MINSUPPLIES = 2;
NOTIFYFLAG = [
[ "ONLINE" "SYSLOG+EXEC" ]
[ "ONBATT" "SYSLOG+EXEC" ]
];
}
'';
};
};
config = {
enable = mkDefault (elem cfg.mode [ "standalone" "netserver" "netclient" ]);
settings = {
RUN_AS_USER = "root"; # TODO: replace 'root' by another username.
MINSUPPLIES = mkDefault 1;
NOTIFYCMD = mkDefault "${pkgs.nut}/bin/upssched";
SHUTDOWNCMD = mkDefault "${pkgs.systemd}/bin/shutdown now";
MONITOR = flip mapAttrsToList cfg.upsmon.monitor (name: monitor: with monitor; [ system powerValue user "\"@upsmon_password_${name}@\"" type ]);
};
};
};
userOptions = {
options = {
passwordFile = mkOption {
type = types.str;
description = lib.mdDoc ''
The full path to a file that contains the user's (clear text)
password. The password file is read on service start.
'';
};
actions = mkOption {
type = with types; listOf str;
default = [];
description = lib.mdDoc ''
Allow the user to do certain things with upsd.
See `man upsd.users` for details.
'';
};
instcmds = mkOption {
type = with types; listOf str;
default = [];
description = lib.mdDoc ''
Let the user initiate specific instant commands. Use "ALL" to grant all commands automatically. For the full list of what your UPS supports, use "upscmd -l".
See `man upsd.users` for details.
'';
};
upsmon = mkOption {
type = with types; nullOr str;
default = null;
description = lib.mdDoc ''
Add the necessary actions for a upsmon process to work.
See `man upsd.users` for details.
'';
};
};
};
in
@ -141,6 +424,13 @@ in
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc ''
Open ports in the firewall for `upsd`.
'';
};
maxStartDelay = mkOption {
default = 45;
@ -154,6 +444,22 @@ in
'';
};
upsmon = mkOption {
default = {};
description = lib.mdDoc ''
Options for the `upsmon.conf` configuration file.
'';
type = types.submodule upsmonOptions;
};
upsd = mkOption {
default = {};
description = lib.mdDoc ''
Options for the `upsd.conf` configuration file.
'';
type = types.submodule upsdOptions;
};
ups = mkOption {
default = {};
# see nut/etc/ups.conf.sample
@ -165,45 +471,84 @@ in
type = with types; attrsOf (submodule upsOptions);
};
users = mkOption {
default = {};
description = lib.mdDoc ''
Users that can access upsd. See `man upsd.users`.
'';
type = with types; attrsOf (submodule userOptions);
};
};
};
config = mkIf cfg.enable {
assertions = [
(let
totalPowerValue = foldl' add 0 (map (monitor: monitor.powerValue) (attrValues cfg.upsmon.monitor));
minSupplies = cfg.upsmon.settings.MINSUPPLIES;
in mkIf cfg.upsmon.enable {
assertion = totalPowerValue >= minSupplies;
message = ''
`power.ups.upsmon`: Total configured power value (${toString totalPowerValue}) must be at least MINSUPPLIES (${toString minSupplies}).
'';
})
];
environment.systemPackages = [ pkgs.nut ];
systemd.services.upsmon = {
enable = mkDefault (elem cfg.mode [ "standalone" "netserver" "netclient" ]);
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts =
if cfg.upsd.listen == []
then [ defaultPort ]
else unique (forEach cfg.upsd.listen (listen: listen.port));
};
systemd.services.upsmon = let
secrets = mapAttrsToList (name: monitor: "upsmon_password_${name}") cfg.upsmon.monitor;
createUpsmonConf = installSecrets upsmonConf "/run/nut/upsmon.conf" secrets;
in {
enable = cfg.upsmon.enable;
description = "Uninterruptible Power Supplies (Monitor)";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "forking";
ExecStartPre = "${createUpsmonConf}";
ExecStart = "${pkgs.nut}/sbin/upsmon";
ExecReload = "${pkgs.nut}/sbin/upsmon -c reload";
LoadCredential = mapAttrsToList (name: monitor: "upsmon_password_${name}:${monitor.passwordFile}") cfg.upsmon.monitor;
};
environment.NUT_CONFPATH = "/etc/nut";
environment.NUT_STATEPATH = "/var/lib/nut";
};
systemd.services.upsd = {
enable = mkDefault (elem cfg.mode [ "standalone" "netserver" ]);
systemd.services.upsd = let
secrets = mapAttrsToList (name: user: "upsdusers_password_${name}") cfg.users;
createUpsdUsers = installSecrets upsdUsers "/run/nut/upsd.users" secrets;
in {
enable = cfg.upsd.enable;
description = "Uninterruptible Power Supplies (Daemon)";
after = [ "network.target" "upsmon.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "forking";
serviceConfig = {
Type = "forking";
ExecStartPre = "${createUpsdUsers}";
# TODO: replace 'root' by another username.
ExecStart = "${pkgs.nut}/sbin/upsd -u root";
ExecReload = "${pkgs.nut}/sbin/upsd -c reload";
LoadCredential = mapAttrsToList (name: user: "upsdusers_password_${name}:${user.passwordFile}") cfg.users;
};
environment.NUT_CONFPATH = "/etc/nut";
environment.NUT_STATEPATH = "/var/lib/nut";
restartTriggers = [
config.environment.etc."nut/upsd.conf".source
];
};
systemd.services.upsdrv = {
enable = mkDefault (elem cfg.mode [ "standalone" "netserver" ]);
enable = cfg.upsd.enable;
description = "Uninterruptible Power Supplies (Register all UPS)";
after = [ "upsd.service" ];
wantedBy = [ "multi-user.target" ];
@ -228,14 +573,14 @@ in
${concatStringsSep "\n\n" (forEach (attrValues cfg.ups) (ups: ups.summary))}
'';
"nut/upsd.conf".source = pkgs.writeText "upsd.conf"
''
${concatStringsSep "\n" (forEach cfg.upsd.listen (listen: "LISTEN ${listen.address} ${toString listen.port}"))}
${cfg.upsd.extraConfig}
'';
"nut/upssched.conf".source = cfg.schedulerRules;
# These file are containing private information and thus should not
# be stored inside the Nix store.
/*
"nut/upsd.conf".source = "";
"nut/upsd.users".source = "";
"nut/upsmon.conf".source = "";
*/
"nut/upsd.users".source = "/run/nut/upsd.users";
"nut/upsmon.conf".source = "/run/nut/upsmon.conf";
};
power.ups.schedulerRules = mkDefault "${pkgs.nut}/etc/upssched.conf.sample";