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 virtualisation.memorySize = 2048;
20 services.netbox = {
21 enable = true;
22 package = netbox;
23 secretKeyFile = pkgs.writeText "secret" ''
24 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
25 '';
26
27 enableLdap = true;
28 ldapConfigPath = pkgs.writeText "ldap_config.py" ''
29 import ldap
30 from django_auth_ldap.config import LDAPSearch, PosixGroupType
31
32 AUTH_LDAP_SERVER_URI = "ldap://localhost/"
33
34 AUTH_LDAP_USER_SEARCH = LDAPSearch(
35 "ou=accounts,ou=posix,${ldapSuffix}",
36 ldap.SCOPE_SUBTREE,
37 "(uid=%(user)s)",
38 )
39
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()
46
47 # Mirror LDAP group assignments.
48 AUTH_LDAP_MIRROR_GROUPS = True
49
50 # For more granular permissions, we can map LDAP groups to Django groups.
51 AUTH_LDAP_FIND_GROUP_PERMS = True
52 '';
53 };
54
55 services.nginx = {
56 enable = true;
57
58 recommendedProxySettings = true;
59
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 };
66
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}
97
98 dn: ou=posix,${ldapSuffix}
99 objectClass: top
100 objectClass: organizationalUnit
101
102 dn: ou=accounts,ou=posix,${ldapSuffix}
103 objectClass: top
104 objectClass: organizationalUnit
105
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: ""
115
116 dn: ou=groups,ou=posix,${ldapSuffix}
117 objectClass: top
118 objectClass: organizationalUnit
119
120 dn: cn=${testGroup},ou=groups,ou=posix,${ldapSuffix}
121 objectClass: posixGroup
122 gidNumber: 2345
123 memberUid: ${testUser}
124 '';
125 };
126 };
127
128 users.users.nginx.extraGroups = [ "netbox" ];
129
130 networking.firewall.allowedTCPPorts = [ 80 ];
131 };
132
133 testScript = let
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')
138 u.save()
139 '';
140 in ''
141 from typing import Any, Dict
142 import json
143
144 start_all()
145 machine.wait_for_unit("netbox.target")
146 machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
147
148 with subtest("Home screen loads"):
149 machine.succeed(
150 "curl -sSfL http://[::1]:8001 | grep '<title>Home | NetBox</title>'"
151 )
152
153 with subtest("Staticfiles are generated"):
154 machine.succeed("test -e /var/lib/netbox/static/netbox.js")
155
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")
162
163 machine.wait_for_unit("network.target")
164
165 with subtest("Home screen loads from nginx"):
166 machine.succeed(
167 "curl -sSfL http://localhost | grep '<title>Home | NetBox</title>'"
168 )
169
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/")
173
174 with subtest("Can interact with API"):
175 json.loads(
176 machine.succeed("curl -sSfL -H 'Accept: application/json' 'http://localhost/api/'")
177 )
178
179 def login(username: str, password: str):
180 encoded_data = json.dumps({"username": username, "password": password})
181 uri = "/users/tokens/provision/"
182 result = json.loads(
183 machine.succeed(
184 "curl -sSfL "
185 "-X POST "
186 "-H 'Accept: application/json' "
187 "-H 'Content-Type: application/json' "
188 f"'http://localhost/api{uri}' "
189 f"--data '{encoded_data}'"
190 )
191 )
192 return result["key"]
193
194 with subtest("Can login"):
195 auth_token = login("netbox", "netbox")
196
197 def get(uri: str):
198 return json.loads(
199 machine.succeed(
200 "curl -sSfL "
201 "-H 'Accept: application/json' "
202 f"-H 'Authorization: Token {auth_token}' "
203 f"'http://localhost/api{uri}'"
204 )
205 )
206
207 def delete(uri: str):
208 return machine.succeed(
209 "curl -sSfL "
210 f"-X DELETE "
211 "-H 'Accept: application/json' "
212 f"-H 'Authorization: Token {auth_token}' "
213 f"'http://localhost/api{uri}'"
214 )
215
216
217 def data_request(uri: str, method: str, data: Dict[str, Any]):
218 encoded_data = json.dumps(data)
219 return json.loads(
220 machine.succeed(
221 "curl -sSfL "
222 f"-X {method} "
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}'"
228 )
229 )
230
231 def post(uri: str, data: Dict[str, Any]):
232 return data_request(uri, "POST", data)
233
234 def patch(uri: str, data: Dict[str, Any]):
235 return data_request(uri, "PATCH", data)
236
237 with subtest("Can create objects"):
238 result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
239 site_id = result["id"]
240
241 # Example from:
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})
244
245 result = post(
246 "/dcim/manufacturers/",
247 {"name": "Test manufacturer", "slug": "test-manufacturer"}
248 )
249 manufacturer_id = result["id"]
250
251 # Had an issue with device-types before NetBox 3.4.0
252 result = post(
253 "/dcim/device-types/",
254 {
255 "model": "Test device type",
256 "manufacturer": manufacturer_id,
257 "slug": "test-device-type",
258 },
259 )
260 device_type_id = result["id"]
261
262 with subtest("Can list objects"):
263 result = get("/dcim/sites/")
264
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"] == ""
269
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"
274
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
280
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}/")
284
285 result = get("/dcim/device-types/")
286 assert result["count"] == 0
287
288 with subtest("Can use the GraphQL API"):
289 encoded_data = json.dumps({
290 "query": "query { prefix_list { prefix, site { id, description } } }",
291 })
292 result = json.loads(
293 machine.succeed(
294 "curl -sSfL "
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}'"
300 )
301 )
302
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
307
308 with subtest("Can login with LDAP"):
309 machine.wait_for_unit("openldap.service")
310 login("alice", "${testPassword}")
311
312 with subtest("Can associate LDAP groups"):
313 result = get("/users/users/?username=${testUser}")
314
315 assert result["count"] == 1
316 assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])
317 '';
318})