From 68feacc010dd85baf31a87ee0878e5686e21c851 Mon Sep 17 00:00:00 2001 From: eyjhb Date: Fri, 6 Dec 2024 22:32:10 +0100 Subject: [PATCH] wger: turned into a module --- machines/gerd/services/wger/default.nix | 222 +++---------- .../gerd/services/wger/wgerpkg/module.nix | 296 ++++++++++++++++++ 2 files changed, 332 insertions(+), 186 deletions(-) create mode 100644 machines/gerd/services/wger/wgerpkg/module.nix diff --git a/machines/gerd/services/wger/default.nix b/machines/gerd/services/wger/default.nix index 6a0e72f..26c0394 100644 --- a/machines/gerd/services/wger/default.nix +++ b/machines/gerd/services/wger/default.nix @@ -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 "; - 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 "; + }; + + # 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; diff --git a/machines/gerd/services/wger/wgerpkg/module.nix b/machines/gerd/services/wger/wgerpkg/module.nix new file mode 100644 index 0000000..9b72ed9 --- /dev/null +++ b/machines/gerd/services/wger/wgerpkg/module.nix @@ -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 "; + 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; + }; + }; + }; +}