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 buildInputs = [ 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 = '' 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 = '' 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 = "pkgs.hydra-unstable"; 104 description = "The Hydra package."; 105 }; 106 107 hydraURL = mkOption { 108 type = types.str; 109 description = '' 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 = '' 119 The hostname or address to listen on or <literal>*</literal> to listen 120 on all interfaces. 121 ''; 122 }; 123 124 port = mkOption { 125 type = types.int; 126 default = 3000; 127 description = '' 128 TCP port the web server should listen to. 129 ''; 130 }; 131 132 minimumDiskFree = mkOption { 133 type = types.int; 134 default = 0; 135 description = '' 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 = '' 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 = '' 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 = '' 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 = '' 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 = '' 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 = "Whether to run the server in debug mode."; 184 }; 185 186 extraConfig = mkOption { 187 type = types.lines; 188 description = "Extra lines for the Hydra configuration."; 189 }; 190 191 extraEnv = mkOption { 192 type = types.attrsOf types.str; 193 default = {}; 194 description = "Extra environment variables for Hydra."; 195 }; 196 197 gcRootsDir = mkOption { 198 type = types.path; 199 default = "/nix/var/nix/gcroots/hydra"; 200 description = "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 example = [ "/etc/nix/machines" "/var/lib/hydra/provisioner/machines" ]; 207 description = "List of files containing build machines."; 208 }; 209 210 useSubstitutes = mkOption { 211 type = types.bool; 212 default = false; 213 description = '' 214 Whether to use binary caches for downloading store paths. Note that 215 binary substitutions trigger (a potentially large number of) additional 216 HTTP requests that slow down the queue monitor thread significantly. 217 Also, this Hydra instance will serve those downloaded store paths to 218 its users with its own signature attached as if it had built them 219 itself, so don't enable this feature unless your active binary caches 220 are absolute trustworthy. 221 ''; 222 }; 223 }; 224 225 }; 226 227 228 ###### implementation 229 230 config = mkIf cfg.enable { 231 232 users.groups.hydra = { 233 gid = config.ids.gids.hydra; 234 }; 235 236 users.users.hydra = 237 { description = "Hydra"; 238 group = "hydra"; 239 # We don't enable `createHome` here because the creation of the home directory is handled by the hydra-init service below. 240 home = baseDir; 241 useDefaultShell = true; 242 uid = config.ids.uids.hydra; 243 }; 244 245 users.users.hydra-queue-runner = 246 { description = "Hydra queue runner"; 247 group = "hydra"; 248 useDefaultShell = true; 249 home = "${baseDir}/queue-runner"; # really only to keep SSH happy 250 uid = config.ids.uids.hydra-queue-runner; 251 }; 252 253 users.users.hydra-www = 254 { description = "Hydra web server"; 255 group = "hydra"; 256 useDefaultShell = true; 257 uid = config.ids.uids.hydra-www; 258 }; 259 260 nix.trustedUsers = [ "hydra-queue-runner" ]; 261 262 services.hydra.extraConfig = 263 '' 264 using_frontend_proxy = 1 265 base_uri = ${cfg.hydraURL} 266 notification_sender = ${cfg.notificationSender} 267 max_servers = 25 268 ${optionalString (cfg.logo != null) '' 269 hydra_logo = ${cfg.logo} 270 ''} 271 gc_roots_dir = ${cfg.gcRootsDir} 272 use-substitutes = ${if cfg.useSubstitutes then "1" else "0"} 273 ''; 274 275 environment.systemPackages = [ hydra-package ]; 276 277 environment.variables = hydraEnv; 278 279 nix.extraOptions = '' 280 keep-outputs = true 281 keep-derivations = true 282 283 284 '' + optionalString (versionOlder (getVersion config.nix.package.out) "2.4pre") '' 285 # The default (`true') slows Nix down a lot since the build farm 286 # has so many GC roots. 287 gc-check-reachability = false 288 ''; 289 290 systemd.services.hydra-init = 291 { wantedBy = [ "multi-user.target" ]; 292 requires = optional haveLocalDB "postgresql.service"; 293 after = optional haveLocalDB "postgresql.service"; 294 environment = env // { 295 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-init"; 296 }; 297 preStart = '' 298 mkdir -p ${baseDir} 299 chown hydra.hydra ${baseDir} 300 chmod 0750 ${baseDir} 301 302 ln -sf ${hydraConf} ${baseDir}/hydra.conf 303 304 mkdir -m 0700 -p ${baseDir}/www 305 chown hydra-www.hydra ${baseDir}/www 306 307 mkdir -m 0700 -p ${baseDir}/queue-runner 308 mkdir -m 0750 -p ${baseDir}/build-logs 309 chown hydra-queue-runner.hydra ${baseDir}/queue-runner ${baseDir}/build-logs 310 311 ${optionalString haveLocalDB '' 312 if ! [ -e ${baseDir}/.db-created ]; then 313 ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createuser hydra 314 ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${config.services.postgresql.package}/bin/createdb -O hydra hydra 315 touch ${baseDir}/.db-created 316 fi 317 echo "create extension if not exists pg_trgm" | ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra 318 ''} 319 320 if [ ! -e ${cfg.gcRootsDir} ]; then 321 322 # Move legacy roots directory. 323 if [ -e /nix/var/nix/gcroots/per-user/hydra/hydra-roots ]; then 324 mv /nix/var/nix/gcroots/per-user/hydra/hydra-roots ${cfg.gcRootsDir} 325 fi 326 327 mkdir -p ${cfg.gcRootsDir} 328 fi 329 330 # Move legacy hydra-www roots. 331 if [ -e /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots ]; then 332 find /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots/ -type f \ 333 | xargs -r mv -f -t ${cfg.gcRootsDir}/ 334 rmdir /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots 335 fi 336 337 chown hydra.hydra ${cfg.gcRootsDir} 338 chmod 2775 ${cfg.gcRootsDir} 339 ''; 340 serviceConfig.ExecStart = "${hydra-package}/bin/hydra-init"; 341 serviceConfig.PermissionsStartOnly = true; 342 serviceConfig.User = "hydra"; 343 serviceConfig.Type = "oneshot"; 344 serviceConfig.RemainAfterExit = true; 345 }; 346 347 systemd.services.hydra-server = 348 { wantedBy = [ "multi-user.target" ]; 349 requires = [ "hydra-init.service" ]; 350 after = [ "hydra-init.service" ]; 351 environment = serverEnv // { 352 HYDRA_DBI = "${serverEnv.HYDRA_DBI};application_name=hydra-server"; 353 }; 354 restartTriggers = [ hydraConf ]; 355 serviceConfig = 356 { ExecStart = 357 "@${hydra-package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' " 358 + "-p ${toString cfg.port} --max_spare_servers 5 --max_servers 25 " 359 + "--max_requests 100 ${optionalString cfg.debugServer "-d"}"; 360 User = "hydra-www"; 361 PermissionsStartOnly = true; 362 Restart = "always"; 363 }; 364 }; 365 366 systemd.services.hydra-queue-runner = 367 { wantedBy = [ "multi-user.target" ]; 368 requires = [ "hydra-init.service" ]; 369 after = [ "hydra-init.service" "network.target" ]; 370 path = [ hydra-package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ]; 371 restartTriggers = [ hydraConf ]; 372 environment = env // { 373 PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr 374 IN_SYSTEMD = "1"; # to get log severity levels 375 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-queue-runner"; 376 }; 377 serviceConfig = 378 { ExecStart = "@${hydra-package}/bin/hydra-queue-runner hydra-queue-runner -v"; 379 ExecStopPost = "${hydra-package}/bin/hydra-queue-runner --unlock"; 380 User = "hydra-queue-runner"; 381 Restart = "always"; 382 383 # Ensure we can get core dumps. 384 LimitCORE = "infinity"; 385 WorkingDirectory = "${baseDir}/queue-runner"; 386 }; 387 }; 388 389 systemd.services.hydra-evaluator = 390 { wantedBy = [ "multi-user.target" ]; 391 requires = [ "hydra-init.service" ]; 392 after = [ "hydra-init.service" "network.target" ]; 393 path = with pkgs; [ hydra-package nettools jq ]; 394 restartTriggers = [ hydraConf ]; 395 environment = env // { 396 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-evaluator"; 397 }; 398 serviceConfig = 399 { ExecStart = "@${hydra-package}/bin/hydra-evaluator hydra-evaluator"; 400 User = "hydra"; 401 Restart = "always"; 402 WorkingDirectory = baseDir; 403 }; 404 }; 405 406 systemd.services.hydra-update-gc-roots = 407 { requires = [ "hydra-init.service" ]; 408 after = [ "hydra-init.service" ]; 409 environment = env // { 410 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-update-gc-roots"; 411 }; 412 serviceConfig = 413 { ExecStart = "@${hydra-package}/bin/hydra-update-gc-roots hydra-update-gc-roots"; 414 User = "hydra"; 415 }; 416 startAt = "2,14:15"; 417 }; 418 419 systemd.services.hydra-send-stats = 420 { wantedBy = [ "multi-user.target" ]; 421 after = [ "hydra-init.service" ]; 422 environment = env // { 423 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-send-stats"; 424 }; 425 serviceConfig = 426 { ExecStart = "@${hydra-package}/bin/hydra-send-stats hydra-send-stats"; 427 User = "hydra"; 428 }; 429 }; 430 431 systemd.services.hydra-notify = 432 { wantedBy = [ "multi-user.target" ]; 433 requires = [ "hydra-init.service" ]; 434 after = [ "hydra-init.service" ]; 435 restartTriggers = [ hydraConf ]; 436 environment = env // { 437 PGPASSFILE = "${baseDir}/pgpass-queue-runner"; 438 HYDRA_DBI = "${env.HYDRA_DBI};application_name=hydra-notify"; 439 }; 440 serviceConfig = 441 { ExecStart = "@${hydra-package}/bin/hydra-notify hydra-notify"; 442 # FIXME: run this under a less privileged user? 443 User = "hydra-queue-runner"; 444 Restart = "always"; 445 RestartSec = 5; 446 }; 447 }; 448 449 # If there is less than a certain amount of free disk space, stop 450 # the queue/evaluator to prevent builds from failing or aborting. 451 systemd.services.hydra-check-space = 452 { script = 453 '' 454 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFree} * 1024**3)) ]; then 455 echo "stopping Hydra queue runner due to lack of free space..." 456 systemctl stop hydra-queue-runner 457 fi 458 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFreeEvaluator} * 1024**3)) ]; then 459 echo "stopping Hydra evaluator due to lack of free space..." 460 systemctl stop hydra-evaluator 461 fi 462 ''; 463 startAt = "*:0/5"; 464 }; 465 466 # Periodically compress build logs. The queue runner compresses 467 # logs automatically after a step finishes, but this doesn't work 468 # if the queue runner is stopped prematurely. 469 systemd.services.hydra-compress-logs = 470 { path = [ pkgs.bzip2 ]; 471 script = 472 '' 473 find /var/lib/hydra/build-logs -type f -name "*.drv" -mtime +3 -size +0c | xargs -r bzip2 -v -f 474 ''; 475 startAt = "Sun 01:45"; 476 }; 477 478 services.postgresql.enable = mkIf haveLocalDB true; 479 480 services.postgresql.identMap = optionalString haveLocalDB 481 '' 482 hydra-users hydra hydra 483 hydra-users hydra-queue-runner hydra 484 hydra-users hydra-www hydra 485 hydra-users root hydra 486 # The postgres user is used to create the pg_trgm extension for the hydra database 487 hydra-users postgres postgres 488 ''; 489 490 services.postgresql.authentication = optionalString haveLocalDB 491 '' 492 local hydra all ident map=hydra-users 493 ''; 494 495 }; 496 497}