at 25.11-pre 13 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.weblate; 10 11 dataDir = "/var/lib/weblate"; 12 settingsDir = "${dataDir}/settings"; 13 14 finalPackage = cfg.package.overridePythonAttrs (old: { 15 # We only support the PostgreSQL backend in this module 16 dependencies = old.dependencies ++ cfg.package.optional-dependencies.postgres; 17 # Use a settings module in dataDir, to avoid having to rebuild the package 18 # when user changes settings. 19 makeWrapperArgs = (old.makeWrapperArgs or [ ]) ++ [ 20 "--set PYTHONPATH \"${settingsDir}\"" 21 "--set DJANGO_SETTINGS_MODULE \"settings\"" 22 ]; 23 }); 24 inherit (finalPackage) python; 25 26 pythonEnv = python.buildEnv.override { 27 extraLibs = with python.pkgs; [ 28 (toPythonModule finalPackage) 29 celery 30 ]; 31 }; 32 33 # This extends and overrides the weblate/settings_example.py code found in upstream. 34 weblateConfig = 35 '' 36 # This was autogenerated by the NixOS module. 37 38 SITE_TITLE = "Weblate" 39 SITE_DOMAIN = "${cfg.localDomain}" 40 # TLS terminates at the reverse proxy, but this setting controls how links to weblate are generated. 41 ENABLE_HTTPS = True 42 SESSION_COOKIE_SECURE = ENABLE_HTTPS 43 DATA_DIR = "${dataDir}" 44 CACHE_DIR = f"{DATA_DIR}/cache" 45 STATIC_ROOT = "${finalPackage.static}" 46 MEDIA_ROOT = "/var/lib/weblate/media" 47 COMPRESS_ROOT = "${finalPackage.static}" 48 COMPRESS_OFFLINE = True 49 DEBUG = False 50 51 with open("${cfg.djangoSecretKeyFile}") as f: 52 SECRET_KEY = f.read().rstrip("\n") 53 54 CACHES = { 55 "default": { 56 "BACKEND": "django_redis.cache.RedisCache", 57 "LOCATION": "unix://${config.services.redis.servers.weblate.unixSocket}", 58 "OPTIONS": { 59 "CLIENT_CLASS": "django_redis.client.DefaultClient", 60 "PASSWORD": None, 61 "CONNECTION_POOL_KWARGS": {}, 62 }, 63 "KEY_PREFIX": "weblate", 64 "TIMEOUT": 3600, 65 }, 66 "avatar": { 67 "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", 68 "LOCATION": "/var/lib/weblate/avatar-cache", 69 "TIMEOUT": 86400, 70 "OPTIONS": {"MAX_ENTRIES": 1000}, 71 } 72 } 73 74 CELERY_TASK_ALWAYS_EAGER = False 75 CELERY_BROKER_URL = "redis+socket://${config.services.redis.servers.weblate.unixSocket}" 76 CELERY_RESULT_BACKEND = CELERY_BROKER_URL 77 78 VCS_BACKENDS = ("weblate.vcs.git.GitRepository",) 79 80 SITE_URL = "https://{}".format(SITE_DOMAIN) 81 82 # WebAuthn 83 OTP_WEBAUTHN_RP_NAME = SITE_TITLE 84 OTP_WEBAUTHN_RP_ID = SITE_DOMAIN.split(":")[0] 85 OTP_WEBAUTHN_ALLOWED_ORIGINS = [SITE_URL] 86 '' 87 + lib.optionalString cfg.configurePostgresql '' 88 DATABASES = { 89 "default": { 90 "ENGINE": "django.db.backends.postgresql", 91 "HOST": "/run/postgresql", 92 "NAME": "weblate", 93 "USER": "weblate", 94 } 95 } 96 '' 97 + lib.optionalString cfg.smtp.enable '' 98 EMAIL_HOST = "${cfg.smtp.host}" 99 EMAIL_USE_TLS = True 100 EMAIL_PORT = ${builtins.toString cfg.smtp.port} 101 SERVER_EMAIL = "${cfg.smtp.from}" 102 DEFAULT_FROM_EMAIL = "${cfg.smtp.from}" 103 '' 104 + lib.optionalString (cfg.smtp.enable && cfg.smtp.user != null) '' 105 ADMINS = (("Weblate Admin", "${cfg.smtp.user}"),) 106 EMAIL_HOST_USER = "${cfg.smtp.user}" 107 '' 108 + lib.optionalString (cfg.smtp.enable && cfg.smtp.passwordFile != null) '' 109 with open("${cfg.smtp.passwordFile}") as f: 110 EMAIL_HOST_PASSWORD = f.read().rstrip("\n") 111 '' 112 + cfg.extraConfig; 113 settings_py = 114 pkgs.runCommand "weblate_settings.py" 115 { 116 inherit weblateConfig; 117 passAsFile = [ "weblateConfig" ]; 118 } 119 '' 120 mkdir -p $out 121 cat \ 122 ${finalPackage}/${python.sitePackages}/weblate/settings_example.py \ 123 $weblateConfigPath \ 124 > $out/settings.py 125 ''; 126 127 environment = { 128 PYTHONPATH = "${settingsDir}:${pythonEnv}/${python.sitePackages}/"; 129 DJANGO_SETTINGS_MODULE = "settings"; 130 # We run Weblate through gunicorn, so we can't utilise the env var set in the wrapper. 131 inherit (finalPackage) GI_TYPELIB_PATH; 132 }; 133 134 weblatePath = with pkgs; [ 135 gitSVN 136 borgbackup 137 138 #optional 139 git-review 140 tesseract 141 licensee 142 mercurial 143 openssh 144 ]; 145in 146{ 147 148 options = { 149 services.weblate = { 150 enable = lib.mkEnableOption "Weblate service"; 151 152 package = lib.mkPackageOption pkgs "weblate" { }; 153 154 localDomain = lib.mkOption { 155 description = "The domain name serving your Weblate instance."; 156 example = "weblate.example.org"; 157 type = lib.types.str; 158 }; 159 160 djangoSecretKeyFile = lib.mkOption { 161 description = '' 162 Location of the Django secret key. 163 164 This should be a path pointing to a file with secure permissions (not /nix/store). 165 166 Can be generated with `weblate-generate-secret-key` which is available as the `weblate` user. 167 ''; 168 type = lib.types.path; 169 }; 170 171 configurePostgresql = lib.mkOption { 172 type = lib.types.bool; 173 default = true; 174 description = '' 175 Whether to enable and configure a local PostgreSQL server by creating a user and database for weblate. 176 The default `settings` reference this database, if you disable this option you must provide a database URL in `extraConfig`. 177 ''; 178 }; 179 180 extraConfig = lib.mkOption { 181 type = lib.types.lines; 182 default = ""; 183 description = '' 184 Text to append to `settings.py` Weblate configuration file. 185 ''; 186 }; 187 188 smtp = { 189 enable = lib.mkEnableOption "Weblate SMTP support"; 190 191 from = lib.mkOption { 192 description = "The from address being used in sent emails."; 193 example = "weblate@example.com"; 194 default = config.services.weblate.smtp.user; 195 defaultText = "config.services.weblate.smtp.user"; 196 type = lib.types.str; 197 }; 198 199 user = lib.mkOption { 200 description = "SMTP login name."; 201 example = "weblate@example.org"; 202 type = lib.types.nullOr lib.types.str; 203 default = null; 204 }; 205 206 host = lib.mkOption { 207 description = "SMTP host used when sending emails to users."; 208 type = lib.types.str; 209 example = "127.0.0.1"; 210 }; 211 212 port = lib.mkOption { 213 description = "SMTP port used when sending emails to users."; 214 type = lib.types.port; 215 default = 587; 216 example = 25; 217 }; 218 219 passwordFile = lib.mkOption { 220 description = '' 221 Location of a file containing the SMTP password. 222 223 This should be a path pointing to a file with secure permissions (not /nix/store). 224 ''; 225 type = lib.types.nullOr lib.types.path; 226 default = null; 227 }; 228 }; 229 }; 230 }; 231 232 config = lib.mkIf cfg.enable { 233 234 systemd.tmpfiles.rules = [ "L+ ${settingsDir} - - - - ${settings_py}" ]; 235 236 services.nginx = { 237 enable = true; 238 virtualHosts."${cfg.localDomain}" = { 239 240 forceSSL = true; 241 enableACME = true; 242 243 locations = { 244 "= /favicon.ico".alias = "${finalPackage}/${python.sitePackages}/weblate/static/favicon.ico"; 245 "/static/".alias = "${finalPackage.static}/"; 246 "/media/".alias = "/var/lib/weblate/media/"; 247 "/".proxyPass = "http://unix:///run/weblate.socket"; 248 }; 249 }; 250 }; 251 252 systemd.services.weblate-postgresql-setup = { 253 description = "Weblate PostgreSQL setup"; 254 after = [ "postgresql.service" ]; 255 serviceConfig = { 256 Type = "oneshot"; 257 User = "postgres"; 258 Group = "postgres"; 259 ExecStart = '' 260 ${config.services.postgresql.package}/bin/psql weblate -c "CREATE EXTENSION IF NOT EXISTS pg_trgm" 261 ''; 262 }; 263 }; 264 265 systemd.services.weblate-migrate = { 266 description = "Weblate migration"; 267 after = [ 268 "weblate-postgresql-setup.service" 269 "redis-weblate.service" 270 ]; 271 requires = [ 272 "weblate-postgresql-setup.service" 273 "redis-weblate.service" 274 ]; 275 # We want this to be active on boot, not just on socket activation 276 wantedBy = [ "multi-user.target" ]; 277 inherit environment; 278 path = weblatePath; 279 serviceConfig = { 280 Type = "oneshot"; 281 StateDirectory = "weblate"; 282 User = "weblate"; 283 Group = "weblate"; 284 ExecStart = "${finalPackage}/bin/weblate migrate --noinput"; 285 }; 286 }; 287 288 systemd.services.weblate-celery = { 289 description = "Weblate Celery"; 290 after = [ 291 "network.target" 292 "redis-weblate.service" 293 "postgresql.service" 294 ]; 295 # We want this to be active on boot, not just on socket activation 296 wantedBy = [ "multi-user.target" ]; 297 environment = environment // { 298 CELERY_WORKER_RUNNING = "1"; 299 }; 300 path = weblatePath; 301 # Recommendations from: 302 # https://github.com/WeblateOrg/weblate/blob/main/weblate/examples/celery-weblate.service 303 serviceConfig = 304 let 305 # We have to push %n through systemd's replacement, therefore %%n. 306 pidFile = "/run/celery/weblate-%%n.pid"; 307 nodes = "celery notify memory backup translate"; 308 cmd = verb: '' 309 ${pythonEnv}/bin/celery multi ${verb} \ 310 ${nodes} \ 311 -A "weblate.utils" \ 312 --pidfile=${pidFile} \ 313 --logfile=/var/log/celery/weblate-%%n%%I.log \ 314 --loglevel=DEBUG \ 315 --beat:celery \ 316 --queues:celery=celery \ 317 --prefetch-multiplier:celery=4 \ 318 --queues:notify=notify \ 319 --prefetch-multiplier:notify=10 \ 320 --queues:memory=memory \ 321 --prefetch-multiplier:memory=10 \ 322 --queues:translate=translate \ 323 --prefetch-multiplier:translate=4 \ 324 --concurrency:backup=1 \ 325 --queues:backup=backup \ 326 --prefetch-multiplier:backup=2 327 ''; 328 in 329 { 330 Type = "forking"; 331 User = "weblate"; 332 Group = "weblate"; 333 WorkingDirectory = "${finalPackage}/${python.sitePackages}/weblate/"; 334 RuntimeDirectory = "celery"; 335 RuntimeDirectoryPreserve = "restart"; 336 LogsDirectory = "celery"; 337 ExecStart = cmd "start"; 338 ExecReload = cmd "restart"; 339 ExecStop = '' 340 ${pythonEnv}/bin/celery multi stopwait \ 341 ${nodes} \ 342 --pidfile=${pidFile} 343 ''; 344 Restart = "always"; 345 }; 346 }; 347 348 systemd.services.weblate = { 349 description = "Weblate Gunicorn app"; 350 after = [ 351 "network.target" 352 "weblate-migrate.service" 353 "weblate-celery.service" 354 ]; 355 requires = [ 356 "weblate-migrate.service" 357 "weblate-celery.service" 358 "weblate.socket" 359 ]; 360 inherit environment; 361 path = weblatePath; 362 serviceConfig = { 363 Type = "notify"; 364 NotifyAccess = "all"; 365 ExecStart = 366 let 367 gunicorn = python.pkgs.gunicorn.overridePythonAttrs (old: { 368 # Allows Gunicorn to set a meaningful process name 369 dependencies = (old.dependencies or [ ]) ++ old.optional-dependencies.setproctitle; 370 }); 371 in 372 '' 373 ${gunicorn}/bin/gunicorn \ 374 --name=weblate \ 375 --bind='unix:///run/weblate.socket' \ 376 weblate.wsgi 377 ''; 378 ExecReload = "kill -s HUP $MAINPID"; 379 KillMode = "mixed"; 380 PrivateTmp = true; 381 WorkingDirectory = dataDir; 382 StateDirectory = "weblate"; 383 RuntimeDirectory = "weblate"; 384 User = "weblate"; 385 Group = "weblate"; 386 }; 387 }; 388 389 systemd.sockets.weblate = { 390 before = [ "nginx.service" ]; 391 wantedBy = [ "sockets.target" ]; 392 socketConfig = { 393 ListenStream = "/run/weblate.socket"; 394 SocketUser = "weblate"; 395 SocketGroup = "weblate"; 396 SocketMode = "770"; 397 }; 398 }; 399 400 services.redis.servers.weblate = { 401 enable = true; 402 user = "weblate"; 403 unixSocket = "/run/redis-weblate/redis.sock"; 404 unixSocketPerm = 770; 405 }; 406 407 services.postgresql = lib.mkIf cfg.configurePostgresql { 408 enable = true; 409 ensureUsers = [ 410 { 411 name = "weblate"; 412 ensureDBOwnership = true; 413 } 414 ]; 415 ensureDatabases = [ "weblate" ]; 416 }; 417 418 users.users.weblate = { 419 isSystemUser = true; 420 group = "weblate"; 421 packages = [ finalPackage ] ++ weblatePath; 422 }; 423 424 users.groups.weblate.members = [ config.services.nginx.user ]; 425 }; 426 427 meta.maintainers = with lib.maintainers; [ erictapen ]; 428 429}