Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

weblate: init at 5.6.2, add module #325541

Merged
merged 13 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1484,6 +1484,7 @@
./services/web-apps/trilium.nix
./services/web-apps/tt-rss.nix
./services/web-apps/vikunja.nix
./services/web-apps/weblate.nix
./services/web-apps/whitebophir.nix
./services/web-apps/wiki-js.nix
./services/web-apps/windmill.nix
Expand Down
388 changes: 388 additions & 0 deletions nixos/modules/services/web-apps/weblate.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,388 @@
{
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\""
];
erictapen marked this conversation as resolved.
Show resolved Hide resolved
});
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}/static"
MEDIA_ROOT = "/var/lib/weblate/media"
COMPRESS_ROOT = "${finalPackage.static}/compressor-cache"
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",
erictapen marked this conversation as resolved.
Show resolved Hide resolved
"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",)

''
+ 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;
erictapen marked this conversation as resolved.
Show resolved Hide resolved
};

weblatePath = with pkgs; [
gitSVN

#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}/static/";
"/static/CACHE/".alias = "${finalPackage.static}/compressor-cache/CACHE/";
"/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 ];

}
Loading