at 25.11-pre 18 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 utils, 6 ... 7}: 8 9let 10 inherit (lib) 11 concatMapStringsSep 12 escapeShellArgs 13 filter 14 filterAttrs 15 getExe 16 getExe' 17 isAttrs 18 isList 19 literalExpression 20 mapAttrs 21 mkDefault 22 mkEnableOption 23 mkIf 24 mkOption 25 mkPackageOption 26 optionals 27 optionalString 28 recursiveUpdate 29 types 30 ; 31 32 filterRecursiveNull = 33 o: 34 if isAttrs o then 35 mapAttrs (_: v: filterRecursiveNull v) (filterAttrs (_: v: v != null) o) 36 else if isList o then 37 map filterRecursiveNull (filter (v: v != null) o) 38 else 39 o; 40 41 cfg = config.services.pretix; 42 format = pkgs.formats.ini { }; 43 44 configFile = format.generate "pretix.cfg" (filterRecursiveNull cfg.settings); 45 46 finalPackage = cfg.package.override { 47 inherit (cfg) plugins; 48 }; 49 50 pythonEnv = cfg.package.python.buildEnv.override { 51 extraLibs = 52 with cfg.package.python.pkgs; 53 [ 54 (toPythonModule finalPackage) 55 gunicorn 56 ] 57 ++ lib.optionals ( 58 cfg.settings.memcached.location != null 59 ) cfg.package.optional-dependencies.memcached; 60 }; 61 62 withRedis = cfg.settings.redis.location != null; 63in 64{ 65 meta = with lib; { 66 maintainers = with maintainers; [ hexa ]; 67 }; 68 69 options.services.pretix = { 70 enable = mkEnableOption "Pretix, a ticket shop application for conferences, festivals, concerts, etc"; 71 72 package = mkPackageOption pkgs "pretix" { }; 73 74 group = mkOption { 75 type = types.str; 76 default = "pretix"; 77 description = '' 78 Group under which pretix should run. 79 ''; 80 }; 81 82 user = mkOption { 83 type = types.str; 84 default = "pretix"; 85 description = '' 86 User under which pretix should run. 87 ''; 88 }; 89 90 environmentFile = mkOption { 91 type = types.nullOr types.path; 92 default = null; 93 example = "/run/keys/pretix-secrets.env"; 94 description = '' 95 Environment file to pass secret configuration values. 96 97 Each line must follow the `PRETIX_SECTION_KEY=value` pattern. 98 ''; 99 }; 100 101 plugins = mkOption { 102 type = types.listOf types.package; 103 default = [ ]; 104 example = literalExpression '' 105 with config.services.pretix.package.plugins; [ 106 passbook 107 pages 108 ]; 109 ''; 110 description = '' 111 Pretix plugins to install into the Python environment. 112 ''; 113 }; 114 115 gunicorn.extraArgs = mkOption { 116 type = with types; listOf str; 117 default = [ 118 "--name=pretix" 119 ]; 120 example = [ 121 "--name=pretix" 122 "--workers=4" 123 "--max-requests=1200" 124 "--max-requests-jitter=50" 125 "--log-level=info" 126 ]; 127 description = '' 128 Extra arguments to pass to gunicorn. 129 See <https://docs.pretix.eu/en/latest/admin/installation/manual_smallscale.html#start-pretix-as-a-service> for details. 130 ''; 131 apply = escapeShellArgs; 132 }; 133 134 celery = { 135 extraArgs = mkOption { 136 type = with types; listOf str; 137 default = [ ]; 138 description = '' 139 Extra arguments to pass to celery. 140 141 See <https://docs.celeryq.dev/en/stable/reference/cli.html#celery-worker> for more info. 142 ''; 143 apply = utils.escapeSystemdExecArgs; 144 }; 145 }; 146 147 nginx = { 148 enable = mkOption { 149 type = types.bool; 150 default = true; 151 example = false; 152 description = '' 153 Whether to set up an nginx virtual host. 154 ''; 155 }; 156 157 domain = mkOption { 158 type = types.str; 159 example = "talks.example.com"; 160 description = '' 161 The domain name under which to set up the virtual host. 162 ''; 163 }; 164 }; 165 166 database.createLocally = mkOption { 167 type = types.bool; 168 default = true; 169 example = false; 170 description = '' 171 Whether to automatically set up the database on the local DBMS instance. 172 173 Only supported for PostgreSQL. Not required for sqlite. 174 ''; 175 }; 176 177 settings = mkOption { 178 type = types.submodule { 179 freeformType = format.type; 180 options = { 181 pretix = { 182 instance_name = mkOption { 183 type = types.str; 184 example = "tickets.example.com"; 185 description = '' 186 The name of this installation. 187 ''; 188 }; 189 190 url = mkOption { 191 type = types.str; 192 example = "https://tickets.example.com"; 193 description = '' 194 The installations full URL, without a trailing slash. 195 ''; 196 }; 197 198 cachedir = mkOption { 199 type = types.path; 200 default = "/var/cache/pretix"; 201 description = '' 202 Directory for storing temporary files. 203 ''; 204 }; 205 206 datadir = mkOption { 207 type = types.path; 208 default = "/var/lib/pretix"; 209 description = '' 210 Directory for storing user uploads and similar data. 211 ''; 212 }; 213 214 logdir = mkOption { 215 type = types.path; 216 default = "/var/log/pretix"; 217 description = '' 218 Directory for storing log files. 219 ''; 220 }; 221 222 currency = mkOption { 223 type = types.str; 224 default = "EUR"; 225 example = "USD"; 226 description = '' 227 Default currency for events in its ISO 4217 three-letter code. 228 ''; 229 }; 230 231 registration = mkOption { 232 type = types.bool; 233 default = false; 234 example = true; 235 description = '' 236 Whether to allow registration of new admin users. 237 ''; 238 }; 239 }; 240 241 database = { 242 backend = mkOption { 243 type = types.enum [ 244 "sqlite3" 245 "postgresql" 246 ]; 247 default = "postgresql"; 248 description = '' 249 Database backend to use. 250 251 Only postgresql is recommended for production setups. 252 ''; 253 }; 254 255 host = mkOption { 256 type = with types; nullOr str; 257 default = if cfg.settings.database.backend == "postgresql" then "/run/postgresql" else null; 258 defaultText = literalExpression '' 259 if config.services.pretix.settings..database.backend == "postgresql" then "/run/postgresql" 260 else null 261 ''; 262 description = '' 263 Database host or socket path. 264 ''; 265 }; 266 267 name = mkOption { 268 type = types.str; 269 default = "pretix"; 270 description = '' 271 Database name. 272 ''; 273 }; 274 275 user = mkOption { 276 type = types.str; 277 default = "pretix"; 278 description = '' 279 Database username. 280 ''; 281 }; 282 }; 283 284 mail = { 285 from = mkOption { 286 type = types.str; 287 example = "tickets@example.com"; 288 description = '' 289 E-Mail address used in the `FROM` header of outgoing mails. 290 ''; 291 }; 292 293 host = mkOption { 294 type = types.str; 295 default = "localhost"; 296 example = "mail.example.com"; 297 description = '' 298 Hostname of the SMTP server use for mail delivery. 299 ''; 300 }; 301 302 port = mkOption { 303 type = types.port; 304 default = 25; 305 example = 587; 306 description = '' 307 Port of the SMTP server to use for mail delivery. 308 ''; 309 }; 310 }; 311 312 celery = { 313 backend = mkOption { 314 type = types.str; 315 default = "redis+socket://${config.services.redis.servers.pretix.unixSocket}?virtual_host=1"; 316 defaultText = literalExpression '' 317 redis+socket://''${config.services.redis.servers.pretix.unixSocket}?virtual_host=1 318 ''; 319 description = '' 320 URI to the celery backend used for the asynchronous job queue. 321 ''; 322 }; 323 324 broker = mkOption { 325 type = types.str; 326 default = "redis+socket://${config.services.redis.servers.pretix.unixSocket}?virtual_host=2"; 327 defaultText = literalExpression '' 328 redis+socket://''${config.services.redis.servers.pretix.unixSocket}?virtual_host=2 329 ''; 330 description = '' 331 URI to the celery broker used for the asynchronous job queue. 332 ''; 333 }; 334 }; 335 336 redis = { 337 location = mkOption { 338 type = with types; nullOr str; 339 default = "unix://${config.services.redis.servers.pretix.unixSocket}?db=0"; 340 defaultText = literalExpression '' 341 "unix://''${config.services.redis.servers.pretix.unixSocket}?db=0" 342 ''; 343 description = '' 344 URI to the redis server, used to speed up locking, caching and session storage. 345 ''; 346 }; 347 348 sessions = mkOption { 349 type = types.bool; 350 default = true; 351 example = false; 352 description = '' 353 Whether to use redis as the session storage. 354 ''; 355 }; 356 }; 357 358 memcached = { 359 location = mkOption { 360 type = with types; nullOr str; 361 default = null; 362 example = "127.0.0.1:11211"; 363 description = '' 364 The `host:port` combination or the path to the UNIX socket of a memcached instance. 365 366 Can be used instead of Redis for caching. 367 ''; 368 }; 369 }; 370 371 tools = { 372 pdftk = mkOption { 373 type = types.path; 374 default = getExe pkgs.pdftk; 375 defaultText = literalExpression '' 376 lib.getExe pkgs.pdftk 377 ''; 378 description = '' 379 Path to the pdftk executable. 380 ''; 381 }; 382 }; 383 }; 384 }; 385 default = { }; 386 description = '' 387 pretix configuration as a Nix attribute set. All settings can also be passed 388 from the environment. 389 390 See <https://docs.pretix.eu/en/latest/admin/config.html> for possible options. 391 ''; 392 }; 393 }; 394 395 config = mkIf cfg.enable { 396 # https://docs.pretix.eu/en/latest/admin/installation/index.html 397 398 environment.systemPackages = [ 399 (pkgs.writeScriptBin "pretix-manage" '' 400 cd ${cfg.settings.pretix.datadir} 401 sudo=exec 402 if [[ "$USER" != ${cfg.user} ]]; then 403 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} ${optionalString withRedis "-g redis-pretix"} --preserve-env=PRETIX_CONFIG_FILE' 404 fi 405 export PRETIX_CONFIG_FILE=${configFile} 406 $sudo ${getExe' pythonEnv "pretix-manage"} "$@" 407 '') 408 ]; 409 410 services.logrotate.settings.pretix = { 411 files = "${cfg.settings.pretix.logdir}/*.log"; 412 su = "${cfg.user} ${cfg.group}"; 413 frequency = "weekly"; 414 rotate = "12"; 415 copytruncate = true; 416 compress = true; 417 }; 418 419 services = { 420 nginx = mkIf cfg.nginx.enable { 421 enable = true; 422 recommendedGzipSettings = mkDefault true; 423 recommendedOptimisation = mkDefault true; 424 recommendedProxySettings = mkDefault true; 425 recommendedTlsSettings = mkDefault true; 426 upstreams.pretix.servers."unix:/run/pretix/pretix.sock" = { }; 427 virtualHosts.${cfg.nginx.domain} = { 428 # https://docs.pretix.eu/en/latest/admin/installation/manual_smallscale.html#ssl 429 extraConfig = '' 430 more_set_headers Referrer-Policy same-origin; 431 more_set_headers X-Content-Type-Options nosniff; 432 ''; 433 locations = { 434 "/".proxyPass = "http://pretix"; 435 "/media/" = { 436 alias = "${cfg.settings.pretix.datadir}/media/"; 437 extraConfig = '' 438 access_log off; 439 expires 7d; 440 ''; 441 }; 442 "^~ (/media/(cachedfiles|invoices)|/static/(staticfiles.json|CACHE/manifest.json))" = { 443 extraConfig = '' 444 deny all; 445 return 404; 446 ''; 447 }; 448 "/static/" = { 449 alias = "${finalPackage}/${cfg.package.python.sitePackages}/pretix/static.dist/"; 450 extraConfig = '' 451 access_log off; 452 more_set_headers Cache-Control "public"; 453 expires 365d; 454 ''; 455 }; 456 }; 457 }; 458 }; 459 460 postgresql = mkIf (cfg.database.createLocally && cfg.settings.database.backend == "postgresql") { 461 enable = true; 462 ensureUsers = [ 463 { 464 name = cfg.settings.database.user; 465 ensureDBOwnership = true; 466 } 467 ]; 468 ensureDatabases = [ cfg.settings.database.name ]; 469 }; 470 471 redis.servers.pretix.enable = withRedis; 472 }; 473 474 systemd.services = 475 let 476 commonUnitConfig = { 477 environment.PRETIX_CONFIG_FILE = configFile; 478 serviceConfig = { 479 User = "pretix"; 480 Group = "pretix"; 481 EnvironmentFile = optionals (cfg.environmentFile != null) [ 482 cfg.environmentFile 483 ]; 484 StateDirectory = [ 485 "pretix" 486 ]; 487 StateDirectoryMode = "0750"; 488 CacheDirectory = "pretix"; 489 LogsDirectory = "pretix"; 490 WorkingDirectory = cfg.settings.pretix.datadir; 491 SupplementaryGroups = optionals withRedis [ 492 "redis-pretix" 493 ]; 494 AmbientCapabilities = ""; 495 CapabilityBoundingSet = [ "" ]; 496 DevicePolicy = "closed"; 497 LockPersonality = true; 498 MemoryDenyWriteExecute = false; # required by pdftk 499 NoNewPrivileges = true; 500 PrivateDevices = true; 501 PrivateTmp = true; 502 ProcSubset = "pid"; 503 ProtectControlGroups = true; 504 ProtectHome = true; 505 ProtectHostname = true; 506 ProtectKernelLogs = true; 507 ProtectKernelModules = true; 508 ProtectKernelTunables = true; 509 ProtectProc = "invisible"; 510 ProtectSystem = "strict"; 511 RemoveIPC = true; 512 RestrictAddressFamilies = [ 513 "AF_INET" 514 "AF_INET6" 515 "AF_UNIX" 516 ]; 517 RestrictNamespaces = true; 518 RestrictRealtime = true; 519 RestrictSUIDSGID = true; 520 SystemCallArchitectures = "native"; 521 SystemCallFilter = [ 522 "@system-service" 523 "~@privileged" 524 "@chown" 525 ]; 526 UMask = "0027"; 527 }; 528 }; 529 in 530 { 531 pretix-web = recursiveUpdate commonUnitConfig { 532 description = "pretix web service"; 533 after = [ 534 "network.target" 535 "redis-pretix.service" 536 "postgresql.service" 537 ]; 538 wantedBy = [ "multi-user.target" ]; 539 preStart = '' 540 versionFile="${cfg.settings.pretix.datadir}/.version" 541 version=$(cat "$versionFile" 2>/dev/null || echo 0) 542 543 pluginsFile="${cfg.settings.pretix.datadir}/.plugins" 544 plugins=$(cat "$pluginsFile" 2>/dev/null || echo "") 545 configuredPlugins="${concatMapStringsSep "|" (package: package.name) cfg.plugins}" 546 547 if [[ $version != ${cfg.package.version} || $plugins != $configuredPlugins ]]; then 548 ${getExe' pythonEnv "pretix-manage"} migrate 549 550 echo "${cfg.package.version}" > "$versionFile" 551 echo "$configuredPlugins" > "$pluginsFile" 552 fi 553 ''; 554 serviceConfig = { 555 TimeoutStartSec = "15min"; 556 ExecStart = "${getExe' pythonEnv "gunicorn"} --bind unix:/run/pretix/pretix.sock ${cfg.gunicorn.extraArgs} pretix.wsgi"; 557 RuntimeDirectory = "pretix"; 558 Restart = "on-failure"; 559 }; 560 }; 561 562 pretix-periodic = recursiveUpdate commonUnitConfig { 563 description = "pretix periodic task runner"; 564 # every 15 minutes 565 startAt = [ "*:3,18,33,48" ]; 566 serviceConfig = { 567 Type = "oneshot"; 568 ExecStart = "${getExe' pythonEnv "pretix-manage"} runperiodic"; 569 }; 570 }; 571 572 pretix-worker = recursiveUpdate commonUnitConfig { 573 description = "pretix asynchronous job runner"; 574 after = [ 575 "network.target" 576 "redis-pretix.service" 577 "postgresql.service" 578 ]; 579 wantedBy = [ "multi-user.target" ]; 580 serviceConfig = { 581 ExecStart = "${getExe' pythonEnv "celery"} -A pretix.celery_app worker ${cfg.celery.extraArgs}"; 582 Restart = "on-failure"; 583 }; 584 }; 585 586 nginx.serviceConfig.SupplementaryGroups = mkIf cfg.nginx.enable [ "pretix" ]; 587 }; 588 589 systemd.sockets.pretix-web.socketConfig = { 590 ListenStream = "/run/pretix/pretix.sock"; 591 SocketUser = "nginx"; 592 }; 593 594 users = { 595 groups.${cfg.group} = { }; 596 users.${cfg.user} = { 597 isSystemUser = true; 598 inherit (cfg) group; 599 }; 600 }; 601 }; 602}