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