Unverified Commit d99b92b3 authored by github-actions[bot]'s avatar github-actions[bot] Committed by GitHub
Browse files

Merge master into staging-next

parents 65f7e5d8 bc9b484d
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
@@ -118,3 +118,33 @@ the symlink, and this path is in `/nix/store/.../lib/systemd/user/`.
Hence [garbage collection](#sec-nix-gc) will remove that file and you
will wind up with a broken symlink in your systemd configuration, which
in turn will not make the service / timer start on login.

## Template units {#sect-nixos-systemd-template-units}

systemd supports templated units where a base unit can be started multiple
times with a different parameter. The syntax to accomplish this is
`service-name@instance-name.service`. Units get the instance name passed to
them (see `systemd.unit(5)`). NixOS has support for these kinds of units and
for template-specific overrides. A service needs to be defined twice, once
for the base unit and once for the instance. All instances must include
`overrideStrategy = "asDropin"` for the change detection to work. This
example illustrates this:
```nix
{
  systemd.services = {
    "base-unit@".serviceConfig = {
      ExecStart = "...";
      User = "...";
    };
    "base-unit@instance-a" = {
      overrideStrategy = "asDropin"; # needed for templates to work
      wantedBy = [ "multi-user.target" ]; # causes NixOS to manage the instance
    };
    "base-unit@instance-b" = {
      overrideStrategy = "asDropin"; # needed for templates to work
      wantedBy = [ "multi-user.target" ]; # causes NixOS to manage the instance
      serviceConfig.User = "root"; # also override something for this specific instance
    };
  };
}
```
+20 −33
Original line number Diff line number Diff line
{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.services.mediamtx;
  package = pkgs.mediamtx;
  format = pkgs.formats.yaml {};
in
{
  meta.maintainers = with lib.maintainers; [ fpletz ];

  options = {
    services.mediamtx = {
      enable = mkEnableOption (lib.mdDoc "MediaMTX");
      enable = lib.mkEnableOption (lib.mdDoc "MediaMTX");

      settings = mkOption {
      package = lib.mkPackageOptionMD pkgs "mediamtx" { };

      settings = lib.mkOption {
        description = lib.mdDoc ''
          Settings for MediaMTX.
          Read more at <https://github.com/aler9/mediamtx/blob/main/mediamtx.yml>
          Settings for MediaMTX. Refer to the defaults at
          <https://github.com/bluenviron/mediamtx/blob/main/mediamtx.yml>.
        '';
        type = format.type;

        default = {
          logLevel = "info";
          logDestinations = [
            "stdout"
          ];
          # we set this so when the user uses it, it just works (see LogsDirectory below). but it's not used by default.
          logFile = "/var/log/mediamtx/mediamtx.log";
        };

        default = {};
        example = {
          paths = {
            cam = {
              runOnInit = "ffmpeg -f v4l2 -i /dev/video0 -f rtsp rtsp://localhost:$RTSP_PORT/$RTSP_PATH";
              runOnInit = "\${lib.getExe pkgs.ffmpeg} -f v4l2 -i /dev/video0 -f rtsp rtsp://localhost:$RTSP_PORT/$RTSP_PATH";
              runOnInitRestart = true;
            };
          };
        };
      };

      env = mkOption {
        type = with types; attrsOf anything;
      env = lib.mkOption {
        type = with lib.types; attrsOf anything;
        description = lib.mdDoc "Extra environment variables for MediaMTX";
        default = {};
        example = {
          MTX_CONFKEY = "mykey";
        };
      };

      allowVideoAccess = lib.mkEnableOption (lib.mdDoc ''
        Enable access to video devices like cameras on the system.
      '');
    };
  };

  config = mkIf (cfg.enable) {
  config = lib.mkIf cfg.enable {
    # NOTE: mediamtx watches this file and automatically reloads if it changes
    environment.etc."mediamtx.yaml".source = format.generate "mediamtx.yaml" cfg.settings;

    systemd.services.mediamtx = {
      environment = cfg.env;

      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];

      path = with pkgs; [
        ffmpeg
      ];
      environment = cfg.env;

      serviceConfig = {
        DynamicUser = true;
        User = "mediamtx";
        Group = "mediamtx";

        LogsDirectory = "mediamtx";

        # user likely may want to stream cameras, can't hurt to add video group
        SupplementaryGroups = "video";

        ExecStart = "${package}/bin/mediamtx /etc/mediamtx.yaml";
        SupplementaryGroups = lib.mkIf cfg.allowVideoAccess "video";
        ExecStart = "${cfg.package}/bin/mediamtx /etc/mediamtx.yaml";
      };
    };
  };
+35 −23
Original line number Diff line number Diff line
@@ -253,16 +253,24 @@ sub parse_systemd_ini {
# If a directory with the same basename ending in .d exists next to the unit file, it will be
# assumed to contain override files which will be parsed as well and handled properly.
sub parse_unit {
    my ($unit_path) = @_;
    my ($unit_path, $base_unit_path) = @_;

    # Parse the main unit and all overrides
    my %unit_data;
    # Replace \ with \\ so glob() still works with units that have a \ in them
    # Valid characters in unit names are ASCII letters, digits, ":", "-", "_", ".", and "\"
    $base_unit_path =~ s/\\/\\\\/gmsx;
    $unit_path =~ s/\\/\\\\/gmsx;
    foreach (glob("${unit_path}{,.d/*.conf}")) {

    foreach (glob("${base_unit_path}{,.d/*.conf}")) {
        parse_systemd_ini(\%unit_data, "$_")
    }
    # Handle drop-in template-unit instance overrides
    if ($unit_path ne $base_unit_path) {
        foreach (glob("${unit_path}.d/*.conf")) {
            parse_systemd_ini(\%unit_data, "$_")
        }
    }
    return %unit_data;
}

@@ -423,7 +431,7 @@ sub compare_units { ## no critic(Subroutines::ProhibitExcessComplexity)
# Called when a unit exists in both the old systemd and the new system and the units
# differ. This figures out of what units are to be stopped, restarted, reloaded, started, and skipped.
sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutines::ProhibitExcessComplexity)
    my ($unit, $base_name, $new_unit_file, $new_unit_info, $active_cur, $units_to_stop, $units_to_start, $units_to_reload, $units_to_restart, $units_to_skip) = @_;
    my ($unit, $base_name, $new_unit_file, $new_base_unit_file, $new_unit_info, $active_cur, $units_to_stop, $units_to_start, $units_to_reload, $units_to_restart, $units_to_skip) = @_;

    if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target" || $unit =~ /\.path$/msx || $unit =~ /\.slice$/msx) {
        # Do nothing.  These cannot be restarted directly.
@@ -442,7 +450,7 @@ sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutin
        # Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
        # More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
    } else {
        my %new_unit_info = $new_unit_info ? %{$new_unit_info} : parse_unit($new_unit_file);
        my %new_unit_info = $new_unit_info ? %{$new_unit_info} : parse_unit($new_unit_file, $new_base_unit_file);
        if (parse_systemd_bool(\%new_unit_info, "Service", "X-ReloadIfChanged", 0) and not $units_to_restart->{$unit} and not $units_to_stop->{$unit}) {
            $units_to_reload->{$unit} = 1;
            record_unit($reload_list_file, $unit);
@@ -538,31 +546,33 @@ my %units_to_filter; # units not shown

my $active_cur = get_active_units();
while (my ($unit, $state) = each(%{$active_cur})) {
    my $base_unit = $unit;
    my $cur_unit_file = "/etc/systemd/system/$unit";
    my $new_unit_file = "$toplevel/etc/systemd/system/$unit";

    my $cur_unit_file = "/etc/systemd/system/$base_unit";
    my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
    my $base_unit = $unit;
    my $cur_base_unit_file = $cur_unit_file;
    my $new_base_unit_file = $new_unit_file;

    # Detect template instances.
    if (!-e $cur_unit_file && !-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
      $base_unit = "$1\@.$2";
      $cur_unit_file = "/etc/systemd/system/$base_unit";
      $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
      $cur_base_unit_file = "/etc/systemd/system/$base_unit";
      $new_base_unit_file = "$toplevel/etc/systemd/system/$base_unit";
    }

    my $base_name = $base_unit;
    $base_name =~ s/\.[[:lower:]]*$//msx;

    if (-e $cur_unit_file && ($state->{state} eq "active" || $state->{state} eq "activating")) {
        if (! -e $new_unit_file || abs_path($new_unit_file) eq "/dev/null") {
            my %cur_unit_info = parse_unit($cur_unit_file);
    if (-e $cur_base_unit_file && ($state->{state} eq "active" || $state->{state} eq "activating")) {
        if (! -e $new_base_unit_file || abs_path($new_base_unit_file) eq "/dev/null") {
            my %cur_unit_info = parse_unit($cur_unit_file, $cur_base_unit_file);
            if (parse_systemd_bool(\%cur_unit_info, "Unit", "X-StopOnRemoval", 1)) {
                $units_to_stop{$unit} = 1;
            }
        }

        elsif ($unit =~ /\.target$/msx) {
            my %new_unit_info = parse_unit($new_unit_file);
            my %new_unit_info = parse_unit($new_unit_file, $new_base_unit_file);

            # Cause all active target units to be restarted below.
            # This should start most changed units we stop here as
@@ -596,11 +606,11 @@ while (my ($unit, $state) = each(%{$active_cur})) {
        }

        else {
            my %cur_unit_info = parse_unit($cur_unit_file);
            my %new_unit_info = parse_unit($new_unit_file);
            my %cur_unit_info = parse_unit($cur_unit_file, $cur_base_unit_file);
            my %new_unit_info = parse_unit($new_unit_file, $new_base_unit_file);
            my $diff = compare_units(\%cur_unit_info, \%new_unit_info);
            if ($diff == 1) {
                handle_modified_unit($unit, $base_name, $new_unit_file, \%new_unit_info, $active_cur, \%units_to_stop, \%units_to_start, \%units_to_reload, \%units_to_restart, \%units_to_skip);
                handle_modified_unit($unit, $base_name, $new_unit_file, $new_base_unit_file, \%new_unit_info, $active_cur, \%units_to_stop, \%units_to_start, \%units_to_reload, \%units_to_restart, \%units_to_skip);
            } elsif ($diff == 2 and not $units_to_restart{$unit}) {
                $units_to_reload{$unit} = 1;
                record_unit($reload_list_file, $unit);
@@ -710,13 +720,14 @@ if ($action eq "dry-activate") {
    # Handle the activation script requesting the restart or reload of a unit.
    foreach (split(/\n/msx, read_file($dry_restart_by_activation_file, err_mode => "quiet") // "")) {
        my $unit = $_;
        my $new_unit_file = "$toplevel/etc/systemd/system/$unit";
        my $base_unit = $unit;
        my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
        my $new_base_unit_file = $new_unit_file;

        # Detect template instances.
        if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
          $base_unit = "$1\@.$2";
          $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
          $new_base_unit_file = "$toplevel/etc/systemd/system/$base_unit";
        }

        my $base_name = $base_unit;
@@ -728,7 +739,7 @@ if ($action eq "dry-activate") {
            next;
        }

        handle_modified_unit($unit, $base_name, $new_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
        handle_modified_unit($unit, $base_name, $new_unit_file, $new_base_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
    }
    unlink($dry_restart_by_activation_file);

@@ -782,13 +793,14 @@ system("$out/activate", "$out") == 0 or $res = 2;
# Handle the activation script requesting the restart or reload of a unit.
foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quiet") // "")) {
    my $unit = $_;
    my $new_unit_file = "$toplevel/etc/systemd/system/$unit";
    my $base_unit = $unit;
    my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
    my $new_base_unit_file = $new_unit_file;

    # Detect template instances.
    if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
      $base_unit = "$1\@.$2";
      $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
      $new_base_unit_file = "$toplevel/etc/systemd/system/$base_unit";
    }

    my $base_name = $base_unit;
@@ -801,7 +813,7 @@ foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quie
        next;
    }

    handle_modified_unit($unit, $base_name, $new_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
    handle_modified_unit($unit, $base_name, $new_unit_file, $new_base_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
}
# We can remove the file now because it has been propagated to the other restart/reload files
unlink($restart_by_activation_file);
@@ -859,7 +871,7 @@ if (scalar(keys(%units_to_reload)) > 0) {
    for my $unit (keys(%units_to_reload)) {
        if (!unit_is_active($unit)) {
            # Figure out if we need to start the unit
            my %unit_info = parse_unit("$toplevel/etc/systemd/system/$unit");
            my %unit_info = parse_unit("$toplevel/etc/systemd/system/$unit", "$toplevel/etc/systemd/system/$unit");
            if (!(parse_systemd_bool(\%unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%unit_info, "Unit", "X-OnlyManualStart", 0))) {
                $units_to_start{$unit} = 1;
                record_unit($start_list_file, $unit);
+1 −0
Original line number Diff line number Diff line
@@ -463,6 +463,7 @@ in {
  matrix-conduit = handleTest ./matrix/conduit.nix {};
  matrix-synapse = handleTest ./matrix/synapse.nix {};
  mattermost = handleTest ./mattermost.nix {};
  mediamtx = handleTest ./mediamtx.nix {};
  mediatomb = handleTest ./mediatomb.nix {};
  mediawiki = handleTest ./mediawiki.nix {};
  meilisearch = handleTest ./meilisearch.nix {};
+57 −0
Original line number Diff line number Diff line
import ./make-test-python.nix ({ pkgs, lib, ...} :

{
  name = "mediamtx";
  meta.maintainers = with lib.maintainers; [ fpletz ];

  nodes = {
    machine = { config, ... }: {
      services.mediamtx = {
        enable = true;
        settings = {
          metrics = true;
          paths.all.source = "publisher";
        };
      };

      systemd.services.rtmp-publish = {
        description = "Publish an RTMP stream to mediamtx";
        after = [ "mediamtx.service" ];
        bindsTo = [ "mediamtx.service" ];
        wantedBy = [ "multi-user.target" ];
        serviceConfig = {
          DynamicUser = true;
          Restart = "on-failure";
          RestartSec = "1s";
          TimeoutStartSec = "10s";
          ExecStart = "${lib.getBin pkgs.ffmpeg-headless}/bin/ffmpeg -re -f lavfi -i smptebars=size=800x600:rate=10 -c libx264 -f flv rtmp://localhost:1935/test";
        };
      };

      systemd.services.rtmp-receive = {
        description = "Receive an RTMP stream from mediamtx";
        after = [ "rtmp-publish.service" ];
        bindsTo = [ "rtmp-publish.service" ];
        wantedBy = [ "multi-user.target" ];
        serviceConfig = {
          DynamicUser = true;
          Restart = "on-failure";
          RestartSec = "1s";
          TimeoutStartSec = "10s";
          ExecStart = "${lib.getBin pkgs.ffmpeg-headless}/bin/ffmpeg -y -re -i rtmp://localhost:1935/test -f flv /dev/null";
        };
      };
    };
  };

  testScript = ''
    start_all()

    machine.wait_for_unit("mediamtx.service")
    machine.wait_for_unit("rtmp-publish.service")
    machine.wait_for_unit("rtmp-receive.service")
    machine.wait_for_open_port(9998)
    machine.succeed("curl http://localhost:9998/metrics | grep '^rtmp_conns.*state=\"publish\".*1$'")
    machine.succeed("curl http://localhost:9998/metrics | grep '^rtmp_conns.*state=\"read\".*1$'")
  '';
})
Loading