Skip to content

Commit

Permalink
nixos/weblate: init module and test
Browse files Browse the repository at this point in the history
Co-authored-by: Taeer Bar-Yam <Radvendii@users.noreply.github.com>
  • Loading branch information
erictapen and Radvendii committed Aug 11, 2024
1 parent bdc0e7d commit d96e777
Show file tree
Hide file tree
Showing 4 changed files with 494 additions and 0 deletions.
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\""
];
});
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",
"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;
};

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

0 comments on commit d96e777

Please sign in to comment.