1let
2 ldapDomain = "example.org";
3 ldapSuffix = "dc=example,dc=org";
4
5 ldapRootUser = "admin";
6 ldapRootPassword = "foobar";
7
8 testUser = "alice";
9 testPassword = "verySecure";
10 testGroup = "netbox-users";
11in import ../make-test-python.nix ({ lib, pkgs, netbox, ... }: {
12 name = "netbox";
13
14 meta = with lib.maintainers; {
15 maintainers = [ minijackson n0emis ];
16 };
17
18 nodes.machine = { config, ... }: {
19 services.netbox = {
20 enable = true;
21 package = netbox;
22 secretKeyFile = pkgs.writeText "secret" ''
23 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
24 '';
25
26 enableLdap = true;
27 ldapConfigPath = pkgs.writeText "ldap_config.py" ''
28 import ldap
29 from django_auth_ldap.config import LDAPSearch, PosixGroupType
30
31 AUTH_LDAP_SERVER_URI = "ldap://localhost/"
32
33 AUTH_LDAP_USER_SEARCH = LDAPSearch(
34 "ou=accounts,ou=posix,${ldapSuffix}",
35 ldap.SCOPE_SUBTREE,
36 "(uid=%(user)s)",
37 )
38
39 AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
40 "ou=groups,ou=posix,${ldapSuffix}",
41 ldap.SCOPE_SUBTREE,
42 "(objectClass=posixGroup)",
43 )
44 AUTH_LDAP_GROUP_TYPE = PosixGroupType()
45
46 # Mirror LDAP group assignments.
47 AUTH_LDAP_MIRROR_GROUPS = True
48
49 # For more granular permissions, we can map LDAP groups to Django groups.
50 AUTH_LDAP_FIND_GROUP_PERMS = True
51 '';
52 };
53
54 services.nginx = {
55 enable = true;
56
57 recommendedProxySettings = true;
58
59 virtualHosts.netbox = {
60 default = true;
61 locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}";
62 locations."/static/".alias = "/var/lib/netbox/static/";
63 };
64 };
65
66 # Adapted from the sssd-ldap NixOS test
67 services.openldap = {
68 enable = true;
69 settings = {
70 children = {
71 "cn=schema".includes = [
72 "${pkgs.openldap}/etc/schema/core.ldif"
73 "${pkgs.openldap}/etc/schema/cosine.ldif"
74 "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
75 "${pkgs.openldap}/etc/schema/nis.ldif"
76 ];
77 "olcDatabase={1}mdb" = {
78 attrs = {
79 objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
80 olcDatabase = "{1}mdb";
81 olcDbDirectory = "/var/lib/openldap/db";
82 olcSuffix = ldapSuffix;
83 olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
84 olcRootPW = ldapRootPassword;
85 };
86 };
87 };
88 };
89 declarativeContents = {
90 ${ldapSuffix} = ''
91 dn: ${ldapSuffix}
92 objectClass: top
93 objectClass: dcObject
94 objectClass: organization
95 o: ${ldapDomain}
96
97 dn: ou=posix,${ldapSuffix}
98 objectClass: top
99 objectClass: organizationalUnit
100
101 dn: ou=accounts,ou=posix,${ldapSuffix}
102 objectClass: top
103 objectClass: organizationalUnit
104
105 dn: uid=${testUser},ou=accounts,ou=posix,${ldapSuffix}
106 objectClass: person
107 objectClass: posixAccount
108 userPassword: ${testPassword}
109 homeDirectory: /home/${testUser}
110 uidNumber: 1234
111 gidNumber: 1234
112 cn: ""
113 sn: ""
114
115 dn: ou=groups,ou=posix,${ldapSuffix}
116 objectClass: top
117 objectClass: organizationalUnit
118
119 dn: cn=${testGroup},ou=groups,ou=posix,${ldapSuffix}
120 objectClass: posixGroup
121 gidNumber: 2345
122 memberUid: ${testUser}
123 '';
124 };
125 };
126
127 users.users.nginx.extraGroups = [ "netbox" ];
128
129 networking.firewall.allowedTCPPorts = [ 80 ];
130 };
131
132 testScript = let
133 changePassword = pkgs.writeText "change-password.py" ''
134 from django.contrib.auth.models import User
135 u = User.objects.get(username='netbox')
136 u.set_password('netbox')
137 u.save()
138 '';
139 in ''
140 from typing import Any, Dict
141 import json
142
143 start_all()
144 machine.wait_for_unit("netbox.target")
145 machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
146
147 with subtest("Home screen loads"):
148 machine.succeed(
149 "curl -sSfL http://[::1]:8001 | grep '<title>Home | NetBox</title>'"
150 )
151
152 with subtest("Staticfiles are generated"):
153 machine.succeed("test -e /var/lib/netbox/static/netbox.js")
154
155 with subtest("Superuser can be created"):
156 machine.succeed(
157 "netbox-manage createsuperuser --noinput --username netbox --email netbox@example.com"
158 )
159 # Django doesn't have a "clean" way of inputting the password from the command line
160 machine.succeed("cat '${changePassword}' | netbox-manage shell")
161
162 machine.wait_for_unit("network.target")
163
164 with subtest("Home screen loads from nginx"):
165 machine.succeed(
166 "curl -sSfL http://localhost | grep '<title>Home | NetBox</title>'"
167 )
168
169 with subtest("Staticfiles can be fetched"):
170 machine.succeed("curl -sSfL http://localhost/static/netbox.js")
171 machine.succeed("curl -sSfL http://localhost/static/docs/")
172
173 with subtest("Can interact with API"):
174 json.loads(
175 machine.succeed("curl -sSfL -H 'Accept: application/json' 'http://localhost/api/'")
176 )
177
178 def login(username: str, password: str):
179 encoded_data = json.dumps({"username": username, "password": password})
180 uri = "/users/tokens/provision/"
181 result = json.loads(
182 machine.succeed(
183 "curl -sSfL "
184 "-X POST "
185 "-H 'Accept: application/json' "
186 "-H 'Content-Type: application/json' "
187 f"'http://localhost/api{uri}' "
188 f"--data '{encoded_data}'"
189 )
190 )
191 return result["key"]
192
193 with subtest("Can login"):
194 auth_token = login("netbox", "netbox")
195
196 def get(uri: str):
197 return json.loads(
198 machine.succeed(
199 "curl -sSfL "
200 "-H 'Accept: application/json' "
201 f"-H 'Authorization: Token {auth_token}' "
202 f"'http://localhost/api{uri}'"
203 )
204 )
205
206 def delete(uri: str):
207 return machine.succeed(
208 "curl -sSfL "
209 f"-X DELETE "
210 "-H 'Accept: application/json' "
211 f"-H 'Authorization: Token {auth_token}' "
212 f"'http://localhost/api{uri}'"
213 )
214
215
216 def data_request(uri: str, method: str, data: Dict[str, Any]):
217 encoded_data = json.dumps(data)
218 return json.loads(
219 machine.succeed(
220 "curl -sSfL "
221 f"-X {method} "
222 "-H 'Accept: application/json' "
223 "-H 'Content-Type: application/json' "
224 f"-H 'Authorization: Token {auth_token}' "
225 f"'http://localhost/api{uri}' "
226 f"--data '{encoded_data}'"
227 )
228 )
229
230 def post(uri: str, data: Dict[str, Any]):
231 return data_request(uri, "POST", data)
232
233 def patch(uri: str, data: Dict[str, Any]):
234 return data_request(uri, "PATCH", data)
235
236 with subtest("Can create objects"):
237 result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
238 site_id = result["id"]
239
240 # Example from:
241 # http://netbox.extra.cea.fr/static/docs/integrations/rest-api/#creating-a-new-object
242 post("/ipam/prefixes/", {"prefix": "192.0.2.0/24", "site": site_id})
243
244 result = post(
245 "/dcim/manufacturers/",
246 {"name": "Test manufacturer", "slug": "test-manufacturer"}
247 )
248 manufacturer_id = result["id"]
249
250 # Had an issue with device-types before NetBox 3.4.0
251 result = post(
252 "/dcim/device-types/",
253 {
254 "model": "Test device type",
255 "manufacturer": manufacturer_id,
256 "slug": "test-device-type",
257 },
258 )
259 device_type_id = result["id"]
260
261 with subtest("Can list objects"):
262 result = get("/dcim/sites/")
263
264 assert result["count"] == 1
265 assert result["results"][0]["id"] == site_id
266 assert result["results"][0]["name"] == "Test site"
267 assert result["results"][0]["description"] == ""
268
269 result = get("/dcim/device-types/")
270 assert result["count"] == 1
271 assert result["results"][0]["id"] == device_type_id
272 assert result["results"][0]["model"] == "Test device type"
273
274 with subtest("Can update objects"):
275 new_description = "Test site description"
276 patch(f"/dcim/sites/{site_id}/", {"description": new_description})
277 result = get(f"/dcim/sites/{site_id}/")
278 assert result["description"] == new_description
279
280 with subtest("Can delete objects"):
281 # Delete a device-type since no object depends on it
282 delete(f"/dcim/device-types/{device_type_id}/")
283
284 result = get("/dcim/device-types/")
285 assert result["count"] == 0
286
287 with subtest("Can use the GraphQL API"):
288 encoded_data = json.dumps({
289 "query": "query { prefix_list { prefix, site { id, description } } }",
290 })
291 result = json.loads(
292 machine.succeed(
293 "curl -sSfL "
294 "-H 'Accept: application/json' "
295 "-H 'Content-Type: application/json' "
296 f"-H 'Authorization: Token {auth_token}' "
297 "'http://localhost/graphql/' "
298 f"--data '{encoded_data}'"
299 )
300 )
301
302 assert len(result["data"]["prefix_list"]) == 1
303 assert result["data"]["prefix_list"][0]["prefix"] == "192.0.2.0/24"
304 assert result["data"]["prefix_list"][0]["site"]["id"] == str(site_id)
305 assert result["data"]["prefix_list"][0]["site"]["description"] == new_description
306
307 with subtest("Can login with LDAP"):
308 machine.wait_for_unit("openldap.service")
309 login("alice", "${testPassword}")
310
311 with subtest("Can associate LDAP groups"):
312 result = get("/users/users/?username=${testUser}")
313
314 assert result["count"] == 1
315 assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])
316 '';
317})