{ lib, config, pkgs, ... }: let inherit (lib) mkEnableOption mkOption types mkIf ; configFormat = pkgs.formats.json { }; cfg = config.services.inventree; pkg = cfg.package; configFile = "${cfg.dataDir}/config.json"; inventree-invoke = pkgs.writeShellApplication { name = "inventree-invoke"; text = '' export INVENTREE_CONFIG_FILE=${configFile} export INVENTREE_SECRET_KEY_FILE=${cfg.secretKeyFile} export PYTHONPATH=${pkg.pythonPath} exec -a "$0" ${pkgs.python3Packages.invoke}/bin/invoke -r ${cfg.package}/opt/inventree "$@" ''; }; in { options.services.inventree = { enable = mkEnableOption "InvenTree parts manager"; package = lib.mkOption { type = types.package; default = pkgs.inventree; description = '' InvenTree package to use ''; }; hostName = mkOption { type = types.str; description = "FQDN for the InvenTree instance."; }; dataDir = mkOption { type = types.path; default = "/var/lib/inventree"; example = "/var/lib/inventree"; description = '' The default path for all inventree data. ''; }; secretKeyFile = mkOption { type = types.path; default = "${cfg.dataDir}/secret_key.txt"; description = '' Path to a file containing the secret key ''; }; config = mkOption { type = types.submodule ({ freeformType = configFormat.type; options = { adminUser = mkOption { type = types.str; default = "admin"; }; adminPasswordFile = mkOption { type = types.path; description = "Path to password file for user `admin`"; }; site_url = mkOption { type = types.str; default = "https://${cfg.hostName}"; }; static_root = mkOption { type = types.path; default = "${cfg.dataDir}/static"; description = '' Static file storage ''; }; media_root = mkOption { type = types.path; default = "${cfg.dataDir}/media_root"; description = "Media root directory"; }; backup_dir = mkOption { type = types.path; default = "${cfg.dataDir}/backups"; description = "Backup directory"; }; oidc_private_key_file = mkOption { type = types.path; default = "${cfg.dataDir}/oidc.key"; }; }; }); default = { }; description = '' Config options, see https://docs.inventree.org/en/stable/start/config/ for details ''; }; serverStartTimeout = mkOption { type = types.str; default = "10min"; description = '' TimeoutStartSec for the server systemd service. See https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#TimeoutStartSec= for more details ''; }; serverStopTimeout = mkOption { type = types.str; default = "5min"; description = '' TimeoutStopSec for the server systemd service. See https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#TimeoutStopSec= for more details ''; }; }; config = mkIf cfg.enable { environment.systemPackages = [ inventree-invoke ]; systemd.tmpfiles.rules = ( map (dir: "d ${dir} 0755 inventree inventree") [ "${cfg.dataDir}" "${cfg.dataDir}/static" "${cfg.dataDir}/media_root" "${cfg.dataDir}/backups" ] ); services.inventree.config = { plugins_enabled = false; plugin_file = "${cfg.dataDir}/plugins.txt"; plugin_dir = "${cfg.dataDir}/plugins"; database = { ENGINE = "postgresql"; NAME = "inventree"; HOST = "/run/postgresql"; }; }; services.postgresql = { enable = true; ensureDatabases = [ "inventree" ]; ensureUsers = [ { name = "inventree"; ensureDBOwnership = true; } ]; }; users.users.inventree = { group = "inventree"; isSystemUser = true; description = "InvenTree daemon user"; }; users.groups.inventree = { }; services.nginx.enable = true; services.nginx.virtualHosts.${cfg.hostName} = { locations = let unixPath = config.systemd.sockets.inventree-gunicorn.socketConfig.ListenStream; in { "/" = { extraConfig = '' client_max_body_size 100M; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ''; proxyPass = "http://unix:${unixPath}"; }; "/static/" = { alias = "${cfg.config.static_root}/"; extraConfig = '' expires 30d; ''; }; "/media/" = { alias = "${cfg.config.media_root}/"; extraConfig = '' auth_request /auth; ''; }; "/auth" = { extraConfig = '' internal; ''; proxyPass = "http://unix:${unixPath}:/auth/"; }; }; }; systemd.targets.inventree = { description = "Target for all InvenTree services"; wantedBy = [ "multi-user.target" ]; wants = [ "network-online.target" ]; after = [ "network-online.target" ]; }; systemd.services.inventree-config = { description = "Inventree config generation"; wantedBy = [ "inventree.target" ]; partOf = [ "inventree.target" ]; before = [ "inventree-static.service" "inventree-gunicorn.service" "inventree-qcluster.service" ]; serviceConfig = { # User = "root"; # Group = "root"; User = "inventree"; Group = "inventree"; Type = "oneshot"; RemainAfterExit = true; PrivateTmp = true; }; environment = { INVENTREE_CONFIG_FILE = configFile; INVENTREE_SECRET_KEY_FILE = cfg.secretKeyFile; INVENTREE_AUTO_UPDATE = "1"; INVENTREE_PLUGINS_ENABLED = "1"; INVENTREE_PLUGIN_NOINSTALL = "1"; INVENTREE_STATIC_ROOT = cfg.config.static_root; INVENTREE_MEDIA_ROOT = cfg.config.media_root; INVENTREE_BACKUP_DIR = cfg.config.backup_dir; INVENTREE_OIDC_PRIVATE_KEY_FILE = cfg.config.oidc_private_key_file; INVENTREE_DB_ENGINE = cfg.config.database.ENGINE; INVENTREE_DB_NAME = cfg.config.database.NAME; INVENTREE_DB_HOST = cfg.config.database.HOST; INVENTREE_DB_USER = "inventree"; INVENTREE_ADMIN_USER = cfg.config.adminUser; INVENTREE_ADMIN_PASSWORD_FILE = cfg.config.adminPasswordFile; INVENTREE_SITE_URL = cfg.config.site_url; PYTHONPATH = pkg.pythonPath; }; script = '' set -euo pipefail umask u=rwx,g=,o= # chown inventree:inventree ${configFile} ${pkg}/opt/inventree/src/backend/InvenTree/manage.py migrate ''; }; systemd.services.inventree-static = { description = "InvenTree static migration"; wantedBy = [ "inventree.target" ]; partOf = [ "inventree.target" ]; before = [ "inventree-gunicorn.service" ]; environment = { INVENTREE_CONFIG_FILE = configFile; INVENTREE_SECRET_KEY_FILE = cfg.secretKeyFile; INVENTREE_AUTO_UPDATE = "1"; INVENTREE_PLUGINS_ENABLED = "1"; INVENTREE_PLUGIN_NOINSTALL = "1"; INVENTREE_STATIC_ROOT = cfg.config.static_root; INVENTREE_MEDIA_ROOT = cfg.config.media_root; INVENTREE_BACKUP_DIR = cfg.config.backup_dir; INVENTREE_OIDC_PRIVATE_KEY_FILE = cfg.config.oidc_private_key_file; INVENTREE_DB_ENGINE = cfg.config.database.ENGINE; INVENTREE_DB_NAME = cfg.config.database.NAME; INVENTREE_DB_HOST = cfg.config.database.HOST; INVENTREE_DB_USER = "inventree"; INVENTREE_ADMIN_USER = cfg.config.adminUser; INVENTREE_ADMIN_PASSWORD_FILE = cfg.config.adminPasswordFile; INVENTREE_SITE_URL = cfg.config.site_url; PYTHONPATH = pkg.pythonPath; }; serviceConfig = { User = "inventree"; Group = "inventree"; StateDirectory = "inventree"; #RuntimeDirectory = "inventree"; PrivateTmp = true; ExecStart = '' ${pkg}/opt/inventree/src/backend/InvenTree/manage.py collectstatic --no-input ''; }; }; systemd.services.inventree-gunicorn = { description = "InvenTree Gunicorn server"; requiredBy = [ "inventree.target" ]; partOf = [ "inventree.target" ]; #wantedBy = [ "inventree.target" ]; environment = { INVENTREE_CONFIG_FILE = configFile; INVENTREE_SECRET_KEY_FILE = cfg.secretKeyFile; INVENTREE_AUTO_UPDATE = "1"; INVENTREE_PLUGINS_ENABLED = "1"; INVENTREE_PLUGIN_NOINSTALL = "1"; INVENTREE_STATIC_ROOT = cfg.config.static_root; INVENTREE_MEDIA_ROOT = cfg.config.media_root; INVENTREE_BACKUP_DIR = cfg.config.backup_dir; INVENTREE_OIDC_PRIVATE_KEY_FILE = cfg.config.oidc_private_key_file; INVENTREE_DB_ENGINE = cfg.config.database.ENGINE; INVENTREE_DB_NAME = cfg.config.database.NAME; INVENTREE_DB_HOST = cfg.config.database.HOST; INVENTREE_DB_USER = "inventree"; INVENTREE_ADMIN_USER = cfg.config.adminUser; INVENTREE_ADMIN_PASSWORD_FILE = cfg.config.adminPasswordFile; INVENTREE_SITE_URL = cfg.config.site_url; PYTHONPATH = pkg.pythonPath; }; serviceConfig = { User = "inventree"; Group = "inventree"; StateDirectory = "inventree"; #RuntimeDirectory = "inventree"; PrivateTmp = true; ExecStart = '' ${pkg.gunicorn}/bin/gunicorn InvenTree.wsgi \ --pythonpath ${pkg}/opt/inventree/src/backend/InvenTree ''; }; }; systemd.sockets.inventree-gunicorn = { wantedBy = [ "sockets.target" ]; partOf = [ "inventree.target" ]; socketConfig.ListenStream = "/run/inventree/gunicorn.socket"; }; systemd.services.inventree-qcluster = { description = "InvenTree qcluster server"; requiredBy = [ "inventree.target" ]; wantedBy = [ "inventree.target" ]; partOf = [ "inventree.target" ]; environment = { INVENTREE_CONFIG_FILE = configFile; INVENTREE_SECRET_KEY_FILE = cfg.secretKeyFile; INVENTREE_AUTO_UPDATE = "1"; INVENTREE_PLUGINS_ENABLED = "0"; INVENTREE_PLUGIN_NOINSTALL = "1"; INVENTREE_STATIC_ROOT = cfg.config.static_root; INVENTREE_MEDIA_ROOT = cfg.config.media_root; INVENTREE_BACKUP_DIR = cfg.config.backup_dir; INVENTREE_OIDC_PRIVATE_KEY_FILE = cfg.config.oidc_private_key_file; INVENTREE_DB_ENGINE = cfg.config.database.ENGINE; INVENTREE_DB_NAME = cfg.config.database.NAME; INVENTREE_DB_HOST = cfg.config.database.HOST; INVENTREE_DB_USER = "inventree"; INVENTREE_ADMIN_USER = cfg.config.adminUser; INVENTREE_ADMIN_PASSWORD_FILE = cfg.config.adminPasswordFile; INVENTREE_SITE_URL = cfg.config.site_url; PYTHONPATH = pkg.pythonPath; }; serviceConfig = { User = "inventree"; Group = "inventree"; StateDirectory = "inventree"; #RuntimeDirectory = "inventree"; PrivateTmp = true; ExecStart = '' ${pkg}/opt/inventree/src/backend/InvenTree/manage.py qcluster ''; }; }; }; }