wger: turned into a module

This commit is contained in:
eyjhb 2024-12-06 22:32:10 +01:00
parent 3121b57181
commit 68feacc010
No known key found for this signature in database
GPG key ID: 609F508E3239F920
2 changed files with 332 additions and 186 deletions

View file

@ -1,191 +1,43 @@
{ config, pkgs, ... }:
{ config, ... }:
let
svc_domain = "wger.${config.mine.shared.settings.domain}";
port = 8000;
wger_user = "wger";
statedir = config.mine.zfsMounts."rpool/safe/svcs/wger";
wgerpkgs = pkgs.callPackage ./wgerpkg/default.nix {};
wger_settings = {
EMAIL_FROM = "wger Workout Manager <wger@${config.mine.shared.settings.domain}>";
ALLOW_REGISTRATION = true;
ALLOW_GUEST_USERS = true;
ALLOW_UPLOAD_VIDEOS = false;
MIN_ACCOUNT_AGE_TO_TRUST = 1;
EXERCISE_CACHE_TTL = 3600; # 1 hour
};
django_settings = rec {
# enable debug for now, otherwise it tries
# to create a CACHE folder/file in the CWD.
# and if I fix that, then static content no
# longer wants to load.
DEBUG = false;
DATABASES.default = {
ENGINE = "django.db.backends.postgresql";
NAME = "wger";
USER = "wger";
PASSWORD = "";
HOST = "/run/postgresql";
PORT = "";
};
ADMINS = [["admin" "admin@${config.mine.shared.settings.domain}"]];
MANAGERS = ADMINS;
TIME_ZONE = "Europe/Copenhagen";
SECRET_KEY = "$SECRET_KEY";
SITE_URL = "https://${svc_domain}";
MEDIA_ROOT = "${statedir}/media";
MEDIA_URL = "/media/";
# EMAIL
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend";
EMAIL_HOST = config.mine.shared.settings.mail.domain_smtp;
EMAIL_PORT = config.mine.shared.settings.mail.ports.submissions;
EMAIL_USE_SSL = true;
EMAIL_HOST_USER = "wger";
EMAIL_HOST_PASSWORD = "$EMAIL_HOST_PASSWORD";
EMAIL_FROM_ADDRESS = wger_settings.EMAIL_FROM;
EMAIL_PAGE_DOMAIN = SITE_URL;
# Cache - Redis
CACHES.default = {
BACKEND = "django_redis.cache.RedisCache";
LOCATION = "unix://${config.services.redis.servers.wger.unixSocket}";
TIMEOUT = 15 * 24 * 60 * 60; # 15 days
OPTIONS.CLIENT_CLASS = "django_redis.client.DefaultClient";
};
# setup allowed hosts
CSRF_TRUSTED_ORIGINS = [ "https://${svc_domain}" ];
ALLOWED_HOSTS = [ svc_domain ];
# disable recaptcha
RECAPTCHA_PUBLIC_KEY = "";
RECAPTCHA_PRIVATE_KEY = "";
USE_RECAPTCHA = false;
# does not work
STATIC_ROOT = "${wgerpkgs}/share/static";
COMPRESS_ROOT = STATIC_ROOT;
COMPRESS_OFFLINE = true;
};
wger_settings_file = pkgs.writeText "settings.json" (builtins.toJSON wger_settings);
django_settings_file = pkgs.writeText "settings.json" (builtins.toJSON django_settings);
settingsFile = pkgs.writeText "settings.py" ''
from wger.settings_global import *
import json
import os
with open("${django_settings_file}") as f:
for k, v in json.load(f).items():
if isinstance(v, str) and v.startswith("$"):
v = os.environ[v[1:]]
globals()[k] = v
with open("${wger_settings_file}") as f:
for k, v in json.load(f).items():
if isinstance(v, str) and v.startswith("$"):
v = os.environ[v[1:]]
WGER_SETTINGS[k] = v
'';
settingsFileDir = pkgs.writeTextDir "settings.py" (builtins.readFile settingsFile);
port = config.services.wger.port;
in {
imports = [
./wgerpkg/module.nix
];
# main service
systemd.services.wger = {
description = "wger fitness";
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" ];
script = let
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
gunicorn
(pkgs.python3Packages.callPackage ./wgerpkg/default.nix {})
]);
in ''
# initial setup
${wgerpkgs}/bin/wger migrate-db -s ${settingsFile} || true
# TODO: fix at some point
# ${wgerpkgs}/bin/wger load-fixtures -s ${settingsFile} || true
# run server
# ${wgerpkgs}/bin/wger start -s ${settingsFile}
PYTHONPATH="${pythonEnv}/${pkgs.python3.sitePackages}:${settingsFileDir}" ${pythonEnv}/bin/gunicorn wger.wsgi:application --reload --bind 127.0.0.1:${builtins.toString port}
'';
serviceConfig = {
EnvironmentFile = config.age.secrets.wger-env.path;
Restart = "on-failure";
RestartSec = "5s";
PrivateTmp = "yes";
User = "wger";
Group = "wger";
};
};
# periodic keep up-to-date
systemd.timers."wger-housekeeping" = {
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = "daily";
};
systemd.services."wger-housekeeping" = {
after = [ "wger.service" ];
requires = [ "wger.service" ];
script = ''
WGER_SETTINGS=${settingsFile} ${wgerpkgs}/bin/manage sync-exercises || true
WGER_SETTINGS=${settingsFile} ${wgerpkgs}/bin/manage download-exercise-images || true
WGER_SETTINGS=${settingsFile} ${wgerpkgs}/bin/manage download-exercise-videos || true
# WGER_SETTINGS=${settingsFile} ${wgerpkgs}/bin/manage sync-ingredients || true
${wgerpkgs}/bin/wger load-online-fixtures -s ${settingsFile} || true
WGER_SETTINGS=${settingsFile} ${wgerpkgs}/bin/manage exercises-health-check || true
'';
serviceConfig = {
EnvironmentFile = config.age.secrets.wger-env.path;
# Type = "oneshot";
User = "wger";
Group = "wger";
};
};
services.postgresql = {
ensureDatabases = [ wger_user ];
ensureUsers = [{
name = wger_user;
ensureDBOwnership = true;
}];
};
# setup redis
services.redis.servers.wger = {
services.wger = {
enable = true;
user = wger_user;
# appendOnly = true;
};
# setup users
users.users."${wger_user}"= {
uid = 738;
isSystemUser = true;
group = wger_user;
};
users.groups."${wger_user}" = {
gid = 738;
members = [ config.users.users.nginx.name ];
configureRedis = true;
configurePostgres = true;
dataDir = config.mine.zfsMounts."rpool/safe/svcs/wger";
# wger specific settings
wgerSettings = {
EMAIL_FROM = "wger Workout Manager <wger@${svc_domain}>";
};
# django specific settings
djangoSettings = rec {
# setup site stuff
SITE_URL = "https://${svc_domain}";
CSRF_TRUSTED_ORIGINS = [ "https://${svc_domain}" ];
ALLOWED_HOSTS = [ svc_domain ];
# setup email
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend";
EMAIL_HOST = config.mine.shared.settings.mail.domain_smtp;
EMAIL_PORT = config.mine.shared.settings.mail.ports.submissions;
EMAIL_USE_SSL = true;
EMAIL_HOST_USER = "wger";
EMAIL_HOST_PASSWORD = "$EMAIL_HOST_PASSWORD";
EMAIL_FROM_ADDRESS = config.services.wger.wgerSettings.EMAIL_FROM;
EMAIL_PAGE_DOMAIN = SITE_URL;
};
};
# nginx
@ -200,10 +52,8 @@ in {
proxyPass = "http://localhost:${builtins.toString port}";
};
# locations."/static".proxyPass = "http://localhost:${builtins.toString port}";
locations."/static".root = "${wgerpkgs}/share";
# locations."/media".proxyPass = "http://localhost:${builtins.toString port}";
locations."/media".root = "${statedir}";
locations."/static".root = "${config.services.wger.package}/share";
locations."/media".root = "${config.services.wger.dataDir}";
locations."/api".proxyPass = "http://localhost:${builtins.toString port}";
};
@ -214,7 +64,7 @@ in {
url = "https://${svc_domain}";
package = let
pkg = wgerpkgs;
pkg = config.services.wger.package;
in {
name = pkg.pname;
version = pkg.version;

View file

@ -0,0 +1,296 @@
{ config, pkgs, lib, ... }:
# TODO: when DEBUG = False, serving static/media does not work, not sure why
with lib;
let
cfg = config.services.wger;
defaultUser = "wger";
wgerpkgs = pkgs.callPackage ./default.nix {};
# generate settings files
settingsFormat = pkgs.formats.json {};
wger_settings_file = pkgs.writeText "settings.json" (builtins.toJSON cfg.wgerSettings);
django_settings_file = pkgs.writeText "settings.json" (builtins.toJSON cfg.djangoSettings);
settingsFile = pkgs.writeText "settings.py" ''
from wger.settings_global import *
import json
import os
with open("${django_settings_file}") as f:
for k, v in json.load(f).items():
if isinstance(v, str) and v.startswith("$"):
v = os.environ[v[1:]]
globals()[k] = v
with open("${wger_settings_file}") as f:
for k, v in json.load(f).items():
if isinstance(v, str) and v.startswith("$"):
v = os.environ[v[1:]]
WGER_SETTINGS[k] = v
'';
settingsFileDir = pkgs.writeTextDir "settings.py" (builtins.readFile settingsFile);
in
{
meta.maintainers = with maintainers; [ eyjhb ];
options.services.wger = {
enable = mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable Wger.
'';
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/wger";
description = "Directory to store the Wger data.";
};
mediaDir = mkOption {
type = types.str;
default = "${cfg.dataDir}/media";
defaultText = literalExpression ''"''${dataDir}/media"'';
description = "Directory to store the Wger media.";
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/var/lib/teeworlds/teeworlds.env";
description = ''
Environment file as defined in {manpage}`systemd.exec(5)`.
Secrets may be passed to the service without adding them to the world-readable
Nix store, by specifying placeholder variables as the option value in Nix and
setting these variables accordingly in the environment file.
```
# snippet of teeworlds-related config
services.teeworlds.settings.SECRET_KEY = "$SECRETS_KEY";
```
```
# content of the environment file
SECRETS_KEY=verysecretpassword
```
Note that this file needs to be available on the host on which
`wger` is running.
'';
};
address = mkOption {
type = types.str;
default = "localhost";
description = "Web interface address.";
};
port = mkOption {
type = types.port;
default = 28391;
description = "Web interface port.";
};
djangoSettings = mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
};
default = { };
};
wgerSettings = mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
};
default = { };
};
user = mkOption {
type = types.str;
default = defaultUser;
# TODO: fix this, it is because of the database thing
readOnly = true;
description = "User under which Wger runs.";
};
configureRedis = lib.mkOption {
type = lib.types.bool;
default = true;
};
configurePostgres = lib.mkOption {
type = lib.types.bool;
default = true;
};
package = mkPackageOption { wger = wgerpkgs; } "wger" { };
};
config = mkIf cfg.enable {
services.wger.wgerSettings = {
EMAIL_FROM = mkDefault "wger Workout Manager <wger@example.com>";
ALLOW_REGISTRATION = true;
ALLOW_GUEST_USERS = true;
ALLOW_UPLOAD_VIDEOS = false;
MIN_ACCOUNT_AGE_TO_TRUST = 1;
EXERCISE_CACHE_TTL = 3600; # 1 hour
};
services.wger.djangoSettings = rec {
DEBUG = false;
# configure database as postgresql or sqlite
DATABASES.default = if cfg.configurePostgres then {
ENGINE = "django.db.backends.postgresql";
NAME = "wger";
USER = "wger";
PASSWORD = "";
HOST = "/run/postgresql";
PORT = "";
} else {
ENGINE = "django.db.backends.sqlite3";
NAME = "${cfg.dataDir}/database.db";
USER = "";
PASSWORD = "";
HOST = "";
PORT = "";
};
SECRET_KEY = "$SECRET_KEY";
MEDIA_ROOT = cfg.mediaDir;
MEDIA_URL = "/media/";
# EMAIL
EMAIL_BACKEND = mkDefault "django.core.mail.backends.console.EmailBackend";
# Cache - Redis
CACHES.default = mkIf cfg.configureRedis {
BACKEND = "django_redis.cache.RedisCache";
LOCATION = "unix://${config.services.redis.servers.wger.unixSocket}";
TIMEOUT = 15 * 24 * 60 * 60; # 15 days
OPTIONS.CLIENT_CLASS = "django_redis.client.DefaultClient";
};
# setup allowed hosts
# CSRF_TRUSTED_ORIGINS = [ "https://${svc_domain}" ];
# ALLOWED_HOSTS = [ svc_domain ];
# disable recaptcha
RECAPTCHA_PUBLIC_KEY = "";
RECAPTCHA_PRIVATE_KEY = "";
USE_RECAPTCHA = false;
# does not work
STATIC_ROOT = "${cfg.package}/share/static";
COMPRESS_ROOT = STATIC_ROOT;
COMPRESS_OFFLINE = true;
};
# main service
systemd.services.wger = {
description = "wger fitness";
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" ];
script = let
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
gunicorn
# TODO: fix this, it should work with cfg.package
(pkgs.python3Packages.callPackage ./default.nix {})
]);
in ''
# initial setup
${cfg.package}/bin/wger migrate-db -s ${settingsFile} || true
# TODO: fix at some point
# ${cfg.package}/bin/wger load-fixtures -s ${settingsFile} || true
# run server
# ${cfg.package}/bin/wger start -s ${settingsFile}
PYTHONPATH="${pythonEnv}/${pkgs.python3.sitePackages}:${settingsFileDir}" ${pythonEnv}/bin/gunicorn wger.wsgi:application --reload --bind ${cfg.address}:${builtins.toString cfg.port}
'';
serviceConfig = {
EnvironmentFile = config.age.secrets.wger-env.path;
Restart = "on-failure";
RestartSec = "5s";
PrivateTmp = "yes";
User = cfg.user;
# TODO: fix this, maybe
Group = cfg.user;
};
};
# periodic keep up-to-date
systemd.timers."wger-housekeeping" = {
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = "daily";
};
systemd.services."wger-housekeeping" = {
after = [ "wger.service" ];
requires = [ "wger.service" ];
script = ''
WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage sync-exercises || true
WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage download-exercise-images || true
WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage download-exercise-videos || true
# WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage sync-ingredients || true
${cfg.package}/bin/wger load-online-fixtures -s ${settingsFile} || true
WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage exercises-health-check || true
'';
serviceConfig = {
EnvironmentFile = config.age.secrets.wger-env.path;
# Type = "oneshot";
User = cfg.user;
# TODO: fix this, maybe
Group = cfg.user;
};
};
# postgresql
services.postgresql = lib.mkIf cfg.configurePostgres {
ensureDatabases = [ cfg.user ];
ensureUsers = [{
name = cfg.user;
ensureDBOwnership = true;
}];
};
# redis
services.redis.servers.wger = lib.mkIf cfg.configureRedis {
enable = true;
user = cfg.user;
};
# setup user
users = optionalAttrs (cfg.user == defaultUser) {
users.${defaultUser} = {
group = defaultUser;
# TODO: fix this
# uid = config.ids.uids.paperless + 2;
uid = 738;
home = cfg.dataDir;
};
groups.${defaultUser} = {
# TODO: fix this
# gid = config.ids.gids.paperless + 2;
gid = 738;
};
};
};
}