Weblate generates an SSH wrapper with some preset options that uses the absolute path of the ssh binary internally. As the wrapper is only regenerated when the generator itself is changed, this absolute nix store path becomes unusable once ssh is updated and the path is garbage collected. As generating the wrappers is a quick operation, simply deleting the wrapper directory before service start ensures they are up to date.
447 lines
13 KiB
Nix
447 lines
13 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
|
|
let
|
|
cfg = config.services.weblate;
|
|
|
|
dataDir = "/var/lib/weblate";
|
|
cacheDir = "${dataDir}/cache";
|
|
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
|
|
|
|
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.configurePostgresql ''
|
|
DATABASES = {
|
|
"default": {
|
|
"ENGINE": "django.db.backends.postgresql",
|
|
"HOST": "/run/postgresql",
|
|
"NAME": "weblate",
|
|
"USER": "weblate",
|
|
}
|
|
}
|
|
''
|
|
+ lib.optionalString cfg.smtp.enable ''
|
|
EMAIL_HOST = "${cfg.smtp.host}"
|
|
EMAIL_USE_TLS = True
|
|
EMAIL_PORT = ${toString cfg.smtp.port}
|
|
SERVER_EMAIL = "${cfg.smtp.from}"
|
|
DEFAULT_FROM_EMAIL = "${cfg.smtp.from}"
|
|
''
|
|
+ lib.optionalString (cfg.smtp.enable && cfg.smtp.user != null) ''
|
|
ADMINS = (("Weblate Admin", "${cfg.smtp.user}"),)
|
|
EMAIL_HOST_USER = "${cfg.smtp.user}"
|
|
''
|
|
+ lib.optionalString (cfg.smtp.enable && cfg.smtp.passwordFile != null) ''
|
|
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;
|
|
};
|
|
|
|
# Packages needed at runtime
|
|
weblatePath = with pkgs; [
|
|
gitSVN
|
|
subversion
|
|
gettext
|
|
fontconfig
|
|
borgbackup
|
|
|
|
#optional
|
|
git-review
|
|
tesseract
|
|
licensee
|
|
mercurial
|
|
openssh
|
|
];
|
|
|
|
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;
|
|
};
|
|
|
|
configurePostgresql = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Whether to enable and configure a local PostgreSQL server by creating a user and database for weblate.
|
|
The default `settings` reference this database, if you disable this option you must provide a database URL in `extraConfig`.
|
|
'';
|
|
};
|
|
|
|
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";
|
|
|
|
from = lib.mkOption {
|
|
description = "The from address being used in sent emails.";
|
|
example = "weblate@example.com";
|
|
default = config.services.weblate.smtp.user;
|
|
defaultText = "config.services.weblate.smtp.user";
|
|
type = lib.types.str;
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
description = "SMTP login name.";
|
|
example = "weblate@example.org";
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
};
|
|
|
|
host = lib.mkOption {
|
|
description = "SMTP host used when sending emails to users.";
|
|
type = lib.types.str;
|
|
example = "127.0.0.1";
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
description = "SMTP port used when sending emails to users.";
|
|
type = lib.types.port;
|
|
default = 587;
|
|
example = 25;
|
|
};
|
|
|
|
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.nullOr lib.types.path;
|
|
default = null;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
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}/";
|
|
"/".proxyPass = "http://unix:///run/weblate.socket";
|
|
};
|
|
};
|
|
};
|
|
|
|
systemd.services.weblate-postgresql-setup = {
|
|
description = "Weblate PostgreSQL setup";
|
|
after = [ "postgresql.target" ];
|
|
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"
|
|
"redis-weblate.service"
|
|
];
|
|
requires = [
|
|
"weblate-postgresql-setup.service"
|
|
"redis-weblate.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-weblate.service"
|
|
"postgresql.target"
|
|
];
|
|
# 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;
|
|
# Weblate generates SSH wrappers with some preset options that use the
|
|
# absolute paths of the ssh and scp binaries internally.
|
|
# As the wrapper is only regenerated when the generator itself is changed,
|
|
# this absolute nix store path becomes unusable once ssh is updated and
|
|
# the path is garbage collected.
|
|
# As generating the wrappers is a quick operation, simply deleting the
|
|
# wrapper directory before service start ensures they are up to date.
|
|
preStart = ''
|
|
if [ -d "${cacheDir}/ssh" ]; then
|
|
rm -r "${cacheDir}/ssh"
|
|
fi
|
|
'';
|
|
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
|
|
''
|
|
${lib.getExe gunicorn} \
|
|
--name=weblate \
|
|
--bind='unix:///run/weblate.socket' \
|
|
--preload \
|
|
weblate.wsgi
|
|
'';
|
|
ExecReload = "${lib.getExe' pkgs.coreutils "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 = lib.mkIf cfg.configurePostgresql {
|
|
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 ];
|
|
|
|
}
|