{ config, lib, pkgs, ... }:
let
cfg = config.services.zoneminder;
pkg = pkgs.zoneminder;
dirName = pkg.dirName;
user = "zoneminder";
group = {
nginx = config.services.nginx.group;
none = user;
}."${cfg.webserver}";
useNginx = cfg.webserver == "nginx";
defaultDir = "/var/lib/${user}";
home = if useCustomDir then cfg.storageDir else defaultDir;
useCustomDir = cfg.storageDir != null;
socket = "/run/phpfpm/${dirName}.sock";
zms = "/cgi-bin/zms";
dirs = dirList: [ dirName ] ++ map (e: "${dirName}/${e}") dirList;
cacheDirs = [ "swap" ];
libDirs = [ "events" "exports" "images" "sounds" ];
dirStanzas = baseDir:
lib.concatStringsSep "\n" (map (e:
"ZM_DIR_${lib.toUpper e}=${baseDir}/${e}"
) libDirs);
defaultsFile = pkgs.writeText "60-defaults.conf" ''
# 01-system-paths.conf
${dirStanzas home}
ZM_PATH_ARP=${lib.getBin pkgs.nettools}/bin/arp
ZM_PATH_LOGS=/var/log/${dirName}
ZM_PATH_MAP=/dev/shm
ZM_PATH_SOCKS=/run/${dirName}
ZM_PATH_SWAP=/var/cache/${dirName}/swap
ZM_PATH_ZMS=${zms}
# 02-multiserver.conf
ZM_SERVER_HOST=
# Database
ZM_DB_TYPE=mysql
ZM_DB_HOST=${cfg.database.host}
ZM_DB_NAME=${cfg.database.name}
ZM_DB_USER=${if cfg.database.createLocally then user else cfg.database.username}
ZM_DB_PASS=${cfg.database.password}
# Web
ZM_WEB_USER=${user}
ZM_WEB_GROUP=${group}
'';
configFile = pkgs.writeText "80-nixos.conf" ''
# You can override defaults here
${cfg.extraConfig}
'';
phpExtensions = with pkgs.phpPackages; [
{ pkg = apcu; name = "apcu"; }
];
in {
options = {
services.zoneminder = with lib; {
enable = lib.mkEnableOption ''
ZoneMinder
If you intend to run the database locally, you should set
`config.services.zoneminder.database.createLocally` to true. Otherwise,
when set to `false` (the default), you will have to create the database
and database user as well as populate the database yourself.
'';
webserver = mkOption {
type = types.enum [ "nginx" "none" ];
default = "nginx";
description = ''
The webserver to configure for the PHP frontend.
Set it to `none` if you want to configure it yourself. PRs are welcome
for support for other web servers.
'';
};
hostname = mkOption {
type = types.str;
default = "localhost";
description = ''
The hostname on which to listen.
'';
};
port = mkOption {
type = types.int;
default = 8095;
description = ''
The port on which to listen.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open the firewall port(s).
'';
};
database = {
createLocally = mkOption {
type = types.bool;
default = false;
description = ''
Create the database and database user locally.
'';
};
host = mkOption {
type = types.str;
default = "localhost";
description = ''
Hostname hosting the database.
'';
};
name = mkOption {
type = types.str;
default = "zm";
description = ''
Name of database.
'';
};
username = mkOption {
type = types.str;
default = "zmuser";
description = ''
Username for accessing the database.
'';
};
password = mkOption {
type = types.str;
default = "zmpass";
description = ''
Username for accessing the database.
'';
};
};
cameras = mkOption {
type = types.int;
default = 1;
description = ''
Set this to the number of cameras you expect to support.
'';
};
storageDir = mkOption {
type = types.nullOr types.str;
default = null;
example = "/storage/tank";
description = ''
ZoneMinder can generate quite a lot of data, so in case you don't want
to use the default ${home}, you can override the path here.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Additional configuration added verbatim to the configuration file.
'';
};
};
};
config = lib.mkIf cfg.enable {
environment.etc = {
"zoneminder/60-defaults.conf".source = defaultsFile;
"zoneminder/80-nixos.conf".source = configFile;
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
services = {
fcgiwrap = lib.mkIf useNginx {
enable = true;
preforkProcesses = cfg.cameras;
inherit user group;
};
mysql = lib.mkIf cfg.database.createLocally {
ensureDatabases = [ cfg.database.name ];
initialDatabases = [{
inherit (cfg.database) name; schema = "${pkg}/share/zoneminder/db/zm_create.sql";
}];
ensureUsers = [{
name = cfg.database.username;
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
}];
};
nginx = lib.mkIf useNginx {
enable = true;
virtualHosts = {
"${cfg.hostname}" = {
default = true;
root = "${pkg}/share/zoneminder/www";
listen = [ { addr = "0.0.0.0"; inherit (cfg) port; } ];
extraConfig = let
fcgi = config.services.fcgiwrap;
in ''
index index.php;
location / {
try_files $uri $uri/ /index.php?$args =404;
rewrite ^/skins/.*/css/fonts/(.*)$ /fonts/$1 permanent;
location ~ /api/(css|img|ico) {
rewrite ^/api(.+)$ /api/app/webroot/$1 break;
try_files $uri $uri/ =404;
}
location ~ \.(gif|ico|jpg|jpeg|png)$ {
access_log off;
expires 30d;
}
location /api {
rewrite ^/api(.+)$ /api/app/webroot/index.php?p=$1 last;
}
location /cgi-bin {
gzip off;
include ${pkgs.nginx}/conf/fastcgi_params;
fastcgi_param SCRIPT_FILENAME ${pkg}/libexec/zoneminder/${zms};
fastcgi_param HTTP_PROXY "";
fastcgi_intercept_errors on;
fastcgi_pass ${fcgi.socketType}:${fcgi.socketAddress};
}
location /cache {
alias /var/cache/${dirName};
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_index index.php;
include ${pkgs.nginx}/conf/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_param HTTP_PROXY "";
fastcgi_pass unix:${socket};
}
}
'';
};
};
};
phpfpm = lib.mkIf useNginx {
pools.zoneminder = {
listen = socket;
phpOptions = ''
date.timezone = "${config.time.timeZone}"
${lib.concatStringsSep "\n" (map (e:
"extension=${e.pkg}/lib/php/extensions/${e.name}.so") phpExtensions)}
'';
extraConfig = ''
user = ${user}
group = ${group}
listen.owner = ${user}
listen.group = ${group}
listen.mode = 0660
pm = dynamic
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 2
pm.max_requests = 500
pm.max_children = 5
pm.status_path = /$pool-status
ping.path = /$pool-ping
'';
};
};
};
systemd.services = {
zoneminder = with pkgs; rec {
inherit (zoneminder.meta) description;
documentation = [ "https://zoneminder.readthedocs.org/en/latest/" ];
path = [
coreutils
procps
psmisc
];
after = [ "mysql.service" "nginx.service" ];
wantedBy = [ "multi-user.target" ];
restartTriggers = [ defaultsFile configFile ];
preStart = lib.mkIf useCustomDir ''
install -dm775 -o ${user} -g ${group} ${cfg.storageDir}/{${lib.concatStringsSep "," libDirs}}
'';
serviceConfig = {
User = user;
Group = group;
SupplementaryGroups = [ "video" ];
ExecStart = "${zoneminder}/bin/zmpkg.pl start";
ExecStop = "${zoneminder}/bin/zmpkg.pl stop";
ExecReload = "${zoneminder}/bin/zmpkg.pl restart";
PIDFile = "/run/${dirName}/zm.pid";
Type = "forking";
Restart = "on-failure";
RestartSec = "10s";
CacheDirectory = dirs cacheDirs;
RuntimeDirectory = dirName;
ReadWriteDirectories = lib.mkIf useCustomDir [ cfg.storageDir ];
StateDirectory = dirs (if useCustomDir then [] else libDirs);
LogsDirectory = dirName;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectKernelTunables = true;
SystemCallArchitectures = "native";
NoNewPrivileges = true;
};
};
};
users.groups."${user}" = {
gid = config.ids.gids.zoneminder;
};
users.users."${user}" = {
uid = config.ids.uids.zoneminder;
group = user;
inherit home;
inherit (pkgs.zoneminder.meta) description;
};
};
meta.maintainers = with lib.maintainers; [ peterhoeg ];
}