at 25.11-pre 16 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 utils, 6 ... 7}: 8 9let 10 cfg = config.services.pretalx; 11 format = pkgs.formats.ini { }; 12 13 configFile = format.generate "pretalx.cfg" cfg.settings; 14 15 finalPackage = cfg.package.override { 16 inherit (cfg) plugins; 17 }; 18 19 pythonEnv = finalPackage.python.buildEnv.override { 20 extraLibs = 21 with finalPackage.python.pkgs; 22 [ 23 (toPythonModule finalPackage) 24 gunicorn 25 ] 26 ++ finalPackage.optional-dependencies.redis 27 ++ lib.optionals cfg.celery.enable [ celery ] 28 ++ lib.optionals ( 29 cfg.settings.database.backend == "postgresql" 30 ) finalPackage.optional-dependencies.postgres; 31 }; 32in 33 34{ 35 meta = with lib; { 36 maintainers = with maintainers; [ hexa ] ++ teams.c3d2.members; 37 }; 38 39 options.services.pretalx = { 40 enable = lib.mkEnableOption "pretalx"; 41 42 package = lib.mkPackageOption pkgs "pretalx" { }; 43 44 group = lib.mkOption { 45 type = lib.types.str; 46 default = "pretalx"; 47 description = "Group under which pretalx should run."; 48 }; 49 50 user = lib.mkOption { 51 type = lib.types.str; 52 default = "pretalx"; 53 description = "User under which pretalx should run."; 54 }; 55 56 plugins = lib.mkOption { 57 type = with lib.types; listOf package; 58 default = [ ]; 59 example = lib.literalExpression '' 60 with config.services.pretalx.package.plugins; [ 61 pages 62 youtube 63 ]; 64 ''; 65 description = '' 66 Pretalx plugins to install into the Python environment. 67 ''; 68 }; 69 70 gunicorn.extraArgs = lib.mkOption { 71 type = with lib.types; listOf str; 72 default = [ 73 "--name=pretalx" 74 ]; 75 example = [ 76 "--name=pretalx" 77 "--workers=4" 78 "--max-requests=1200" 79 "--max-requests-jitter=50" 80 "--log-level=info" 81 ]; 82 description = '' 83 Extra arguments to pass to gunicorn. 84 See <https://docs.pretalx.org/administrator/installation.html#step-6-starting-pretalx-as-a-service> for details. 85 ''; 86 apply = lib.escapeShellArgs; 87 }; 88 89 celery = { 90 enable = lib.mkOption { 91 type = lib.types.bool; 92 default = true; 93 example = false; 94 description = '' 95 Whether to set up celery as an asynchronous task runner. 96 ''; 97 }; 98 99 extraArgs = lib.mkOption { 100 type = with lib.types; listOf str; 101 default = [ ]; 102 description = '' 103 Extra arguments to pass to celery. 104 105 See <https://docs.celeryq.dev/en/stable/reference/cli.html#celery-worker> for more info. 106 ''; 107 apply = utils.escapeSystemdExecArgs; 108 }; 109 }; 110 111 nginx = { 112 enable = lib.mkOption { 113 type = lib.types.bool; 114 default = true; 115 example = false; 116 description = '' 117 Whether to set up an nginx virtual host. 118 ''; 119 }; 120 121 domain = lib.mkOption { 122 type = lib.types.str; 123 example = "talks.example.com"; 124 description = '' 125 The domain name under which to set up the virtual host. 126 ''; 127 }; 128 }; 129 130 database.createLocally = lib.mkOption { 131 type = lib.types.bool; 132 default = true; 133 example = false; 134 description = '' 135 Whether to automatically set up the database on the local DBMS instance. 136 137 Currently only supported for PostgreSQL. Not required for sqlite. 138 ''; 139 }; 140 141 settings = lib.mkOption { 142 type = lib.types.submodule { 143 freeformType = format.type; 144 options = { 145 database = { 146 backend = lib.mkOption { 147 type = lib.types.enum [ 148 "postgresql" 149 ]; 150 default = "postgresql"; 151 description = '' 152 Database backend to use. 153 154 Currently only PostgreSQL gets tested, and as such we don't support any other DBMS. 155 ''; 156 readOnly = true; # only postgres supported right now 157 }; 158 159 host = lib.mkOption { 160 type = with lib.types; nullOr types.path; 161 default = 162 if cfg.settings.database.backend == "postgresql" then 163 "/run/postgresql" 164 else if cfg.settings.database.backend == "mysql" then 165 "/run/mysqld/mysqld.sock" 166 else 167 null; 168 defaultText = lib.literalExpression '' 169 if config.services.pretalx.settings..database.backend == "postgresql" then "/run/postgresql" 170 else if config.services.pretalx.settings.database.backend == "mysql" then "/run/mysqld/mysqld.sock" 171 else null 172 ''; 173 description = '' 174 Database host or socket path. 175 ''; 176 }; 177 178 name = lib.mkOption { 179 type = lib.types.str; 180 default = "pretalx"; 181 description = '' 182 Database name. 183 ''; 184 }; 185 186 user = lib.mkOption { 187 type = lib.types.str; 188 default = "pretalx"; 189 description = '' 190 Database username. 191 ''; 192 }; 193 }; 194 195 files = { 196 upload_limit = lib.mkOption { 197 type = lib.types.ints.positive; 198 default = 10; 199 example = 50; 200 description = '' 201 Maximum file upload size in MiB. 202 ''; 203 }; 204 }; 205 206 filesystem = { 207 data = lib.mkOption { 208 type = lib.types.path; 209 default = "/var/lib/pretalx"; 210 description = '' 211 Base path for all other storage paths. 212 ''; 213 }; 214 logs = lib.mkOption { 215 type = lib.types.path; 216 default = "/var/log/pretalx"; 217 description = '' 218 Path to the log directory, that pretalx logs message to. 219 ''; 220 }; 221 static = lib.mkOption { 222 type = lib.types.path; 223 default = "${cfg.package.static}/"; 224 defaultText = lib.literalExpression "\${config.services.pretalx.package}.static}/"; 225 readOnly = true; 226 description = '' 227 Path to the directory that contains static files. 228 ''; 229 }; 230 }; 231 232 celery = { 233 backend = lib.mkOption { 234 type = with lib.types; nullOr str; 235 default = lib.optionalString cfg.celery.enable "redis+socket://${config.services.redis.servers.pretalx.unixSocket}?virtual_host=1"; 236 defaultText = lib.literalExpression '' 237 optionalString config.services.pretalx.celery.enable "redis+socket://''${config.services.redis.servers.pretalx.unixSocket}?virtual_host=1" 238 ''; 239 description = '' 240 URI to the celery backend used for the asynchronous job queue. 241 ''; 242 }; 243 244 broker = lib.mkOption { 245 type = with lib.types; nullOr str; 246 default = lib.optionalString cfg.celery.enable "redis+socket://${config.services.redis.servers.pretalx.unixSocket}?virtual_host=2"; 247 defaultText = lib.literalExpression '' 248 optionalString config.services.pretalx.celery.enable "redis+socket://''${config.services.redis.servers.pretalx.unixSocket}?virtual_host=2" 249 ''; 250 description = '' 251 URI to the celery broker used for the asynchronous job queue. 252 ''; 253 }; 254 }; 255 256 redis = { 257 location = lib.mkOption { 258 type = with lib.types; nullOr str; 259 default = "unix://${config.services.redis.servers.pretalx.unixSocket}?db=0"; 260 defaultText = lib.literalExpression '' 261 "unix://''${config.services.redis.servers.pretalx.unixSocket}?db=0" 262 ''; 263 description = '' 264 URI to the redis server, used to speed up locking, caching and session storage. 265 ''; 266 }; 267 268 session = lib.mkOption { 269 type = lib.types.bool; 270 default = true; 271 example = false; 272 description = '' 273 Whether to use redis as the session storage. 274 ''; 275 }; 276 }; 277 278 site = { 279 url = lib.mkOption { 280 type = lib.types.str; 281 default = "https://${cfg.nginx.domain}"; 282 defaultText = lib.literalExpression "https://\${config.services.pretalx.nginx.domain}"; 283 example = "https://talks.example.com"; 284 description = '' 285 The base URI below which your pretalx instance will be reachable. 286 ''; 287 }; 288 }; 289 }; 290 }; 291 default = { }; 292 description = '' 293 pretalx configuration as a Nix attribute set. All settings can also be passed 294 from the environment. 295 296 See <https://docs.pretalx.org/administrator/configure.html> for possible options. 297 ''; 298 }; 299 }; 300 301 config = lib.mkIf cfg.enable { 302 # https://docs.pretalx.org/administrator/installation.html 303 304 environment.systemPackages = [ 305 (pkgs.writeScriptBin "pretalx-manage" '' 306 cd ${cfg.settings.filesystem.data} 307 sudo=exec 308 if [[ "$USER" != ${cfg.user} ]]; then 309 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env=PRETALX_CONFIG_FILE' 310 fi 311 export PRETALX_CONFIG_FILE=${configFile} 312 $sudo ${lib.getExe' pythonEnv "pretalx-manage"} "$@" 313 '') 314 ]; 315 316 services.logrotate.settings.pretalx = { 317 files = "${cfg.settings.filesystem.logs}/*.log"; 318 su = "${cfg.user} ${cfg.group}"; 319 frequency = "weekly"; 320 rotate = "12"; 321 copytruncate = true; 322 compress = true; 323 }; 324 325 services = { 326 nginx = lib.mkIf cfg.nginx.enable { 327 enable = true; 328 recommendedGzipSettings = lib.mkDefault true; 329 recommendedOptimisation = lib.mkDefault true; 330 recommendedProxySettings = lib.mkDefault true; 331 recommendedTlsSettings = lib.mkDefault true; 332 upstreams.pretalx.servers."unix:/run/pretalx/pretalx.sock" = { }; 333 virtualHosts.${cfg.nginx.domain} = { 334 # https://docs.pretalx.org/administrator/installation.html#step-7-ssl 335 extraConfig = '' 336 more_set_headers "Referrer-Policy: same-origin"; 337 more_set_headers "X-Content-Type-Options: nosniff"; 338 ''; 339 locations = { 340 "/".proxyPass = "http://pretalx"; 341 "/media/" = { 342 alias = "${cfg.settings.filesystem.data}/media/"; 343 extraConfig = '' 344 access_log off; 345 more_set_headers 'Content-Disposition: attachment; filename="$1"'; 346 expires 7d; 347 ''; 348 }; 349 "/static/" = { 350 alias = cfg.settings.filesystem.static; 351 extraConfig = '' 352 access_log off; 353 more_set_headers Cache-Control "public"; 354 expires 365d; 355 ''; 356 }; 357 }; 358 }; 359 }; 360 361 postgresql = 362 lib.mkIf (cfg.database.createLocally && cfg.settings.database.backend == "postgresql") 363 { 364 enable = true; 365 ensureUsers = [ 366 { 367 name = cfg.settings.database.user; 368 ensureDBOwnership = true; 369 } 370 ]; 371 ensureDatabases = [ cfg.settings.database.name ]; 372 }; 373 374 redis.servers.pretalx.enable = true; 375 }; 376 377 systemd.services = 378 let 379 commonUnitConfig = { 380 environment.PRETALX_CONFIG_FILE = configFile; 381 serviceConfig = { 382 User = "pretalx"; 383 Group = "pretalx"; 384 StateDirectory = [ 385 "pretalx" 386 "pretalx/media" 387 ]; 388 StateDirectoryMode = "0750"; 389 LogsDirectory = "pretalx"; 390 WorkingDirectory = cfg.settings.filesystem.data; 391 SupplementaryGroups = [ "redis-pretalx" ]; 392 AmbientCapabilities = ""; 393 CapabilityBoundingSet = [ "" ]; 394 DevicePolicy = "closed"; 395 LockPersonality = true; 396 MemoryDenyWriteExecute = true; 397 NoNewPrivileges = true; 398 PrivateDevices = true; 399 PrivateTmp = true; 400 ProcSubset = "pid"; 401 ProtectControlGroups = true; 402 ProtectHome = true; 403 ProtectHostname = true; 404 ProtectKernelLogs = true; 405 ProtectKernelModules = true; 406 ProtectKernelTunables = true; 407 ProtectProc = "invisible"; 408 ProtectSystem = "strict"; 409 RemoveIPC = true; 410 RestrictAddressFamilies = [ 411 "AF_INET" 412 "AF_INET6" 413 "AF_UNIX" 414 ]; 415 RestrictNamespaces = true; 416 RestrictRealtime = true; 417 RestrictSUIDSGID = true; 418 SystemCallArchitectures = "native"; 419 SystemCallFilter = [ 420 "@system-service" 421 "~@privileged" 422 "@chown" 423 ]; 424 UMask = "0027"; 425 }; 426 }; 427 in 428 { 429 pretalx-web = lib.recursiveUpdate commonUnitConfig { 430 description = "pretalx web service"; 431 after = 432 [ 433 "network.target" 434 "redis-pretalx.service" 435 ] 436 ++ lib.optionals (cfg.settings.database.backend == "postgresql") [ 437 "postgresql.service" 438 ] 439 ++ lib.optionals (cfg.settings.database.backend == "mysql") [ 440 "mysql.service" 441 ]; 442 wantedBy = [ "multi-user.target" ]; 443 preStart = '' 444 versionFile="${cfg.settings.filesystem.data}/.version" 445 version=$(cat "$versionFile" 2>/dev/null || echo 0) 446 447 if [[ $version != ${cfg.package.version} ]]; then 448 ${lib.getExe' pythonEnv "pretalx-manage"} migrate 449 450 echo "${cfg.package.version}" > "$versionFile" 451 fi 452 ''; 453 serviceConfig = { 454 ExecStart = "${lib.getExe' pythonEnv "gunicorn"} --bind unix:/run/pretalx/pretalx.sock ${cfg.gunicorn.extraArgs} pretalx.wsgi"; 455 RuntimeDirectory = "pretalx"; 456 }; 457 }; 458 459 pretalx-periodic = lib.recursiveUpdate commonUnitConfig { 460 description = "pretalx periodic task runner"; 461 # every 15 minutes 462 startAt = [ "*:3,18,33,48" ]; 463 serviceConfig = { 464 Type = "oneshot"; 465 ExecStart = "${lib.getExe' pythonEnv "pretalx-manage"} runperiodic"; 466 }; 467 }; 468 469 pretalx-clear-sessions = lib.recursiveUpdate commonUnitConfig { 470 description = "pretalx session pruning"; 471 startAt = [ "monthly" ]; 472 serviceConfig = { 473 Type = "oneshot"; 474 ExecStart = "${lib.getExe' pythonEnv "pretalx-manage"} clearsessions"; 475 }; 476 }; 477 478 pretalx-worker = lib.mkIf cfg.celery.enable ( 479 lib.recursiveUpdate commonUnitConfig { 480 description = "pretalx asynchronous job runner"; 481 after = 482 [ 483 "network.target" 484 "redis-pretalx.service" 485 ] 486 ++ lib.optionals (cfg.settings.database.backend == "postgresql") [ 487 "postgresql.service" 488 ] 489 ++ lib.optionals (cfg.settings.database.backend == "mysql") [ 490 "mysql.service" 491 ]; 492 wantedBy = [ "multi-user.target" ]; 493 serviceConfig.ExecStart = "${lib.getExe' pythonEnv "celery"} -A pretalx.celery_app worker ${cfg.celery.extraArgs}"; 494 } 495 ); 496 497 nginx.serviceConfig.SupplementaryGroups = lib.mkIf cfg.nginx.enable [ "pretalx" ]; 498 }; 499 500 systemd.sockets.pretalx-web.socketConfig = { 501 ListenStream = "/run/pretalx/pretalx.sock"; 502 SocketUser = "nginx"; 503 }; 504 505 users = { 506 groups.${cfg.group} = { }; 507 users.${cfg.user} = { 508 isSystemUser = true; 509 inherit (cfg) group; 510 }; 511 }; 512 }; 513}