1{ config, pkgs, lib, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.hydra;
8
9 baseDir = "/var/lib/hydra";
10
11 hydraConf = pkgs.writeScript "hydra.conf" cfg.extraConfig;
12
13 hydraEnv =
14 { HYDRA_DBI = cfg.dbi;
15 HYDRA_CONFIG = "${baseDir}/hydra.conf";
16 HYDRA_DATA = "${baseDir}";
17 };
18
19 env =
20 { NIX_REMOTE = "daemon";
21 SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt"; # Remove in 16.03
22 PGPASSFILE = "${baseDir}/pgpass";
23 NIX_REMOTE_SYSTEMS = concatStringsSep ":" cfg.buildMachinesFiles;
24 } // optionalAttrs (cfg.smtpHost != null) {
25 EMAIL_SENDER_TRANSPORT = "SMTP";
26 EMAIL_SENDER_TRANSPORT_host = cfg.smtpHost;
27 } // hydraEnv // cfg.extraEnv;
28
29 serverEnv = env //
30 { HYDRA_TRACKER = cfg.tracker;
31 XDG_CACHE_HOME = "${baseDir}/www/.cache";
32 COLUMNS = "80";
33 PGPASSFILE = "${baseDir}/pgpass-www"; # grrr
34 } // (optionalAttrs cfg.debugServer { DBIC_TRACE = "1"; });
35
36 localDB = "dbi:Pg:dbname=hydra;user=hydra;";
37
38 haveLocalDB = cfg.dbi == localDB;
39
40in
41
42{
43 ###### interface
44 options = {
45
46 services.hydra = rec {
47
48 enable = mkOption {
49 type = types.bool;
50 default = false;
51 description = ''
52 Whether to run Hydra services.
53 '';
54 };
55
56 dbi = mkOption {
57 type = types.str;
58 default = localDB;
59 example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;";
60 description = ''
61 The DBI string for Hydra database connection.
62 '';
63 };
64
65 package = mkOption {
66 type = types.path;
67 default = pkgs.hydra;
68 defaultText = "pkgs.hydra";
69 description = "The Hydra package.";
70 };
71
72 hydraURL = mkOption {
73 type = types.str;
74 description = ''
75 The base URL for the Hydra webserver instance. Used for links in emails.
76 '';
77 };
78
79 listenHost = mkOption {
80 type = types.str;
81 default = "*";
82 example = "localhost";
83 description = ''
84 The hostname or address to listen on or <literal>*</literal> to listen
85 on all interfaces.
86 '';
87 };
88
89 port = mkOption {
90 type = types.int;
91 default = 3000;
92 description = ''
93 TCP port the web server should listen to.
94 '';
95 };
96
97 minimumDiskFree = mkOption {
98 type = types.int;
99 default = 0;
100 description = ''
101 Threshold of minimum disk space (GiB) to determine if the queue runner should run or not.
102 '';
103 };
104
105 minimumDiskFreeEvaluator = mkOption {
106 type = types.int;
107 default = 0;
108 description = ''
109 Threshold of minimum disk space (GiB) to determine if the evaluator should run or not.
110 '';
111 };
112
113 notificationSender = mkOption {
114 type = types.str;
115 description = ''
116 Sender email address used for email notifications.
117 '';
118 };
119
120 smtpHost = mkOption {
121 type = types.nullOr types.str;
122 default = null;
123 example = ["localhost"];
124 description = ''
125 Hostname of the SMTP server to use to send email.
126 '';
127 };
128
129 tracker = mkOption {
130 type = types.str;
131 default = "";
132 description = ''
133 Piece of HTML that is included on all pages.
134 '';
135 };
136
137 logo = mkOption {
138 type = types.nullOr types.path;
139 default = null;
140 description = ''
141 Path to a file containing the logo of your Hydra instance.
142 '';
143 };
144
145 debugServer = mkOption {
146 type = types.bool;
147 default = false;
148 description = "Whether to run the server in debug mode.";
149 };
150
151 extraConfig = mkOption {
152 type = types.lines;
153 description = "Extra lines for the Hydra configuration.";
154 };
155
156 extraEnv = mkOption {
157 type = types.attrsOf types.str;
158 default = {};
159 description = "Extra environment variables for Hydra.";
160 };
161
162 gcRootsDir = mkOption {
163 type = types.path;
164 default = "/nix/var/nix/gcroots/hydra";
165 description = "Directory that holds Hydra garbage collector roots.";
166 };
167
168 buildMachinesFiles = mkOption {
169 type = types.listOf types.path;
170 default = [ "/etc/nix/machines" ];
171 example = [ "/etc/nix/machines" "/var/lib/hydra/provisioner/machines" ];
172 description = "List of files containing build machines.";
173 };
174
175 useSubstitutes = mkOption {
176 type = types.bool;
177 default = false;
178 description = ''
179 Whether to use binary caches for downloading store paths. Note that
180 binary substitutions trigger (a potentially large number of) additional
181 HTTP requests that slow down the queue monitor thread significantly.
182 Also, this Hydra instance will serve those downloaded store paths to
183 its users with its own signature attached as if it had built them
184 itself, so don't enable this feature unless your active binary caches
185 are absolute trustworthy.
186 '';
187 };
188 };
189
190 };
191
192
193 ###### implementation
194
195 config = mkIf cfg.enable {
196
197 users.groups.hydra = {
198 gid = config.ids.gids.hydra;
199 };
200
201 users.users.hydra =
202 { description = "Hydra";
203 group = "hydra";
204 createHome = true;
205 home = baseDir;
206 useDefaultShell = true;
207 uid = config.ids.uids.hydra;
208 };
209
210 users.users.hydra-queue-runner =
211 { description = "Hydra queue runner";
212 group = "hydra";
213 useDefaultShell = true;
214 home = "${baseDir}/queue-runner"; # really only to keep SSH happy
215 uid = config.ids.uids.hydra-queue-runner;
216 };
217
218 users.users.hydra-www =
219 { description = "Hydra web server";
220 group = "hydra";
221 useDefaultShell = true;
222 uid = config.ids.uids.hydra-www;
223 };
224
225 nix.trustedUsers = [ "hydra-queue-runner" ];
226
227 services.hydra.extraConfig =
228 ''
229 using_frontend_proxy = 1
230 base_uri = ${cfg.hydraURL}
231 notification_sender = ${cfg.notificationSender}
232 max_servers = 25
233 ${optionalString (cfg.logo != null) ''
234 hydra_logo = ${cfg.logo}
235 ''}
236 gc_roots_dir = ${cfg.gcRootsDir}
237 use-substitutes = ${if cfg.useSubstitutes then "1" else "0"}
238 '';
239
240 environment.systemPackages = [ cfg.package ];
241
242 environment.variables = hydraEnv;
243
244 nix.extraOptions = ''
245 gc-keep-outputs = true
246 gc-keep-derivations = true
247
248 # The default (`true') slows Nix down a lot since the build farm
249 # has so many GC roots.
250 gc-check-reachability = false
251 '';
252
253 systemd.services.hydra-init =
254 { wantedBy = [ "multi-user.target" ];
255 requires = optional haveLocalDB "postgresql.service";
256 after = optional haveLocalDB "postgresql.service";
257 environment = env;
258 preStart = ''
259 mkdir -p ${baseDir}
260 chown hydra.hydra ${baseDir}
261 chmod 0750 ${baseDir}
262
263 ln -sf ${hydraConf} ${baseDir}/hydra.conf
264
265 mkdir -m 0700 -p ${baseDir}/www
266 chown hydra-www.hydra ${baseDir}/www
267
268 mkdir -m 0700 -p ${baseDir}/queue-runner
269 mkdir -m 0750 -p ${baseDir}/build-logs
270 chown hydra-queue-runner.hydra ${baseDir}/queue-runner ${baseDir}/build-logs
271
272 ${optionalString haveLocalDB ''
273 if ! [ -e ${baseDir}/.db-created ]; then
274 ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createuser hydra
275 ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createdb -O hydra hydra
276 touch ${baseDir}/.db-created
277 fi
278 ''}
279
280 if [ ! -e ${cfg.gcRootsDir} ]; then
281
282 # Move legacy roots directory.
283 if [ -e /nix/var/nix/gcroots/per-user/hydra/hydra-roots ]; then
284 mv /nix/var/nix/gcroots/per-user/hydra/hydra-roots ${cfg.gcRootsDir}
285 fi
286
287 mkdir -p ${cfg.gcRootsDir}
288 fi
289
290 # Move legacy hydra-www roots.
291 if [ -e /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots ]; then
292 find /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots/ -type f \
293 | xargs -r mv -f -t ${cfg.gcRootsDir}/
294 rmdir /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots
295 fi
296
297 chown hydra.hydra ${cfg.gcRootsDir}
298 chmod 2775 ${cfg.gcRootsDir}
299 '';
300 serviceConfig.ExecStart = "${cfg.package}/bin/hydra-init";
301 serviceConfig.PermissionsStartOnly = true;
302 serviceConfig.User = "hydra";
303 serviceConfig.Type = "oneshot";
304 serviceConfig.RemainAfterExit = true;
305 };
306
307 systemd.services.hydra-server =
308 { wantedBy = [ "multi-user.target" ];
309 requires = [ "hydra-init.service" ];
310 after = [ "hydra-init.service" ];
311 environment = serverEnv;
312 restartTriggers = [ hydraConf ];
313 serviceConfig =
314 { ExecStart =
315 "@${cfg.package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' "
316 + "-p ${toString cfg.port} --max_spare_servers 5 --max_servers 25 "
317 + "--max_requests 100 ${optionalString cfg.debugServer "-d"}";
318 User = "hydra-www";
319 PermissionsStartOnly = true;
320 Restart = "always";
321 };
322 };
323
324 systemd.services.hydra-queue-runner =
325 { wantedBy = [ "multi-user.target" ];
326 requires = [ "hydra-init.service" ];
327 after = [ "hydra-init.service" "network.target" ];
328 path = [ cfg.package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ];
329 restartTriggers = [ hydraConf ];
330 environment = env // {
331 PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
332 IN_SYSTEMD = "1"; # to get log severity levels
333 };
334 serviceConfig =
335 { ExecStart = "@${cfg.package}/bin/hydra-queue-runner hydra-queue-runner -v --option build-use-substitutes ${boolToString cfg.useSubstitutes}";
336 ExecStopPost = "${cfg.package}/bin/hydra-queue-runner --unlock";
337 User = "hydra-queue-runner";
338 Restart = "always";
339
340 # Ensure we can get core dumps.
341 LimitCORE = "infinity";
342 WorkingDirectory = "${baseDir}/queue-runner";
343 };
344 };
345
346 systemd.services.hydra-evaluator =
347 { wantedBy = [ "multi-user.target" ];
348 requires = [ "hydra-init.service" ];
349 after = [ "hydra-init.service" "network.target" ];
350 path = with pkgs; [ cfg.package nettools jq ];
351 restartTriggers = [ hydraConf ];
352 environment = env;
353 serviceConfig =
354 { ExecStart = "@${cfg.package}/bin/hydra-evaluator hydra-evaluator";
355 User = "hydra";
356 Restart = "always";
357 WorkingDirectory = baseDir;
358 };
359 };
360
361 systemd.services.hydra-update-gc-roots =
362 { requires = [ "hydra-init.service" ];
363 after = [ "hydra-init.service" ];
364 environment = env;
365 serviceConfig =
366 { ExecStart = "@${cfg.package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
367 User = "hydra";
368 };
369 startAt = "2,14:15";
370 };
371
372 systemd.services.hydra-send-stats =
373 { wantedBy = [ "multi-user.target" ];
374 after = [ "hydra-init.service" ];
375 environment = env;
376 serviceConfig =
377 { ExecStart = "@${cfg.package}/bin/hydra-send-stats hydra-send-stats";
378 User = "hydra";
379 };
380 };
381
382 # If there is less than a certain amount of free disk space, stop
383 # the queue/evaluator to prevent builds from failing or aborting.
384 systemd.services.hydra-check-space =
385 { script =
386 ''
387 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFree} * 1024**3)) ]; then
388 echo "stopping Hydra queue runner due to lack of free space..."
389 systemctl stop hydra-queue-runner
390 fi
391 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFreeEvaluator} * 1024**3)) ]; then
392 echo "stopping Hydra evaluator due to lack of free space..."
393 systemctl stop hydra-evaluator
394 fi
395 '';
396 startAt = "*:0/5";
397 };
398
399 # Periodically compress build logs. The queue runner compresses
400 # logs automatically after a step finishes, but this doesn't work
401 # if the queue runner is stopped prematurely.
402 systemd.services.hydra-compress-logs =
403 { path = [ pkgs.bzip2 ];
404 script =
405 ''
406 find /var/lib/hydra/build-logs -type f -name "*.drv" -mtime +3 -size +0c | xargs -r bzip2 -v -f
407 '';
408 startAt = "Sun 01:45";
409 };
410
411 services.postgresql.enable = mkIf haveLocalDB true;
412
413 services.postgresql.identMap = optionalString haveLocalDB
414 ''
415 hydra-users hydra hydra
416 hydra-users hydra-queue-runner hydra
417 hydra-users hydra-www hydra
418 hydra-users root hydra
419 '';
420
421 services.postgresql.authentication = optionalString haveLocalDB
422 ''
423 local hydra all ident map=hydra-users
424 '';
425
426 };
427
428}