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