{ config, pkgs, ... }:
with pkgs.lib;
let
upstart = pkgs.upstart;
userExists = u:
(u == "") || any (uu: uu.name == u) (attrValues config.users.extraUsers);
groupExists = g:
(g == "") || any (gg: gg.name == g) (attrValues config.users.extraGroups);
makeJobScript = name: content: "${pkgs.writeScriptBin name content}/bin/${name}";
# From a job description, generate an systemd unit file.
makeUnit = job:
let
hasMain = job.script != "" || job.exec != "";
env = config.system.upstartEnvironment // job.environment;
preStartScript = makeJobScript "${job.name}-pre-start.sh"
''
#! ${pkgs.stdenv.shell} -e
${job.preStart}
'';
startScript = makeJobScript "${job.name}-start.sh"
''
#! ${pkgs.stdenv.shell} -e
${if job.script != "" then job.script else ''
exec ${job.exec}
''}
'';
postStartScript = makeJobScript "${job.name}-post-start.sh"
''
#! ${pkgs.stdenv.shell} -e
${job.postStart}
'';
preStopScript = makeJobScript "${job.name}-pre-stop.sh"
''
#! ${pkgs.stdenv.shell} -e
${job.preStop}
'';
postStopScript = makeJobScript "${job.name}-post-stop.sh"
''
#! ${pkgs.stdenv.shell} -e
${job.postStop}
'';
in {
inherit (job) description path environment;
after =
if job.startOn == "stopped udevtrigger" then [ "systemd-udev-settle.service" ] else
if job.startOn == "started udev" then [ "systemd-udev.service" ] else
[];
wantedBy = if job.startOn == "" then [ ] else [ "multi-user.target" ];
serviceConfig =
''
${optionalString (job.preStart != "" && (job.script != "" || job.exec != "")) ''
ExecStartPre=${preStartScript}
''}
${optionalString (job.preStart != "" && job.script == "" && job.exec == "") ''
ExecStart=${preStartScript}
''}
${optionalString (job.script != "" || job.exec != "") ''
ExecStart=${startScript}
''}
${optionalString (job.postStart != "") ''
ExecStartPost=${postStartScript}
''}
${optionalString (job.preStop != "") ''
ExecStop=${preStopScript}
''}
${optionalString (job.postStop != "") ''
ExecStopPost=${postStopScript}
''}
${if job.script == "" && job.exec == "" then "Type=oneshot\nRemainAfterExit=true" else
if job.daemonType == "fork" then "Type=forking\nGuessMainPID=true" else
if job.daemonType == "none" then "" else
throw "invalid daemon type `${job.daemonType}'"}
${optionalString (!job.task && job.respawn) "Restart=always"}
'';
};
jobOptions = {
name = mkOption {
# !!! The type should ensure that this could be a filename.
type = types.string;
example = "sshd";
description = ''
Name of the Upstart job.
'';
};
description = mkOption {
type = types.string;
default = "";
description = ''
A short description of this job.
'';
};
startOn = mkOption {
# !!! Re-enable this once we're on Upstart >= 0.6.
#type = types.string;
default = "";
description = ''
The Upstart event that triggers this job to be started.
If empty, the job will not start automatically.
'';
};
stopOn = mkOption {
type = types.string;
default = "starting shutdown";
description = ''
The Upstart event that triggers this job to be stopped.
'';
};
preStart = mkOption {
type = types.string;
default = "";
description = ''
Shell commands executed before the job is started
(i.e. before the job's main process is started).
'';
};
postStart = mkOption {
type = types.string;
default = "";
description = ''
Shell commands executed after the job is started (i.e. after
the job's main process is started), but before the job is
considered “running”.
'';
};
preStop = mkOption {
type = types.string;
default = "";
description = ''
Shell commands executed before the job is stopped
(i.e. before Upstart kills the job's main process). This can
be used to cleanly shut down a daemon.
'';
};
postStop = mkOption {
type = types.string;
default = "";
description = ''
Shell commands executed after the job has stopped
(i.e. after the job's main process has terminated).
'';
};
exec = mkOption {
type = types.string;
default = "";
description = ''
Command to start the job's main process. If empty, the
job has no main process, but can still have pre/post-start
and pre/post-stop scripts, and is considered “running”
until it is stopped.
'';
};
script = mkOption {
type = types.string;
default = "";
description = ''
Shell commands executed as the job's main process. Can be
specified instead of the exec attribute.
'';
};
respawn = mkOption {
type = types.bool;
default = true;
description = ''
Whether to restart the job automatically if its process
ends unexpectedly.
'';
};
restartIfChanged = mkOption {
type = types.bool;
default = true;
description = ''
Whether the job should be restarted if it has changed after a
NixOS configuration switch.
'';
};
task = mkOption {
type = types.bool;
default = false;
description = ''
Whether this job is a task rather than a service. Tasks
are executed only once, while services are restarted when
they exit.
'';
};
environment = mkOption {
type = types.attrs;
default = {};
example = { PATH = "/foo/bar/bin"; LANG = "nl_NL.UTF-8"; };
description = ''
Environment variables passed to the job's processes.
'';
};
daemonType = mkOption {
type = types.string;
default = "none";
description = ''
Determines how Upstart detects when a daemon should be
considered “running”. The value none means
that the daemon is considered ready immediately. The value
fork means that the daemon will fork once.
The value daemon means that the daemon will
fork twice. The value stop means that the
daemon will raise the SIGSTOP signal to indicate readiness.
'';
};
setuid = mkOption {
type = types.string;
check = userExists;
default = "";
description = ''
Run the daemon as a different user.
'';
};
setgid = mkOption {
type = types.string;
check = groupExists;
default = "";
description = ''
Run the daemon as a different group.
'';
};
extraConfig = mkOption {
type = types.string;
default = "";
example = "limit nofile 4096 4096";
description = ''
Additional Upstart stanzas not otherwise supported.
'';
};
path = mkOption {
default = [];
description = ''
Packages added to the job's PATH environment variable.
Both the bin and sbin
subdirectories of each package are added.
'';
};
console = mkOption {
default = "";
example = "console";
description = ''
If set to output, job output is written to
the console. If it's owner, additionally
the job becomes owner of the console. It it's empty (the
default), output is written to
/var/log/upstart/jobname
'';
};
};
upstartJob = {name, config, ...}: {
options = {
unit = mkOption {
default = makeUnit config;
description = "Generated definition of the systemd unit corresponding to this job.";
};
};
config = {
# The default name is the name extracted from the attribute path.
name = mkDefaultValue name;
# Default path for Upstart jobs. Should be quite minimal.
path =
[ pkgs.coreutils
pkgs.findutils
pkgs.gnugrep
pkgs.gnused
upstart
];
};
};
in
{
###### interface
options = {
jobs = mkOption {
default = {};
description = ''
This option defines the system jobs started and managed by the
Upstart daemon.
'';
type = types.loaOf types.optionSet;
options = [ jobOptions upstartJob ];
};
system.upstartEnvironment = mkOption {
type = types.attrs;
default = {};
example = { TZ = "CET"; };
description = ''
Environment variables passed to all Upstart jobs.
'';
};
};
###### implementation
config = {
system.build.upstart = "/no-upstart";
boot.systemd.services =
flip mapAttrs' config.jobs (name: job:
nameValuePair "${job.name}.service" job.unit);
};
}