grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / netbox.nix
blob7bcbde2a018ea2ce4e38b5d1de8efa1a8af470d7
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.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     };
99     port = lib.mkOption {
100       type = lib.types.port;
101       default = 8001;
102       description = ''
103         Port the server will listen on.
104       '';
105     };
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     };
118     dataDir = lib.mkOption {
119       type = lib.types.str;
120       default = "/var/lib/netbox";
121       description = ''
122         Storage path of netbox.
123       '';
124     };
126     secretKeyFile = lib.mkOption {
127       type = lib.types.path;
128       description = ''
129         Path to a file containing the secret key.
130       '';
131     };
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     };
142     enableLdap = lib.mkOption {
143       type = lib.types.bool;
144       default = false;
145       description = ''
146         Enable LDAP-Authentication for Netbox.
148         This requires a configuration file being pass through `ldapConfigPath`.
149       '';
150     };
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
163         AUTH_LDAP_SERVER_URI = "ldaps://ldap.example.com/"
165         AUTH_LDAP_USER_SEARCH = LDAPSearch(
166             "ou=accounts,ou=posix,dc=example,dc=com",
167             ldap.SCOPE_SUBTREE,
168             "(uid=%(user)s)",
169         )
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()
178         # Mirror LDAP group assignments.
179         AUTH_LDAP_MIRROR_GROUPS = True
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   };
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";
203         GIT_PATH = "${pkgs.gitMinimal}/bin/git";
205         DATABASE = {
206           NAME = "netbox";
207           USER = "netbox";
208           HOST = "/run/postgresql";
209         };
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         };
227         REMOTE_AUTH_BACKEND = lib.mkIf cfg.enableLdap "netbox.authentication.LDAPBackend";
229         LOGGING = lib.mkDefault {
230           version = 1;
232           formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
234           handlers.console = {
235             class = "logging.StreamHandler";
236             formatter = "precise";
237           };
239           # log to console/systemd instead of file
240           root = {
241             level = "INFO";
242             handlers = [ "console" ];
243           };
244         };
245       };
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     };
256     services.redis.servers.netbox.enable = true;
258     services.postgresql = {
259       enable = true;
260       ensureDatabases = [ "netbox" ];
261       ensureUsers = [
262         {
263           name = "netbox";
264           ensureDBOwnership = true;
265         }
266       ];
267     };
269     environment.systemPackages = [ netboxManageScript ];
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     };
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/" ];
293         wantedBy = [ "netbox.target" ];
295         after = [ "network-online.target" ];
296         wants = [ "network-online.target" ];
298         environment.PYTHONPATH = pkg.pythonPath;
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"
305           if [[ -e "$versionFile" && "$(cat "$versionFile")" == "${cfg.package.version}" ]]; then
306             exit 0
307           fi
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"}
321           echo "${cfg.package.version}" > "$versionFile"
322         '';
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       };
334       netbox-rq = {
335         description = "NetBox Request Queue Worker";
336         documentation = [ "https://docs.netbox.dev/" ];
338         wantedBy = [ "netbox.target" ];
339         after = [ "netbox.service" ];
341         environment.PYTHONPATH = pkg.pythonPath;
343         serviceConfig = defaultServiceConfig // {
344           ExecStart = ''
345             ${pkg}/bin/netbox rqworker high default low
346           '';
347           PrivateTmp = true;
348         };
349       };
351       netbox-housekeeping = {
352         description = "NetBox housekeeping job";
353         documentation = [ "https://docs.netbox.dev/" ];
355         wantedBy = [ "multi-user.target" ];
357         after = [ "network-online.target" "netbox.service" ];
358         wants = [ "network-online.target" ];
360         environment.PYTHONPATH = pkg.pythonPath;
362         serviceConfig = defaultServiceConfig // {
363           Type = "oneshot";
364           ExecStart = ''
365             ${pkg}/bin/netbox housekeeping
366           '';
367         };
368       };
369     };
371     systemd.timers.netbox-housekeeping = {
372       description = "Run NetBox housekeeping job";
373       documentation = [ "https://docs.netbox.dev/" ];
375       wantedBy = [ "multi-user.target" ];
377       after = [ "network-online.target" "netbox.service" ];
378       wants = [ "network-online.target" ];
380       timerConfig = {
381         OnCalendar = "daily";
382         AccuracySec = "1h";
383         Persistent = true;
384       };
385     };
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   };