diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index 46f324c3c0f6..28e4df793daf 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -235,6 +235,10 @@ In addition to numerous new and upgraded packages, this release has the followin - `services.openssh.ciphers` to `services.openssh.settings.Ciphers` - `services.openssh.gatewayPorts` to `services.openssh.settings.GatewayPorts` +- `netbox` was updated to 3.4. NixOS' `services.netbox.package` still defaults to 3.3 if `stateVersion` is earlier than 23.05. Please review upstream's [breaking changes](https://github.com/netbox-community/netbox/releases/tag/v3.4.0), and upgrade NetBox by changing `services.netbox.package`. Database migrations will be run automatically. + +- `services.netbox` now support RFC42-style options, through `services.netbox.settings`. + - `services.mastodon` gained a tootctl wrapped named `mastodon-tootctl` similar to `nextcloud-occ` which can be executed from any user and switches to the configured mastodon user with sudo and sources the environment variables. - DocBook option documentation, which has been deprecated since 22.11, will now cause a warning when documentation is built. Out-of-tree modules should migrate to using CommonMark documentation as outlined in [](#sec-option-declarations) to silence this warning. diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix index e028f16004ef..0ecb20e8c2c0 100644 --- a/nixos/modules/services/web-apps/netbox.nix +++ b/nixos/modules/services/web-apps/netbox.nix @@ -4,45 +4,17 @@ with lib; let cfg = config.services.netbox; + pythonFmt = pkgs.formats.pythonVars {}; staticDir = cfg.dataDir + "/static"; - configFile = pkgs.writeTextFile { - name = "configuration.py"; - text = '' - STATIC_ROOT = '${staticDir}' - MEDIA_ROOT = '${cfg.dataDir}/media' - REPORTS_ROOT = '${cfg.dataDir}/reports' - SCRIPTS_ROOT = '${cfg.dataDir}/scripts' - 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} - ''; + settingsFile = pythonFmt.generate "netbox-settings.py" cfg.settings; + extraConfigFile = pkgs.writeTextFile { + name = "netbox-extraConfig.py"; + text = cfg.extraConfig; }; - pkg = (pkgs.netbox.overrideAttrs (old: { + configFile = pkgs.concatText "configuration.py" [ settingsFile extraConfigFile ]; + + pkg = (cfg.package.overrideAttrs (old: { installPhase = old.installPhase + '' ln -s ${configFile} $out/opt/netbox/netbox/netbox/configuration.py '' + optionalString cfg.enableLdap '' @@ -70,6 +42,30 @@ in { ''; }; + settings = lib.mkOption { + description = lib.mdDoc '' + Configuration options to set in `configuration.py`. + See the [documentation](https://docs.netbox.dev/en/stable/configuration/) for more possible options. + ''; + + default = { }; + + type = lib.types.submodule { + freeformType = pythonFmt.type; + + options = { + ALLOWED_HOSTS = lib.mkOption { + type = with lib.types; listOf str; + default = ["*"]; + description = lib.mdDoc '' + A list of valid fully-qualified domain names (FQDNs) and/or IP + addresses that can be used to reach the NetBox service. + ''; + }; + }; + }; + }; + listenAddress = mkOption { type = types.str; default = "[::1]"; @@ -78,6 +74,17 @@ in { ''; }; + package = mkOption { + type = types.package; + default = if versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox else pkgs.netbox_3_3; + defaultText = literalExpression '' + if versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox else pkgs.netbox_3_3; + ''; + description = lib.mdDoc '' + NetBox package to use. + ''; + }; + port = mkOption { type = types.port; default = 8001; @@ -117,7 +124,7 @@ in { default = ""; description = lib.mdDoc '' Additional lines of configuration appended to the `configuration.py`. - See the [documentation](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/) for more possible options. + See the [documentation](https://docs.netbox.dev/en/stable/configuration/) for more possible options. ''; }; @@ -138,11 +145,90 @@ in { Path to the Configuration-File for LDAP-Authentication, will be loaded as `ldap_config.py`. See the [documentation](https://netbox.readthedocs.io/en/stable/installation/6-ldap/#configuration) for possible options. ''; + example = '' + import ldap + from django_auth_ldap.config import LDAPSearch, PosixGroupType + + AUTH_LDAP_SERVER_URI = "ldaps://ldap.example.com/" + + AUTH_LDAP_USER_SEARCH = LDAPSearch( + "ou=accounts,ou=posix,dc=example,dc=com", + ldap.SCOPE_SUBTREE, + "(uid=%(user)s)", + ) + + AUTH_LDAP_GROUP_SEARCH = LDAPSearch( + "ou=groups,ou=posix,dc=example,dc=com", + ldap.SCOPE_SUBTREE, + "(objectClass=posixGroup)", + ) + AUTH_LDAP_GROUP_TYPE = PosixGroupType() + + # Mirror LDAP group assignments. + AUTH_LDAP_MIRROR_GROUPS = True + + # For more granular permissions, we can map LDAP groups to Django groups. + AUTH_LDAP_FIND_GROUP_PERMS = True + ''; }; }; config = mkIf cfg.enable { - services.netbox.plugins = mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]); + services.netbox = { + plugins = mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]); + settings = { + STATIC_ROOT = staticDir; + MEDIA_ROOT = "${cfg.dataDir}/media"; + REPORTS_ROOT = "${cfg.dataDir}/reports"; + SCRIPTS_ROOT = "${cfg.dataDir}/scripts"; + + 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; + }; + }; + + REMOTE_AUTH_BACKEND = lib.mkIf cfg.enableLdap "netbox.authentication.LDAPBackend"; + + LOGGING = lib.mkDefault { + version = 1; + + formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s"; + + handlers.console = { + class = "logging.StreamHandler"; + formatter = "precise"; + }; + + # log to console/systemd instead of file + root = { + level = "INFO"; + handlers = [ "console" ]; + }; + }; + }; + + extraConfig = '' + with open("${cfg.secretKeyFile}", "r") as file: + SECRET_KEY = file.readline() + ''; + }; services.redis.servers.netbox.enable = true; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a155510450b1..0783f3bf68e2 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -460,7 +460,8 @@ in { netdata = handleTest ./netdata.nix {}; networking.networkd = handleTest ./networking.nix { networkd = true; }; networking.scripted = handleTest ./networking.nix { networkd = false; }; - netbox = handleTest ./web-apps/netbox.nix {}; + netbox = handleTest ./web-apps/netbox.nix { inherit (pkgs) netbox; }; + netbox_3_3 = handleTest ./web-apps/netbox.nix { netbox = pkgs.netbox_3_3; }; # 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 index 35decdd49e87..30de74f1886c 100644 --- a/nixos/tests/web-apps/netbox.nix +++ b/nixos/tests/web-apps/netbox.nix @@ -1,21 +1,146 @@ -import ../make-test-python.nix ({ lib, pkgs, ... }: { +let + ldapDomain = "example.org"; + ldapSuffix = "dc=example,dc=org"; + + ldapRootUser = "admin"; + ldapRootPassword = "foobar"; + + testUser = "alice"; + testPassword = "verySecure"; + testGroup = "netbox-users"; +in import ../make-test-python.nix ({ lib, pkgs, netbox, ... }: { name = "netbox"; meta = with lib.maintainers; { - maintainers = [ n0emis ]; + maintainers = [ minijackson n0emis ]; }; - nodes.machine = { ... }: { + nodes.machine = { config, ... }: { services.netbox = { enable = true; + package = netbox; secretKeyFile = pkgs.writeText "secret" '' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ''; + + enableLdap = true; + ldapConfigPath = pkgs.writeText "ldap_config.py" '' + import ldap + from django_auth_ldap.config import LDAPSearch, PosixGroupType + + AUTH_LDAP_SERVER_URI = "ldap://localhost/" + + AUTH_LDAP_USER_SEARCH = LDAPSearch( + "ou=accounts,ou=posix,${ldapSuffix}", + ldap.SCOPE_SUBTREE, + "(uid=%(user)s)", + ) + + AUTH_LDAP_GROUP_SEARCH = LDAPSearch( + "ou=groups,ou=posix,${ldapSuffix}", + ldap.SCOPE_SUBTREE, + "(objectClass=posixGroup)", + ) + AUTH_LDAP_GROUP_TYPE = PosixGroupType() + + # Mirror LDAP group assignments. + AUTH_LDAP_MIRROR_GROUPS = True + + # For more granular permissions, we can map LDAP groups to Django groups. + AUTH_LDAP_FIND_GROUP_PERMS = True + ''; }; + + services.nginx = { + enable = true; + + recommendedProxySettings = true; + + virtualHosts.netbox = { + default = true; + locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}"; + locations."/static/".alias = "/var/lib/netbox/static/"; + }; + }; + + # Adapted from the sssd-ldap NixOS test + services.openldap = { + enable = true; + settings = { + children = { + "cn=schema".includes = [ + "${pkgs.openldap}/etc/schema/core.ldif" + "${pkgs.openldap}/etc/schema/cosine.ldif" + "${pkgs.openldap}/etc/schema/inetorgperson.ldif" + "${pkgs.openldap}/etc/schema/nis.ldif" + ]; + "olcDatabase={1}mdb" = { + attrs = { + objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ]; + olcDatabase = "{1}mdb"; + olcDbDirectory = "/var/lib/openldap/db"; + olcSuffix = ldapSuffix; + olcRootDN = "cn=${ldapRootUser},${ldapSuffix}"; + olcRootPW = ldapRootPassword; + }; + }; + }; + }; + declarativeContents = { + ${ldapSuffix} = '' + dn: ${ldapSuffix} + objectClass: top + objectClass: dcObject + objectClass: organization + o: ${ldapDomain} + + dn: ou=posix,${ldapSuffix} + objectClass: top + objectClass: organizationalUnit + + dn: ou=accounts,ou=posix,${ldapSuffix} + objectClass: top + objectClass: organizationalUnit + + dn: uid=${testUser},ou=accounts,ou=posix,${ldapSuffix} + objectClass: person + objectClass: posixAccount + userPassword: ${testPassword} + homeDirectory: /home/${testUser} + uidNumber: 1234 + gidNumber: 1234 + cn: "" + sn: "" + + dn: ou=groups,ou=posix,${ldapSuffix} + objectClass: top + objectClass: organizationalUnit + + dn: cn=${testGroup},ou=groups,ou=posix,${ldapSuffix} + objectClass: posixGroup + gidNumber: 2345 + memberUid: ${testUser} + ''; + }; + }; + + users.users.nginx.extraGroups = [ "netbox" ]; + + networking.firewall.allowedTCPPorts = [ 80 ]; }; - testScript = '' - machine.start() + testScript = let + changePassword = pkgs.writeText "change-password.py" '' + from django.contrib.auth.models import User + u = User.objects.get(username='netbox') + u.set_password('netbox') + u.save() + ''; + in '' + from typing import Any, Dict + import json + + start_all() machine.wait_for_unit("netbox.target") machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening") @@ -26,5 +151,167 @@ import ../make-test-python.nix ({ lib, pkgs, ... }: { with subtest("Staticfiles are generated"): machine.succeed("test -e /var/lib/netbox/static/netbox.js") + + with subtest("Superuser can be created"): + machine.succeed( + "netbox-manage createsuperuser --noinput --username netbox --email netbox@example.com" + ) + # Django doesn't have a "clean" way of inputting the password from the command line + machine.succeed("cat '${changePassword}' | netbox-manage shell") + + machine.wait_for_unit("network.target") + + with subtest("Home screen loads from nginx"): + machine.succeed( + "curl -sSfL http://localhost | grep 'Home | NetBox'" + ) + + with subtest("Staticfiles can be fetched"): + machine.succeed("curl -sSfL http://localhost/static/netbox.js") + machine.succeed("curl -sSfL http://localhost/static/docs/") + + with subtest("Can interact with API"): + json.loads( + machine.succeed("curl -sSfL -H 'Accept: application/json' 'http://localhost/api/'") + ) + + def login(username: str, password: str): + encoded_data = json.dumps({"username": username, "password": password}) + uri = "/users/tokens/provision/" + result = json.loads( + machine.succeed( + "curl -sSfL " + "-X POST " + "-H 'Accept: application/json' " + "-H 'Content-Type: application/json' " + f"'http://localhost/api{uri}' " + f"--data '{encoded_data}'" + ) + ) + return result["key"] + + with subtest("Can login"): + auth_token = login("netbox", "netbox") + + def get(uri: str): + return json.loads( + machine.succeed( + "curl -sSfL " + "-H 'Accept: application/json' " + f"-H 'Authorization: Token {auth_token}' " + f"'http://localhost/api{uri}'" + ) + ) + + def delete(uri: str): + return machine.succeed( + "curl -sSfL " + f"-X DELETE " + "-H 'Accept: application/json' " + f"-H 'Authorization: Token {auth_token}' " + f"'http://localhost/api{uri}'" + ) + + + def data_request(uri: str, method: str, data: Dict[str, Any]): + encoded_data = json.dumps(data) + return json.loads( + machine.succeed( + "curl -sSfL " + f"-X {method} " + "-H 'Accept: application/json' " + "-H 'Content-Type: application/json' " + f"-H 'Authorization: Token {auth_token}' " + f"'http://localhost/api{uri}' " + f"--data '{encoded_data}'" + ) + ) + + def post(uri: str, data: Dict[str, Any]): + return data_request(uri, "POST", data) + + def patch(uri: str, data: Dict[str, Any]): + return data_request(uri, "PATCH", data) + + with subtest("Can create objects"): + result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"}) + site_id = result["id"] + + # Example from: + # http://netbox.extra.cea.fr/static/docs/integrations/rest-api/#creating-a-new-object + post("/ipam/prefixes/", {"prefix": "192.0.2.0/24", "site": site_id}) + + result = post( + "/dcim/manufacturers/", + {"name": "Test manufacturer", "slug": "test-manufacturer"} + ) + manufacturer_id = result["id"] + + # Had an issue with device-types before NetBox 3.4.0 + result = post( + "/dcim/device-types/", + { + "model": "Test device type", + "manufacturer": manufacturer_id, + "slug": "test-device-type", + }, + ) + device_type_id = result["id"] + + with subtest("Can list objects"): + result = get("/dcim/sites/") + + assert result["count"] == 1 + assert result["results"][0]["id"] == site_id + assert result["results"][0]["name"] == "Test site" + assert result["results"][0]["description"] == "" + + result = get("/dcim/device-types/") + assert result["count"] == 1 + assert result["results"][0]["id"] == device_type_id + assert result["results"][0]["model"] == "Test device type" + + with subtest("Can update objects"): + new_description = "Test site description" + patch(f"/dcim/sites/{site_id}/", {"description": new_description}) + result = get(f"/dcim/sites/{site_id}/") + assert result["description"] == new_description + + with subtest("Can delete objects"): + # Delete a device-type since no object depends on it + delete(f"/dcim/device-types/{device_type_id}/") + + result = get("/dcim/device-types/") + assert result["count"] == 0 + + with subtest("Can use the GraphQL API"): + encoded_data = json.dumps({ + "query": "query { prefix_list { prefix, site { id, description } } }", + }) + result = json.loads( + machine.succeed( + "curl -sSfL " + "-H 'Accept: application/json' " + "-H 'Content-Type: application/json' " + f"-H 'Authorization: Token {auth_token}' " + "'http://localhost/graphql/' " + f"--data '{encoded_data}'" + ) + ) + + assert len(result["data"]["prefix_list"]) == 1 + assert result["data"]["prefix_list"][0]["prefix"] == "192.0.2.0/24" + assert result["data"]["prefix_list"][0]["site"]["id"] == str(site_id) + assert result["data"]["prefix_list"][0]["site"]["description"] == new_description + + with subtest("Can login with LDAP"): + machine.wait_for_unit("openldap.service") + login("alice", "${testPassword}") + + with subtest("Can associate LDAP groups"): + result = get("/users/users/?username=${testUser}") + + assert result["count"] == 1 + assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"]) ''; }) diff --git a/pkgs/pkgs-lib/formats.nix b/pkgs/pkgs-lib/formats.nix index 13aada3681f7..3a47d3dc849c 100644 --- a/pkgs/pkgs-lib/formats.nix +++ b/pkgs/pkgs-lib/formats.nix @@ -417,4 +417,39 @@ rec { ''; }; + # Outputs a succession of Python variable assignments + # Useful for many Django-based services + pythonVars = {}: { + type = with lib.types; let + valueType = nullOr(oneOf [ + bool + float + int + path + str + (attrsOf valueType) + (listOf valueType) + ]) // { + description = "Python value"; + }; + in attrsOf valueType; + generate = name: value: pkgs.callPackage ({ runCommand, python3, black }: runCommand name { + nativeBuildInputs = [ python3 black ]; + value = builtins.toJSON value; + pythonGen = '' + import json + import os + + with open(os.environ["valuePath"], "r") as f: + for key, value in json.load(f).items(): + print(f"{key} = {repr(value)}") + ''; + passAsFile = [ "value" "pythonGen" ]; + } '' + cat "$valuePath" + python3 "$pythonGenPath" > $out + black $out + '') {}; + }; + } diff --git a/pkgs/servers/web-apps/netbox/config.patch b/pkgs/servers/web-apps/netbox/config.patch index 1adc0b537a4e..a2e0b0b95a87 100644 --- a/pkgs/servers/web-apps/netbox/config.patch +++ b/pkgs/servers/web-apps/netbox/config.patch @@ -1,30 +1,30 @@ diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py -index d5a7bfaec..68754a8c5 100644 +index 2de06dd10..00406af48 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) +@@ -236,6 +236,7 @@ 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_CA_CERT_PATH = TASKS_REDIS.get('CA_CERT_PATH', 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') +@@ -253,11 +254,12 @@ CACHING_REDIS_SENTINEL_SERVICE = REDIS['caching'].get('SENTINEL_SERVICE', 'defau 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_CA_CERT_PATH = REDIS['caching'].get('CA_CERT_PATH', 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': f'{CACHING_REDIS_PROTO}://{CACHING_REDIS_USERNAME_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 +@@ -410,7 +412,7 @@ USE_X_FORWARDED_HOST = True X_FRAME_OPTIONS = 'SAMEORIGIN' # Static files (CSS, JavaScript, Images) @@ -33,7 +33,7 @@ index d5a7bfaec..68754a8c5 100644 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: +@@ -640,6 +642,14 @@ if TASKS_REDIS_USING_SENTINEL: 'socket_connect_timeout': TASKS_REDIS_SENTINEL_TIMEOUT }, } diff --git a/pkgs/servers/web-apps/netbox/config_3_3.patch b/pkgs/servers/web-apps/netbox/config_3_3.patch new file mode 100644 index 000000000000..1adc0b537a4e --- /dev/null +++ b/pkgs/servers/web-apps/netbox/config_3_3.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 index aeeb57fa3792..fb9e8a6aa57f 100644 --- a/pkgs/servers/web-apps/netbox/default.nix +++ b/pkgs/servers/web-apps/netbox/default.nix @@ -1,37 +1,14 @@ -{ lib -, pkgs -, fetchFromGitHub -, fetchpatch -, nixosTests -, python3 - -, plugins ? ps: [] }: - +{ lib, nixosTests, callPackage, fetchpatch }: let - py = python3 // { - pkgs = python3.pkgs.overrideScope (self: super: { - django = super.django_4; - }); - }; - - extraBuildInputs = plugins py.pkgs; + generic = import ./generic.nix; in -py.pkgs.buildPythonApplication rec { - pname = "netbox"; - version = "3.3.9"; - - format = "other"; - - src = fetchFromGitHub { - owner = "netbox-community"; - repo = pname; - rev = "refs/tags/v${version}"; - sha256 = "sha256-KhnxD5pjlEIgISl4RMbhLCDwgUDfGFRi88ZcP1ndMhI="; - }; - - patches = [ +{ + netbox_3_3 = callPackage generic { + version = "3.3.10"; + hash = "sha256-MeOfTU5IxNDoUh7FyvwAQNRC/CE0R6p40WnlF+3RuxA="; + extraPatches = [ # Allow setting the STATIC_ROOT from within the configuration and setting a custom redis URL - ./config.patch + ./config_3_3.patch ./graphql-3_2_0.patch # fix compatibility ith django 4.1 (fetchpatch { @@ -40,77 +17,22 @@ py.pkgs.buildPythonApplication rec { }) ]; - propagatedBuildInputs = with py.pkgs; [ - bleach - django_4 - 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 - swagger-spec-validator # from drf-yasg[validation] - graphene-django - jinja2 - markdown - markdown-include - netaddr - pillow - psycopg2 - pyyaml - sentry-sdk - social-auth-core - social-auth-app-django - svgwrite - tablib - jsonschema - ] ++ extraBuildInputs; + tests.netbox = nixosTests.netbox_3_3; + maintainers = with lib.maintainers; [ n0emis raitobezarius ]; + eol = true; + }; - buildInputs = with py.pkgs; [ - mkdocs-material - mkdocs-material-extensions - mkdocstrings - mkdocstrings-python + netbox = callPackage generic { + version = "3.4.7"; + hash = "sha256-pWHGyzLc0tqfehWbCMF1l96L1pewb5FXBUkw9EqPtP8="; + extraPatches = [ + # Allow setting the STATIC_ROOT from within the configuration and setting a custom redis URL + ./config.patch ]; - - nativeBuildInputs = [ - py.pkgs.mkdocs - ]; - - postBuild = '' - PYTHONPATH=$PYTHONPATH:netbox/ - python -m mkdocs build - ''; - - 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; - }; + 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 ]; - }; - } + maintainers = with lib.maintainers; [ minijackson n0emis raitobezarius ]; + }; +} diff --git a/pkgs/servers/web-apps/netbox/generic.nix b/pkgs/servers/web-apps/netbox/generic.nix new file mode 100644 index 000000000000..ace3e4f011f0 --- /dev/null +++ b/pkgs/servers/web-apps/netbox/generic.nix @@ -0,0 +1,110 @@ +{ lib +, fetchFromGitHub +, python3 +, version +, hash +, plugins ? ps: [] +, extraPatches ? [] +, tests ? {} +, maintainers ? [] +, eol ? false +}: + let + py = python3 // { + pkgs = python3.pkgs.overrideScope (self: super: { + django = super.django_4; + }); + }; + + extraBuildInputs = plugins py.pkgs; + in + py.pkgs.buildPythonApplication rec { + pname = "netbox"; + inherit version; + + format = "other"; + + src = fetchFromGitHub { + owner = "netbox-community"; + repo = pname; + rev = "refs/tags/v${version}"; + inherit hash; + }; + + patches = extraPatches; + + propagatedBuildInputs = with py.pkgs; [ + bleach + django_4 + 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 + swagger-spec-validator # from drf-yasg[validation] + graphene-django + jinja2 + markdown + markdown-include + netaddr + pillow + psycopg2 + pyyaml + sentry-sdk + social-auth-core + social-auth-app-django + svgwrite + tablib + jsonschema + ] ++ extraBuildInputs; + + buildInputs = with py.pkgs; [ + mkdocs-material + mkdocs-material-extensions + mkdocstrings + mkdocstrings-python + ]; + + nativeBuildInputs = [ + py.pkgs.mkdocs + ]; + + postBuild = '' + PYTHONPATH=$PYTHONPATH:netbox/ + python -m mkdocs build + ''; + + 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; + inherit tests; + }; + + meta = { + homepage = "https://github.com/netbox-community/netbox"; + description = "IP address management (IPAM) and data center infrastructure management (DCIM) tool"; + license = lib.licenses.asl20; + knownVulnerabilities = (lib.optional eol "Netbox version ${version} is EOL; please upgrade by following the current release notes instructions."); + # Warning: + # Notice the missing `lib` in the inherit: it is using this function argument rather than a `with lib;` argument. + # If you replace this by `with lib;`, pay attention it does not inherit all maintainers in nixpkgs. + inherit maintainers; + }; + } diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 2618128907d8..6ee29af177b0 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -10170,7 +10170,8 @@ with pkgs; netbootxyz-efi = callPackage ../tools/misc/netbootxyz-efi { }; - netbox = callPackage ../servers/web-apps/netbox { }; + inherit (callPackage ../servers/web-apps/netbox { }) + netbox_3_3 netbox; netcat = libressl.nc;