1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.netbox;
10 pythonFmt = pkgs.formats.pythonVars { };
11 staticDir = cfg.dataDir + "/static";
12
13 settingsFile = pythonFmt.generate "netbox-settings.py" cfg.settings;
14 extraConfigFile = pkgs.writeTextFile {
15 name = "netbox-extraConfig.py";
16 text = cfg.extraConfig;
17 };
18 configFile = pkgs.concatText "configuration.py" [
19 settingsFile
20 extraConfigFile
21 ];
22
23 pkg =
24 (cfg.package.overrideAttrs (old: {
25 installPhase =
26 old.installPhase
27 + ''
28 ln -s ${configFile} $out/opt/netbox/netbox/netbox/configuration.py
29 ''
30 + lib.optionalString cfg.enableLdap ''
31 ln -s ${cfg.ldapConfigPath} $out/opt/netbox/netbox/netbox/ldap_config.py
32 '';
33 })).override
34 {
35 inherit (cfg) plugins;
36 };
37 netboxManageScript =
38 with pkgs;
39 (writeScriptBin "netbox-manage" ''
40 #!${stdenv.shell}
41 export PYTHONPATH=${pkg.pythonPath}
42 case "$(whoami)" in
43 "root")
44 ${util-linux}/bin/runuser -u netbox -- ${pkg}/bin/netbox "$@";;
45 "netbox")
46 ${pkg}/bin/netbox "$@";;
47 *)
48 echo "This must be run by either by root 'netbox' user"
49 esac
50 '');
51
52in
53{
54 options.services.netbox = {
55 enable = lib.mkOption {
56 type = lib.types.bool;
57 default = false;
58 description = ''
59 Enable Netbox.
60
61 This module requires a reverse proxy that serves `/static` separately.
62 See this [example](https://github.com/netbox-community/netbox/blob/develop/contrib/nginx.conf/) on how to configure this.
63 '';
64 };
65
66 settings = lib.mkOption {
67 description = ''
68 Configuration options to set in `configuration.py`.
69 See the [documentation](https://docs.netbox.dev/en/stable/configuration/) for more possible options.
70 '';
71
72 default = { };
73
74 type = lib.types.submodule {
75 freeformType = pythonFmt.type;
76
77 options = {
78 ALLOWED_HOSTS = lib.mkOption {
79 type = with lib.types; listOf str;
80 default = [ "*" ];
81 description = ''
82 A list of valid fully-qualified domain names (FQDNs) and/or IP
83 addresses that can be used to reach the NetBox service.
84 '';
85 };
86 };
87 };
88 };
89
90 listenAddress = lib.mkOption {
91 type = lib.types.str;
92 default = "[::1]";
93 description = ''
94 Address the server will listen on.
95 Ignored if `unixSocket` is set.
96 '';
97 };
98
99 unixSocket = lib.mkOption {
100 type = lib.types.nullOr lib.types.str;
101 default = null;
102 description = ''
103 Enable Unix Socket for the server to listen on.
104 `listenAddress` and `port` will be ignored.
105 '';
106 example = "/run/netbox/netbox.sock";
107 };
108
109 package = lib.mkOption {
110 type = lib.types.package;
111 default =
112 if lib.versionAtLeast config.system.stateVersion "25.11" then
113 pkgs.netbox_4_3
114 else if lib.versionAtLeast config.system.stateVersion "25.05" then
115 pkgs.netbox_4_2
116 else
117 pkgs.netbox_4_1;
118 defaultText = lib.literalExpression ''
119 if lib.versionAtLeast config.system.stateVersion "25.11"
120 then pkgs.netbox_4_3
121 else if lib.versionAtLeast config.system.stateVersion "25.05"
122 then pkgs.netbox_4_2
123 else pkgs.netbox_4_1;
124 '';
125 description = ''
126 NetBox package to use.
127 '';
128 };
129
130 port = lib.mkOption {
131 type = lib.types.port;
132 default = 8001;
133 description = ''
134 Port the server will listen on.
135 Ignored if `unixSocket` is set.
136 '';
137 };
138
139 plugins = lib.mkOption {
140 type = with lib.types; functionTo (listOf package);
141 default = _: [ ];
142 defaultText = lib.literalExpression ''
143 python3Packages: with python3Packages; [];
144 '';
145 description = ''
146 List of plugin packages to install.
147 '';
148 };
149
150 dataDir = lib.mkOption {
151 type = lib.types.str;
152 default = "/var/lib/netbox";
153 description = ''
154 Storage path of netbox.
155 '';
156 };
157
158 secretKeyFile = lib.mkOption {
159 type = lib.types.path;
160 description = ''
161 Path to a file containing the secret key.
162 '';
163 };
164
165 extraConfig = lib.mkOption {
166 type = lib.types.lines;
167 default = "";
168 description = ''
169 Additional lines of configuration appended to the `configuration.py`.
170 See the [documentation](https://docs.netbox.dev/en/stable/configuration/) for more possible options.
171 '';
172 };
173
174 enableLdap = lib.mkOption {
175 type = lib.types.bool;
176 default = false;
177 description = ''
178 Enable LDAP-Authentication for Netbox.
179
180 This requires a configuration file being pass through `ldapConfigPath`.
181 '';
182 };
183
184 ldapConfigPath = lib.mkOption {
185 type = lib.types.path;
186 default = "";
187 description = ''
188 Path to the Configuration-File for LDAP-Authentication, will be loaded as `ldap_config.py`.
189 See the [documentation](https://netbox.readthedocs.io/en/stable/installation/6-ldap/#configuration) for possible options.
190 '';
191 example = ''
192 import ldap
193 from django_auth_ldap.config import LDAPSearch, PosixGroupType
194
195 AUTH_LDAP_SERVER_URI = "ldaps://ldap.example.com/"
196
197 AUTH_LDAP_USER_SEARCH = LDAPSearch(
198 "ou=accounts,ou=posix,dc=example,dc=com",
199 ldap.SCOPE_SUBTREE,
200 "(uid=%(user)s)",
201 )
202
203 AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
204 "ou=groups,ou=posix,dc=example,dc=com",
205 ldap.SCOPE_SUBTREE,
206 "(objectClass=posixGroup)",
207 )
208 AUTH_LDAP_GROUP_TYPE = PosixGroupType()
209
210 # Mirror LDAP group assignments.
211 AUTH_LDAP_MIRROR_GROUPS = True
212
213 # For more granular permissions, we can map LDAP groups to Django groups.
214 AUTH_LDAP_FIND_GROUP_PERMS = True
215 '';
216 };
217 keycloakClientSecret = lib.mkOption {
218 type = with lib.types; nullOr path;
219 default = null;
220 description = ''
221 File that contains the keycloak client secret.
222 '';
223 };
224 };
225
226 config = lib.mkIf cfg.enable {
227 services.netbox = {
228 plugins = lib.mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]);
229 settings = {
230 STATIC_ROOT = staticDir;
231 MEDIA_ROOT = "${cfg.dataDir}/media";
232 REPORTS_ROOT = "${cfg.dataDir}/reports";
233 SCRIPTS_ROOT = "${cfg.dataDir}/scripts";
234
235 GIT_PATH = "${pkgs.gitMinimal}/bin/git";
236
237 DATABASE = {
238 NAME = "netbox";
239 USER = "netbox";
240 HOST = "/run/postgresql";
241 };
242
243 # Redis database settings. Redis is used for caching and for queuing
244 # background tasks such as webhook events. A separate configuration
245 # exists for each. Full connection details are required in both
246 # sections, and it is strongly recommended to use two separate database
247 # IDs.
248 REDIS = {
249 tasks = {
250 URL = "unix://${config.services.redis.servers.netbox.unixSocket}?db=0";
251 SSL = false;
252 };
253 caching = {
254 URL = "unix://${config.services.redis.servers.netbox.unixSocket}?db=1";
255 SSL = false;
256 };
257 };
258
259 REMOTE_AUTH_BACKEND = lib.mkIf cfg.enableLdap "netbox.authentication.LDAPBackend";
260
261 LOGGING = lib.mkDefault {
262 version = 1;
263
264 formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
265
266 handlers.console = {
267 class = "logging.StreamHandler";
268 formatter = "precise";
269 };
270
271 # log to console/systemd instead of file
272 root = {
273 level = "INFO";
274 handlers = [ "console" ];
275 };
276 };
277 };
278
279 extraConfig = ''
280 with open("${cfg.secretKeyFile}", "r") as file:
281 SECRET_KEY = file.readline()
282 ''
283 + (lib.optionalString (cfg.keycloakClientSecret != null) ''
284 with open("${cfg.keycloakClientSecret}", "r") as file:
285 SOCIAL_AUTH_KEYCLOAK_SECRET = file.readline()
286 '');
287 };
288
289 services.redis.servers.netbox.enable = true;
290
291 services.postgresql = {
292 enable = true;
293 ensureDatabases = [ "netbox" ];
294 ensureUsers = [
295 {
296 name = "netbox";
297 ensureDBOwnership = true;
298 }
299 ];
300 };
301
302 environment.systemPackages = [ netboxManageScript ];
303
304 systemd.targets.netbox = {
305 description = "Target for all NetBox services";
306 wantedBy = [ "multi-user.target" ];
307 wants = [ "network-online.target" ];
308 after = [
309 "network-online.target"
310 "redis-netbox.service"
311 ];
312 };
313
314 systemd.services =
315 let
316 defaultServiceConfig = {
317 WorkingDirectory = "${cfg.dataDir}";
318 User = "netbox";
319 Group = "netbox";
320 StateDirectory = "netbox";
321 StateDirectoryMode = "0750";
322 Restart = "on-failure";
323 RestartSec = 30;
324 };
325 in
326 {
327 netbox = {
328 description = "NetBox WSGI Service";
329 documentation = [ "https://docs.netbox.dev/" ];
330
331 wantedBy = [ "netbox.target" ];
332
333 after = [ "network-online.target" ];
334 wants = [ "network-online.target" ];
335
336 environment.PYTHONPATH = pkg.pythonPath;
337
338 preStart = ''
339 # On the first run, or on upgrade / downgrade, run migrations and related.
340 # This mostly correspond to upstream NetBox's 'upgrade.sh' script.
341 versionFile="${cfg.dataDir}/version"
342
343 if [[ -h "$versionFile" && "$(readlink -- "$versionFile")" == "${cfg.package}" ]]; then
344 exit 0
345 fi
346
347 ${pkg}/bin/netbox migrate
348 ${pkg}/bin/netbox trace_paths --no-input
349 ${pkg}/bin/netbox collectstatic --clear --no-input
350 ${pkg}/bin/netbox remove_stale_contenttypes --no-input
351 ${pkg}/bin/netbox reindex --lazy
352 ${pkg}/bin/netbox clearsessions
353 ${lib.optionalString
354 # The clearcache command was removed in 3.7.0:
355 # https://github.com/netbox-community/netbox/issues/14458
356 (lib.versionOlder cfg.package.version "3.7.0")
357 "${pkg}/bin/netbox clearcache"
358 }
359
360 ln -sfn "${cfg.package}" "$versionFile"
361 '';
362
363 serviceConfig = defaultServiceConfig // {
364 ExecStart = ''
365 ${pkg.gunicorn}/bin/gunicorn netbox.wsgi \
366 --bind ${
367 if (cfg.unixSocket != null) then
368 "unix:${cfg.unixSocket}"
369 else
370 "${cfg.listenAddress}:${toString cfg.port}"
371 } \
372 --pythonpath ${pkg}/opt/netbox/netbox
373 '';
374 PrivateTmp = true;
375 TimeoutStartSec = lib.mkDefault "5min";
376 };
377 };
378
379 netbox-rq = {
380 description = "NetBox Request Queue Worker";
381 documentation = [ "https://docs.netbox.dev/" ];
382
383 wantedBy = [ "netbox.target" ];
384 after = [ "netbox.service" ];
385
386 environment.PYTHONPATH = pkg.pythonPath;
387
388 serviceConfig = defaultServiceConfig // {
389 ExecStart = ''
390 ${pkg}/bin/netbox rqworker high default low
391 '';
392 PrivateTmp = true;
393 };
394 };
395
396 netbox-housekeeping = {
397 description = "NetBox housekeeping job";
398 documentation = [ "https://docs.netbox.dev/" ];
399
400 wantedBy = [ "multi-user.target" ];
401
402 after = [
403 "network-online.target"
404 "netbox.service"
405 ];
406 wants = [ "network-online.target" ];
407
408 environment.PYTHONPATH = pkg.pythonPath;
409
410 serviceConfig = defaultServiceConfig // {
411 Type = "oneshot";
412 ExecStart = ''
413 ${pkg}/bin/netbox housekeeping
414 '';
415 };
416 };
417 };
418
419 systemd.timers.netbox-housekeeping = {
420 description = "Run NetBox housekeeping job";
421 documentation = [ "https://docs.netbox.dev/" ];
422
423 wantedBy = [ "multi-user.target" ];
424
425 after = [
426 "network-online.target"
427 "netbox.service"
428 ];
429 wants = [ "network-online.target" ];
430
431 timerConfig = {
432 OnCalendar = "daily";
433 AccuracySec = "1h";
434 Persistent = true;
435 };
436 };
437
438 users.users.netbox = {
439 home = "${cfg.dataDir}";
440 isSystemUser = true;
441 group = "netbox";
442 };
443 users.groups.netbox = { };
444 users.groups."${config.services.redis.servers.netbox.user}".members = [ "netbox" ];
445 };
446}