1{ config, lib, pkgs, ... }:
2
3let
4 cfg = config.services.zoneminder;
5 fpm = config.services.phpfpm.pools.zoneminder;
6 pkg = pkgs.zoneminder;
7
8 dirName = pkg.dirName;
9
10 user = "zoneminder";
11 group = {
12 nginx = config.services.nginx.group;
13 none = user;
14 }.${cfg.webserver};
15
16 useNginx = cfg.webserver == "nginx";
17
18 defaultDir = "/var/lib/${user}";
19 home = if useCustomDir then cfg.storageDir else defaultDir;
20
21 useCustomDir = cfg.storageDir != null;
22
23 zms = "/cgi-bin/zms";
24
25 dirs = dirList: [ dirName ] ++ map (e: "${dirName}/${e}") dirList;
26
27 cacheDirs = [ "swap" ];
28 libDirs = [ "events" "exports" "images" "sounds" ];
29
30 dirStanzas = baseDir:
31 lib.concatStringsSep "\n" (map (e:
32 "ZM_DIR_${lib.toUpper e}=${baseDir}/${e}"
33 ) libDirs);
34
35 defaultsFile = pkgs.writeText "60-defaults.conf" ''
36 # 01-system-paths.conf
37 ${dirStanzas home}
38 ZM_PATH_ARP=${lib.getBin pkgs.nettools}/bin/arp
39 ZM_PATH_LOGS=/var/log/${dirName}
40 ZM_PATH_MAP=/dev/shm
41 ZM_PATH_SOCKS=/run/${dirName}
42 ZM_PATH_SWAP=/var/cache/${dirName}/swap
43 ZM_PATH_ZMS=${zms}
44
45 # 02-multiserver.conf
46 ZM_SERVER_HOST=
47
48 # Database
49 ZM_DB_TYPE=mysql
50 ZM_DB_HOST=${cfg.database.host}
51 ZM_DB_NAME=${cfg.database.name}
52 ZM_DB_USER=${cfg.database.username}
53 ZM_DB_PASS=${cfg.database.password}
54
55 # Web
56 ZM_WEB_USER=${user}
57 ZM_WEB_GROUP=${group}
58 '';
59
60 configFile = pkgs.writeText "80-nixos.conf" ''
61 # You can override defaults here
62
63 ${cfg.extraConfig}
64 '';
65
66in {
67 options = {
68 services.zoneminder = with lib; {
69 enable = lib.mkEnableOption (lib.mdDoc ''
70 ZoneMinder
71
72 If you intend to run the database locally, you should set
73 `config.services.zoneminder.database.createLocally` to true. Otherwise,
74 when set to `false` (the default), you will have to create the database
75 and database user as well as populate the database yourself.
76 Additionally, you will need to run `zmupdate.pl` yourself when
77 upgrading to a newer version.
78 '');
79
80 webserver = mkOption {
81 type = types.enum [ "nginx" "none" ];
82 default = "nginx";
83 description = lib.mdDoc ''
84 The webserver to configure for the PHP frontend.
85
86 Set it to `none` if you want to configure it yourself. PRs are welcome
87 for support for other web servers.
88 '';
89 };
90
91 hostname = mkOption {
92 type = types.str;
93 default = "localhost";
94 description = lib.mdDoc ''
95 The hostname on which to listen.
96 '';
97 };
98
99 port = mkOption {
100 type = types.port;
101 default = 8095;
102 description = lib.mdDoc ''
103 The port on which to listen.
104 '';
105 };
106
107 openFirewall = mkOption {
108 type = types.bool;
109 default = false;
110 description = lib.mdDoc ''
111 Open the firewall port(s).
112 '';
113 };
114
115 database = {
116 createLocally = mkOption {
117 type = types.bool;
118 default = false;
119 description = lib.mdDoc ''
120 Create the database and database user locally.
121 '';
122 };
123
124 host = mkOption {
125 type = types.str;
126 default = "localhost";
127 description = lib.mdDoc ''
128 Hostname hosting the database.
129 '';
130 };
131
132 name = mkOption {
133 type = types.str;
134 default = "zm";
135 description = lib.mdDoc ''
136 Name of database.
137 '';
138 };
139
140 username = mkOption {
141 type = types.str;
142 default = "zmuser";
143 description = lib.mdDoc ''
144 Username for accessing the database.
145 '';
146 };
147
148 password = mkOption {
149 type = types.str;
150 default = "zmpass";
151 description = lib.mdDoc ''
152 Username for accessing the database.
153 Not used if `createLocally` is set.
154 '';
155 };
156 };
157
158 cameras = mkOption {
159 type = types.int;
160 default = 1;
161 description = lib.mdDoc ''
162 Set this to the number of cameras you expect to support.
163 '';
164 };
165
166 storageDir = mkOption {
167 type = types.nullOr types.str;
168 default = null;
169 example = "/storage/tank";
170 description = lib.mdDoc ''
171 ZoneMinder can generate quite a lot of data, so in case you don't want
172 to use the default ${defaultDir}, you can override the path here.
173 '';
174 };
175
176 extraConfig = mkOption {
177 type = types.lines;
178 default = "";
179 description = lib.mdDoc ''
180 Additional configuration added verbatim to the configuration file.
181 '';
182 };
183 };
184 };
185
186 config = lib.mkIf cfg.enable {
187
188 assertions = [
189 { assertion = cfg.database.createLocally -> cfg.database.username == user;
190 message = "services.zoneminder.database.username must be set to ${user} if services.zoneminder.database.createLocally is set true";
191 }
192 ];
193
194 environment.etc = {
195 "zoneminder/60-defaults.conf".source = defaultsFile;
196 "zoneminder/80-nixos.conf".source = configFile;
197 };
198
199 networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
200 cfg.port
201 6802 # zmtrigger
202 ];
203
204 services = {
205 fcgiwrap = lib.mkIf useNginx {
206 enable = true;
207 preforkProcesses = cfg.cameras;
208 inherit user group;
209 };
210
211 mysql = lib.mkIf cfg.database.createLocally {
212 enable = true;
213 package = lib.mkDefault pkgs.mariadb;
214 ensureDatabases = [ cfg.database.name ];
215 ensureUsers = [{
216 name = cfg.database.username;
217 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
218 }];
219 };
220
221 nginx = lib.mkIf useNginx {
222 enable = true;
223 virtualHosts = {
224 ${cfg.hostname} = {
225 default = true;
226 root = "${pkg}/share/zoneminder/www";
227 listen = [ { addr = "0.0.0.0"; inherit (cfg) port; } ];
228 extraConfig = let
229 fcgi = config.services.fcgiwrap;
230 in ''
231 index index.php;
232
233 location / {
234 try_files $uri $uri/ /index.php?$args =404;
235
236 rewrite ^/skins/.*/css/fonts/(.*)$ /fonts/$1 permanent;
237
238 location ~ /api/(css|img|ico) {
239 rewrite ^/api(.+)$ /api/app/webroot/$1 break;
240 try_files $uri $uri/ =404;
241 }
242
243 location ~ \.(gif|ico|jpg|jpeg|png)$ {
244 access_log off;
245 expires 30d;
246 }
247
248 location /api {
249 rewrite ^/api(.+)$ /api/app/webroot/index.php?p=$1 last;
250 }
251
252 location /cgi-bin {
253 gzip off;
254
255 include ${config.services.nginx.package}/conf/fastcgi_params;
256 fastcgi_param SCRIPT_FILENAME ${pkg}/libexec/zoneminder/${zms};
257 fastcgi_param HTTP_PROXY "";
258 fastcgi_intercept_errors on;
259
260 fastcgi_pass ${fcgi.socketType}:${fcgi.socketAddress};
261 }
262
263 location /cache/ {
264 alias /var/cache/${dirName}/;
265 }
266
267 location ~ \.php$ {
268 try_files $uri =404;
269 fastcgi_index index.php;
270
271 include ${config.services.nginx.package}/conf/fastcgi_params;
272 fastcgi_param SCRIPT_FILENAME $request_filename;
273 fastcgi_param HTTP_PROXY "";
274
275 fastcgi_pass unix:${fpm.socket};
276 }
277 }
278 '';
279 };
280 };
281 };
282
283 phpfpm = lib.mkIf useNginx {
284 pools.zoneminder = {
285 inherit user group;
286 phpPackage = pkgs.php.withExtensions (
287 { enabled, all }: enabled ++ [ all.apcu all.sysvsem ]);
288 phpOptions = ''
289 date.timezone = "${config.time.timeZone}"
290 '';
291 settings = lib.mapAttrs (name: lib.mkDefault) {
292 "listen.owner" = user;
293 "listen.group" = group;
294 "listen.mode" = "0660";
295
296 "pm" = "dynamic";
297 "pm.start_servers" = 1;
298 "pm.min_spare_servers" = 1;
299 "pm.max_spare_servers" = 2;
300 "pm.max_requests" = 500;
301 "pm.max_children" = 5;
302 "pm.status_path" = "/$pool-status";
303 "ping.path" = "/$pool-ping";
304 };
305 };
306 };
307 };
308
309 systemd.services = {
310 zoneminder = with pkgs; {
311 inherit (zoneminder.meta) description;
312 documentation = [ "https://zoneminder.readthedocs.org/en/latest/" ];
313 path = [
314 coreutils
315 procps
316 psmisc
317 ];
318 after = [ "nginx.service" ] ++ lib.optional cfg.database.createLocally "mysql.service";
319 wantedBy = [ "multi-user.target" ];
320 restartTriggers = [ defaultsFile configFile ];
321 preStart = lib.optionalString useCustomDir ''
322 install -dm775 -o ${user} -g ${group} ${cfg.storageDir}/{${lib.concatStringsSep "," libDirs}}
323 '' + lib.optionalString cfg.database.createLocally ''
324 if ! test -e "/var/lib/${dirName}/db-created"; then
325 ${config.services.mysql.package}/bin/mysql < ${pkg}/share/zoneminder/db/zm_create.sql
326 touch "/var/lib/${dirName}/db-created"
327 fi
328
329 ${zoneminder}/bin/zmupdate.pl -nointeractive
330 ${zoneminder}/bin/zmupdate.pl --nointeractive -f
331
332 # Update ZM's Nix store path in the configuration table. Do nothing if the config doesn't
333 # contain ZM's Nix store path.
334 ${config.services.mysql.package}/bin/mysql -u zoneminder zm << EOF
335 UPDATE Config
336 SET Value = REGEXP_REPLACE(Value, "^/nix/store/[^-/]+-zoneminder-[^/]+", "${pkgs.zoneminder}")
337 WHERE Name = "ZM_FONT_FILE_LOCATION";
338 EOF
339 '';
340 serviceConfig = {
341 User = user;
342 Group = group;
343 SupplementaryGroups = [ "video" ];
344 ExecStart = "${zoneminder}/bin/zmpkg.pl start";
345 ExecStop = "${zoneminder}/bin/zmpkg.pl stop";
346 ExecReload = "${zoneminder}/bin/zmpkg.pl restart";
347 PIDFile = "/run/${dirName}/zm.pid";
348 Type = "forking";
349 Restart = "on-failure";
350 RestartSec = "10s";
351 CacheDirectory = dirs cacheDirs;
352 RuntimeDirectory = dirName;
353 ReadWriteDirectories = lib.mkIf useCustomDir [ cfg.storageDir ];
354 StateDirectory = dirs (if useCustomDir then [] else libDirs);
355 LogsDirectory = dirName;
356 PrivateTmp = true;
357 ProtectSystem = "strict";
358 ProtectKernelTunables = true;
359 SystemCallArchitectures = "native";
360 NoNewPrivileges = true;
361 };
362 };
363 };
364
365 users.groups.${user} = {
366 gid = config.ids.gids.zoneminder;
367 };
368
369 users.users.${user} = {
370 uid = config.ids.uids.zoneminder;
371 group = user;
372 inherit home;
373 inherit (pkgs.zoneminder.meta) description;
374 };
375 };
376
377 meta.maintainers = with lib.maintainers; [ ];
378}