2 ldapDomain = "example.org";
3 ldapSuffix = "dc=example,dc=org";
5 ldapRootUser = "admin";
6 ldapRootPassword = "foobar";
9 testPassword = "verySecure";
10 testGroup = "netbox-users";
11 in import ../make-test-python.nix ({ lib, pkgs, netbox, ... }: {
14 meta = with lib.maintainers; {
15 maintainers = [ minijackson n0emis ];
18 nodes.machine = { config, ... }: {
19 virtualisation.memorySize = 2048;
23 secretKeyFile = pkgs.writeText "secret" ''
24 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
28 ldapConfigPath = pkgs.writeText "ldap_config.py" ''
30 from django_auth_ldap.config import LDAPSearch, PosixGroupType
32 AUTH_LDAP_SERVER_URI = "ldap://localhost/"
34 AUTH_LDAP_USER_SEARCH = LDAPSearch(
35 "ou=accounts,ou=posix,${ldapSuffix}",
40 AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
41 "ou=groups,ou=posix,${ldapSuffix}",
43 "(objectClass=posixGroup)",
45 AUTH_LDAP_GROUP_TYPE = PosixGroupType()
47 # Mirror LDAP group assignments.
48 AUTH_LDAP_MIRROR_GROUPS = True
50 # For more granular permissions, we can map LDAP groups to Django groups.
51 AUTH_LDAP_FIND_GROUP_PERMS = True
58 recommendedProxySettings = true;
60 virtualHosts.netbox = {
62 locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}";
63 locations."/static/".alias = "/var/lib/netbox/static/";
67 # Adapted from the sssd-ldap NixOS test
72 "cn=schema".includes = [
73 "${pkgs.openldap}/etc/schema/core.ldif"
74 "${pkgs.openldap}/etc/schema/cosine.ldif"
75 "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
76 "${pkgs.openldap}/etc/schema/nis.ldif"
78 "olcDatabase={1}mdb" = {
80 objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
81 olcDatabase = "{1}mdb";
82 olcDbDirectory = "/var/lib/openldap/db";
83 olcSuffix = ldapSuffix;
84 olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
85 olcRootPW = ldapRootPassword;
90 declarativeContents = {
95 objectClass: organization
98 dn: ou=posix,${ldapSuffix}
100 objectClass: organizationalUnit
102 dn: ou=accounts,ou=posix,${ldapSuffix}
104 objectClass: organizationalUnit
106 dn: uid=${testUser},ou=accounts,ou=posix,${ldapSuffix}
108 objectClass: posixAccount
109 userPassword: ${testPassword}
110 homeDirectory: /home/${testUser}
116 dn: ou=groups,ou=posix,${ldapSuffix}
118 objectClass: organizationalUnit
120 dn: cn=${testGroup},ou=groups,ou=posix,${ldapSuffix}
121 objectClass: posixGroup
123 memberUid: ${testUser}
128 users.users.nginx.extraGroups = [ "netbox" ];
130 networking.firewall.allowedTCPPorts = [ 80 ];
134 changePassword = pkgs.writeText "change-password.py" ''
135 from django.contrib.auth.models import User
136 u = User.objects.get(username='netbox')
137 u.set_password('netbox')
141 from typing import Any, Dict
145 machine.wait_for_unit("netbox.target")
146 machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
148 with subtest("Home screen loads"):
150 "curl -sSfL http://[::1]:8001 | grep '<title>Home | NetBox</title>'"
153 with subtest("Staticfiles are generated"):
154 machine.succeed("test -e /var/lib/netbox/static/netbox.js")
156 with subtest("Superuser can be created"):
158 "netbox-manage createsuperuser --noinput --username netbox --email netbox@example.com"
160 # Django doesn't have a "clean" way of inputting the password from the command line
161 machine.succeed("cat '${changePassword}' | netbox-manage shell")
163 machine.wait_for_unit("network.target")
165 with subtest("Home screen loads from nginx"):
167 "curl -sSfL http://localhost | grep '<title>Home | NetBox</title>'"
170 with subtest("Staticfiles can be fetched"):
171 machine.succeed("curl -sSfL http://localhost/static/netbox.js")
172 machine.succeed("curl -sSfL http://localhost/static/docs/")
174 with subtest("Can interact with API"):
176 machine.succeed("curl -sSfL -H 'Accept: application/json' 'http://localhost/api/'")
179 def login(username: str, password: str):
180 encoded_data = json.dumps({"username": username, "password": password})
181 uri = "/users/tokens/provision/"
186 "-H 'Accept: application/json' "
187 "-H 'Content-Type: application/json' "
188 f"'http://localhost/api{uri}' "
189 f"--data '{encoded_data}'"
194 with subtest("Can login"):
195 auth_token = login("netbox", "netbox")
201 "-H 'Accept: application/json' "
202 f"-H 'Authorization: Token {auth_token}' "
203 f"'http://localhost/api{uri}'"
207 def delete(uri: str):
208 return machine.succeed(
211 "-H 'Accept: application/json' "
212 f"-H 'Authorization: Token {auth_token}' "
213 f"'http://localhost/api{uri}'"
217 def data_request(uri: str, method: str, data: Dict[str, Any]):
218 encoded_data = json.dumps(data)
223 "-H 'Accept: application/json' "
224 "-H 'Content-Type: application/json' "
225 f"-H 'Authorization: Token {auth_token}' "
226 f"'http://localhost/api{uri}' "
227 f"--data '{encoded_data}'"
231 def post(uri: str, data: Dict[str, Any]):
232 return data_request(uri, "POST", data)
234 def patch(uri: str, data: Dict[str, Any]):
235 return data_request(uri, "PATCH", data)
237 with subtest("Can create objects"):
238 result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
239 site_id = result["id"]
242 # http://netbox.extra.cea.fr/static/docs/integrations/rest-api/#creating-a-new-object
243 post("/ipam/prefixes/", {"prefix": "192.0.2.0/24", "site": site_id})
246 "/dcim/manufacturers/",
247 {"name": "Test manufacturer", "slug": "test-manufacturer"}
249 manufacturer_id = result["id"]
251 # Had an issue with device-types before NetBox 3.4.0
253 "/dcim/device-types/",
255 "model": "Test device type",
256 "manufacturer": manufacturer_id,
257 "slug": "test-device-type",
260 device_type_id = result["id"]
262 with subtest("Can list objects"):
263 result = get("/dcim/sites/")
265 assert result["count"] == 1
266 assert result["results"][0]["id"] == site_id
267 assert result["results"][0]["name"] == "Test site"
268 assert result["results"][0]["description"] == ""
270 result = get("/dcim/device-types/")
271 assert result["count"] == 1
272 assert result["results"][0]["id"] == device_type_id
273 assert result["results"][0]["model"] == "Test device type"
275 with subtest("Can update objects"):
276 new_description = "Test site description"
277 patch(f"/dcim/sites/{site_id}/", {"description": new_description})
278 result = get(f"/dcim/sites/{site_id}/")
279 assert result["description"] == new_description
281 with subtest("Can delete objects"):
282 # Delete a device-type since no object depends on it
283 delete(f"/dcim/device-types/{device_type_id}/")
285 result = get("/dcim/device-types/")
286 assert result["count"] == 0
288 with subtest("Can use the GraphQL API"):
289 encoded_data = json.dumps({
290 "query": "query { prefix_list { prefix, site { id, description } } }",
295 "-H 'Accept: application/json' "
296 "-H 'Content-Type: application/json' "
297 f"-H 'Authorization: Token {auth_token}' "
298 "'http://localhost/graphql/' "
299 f"--data '{encoded_data}'"
303 assert len(result["data"]["prefix_list"]) == 1
304 assert result["data"]["prefix_list"][0]["prefix"] == "192.0.2.0/24"
305 assert result["data"]["prefix_list"][0]["site"]["id"] == str(site_id)
306 assert result["data"]["prefix_list"][0]["site"]["description"] == new_description
308 with subtest("Can login with LDAP"):
309 machine.wait_for_unit("openldap.service")
310 login("alice", "${testPassword}")
312 with subtest("Can associate LDAP groups"):
313 result = get("/users/users/?username=${testUser}")
315 assert result["count"] == 1
316 assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])