diff --git a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
index 6c8d881b15fc..797cb1f16939 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
@@ -229,6 +229,13 @@
services.prometheus.exporters.pve.
+
+
+ netbox,
+ infrastructure resource modeling (IRM) tool. Available as
+ services.netbox.
+
+
tetrd, share your
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index 9aba688cb9b1..351ef6448289 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -67,6 +67,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [prometheus-pve-exporter](https://github.com/prometheus-pve/prometheus-pve-exporter), a tool that exposes information from the Proxmox VE API for use by Prometheus. Available as [services.prometheus.exporters.pve](options.html#opt-services.prometheus.exporters.pve).
+- [netbox](https://github.com/netbox-community/netbox), infrastructure resource modeling (IRM) tool. Available as [services.netbox](options.html#opt-services.netbox.enable).
+
- [tetrd](https://tetrd.app), share your internet connection from your device to your PC and vice versa through a USB cable. Available at [services.tetrd](#opt-services.tetrd.enable).
- [agate](https://github.com/mbrubeck/agate), a very simple server for the Gemini hypertext protocol. Available as [services.agate](options.html#opt-services.agate.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index adeddbf139a3..a9ed8f251283 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1048,6 +1048,7 @@
./services/web-apps/mediawiki.nix
./services/web-apps/miniflux.nix
./services/web-apps/moodle.nix
+ ./services/web-apps/netbox.nix
./services/web-apps/nextcloud.nix
./services/web-apps/nexus.nix
./services/web-apps/node-red.nix
diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix
new file mode 100644
index 000000000000..a7d8bede74b4
--- /dev/null
+++ b/nixos/modules/services/web-apps/netbox.nix
@@ -0,0 +1,265 @@
+{ config, lib, pkgs, buildEnv, ... }:
+
+with lib;
+
+let
+ cfg = config.services.netbox;
+ staticDir = cfg.dataDir + "/static";
+ configFile = pkgs.writeTextFile {
+ name = "configuration.py";
+ text = ''
+ STATIC_ROOT = '${staticDir}'
+ ALLOWED_HOSTS = ['*']
+ DATABASE = {
+ 'NAME': 'netbox',
+ 'USER': 'netbox',
+ 'HOST': '/run/postgresql',
+ }
+
+ # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
+ # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
+ # to use two separate database IDs.
+ REDIS = {
+ 'tasks': {
+ 'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=0',
+ 'SSL': False,
+ },
+ 'caching': {
+ 'URL': 'unix://${config.services.redis.servers.netbox.unixSocket}?db=1',
+ 'SSL': False,
+ }
+ }
+
+ with open("${cfg.secretKeyFile}", "r") as file:
+ SECRET_KEY = file.readline()
+
+ ${optionalString cfg.enableLdap "REMOTE_AUTH_BACKEND = 'netbox.authentication.LDAPBackend'"}
+
+ ${cfg.extraConfig}
+ '';
+ };
+ pkg = (pkgs.netbox.overrideAttrs (old: {
+ installPhase = old.installPhase + ''
+ ln -s ${configFile} $out/opt/netbox/netbox/netbox/configuration.py
+ '' + optionalString cfg.enableLdap ''
+ ln -s ${ldapConfigPath} $out/opt/netbox/netbox/netbox/ldap_config.py
+ '';
+ })).override {
+ plugins = ps: ((cfg.plugins ps)
+ ++ optional cfg.enableLdap [ ps.django-auth-ldap ]);
+ };
+ netboxManageScript = with pkgs; (writeScriptBin "netbox-manage" ''
+ #!${stdenv.shell}
+ export PYTHONPATH=${pkg.pythonPath}
+ sudo -u netbox ${pkg}/bin/netbox "$@"
+ '');
+
+in {
+ options.services.netbox = {
+ enable = mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Enable Netbox.
+
+ This module requires a reverse proxy that serves /static separately.
+ See this example on how to configure this.
+ '';
+ };
+
+ listenAddress = mkOption {
+ type = types.str;
+ default = "[::1]";
+ description = ''
+ Address the server will listen on.
+ '';
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 8001;
+ description = ''
+ Port the server will listen on.
+ '';
+ };
+
+ plugins = mkOption {
+ type = types.functionTo (types.listOf types.package);
+ default = _: [];
+ defaultText = literalExpression ''
+ python3Packages: with python3Packages; [];
+ '';
+ description = ''
+ List of plugin packages to install.
+ '';
+ };
+
+ dataDir = mkOption {
+ type = types.str;
+ default = "/var/lib/netbox";
+ description = ''
+ Storage path of netbox.
+ '';
+ };
+
+ secretKeyFile = mkOption {
+ type = types.path;
+ description = ''
+ Path to a file containing the secret key.
+ '';
+ };
+
+ extraConfig = mkOption {
+ type = types.lines;
+ default = "";
+ description = ''
+ Additional lines of configuration appended to the configuration.py.
+ See the documentation for more possible options.
+ '';
+ };
+
+ enableLdap = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Enable LDAP-Authentication for Netbox.
+
+ This requires a configuration file being pass through ldapConfigPath.
+ '';
+ };
+
+ ldapConfigPath = mkOption {
+ type = types.path;
+ default = "";
+ description = ''
+ Path to the Configuration-File for LDAP-Authentification, will be loaded as ldap_config.py.
+ See the documentation for possible options.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ services.redis.servers.netbox.enable = true;
+
+ services.postgresql = {
+ enable = true;
+ ensureDatabases = [ "netbox" ];
+ ensureUsers = [
+ {
+ name = "netbox";
+ ensurePermissions = {
+ "DATABASE netbox" = "ALL PRIVILEGES";
+ };
+ }
+ ];
+ };
+
+ environment.systemPackages = [ netboxManageScript ];
+
+ systemd.targets.netbox = {
+ description = "Target for all NetBox services";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network-online.target" "redis-netbox.service" ];
+ };
+
+ systemd.services = let
+ defaultServiceConfig = {
+ WorkingDirectory = "${cfg.dataDir}";
+ User = "netbox";
+ Group = "netbox";
+ StateDirectory = "netbox";
+ StateDirectoryMode = "0750";
+ Restart = "on-failure";
+ };
+ in {
+ netbox-migration = {
+ description = "NetBox migrations";
+ wantedBy = [ "netbox.target" ];
+
+ environment = {
+ PYTHONPATH = pkg.pythonPath;
+ };
+
+ serviceConfig = defaultServiceConfig // {
+ Type = "oneshot";
+ ExecStart = ''
+ ${pkg}/bin/netbox migrate
+ '';
+ };
+ };
+
+ netbox = {
+ description = "NetBox WSGI Service";
+ wantedBy = [ "netbox.target" ];
+ after = [ "netbox-migration.service" ];
+
+ preStart = ''
+ ${pkg}/bin/netbox trace_paths --no-input
+ ${pkg}/bin/netbox collectstatic --no-input
+ ${pkg}/bin/netbox remove_stale_contenttypes --no-input
+ '';
+
+ environment = {
+ PYTHONPATH = pkg.pythonPath;
+ };
+
+ serviceConfig = defaultServiceConfig // {
+ ExecStart = ''
+ ${pkgs.python3Packages.gunicorn}/bin/gunicorn netbox.wsgi \
+ --bind ${cfg.listenAddress}:${toString cfg.port} \
+ --pythonpath ${pkg}/opt/netbox/netbox
+ '';
+ };
+ };
+
+ netbox-rq = {
+ description = "NetBox Request Queue Worker";
+ wantedBy = [ "netbox.target" ];
+ after = [ "netbox.service" ];
+
+ environment = {
+ PYTHONPATH = pkg.pythonPath;
+ };
+
+ serviceConfig = defaultServiceConfig // {
+ ExecStart = ''
+ ${pkg}/bin/netbox rqworker high default low
+ '';
+ };
+ };
+
+ netbox-housekeeping = {
+ description = "NetBox housekeeping job";
+ after = [ "netbox.service" ];
+
+ environment = {
+ PYTHONPATH = pkg.pythonPath;
+ };
+
+ serviceConfig = defaultServiceConfig // {
+ Type = "oneshot";
+ ExecStart = ''
+ ${pkg}/bin/netbox housekeeping
+ '';
+ };
+ };
+ };
+
+ systemd.timers.netbox-housekeeping = {
+ description = "Run NetBox housekeeping job";
+ wantedBy = [ "timers.target" ];
+
+ timerConfig = {
+ OnCalendar = "daily";
+ };
+ };
+
+ users.users.netbox = {
+ home = "${cfg.dataDir}";
+ isSystemUser = true;
+ group = "netbox";
+ };
+ users.groups.netbox = {};
+ users.groups."${config.services.redis.servers.netbox.user}".members = [ "netbox" ];
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index cb6089983f8c..ffccb6b44660 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -341,6 +341,7 @@ in
networking.networkd = handleTest ./networking.nix { networkd = true; };
networking.scripted = handleTest ./networking.nix { networkd = false; };
specialisation = handleTest ./specialisation.nix {};
+ netbox = handleTest ./web-apps/netbox.nix {};
# TODO: put in networking.nix after the test becomes more complete
networkingProxy = handleTest ./networking-proxy.nix {};
nextcloud = handleTest ./nextcloud {};
diff --git a/nixos/tests/web-apps/netbox.nix b/nixos/tests/web-apps/netbox.nix
new file mode 100644
index 000000000000..95f24029ec92
--- /dev/null
+++ b/nixos/tests/web-apps/netbox.nix
@@ -0,0 +1,30 @@
+import ../make-test-python.nix ({ lib, pkgs, ... }: {
+ name = "netbox";
+
+ meta = with lib.maintainers; {
+ maintainers = [ n0emis ];
+ };
+
+ machine = { ... }: {
+ services.netbox = {
+ enable = true;
+ secretKeyFile = pkgs.writeText "secret" ''
+ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
+ '';
+ };
+ };
+
+ testScript = ''
+ machine.start()
+ machine.wait_for_unit("netbox.target")
+ machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
+
+ with subtest("Home screen loads"):
+ machine.succeed(
+ "curl -sSfL http://[::1]:8001 | grep 'Home | NetBox'"
+ )
+
+ with subtest("Staticfiles are generated"):
+ machine.succeed("test -e /var/lib/netbox/static/netbox.js")
+ '';
+})
diff --git a/pkgs/development/python-modules/social-auth-app-django/default.nix b/pkgs/development/python-modules/social-auth-app-django/default.nix
new file mode 100644
index 000000000000..bd38ef3a5a24
--- /dev/null
+++ b/pkgs/development/python-modules/social-auth-app-django/default.nix
@@ -0,0 +1,34 @@
+{ lib, buildPythonPackage, fetchFromGitHub, social-auth-core, django, python }:
+
+buildPythonPackage rec {
+ pname = "social-auth-app-django";
+ version = "5.0.0";
+
+ src = fetchFromGitHub {
+ owner = "python-social-auth";
+ repo = "social-app-django";
+ rev = version;
+ sha256 = "sha256-ONhdXxclHRpVtijpKEZlmGDhjid/jnTaPq6LQtjxCC4=";
+ };
+
+ propagatedBuildInputs = [
+ social-auth-core
+ ];
+
+ pythonImportsCheck = [ "social_django" ];
+
+ checkInputs = [
+ django
+ ];
+
+ checkPhase = ''
+ ${python.interpreter} -m django test --settings="tests.settings"
+ '';
+
+ meta = with lib; {
+ homepage = "https://github.com/python-social-auth/social-app-django";
+ description = "Python Social Auth - Application - Django";
+ license = licenses.bsd3;
+ maintainers = with maintainers; [ n0emis ];
+ };
+}
diff --git a/pkgs/development/python-modules/social-auth-core/default.nix b/pkgs/development/python-modules/social-auth-core/default.nix
new file mode 100644
index 000000000000..ede138218e01
--- /dev/null
+++ b/pkgs/development/python-modules/social-auth-core/default.nix
@@ -0,0 +1,63 @@
+{ lib
+, buildPythonPackage
+, fetchFromGitHub
+, requests
+, oauthlib
+, requests_oauthlib
+, pyjwt
+, cryptography
+, defusedxml
+, python3-openid
+, python-jose
+, python3-saml
+, pytestCheckHook
+, httpretty
+}:
+
+buildPythonPackage rec {
+ pname = "social-auth-core";
+ version = "4.2.0";
+
+ src = fetchFromGitHub {
+ owner = "python-social-auth";
+ repo = "social-core";
+ rev = version;
+ sha256 = "sha256-kaL6sfAyQlzxszCEbhW7sns/mcOv0U+QgplmUd6oegQ=";
+ };
+
+ # Disable checking the code coverage
+ prePatch = ''
+ substituteInPlace social_core/tests/requirements.txt \
+ --replace "coverage>=3.6" "" \
+ --replace "pytest-cov>=2.7.1" ""
+
+ substituteInPlace tox.ini \
+ --replace "{posargs:-v --cov=social_core}" "{posargs:-v}"
+ '';
+
+ propagatedBuildInputs = [
+ requests
+ oauthlib
+ requests_oauthlib
+ pyjwt
+ cryptography
+ defusedxml
+ python3-openid
+ python-jose
+ python3-saml
+ ];
+
+ checkInputs = [
+ pytestCheckHook
+ httpretty
+ ];
+
+ pythonImportsCheck = [ "social_core" ];
+
+ meta = with lib; {
+ homepage = "https://github.com/python-social-auth/social-core";
+ description = "Python Social Auth - Core";
+ license = licenses.bsd3;
+ maintainers = with maintainers; [ n0emis ];
+ };
+}
diff --git a/pkgs/servers/web-apps/netbox/config.patch b/pkgs/servers/web-apps/netbox/config.patch
new file mode 100644
index 000000000000..1adc0b537a4e
--- /dev/null
+++ b/pkgs/servers/web-apps/netbox/config.patch
@@ -0,0 +1,50 @@
+diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
+index d5a7bfaec..68754a8c5 100644
+--- a/netbox/netbox/settings.py
++++ b/netbox/netbox/settings.py
+@@ -222,6 +222,7 @@ TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
+ TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
+ TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
+ TASKS_REDIS_SKIP_TLS_VERIFY = TASKS_REDIS.get('INSECURE_SKIP_TLS_VERIFY', False)
++TASKS_REDIS_URL = TASKS_REDIS.get('URL')
+
+ # Caching
+ if 'caching' not in REDIS:
+@@ -236,11 +237,12 @@ CACHING_REDIS_SENTINELS = REDIS['caching'].get('SENTINELS', [])
+ CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'default')
+ CACHING_REDIS_PROTO = 'rediss' if REDIS['caching'].get('SSL', False) else 'redis'
+ CACHING_REDIS_SKIP_TLS_VERIFY = REDIS['caching'].get('INSECURE_SKIP_TLS_VERIFY', False)
++CACHING_REDIS_URL = REDIS['caching'].get('URL', f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}')
+
+ CACHES = {
+ 'default': {
+ 'BACKEND': 'django_redis.cache.RedisCache',
+- 'LOCATION': f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_HOST}:{CACHING_REDIS_PORT}/{CACHING_REDIS_DATABASE}',
++ 'LOCATION': CACHING_REDIS_URL,
+ 'OPTIONS': {
+ 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
+ 'PASSWORD': CACHING_REDIS_PASSWORD,
+@@ -383,7 +385,7 @@ USE_X_FORWARDED_HOST = True
+ X_FRAME_OPTIONS = 'SAMEORIGIN'
+
+ # Static files (CSS, JavaScript, Images)
+-STATIC_ROOT = BASE_DIR + '/static'
++STATIC_ROOT = getattr(configuration, 'STATIC_ROOT', os.path.join(BASE_DIR, 'static')).rstrip('/')
+ STATIC_URL = f'/{BASE_PATH}static/'
+ STATICFILES_DIRS = (
+ os.path.join(BASE_DIR, 'project-static', 'dist'),
+@@ -562,6 +564,14 @@ if TASKS_REDIS_USING_SENTINEL:
+ 'socket_connect_timeout': TASKS_REDIS_SENTINEL_TIMEOUT
+ },
+ }
++elif TASKS_REDIS_URL:
++ RQ_PARAMS = {
++ 'URL': TASKS_REDIS_URL,
++ 'PASSWORD': TASKS_REDIS_PASSWORD,
++ 'SSL': TASKS_REDIS_SSL,
++ 'SSL_CERT_REQS': None if TASKS_REDIS_SKIP_TLS_VERIFY else 'required',
++ 'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
++ }
+ else:
+ RQ_PARAMS = {
+ 'HOST': TASKS_REDIS_HOST,
diff --git a/pkgs/servers/web-apps/netbox/default.nix b/pkgs/servers/web-apps/netbox/default.nix
new file mode 100644
index 000000000000..a8abcf9d8573
--- /dev/null
+++ b/pkgs/servers/web-apps/netbox/default.nix
@@ -0,0 +1,117 @@
+{ lib
+, pkgs
+, fetchFromGitHub
+, nixosTests
+, python3
+
+, plugins ? ps: [] }:
+
+let
+ py = python3.override {
+ packageOverrides = self: super: {
+ django = super.django_3;
+ graphql-core = super.graphql-core.overridePythonAttrs (old: rec {
+ version = "3.1.7";
+ src = fetchFromGitHub {
+ owner = "graphql-python";
+ repo = old.pname;
+ rev = "v${version}";
+ sha256 = "1mwwh55qd5bcpvgy6pyliwn8jkmj4yk4d2pqb6mdkgqhdic2081l";
+ };
+ });
+ jsonschema = super.jsonschema.overridePythonAttrs (old: rec {
+ version = "3.2.0";
+ src = self.fetchPypi {
+ pname = old.pname;
+ inherit version;
+ sha256 = "c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a";
+ };
+ });
+ lxml = super.lxml.overridePythonAttrs (old: rec {
+ pname = "lxml";
+ version = "4.6.5";
+
+ src = self.fetchPypi {
+ inherit pname version;
+ sha256 = "6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca";
+ };
+ });
+ };
+ };
+
+ extraBuildInputs = plugins py.pkgs;
+in
+py.pkgs.buildPythonApplication rec {
+ pname = "netbox";
+ version = "3.1.10";
+
+ src = fetchFromGitHub {
+ owner = "netbox-community";
+ repo = pname;
+ rev = "v${version}";
+ sha256 = "sha256-qREq4FJHHTA9Vm6f9kSfiYqur2omFmdsoZ4OdaPFcpU=";
+ };
+
+ format = "other";
+
+ patches = [
+ # Allow setting the STATIC_ROOT from within the configuration and setting a custom redis URL
+ ./config.patch
+ ];
+
+ propagatedBuildInputs = with py.pkgs; [
+ django_3
+ django-cors-headers
+ django-debug-toolbar
+ django-filter
+ django-graphiql-debug-toolbar
+ django-mptt
+ django-pglocks
+ django-prometheus
+ django-redis
+ django-rq
+ django-tables2
+ django-taggit
+ django-timezone-field
+ djangorestframework
+ drf-yasg
+ graphene-django
+ jinja2
+ markdown
+ markdown-include
+ mkdocs-material
+ netaddr
+ pillow
+ psycopg2
+ pyyaml
+ social-auth-core
+ social-auth-app-django
+ svgwrite
+ tablib
+ jsonschema
+ ] ++ extraBuildInputs;
+
+ installPhase = ''
+ mkdir -p $out/opt/netbox
+ cp -r . $out/opt/netbox
+ chmod +x $out/opt/netbox/netbox/manage.py
+ makeWrapper $out/opt/netbox/netbox/manage.py $out/bin/netbox \
+ --prefix PYTHONPATH : "$PYTHONPATH"
+ '';
+
+ passthru = {
+ # PYTHONPATH of all dependencies used by the package
+ pythonPath = python3.pkgs.makePythonPath propagatedBuildInputs;
+
+ tests = {
+ inherit (nixosTests) netbox;
+ };
+ };
+
+ meta = with lib; {
+ homepage = "https://github.com/netbox-community/netbox";
+ description = "IP address management (IPAM) and data center infrastructure management (DCIM) tool";
+ license = licenses.asl20;
+ maintainers = with maintainers; [ n0emis raitobezarius ];
+ };
+ }
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 33c961448916..08479d754a1c 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -8266,6 +8266,8 @@ with pkgs;
netbootxyz-efi = callPackage ../tools/misc/netbootxyz-efi { };
+ netbox = callPackage ../servers/web-apps/netbox { };
+
netcat = libressl.nc;
netcat-gnu = callPackage ../tools/networking/netcat { };
diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix
index 57a1e39a5ac2..84dd3512e9de 100644
--- a/pkgs/top-level/python-packages.nix
+++ b/pkgs/top-level/python-packages.nix
@@ -9379,6 +9379,10 @@ in {
socketio-client = callPackage ../development/python-modules/socketio-client { };
+ social-auth-app-django = callPackage ../development/python-modules/social-auth-app-django { };
+
+ social-auth-core = callPackage ../development/python-modules/social-auth-core { };
+
socialscan = callPackage ../development/python-modules/socialscan { };
socid-extractor = callPackage ../development/python-modules/socid-extractor { };