base16-schemes: unstable-2024-06-21 -> unstable-2024-11-12
[NixPkgs.git] / nixos / tests / web-apps / netbox.nix
blob2fdd70cfb1bfcde4261b3bf3d1bc82f6b2213b17
1 let
2   ldapDomain = "example.org";
3   ldapSuffix = "dc=example,dc=org";
5   ldapRootUser = "admin";
6   ldapRootPassword = "foobar";
8   testUser = "alice";
9   testPassword = "verySecure";
10   testGroup = "netbox-users";
11 in import ../make-test-python.nix ({ lib, pkgs, netbox, ... }: {
12   name = "netbox";
14   meta = with lib.maintainers; {
15     maintainers = [ minijackson n0emis ];
16   };
18   nodes.machine = { config, ... }: {
19     virtualisation.memorySize = 2048;
20     services.netbox = {
21       enable = true;
22       package = netbox;
23       secretKeyFile = pkgs.writeText "secret" ''
24         abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
25       '';
27       enableLdap = true;
28       ldapConfigPath = pkgs.writeText "ldap_config.py" ''
29         import ldap
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}",
36             ldap.SCOPE_SUBTREE,
37             "(uid=%(user)s)",
38         )
40         AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
41             "ou=groups,ou=posix,${ldapSuffix}",
42             ldap.SCOPE_SUBTREE,
43             "(objectClass=posixGroup)",
44         )
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
52       '';
53     };
55     services.nginx = {
56       enable = true;
58       recommendedProxySettings = true;
60       virtualHosts.netbox = {
61         default = true;
62         locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}";
63         locations."/static/".alias = "/var/lib/netbox/static/";
64       };
65     };
67     # Adapted from the sssd-ldap NixOS test
68     services.openldap = {
69       enable = true;
70       settings = {
71         children = {
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"
77           ];
78           "olcDatabase={1}mdb" = {
79             attrs = {
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;
86             };
87           };
88         };
89       };
90       declarativeContents = {
91         ${ldapSuffix} = ''
92           dn: ${ldapSuffix}
93           objectClass: top
94           objectClass: dcObject
95           objectClass: organization
96           o: ${ldapDomain}
98           dn: ou=posix,${ldapSuffix}
99           objectClass: top
100           objectClass: organizationalUnit
102           dn: ou=accounts,ou=posix,${ldapSuffix}
103           objectClass: top
104           objectClass: organizationalUnit
106           dn: uid=${testUser},ou=accounts,ou=posix,${ldapSuffix}
107           objectClass: person
108           objectClass: posixAccount
109           userPassword: ${testPassword}
110           homeDirectory: /home/${testUser}
111           uidNumber: 1234
112           gidNumber: 1234
113           cn: ""
114           sn: ""
116           dn: ou=groups,ou=posix,${ldapSuffix}
117           objectClass: top
118           objectClass: organizationalUnit
120           dn: cn=${testGroup},ou=groups,ou=posix,${ldapSuffix}
121           objectClass: posixGroup
122           gidNumber: 2345
123           memberUid: ${testUser}
124         '';
125       };
126     };
128     users.users.nginx.extraGroups = [ "netbox" ];
130     networking.firewall.allowedTCPPorts = [ 80 ];
131   };
133   testScript = let
134     changePassword = pkgs.writeText "change-password.py" ''
135       from users.models import User
136       u = User.objects.get(username='netbox')
137       u.set_password('netbox')
138       u.save()
139     '';
140   in ''
141     from typing import Any, Dict
142     import json
144     start_all()
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"):
149         machine.succeed(
150             "curl -sSfL http://[::1]:8001 | grep '<title>Home | NetBox</title>'"
151         )
153     with subtest("Staticfiles are generated"):
154         machine.succeed("test -e /var/lib/netbox/static/netbox.js")
156     with subtest("Superuser can be created"):
157         machine.succeed(
158             "netbox-manage createsuperuser --noinput --username netbox --email netbox@example.com"
159         )
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"):
166         machine.succeed(
167             "curl -sSfL http://localhost | grep '<title>Home | NetBox</title>'"
168         )
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     def login(username: str, password: str):
175         encoded_data = json.dumps({"username": username, "password": password})
176         uri = "/users/tokens/provision/"
177         result = json.loads(
178             machine.succeed(
179                 "curl -sSfL "
180                 "-X POST "
181                 "-H 'Accept: application/json' "
182                 "-H 'Content-Type: application/json' "
183                 f"'http://localhost/api{uri}' "
184                 f"--data '{encoded_data}'"
185             )
186         )
187         return result["key"]
189     with subtest("Can login"):
190         auth_token = login("netbox", "netbox")
192     def get(uri: str):
193         return json.loads(
194             machine.succeed(
195                 "curl -sSfL "
196                 "-H 'Accept: application/json' "
197                 f"-H 'Authorization: Token {auth_token}' "
198                 f"'http://localhost/api{uri}'"
199             )
200         )
202     def delete(uri: str):
203         return machine.succeed(
204             "curl -sSfL "
205             f"-X DELETE "
206             "-H 'Accept: application/json' "
207             f"-H 'Authorization: Token {auth_token}' "
208             f"'http://localhost/api{uri}'"
209         )
212     def data_request(uri: str, method: str, data: Dict[str, Any]):
213         encoded_data = json.dumps(data)
214         return json.loads(
215             machine.succeed(
216                 "curl -sSfL "
217                 f"-X {method} "
218                 "-H 'Accept: application/json' "
219                 "-H 'Content-Type: application/json' "
220                 f"-H 'Authorization: Token {auth_token}' "
221                 f"'http://localhost/api{uri}' "
222                 f"--data '{encoded_data}'"
223             )
224         )
226     def post(uri: str, data: Dict[str, Any]):
227       return data_request(uri, "POST", data)
229     def patch(uri: str, data: Dict[str, Any]):
230       return data_request(uri, "PATCH", data)
232     with subtest("Can create objects"):
233         result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
234         site_id = result["id"]
236         # Example from:
237         # http://netbox.extra.cea.fr/static/docs/integrations/rest-api/#creating-a-new-object
238         post("/ipam/prefixes/", {"prefix": "192.0.2.0/24", "site": site_id})
240         result = post(
241             "/dcim/manufacturers/",
242             {"name": "Test manufacturer", "slug": "test-manufacturer"}
243         )
244         manufacturer_id = result["id"]
246         # Had an issue with device-types before NetBox 3.4.0
247         result = post(
248             "/dcim/device-types/",
249             {
250                 "model": "Test device type",
251                 "manufacturer": manufacturer_id,
252                 "slug": "test-device-type",
253             },
254         )
255         device_type_id = result["id"]
257     with subtest("Can list objects"):
258         result = get("/dcim/sites/")
260         assert result["count"] == 1
261         assert result["results"][0]["id"] == site_id
262         assert result["results"][0]["name"] == "Test site"
263         assert result["results"][0]["description"] == ""
265         result = get("/dcim/device-types/")
266         assert result["count"] == 1
267         assert result["results"][0]["id"] == device_type_id
268         assert result["results"][0]["model"] == "Test device type"
270     with subtest("Can update objects"):
271         new_description = "Test site description"
272         patch(f"/dcim/sites/{site_id}/", {"description": new_description})
273         result = get(f"/dcim/sites/{site_id}/")
274         assert result["description"] == new_description
276     with subtest("Can delete objects"):
277         # Delete a device-type since no object depends on it
278         delete(f"/dcim/device-types/{device_type_id}/")
280         result = get("/dcim/device-types/")
281         assert result["count"] == 0
283     with subtest("Can use the GraphQL API"):
284         encoded_data = json.dumps({
285             "query": "query { prefix_list { prefix, site { id, description } } }",
286         })
287         result = json.loads(
288             machine.succeed(
289                 "curl -sSfL "
290                 "-H 'Accept: application/json' "
291                 "-H 'Content-Type: application/json' "
292                 f"-H 'Authorization: Token {auth_token}' "
293                 "'http://localhost/graphql/' "
294                 f"--data '{encoded_data}'"
295             )
296         )
298         assert len(result["data"]["prefix_list"]) == 1
299         assert result["data"]["prefix_list"][0]["prefix"] == "192.0.2.0/24"
300         assert result["data"]["prefix_list"][0]["site"]["id"] == str(site_id)
301         assert result["data"]["prefix_list"][0]["site"]["description"] == new_description
303     with subtest("Can login with LDAP"):
304         machine.wait_for_unit("openldap.service")
305         login("alice", "${testPassword}")
307     with subtest("Can associate LDAP groups"):
308         result = get("/users/users/?username=${testUser}")
310         assert result["count"] == 1
311         assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])
312   '';