diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index d9ba2efa0c8..49f30dc85a0 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -338,6 +338,7 @@
minetest = 311;
rss2email = 312;
cockroachdb = 313;
+ zoneminder = 314;
# When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399!
@@ -636,6 +637,7 @@
minetest = 311;
rss2email = 312;
cockroachdb = 313;
+ zoneminder = 314;
# When adding a gid, make sure it doesn't match an existing
# uid. Users and groups with the same name should have equal
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 1ae96f427ad..a597485120c 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -432,6 +432,7 @@
./services/misc/uhub.nix
./services/misc/weechat.nix
./services/misc/xmr-stak.nix
+ ./services/misc/zoneminder.nix
./services/misc/zookeeper.nix
./services/monitoring/alerta.nix
./services/monitoring/apcupsd.nix
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
new file mode 100644
index 00000000000..a40e9e84613
--- /dev/null
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -0,0 +1,362 @@
+{ 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 = !(builtins.isNull cfg.storageDir);
+
+ 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=${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 ];
+ ensureUsers = {
+ name = cfg.database.username;
+ ensurePermissions = [
+ { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }
+ ];
+ initialDatabases = [
+ { inherit (cfg.database) name; schema = "${pkg}/share/zoneminder/db/zm_create.sql"; }
+ ];
+ };
+ };
+
+ 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;
+
+ 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 {
+ phpOptions = ''
+ date.timezone = "${config.time.timeZone}"
+
+ ${lib.concatStringsSep "\n" (map (e:
+ "extension=${e.pkg}/lib/php/extensions/${e.name}.so") phpExtensions)}
+ '';
+ pools.zoneminder = {
+ listen = socket;
+ 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 ];
+}
diff --git a/pkgs/servers/zoneminder/default-to-http-1dot1.patch b/pkgs/servers/zoneminder/default-to-http-1dot1.patch
new file mode 100644
index 00000000000..abd7ffccbb5
--- /dev/null
+++ b/pkgs/servers/zoneminder/default-to-http-1dot1.patch
@@ -0,0 +1,13 @@
+diff --git a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in
+index fa7b86079..c9d3c6f6c 100644
+--- a/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in
++++ b/scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in
+@@ -877,7 +877,7 @@ our @options = (
+ },
+ {
+ name => 'ZM_HTTP_VERSION',
+- default => '1.0',
++ default => '1.1',
+ description => 'The version of HTTP that ZoneMinder will use to connect',
+ help => q`
+ ZoneMinder can communicate with network cameras using either of
diff --git a/pkgs/servers/zoneminder/default.nix b/pkgs/servers/zoneminder/default.nix
new file mode 100644
index 00000000000..d9fd7d27ee5
--- /dev/null
+++ b/pkgs/servers/zoneminder/default.nix
@@ -0,0 +1,192 @@
+{ stdenv, lib, fetchFromGitHub, fetchurl, cmake, makeWrapper, pkgconfig
+, curl, ffmpeg, glib, libjpeg, libselinux, libsepol, mp4v2, mysql, nettools, pcre, perl, perlPackages
+, polkit, utillinuxMinimal, x264, zlib
+, avahi, dbus, gettext, git, gnutar, gzip, bzip2, libiconv, openssl, python
+, coreutils, procps, psmisc }:
+
+# NOTES:
+#
+# 1. ZM_CONFIG_DIR is set to $out/etc/zoneminder as the .conf file distributed
+# by upstream contains defaults and is not supposed to be edited so it is fine
+# to keep it read-only.
+#
+# 2. ZM_CONFIG_SUBDIR is where we place our configuration from the NixOS module
+# but as the installer will try to put files there, we patch Config.pm after the
+# install.
+#
+# 3. ZoneMinder is run with -T passed to the perl interpreter which makes perl
+# ignore PERL5LIB. We therefore have to do the substitution into -I parameters
+# ourselves which results in ugly wrappers.
+#
+# 4. The makefile for the perl modules needs patching to put things into the
+# right place. That also means we have to not run "make install" for them.
+#
+# 5. In principal the various ZM_xx variables should be overridable from the
+# config file but some of them are baked into the perl scripts, so we *have* to
+# set them here instead of in the configuration in the NixOS module.
+#
+# 6. I am no PolicyKit expert but the .policy file looks fishy:
+# a. The user needs to be known at build-time so we should probably throw
+# upstream's policy file away and generate it from the NixOS module
+# b. I *think* we may have to substitute the store paths with
+# /run/current-system/sw/bin paths for it to work.
+#
+# 7. we manually fix up the perl paths in the scripts as fixupPhase will only
+# handle pkexec and not perl if both are present.
+#
+# 8. There are several perl modules needed at runtime which are not checked when
+# building so if a new version stops working, check if there is a missing
+# dependency by running the failing component manually.
+#
+# 9. Parts of the web UI has a hardcoded /zm path so we create a symlink to work
+# around it.
+
+let
+ modules = [
+ {
+ path = "web/api/app/Plugin/Crud";
+ src = fetchFromGitHub {
+ owner = "ZoneMinder";
+ repo = "crud";
+ rev = "3.1.0-zm";
+ sha256 = "061avzyml7mla4hlx057fm8a9yjh6m6qslgyzn74cv5p2y7f463l";
+ };
+ }
+ {
+ path = "web/api/app/Plugin/CakePHP-Enum-Behavior";
+ src = fetchFromGitHub {
+ owner = "ZoneMinder";
+ repo = "CakePHP-Enum-Behavior";
+ rev = "1.0-zm";
+ sha256 = "0zsi6s8xymb183kx3szspbrwfjqcgga7786zqvydy6hc8c909cgx";
+ };
+ }
+ ];
+
+ addons = [
+ {
+ path = "scripts/ZoneMinder/lib/ZoneMinder/Control/Xiaomi.pm";
+ src = fetchurl {
+ url = "https://gist.githubusercontent.com/joshstrange/73a2f24dfaf5cd5b470024096ce2680f/raw/e964270c5cdbf95e5b7f214f7f0fc6113791530e/Xiaomi.pm";
+ sha256 = "04n1ap8fx66xfl9q9rypj48pzbgzikq0gisfsfm8wdsmflarz43v";
+ };
+ }
+ ];
+
+ user = "zoneminder";
+ dirName = "zoneminder";
+ perlBin = "${perl}/bin/perl";
+
+in stdenv.mkDerivation rec {
+ name = "zoneminder-${version}";
+ version = "1.32.3";
+
+ src = fetchFromGitHub {
+ owner = "ZoneMinder";
+ repo = "zoneminder";
+ rev = version;
+ sha256 = "1sx2fn99861zh0gp8g53ynr1q6yfmymxamn82y54jqj6nv475njz";
+ };
+
+ patches = [
+ ./default-to-http-1dot1.patch
+ ];
+
+ postPatch = ''
+ ${lib.concatStringsSep "\n" (map (e: ''
+ rm -rf ${e.path}/*
+ cp -r ${e.src}/* ${e.path}/
+ '') modules)}
+
+ rm -rf web/api/lib/Cake/Test
+
+ ${lib.concatStringsSep "\n" (map (e: ''
+ cp ${e.src} ${e.path}
+ '') addons)}
+
+ for d in scripts/ZoneMinder onvif/{modules,proxy} ; do
+ substituteInPlace $d/CMakeLists.txt \
+ --replace 'DESTDIR="''${CMAKE_CURRENT_BINARY_DIR}/output"' "PREFIX=$out INSTALLDIRS=site"
+ sed -i '/^install/d' $d/CMakeLists.txt
+ done
+
+ substituteInPlace misc/CMakeLists.txt \
+ --replace '"''${PC_POLKIT_PREFIX}/''${CMAKE_INSTALL_DATAROOTDIR}' "\"$out/share"
+
+ for f in misc/*.policy.in \
+ scripts/*.pl* \
+ scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in ; do
+ substituteInPlace $f \
+ --replace '/usr/bin/perl' '${perlBin}' \
+ --replace '/bin:/usr/bin' "$out/bin:${lib.makeBinPath [ coreutils procps psmisc ]}"
+ done
+
+ substituteInPlace scripts/zmdbbackup.in \
+ --replace /usr/bin/mysqldump ${mysql}/bin/mysqldump
+
+ for f in scripts/ZoneMinder/lib/ZoneMinder/Config.pm.in \
+ scripts/zmupdate.pl.in \
+ src/zm_config.h.in \
+ web/api/app/Config/bootstrap.php.in \
+ web/includes/config.php.in ; do
+ substituteInPlace $f --replace @ZM_CONFIG_SUBDIR@ /etc/zoneminder
+ done
+
+ for f in includes/Event.php views/image.php skins/classic/views/image-ffmpeg.php ; do
+ substituteInPlace web/$f \
+ --replace "'ffmpeg " "'${ffmpeg}/bin/ffmpeg "
+ done
+ '';
+
+ buildInputs = [
+ curl ffmpeg glib libjpeg libselinux libsepol mp4v2 mysql pcre perl polkit x264 zlib
+ utillinuxMinimal # for libmount
+ ] ++ (with perlPackages; [
+ DateManip DBI DBDmysql LWP SysMmap
+ # runtime dependencies not checked at build-time
+ JSONMaybeXS LWPProtocolHttps NumberBytesHuman SysCPU SysMemInfo TimeDate
+ ]);
+
+ nativeBuildInputs = [ cmake makeWrapper pkgconfig ];
+
+ enableParallelBuilding = true;
+
+ cmakeFlags = [
+ "-DWITH_SYSTEMD=ON"
+ "-DZM_LOGDIR=/var/log/${dirName}"
+ "-DZM_RUNDIR=/run/${dirName}"
+ "-DZM_SOCKDIR=/run/${dirName}"
+ "-DZM_TMPDIR=/tmp/${dirName}"
+ "-DZM_CONFIG_DIR=${placeholder "out"}/etc/zoneminder"
+ "-DZM_WEB_USER=${user}"
+ "-DZM_WEB_GROUP=${user}"
+ ];
+
+ passthru = { inherit dirName; };
+
+ postInstall = ''
+ PERL5LIB="$PERL5LIB''${PERL5LIB:+:}$out/${perl.libPrefix}"
+
+ perlFlags="-wT"
+ for i in $(IFS=$'\n'; echo $PERL5LIB | tr ':' "\n" | sort -u); do
+ perlFlags="$perlFlags -I$i"
+ done
+
+ for f in $out/bin/*.pl ; do
+ mv $f $out/libexec/
+ makeWrapper ${perlBin} $f \
+ --prefix PATH : $out/bin \
+ --add-flags "$perlFlags $out/libexec/$(basename $f)"
+ done
+
+ ln -s $out/share/zoneminder/www $out/share/zoneminder/www/zm
+ '';
+
+ meta = with stdenv.lib; {
+ description = "Video surveillance software system";
+ homepage = https://zoneminder.com;
+ license = licenses.gpl3;
+ maintainers = with maintainers; [ peterhoeg ];
+ platforms = platforms.unix;
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 6c18c315866..25c0457c247 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -23100,6 +23100,8 @@ in
callPackage ../applications/networking/znc/modules.nix { }
);
+ zoneminder = callPackage ../servers/zoneminder { };
+
zsnes = pkgsi686Linux.callPackage ../misc/emulators/zsnes { };
xcpc = callPackage ../misc/emulators/xcpc { };
diff --git a/pkgs/top-level/perl-packages.nix b/pkgs/top-level/perl-packages.nix
index 4885b243900..fd0cc917954 100644
--- a/pkgs/top-level/perl-packages.nix
+++ b/pkgs/top-level/perl-packages.nix
@@ -11497,6 +11497,14 @@ let
};
};
+ NumberBytesHuman = buildPerlPackage rec {
+ name = "Number-Bytes-Human-0.11";
+ src = fetchurl {
+ url = "mirror://cpan/authors/id/F/FE/FERREIRA/${name}.tar.gz";
+ sha256 = "0b3gprpbcrdwc2gqalpys5m2ngilh5injhww8y0gf3dln14rrisz";
+ };
+ };
+
NumberCompare = buildPerlPackage rec {
name = "Number-Compare-0.03";
src = fetchurl {
@@ -14318,6 +14326,19 @@ let
};
};
+ SysMmap = buildPerlPackage rec {
+ name = "Sys-Mmap-0.19";
+ src = fetchurl {
+ url = "mirror://cpan/authors/id/S/SW/SWALTERS/${name}.tar.gz";
+ sha256 = "1yh0170xfw3z7n3lynffcb6axv7wi6zb46cx03crj1cvrhjmwa89";
+ };
+ meta = with stdenv.lib; {
+ description = "Use mmap to map in a file as a Perl variable";
+ maintainers = with maintainers; [ peterhoeg ];
+ license = with licenses; [ gpl2Plus ];
+ };
+ };
+
SysMemInfo = buildPerlPackage rec {
name = "Sys-MemInfo-0.99";
src = fetchurl {