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 ''; 237 238 environment.systemPackages = [ cfg.package ]; 239 240 environment.variables = hydraEnv; 241 242 nix.extraOptions = '' 243 gc-keep-outputs = true 244 gc-keep-derivations = true 245 246 # The default (`true') slows Nix down a lot since the build farm 247 # has so many GC roots. 248 gc-check-reachability = false 249 ''; 250 251 systemd.services.hydra-init = 252 { wantedBy = [ "multi-user.target" ]; 253 requires = optional haveLocalDB "postgresql.service"; 254 after = optional haveLocalDB "postgresql.service"; 255 environment = env; 256 preStart = '' 257 mkdir -p ${baseDir} 258 chown hydra.hydra ${baseDir} 259 chmod 0750 ${baseDir} 260 261 ln -sf ${hydraConf} ${baseDir}/hydra.conf 262 263 mkdir -m 0700 -p ${baseDir}/www 264 chown hydra-www.hydra ${baseDir}/www 265 266 mkdir -m 0700 -p ${baseDir}/queue-runner 267 mkdir -m 0750 -p ${baseDir}/build-logs 268 chown hydra-queue-runner.hydra ${baseDir}/queue-runner ${baseDir}/build-logs 269 270 ${optionalString haveLocalDB '' 271 if ! [ -e ${baseDir}/.db-created ]; then 272 ${config.services.postgresql.package}/bin/createuser hydra 273 ${config.services.postgresql.package}/bin/createdb -O hydra hydra 274 touch ${baseDir}/.db-created 275 fi 276 ''} 277 278 if [ ! -e ${cfg.gcRootsDir} ]; then 279 280 # Move legacy roots directory. 281 if [ -e /nix/var/nix/gcroots/per-user/hydra/hydra-roots ]; then 282 mv /nix/var/nix/gcroots/per-user/hydra/hydra-roots ${cfg.gcRootsDir} 283 fi 284 285 mkdir -p ${cfg.gcRootsDir} 286 fi 287 288 # Move legacy hydra-www roots. 289 if [ -e /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots ]; then 290 find /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots/ -type f \ 291 | xargs -r mv -f -t ${cfg.gcRootsDir}/ 292 rmdir /nix/var/nix/gcroots/per-user/hydra-www/hydra-roots 293 fi 294 295 chown hydra.hydra ${cfg.gcRootsDir} 296 chmod 2775 ${cfg.gcRootsDir} 297 ''; 298 serviceConfig.ExecStart = "${cfg.package}/bin/hydra-init"; 299 serviceConfig.PermissionsStartOnly = true; 300 serviceConfig.User = "hydra"; 301 serviceConfig.Type = "oneshot"; 302 serviceConfig.RemainAfterExit = true; 303 }; 304 305 systemd.services.hydra-server = 306 { wantedBy = [ "multi-user.target" ]; 307 requires = [ "hydra-init.service" ]; 308 after = [ "hydra-init.service" ]; 309 environment = serverEnv; 310 serviceConfig = 311 { ExecStart = 312 "@${cfg.package}/bin/hydra-server hydra-server -f -h '${cfg.listenHost}' " 313 + "-p ${toString cfg.port} --max_spare_servers 5 --max_servers 25 " 314 + "--max_requests 100 ${optionalString cfg.debugServer "-d"}"; 315 User = "hydra-www"; 316 PermissionsStartOnly = true; 317 Restart = "always"; 318 }; 319 }; 320 321 systemd.services.hydra-queue-runner = 322 { wantedBy = [ "multi-user.target" ]; 323 requires = [ "hydra-init.service" ]; 324 after = [ "hydra-init.service" "network.target" ]; 325 path = [ cfg.package pkgs.nettools pkgs.openssh pkgs.bzip2 config.nix.package ]; 326 environment = env // { 327 PGPASSFILE = "${baseDir}/pgpass-queue-runner"; # grrr 328 IN_SYSTEMD = "1"; # to get log severity levels 329 }; 330 serviceConfig = 331 { ExecStart = "@${cfg.package}/bin/hydra-queue-runner hydra-queue-runner -v --option build-use-substitutes ${if cfg.useSubstitutes then "true" else "false"}"; 332 ExecStopPost = "${cfg.package}/bin/hydra-queue-runner --unlock"; 333 User = "hydra-queue-runner"; 334 Restart = "always"; 335 336 # Ensure we can get core dumps. 337 LimitCORE = "infinity"; 338 WorkingDirectory = "${baseDir}/queue-runner"; 339 }; 340 }; 341 342 systemd.services.hydra-evaluator = 343 { wantedBy = [ "multi-user.target" ]; 344 requires = [ "hydra-init.service" ]; 345 after = [ "hydra-init.service" "network.target" ]; 346 path = [ pkgs.nettools ]; 347 environment = env; 348 serviceConfig = 349 { ExecStart = "@${cfg.package}/bin/hydra-evaluator hydra-evaluator"; 350 User = "hydra"; 351 Restart = "always"; 352 WorkingDirectory = baseDir; 353 }; 354 }; 355 356 systemd.services.hydra-update-gc-roots = 357 { requires = [ "hydra-init.service" ]; 358 after = [ "hydra-init.service" ]; 359 environment = env; 360 serviceConfig = 361 { ExecStart = "@${cfg.package}/bin/hydra-update-gc-roots hydra-update-gc-roots"; 362 User = "hydra"; 363 }; 364 startAt = "2,14:15"; 365 }; 366 367 systemd.services.hydra-send-stats = 368 { wantedBy = [ "multi-user.target" ]; 369 after = [ "hydra-init.service" ]; 370 environment = env; 371 serviceConfig = 372 { ExecStart = "@${cfg.package}/bin/hydra-send-stats hydra-send-stats"; 373 User = "hydra"; 374 }; 375 }; 376 377 # If there is less than a certain amount of free disk space, stop 378 # the queue/evaluator to prevent builds from failing or aborting. 379 systemd.services.hydra-check-space = 380 { script = 381 '' 382 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFree} * 1024**3)) ]; then 383 echo "stopping Hydra queue runner due to lack of free space..." 384 systemctl stop hydra-queue-runner 385 fi 386 if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFreeEvaluator} * 1024**3)) ]; then 387 echo "stopping Hydra evaluator due to lack of free space..." 388 systemctl stop hydra-evaluator 389 fi 390 ''; 391 startAt = "*:0/5"; 392 }; 393 394 # Periodically compress build logs. The queue runner compresses 395 # logs automatically after a step finishes, but this doesn't work 396 # if the queue runner is stopped prematurely. 397 systemd.services.hydra-compress-logs = 398 { path = [ pkgs.bzip2 ]; 399 script = 400 '' 401 find /var/lib/hydra/build-logs -type f -name "*.drv" -mtime +3 -size +0c | xargs -r bzip2 -v -f 402 ''; 403 startAt = "Sun 01:45"; 404 }; 405 406 services.postgresql.enable = mkIf haveLocalDB true; 407 408 services.postgresql.identMap = optionalString haveLocalDB 409 '' 410 hydra-users hydra hydra 411 hydra-users hydra-queue-runner hydra 412 hydra-users hydra-www hydra 413 hydra-users root hydra 414 ''; 415 416 services.postgresql.authentication = optionalString haveLocalDB 417 '' 418 local hydra all ident map=hydra-users 419 ''; 420 421 }; 422 423}