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