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