Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

zoneminder: Path Fix #83261

Closed
wants to merge 2 commits into from
Closed

zoneminder: Path Fix #83261

wants to merge 2 commits into from

Conversation

unya
Copy link

@unya unya commented Mar 24, 2020

Event.pm calls /bin/rm by absolute path which was not patched by nixpkgs, due to that zoneminder installs using nixos would not delete events, whether from server or zmaudit.pl

Motivation for this change

Fix for #83260

Things done
  • Tested using sandboxing (nix.useSandbox on NixOS, or option sandbox in nix.conf on non-NixOS linux)
  • Built on platform(s)
    • NixOS
    • macOS
    • other Linux distributions
  • Tested via one or more NixOS test(s) if existing and applicable for the change (look inside nixos/tests)
  • Tested compilation of all pkgs that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review wip"
  • Tested execution of all binary files (usually in ./result/bin/)
  • Determined the impact on package closure size (by running nix path-info -S before and after)
  • Ensured that relevant documentation is up to date
  • Fits CONTRIBUTING.md.

Event.pm calls /bin/rm by absolute path which was not patched by nixpkgs, due to that zoneminder installs using nixos would not delete events, whether from server or zmaudit.pl
Wrong file name in patching script, was Event.pm.in, should be Event.pm
@veprbl
Copy link
Member

veprbl commented Mar 24, 2020

substitute(): ERROR: file 'scripts/ZoneMinder/lib/ZoneMinder/Event.pm.in' does not exist

@unya
Copy link
Author

unya commented Mar 24, 2020

substitute(): ERROR: file 'scripts/ZoneMinder/lib/ZoneMinder/Event.pm.in' does not exist

Missed pushing a commit that fixed that error. It's now pushed and fixed.

