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