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 ''
70 ZoneMinder
71 </para><para>
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 = ''
84 The webserver to configure for the PHP frontend.
85 </para>
86 <para>
87
88 Set it to `none` if you want to configure it yourself. PRs are welcome
89 for support for other web servers.
90 '';
91 };
92
93 hostname = mkOption {
94 type = types.str;
95 default = "localhost";
96 description = ''
97 The hostname on which to listen.
98 '';
99 };
100
101 port = mkOption {
102 type = types.int;
103 default = 8095;
104 description = ''
105 The port on which to listen.
106 '';
107 };
108
109 openFirewall = mkOption {
110 type = types.bool;
111 default = false;
112 description = ''
113 Open the firewall port(s).
114 '';
115 };
116
117 database = {
118 createLocally = mkOption {
119 type = types.bool;
120 default = false;
121 description = ''
122 Create the database and database user locally.
123 '';
124 };
125
126 host = mkOption {
127 type = types.str;
128 default = "localhost";
129 description = ''
130 Hostname hosting the database.
131 '';
132 };
133
134 name = mkOption {
135 type = types.str;
136 default = "zm";
137 description = ''
138 Name of database.
139 '';
140 };
141
142 username = mkOption {
143 type = types.str;
144 default = "zmuser";
145 description = ''
146 Username for accessing the database.
147 '';
148 };
149
150 password = mkOption {
151 type = types.str;
152 default = "zmpass";
153 description = ''
154 Username for accessing the database.
155 Not used if <literal>createLocally</literal> is set.
156 '';
157 };
158 };
159
160 cameras = mkOption {
161 type = types.int;
162 default = 1;
163 description = ''
164 Set this to the number of cameras you expect to support.
165 '';
166 };
167
168 storageDir = mkOption {
169 type = types.nullOr types.str;
170 default = null;
171 example = "/storage/tank";
172 description = ''
173 ZoneMinder can generate quite a lot of data, so in case you don't want
174 to use the default ${home}, you can override the path here.
175 '';
176 };
177
178 extraConfig = mkOption {
179 type = types.lines;
180 default = "";
181 description = ''
182 Additional configuration added verbatim to the configuration file.
183 '';
184 };
185 };
186 };
187
188 config = lib.mkIf cfg.enable {
189
190 assertions = [
191 { assertion = cfg.database.createLocally -> cfg.database.username == user;
192 message = "services.zoneminder.database.username must be set to ${user} if services.zoneminder.database.createLocally is set true";
193 }
194 ];
195
196 environment.etc = {
197 "zoneminder/60-defaults.conf".source = defaultsFile;
198 "zoneminder/80-nixos.conf".source = configFile;
199 };
200
201 networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
202 cfg.port
203 6802 # zmtrigger
204 ];
205
206 services = {
207 fcgiwrap = lib.mkIf useNginx {
208 enable = true;
209 preforkProcesses = cfg.cameras;
210 inherit user group;
211 };
212
213 mysql = lib.mkIf cfg.database.createLocally {
214 enable = true;
215 package = lib.mkDefault pkgs.mariadb;
216 ensureDatabases = [ cfg.database.name ];
217 ensureUsers = [{
218 name = cfg.database.username;
219 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
220 }];
221 };
222
223 nginx = lib.mkIf useNginx {
224 enable = true;
225 virtualHosts = {
226 ${cfg.hostname} = {
227 default = true;
228 root = "${pkg}/share/zoneminder/www";
229 listen = [ { addr = "0.0.0.0"; inherit (cfg) port; } ];
230 extraConfig = let
231 fcgi = config.services.fcgiwrap;
232 in ''
233 index index.php;
234
235 location / {
236 try_files $uri $uri/ /index.php?$args =404;
237
238 rewrite ^/skins/.*/css/fonts/(.*)$ /fonts/$1 permanent;
239
240 location ~ /api/(css|img|ico) {
241 rewrite ^/api(.+)$ /api/app/webroot/$1 break;
242 try_files $uri $uri/ =404;
243 }
244
245 location ~ \.(gif|ico|jpg|jpeg|png)$ {
246 access_log off;
247 expires 30d;
248 }
249
250 location /api {
251 rewrite ^/api(.+)$ /api/app/webroot/index.php?p=$1 last;
252 }
253
254 location /cgi-bin {
255 gzip off;
256
257 include ${pkgs.nginx}/conf/fastcgi_params;
258 fastcgi_param SCRIPT_FILENAME ${pkg}/libexec/zoneminder/${zms};
259 fastcgi_param HTTP_PROXY "";
260 fastcgi_intercept_errors on;
261
262 fastcgi_pass ${fcgi.socketType}:${fcgi.socketAddress};
263 }
264
265 location /cache/ {
266 alias /var/cache/${dirName}/;
267 }
268
269 location ~ \.php$ {
270 try_files $uri =404;
271 fastcgi_index index.php;
272
273 include ${pkgs.nginx}/conf/fastcgi_params;
274 fastcgi_param SCRIPT_FILENAME $request_filename;
275 fastcgi_param HTTP_PROXY "";
276
277 fastcgi_pass unix:${fpm.socket};
278 }
279 }
280 '';
281 };
282 };
283 };
284
285 phpfpm = lib.mkIf useNginx {
286 pools.zoneminder = {
287 inherit user group;
288 phpPackage = pkgs.php.withExtensions ({ enabled, all }: enabled ++ [ all.apcu ]);
289 phpOptions = ''
290 date.timezone = "${config.time.timeZone}"
291 '';
292 settings = lib.mapAttrs (name: lib.mkDefault) {
293 "listen.owner" = user;
294 "listen.group" = group;
295 "listen.mode" = "0660";
296
297 "pm" = "dynamic";
298 "pm.start_servers" = 1;
299 "pm.min_spare_servers" = 1;
300 "pm.max_spare_servers" = 2;
301 "pm.max_requests" = 500;
302 "pm.max_children" = 5;
303 "pm.status_path" = "/$pool-status";
304 "ping.path" = "/$pool-ping";
305 };
306 };
307 };
308 };
309
310 systemd.services = {
311 zoneminder = with pkgs; {
312 inherit (zoneminder.meta) description;
313 documentation = [ "https://zoneminder.readthedocs.org/en/latest/" ];
314 path = [
315 coreutils
316 procps
317 psmisc
318 ];
319 after = [ "nginx.service" ] ++ lib.optional cfg.database.createLocally "mysql.service";
320 wantedBy = [ "multi-user.target" ];
321 restartTriggers = [ defaultsFile configFile ];
322 preStart = lib.optionalString useCustomDir ''
323 install -dm775 -o ${user} -g ${group} ${cfg.storageDir}/{${lib.concatStringsSep "," libDirs}}
324 '' + lib.optionalString cfg.database.createLocally ''
325 if ! test -e "/var/lib/${dirName}/db-created"; then
326 ${config.services.mysql.package}/bin/mysql < ${pkg}/share/zoneminder/db/zm_create.sql
327 touch "/var/lib/${dirName}/db-created"
328 fi
329
330 ${zoneminder}/bin/zmupdate.pl -nointeractive
331 '';
332 serviceConfig = {
333 User = user;
334 Group = group;
335 SupplementaryGroups = [ "video" ];
336 ExecStart = "${zoneminder}/bin/zmpkg.pl start";
337 ExecStop = "${zoneminder}/bin/zmpkg.pl stop";
338 ExecReload = "${zoneminder}/bin/zmpkg.pl restart";
339 PIDFile = "/run/${dirName}/zm.pid";
340 Type = "forking";
341 Restart = "on-failure";
342 RestartSec = "10s";
343 CacheDirectory = dirs cacheDirs;
344 RuntimeDirectory = dirName;
345 ReadWriteDirectories = lib.mkIf useCustomDir [ cfg.storageDir ];
346 StateDirectory = dirs (if useCustomDir then [] else libDirs);
347 LogsDirectory = dirName;
348 PrivateTmp = true;
349 ProtectSystem = "strict";
350 ProtectKernelTunables = true;
351 SystemCallArchitectures = "native";
352 NoNewPrivileges = true;
353 };
354 };
355 };
356
357 users.groups.${user} = {
358 gid = config.ids.gids.zoneminder;
359 };
360
361 users.users.${user} = {
362 uid = config.ids.uids.zoneminder;
363 group = user;
364 inherit home;
365 inherit (pkgs.zoneminder.meta) description;
366 };
367 };
368
369 meta.maintainers = with lib.maintainers; [ ];
370}