From 2b762856c95434bf968ae989a5f020ffc474d0a6 Mon Sep 17 00:00:00 2001 From: Quentin Smith Date: Wed, 2 Apr 2025 23:13:31 -0400 Subject: [PATCH 1/3] Declarative configuration for system settings --- flake.nix | 25 ++++-- ...efresh-users.nix => refresh-db-config.nix} | 4 +- pkgs/refresh_db_config.py | 90 +++++++++++++++++++ pkgs/refresh_users.py | 74 --------------- pkgs/shell.nix | 4 +- 5 files changed, 113 insertions(+), 84 deletions(-) rename pkgs/{refresh-users.nix => refresh-db-config.nix} (70%) create mode 100644 pkgs/refresh_db_config.py delete mode 100644 pkgs/refresh_users.py diff --git a/flake.nix b/flake.nix index ba65cd7..68ca45f 100644 --- a/flake.nix +++ b/flake.nix @@ -137,7 +137,7 @@ cluster invoke python - refresh-users + refresh-db-config gen-secret shell ; @@ -182,7 +182,7 @@ cluster = _self.callPackage ./pkgs/cluster.nix { }; invoke = _self.callPackage ./pkgs/invoke.nix { }; python = _self.callPackage ./pkgs/python.nix { }; - refresh-users = _self.callPackage ./pkgs/refresh-users.nix { }; + refresh-db-config = _self.callPackage ./pkgs/refresh-db-config.nix { }; gen-secret = _self.callPackage ./pkgs/gen-secret.nix { }; # Requires pip2nix overlay, which is managed by the flake. @@ -205,7 +205,9 @@ defaultUser = "inventree"; defaultGroup = defaultUser; configFile = pkgs.writeText "config.yaml" (builtins.toJSON cfg.config); - usersFile = pkgs.writeText "users.json" (builtins.toJSON cfg.users); + dbConfigFile = pkgs.writeText "users.json" (builtins.toJSON { + inherit (cfg) users systemSettings; + }); inventree = pkgs.inventree; serverBind = "${cfg.bindIp}:${toString cfg.bindPort}"; allowedHostsStr = concatStringsSep "," cfg.allowedHosts; @@ -366,10 +368,21 @@ ''; }; + systemSettings = mkOption { + type = types.attrsOf types.anything; + default = {}; + description = lib.mdDoc '' + System settings, see https://docs.inventree.org/en/stable/settings/global/ + and https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/common/setting/system.py + for details + ''; + }; + users = mkOption { default = { }; description = mdDoc '' Users which should be present on the InvenTree server + If specified, ALL OTHER USERS WILL BE DELETED ''; example = { admin = { @@ -468,9 +481,9 @@ find . -type f -exec install -Dm 644 "{}" "${cfg.config.static_root}/{}" \; popd - echo "Setting up users" - cat ${usersFile} | \ - ${inventree.refresh-users}/bin/inventree-refresh-users + echo "Setting up users and system settings" + cat ${dbConfigFile} | \ + ${inventree.refresh-db-config}/bin/inventree-refresh-db-config ''}"; ExecStart = '' ${inventree.server}/bin/inventree-server -b ${serverBind} diff --git a/pkgs/refresh-users.nix b/pkgs/refresh-db-config.nix similarity index 70% rename from pkgs/refresh-users.nix rename to pkgs/refresh-db-config.nix index 4c47391..0b0d414 100644 --- a/pkgs/refresh-users.nix +++ b/pkgs/refresh-db-config.nix @@ -6,11 +6,11 @@ }: let - refreshScript = writeScript "refresh_users.py" (builtins.readFile ./refresh_users.py); + refreshScript = writeScript "refresh_db_config.py" (builtins.readFile ./refresh_db_config.py); in writeShellApplication rec { - name = "inventree-refresh-users"; + name = "inventree-refresh-db-config"; runtimeInputs = [ pythonWithPackages src diff --git a/pkgs/refresh_db_config.py b/pkgs/refresh_db_config.py new file mode 100644 index 0000000..406b680 --- /dev/null +++ b/pkgs/refresh_db_config.py @@ -0,0 +1,90 @@ +import json +import os +import sys +# This is required to pickup InvenTree.settings for some reason +sys.path.append(os.getcwd()) + +import django +from django.db import transaction +from django.db.utils import IntegrityError + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'InvenTree.settings') +django.setup() + +from common.models import InvenTreeSetting +from django.contrib.auth import get_user_model + +def _get_db_config_data(): + if os.isatty(0): + print("No config data piped in, exiting") + exit(1) + _data = sys.stdin.read() + try: + data = json.loads(_data) + except json.decoder.JSONDecodeError: + print(f"Error parsing data json:\n{_data}") + exit(1) + for username, fields in data["users"].items(): + email = fields["email"] + password_file = fields["password_file"] + print( + f"Reading password file for {username} ({email}) " + f"from {password_file}") + with open(password_file, "r") as f: + # Strip leading/trailing whitespace from password + fields["password"] = f.read().strip() + return data + + +def _commit_db_config(data): + try: + with transaction.atomic(): + _commit_users(data["users"]) + _commit_system_settings(data["systemSettings"]) + + except IntegrityError: + print("integrity error") + + +def _commit_users(data): + if not data: + print("No users to configure") + return + + user_model = get_user_model() + print("Deleting all users") + all_users = user_model.objects.all() + print(all_users) + all_users.delete() + + for username, fields in data.items(): + password = fields["password"] + email = fields["email"] + is_superuser = fields.get("is_superuser", False) + # can we use kwargs to do this? + if is_superuser: + print(f"Creating superuser {username}") + new_user = user_model.objects.create_superuser( + username, email, password + ) + print(f"User {new_user} was created!") + else: + print(f"Creating regular user {username}") + new_user = user_model.objects.create_user( + username, email, password + ) + print(f"User {new_user} was created!") + + +def _commit_system_settings(settings): + for key, value in settings.items(): + InvenTreeSetting.set_setting(key, value) + print(f"Setting {key}={value} was set!") + +def main(): + data = _get_db_config_data() + _commit_db_config(data) + +if __name__ == "__main__": + main() + diff --git a/pkgs/refresh_users.py b/pkgs/refresh_users.py deleted file mode 100644 index 627cf98..0000000 --- a/pkgs/refresh_users.py +++ /dev/null @@ -1,74 +0,0 @@ -import json -import os -import sys -# This is required to pickup InvenTree.settings for some reason -sys.path.append(os.getcwd()) - -import django -from django.db import transaction -from django.db.utils import IntegrityError - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'InvenTree.settings') -django.setup() - -from django.contrib.auth import get_user_model - -def _get_user_data(): - if os.isatty(0): - print("No user data piped in, exiting") - exit(1) - _data = sys.stdin.read() - try: - data = json.loads(_data) - except json.decoder.JSONDecodeError: - print(f"Error parsing user data json:\n{_data}") - exit(1) - for username, fields in data.items(): - email = fields["email"] - password_file = fields["password_file"] - print( - f"Reading password file for {username} ({email}) " - f"from {password_file}") - with open(password_file, "r") as f: - # Strip leading/trailing whitespace from password - fields["password"] = f.read().strip() - return data - - -def _commit_users(data): - user_model = get_user_model() - try: - with transaction.atomic(): - print("Deleting all users") - all_users = user_model.objects.all() - print(all_users) - all_users.delete() - - for username, fields in data.items(): - password = fields["password"] - email = fields["email"] - is_superuser = fields.get("is_superuser", False) - # can we use kwargs to do this? - if is_superuser: - print(f"Creating superuser {username}") - new_user = user_model.objects.create_superuser( - username, email, password - ) - print(f"User {new_user} was created!") - else: - print(f"Creating regular user {username}") - new_user = user_model.objects.create_user( - username, email, password - ) - print(f"User {new_user} was created!") - except IntegrityError: - print("integrity error") - - -def main(): - data = _get_user_data() - _commit_users(data) - -if __name__ == "__main__": - main() - diff --git a/pkgs/shell.nix b/pkgs/shell.nix index c396278..66ed35f 100644 --- a/pkgs/shell.nix +++ b/pkgs/shell.nix @@ -6,7 +6,7 @@ gen-secret, python, invoke, - refresh-users, + refresh-db-config, yarn, yarn2nix, }: @@ -24,6 +24,6 @@ mkShell { gen-secret python invoke - refresh-users + refresh-db-config ]; } From c0c219f9587a5799b889392361ab729c098c4de5 Mon Sep 17 00:00:00 2001 From: Quentin Smith Date: Thu, 27 Nov 2025 03:15:43 -0500 Subject: [PATCH 2/3] Fix support for configuring a Unix socket listener --- flake.nix | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 68ca45f..a2bad5a 100644 --- a/flake.nix +++ b/flake.nix @@ -209,7 +209,6 @@ inherit (cfg) users systemSettings; }); inventree = pkgs.inventree; - serverBind = "${cfg.bindIp}:${toString cfg.bindPort}"; allowedHostsStr = concatStringsSep "," cfg.allowedHosts; # Pre-compute SystemdDirectories to create the directories if they do not exists. @@ -266,6 +265,15 @@ # ''; #}; + serverBind = mkOption { + type = types.str; + default = "${cfg.bindIp}:${toString cfg.bindPort}"; + example = "unix:/run/inventree/inventree.sock"; + description = '' + The address and port the server will bind to. + ''; + }; + bindIp = mkOption { type = types.str; default = "127.0.0.1"; @@ -486,7 +494,7 @@ ${inventree.refresh-db-config}/bin/inventree-refresh-db-config ''}"; ExecStart = '' - ${inventree.server}/bin/inventree-server -b ${serverBind} + ${inventree.server}/bin/inventree-server -b ${cfg.serverBind} ''; }; }; From eb30d937a6a62a32956167cdf90c24e710c51798 Mon Sep 17 00:00:00 2001 From: Quentin Smith Date: Mon, 1 Dec 2025 01:36:17 -0500 Subject: [PATCH 3/3] Set site_url and allowed_hosts via the config file --- flake.nix | 93 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/flake.nix b/flake.nix index a2bad5a..668fe85 100644 --- a/flake.nix +++ b/flake.nix @@ -204,12 +204,12 @@ settingsFormat = pkgs.formats.json { }; defaultUser = "inventree"; defaultGroup = defaultUser; - configFile = pkgs.writeText "config.yaml" (builtins.toJSON cfg.config); + configFormat = pkgs.formats.yaml {}; + configFile = configFormat.generate "config.yaml" cfg.config; dbConfigFile = pkgs.writeText "users.json" (builtins.toJSON { inherit (cfg) users systemSettings; }); inventree = pkgs.inventree; - allowedHostsStr = concatStringsSep "," cfg.allowedHosts; # Pre-compute SystemdDirectories to create the directories if they do not exists. singletonIfPrefix = prefix: str: optional (hasPrefix prefix str) (removePrefix prefix str); @@ -294,41 +294,6 @@ ''; }; - siteUrl = mkOption { - type = types.str; - default = ""; - example = "https://inventree.example.com"; - description = lib.mdDoc '' - The INVENTREE_SITE_URL option defines the base URL for the - InvenTree server. This is a critical setting, and it is required - for correct operation of the server. If not specified, the - server will attempt to determine the site URL automatically - - but this may not always be correct! - - The site URL is the URL that users will use to access the - InvenTree server. For example, if the server is accessible at - `https://inventree.example.com`, the site URL should be set to - `https://inventree.example.com`. Note that this is not - necessarily the same as the internal URL that the server is - running on - the internal URL will depend entirely on your - server configuration and may be obscured by a reverse proxy or - other such setup. - ''; - }; - - allowedHosts = mkOption { - type = types.listOf types.str; - default = []; - example = ["*"]; - description = lib.mdDoc '' - List of allowed hosts used to connect to the server. - - If set, siteUrl is appended to this list at runtime. - If the list evaluates to empty at runtime, it defaults to allow - all (`["*"]`). - ''; - }; - dataDir = mkOption { type = types.str; default = "/var/lib/inventree"; @@ -368,7 +333,44 @@ }; config = mkOption { - type = types.attrs; + type = types.submodule { + freeformType = configFormat.type; + options = { + site_url = mkOption { + type = types.str; + default = ""; + example = "https://inventree.example.com"; + description = lib.mdDoc '' + The INVENTREE_SITE_URL option defines the base URL for the + InvenTree server. This is a critical setting, and it is required + for correct operation of the server. If not specified, the + server will attempt to determine the site URL automatically - + but this may not always be correct! + + The site URL is the URL that users will use to access the + InvenTree server. For example, if the server is accessible at + `https://inventree.example.com`, the site URL should be set to + `https://inventree.example.com`. Note that this is not + necessarily the same as the internal URL that the server is + running on - the internal URL will depend entirely on your + server configuration and may be obscured by a reverse proxy or + other such setup. + ''; + }; + allowed_hosts = mkOption { + type = types.listOf types.str; + default = []; + example = ["*"]; + description = lib.mdDoc '' + List of allowed hosts used to connect to the server. + + If set, site_url is appended to this list at runtime. + If the list evaluates to empty at runtime, it defaults to allow + all (`["*"]`). + ''; + }; + }; + }; default = { }; description = lib.mdDoc '' Config options, see https://docs.inventree.org/en/stable/start/config/ @@ -434,6 +436,17 @@ }; }; + imports = [ + (lib.mkRenamedOptionModule + [ "services" "inventree" "siteUrl" ] + [ "services" "inventree" "config" "site_url" ] + ) + (lib.mkRenamedOptionModule + [ "services" "inventree" "allowedHosts" ] + [ "services" "inventree" "config" "allowed_hosts" ] + ) + ]; + config = mkIf cfg.enable { nixpkgs.overlays = [ self.overlays.default ]; @@ -468,8 +481,6 @@ wantedBy = [ "multi-user.target" ]; environment = { INVENTREE_CONFIG_FILE = toString cfg.configPath; - INVENTREE_SITE_URL = cfg.siteUrl; - INVENTREE_ALLOWED_HOSTS = allowedHostsStr; }; serviceConfig = systemdDirectories // { User = defaultUser; @@ -503,8 +514,6 @@ wantedBy = [ "multi-user.target" ]; environment = { INVENTREE_CONFIG_FILE = toString cfg.configPath; - INVENTREE_SITE_URL = cfg.siteUrl; - INVENTREE_ALLOWED_HOSTS = allowedHostsStr; }; serviceConfig = systemdDirectories // { User = defaultUser;