A NixOS Module for Timed Jobs
I often want to run some script on my machine on a schedule. There are a few different tools commonly used to achieve this, cron being a popular choice. With systemd, this is conventionally achieved using a timer and service unit. The units do however require a wee-bit of boilerplate. With NixOS, we can define a module to avoid that boilerplate.
Without the module, the configuration for an example task looks like this:
systemd.services.chaos = {
serviceConfig.Type = "oneshot";
serviceConfig.User = "root";
script = ''
kill -9 $(ps -ef | awk '{print $2}' | shuf -n 1)
'';
path = [ ];
};
systemd.services.timers.chaos = {
wantedBy = [ "timers.target" ];
partOf = [ "chaos.service" ];
timerConfig.OnCalendar = "*-*-* *:00:00";
};
At a minimum, I want to specify only two things:
- the script to run
- the time specification of when to run the script
And let’s add options for these commonly needed configurations:
- set the user the script runs as
- packages to make available in the script
The specification of a nixos module for tasks with the above configuration options is as follows:
let
taskSpec = types.submodule {
options = {
script = mkOption {
type = types.lines;
description = "The script to execute.";
};
onCalendar = mkOption {
type = types.str;
description = "The systemd time specification.";
};
user = mkOption {
type = types.str;
description = "The user to run the service as.";
default = "root";
};
path = mkOption {
type = types.listOf types.package;
default = [ ];
description = "List of pkgs in the path for the service script.";
};
};
};
in {
options.tasks = mkOption {
type = types.attrsOf taskSpec;
description = { };
default = { };
};
}
To specify the systemd service and timer units, we’ll use the following utility function:
mkService = name:
{ script, path, user, ... }: {
serviceConfig.Type = "oneshot";
serviceConfig.User = user;
script = script;
path = path;
};
mkTimer = name:
{ onCalendar, ... }: {
wantedBy = [ "timers.target" ];
partOf = [ "${name}.service" ];
timerConfig.OnCalendar = onCalendar;
};
With these utility functions, we can map over the configured tasks to set timer and service units:
let
cfg = config.tasks;
in {
config.systemd.services = let
units = mapAttrs' (name: info: {
name = "${name}";
value = (mkService name info);
}) cfg;
in units;
config.systemd.timers = let
timers = mapAttrs' (name: info: {
name = "${name}";
value = (mkTimer name info);
}) cfg;
in timers;
}
The full module looks like this:
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.tasks;
taskSpec = types.submodule {
options = {
script = mkOption {
type = types.lines;
description = "The script to execute.";
};
onCalendar = mkOption {
type = types.str;
description = "The systemd time specification.";
};
user = mkOption {
type = types.str;
description = "The user to run the service as.";
default = "root";
};
path = mkOption {
type = types.listOf types.package;
default = [ ];
description = "List of pkgs in the path for the service script.";
};
};
};
mkService = name:
{ script, path, user, ... }: {
serviceConfig.Type = "oneshot";
serviceConfig.User = user;
script = script;
path = path;
};
mkTimer = name:
{ onCalendar, ... }: {
wantedBy = [ "timers.target" ];
partOf = [ "${name}.service" ];
timerConfig.OnCalendar = onCalendar;
};
in {
options.tasks = mkOption {
type = types.attrsOf taskSpec;
description = { };
default = { };
};
config.systemd.services = let
units = mapAttrs' (name: info: {
name = "${name}";
value = (mkService name info);
}) cfg;
in units;
config.systemd.timers = let
timers = mapAttrs' (name: info: {
name = "${name}";
value = (mkTimer name info);
}) cfg;
in timers;
}
Now we can specify a task to run every hour as follows:
tasks.chaos = {
onCalendar = "*-*-* *:00:00";
script = ''
kill -9 $(ps -ef | awk '{print $2}' | shuf -n 1)
'';
};
If we want to further configure the systemd units, beyond what is configured by our module, we can do so by configuring the unit directly.
systemd.timers.chaos.timerConfig.Persistent = true;
systemd.services.chaos.serviceConfig.Nice = -20;