@@ -115,7 +115,8 @@ in stdenv.mkDerivation rec {

for f in misc/*.policy.in \
scripts/*.pl* \
scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in ; do
scripts/ZoneMinder/lib/ZoneMinder/Memory.pm.in \
scripts/ZoneMinder/lib/ZoneMinder/Event.pm; do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

substituteStream(): WARNING: pattern '/usr/bin/perl' doesn't match anything in file 'scripts/ZoneMinder/lib/ZoneMinder/Event.pm'
substituteStream(): WARNING: pattern '/bin:/usr/bin' doesn't match anything in file 'scripts/ZoneMinder/lib/ZoneMinder/Event.pm'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left as is due to criticality of specified patterns and high chance of them showing again given past experience.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this PR you propose to do two substitutions in scripts/ZoneMinder/lib/ZoneMinder/Event.pm and neither of them was needed at the time. If we were to merge this right now, this would be removed by the next attentive person unless you provide a reference for why this is needed (e.g. "we expect it to break in the future because ...").

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I initially packaged zoneminder, there were tons of replacement warnings as it was easier to simply loop through all the files that might need fixing and fix up all the paths that had to be replaced somewhere because there soooo many and having to manually specify it for each file was a royal pain. The ZM code base is just a mess.

I suggest sticking a big fat disclaimer in there about that and simply accept the warnings.

Then again, I'm also hoping that somebody will maintain ZM as I'm not using it any more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this was a simple bit of paid work - it was hacked up locally by overwriting files in nix store, but provided back as a fix because we (me and the client) both care about upstreaming fixes.

I'd honestly prefer @peterhoeg solution of big fat warning coupled with looping over all Perl files in ZM, as the random calls to hardcoded absolute paths are a bit of a problem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please amend the PR to include the note - this will be good to go then.

@stale
Copy link

stale bot commented Oct 26, 2020

Nixpkgs stale bot info

@stale stale bot added the 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md label Oct 26, 2020
@SuperSandro2000
Copy link
Member

Result of nixpkgs-review pr 83261 run on x86_64-linux 1

1 package built:
  • zoneminder

@stale stale bot removed the 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md label Nov 26, 2020
@SuperSandro2000
Copy link
Member

@peterhoeg would you approve this with a comment?

@peterhoeg
Copy link
Member

We still need a note/comment in there explaining why we do the mass replace and why people should ignore the warnings.

@mweinelt
Copy link
Member

mweinelt commented Nov 28, 2020

Last I tried the zoneminder module wasn't working that well either. Among other things these problems turned up:

  • The nginx configuration is assuming, that it is the default_server
  • The nginx configuration sets a default listen statement, defaulting to IPv4 only
  • The services.zoneminder.port option used with nginx makes nginx listen on port 8095 by default.
  • Much of the webinterface was only working intermittently, the logs spewed warnings and errors

I can reinstall zoneminder to give a better picture, but unless someone maintains this, I don't think this is in a very usable state.

@peterhoeg What are you using instead?

@peterhoeg
Copy link
Member

@peterhoeg What are you using instead?

Motioneye. I do have a module for this which I haven't upstreamed to nixpkgs yet.

@mweinelt
Copy link
Member

@peterhoeg What are you using instead?

Motioneye. I do have a module for this which I haven't upstreamed to nixpkgs yet.

If you can, please do. It's what I had been looking into as well.

@veprbl veprbl closed this Mar 23, 2021
@peterhoeg
Copy link
Member

peterhoeg commented Mar 23, 2021

@mweinelt, this is what I use:

{ config, lib, pkgs, ... }:
let
  inherit (lib) mkEnableOption mkIf mkOption types;
  inherit (import <common/lib.nix> { inherit lib pkgs; }) secureService;

  cfg = config.toupstream.services.motioneye;

  confDir =
    finalSettings.conf_path;

  nonDefaultConfDir =
    confDir != "/var/lib/motioneye";

  toText = val: boolFn:
    if builtins.isBool val
    then boolFn val
    else toString val;

  boolToOnOff = bool:
    if bool then "on" else "off";

  attrsToFile = file: attrs: toBoolFn:
    pkgs.writeText "${file}.conf"
      (lib.concatStringsSep "\n" (lib.mapAttrsToList (k: v: "${k} ${toText v toBoolFn}") attrs));

  cfgFile =
    attrsToFile "motioneye" finalSettings lib.boolToString;

  activeCameras =
    builtins.filter (e: e.enabled) cfg.cameras;

  cameraSettings = camera:
    lib.recursiveUpdate
      (lib.recursiveUpdate cameraDefaults
        ({
          camera_id = camera.id;
          camera_name = "cam-${toString camera.id}";
          netcam_url = camera.url;
          stream_port = cameraPort camera.id;
          target_dir = cameraDir camera.id;
          text_right = camera.location;
        } // lib.optionalAttrs (! lib.hasPrefix camera.url "rtsp")
          {
            netcam_keepalive = true;
            netcam_tolerant_check = true;
          }))
      camera.settings;

  cameraFile = camera:
    attrsToFile "camera-${toString camera.id}" (cameraSettings camera) boolToOnOff;

  finalSettings = lib.recursiveUpdate defaultSettings cfg.settings;

  defaultSettings = rec {
    # path to the configuration directory (must be writable by motionEye)
    conf_path = "/var/lib/motioneye";
    run_path = "/run/motioneye";
    log_path = "/var/log/motioneye";

    # the log level (use quiet, error, warning, info or debug)
    log_level = "info";

    # the IP address to listen on
    # (0.0.0.0 for all interfaces, 127.0.0.1 for localhost)
    listen = "0.0.0.0";

    # the TCP port to listen on
    port = 8765;

    # path to the motion binary to use (automatically detected if commented)
    motion_binary = "${pkgs.motion}/bin/motion";

    # whether motion HTTP control interface listens on
    # localhost or on all interfaces
    motion_control_localhost = true;

    # the TCP port that motion HTTP control interface listens on
    motion_control_port = 7999;

    # interval in seconds at which motionEye checks if motion is running
    motion_check_interval = 10;

    # whether to restart the motion daemon when an error occurs while communicating with it
    # this seems to randomly restart the whole motioneye process if one camera is unavailable
    motion_restart_on_errors = false;

    # interval in seconds at which motionEye checks the SMB mounts
    mount_check_interval = 300;

    # interval in seconds at which the janitor is called
    # to remove old pictures and movies
    cleanup_interval = 0;

    # timeout in seconds to wait for response from a remote motionEye server
    remote_request_timeout = 10;

    # timeout in seconds to wait for mjpg data from the motion daemon
    mjpg_client_timeout = 10;

    # timeout in seconds after which an idle mjpg client is removed
    # (set to 0 to disable)
    mjpg_client_idle_timeout = 10;

    smb_shares = false;
    smb_mount_root = "/media";

    local_time_file = "/etc/localtime";

    # enables shutdown and rebooting after changing system settings
    # (such as wifi settings or time zone)
    enable_reboot = false;

    # timeout in seconds to use when talking to the SMTP server
    smtp_timeout = 60;

    # timeout in seconds to wait for media files list
    list_media_timeout = 120;

    # timeout in seconds to wait for media files list, when sending emails
    list_media_timeout_email = 10;

    # timeout in seconds to wait for zip file creation
    zip_timeout = 500;

    # timeout in seconds to wait for timelapse creation
    timelapse_timeout = 500;

    # enable adding and removing cameras from UI
    add_remove_cameras = true;

    # enables HTTP basic authentication scheme (in addition to, not instead of the signature mechanism)
    http_basic_auth = false;

    # overrides the hostname (useful if motionEye runs behind a reverse proxy)
    # server_name motionEye
  };

  cameraDefaults = {
    # @clean_cloud_enabled off
    # @enabled on
    # @id ${id}
    # @manual_record off
    # @manual_snapshots on
    # @motion_detection on
    # @network_password
    # @network_server
    # @network_share_name
    # @network_smb_ver 1.0
    # @network_username
    # @preserve_movies 0
    # @preserve_pictures 0
    # @storage_device custom-path
    # @upload_enabled off
    # @upload_location
    # @upload_method post
    # @upload_movie on
    # @upload_password
    # @upload_picture on
    # @upload_port
    # @upload_server
    # @upload_service ftp
    # @upload_subfolders on
    # @upload_username
    # @webcam_resolution 100
    # @webcam_server_resize off
    # @working_schedule
    # @working_schedule_type outside

    auto_brightness = false;
    despeckle_filter = null;
    emulate_motion = false;
    event_gap = 30;
    framerate = 5;
    height = 720;
    lightswitch_percent = 0;
    locate_motion_mode = false;
    locate_motion_style = "redbox";
    minimum_motion_frames = 20;
    movie_codec = "mkv";
    movie_filename = "%Y-%m-%d/%H-%M-%S";
    movie_max_time = 0;
    movie_output = true;
    movie_output_motion = false;
    movie_passthrough = false;
    movie_quality = 75;
    netcam_use_tcp = true;
    noise_tune = true;

    # on_event_end    ${cfg.package}/bin/relayevent.sh ${cfgFile} stop %t
    # on_event_start  ${cfg.package}/bin/relayevent.sh ${cfgFile} start %t
    # on_movie_end    ${cfg.package}/bin/relayevent.sh ${cfgFile} movie_end %t %f
    # on_picture_save ${cfg.package}/bin/relayevent.sh ${cfgFile} picture_save %t %f

    # these trigger several times per second
    # on_area_detected  {log} area_detected %t
    # on_motion_detected {log} motion_detected %t

    picture_filename = "%Y-%m-%d/%H-%M-%S";
    picture_output = "best";
    picture_output_motion = false;
    picture_quality = 85;
    post_capture = 1;
    pre_capture = 1;
    rotate = 0;
    smart_mask_speed = 5;
    snapshot_filename = "%Y-%m-%d/%H-%M-%S";
    snapshot_interval = 0;
    stream_auth_method = 0;
    # stream_authentication = "user"; # ????
    stream_localhost = false;
    stream_maxrate = 5;
    stream_motion = false;
    stream_quality = 75;
    text_changes = true;
    text_left = "%Y-%m-%d %T";
    text_scale = 2;
    width = 1280;
  };

  camerasWithMask =
    # process all masks when doing the cams - doesn't matter if enabled or not
    builtins.filter (e: (builtins.hasAttr "mask_file" (cameraSettings e)) && e.mask != null) cfg.cameras;

  motionConf = pkgs.writeText "motion.conf" (''
    # @admin_password
    # @admin_username admin
    # @enabled on
    # @normal_password
    # @normal_username user
    # @show_advanced on
    setup_mode off
    webcontrol_interface 1
    webcontrol_localhost off
    webcontrol_parms 0
    webcontrol_port ${toString finalSettings.motion_control_port}
  '' + lib.concatMapStringsSep "\n" (e: "camera ${(cameraFile e).name}") activeCameras);

  cameraDir = id:
    "${finalSettings.media_path}/Camera${toString id}";

  cameraPort = id:
    cfg.firstStreamingPort + id;

  cfgDrv = pkgs.stdenv.mkDerivation {
    name = "motioneye-config";
    buildCommand = ''
      dir=$out/etc/motioneye

      install -Dm444 ${motionConf} $dir/${motionConf.name}

    '' + lib.concatMapStringsSep "\n"
      (e: "install -Dm644 ${cameraFile e} $dir/${(cameraFile e).name}")
      activeCameras;
  };

in
{
  meta.maintainers = with lib.maintainers; [ peterhoeg ];

  options.toupstream.services.motioneye = {
    enable = mkEnableOption "Enable MotionEye";

    openFirewall = mkOption {
      description = "Open firewall";
      type = types.bool;
      default = true;
    };

    package = mkOption {
      description = "Package";
      type = types.package;
      default = pkgs.callPackage <pkgs/motioneye> { };
    };

    cameras = mkOption {
      description = "Cameras";
      type = types.listOf (types.submodule {
        options = {
          enabled = mkOption {
            description = "Camera";
            type = types.bool;
            default = true;
          };

          id = mkOption {
            description = "Camera id";
            type = types.ints.positive;
          };

          location = mkOption {
            description = "Camera location";
            type = types.str;
          };

          url = mkOption {
            description = "Camera URL";
            type = types.str;
          };

          settings = mkOption {
            description = "Camera settings";
            type = types.attrs;
            default = { };
          };
        };
      });
      default = [ ];
    };

    settings = mkOption {
      description = "Settings";
      type = types.attrs;
    };

    firstStreamingPort = mkOption {
      description = "First port for streaming cameras";
      type = types.port;
      default = 8080;
    };

    declarativeConfig = mkOption {
      description = "Use declarative config only";
      type = types.bool;
      default = builtins.length cfg.cameras > 0;
    };

    retention = mkOption {
      description = "How long to keep recordings. Set to '-' to keep forever.";
      type = types.str;
      default = "60d";
    };

    mqtt = {
      enable = mkOption {
        description = "Enable MQTT integration";
        type = types.bool;
        default = cfg.mqtt.username != null && cfg.mqtt.password != null;
      };

      username = mkOption {
        description = "MQTT user name";
        type = types.str;
      };

      password = mkOption {
        description = "MQTT password";
        type = types.str;
      };
    };
  };

  config = mkIf cfg.enable {

    networking.firewall = mkIf cfg.openFirewall {
      allowedTCPPorts = (with finalSettings; [
        port
        motion_control_port
      ]);

      allowedTCPPortRanges = [
        # let's open all
        { from = (cameraPort 1); to = (cameraPort (builtins.length cfg.cameras)); }
      ];
    };

    systemd = {
      services.motioneye = secureService {
        description = "MotionEye";
        after = [ "network.target" ];
        wantedBy = [ "multi-user.target" ];

        environment = {
          DEBUG = "1";
          MQTT_USER = cfg.mqtt.username;
          MQTT_PASSWORD = cfg.mqtt.password;
        };

        serviceConfig = {
          User = "motioneye";
          Group = "motioneye";
          ExecStartPre = pkgs.writeShellScript "motioneye-setup"
            (
              let
                cmd =
                  if cfg.declarativeConfig
                  then "ln -sf"
                  else "cp --no-preserve=owner,mode";
              in
              ''
                rm -rf ${confDir}/{camera*,motion,motioneye}.conf
                rm -rf ${confDir}/mask_*.pgm

                ${cmd} ${cfgFile} ${confDir}/motioneye.conf

                for f in ${cfgDrv}/etc/motioneye/*.conf ; do
                  ${cmd} $f ${confDir}/
                done

              '' + lib.concatMapStringsSep "\n"
                (e: ''
                  ${cmd} ${e.settings.mask_file} ${confDir}/mask_${toString e.id}.pgm
                '')
                camerasWithMask
            );

          ExecStart = "${cfg.package}/bin/meyectl startserver -c ${confDir}/motioneye.conf";
          ReadWriteDirectories = [
            finalSettings.media_path
          ] ++ lib.optional nonDefaultConfDir confDir;
          LogsDirectory = "motioneye";
          RuntimeDirectory = "motioneye";
          # create /var/lib/motioneye no matter what. You can still set another conf_path if needed
          StateDirectory = "motioneye";
          TasksMax = 128 * (builtins.length activeCameras);
        };
      };

      tmpfiles.rules = [
        "d ${finalSettings.media_path} 0755 motioneye motioneye - -"
      ]
      ++ lib.optional nonDefaultConfDir "d ${confDir} 0755 motioneye motioneye - -"
      # create all dirs, active or not
      ++ map (e: "d ${cameraDir e.id} 0755 motioneye motioneye ${cfg.retention} -") cfg.cameras;
    };

    users = {
      users.motioneye = {
        description = "MotionEye";
        home = confDir;
        isSystemUser = true;
        group = "motioneye";
      };

      groups.motioneye = { };
    };
  };
}

@mweinelt
Copy link
Member

mweinelt commented Mar 23, 2021

@peterhoeg Awesome! If you would kindly provide the package as well, that would help me alot.

  default = pkgs.callPackage <pkgs/motioneye> { };

@peterhoeg
Copy link
Member

Ah yes, sure:

{ fetchFromGitHub
, curl
, ffmpeg
, lib
, lsb-release
, makeWrapper
, motion
, python3Packages
, v4l_utils
, which
}:
let
  pypkgs = python3Packages;

  bins = [ ffmpeg lsb-release motion (v4l_utils.override { withGUI = false; }) which ];

in
pypkgs.buildPythonApplication rec {
  pname = "motioneye";
  version = "0.42.1";

  src = fetchFromGitHub {
    owner = "ccrisan";
    repo = pname;
    # python3
    rev = "25ee8195a699b70c7c6986f702d369201ad71656";
    sha256 = "sha256-1bG8Bb5u/nv7aQzsG6Bg12YUg+0GwTcBnrhWk0E7Kw4=";
  };

  buildInputs = bins;

  postPatch = ''
    substituteInPlace motioneye/scripts/relayevent.sh \
      --replace curl ${curl}/bin/curl
  '';

  postInstall = ''
    mv $out/${pypkgs.python.sitePackages}/motioneye/scripts/*.sh $out/bin
    rmdir $out/${pypkgs.python.sitePackages}/motioneye/scripts
  '';

  # I don't know why I can't just use buildInputs
  makeWrapperArgs = [
    "--prefix PATH : ${lib.makeBinPath bins}"
  ];

  propagatedBuildInputs = with pypkgs; [ jinja2 pillow pycurl pytz six tornado_5 ];

  doCheck = false;

  meta = with lib; {
    description = "MotionEye";
    maintainers = with maintainers; [ peterhoeg ];
  };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants