1
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-07-02 22:10:08 +03:00

Merge pull request #206983 from minijackson/netbox-3.4.1

netbox: 3.3.9 -> 3.4.7, netbox_3_3: init at 3.3.10, RFC42-style options, more tests
This commit is contained in:
Ryan Lahfa 2023-04-05 14:04:27 +02:00 committed by GitHub
commit a6bc6ed645
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 650 additions and 154 deletions

View file

@ -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.

View file

@ -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;

View file

@ -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 {};

View file

@ -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 '<title>Home | NetBox</title>'"
)
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"])
'';
})