mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-11-14 11:44:19 +01:00
This avoids restarting the postgresql server, when only ensureDatabases or ensureUsers have been changed. It will also allow to properly wait for recovery to finish later. To wait for "postgresql is ready" in other services, we now provide a postgresql.target. Resolves #400018 Co-authored-by: Marcel <me@m4rc3l.de>
430 lines
13 KiB
Nix
430 lines
13 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
|
|
|
|
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 = ${builtins.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;
|
|
};
|
|
|
|
weblatePath = with pkgs; [
|
|
gitSVN
|
|
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}/";
|
|
"/media/".alias = "/var/lib/weblate/media/";
|
|
"/".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;
|
|
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 = 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 ];
|
|
|
|
}
|