lib.packagesFromDirectoryRecursive: Improved documentation (#359898)
[NixPkgs.git] / nixos / modules / services / web-apps / netbox.nix
blob6f5e1536d616402b1318f75bd10d0d17db65729f
1 { config, lib, pkgs, ... }:
3 let
4   cfg = config.services.netbox;
5   pythonFmt = pkgs.formats.pythonVars {};
6   staticDir = cfg.dataDir + "/static";
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 ];
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   '');
30 in {
31   options.services.netbox = {
32     enable = lib.mkOption {
33       type = lib.types.bool;
34       default = false;
35       description = ''
36         Enable Netbox.
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     };
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       '';
49       default = { };
51       type = lib.types.submodule {
52         freeformType = pythonFmt.type;
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     };
67     listenAddress = lib.mkOption {
68       type = lib.types.str;
69       default = "[::1]";
70       description = ''
71         Address the server will listen on.
72       '';
73     };
75     package = lib.mkOption {
76       type = lib.types.package;
77       default =
78         if lib.versionAtLeast config.system.stateVersion "24.11"
79         then pkgs.netbox_4_1
80         else if lib.versionAtLeast config.system.stateVersion "24.05"
81         then pkgs.netbox_3_7
82         else pkgs.netbox_3_6;
83       defaultText = lib.literalExpression ''
84         if lib.versionAtLeast config.system.stateVersion "24.11"
85         then pkgs.netbox_4_1
86         else if lib.versionAtLeast config.system.stateVersion "24.05"
87         then pkgs.netbox_3_7
88         else pkgs.netbox_3_6;
89       '';
90       description = ''
91         NetBox package to use.
92       '';
93     };
95     port = lib.mkOption {
96       type = lib.types.port;
97       default = 8001;
98       description = ''
99         Port the server will listen on.
100       '';
101     };
103     plugins = lib.mkOption {
104       type = with lib.types; functionTo (listOf package);
105       default = _: [];
106       defaultText = lib.literalExpression ''
107         python3Packages: with python3Packages; [];
108       '';
109       description = ''
110         List of plugin packages to install.
111       '';
112     };
114     dataDir = lib.mkOption {
115       type = lib.types.str;
116       default = "/var/lib/netbox";
117       description = ''
118         Storage path of netbox.
119       '';
120     };
122     secretKeyFile = lib.mkOption {
123       type = lib.types.path;
124       description = ''
125         Path to a file containing the secret key.
126       '';
127     };
129     extraConfig = lib.mkOption {
130       type = lib.types.lines;
131       default = "";
132       description = ''
133         Additional lines of configuration appended to the `configuration.py`.
134         See the [documentation](https://docs.netbox.dev/en/stable/configuration/) for more possible options.
135       '';
136     };
138     enableLdap = lib.mkOption {
139       type = lib.types.bool;
140       default = false;
141       description = ''
142         Enable LDAP-Authentication for Netbox.
144         This requires a configuration file being pass through `ldapConfigPath`.
145       '';
146     };
148     ldapConfigPath = lib.mkOption {
149       type = lib.types.path;
150       default = "";
151       description = ''
152         Path to the Configuration-File for LDAP-Authentication, will be loaded as `ldap_config.py`.
153         See the [documentation](https://netbox.readthedocs.io/en/stable/installation/6-ldap/#configuration) for possible options.
154       '';
155       example = ''
156         import ldap
157         from django_auth_ldap.config import LDAPSearch, PosixGroupType
159         AUTH_LDAP_SERVER_URI = "ldaps://ldap.example.com/"
161         AUTH_LDAP_USER_SEARCH = LDAPSearch(
162             "ou=accounts,ou=posix,dc=example,dc=com",
163             ldap.SCOPE_SUBTREE,
164             "(uid=%(user)s)",
165         )
167         AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
168             "ou=groups,ou=posix,dc=example,dc=com",
169             ldap.SCOPE_SUBTREE,
170             "(objectClass=posixGroup)",
171         )
172         AUTH_LDAP_GROUP_TYPE = PosixGroupType()
174         # Mirror LDAP group assignments.
175         AUTH_LDAP_MIRROR_GROUPS = True
177         # For more granular permissions, we can map LDAP groups to Django groups.
178         AUTH_LDAP_FIND_GROUP_PERMS = True
179       '';
180     };
181     keycloakClientSecret = lib.mkOption {
182       type = with lib.types; nullOr path;
183       default = null;
184       description = ''
185         File that contains the keycloak client secret.
186       '';
187     };
188   };
190   config = lib.mkIf cfg.enable {
191     services.netbox = {
192       plugins = lib.mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]);
193       settings = {
194         STATIC_ROOT = staticDir;
195         MEDIA_ROOT = "${cfg.dataDir}/media";
196         REPORTS_ROOT = "${cfg.dataDir}/reports";
197         SCRIPTS_ROOT = "${cfg.dataDir}/scripts";
199         GIT_PATH = "${pkgs.gitMinimal}/bin/git";
201         DATABASE = {
202           NAME = "netbox";
203           USER = "netbox";
204           HOST = "/run/postgresql";
205         };
207         # Redis database settings. Redis is used for caching and for queuing
208         # background tasks such as webhook events. A separate configuration
209         # exists for each. Full connection details are required in both
210         # sections, and it is strongly recommended to use two separate database
211         # IDs.
212         REDIS = {
213             tasks = {
214                 URL = "unix://${config.services.redis.servers.netbox.unixSocket}?db=0";
215                 SSL = false;
216             };
217             caching =  {
218                 URL = "unix://${config.services.redis.servers.netbox.unixSocket}?db=1";
219                 SSL = false;
220             };
221         };
223         REMOTE_AUTH_BACKEND = lib.mkIf cfg.enableLdap "netbox.authentication.LDAPBackend";
225         LOGGING = lib.mkDefault {
226           version = 1;
228           formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
230           handlers.console = {
231             class = "logging.StreamHandler";
232             formatter = "precise";
233           };
235           # log to console/systemd instead of file
236           root = {
237             level = "INFO";
238             handlers = [ "console" ];
239           };
240         };
241       };
243       extraConfig = ''
244         with open("${cfg.secretKeyFile}", "r") as file:
245             SECRET_KEY = file.readline()
246       '' + (lib.optionalString (cfg.keycloakClientSecret != null) ''
247         with open("${cfg.keycloakClientSecret}", "r") as file:
248             SOCIAL_AUTH_KEYCLOAK_SECRET = file.readline()
249       '');
250     };
252     services.redis.servers.netbox.enable = true;
254     services.postgresql = {
255       enable = true;
256       ensureDatabases = [ "netbox" ];
257       ensureUsers = [
258         {
259           name = "netbox";
260           ensureDBOwnership = true;
261         }
262       ];
263     };
265     environment.systemPackages = [ netboxManageScript ];
267     systemd.targets.netbox = {
268       description = "Target for all NetBox services";
269       wantedBy = [ "multi-user.target" ];
270       wants = [ "network-online.target" ];
271       after = [ "network-online.target" "redis-netbox.service" ];
272     };
274     systemd.services = let
275       defaultServiceConfig = {
276         WorkingDirectory = "${cfg.dataDir}";
277         User = "netbox";
278         Group = "netbox";
279         StateDirectory = "netbox";
280         StateDirectoryMode = "0750";
281         Restart = "on-failure";
282         RestartSec = 30;
283       };
284     in {
285       netbox = {
286         description = "NetBox WSGI Service";
287         documentation = [ "https://docs.netbox.dev/" ];
289         wantedBy = [ "netbox.target" ];
291         after = [ "network-online.target" ];
292         wants = [ "network-online.target" ];
294         environment.PYTHONPATH = pkg.pythonPath;
296         preStart = ''
297           # On the first run, or on upgrade / downgrade, run migrations and related.
298           # This mostly correspond to upstream NetBox's 'upgrade.sh' script.
299           versionFile="${cfg.dataDir}/version"
301           if [[ -h "$versionFile" && "$(readlink -- "$versionFile")" == "${cfg.package}" ]]; then
302             exit 0
303           fi
305           ${pkg}/bin/netbox migrate
306           ${pkg}/bin/netbox trace_paths --no-input
307           ${pkg}/bin/netbox collectstatic --clear --no-input
308           ${pkg}/bin/netbox remove_stale_contenttypes --no-input
309           ${pkg}/bin/netbox reindex --lazy
310           ${pkg}/bin/netbox clearsessions
311           ${lib.optionalString
312             # The clearcache command was removed in 3.7.0:
313             # https://github.com/netbox-community/netbox/issues/14458
314             (lib.versionOlder cfg.package.version "3.7.0")
315             "${pkg}/bin/netbox clearcache"}
317           ln -sfn "${cfg.package}" "$versionFile"
318         '';
320         serviceConfig = defaultServiceConfig // {
321           ExecStart = ''
322             ${pkg.gunicorn}/bin/gunicorn netbox.wsgi \
323               --bind ${cfg.listenAddress}:${toString cfg.port} \
324               --pythonpath ${pkg}/opt/netbox/netbox
325           '';
326           PrivateTmp = true;
327           TimeoutStartSec = lib.mkDefault "5min";
328         };
329       };
331       netbox-rq = {
332         description = "NetBox Request Queue Worker";
333         documentation = [ "https://docs.netbox.dev/" ];
335         wantedBy = [ "netbox.target" ];
336         after = [ "netbox.service" ];
338         environment.PYTHONPATH = pkg.pythonPath;
340         serviceConfig = defaultServiceConfig // {
341           ExecStart = ''
342             ${pkg}/bin/netbox rqworker high default low
343           '';
344           PrivateTmp = true;
345         };
346       };
348       netbox-housekeeping = {
349         description = "NetBox housekeeping job";
350         documentation = [ "https://docs.netbox.dev/" ];
352         wantedBy = [ "multi-user.target" ];
354         after = [ "network-online.target" "netbox.service" ];
355         wants = [ "network-online.target" ];
357         environment.PYTHONPATH = pkg.pythonPath;
359         serviceConfig = defaultServiceConfig // {
360           Type = "oneshot";
361           ExecStart = ''
362             ${pkg}/bin/netbox housekeeping
363           '';
364         };
365       };
366     };
368     systemd.timers.netbox-housekeeping = {
369       description = "Run NetBox housekeeping job";
370       documentation = [ "https://docs.netbox.dev/" ];
372       wantedBy = [ "multi-user.target" ];
374       after = [ "network-online.target" "netbox.service" ];
375       wants = [ "network-online.target" ];
377       timerConfig = {
378         OnCalendar = "daily";
379         AccuracySec = "1h";
380         Persistent = true;
381       };
382     };
384     users.users.netbox = {
385       home = "${cfg.dataDir}";
386       isSystemUser = true;
387       group = "netbox";
388     };
389     users.groups.netbox = {};
390     users.groups."${config.services.redis.servers.netbox.user}".members = [ "netbox" ];
391   };