nixpkgs/nixos/modules/services/web-apps/weblate.nix
2024-09-04 12:54:31 +02:00

397 lines
11 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.weblate;
dataDir = "/var/lib/weblate";
settingsDir = "${dataDir}/settings";
finalPackage = cfg.package.overridePythonAttrs (old: {
# We only support the PostgreSQL backend in this module
dependencies = old.dependencies ++ cfg.package.optional-dependencies.postgres;
# Use a settings module in dataDir, to avoid having to rebuild the package
# when user changes settings.
makeWrapperArgs = (old.makeWrapperArgs or [ ]) ++ [
"--set PYTHONPATH \"${settingsDir}\""
"--set DJANGO_SETTINGS_MODULE \"settings\""
];
});
inherit (finalPackage) python;
pythonEnv = python.buildEnv.override {
extraLibs = with python.pkgs; [
(toPythonModule finalPackage)
celery
];
};
# This extends and overrides the weblate/settings_example.py code found in upstream.
weblateConfig =
''
# This was autogenerated by the NixOS module.
SITE_TITLE = "Weblate"
SITE_DOMAIN = "${cfg.localDomain}"
# TLS terminates at the reverse proxy, but this setting controls how links to weblate are generated.
ENABLE_HTTPS = True
SESSION_COOKIE_SECURE = ENABLE_HTTPS
DATA_DIR = "${dataDir}"
CACHE_DIR = f"{DATA_DIR}/cache"
STATIC_ROOT = "${finalPackage.static}"
MEDIA_ROOT = "/var/lib/weblate/media"
COMPRESS_ROOT = "${finalPackage.static}"
COMPRESS_OFFLINE = True
DEBUG = False
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": "/run/postgresql",
"NAME": "weblate",
"USER": "weblate",
}
}
with open("${cfg.djangoSecretKeyFile}") as f:
SECRET_KEY = f.read().rstrip("\n")
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "unix://${config.services.redis.servers.weblate.unixSocket}",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"PASSWORD": None,
"CONNECTION_POOL_KWARGS": {},
},
"KEY_PREFIX": "weblate",
"TIMEOUT": 3600,
},
"avatar": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/lib/weblate/avatar-cache",
"TIMEOUT": 86400,
"OPTIONS": {"MAX_ENTRIES": 1000},
}
}
CELERY_TASK_ALWAYS_EAGER = False
CELERY_BROKER_URL = "redis+socket://${config.services.redis.servers.weblate.unixSocket}"
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
VCS_BACKENDS = ("weblate.vcs.git.GitRepository",)
SITE_URL = "https://{}".format(SITE_DOMAIN)
# WebAuthn
OTP_WEBAUTHN_RP_NAME = SITE_TITLE
OTP_WEBAUTHN_RP_ID = SITE_DOMAIN.split(":")[0]
OTP_WEBAUTHN_ALLOWED_ORIGINS = [SITE_URL]
''
+ lib.optionalString cfg.smtp.enable ''
ADMINS = (("Weblate Admin", "${cfg.smtp.user}"),)
EMAIL_HOST = "${cfg.smtp.host}"
EMAIL_USE_TLS = True
EMAIL_HOST_USER = "${cfg.smtp.user}"
SERVER_EMAIL = "${cfg.smtp.user}"
DEFAULT_FROM_EMAIL = "${cfg.smtp.user}"
EMAIL_PORT = 587
with open("${cfg.smtp.passwordFile}") as f:
EMAIL_HOST_PASSWORD = f.read().rstrip("\n")
''
+ cfg.extraConfig;
settings_py =
pkgs.runCommand "weblate_settings.py"
{
inherit weblateConfig;
passAsFile = [ "weblateConfig" ];
}
''
mkdir -p $out
cat \
${finalPackage}/${python.sitePackages}/weblate/settings_example.py \
$weblateConfigPath \
> $out/settings.py
'';
environment = {
PYTHONPATH = "${settingsDir}:${pythonEnv}/${python.sitePackages}/";
DJANGO_SETTINGS_MODULE = "settings";
# We run Weblate through gunicorn, so we can't utilise the env var set in the wrapper.
inherit (finalPackage) GI_TYPELIB_PATH;
};
weblatePath = with pkgs; [
gitSVN
borgbackup
#optional
git-review
tesseract
licensee
mercurial
];
in
{
options = {
services.weblate = {
enable = lib.mkEnableOption "Weblate service";
package = lib.mkPackageOption pkgs "weblate" { };
localDomain = lib.mkOption {
description = "The domain name serving your Weblate instance.";
example = "weblate.example.org";
type = lib.types.str;
};
djangoSecretKeyFile = lib.mkOption {
description = ''
Location of the Django secret key.
This should be a path pointing to a file with secure permissions (not /nix/store).
Can be generated with `weblate-generate-secret-key` which is available as the `weblate` user.
'';
type = lib.types.path;
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Text to append to `settings.py` Weblate configuration file.
'';
};
smtp = {
enable = lib.mkEnableOption "Weblate SMTP support";
user = lib.mkOption {
description = "SMTP login name.";
example = "weblate@example.org";
type = lib.types.str;
};
host = lib.mkOption {
description = "SMTP host used when sending emails to users.";
type = lib.types.str;
example = "127.0.0.1";
};
passwordFile = lib.mkOption {
description = ''
Location of a file containing the SMTP password.
This should be a path pointing to a file with secure permissions (not /nix/store).
'';
type = lib.types.path;
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.tmpfiles.rules = [ "L+ ${settingsDir} - - - - ${settings_py}" ];
services.nginx = {
enable = true;
virtualHosts."${cfg.localDomain}" = {
forceSSL = true;
enableACME = true;
locations = {
"= /favicon.ico".alias = "${finalPackage}/${python.sitePackages}/weblate/static/favicon.ico";
"/static/".alias = "${finalPackage.static}/";
"/media/".alias = "/var/lib/weblate/media/";
"/".proxyPass = "http://unix:///run/weblate.socket";
};
};
};
systemd.services.weblate-postgresql-setup = {
description = "Weblate PostgreSQL setup";
after = [ "postgresql.service" ];
serviceConfig = {
Type = "oneshot";
User = "postgres";
Group = "postgres";
ExecStart = ''
${config.services.postgresql.package}/bin/psql weblate -c "CREATE EXTENSION IF NOT EXISTS pg_trgm"
'';
};
};
systemd.services.weblate-migrate = {
description = "Weblate migration";
after = [ "weblate-postgresql-setup.service" ];
requires = [ "weblate-postgresql-setup.service" ];
# We want this to be active on boot, not just on socket activation
wantedBy = [ "multi-user.target" ];
inherit environment;
path = weblatePath;
serviceConfig = {
Type = "oneshot";
StateDirectory = "weblate";
User = "weblate";
Group = "weblate";
ExecStart = "${finalPackage}/bin/weblate migrate --noinput";
};
};
systemd.services.weblate-celery = {
description = "Weblate Celery";
after = [
"network.target"
"redis.service"
"postgresql.service"
];
# We want this to be active on boot, not just on socket activation
wantedBy = [ "multi-user.target" ];
environment = environment // {
CELERY_WORKER_RUNNING = "1";
};
path = weblatePath;
# Recommendations from:
# https://github.com/WeblateOrg/weblate/blob/main/weblate/examples/celery-weblate.service
serviceConfig =
let
# We have to push %n through systemd's replacement, therefore %%n.
pidFile = "/run/celery/weblate-%%n.pid";
nodes = "celery notify memory backup translate";
cmd = verb: ''
${pythonEnv}/bin/celery multi ${verb} \
${nodes} \
-A "weblate.utils" \
--pidfile=${pidFile} \
--logfile=/var/log/celery/weblate-%%n%%I.log \
--loglevel=DEBUG \
--beat:celery \
--queues:celery=celery \
--prefetch-multiplier:celery=4 \
--queues:notify=notify \
--prefetch-multiplier:notify=10 \
--queues:memory=memory \
--prefetch-multiplier:memory=10 \
--queues:translate=translate \
--prefetch-multiplier:translate=4 \
--concurrency:backup=1 \
--queues:backup=backup \
--prefetch-multiplier:backup=2
'';
in
{
Type = "forking";
User = "weblate";
Group = "weblate";
WorkingDirectory = "${finalPackage}/${python.sitePackages}/weblate/";
RuntimeDirectory = "celery";
RuntimeDirectoryPreserve = "restart";
LogsDirectory = "celery";
ExecStart = cmd "start";
ExecReload = cmd "restart";
ExecStop = ''
${pythonEnv}/bin/celery multi stopwait \
${nodes} \
--pidfile=${pidFile}
'';
Restart = "always";
};
};
systemd.services.weblate = {
description = "Weblate Gunicorn app";
after = [
"network.target"
"weblate-migrate.service"
"weblate-celery.service"
];
requires = [
"weblate-migrate.service"
"weblate-celery.service"
"weblate.socket"
];
inherit environment;
path = weblatePath;
serviceConfig = {
Type = "notify";
NotifyAccess = "all";
ExecStart =
let
gunicorn = python.pkgs.gunicorn.overridePythonAttrs (old: {
# Allows Gunicorn to set a meaningful process name
dependencies = (old.dependencies or [ ]) ++ old.optional-dependencies.setproctitle;
});
in
''
${gunicorn}/bin/gunicorn \
--name=weblate \
--bind='unix:///run/weblate.socket' \
weblate.wsgi
'';
ExecReload = "kill -s HUP $MAINPID";
KillMode = "mixed";
PrivateTmp = true;
WorkingDirectory = dataDir;
StateDirectory = "weblate";
RuntimeDirectory = "weblate";
User = "weblate";
Group = "weblate";
};
};
systemd.sockets.weblate = {
before = [ "nginx.service" ];
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = "/run/weblate.socket";
SocketUser = "weblate";
SocketGroup = "weblate";
SocketMode = "770";
};
};
services.redis.servers.weblate = {
enable = true;
user = "weblate";
unixSocket = "/run/redis-weblate/redis.sock";
unixSocketPerm = 770;
};
services.postgresql = {
enable = true;
ensureUsers = [
{
name = "weblate";
ensureDBOwnership = true;
}
];
ensureDatabases = [ "weblate" ];
};
users.users.weblate = {
isSystemUser = true;
group = "weblate";
packages = [ finalPackage ] ++ weblatePath;
};
users.groups.weblate.members = [ config.services.nginx.user ];
};
meta.maintainers = with lib.maintainers; [ erictapen ];
}