2023-04-12 12:48:02 +00:00
|
|
|
let
|
|
|
|
ldapDomain = "example.org";
|
|
|
|
ldapSuffix = "dc=example,dc=org";
|
|
|
|
|
|
|
|
ldapRootUser = "admin";
|
|
|
|
ldapRootPassword = "foobar";
|
|
|
|
|
|
|
|
testUser = "alice";
|
|
|
|
testPassword = "verySecure";
|
|
|
|
testGroup = "netbox-users";
|
|
|
|
in import ../make-test-python.nix ({ lib, pkgs, netbox, ... }: {
|
2022-04-03 18:54:34 +00:00
|
|
|
name = "netbox";
|
|
|
|
|
|
|
|
meta = with lib.maintainers; {
|
2023-04-12 12:48:02 +00:00
|
|
|
maintainers = [ minijackson n0emis ];
|
2022-04-03 18:54:34 +00:00
|
|
|
};
|
|
|
|
|
2023-04-12 12:48:02 +00:00
|
|
|
nodes.machine = { config, ... }: {
|
2022-04-03 18:54:34 +00:00
|
|
|
services.netbox = {
|
|
|
|
enable = true;
|
2023-04-12 12:48:02 +00:00
|
|
|
package = netbox;
|
2022-04-03 18:54:34 +00:00
|
|
|
secretKeyFile = pkgs.writeText "secret" ''
|
|
|
|
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
|
|
|
|
'';
|
2023-04-12 12:48:02 +00:00
|
|
|
|
|
|
|
enableLdap = true;
|
|
|
|
ldapConfigPath = pkgs.writeText "ldap_config.py" ''
|
|
|
|
import ldap
|
|
|
|
from django_auth_ldap.config import LDAPSearch, PosixGroupType
|
|
|
|
|
|
|
|
AUTH_LDAP_SERVER_URI = "ldap://localhost/"
|
|
|
|
|
|
|
|
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
|
|
|
"ou=accounts,ou=posix,${ldapSuffix}",
|
|
|
|
ldap.SCOPE_SUBTREE,
|
|
|
|
"(uid=%(user)s)",
|
|
|
|
)
|
|
|
|
|
|
|
|
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
|
|
|
|
"ou=groups,ou=posix,${ldapSuffix}",
|
|
|
|
ldap.SCOPE_SUBTREE,
|
|
|
|
"(objectClass=posixGroup)",
|
|
|
|
)
|
|
|
|
AUTH_LDAP_GROUP_TYPE = PosixGroupType()
|
|
|
|
|
|
|
|
# Mirror LDAP group assignments.
|
|
|
|
AUTH_LDAP_MIRROR_GROUPS = True
|
|
|
|
|
|
|
|
# For more granular permissions, we can map LDAP groups to Django groups.
|
|
|
|
AUTH_LDAP_FIND_GROUP_PERMS = True
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
services.nginx = {
|
|
|
|
enable = true;
|
|
|
|
|
|
|
|
recommendedProxySettings = true;
|
|
|
|
|
|
|
|
virtualHosts.netbox = {
|
|
|
|
default = true;
|
|
|
|
locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}";
|
|
|
|
locations."/static/".alias = "/var/lib/netbox/static/";
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
# Adapted from the sssd-ldap NixOS test
|
|
|
|
services.openldap = {
|
|
|
|
enable = true;
|
|
|
|
settings = {
|
|
|
|
children = {
|
|
|
|
"cn=schema".includes = [
|
|
|
|
"${pkgs.openldap}/etc/schema/core.ldif"
|
|
|
|
"${pkgs.openldap}/etc/schema/cosine.ldif"
|
|
|
|
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
|
|
|
|
"${pkgs.openldap}/etc/schema/nis.ldif"
|
|
|
|
];
|
|
|
|
"olcDatabase={1}mdb" = {
|
|
|
|
attrs = {
|
|
|
|
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
|
|
|
|
olcDatabase = "{1}mdb";
|
|
|
|
olcDbDirectory = "/var/lib/openldap/db";
|
|
|
|
olcSuffix = ldapSuffix;
|
|
|
|
olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
|
|
|
|
olcRootPW = ldapRootPassword;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
declarativeContents = {
|
|
|
|
${ldapSuffix} = ''
|
|
|
|
dn: ${ldapSuffix}
|
|
|
|
objectClass: top
|
|
|
|
objectClass: dcObject
|
|
|
|
objectClass: organization
|
|
|
|
o: ${ldapDomain}
|
|
|
|
|
|
|
|
dn: ou=posix,${ldapSuffix}
|
|
|
|
objectClass: top
|
|
|
|
objectClass: organizationalUnit
|
|
|
|
|
|
|
|
dn: ou=accounts,ou=posix,${ldapSuffix}
|
|
|
|
objectClass: top
|
|
|
|
objectClass: organizationalUnit
|
|
|
|
|
|
|
|
dn: uid=${testUser},ou=accounts,ou=posix,${ldapSuffix}
|
|
|
|
objectClass: person
|
|
|
|
objectClass: posixAccount
|
|
|
|
userPassword: ${testPassword}
|
|
|
|
homeDirectory: /home/${testUser}
|
|
|
|
uidNumber: 1234
|
|
|
|
gidNumber: 1234
|
|
|
|
cn: ""
|
|
|
|
sn: ""
|
|
|
|
|
|
|
|
dn: ou=groups,ou=posix,${ldapSuffix}
|
|
|
|
objectClass: top
|
|
|
|
objectClass: organizationalUnit
|
|
|
|
|
|
|
|
dn: cn=${testGroup},ou=groups,ou=posix,${ldapSuffix}
|
|
|
|
objectClass: posixGroup
|
|
|
|
gidNumber: 2345
|
|
|
|
memberUid: ${testUser}
|
|
|
|
'';
|
|
|
|
};
|
2022-04-03 18:54:34 +00:00
|
|
|
};
|
2023-04-12 12:48:02 +00:00
|
|
|
|
|
|
|
users.users.nginx.extraGroups = [ "netbox" ];
|
|
|
|
|
|
|
|
networking.firewall.allowedTCPPorts = [ 80 ];
|
2022-04-03 18:54:34 +00:00
|
|
|
};
|
|
|
|
|
2023-04-12 12:48:02 +00:00
|
|
|
testScript = let
|
|
|
|
changePassword = pkgs.writeText "change-password.py" ''
|
|
|
|
from django.contrib.auth.models import User
|
|
|
|
u = User.objects.get(username='netbox')
|
|
|
|
u.set_password('netbox')
|
|
|
|
u.save()
|
|
|
|
'';
|
|
|
|
in ''
|
|
|
|
from typing import Any, Dict
|
|
|
|
import json
|
|
|
|
|
|
|
|
start_all()
|
2022-04-03 18:54:34 +00:00
|
|
|
machine.wait_for_unit("netbox.target")
|
|
|
|
machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
|
|
|
|
|
|
|
|
with subtest("Home screen loads"):
|
|
|
|
machine.succeed(
|
|
|
|
"curl -sSfL http://[::1]:8001 | grep '<title>Home | NetBox</title>'"
|
|
|
|
)
|
|
|
|
|
|
|
|
with subtest("Staticfiles are generated"):
|
|
|
|
machine.succeed("test -e /var/lib/netbox/static/netbox.js")
|
2023-04-12 12:48:02 +00:00
|
|
|
|
|
|
|
with subtest("Superuser can be created"):
|
|
|
|
machine.succeed(
|
|
|
|
"netbox-manage createsuperuser --noinput --username netbox --email netbox@example.com"
|
|
|
|
)
|
|
|
|
# Django doesn't have a "clean" way of inputting the password from the command line
|
|
|
|
machine.succeed("cat '${changePassword}' | netbox-manage shell")
|
|
|
|
|
|
|
|
machine.wait_for_unit("network.target")
|
|
|
|
|
|
|
|
with subtest("Home screen loads from nginx"):
|
|
|
|
machine.succeed(
|
|
|
|
"curl -sSfL http://localhost | grep '<title>Home | NetBox</title>'"
|
|
|
|
)
|
|
|
|
|
|
|
|
with subtest("Staticfiles can be fetched"):
|
|
|
|
machine.succeed("curl -sSfL http://localhost/static/netbox.js")
|
|
|
|
machine.succeed("curl -sSfL http://localhost/static/docs/")
|
|
|
|
|
|
|
|
with subtest("Can interact with API"):
|
|
|
|
json.loads(
|
|
|
|
machine.succeed("curl -sSfL -H 'Accept: application/json' 'http://localhost/api/'")
|
|
|
|
)
|
|
|
|
|
|
|
|
def login(username: str, password: str):
|
|
|
|
encoded_data = json.dumps({"username": username, "password": password})
|
|
|
|
uri = "/users/tokens/provision/"
|
|
|
|
result = json.loads(
|
|
|
|
machine.succeed(
|
|
|
|
"curl -sSfL "
|
|
|
|
"-X POST "
|
|
|
|
"-H 'Accept: application/json' "
|
|
|
|
"-H 'Content-Type: application/json' "
|
|
|
|
f"'http://localhost/api{uri}' "
|
|
|
|
f"--data '{encoded_data}'"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return result["key"]
|
|
|
|
|
|
|
|
with subtest("Can login"):
|
|
|
|
auth_token = login("netbox", "netbox")
|
|
|
|
|
|
|
|
def get(uri: str):
|
|
|
|
return json.loads(
|
|
|
|
machine.succeed(
|
|
|
|
"curl -sSfL "
|
|
|
|
"-H 'Accept: application/json' "
|
|
|
|
f"-H 'Authorization: Token {auth_token}' "
|
|
|
|
f"'http://localhost/api{uri}'"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
def delete(uri: str):
|
|
|
|
return machine.succeed(
|
|
|
|
"curl -sSfL "
|
|
|
|
f"-X DELETE "
|
|
|
|
"-H 'Accept: application/json' "
|
|
|
|
f"-H 'Authorization: Token {auth_token}' "
|
|
|
|
f"'http://localhost/api{uri}'"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def data_request(uri: str, method: str, data: Dict[str, Any]):
|
|
|
|
encoded_data = json.dumps(data)
|
|
|
|
return json.loads(
|
|
|
|
machine.succeed(
|
|
|
|
"curl -sSfL "
|
|
|
|
f"-X {method} "
|
|
|
|
"-H 'Accept: application/json' "
|
|
|
|
"-H 'Content-Type: application/json' "
|
|
|
|
f"-H 'Authorization: Token {auth_token}' "
|
|
|
|
f"'http://localhost/api{uri}' "
|
|
|
|
f"--data '{encoded_data}'"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
def post(uri: str, data: Dict[str, Any]):
|
|
|
|
return data_request(uri, "POST", data)
|
|
|
|
|
|
|
|
def patch(uri: str, data: Dict[str, Any]):
|
|
|
|
return data_request(uri, "PATCH", data)
|
|
|
|
|
|
|
|
with subtest("Can create objects"):
|
|
|
|
result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
|
|
|
|
site_id = result["id"]
|
|
|
|
|
|
|
|
# Example from:
|
|
|
|
# http://netbox.extra.cea.fr/static/docs/integrations/rest-api/#creating-a-new-object
|
|
|
|
post("/ipam/prefixes/", {"prefix": "192.0.2.0/24", "site": site_id})
|
|
|
|
|
|
|
|
result = post(
|
|
|
|
"/dcim/manufacturers/",
|
|
|
|
{"name": "Test manufacturer", "slug": "test-manufacturer"}
|
|
|
|
)
|
|
|
|
manufacturer_id = result["id"]
|
|
|
|
|
|
|
|
# Had an issue with device-types before NetBox 3.4.0
|
|
|
|
result = post(
|
|
|
|
"/dcim/device-types/",
|
|
|
|
{
|
|
|
|
"model": "Test device type",
|
|
|
|
"manufacturer": manufacturer_id,
|
|
|
|
"slug": "test-device-type",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
device_type_id = result["id"]
|
|
|
|
|
|
|
|
with subtest("Can list objects"):
|
|
|
|
result = get("/dcim/sites/")
|
|
|
|
|
|
|
|
assert result["count"] == 1
|
|
|
|
assert result["results"][0]["id"] == site_id
|
|
|
|
assert result["results"][0]["name"] == "Test site"
|
|
|
|
assert result["results"][0]["description"] == ""
|
|
|
|
|
|
|
|
result = get("/dcim/device-types/")
|
|
|
|
assert result["count"] == 1
|
|
|
|
assert result["results"][0]["id"] == device_type_id
|
|
|
|
assert result["results"][0]["model"] == "Test device type"
|
|
|
|
|
|
|
|
with subtest("Can update objects"):
|
|
|
|
new_description = "Test site description"
|
|
|
|
patch(f"/dcim/sites/{site_id}/", {"description": new_description})
|
|
|
|
result = get(f"/dcim/sites/{site_id}/")
|
|
|
|
assert result["description"] == new_description
|
|
|
|
|
|
|
|
with subtest("Can delete objects"):
|
|
|
|
# Delete a device-type since no object depends on it
|
|
|
|
delete(f"/dcim/device-types/{device_type_id}/")
|
|
|
|
|
|
|
|
result = get("/dcim/device-types/")
|
|
|
|
assert result["count"] == 0
|
|
|
|
|
|
|
|
with subtest("Can use the GraphQL API"):
|
|
|
|
encoded_data = json.dumps({
|
|
|
|
"query": "query { prefix_list { prefix, site { id, description } } }",
|
|
|
|
})
|
|
|
|
result = json.loads(
|
|
|
|
machine.succeed(
|
|
|
|
"curl -sSfL "
|
|
|
|
"-H 'Accept: application/json' "
|
|
|
|
"-H 'Content-Type: application/json' "
|
|
|
|
f"-H 'Authorization: Token {auth_token}' "
|
|
|
|
"'http://localhost/graphql/' "
|
|
|
|
f"--data '{encoded_data}'"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
assert len(result["data"]["prefix_list"]) == 1
|
|
|
|
assert result["data"]["prefix_list"][0]["prefix"] == "192.0.2.0/24"
|
|
|
|
assert result["data"]["prefix_list"][0]["site"]["id"] == str(site_id)
|
|
|
|
assert result["data"]["prefix_list"][0]["site"]["description"] == new_description
|
|
|
|
|
|
|
|
with subtest("Can login with LDAP"):
|
|
|
|
machine.wait_for_unit("openldap.service")
|
|
|
|
login("alice", "${testPassword}")
|
|
|
|
|
|
|
|
with subtest("Can associate LDAP groups"):
|
|
|
|
result = get("/users/users/?username=${testUser}")
|
|
|
|
|
|
|
|
assert result["count"] == 1
|
|
|
|
assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])
|
2022-04-03 18:54:34 +00:00
|
|
|
'';
|
|
|
|
})
|