1{ config, lib, pkgs, ... }:
2
3with lib;
4with import ../boot/systemd-unit-options.nix { inherit config lib; };
5
6let
7
8 userExists = u:
9 (u == "") || any (uu: uu.name == u) (attrValues config.users.extraUsers);
10
11 groupExists = g:
12 (g == "") || any (gg: gg.name == g) (attrValues config.users.extraGroups);
13
14 makeJobScript = name: content: "${pkgs.writeScriptBin name content}/bin/${name}";
15
16 # From a job description, generate an systemd unit file.
17 makeUnit = job:
18
19 let
20 hasMain = job.script != "" || job.exec != "";
21
22 env = job.environment;
23
24 preStartScript = makeJobScript "${job.name}-pre-start"
25 ''
26 #! ${pkgs.stdenv.shell} -e
27 ${job.preStart}
28 '';
29
30 startScript = makeJobScript "${job.name}-start"
31 ''
32 #! ${pkgs.stdenv.shell} -e
33 ${if job.script != "" then job.script else ''
34 exec ${job.exec}
35 ''}
36 '';
37
38 postStartScript = makeJobScript "${job.name}-post-start"
39 ''
40 #! ${pkgs.stdenv.shell} -e
41 ${job.postStart}
42 '';
43
44 preStopScript = makeJobScript "${job.name}-pre-stop"
45 ''
46 #! ${pkgs.stdenv.shell} -e
47 ${job.preStop}
48 '';
49
50 postStopScript = makeJobScript "${job.name}-post-stop"
51 ''
52 #! ${pkgs.stdenv.shell} -e
53 ${job.postStop}
54 '';
55 in {
56
57 inherit (job) description requires before partOf environment path restartIfChanged unitConfig;
58
59 after =
60 (if job.startOn == "stopped udevtrigger" then [ "systemd-udev-settle.service" ] else
61 if job.startOn == "started udev" then [ "systemd-udev.service" ] else
62 if job.startOn == "started network-interfaces" then [ "network-interfaces.target" ] else
63 if job.startOn == "started networking" then [ "network.target" ] else
64 if job.startOn == "ip-up" then [] else
65 if job.startOn == "" || job.startOn == "startup" then [] else
66 builtins.trace "Warning: job ‘${job.name}’ has unknown startOn value ‘${job.startOn}’." []
67 ) ++ job.after;
68
69 wants =
70 (if job.startOn == "stopped udevtrigger" then [ "systemd-udev-settle.service" ] else []
71 ) ++ job.wants;
72
73 wantedBy =
74 (if job.startOn == "" then [] else
75 if job.startOn == "ip-up" then [ "ip-up.target" ] else
76 [ "multi-user.target" ]) ++ job.wantedBy;
77
78 serviceConfig =
79 job.serviceConfig
80 // optionalAttrs (job.preStart != "" && (job.script != "" || job.exec != ""))
81 { ExecStartPre = preStartScript; }
82 // optionalAttrs (job.preStart != "" && job.script == "" && job.exec == "")
83 { ExecStart = preStartScript; }
84 // optionalAttrs (job.script != "" || job.exec != "")
85 { ExecStart = startScript; }
86 // optionalAttrs (job.postStart != "")
87 { ExecStartPost = postStartScript; }
88 // optionalAttrs (job.preStop != "")
89 { ExecStop = preStopScript; }
90 // optionalAttrs (job.postStop != "")
91 { ExecStopPost = postStopScript; }
92 // (if job.script == "" && job.exec == "" then { Type = "oneshot"; RemainAfterExit = true; } else
93 if job.daemonType == "fork" || job.daemonType == "daemon" then { Type = "forking"; GuessMainPID = true; } else
94 if job.daemonType == "none" then { } else
95 throw "invalid daemon type `${job.daemonType}'")
96 // optionalAttrs (!job.task && !(job.script == "" && job.exec == "") && job.respawn)
97 { Restart = "always"; }
98 // optionalAttrs job.task
99 { Type = "oneshot"; RemainAfterExit = false; };
100 };
101
102
103 jobOptions = serviceOptions // {
104
105 name = mkOption {
106 # !!! The type should ensure that this could be a filename.
107 type = types.str;
108 example = "sshd";
109 description = ''
110 Name of the job, mapped to the systemd unit
111 <literal><replaceable>name</replaceable>.service</literal>.
112 '';
113 };
114
115 startOn = mkOption {
116 #type = types.str;
117 default = "";
118 description = ''
119 The Upstart event that triggers this job to be started. Some
120 are mapped to systemd dependencies; otherwise you will get a
121 warning. If empty, the job will not start automatically.
122 '';
123 };
124
125 stopOn = mkOption {
126 type = types.str;
127 default = "starting shutdown";
128 description = ''
129 Ignored; this was the Upstart event that triggers this job to be stopped.
130 '';
131 };
132
133 postStart = mkOption {
134 type = types.lines;
135 default = "";
136 description = ''
137 Shell commands executed after the job is started (i.e. after
138 the job's main process is started), but before the job is
139 considered “running”.
140 '';
141 };
142
143 preStop = mkOption {
144 type = types.lines;
145 default = "";
146 description = ''
147 Shell commands executed before the job is stopped
148 (i.e. before systemd kills the job's main process). This can
149 be used to cleanly shut down a daemon.
150 '';
151 };
152
153 postStop = mkOption {
154 type = types.lines;
155 default = "";
156 description = ''
157 Shell commands executed after the job has stopped
158 (i.e. after the job's main process has terminated).
159 '';
160 };
161
162 exec = mkOption {
163 type = types.str;
164 default = "";
165 description = ''
166 Command to start the job's main process. If empty, the
167 job has no main process, but can still have pre/post-start
168 and pre/post-stop scripts, and is considered “running”
169 until it is stopped.
170 '';
171 };
172
173 respawn = mkOption {
174 type = types.bool;
175 default = true;
176 description = ''
177 Whether to restart the job automatically if its process
178 ends unexpectedly.
179 '';
180 };
181
182 task = mkOption {
183 type = types.bool;
184 default = false;
185 description = ''
186 Whether this job is a task rather than a service. Tasks
187 are executed only once, while services are restarted when
188 they exit.
189 '';
190 };
191
192 daemonType = mkOption {
193 type = types.str;
194 default = "none";
195 description = ''
196 Determines how systemd detects when a daemon should be
197 considered “running”. The value <literal>none</literal> means
198 that the daemon is considered ready immediately. The value
199 <literal>fork</literal> means that the daemon will fork once.
200 The value <literal>daemon</literal> means that the daemon will
201 fork twice. The value <literal>stop</literal> means that the
202 daemon will raise the SIGSTOP signal to indicate readiness.
203 '';
204 };
205
206 setuid = mkOption {
207 type = types.addCheck types.str userExists;
208 default = "";
209 description = ''
210 Run the daemon as a different user.
211 '';
212 };
213
214 setgid = mkOption {
215 type = types.addCheck types.str groupExists;
216 default = "";
217 description = ''
218 Run the daemon as a different group.
219 '';
220 };
221
222 path = mkOption {
223 default = [];
224 description = ''
225 Packages added to the job's <envar>PATH</envar> environment variable.
226 Both the <filename>bin</filename> and <filename>sbin</filename>
227 subdirectories of each package are added.
228 '';
229 };
230
231 };
232
233
234 upstartJob = { name, config, ... }: {
235
236 options = {
237
238 unit = mkOption {
239 default = makeUnit config;
240 description = "Generated definition of the systemd unit corresponding to this job.";
241 };
242
243 };
244
245 config = {
246
247 # The default name is the name extracted from the attribute path.
248 name = mkDefault name;
249
250 };
251
252 };
253
254in
255
256{
257
258 ###### interface
259
260 options = {
261
262 jobs = mkOption {
263 default = {};
264 description = ''
265 This option is a legacy method to define system services,
266 dating from the era where NixOS used Upstart instead of
267 systemd. You should use <option>systemd.services</option>
268 instead. Services defined using <option>jobs</option> are
269 mapped automatically to <option>systemd.services</option>, but
270 may not work perfectly; in particular, most
271 <option>startOn</option> conditions are not supported.
272 '';
273 type = types.loaOf types.optionSet;
274 options = [ jobOptions upstartJob ];
275 };
276
277 };
278
279
280 ###### implementation
281
282 config = {
283
284 systemd.services =
285 flip mapAttrs' config.jobs (name: job:
286 nameValuePair job.name job.unit);
287
288 };
289
290}