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
40 hydra-package =
41 let
42 makeWrapperArgs = concatStringsSep " " (mapAttrsToList (key: value: "--set \"${key}\" \"${value}\"") hydraEnv);
43 in pkgs.buildEnv rec {
44 name = "hydra-env";
45 nativeBuildInputs = [ pkgs.makeWrapper ];
46 paths = [ cfg.package ];
47
48 postBuild = ''
49 if [ -L "$out/bin" ]; then
50 unlink "$out/bin"
51 fi
52 mkdir -p "$out/bin"
53
54 for path in ${concatStringsSep " " paths}; do
55 if [ -d "$path/bin" ]; then
56 cd "$path/bin"
57 for prg in *; do
58 if [ -f "$prg" ]; then
59 rm -f "$out/bin/$prg"
60 if [ -x "$prg" ]; then
61 makeWrapper "$path/bin/$prg" "$out/bin/$prg" ${makeWrapperArgs}
62 fi
63 fi
64 done
65 fi
66 done
67 '';
68 };
69
70in
71
72{
73 ###### interface
74 options = {
75
76 services.hydra = {
77
78 enable = mkOption {
79 type = types.bool;
80 default = false;
81 description = lib.mdDoc ''
82 Whether to run Hydra services.
83 '';
84 };
85
86 dbi = mkOption {
87 type = types.str;
88 default = localDB;
89 example = "dbi:Pg:dbname=hydra;host=postgres.example.org;user=foo;";
90 description = lib.mdDoc ''
91 The DBI string for Hydra database connection.
92
93 NOTE: Attempts to set `application_name` will be overridden by
94 `hydra-TYPE` (where TYPE is e.g. `evaluator`, `queue-runner`,
95 etc.) in all hydra services to more easily distinguish where
96 queries are coming from.
97 '';
98 };
99
100 package = mkOption {
101 type = types.package;
102 default = pkgs.hydra_unstable;
103 defaultText = literalExpression "pkgs.hydra_unstable";
104 description = lib.mdDoc "The Hydra package.";
105 };
106
107 hydraURL = mkOption {
108 type = types.str;
109 description = lib.mdDoc ''
110 The base URL for the Hydra webserver instance. Used for links in emails.
111 '';
112 };
113
114 listenHost = mkOption {
115 type = types.str;
116 default = "*";
117 example = "localhost";
118 description = lib.mdDoc ''
119 The hostname or address to listen on or `*` to listen
120 on all interfaces.
121 '';
122 };
123
124 port = mkOption {
125 type = types.int;
126 default = 3000;
127 description = lib.mdDoc ''
128 TCP port the web server should listen to.
129 '';
130 };
131
132 minimumDiskFree = mkOption {
133 type = types.int;
134 default = 0;
135 description = lib.mdDoc ''
136 Threshold of minimum disk space (GiB) to determine if the queue runner should run or not.
137 '';
138 };
139
140 minimumDiskFreeEvaluator = mkOption {
141 type = types.int;
142 default = 0;
143 description = lib.mdDoc ''
144 Threshold of minimum disk space (GiB) to determine if the evaluator should run or not.
145 '';
146 };
147
148 notificationSender = mkOption {
149 type = types.str;
150 description = lib.mdDoc ''
151 Sender email address used for email notifications.
152 '';
153 };
154
155 smtpHost = mkOption {
156 type = types.nullOr types.str;
157 default = null;
158 example = "localhost";
159 description = lib.mdDoc ''
160 Hostname of the SMTP server to use to send email.
161 '';
162 };
163
164 tracker = mkOption {
165 type = types.str;
166 default = "";
167 description = lib.mdDoc ''
168 Piece of HTML that is included on all pages.
169 '';
170 };
171
172 logo = mkOption {
173 type = types.nullOr types.path;
174 default = null;
175 description = lib.mdDoc ''
176 Path to a file containing the logo of your Hydra instance.
177 '';
178 };
179
180 debugServer = mkOption {
181 type = types.bool;
182 default = false;
183 description = lib.mdDoc "Whether to run the server in debug mode.";
184 };
185
186 extraConfig = mkOption {
187 type = types.lines;
188 description = lib.mdDoc "Extra lines for the Hydra configuration.";
189 };
190
191 extraEnv = mkOption {
192 type = types.attrsOf types.str;
193 default = {};
194 description = lib.mdDoc "Extra environment variables for Hydra.";
195 };
196
197 gcRootsDir = mkOption {
198 type = types.path;
199 default = "/nix/var/nix/gcroots/hydra";
200 description = lib.mdDoc "Directory that holds Hydra garbage collector roots.";
201 };
202
203 buildMachinesFiles = mkOption {
204 type = types.listOf types.path;
205 default = optional (config.nix.buildMachines != []) "/etc/nix/machines";
206 defaultText = literalExpression ''optional (config.nix.buildMachines != []) "/etc/nix/machines"'';
207 example = [ "/etc/nix/machines" "/var/lib/hydra/provisioner/machines" ];
208 description = lib.mdDoc "List of files containing build machines.";
209 };
210
211 useSubstitutes = mkOption {
212 type = types.bool;
213 default = false;
214 description = lib.mdDoc ''
215 Whether to use binary caches for downloading store paths. Note that
216 binary substitutions trigger (a potentially large number of) additional
217 HTTP requests that slow down the queue monitor thread significantly.
218 Also, this Hydra instance will serve those downloaded store paths to
219 its users with its own signature attached as if it had built them
220 itself, so don't enable this feature unless your active binary caches
221 are absolute trustworthy.
222 '';
223 };
224 };
225
226 };
227
228
229 ###### implementation
230
231 config = mkIf cfg.enable {
232
233 users.groups.hydra = {
234 gid = config.ids.gids.hydra;
235 };
236
237 users.users.hydra =
238 { description = "Hydra";
239 group = "hydra";
240 # We don't enable `createHome` here because the creation of the home directory is handled by the hydra-init service below.
241 home = baseDir;
242 useDefaultShell = true;
243 uid = config.ids.uids.hydra;
244 };
245
246 users.users.hydra-queue-runner =
247 { description = "Hydra queue runner";
248 group = "hydra";
249 useDefaultShell = true;
250 home = "${baseDir}/queue-runner"; # really only to keep SSH happy
251 uid = config.ids.uids.hydra-queue-runner;
252 };
253
254 users.users.hydra-www =
255 { description = "Hydra web server";
256 group = "hydra";
257 useDefaultShell = true;
258 uid = config.ids.uids.hydra-www;
259 };
260
261 services.hydra.extraConfig =
262 ''
263 using_frontend_proxy = 1
264 base_uri = ${cfg.hydraURL}
265 notification_sender = ${cfg.notificationSender}
266 max_servers = 25
267 ${optionalString (cfg.logo != null) ''
268 hydra_logo = ${cfg.logo}
269 ''}
270 gc_roots_dir = ${cfg.gcRootsDir}
271 use-substitutes = ${if cfg.useSubstitutes then "1" else "0"}
272 '';
273
274 environment.systemPackages = [ hydra-package ];
275
276 environment.variables = hydraEnv;
277
278 nix.settings = mkMerge [
279 {
280 keep-outputs = true;
281 keep-derivations = true;
282 trusted-users = [ "hydra-queue-runner" ];
283 }
284
285 (mkIf (versionOlder (getVersion config.nix.package.out) "2.4pre")
286 {
287 # The default (`true') slows Nix down a lot since the build farm
288 # has so many GC roots.
289 gc-check-reachability = false;
290 }
291 )
292 ];
293
294 systemd.services.hydra-init =
295 { wantedBy = [ "multi-user.target" ];
296 requires = optional haveLocalDB "postgresql.service";
297 after = optional haveLocalDB "postgresql.service";
298 environment = env // {
299 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-init";
300 };
301 path = [ pkgs.util-linux ];
302 preStart = ''
303 mkdir -p ${baseDir}
304 chown hydra:hydra ${baseDir}
305 chmod 0750 ${baseDir}
306
307 ln -sf ${hydraConf} ${baseDir}/hydra.conf
308
309 mkdir -m 0700 -p ${baseDir}/www
310 chown hydra-www:hydra ${baseDir}/www
311
312 mkdir -m 0700 -p ${baseDir}/queue-runner
313 mkdir -m 0750 -p ${baseDir}/build-logs
314 mkdir -m 0750 -p ${baseDir}/runcommand-logs
315 chown hydra-queue-runner.hydra \
316 ${baseDir}/queue-runner \
317 ${baseDir}/build-logs \
318 ${baseDir}/runcommand-logs
319
320 ${optionalString haveLocalDB ''
321 if ! [ -e ${baseDir}/.db-created ]; then
322 runuser -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createuser hydra
323 runuser -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createdb -- -O hydra hydra
324 touch ${baseDir}/.db-created
325 fi
326 echo "create extension if not exists pg_trgm" | runuser -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra
327 ''}
328
329 if [ ! -e ${cfg.gcRootsDir} ]; then
330
331 # Move legacy roots directory.
332 if [ -e /nix/var/nix/gcroots/per-user/hydra/hydra-roots ]; then
333 mv /nix/var/nix/gcroots/per-user/hydra/hydra-roots ${cfg.gcRootsDir}
334 fi
335
336 mkdir -p ${cfg.gcRootsDir}
337 fi
338
339 # Move legacy hydra-www roots.
340 if [ -e /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots ]; then
341 find /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots/ -type f \
342 | xargs -r mv -f -t ${cfg.gcRootsDir}/
343 rmdir /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots
344 fi
345
346 chown hydra:hydra ${cfg.gcRootsDir}
347 chmod 2775 ${cfg.gcRootsDir}
348 '';
349 serviceConfig.ExecStart = "${hydra-package}/bin/hydra-init";
350 serviceConfig.PermissionsStartOnly = true;
351 serviceConfig.User = "hydra";
352 serviceConfig.Type = "oneshot";
353 serviceConfig.RemainAfterExit = true;
354 };
355
356 systemd.services.hydra-server =
357 { wantedBy = [ "multi-user.target" ];
358 requires = [ "hydra-init.service" ];
359 after = [ "hydra-init.service" ];
360 environment = serverEnv // {
361 HYDRA_DBI = "${serverEnv.HYDRA_DBI};application_name=hydra-server";
362 };
363 restartTriggers = [ hydraConf ];
364 serviceConfig =
365 { ExecStart =
366 "@${hydra-package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' "
367 + "-p ${toString cfg.port} --max_spare_servers 5 --max_servers 25 "
368 + "--max_requests 100 ${optionalString cfg.debugServer "-d"}";
369 User = "hydra-www";
370 PermissionsStartOnly = true;
371 Restart = "always";
372 };
373 };
374
375 systemd.services.hydra-queue-runner =
376 { wantedBy = [ "multi-user.target" ];
377 requires = [ "hydra-init.service" ];
378 after = [ "hydra-init.service" "network.target" ];
379 path = [ hydra-package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ];
380 restartTriggers = [ hydraConf ];
381 environment = env // {
382 PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr
383 IN_SYSTEMD = "1"; # to get log severity levels
384 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-queue-runner";
385 };
386 serviceConfig =
387 { ExecStart = "@${hydra-package}/bin/hydra-queue-runner hydra-queue-runner -v";
388 ExecStopPost = "${hydra-package}/bin/hydra-queue-runner --unlock";
389 User = "hydra-queue-runner";
390 Restart = "always";
391
392 # Ensure we can get core dumps.
393 LimitCORE = "infinity";
394 WorkingDirectory = "${baseDir}/queue-runner";
395 };
396 };
397
398 systemd.services.hydra-evaluator =
399 { wantedBy = [ "multi-user.target" ];
400 requires = [ "hydra-init.service" ];
401 after = [ "hydra-init.service" "network.target" ];
402 path = with pkgs; [ hydra-package nettools jq ];
403 restartTriggers = [ hydraConf ];
404 environment = env // {
405 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator";
406 };
407 serviceConfig =
408 { ExecStart = "@${hydra-package}/bin/hydra-evaluator hydra-evaluator";
409 User = "hydra";
410 Restart = "always";
411 WorkingDirectory = baseDir;
412 };
413 };
414
415 systemd.services.hydra-update-gc-roots =
416 { requires = [ "hydra-init.service" ];
417 after = [ "hydra-init.service" ];
418 environment = env // {
419 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-update-gc-roots";
420 };
421 serviceConfig =
422 { ExecStart = "@${hydra-package}/bin/hydra-update-gc-roots hydra-update-gc-roots";
423 User = "hydra";
424 };
425 startAt = "2,14:15";
426 };
427
428 systemd.services.hydra-send-stats =
429 { wantedBy = [ "multi-user.target" ];
430 after = [ "hydra-init.service" ];
431 environment = env // {
432 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-send-stats";
433 };
434 serviceConfig =
435 { ExecStart = "@${hydra-package}/bin/hydra-send-stats hydra-send-stats";
436 User = "hydra";
437 };
438 };
439
440 systemd.services.hydra-notify =
441 { wantedBy = [ "multi-user.target" ];
442 requires = [ "hydra-init.service" ];
443 after = [ "hydra-init.service" ];
444 restartTriggers = [ hydraConf ];
445 environment = env // {
446 PGPASSFILE = "${baseDir}/pgpass-queue-runner";
447 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-notify";
448 };
449 serviceConfig =
450 { ExecStart = "@${hydra-package}/bin/hydra-notify hydra-notify";
451 # FIXME: run this under a less privileged user?
452 User = "hydra-queue-runner";
453 Restart = "always";
454 RestartSec = 5;
455 };
456 };
457
458 # If there is less than a certain amount of free disk space, stop
459 # the queue/evaluator to prevent builds from failing or aborting.
460 systemd.services.hydra-check-space =
461 { script =
462 ''
463 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFree} * 1024**3)) ]; then
464 echo "stopping Hydra queue runner due to lack of free space..."
465 systemctl stop hydra-queue-runner
466 fi
467 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFreeEvaluator} * 1024**3)) ]; then
468 echo "stopping Hydra evaluator due to lack of free space..."
469 systemctl stop hydra-evaluator
470 fi
471 '';
472 startAt = "*:0/5";
473 };
474
475 # Periodically compress build logs. The queue runner compresses
476 # logs automatically after a step finishes, but this doesn't work
477 # if the queue runner is stopped prematurely.
478 systemd.services.hydra-compress-logs =
479 { path = [ pkgs.bzip2 ];
480 script =
481 ''
482 find /var/lib/hydra/build-logs -type f -name "*.drv" -mtime +3 -size +0c | xargs -r bzip2 -v -f
483 '';
484 startAt = "Sun 01:45";
485 };
486
487 services.postgresql.enable = mkIf haveLocalDB true;
488
489 services.postgresql.identMap = optionalString haveLocalDB
490 ''
491 hydra-users hydra hydra
492 hydra-users hydra-queue-runner hydra
493 hydra-users hydra-www hydra
494 hydra-users root hydra
495 # The postgres user is used to create the pg_trgm extension for the hydra database
496 hydra-users postgres postgres
497 '';
498
499 services.postgresql.authentication = optionalString haveLocalDB
500 ''
501 local hydra all ident map=hydra-users
502 '';
503
504 };
505
506}