2 ldapDomain = "example.org";
3 ldapSuffix = "dc=example,dc=org";
5 ldapRootUser = "admin";
6 ldapRootPassword = "foobar";
9 testPassword = "verySecure";
10 testGroup = "netbox-users";
12 import ../make-test-python.nix (
22 meta = with lib.maintainers; {
31 virtualisation.memorySize = 2048;
35 secretKeyFile = pkgs.writeText "secret" ''
36 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
40 ldapConfigPath = pkgs.writeText "ldap_config.py" ''
42 from django_auth_ldap.config import LDAPSearch, PosixGroupType
44 AUTH_LDAP_SERVER_URI = "ldap://localhost/"
46 AUTH_LDAP_USER_SEARCH = LDAPSearch(
47 "ou=accounts,ou=posix,${ldapSuffix}",
52 AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
53 "ou=groups,ou=posix,${ldapSuffix}",
55 "(objectClass=posixGroup)",
57 AUTH_LDAP_GROUP_TYPE = PosixGroupType()
59 # Mirror LDAP group assignments.
60 AUTH_LDAP_MIRROR_GROUPS = True
62 # For more granular permissions, we can map LDAP groups to Django groups.
63 AUTH_LDAP_FIND_GROUP_PERMS = True
70 recommendedProxySettings = true;
72 virtualHosts.netbox = {
74 locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}";
75 locations."/static/".alias = "/var/lib/netbox/static/";
79 # Adapted from the sssd-ldap NixOS test
84 "cn=schema".includes = [
85 "${pkgs.openldap}/etc/schema/core.ldif"
86 "${pkgs.openldap}/etc/schema/cosine.ldif"
87 "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
88 "${pkgs.openldap}/etc/schema/nis.ldif"
90 "olcDatabase={1}mdb" = {
96 olcDatabase = "{1}mdb";
97 olcDbDirectory = "/var/lib/openldap/db";
98 olcSuffix = ldapSuffix;
99 olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
100 olcRootPW = ldapRootPassword;
105 declarativeContents = {
109 objectClass: dcObject
110 objectClass: organization
113 dn: ou=posix,${ldapSuffix}
115 objectClass: organizationalUnit
117 dn: ou=accounts,ou=posix,${ldapSuffix}
119 objectClass: organizationalUnit
121 dn: uid=${testUser},ou=accounts,ou=posix,${ldapSuffix}
123 objectClass: posixAccount
124 userPassword: ${testPassword}
125 homeDirectory: /home/${testUser}
131 dn: ou=groups,ou=posix,${ldapSuffix}
133 objectClass: organizationalUnit
135 dn: cn=${testGroup},ou=groups,ou=posix,${ldapSuffix}
136 objectClass: posixGroup
138 memberUid: ${testUser}
143 users.users.nginx.extraGroups = [ "netbox" ];
145 networking.firewall.allowedTCPPorts = [ 80 ];
150 changePassword = pkgs.writeText "change-password.py" ''
151 from users.models import User
152 u = User.objects.get(username='netbox')
153 u.set_password('netbox')
158 from typing import Any, Dict
162 machine.wait_for_unit("netbox.target")
163 machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
165 with subtest("Home screen loads"):
167 "curl -sSfL http://[::1]:8001 | grep '<title>Home | NetBox</title>'"
170 with subtest("Staticfiles are generated"):
171 machine.succeed("test -e /var/lib/netbox/static/netbox.js")
173 with subtest("Superuser can be created"):
175 "netbox-manage createsuperuser --noinput --username netbox --email netbox@example.com"
177 # Django doesn't have a "clean" way of inputting the password from the command line
178 machine.succeed("cat '${changePassword}' | netbox-manage shell")
180 machine.wait_for_unit("network.target")
182 with subtest("Home screen loads from nginx"):
184 "curl -sSfL http://localhost | grep '<title>Home | NetBox</title>'"
187 with subtest("Staticfiles can be fetched"):
188 machine.succeed("curl -sSfL http://localhost/static/netbox.js")
189 machine.succeed("curl -sSfL http://localhost/static/docs/")
191 def login(username: str, password: str):
192 encoded_data = json.dumps({"username": username, "password": password})
193 uri = "/users/tokens/provision/"
198 "-H 'Accept: application/json' "
199 "-H 'Content-Type: application/json' "
200 f"'http://localhost/api{uri}' "
201 f"--data '{encoded_data}'"
206 with subtest("Can login"):
207 auth_token = login("netbox", "netbox")
213 "-H 'Accept: application/json' "
214 f"-H 'Authorization: Token {auth_token}' "
215 f"'http://localhost/api{uri}'"
219 def delete(uri: str):
220 return machine.succeed(
223 "-H 'Accept: application/json' "
224 f"-H 'Authorization: Token {auth_token}' "
225 f"'http://localhost/api{uri}'"
229 def data_request(uri: str, method: str, data: Dict[str, Any]):
230 encoded_data = json.dumps(data)
235 "-H 'Accept: application/json' "
236 "-H 'Content-Type: application/json' "
237 f"-H 'Authorization: Token {auth_token}' "
238 f"'http://localhost/api{uri}' "
239 f"--data '{encoded_data}'"
243 def post(uri: str, data: Dict[str, Any]):
244 return data_request(uri, "POST", data)
246 def patch(uri: str, data: Dict[str, Any]):
247 return data_request(uri, "PATCH", data)
249 with subtest("Can create objects"):
250 result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
251 site_id = result["id"]
254 # http://netbox.extra.cea.fr/static/docs/integrations/rest-api/#creating-a-new-object
255 post("/ipam/prefixes/", {"prefix": "192.0.2.0/24", "site": site_id})
258 "/dcim/manufacturers/",
259 {"name": "Test manufacturer", "slug": "test-manufacturer"}
261 manufacturer_id = result["id"]
263 # Had an issue with device-types before NetBox 3.4.0
265 "/dcim/device-types/",
267 "model": "Test device type",
268 "manufacturer": manufacturer_id,
269 "slug": "test-device-type",
272 device_type_id = result["id"]
274 with subtest("Can list objects"):
275 result = get("/dcim/sites/")
277 assert result["count"] == 1
278 assert result["results"][0]["id"] == site_id
279 assert result["results"][0]["name"] == "Test site"
280 assert result["results"][0]["description"] == ""
282 result = get("/dcim/device-types/")
283 assert result["count"] == 1
284 assert result["results"][0]["id"] == device_type_id
285 assert result["results"][0]["model"] == "Test device type"
287 with subtest("Can update objects"):
288 new_description = "Test site description"
289 patch(f"/dcim/sites/{site_id}/", {"description": new_description})
290 result = get(f"/dcim/sites/{site_id}/")
291 assert result["description"] == new_description
293 with subtest("Can delete objects"):
294 # Delete a device-type since no object depends on it
295 delete(f"/dcim/device-types/{device_type_id}/")
297 result = get("/dcim/device-types/")
298 assert result["count"] == 0
300 with subtest("Can use the GraphQL API"):
301 encoded_data = json.dumps({
302 "query": "query { prefix_list { prefix, site { id, description } } }",
307 "-H 'Accept: application/json' "
308 "-H 'Content-Type: application/json' "
309 f"-H 'Authorization: Token {auth_token}' "
310 "'http://localhost/graphql/' "
311 f"--data '{encoded_data}'"
315 assert len(result["data"]["prefix_list"]) == 1
316 assert result["data"]["prefix_list"][0]["prefix"] == "192.0.2.0/24"
317 assert result["data"]["prefix_list"][0]["site"]["id"] == str(site_id)
318 assert result["data"]["prefix_list"][0]["site"]["description"] == new_description
320 with subtest("Can login with LDAP"):
321 machine.wait_for_unit("openldap.service")
322 login("alice", "${testPassword}")
324 with subtest("Can associate LDAP groups"):
325 result = get("/users/users/?username=${testUser}")
327 assert result["count"] == 1
328 assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])