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