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