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