diff options
Diffstat (limited to 'nixos/modules')
20 files changed, 942 insertions, 497 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 6430993d5c6..13703968167 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -180,6 +180,7 @@ ./programs/msmtp.nix ./programs/mtr.nix ./programs/nano.nix + ./programs/nbd.nix ./programs/neovim.nix ./programs/nm-applet.nix ./programs/npm.nix @@ -819,6 +820,7 @@ ./services/networking/nar-serve.nix ./services/networking/nat.nix ./services/networking/nats.nix + ./services/networking/nbd.nix ./services/networking/ndppd.nix ./services/networking/nebula.nix ./services/networking/networkmanager.nix @@ -985,6 +987,7 @@ ./services/system/nscd.nix ./services/system/saslauthd.nix ./services/system/self-deploy.nix + ./services/system/systembus-notify.nix ./services/system/uptimed.nix ./services/torrent/deluge.nix ./services/torrent/flexget.nix diff --git a/nixos/modules/programs/captive-browser.nix b/nixos/modules/programs/captive-browser.nix index dc054504ea4..aad554c2bd6 100644 --- a/nixos/modules/programs/captive-browser.nix +++ b/nixos/modules/programs/captive-browser.nix @@ -1,8 +1,12 @@ { config, lib, pkgs, ... }: -with lib; let cfg = config.programs.captive-browser; + + inherit (lib) + concatStringsSep escapeShellArgs optionalString + literalExpression mkEnableOption mkIf mkOption mkOptionDefault types; + browserDefault = chromium: concatStringsSep " " [ ''env XDG_CONFIG_HOME="$PREV_CONFIG_HOME"'' ''${chromium}/bin/chromium'' @@ -15,6 +19,15 @@ let ''-no-default-browser-check'' ''http://cache.nixos.org/'' ]; + + desktopItem = pkgs.makeDesktopItem { + name = "captive-browser"; + desktopName = "Captive Portal Browser"; + exec = "/run/wrappers/bin/captive-browser"; + icon = "nix-snowflake"; + categories = [ "Network" ]; + }; + in { ###### interface @@ -84,6 +97,11 @@ in ###### implementation config = mkIf cfg.enable { + environment.systemPackages = [ + (pkgs.runCommandNoCC "captive-browser-desktop-item" { } '' + install -Dm444 -t $out/share/applications ${desktopItem}/share/applications/*.desktop + '') + ]; programs.captive-browser.dhcp-dns = let diff --git a/nixos/modules/programs/nbd.nix b/nixos/modules/programs/nbd.nix new file mode 100644 index 00000000000..fea9bc1ff71 --- /dev/null +++ b/nixos/modules/programs/nbd.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.nbd; +in +{ + options = { + programs.nbd = { + enable = mkEnableOption "Network Block Device (nbd) support"; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = with pkgs; [ nbd ]; + boot.kernelModules = [ "nbd" ]; + }; +} diff --git a/nixos/modules/services/audio/squeezelite.nix b/nixos/modules/services/audio/squeezelite.nix index 05506f5bcc7..36295e21c60 100644 --- a/nixos/modules/services/audio/squeezelite.nix +++ b/nixos/modules/services/audio/squeezelite.nix @@ -1,50 +1,46 @@ { config, lib, pkgs, ... }: -with lib; - let + inherit (lib) mkEnableOption mkIf mkOption optionalString types; + dataDir = "/var/lib/squeezelite"; cfg = config.services.squeezelite; + pkg = if cfg.pulseAudio then pkgs.squeezelite-pulse else pkgs.squeezelite; + bin = "${pkg}/bin/${pkg.pname}"; -in { +in +{ ###### interface - options = { - - services.squeezelite= { + options.services.squeezelite = { + enable = mkEnableOption "Squeezelite, a software Squeezebox emulator"; - enable = mkEnableOption "Squeezelite, a software Squeezebox emulator"; - - extraArguments = mkOption { - default = ""; - type = types.str; - description = '' - Additional command line arguments to pass to Squeezelite. - ''; - }; + pulseAudio = mkEnableOption "pulseaudio support"; + extraArguments = mkOption { + default = ""; + type = types.str; + description = '' + Additional command line arguments to pass to Squeezelite. + ''; }; - }; ###### implementation config = mkIf cfg.enable { - - systemd.services.squeezelite= { + systemd.services.squeezelite = { wantedBy = [ "multi-user.target" ]; after = [ "network.target" "sound.target" ]; description = "Software Squeezebox emulator"; serviceConfig = { DynamicUser = true; - ExecStart = "${pkgs.squeezelite}/bin/squeezelite -N ${dataDir}/player-name ${cfg.extraArguments}"; + ExecStart = "${bin} -N ${dataDir}/player-name ${cfg.extraArguments}"; StateDirectory = builtins.baseNameOf dataDir; SupplementaryGroups = "audio"; }; }; - }; - } diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix index c3bd8f99c57..a7645e1f56e 100644 --- a/nixos/modules/services/continuous-integration/github-runner.nix +++ b/nixos/modules/services/continuous-integration/github-runner.nix @@ -34,6 +34,14 @@ in Repository to add the runner to. Changing this option triggers a new runner registration. + + IMPORTANT: If your token is org-wide (not per repository), you need to + provide a github org link, not a single repository, so do it like this + <literal>https://github.com/nixos</literal>, not like this + <literal>https://github.com/nixos/nixpkgs</literal>. + Otherwise, you are going to get a <literal>404 NotFound</literal> + from <literal>POST https://api.github.com/actions/runner-registration</literal> + in the configure script. ''; example = "https://github.com/nixos/nixpkgs"; }; diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix index 64b74ddd708..04cf82f8a46 100644 --- a/nixos/modules/services/misc/jellyfin.nix +++ b/nixos/modules/services/misc/jellyfin.nix @@ -70,7 +70,8 @@ in LockPersonality = true; PrivateTmp = true; - PrivateDevices = true; + # Disabled to allow Jellyfin to access hw accel devices endpoints + # PrivateDevices = true; PrivateUsers = true; # Disabled as it does not allow Jellyfin to interface with CUDA devices diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix index d29d50706ef..46e410f11d4 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters.nix @@ -55,6 +55,7 @@ let "postfix" "postgres" "process" + "pve" "py-air-control" "redis" "rspamd" diff --git a/nixos/modules/services/monitoring/prometheus/exporters/pve.nix b/nixos/modules/services/monitoring/prometheus/exporters/pve.nix new file mode 100644 index 00000000000..ef708414c95 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/pve.nix @@ -0,0 +1,118 @@ +{ config, lib, pkgs, options }: + +with lib; +let + cfg = config.services.prometheus.exporters.pve; + + # pve exporter requires a config file so create an empty one if configFile is not provided + emptyConfigFile = pkgs.writeTextFile { + name = "pve.yml"; + text = "default:"; + }; + + computedConfigFile = "${if cfg.configFile == null then emptyConfigFile else cfg.configFile}"; +in +{ + port = 9221; + extraOpts = { + package = mkOption { + type = types.package; + default = pkgs.prometheus-pve-exporter; + defaultText = literalExpression "pkgs.prometheus-pve-exporter"; + example = literalExpression "pkgs.prometheus-pve-exporter"; + description = '' + The package to use for prometheus-pve-exporter + ''; + }; + + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/etc/prometheus-pve-exporter/pve.env"; + description = '' + Path to the service's environment file. This path can either be a computed path in /nix/store or a path in the local filesystem. + + The environment file should NOT be stored in /nix/store as it contains passwords and/or keys in plain text. + + Environment reference: https://github.com/prometheus-pve/prometheus-pve-exporter#authentication + ''; + }; + + configFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/etc/prometheus-pve-exporter/pve.yml"; + description = '' + Path to the service's config file. This path can either be a computed path in /nix/store or a path in the local filesystem. + + The config file should NOT be stored in /nix/store as it will contain passwords and/or keys in plain text. + + If both configFile and environmentFile are provided, the configFile option will be ignored. + + Configuration reference: https://github.com/prometheus-pve/prometheus-pve-exporter/#authentication + ''; + }; + + collectors = { + status = mkOption { + type = types.bool; + default = true; + description = '' + Collect Node/VM/CT status + ''; + }; + version = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE version info + ''; + }; + node = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE node info + ''; + }; + cluster = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE cluster info + ''; + }; + resources = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE resources info + ''; + }; + config = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE onboot status + ''; + }; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${cfg.package}/bin/pve_exporter \ + --${if cfg.collectors.status == true then "" else "no-"}collector.status \ + --${if cfg.collectors.version == true then "" else "no-"}collector.version \ + --${if cfg.collectors.node == true then "" else "no-"}collector.node \ + --${if cfg.collectors.cluster == true then "" else "no-"}collector.cluster \ + --${if cfg.collectors.resources == true then "" else "no-"}collector.resources \ + --${if cfg.collectors.config == true then "" else "no-"}collector.config \ + ${computedConfigFile} \ + ${toString cfg.port} ${cfg.listenAddress} + ''; + } // optionalAttrs (cfg.environmentFile != null) { + EnvironmentFile = cfg.environmentFile; + }; + }; +} diff --git a/nixos/modules/services/networking/amuled.nix b/nixos/modules/services/networking/amuled.nix index e55ac7a6b18..aa72a047526 100644 --- a/nixos/modules/services/networking/amuled.nix +++ b/nixos/modules/services/networking/amuled.nix @@ -76,7 +76,7 @@ in script = '' ${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${user} \ - -c 'HOME="${cfg.dataDir}" ${pkgs.amuleDaemon}/bin/amuled' + -c 'HOME="${cfg.dataDir}" ${pkgs.amule-daemon}/bin/amuled' ''; }; }; diff --git a/nixos/modules/services/networking/nbd.nix b/nixos/modules/services/networking/nbd.nix new file mode 100644 index 00000000000..87f8c41a8e5 --- /dev/null +++ b/nixos/modules/services/networking/nbd.nix @@ -0,0 +1,146 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.nbd; + configFormat = pkgs.formats.ini { }; + iniFields = with types; attrsOf (oneOf [ bool int float str ]); + serverConfig = configFormat.generate "nbd-server-config" + ({ + generic = + (cfg.server.extraOptions // { + user = "root"; + group = "root"; + port = cfg.server.listenPort; + } // (optionalAttrs (cfg.server.listenAddress != null) { + listenaddr = cfg.server.listenAddress; + })); + } + // (mapAttrs + (_: { path, allowAddresses, extraOptions }: + extraOptions // { + exportname = path; + } // (optionalAttrs (allowAddresses != null) { + authfile = pkgs.writeText "authfile" (concatStringsSep "\n" allowAddresses); + })) + cfg.server.exports) + ); + splitLists = + partition + (path: hasPrefix "/dev/" path) + (mapAttrsToList (_: { path, ... }: path) cfg.server.exports); + allowedDevices = splitLists.right; + boundPaths = splitLists.wrong; +in +{ + options = { + services.nbd = { + server = { + enable = mkEnableOption "the Network Block Device (nbd) server"; + + listenPort = mkOption { + type = types.port; + default = 10809; + description = "Port to listen on. The port is NOT automatically opened in the firewall."; + }; + + extraOptions = mkOption { + type = iniFields; + default = { + allowlist = false; + }; + description = '' + Extra options for the server. See + <citerefentry><refentrytitle>nbd-server</refentrytitle> + <manvolnum>5</manvolnum></citerefentry>. + ''; + }; + + exports = mkOption { + description = "Files or block devices to make available over the network."; + default = { }; + type = with types; attrsOf + (submodule { + options = { + path = mkOption { + type = str; + description = "File or block device to export."; + example = "/dev/sdb1"; + }; + + allowAddresses = mkOption { + type = nullOr (listOf str); + default = null; + example = [ "10.10.0.0/24" "127.0.0.1" ]; + description = "IPs and subnets that are authorized to connect for this device. If not specified, the server will allow all connections."; + }; + + extraOptions = mkOption { + type = iniFields; + default = { + flush = true; + fua = true; + }; + description = '' + Extra options for this export. See + <citerefentry><refentrytitle>nbd-server</refentrytitle> + <manvolnum>5</manvolnum></citerefentry>. + ''; + }; + }; + }); + }; + + listenAddress = mkOption { + type = with types; nullOr str; + description = "Address to listen on. If not specified, the server will listen on all interfaces."; + default = null; + example = "10.10.0.1"; + }; + }; + }; + }; + + config = mkIf cfg.server.enable { + boot.kernelModules = [ "nbd" ]; + + systemd.services.nbd-server = { + after = [ "network-online.target" ]; + before = [ "multi-user.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${pkgs.nbd}/bin/nbd-server -C ${serverConfig}"; + Type = "forking"; + + DeviceAllow = map (path: "${path} rw") allowedDevices; + BindPaths = boundPaths; + + CapabilityBoundingSet = ""; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = false; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "noaccess"; + ProtectSystem = "strict"; + RestrictAddressFamilies = "AF_INET AF_INET6"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + UMask = "0077"; + }; + }; + }; +} diff --git a/nixos/modules/services/networking/tox-node.nix b/nixos/modules/services/networking/tox-node.nix index c24e7fd1285..c6e5c2d6e81 100644 --- a/nixos/modules/services/networking/tox-node.nix +++ b/nixos/modules/services/networking/tox-node.nix @@ -8,12 +8,7 @@ let homeDir = "/var/lib/tox-node"; configFile = let - # fetchurl should be switched to getting this file from tox-node.src once - # the dpkg directory is in a release - src = pkgs.fetchurl { - url = "https://raw.githubusercontent.com/tox-rs/tox-node/master/dpkg/config.yml"; - sha256 = "1431wzpzm786mcvyzk1rp7ar418n45dr75hdggxvlm7pkpam31xa"; - }; + src = "${pkg.src}/dpkg/config.yml"; confJSON = pkgs.writeText "config.json" ( builtins.toJSON { log-type = cfg.logType; diff --git a/nixos/modules/services/system/earlyoom.nix b/nixos/modules/services/system/earlyoom.nix index b355df056bc..ddd5bcebcdd 100644 --- a/nixos/modules/services/system/earlyoom.nix +++ b/nixos/modules/services/system/earlyoom.nix @@ -1,81 +1,73 @@ { config, lib, pkgs, ... }: -with lib; - let - ecfg = config.services.earlyoom; + cfg = config.services.earlyoom; + + inherit (lib) + mkDefault mkEnableOption mkIf mkOption types + mkRemovedOptionModule + concatStringsSep optional; + in { - options = { - services.earlyoom = { + options.services.earlyoom = { + enable = mkEnableOption "Early out of memory killing"; - enable = mkOption { - type = types.bool; - default = false; - description = '' - Enable early out of memory killing. - ''; - }; + freeMemThreshold = mkOption { + type = types.ints.between 1 100; + default = 10; + description = '' + Minimum of availabe memory (in percent). + If the free memory falls below this threshold and the analog is true for + <option>services.earlyoom.freeSwapThreshold</option> + the killing begins. + ''; + }; - freeMemThreshold = mkOption { - type = types.int; - default = 10; - description = '' - Minimum of availabe memory (in percent). - If the free memory falls below this threshold and the analog is true for - <option>services.earlyoom.freeSwapThreshold</option> - the killing begins. - ''; - }; + freeSwapThreshold = mkOption { + type = types.ints.between 1 100; + default = 10; + description = '' + Minimum of availabe swap space (in percent). + If the available swap space falls below this threshold and the analog + is true for <option>services.earlyoom.freeMemThreshold</option> + the killing begins. + ''; + }; - freeSwapThreshold = mkOption { - type = types.int; - default = 10; - description = '' - Minimum of availabe swap space (in percent). - If the available swap space falls below this threshold and the analog - is true for <option>services.earlyoom.freeMemThreshold</option> - the killing begins. - ''; - }; + # TODO: remove or warn after 1.7 (https://github.com/rfjakob/earlyoom/commit/7ebc4554) + ignoreOOMScoreAdjust = mkOption { + type = types.bool; + default = false; + description = '' + Ignore oom_score_adjust values of processes. + ''; + }; - # TODO: remove or warn after 1.7 (https://github.com/rfjakob/earlyoom/commit/7ebc4554) - ignoreOOMScoreAdjust = mkOption { - type = types.bool; - default = false; - description = '' - Ignore oom_score_adjust values of processes. - ''; - }; + enableDebugInfo = mkOption { + type = types.bool; + default = false; + description = '' + Enable debugging messages. + ''; + }; - enableDebugInfo = mkOption { - type = types.bool; - default = false; - description = '' - Enable debugging messages. - ''; - }; + enableNotifications = mkOption { + type = types.bool; + default = false; + description = '' + Send notifications about killed processes via the system d-bus. - notificationsCommand = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - This option is deprecated and ignored by earlyoom since 1.6. - Use <option>services.earlyoom.enableNotifications</option> instead. - ''; - }; + WARNING: enabling this option (while convenient) should *not* be done on a + machine where you do not trust the other users as it allows any other + local user to DoS your session by spamming notifications. - enableNotifications = mkOption { - type = types.bool; - default = false; - description = '' - Send notifications about killed processes via the system d-bus. - To actually see the notifications in your GUI session, you need to have - <literal>systembus-notify</literal> running as your user. + To actually see the notifications in your GUI session, you need to have + <literal>systembus-notify</literal> running as your user which this + option handles. - See <link xlink:href="https://github.com/rfjakob/earlyoom#notifications">README</link> for details. - ''; - }; + See <link xlink:href="https://github.com/rfjakob/earlyoom#notifications">README</link> for details. + ''; }; }; @@ -83,37 +75,30 @@ in (mkRemovedOptionModule [ "services" "earlyoom" "useKernelOOMKiller" ] '' This option is deprecated and ignored by earlyoom since 1.2. '') + (mkRemovedOptionModule [ "services" "earlyoom" "notificationsCommand" ] '' + This option is deprecated and ignored by earlyoom since 1.6. + '') ]; - config = mkIf ecfg.enable { - assertions = [ - { assertion = ecfg.freeMemThreshold > 0 && ecfg.freeMemThreshold <= 100; - message = "Needs to be a positive percentage"; } - { assertion = ecfg.freeSwapThreshold > 0 && ecfg.freeSwapThreshold <= 100; - message = "Needs to be a positive percentage"; } - ]; - - # TODO: reimplement this option as -N after 1.7 (https://github.com/rfjakob/earlyoom/commit/afe03606) - warnings = optional (ecfg.notificationsCommand != null) - "`services.earlyoom.notificationsCommand` is deprecated and ignored by earlyoom since 1.6."; + config = mkIf cfg.enable { + services.systembus-notify.enable = mkDefault cfg.enableNotifications; systemd.services.earlyoom = { description = "Early OOM Daemon for Linux"; wantedBy = [ "multi-user.target" ]; - path = optional ecfg.enableNotifications pkgs.dbus; + path = optional cfg.enableNotifications pkgs.dbus; serviceConfig = { - StandardOutput = "null"; StandardError = "journal"; ExecStart = concatStringsSep " " ([ "${pkgs.earlyoom}/bin/earlyoom" - "-m ${toString ecfg.freeMemThreshold}" - "-s ${toString ecfg.freeSwapThreshold}" - ] ++ optional ecfg.ignoreOOMScoreAdjust "-i" - ++ optional ecfg.enableDebugInfo "-d" - ++ optional ecfg.enableNotifications "-n"); + "-m ${toString cfg.freeMemThreshold}" + "-s ${toString cfg.freeSwapThreshold}" + ] + ++ optional cfg.ignoreOOMScoreAdjust "-i" + ++ optional cfg.enableDebugInfo "-d" + ++ optional cfg.enableNotifications "-n" + ); }; }; - - environment.systemPackages = optional ecfg.enableNotifications pkgs.systembus-notify; }; } diff --git a/nixos/modules/services/system/systembus-notify.nix b/nixos/modules/services/system/systembus-notify.nix new file mode 100644 index 00000000000..e918bc552ec --- /dev/null +++ b/nixos/modules/services/system/systembus-notify.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.systembus-notify; + + inherit (lib) mkEnableOption mkIf; + +in +{ + options.services.systembus-notify = { + enable = mkEnableOption '' + System bus notification support + + WARNING: enabling this option (while convenient) should *not* be done on a + machine where you do not trust the other users as it allows any other + local user to DoS your session by spamming notifications. + ''; + }; + + config = mkIf cfg.enable { + systemd = { + packages = with pkgs; [ systembus-notify ]; + + user.services.systembus-notify.wantedBy = [ "graphical-session.target" ]; + }; + }; +} diff --git a/nixos/modules/services/video/epgstation/default.nix b/nixos/modules/services/video/epgstation/default.nix index 41613dcbb3b..191f6eb52e5 100644 --- a/nixos/modules/services/video/epgstation/default.nix +++ b/nixos/modules/services/video/epgstation/default.nix @@ -1,30 +1,40 @@ { config, lib, options, pkgs, ... }: -with lib; - let cfg = config.services.epgstation; opt = options.services.epgstation; + description = "EPGStation: DVR system for Mirakurun-managed TV tuners"; + username = config.users.users.epgstation.name; groupname = config.users.users.epgstation.group; + mirakurun = { + sock = config.services.mirakurun.unixSocket; + option = options.services.mirakurun.unixSocket; + }; - settingsFmt = pkgs.formats.json {}; - settingsTemplate = settingsFmt.generate "config.json" cfg.settings; + yaml = pkgs.formats.yaml { }; + settingsTemplate = yaml.generate "config.yml" cfg.settings; preStartScript = pkgs.writeScript "epgstation-prestart" '' #!${pkgs.runtimeShell} - PASSWORD="$(head -n1 "${cfg.basicAuth.passwordFile}")" - DB_PASSWORD="$(head -n1 "${cfg.database.passwordFile}")" + DB_PASSWORD_FILE=${lib.escapeShellArg cfg.database.passwordFile} + + if [[ ! -f "$DB_PASSWORD_FILE" ]]; then + printf "[FATAL] File containing the DB password was not found in '%s'. Double check the NixOS option '%s'." \ + "$DB_PASSWORD_FILE" ${lib.escapeShellArg opt.database.passwordFile} >&2 + exit 1 + fi + + DB_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.database.passwordFile})" # setup configuration - touch /etc/epgstation/config.json - chmod 640 /etc/epgstation/config.json + touch /etc/epgstation/config.yml + chmod 640 /etc/epgstation/config.yml sed \ - -e "s,@password@,$PASSWORD,g" \ -e "s,@dbPassword@,$DB_PASSWORD,g" \ - ${settingsTemplate} > /etc/epgstation/config.json - chown "${username}:${groupname}" /etc/epgstation/config.json + ${settingsTemplate} > /etc/epgstation/config.yml + chown "${username}:${groupname}" /etc/epgstation/config.yml # NOTE: Use password authentication, since mysqljs does not yet support auth_socket if [ ! -e /var/lib/epgstation/db-created ]; then @@ -35,7 +45,7 @@ let ''; streamingConfig = lib.importJSON ./streaming.json; - logConfig = { + logConfig = yaml.generate "logConfig.yml" { appenders.stdout.type = "stdout"; categories = { default = { appenders = [ "stdout" ]; level = "info"; }; @@ -45,53 +55,51 @@ let }; }; - defaultPassword = "INSECURE_GO_CHECK_CONFIGURATION_NIX\n"; + # Deprecate top level options that are redundant. + deprecateTopLevelOption = config: + lib.mkRenamedOptionModule + ([ "services" "epgstation" ] ++ config) + ([ "services" "epgstation" "settings" ] ++ config); + + removeOption = config: instruction: + lib.mkRemovedOptionModule + ([ "services" "epgstation" ] ++ config) + instruction; in { - options.services.epgstation = { - enable = mkEnableOption "EPGStation: DTV Software in Japan"; + meta.maintainers = with lib.maintainers; [ midchildan ]; - usePreconfiguredStreaming = mkOption { - type = types.bool; - default = true; - description = '' - Use preconfigured default streaming options. + imports = [ + (deprecateTopLevelOption [ "port" ]) + (deprecateTopLevelOption [ "socketioPort" ]) + (deprecateTopLevelOption [ "clientSocketioPort" ]) + (removeOption [ "basicAuth" ] + "Use a TLS-terminated reverse proxy with authentication instead.") + ]; - Upstream defaults: - <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.sample.json"/> - ''; - }; + options.services.epgstation = { + enable = lib.mkEnableOption description; - port = mkOption { - type = types.port; - default = 20772; - description = '' - HTTP port for EPGStation to listen on. - ''; + package = lib.mkOption { + default = pkgs.epgstation; + type = lib.types.package; + defaultText = lib.literalExpression "pkgs.epgstation"; + description = "epgstation package to use"; }; - socketioPort = mkOption { - type = types.port; - default = cfg.port + 1; - defaultText = literalExpression "config.${opt.port} + 1"; + usePreconfiguredStreaming = lib.mkOption { + type = lib.types.bool; + default = true; description = '' - Socket.io port for EPGStation to listen on. - ''; - }; + Use preconfigured default streaming options. - clientSocketioPort = mkOption { - type = types.port; - default = cfg.socketioPort; - defaultText = literalExpression "config.${opt.socketioPort}"; - description = '' - Socket.io port that the web client is going to connect to. This may be - different from <option>socketioPort</option> if EPGStation is hidden - behind a reverse proxy. + Upstream defaults: + <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/config/config.yml.template"/> ''; }; - openFirewall = mkOption { - type = types.bool; + openFirewall = lib.mkOption { + type = lib.types.bool; default = false; description = '' Open ports in the firewall for the EPGStation web interface. @@ -106,50 +114,17 @@ in ''; }; - basicAuth = { - user = mkOption { - type = with types; nullOr str; - default = null; - example = "epgstation"; - description = '' - Basic auth username for EPGStation. If <literal>null</literal>, basic - auth will be disabled. - - <warning> - <para> - Basic authentication has known weaknesses, the most critical being - that it sends passwords over the network in clear text. Use this - feature to control access to EPGStation within your family and - friends, but don't rely on it for security. - </para> - </warning> - ''; - }; - - passwordFile = mkOption { - type = types.path; - default = pkgs.writeText "epgstation-password" defaultPassword; - defaultText = literalDocBook ''a file containing <literal>${defaultPassword}</literal>''; - example = "/run/keys/epgstation-password"; - description = '' - A file containing the password for <option>basicAuth.user</option>. - ''; - }; - }; - - database = { - name = mkOption { - type = types.str; + database = { + name = lib.mkOption { + type = lib.types.str; default = "epgstation"; description = '' Name of the MySQL database that holds EPGStation's data. ''; }; - passwordFile = mkOption { - type = types.path; - default = pkgs.writeText "epgstation-db-password" defaultPassword; - defaultText = literalDocBook ''a file containing <literal>${defaultPassword}</literal>''; + passwordFile = lib.mkOption { + type = lib.types.path; example = "/run/keys/epgstation-db-password"; description = '' A file containing the password for the database named @@ -158,69 +133,106 @@ in }; }; - settings = mkOption { + # The defaults for some options come from the upstream template + # configuration, which is the one that users would get if they follow the + # upstream instructions. This is, in some cases, different from the + # application defaults. Some options like encodeProcessNum and + # concurrentEncodeNum doesn't have an optimal default value that works for + # all hardware setups and/or performance requirements. For those kind of + # options, the application default wouldn't always result in the expected + # out-of-the-box behavior because it's the responsibility of the user to + # configure them according to their needs. In these cases, the value in the + # upstream template configuration should serve as a "good enough" default. + settings = lib.mkOption { description = '' - Options to add to config.json. + Options to add to config.yml. Documentation: <link xlink:href="https://github.com/l3tnun/EPGStation/blob/master/doc/conf-manual.md"/> ''; - default = {}; + default = { }; example = { recPriority = 20; conflictPriority = 10; }; - type = types.submodule { - freeformType = settingsFmt.type; + type = lib.types.submodule { + freeformType = yaml.type; + + options.port = lib.mkOption { + type = lib.types.port; + default = 20772; + description = '' + HTTP port for EPGStation to listen on. + ''; + }; - options.readOnlyOnce = mkOption { - type = types.bool; - default = false; - description = "Don't reload configuration files at runtime."; + options.socketioPort = lib.mkOption { + type = lib.types.port; + default = cfg.settings.port + 1; + defaultText = lib.literalExpression "config.${opt.settings}.port + 1"; + description = '' + Socket.io port for EPGStation to listen on. It is valid to share + ports with <option>${opt.settings}.port</option>. + ''; }; - options.mirakurunPath = mkOption (let - sockPath = config.services.mirakurun.unixSocket; - in { - type = types.str; - default = "http+unix://${replaceStrings ["/"] ["%2F"] sockPath}"; - defaultText = literalExpression '' - "http+unix://''${replaceStrings ["/"] ["%2F"] config.${options.services.mirakurun.unixSocket}}" + options.clientSocketioPort = lib.mkOption { + type = lib.types.port; + default = cfg.settings.socketioPort; + defaultText = lib.literalExpression "config.${opt.settings}.socketioPort"; + description = '' + Socket.io port that the web client is going to connect to. This may + be different from <option>${opt.settings}.socketioPort</option> if + EPGStation is hidden behind a reverse proxy. + ''; + }; + + options.mirakurunPath = with mirakurun; lib.mkOption { + type = lib.types.str; + default = "http+unix://${lib.replaceStrings ["/"] ["%2F"] sock}"; + defaultText = lib.literalExpression '' + "http+unix://''${lib.replaceStrings ["/"] ["%2F"] config.${option}}" ''; example = "http://localhost:40772"; description = "URL to connect to Mirakurun."; - }); + }; + + options.encodeProcessNum = lib.mkOption { + type = lib.types.ints.positive; + default = 4; + description = '' + The maximum number of processes that EPGStation would allow to run + at the same time for encoding or streaming videos. + ''; + }; + + options.concurrentEncodeNum = lib.mkOption { + type = lib.types.ints.positive; + default = 1; + description = '' + The maximum number of encoding jobs that EPGStation would run at the + same time. + ''; + }; - options.encode = mkOption { - type = with types; listOf attrs; + options.encode = lib.mkOption { + type = with lib.types; listOf attrs; description = "Encoding presets for recorded videos."; default = [ { - name = "H264"; - cmd = "${pkgs.epgstation}/libexec/enc.sh main"; + name = "H.264"; + cmd = "%NODE% ${cfg.package}/libexec/enc.js"; suffix = ".mp4"; - default = true; - } - { - name = "H264-sub"; - cmd = "${pkgs.epgstation}/libexec/enc.sh sub"; - suffix = "-sub.mp4"; } ]; - defaultText = literalExpression '' + defaultText = lib.literalExpression '' [ { - name = "H264"; - cmd = "''${pkgs.epgstation}/libexec/enc.sh main"; + name = "H.264"; + cmd = "%NODE% config.${opt.package}/libexec/enc.js"; suffix = ".mp4"; - default = true; - } - { - name = "H264-sub"; - cmd = "''${pkgs.epgstation}/libexec/enc.sh sub"; - suffix = "-sub.mp4"; } ] ''; @@ -229,14 +241,25 @@ in }; }; - config = mkIf cfg.enable { + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = !(lib.hasAttr "readOnlyOnce" cfg.settings); + message = '' + The option config.${opt.settings}.readOnlyOnce can no longer be used + since it's been removed. No replacements are available. + ''; + } + ]; + environment.etc = { - "epgstation/operatorLogConfig.json".text = builtins.toJSON logConfig; - "epgstation/serviceLogConfig.json".text = builtins.toJSON logConfig; + "epgstation/epgUpdaterLogConfig.yml".source = logConfig; + "epgstation/operatorLogConfig.yml".source = logConfig; + "epgstation/serviceLogConfig.yml".source = logConfig; }; - networking.firewall = mkIf cfg.openFirewall { - allowedTCPPorts = with cfg; [ port socketioPort ]; + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = with cfg.settings; [ port socketioPort ]; }; users.users.epgstation = { @@ -245,13 +268,13 @@ in isSystemUser = true; }; - users.groups.epgstation = {}; + users.groups.epgstation = { }; - services.mirakurun.enable = mkDefault true; + services.mirakurun.enable = lib.mkDefault true; services.mysql = { - enable = mkDefault true; - package = mkDefault pkgs.mariadb; + enable = lib.mkDefault true; + package = lib.mkDefault pkgs.mariadb; ensureDatabases = [ cfg.database.name ]; # FIXME: enable once mysqljs supports auth_socket # ensureUsers = [ { @@ -260,39 +283,28 @@ in # } ]; }; - services.epgstation.settings = let - defaultSettings = { - serverPort = cfg.port; - socketioPort = cfg.socketioPort; - clientSocketioPort = cfg.clientSocketioPort; - - dbType = mkDefault "mysql"; - mysql = { - user = username; - database = cfg.database.name; - socketPath = mkDefault "/run/mysqld/mysqld.sock"; - password = mkDefault "@dbPassword@"; - connectTimeout = mkDefault 1000; - connectionLimit = mkDefault 10; + services.epgstation.settings = + let + defaultSettings = { + dbtype = lib.mkDefault "mysql"; + mysql = { + socketPath = lib.mkDefault "/run/mysqld/mysqld.sock"; + user = username; + password = lib.mkDefault "@dbPassword@"; + database = cfg.database.name; + }; + + ffmpeg = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg"; + ffprobe = lib.mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe"; + + # for disambiguation with TypeScript files + recordedFileExtension = lib.mkDefault ".m2ts"; }; - - basicAuth = mkIf (cfg.basicAuth.user != null) { - user = mkDefault cfg.basicAuth.user; - password = mkDefault "@password@"; - }; - - ffmpeg = mkDefault "${pkgs.ffmpeg-full}/bin/ffmpeg"; - ffprobe = mkDefault "${pkgs.ffmpeg-full}/bin/ffprobe"; - - fileExtension = mkDefault ".m2ts"; - maxEncode = mkDefault 2; - maxStreaming = mkDefault 2; - }; - in - mkMerge [ - defaultSettings - (mkIf cfg.usePreconfiguredStreaming streamingConfig) - ]; + in + lib.mkMerge [ + defaultSettings + (lib.mkIf cfg.usePreconfiguredStreaming streamingConfig) + ]; systemd.tmpfiles.rules = [ "d '/var/lib/epgstation/streamfiles' - ${username} ${groupname} - -" @@ -301,15 +313,15 @@ in ]; systemd.services.epgstation = { - description = pkgs.epgstation.meta.description; + inherit description; + wantedBy = [ "multi-user.target" ]; - after = [ - "network.target" - ] ++ optional config.services.mirakurun.enable "mirakurun.service" - ++ optional config.services.mysql.enable "mysql.service"; + after = [ "network.target" ] + ++ lib.optional config.services.mirakurun.enable "mirakurun.service" + ++ lib.optional config.services.mysql.enable "mysql.service"; serviceConfig = { - ExecStart = "${pkgs.epgstation}/bin/epgstation start"; + ExecStart = "${cfg.package}/bin/epgstation start"; ExecStartPre = "+${preStartScript}"; User = username; Group = groupname; diff --git a/nixos/modules/services/video/epgstation/streaming.json b/nixos/modules/services/video/epgstation/streaming.json index 8eb99cf8558..7f8df0817fc 100644 --- a/nixos/modules/services/video/epgstation/streaming.json +++ b/nixos/modules/services/video/epgstation/streaming.json @@ -1,119 +1,140 @@ { - "liveHLS": [ - { - "name": "720p", - "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" + "urlscheme": { + "m2ts": { + "ios": "vlc-x-callback://x-callback-url/stream?url=PROTOCOL://ADDRESS", + "android": "intent://ADDRESS#Intent;package=org.videolan.vlc;type=video;scheme=PROTOCOL;end" }, - { - "name": "480p", - "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%" + "video": { + "ios": "infuse://x-callback-url/play?url=PROTOCOL://ADDRESS", + "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=PROTOCOL;end" }, - { - "name": "180p", - "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 48k -ac 2 -c:v libx264 -vf yadif,scale=-2:180 -b:v 100k -preset veryfast -maxrate 110k -bufsize 1000k -flags +loop-global_header %OUTPUT%" + "download": { + "ios": "vlc-x-callback://x-callback-url/download?url=PROTOCOL://ADDRESS&filename=FILENAME" } - ], - "liveMP4": [ - { - "name": "720p", - "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" - }, - { - "name": "480p", - "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" - } - ], - "liveWebM": [ - { - "name": "720p", - "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" - }, - { - "name": "480p", - "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" - } - ], - "mpegTsStreaming": [ - { - "name": "720p", - "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1" - }, - { - "name": "480p", - "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1" - }, - { - "name": "Original" - } - ], - "mpegTsViewer": { - "ios": "vlc-x-callback://x-callback-url/stream?url=http://ADDRESS", - "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end" - }, - "recordedDownloader": { - "ios": "vlc-x-callback://x-callback-url/download?url=http://ADDRESS&filename=FILENAME", - "android": "intent://ADDRESS#Intent;package=com.dv.adm;type=video;scheme=http;end" }, - "recordedStreaming": { - "webm": [ - { - "name": "720p", - "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1", - "vb": "3000k", - "ab": "192k" - }, - { - "name": "360p", - "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1", - "vb": "1500k", - "ab": "128k" - } - ], - "mp4": [ - { - "name": "720p", - "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1", - "vb": "3000k", - "ab": "192k" - }, - { - "name": "360p", - "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1", - "vb": "1500k", - "ab": "128k" + "stream": { + "live": { + "ts": { + "m2ts": [ + { + "name": "720p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1" + }, + { + "name": "無変換" + } + ], + "m2tsll": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -y -f mpegts pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -f mpegts -analyzeduration 500000 -i pipe:0 -map 0 -c:s copy -c:d copy -ignore_unknown -fflags nobuffer -flags low_delay -max_delay 250000 -max_interleave_delta 1 -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -flags +cgop -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -y -f mpegts pipe:1" + } + ], + "webm": [ + { + "name": "720p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 2 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + } + ], + "mp4": [ + { + "name": "720p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" + } + ], + "hls": [ + { + "name": "720p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -re -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 17 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%" + } + ] } - ], - "mpegTs": [ - { - "name": "720p (H.264)", - "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:720 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1", - "vb": "3000k", - "ab": "192k" + }, + "recorded": { + "ts": { + "webm": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf yadif,scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + } + ], + "mp4": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" + } + ], + "hls": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -i pipe:0 -sn -map 0 -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%" + } + ] }, - { - "name": "360p (H.264)", - "cmd": "%FFMPEG% -dual_mono_mode main %RE% -i pipe:0 -sn -threads 0 -c:a aac -ar 48000 -ac 2 -c:v libx264 -vf yadif,scale=-2:360 %VB% %VBUFFER% %AB% %ABUFFER% -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -y -f mpegts pipe:1", - "vb": "1500k", - "ab": "128k" + "encoded": { + "webm": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 192k -ac 2 -c:v libvpx-vp9 -vf scale=-2:720 -b:v 3000k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 3 -c:a libvorbis -ar 48000 -b:a 128k -ac 2 -c:v libvpx-vp9 -vf scale=-2:480 -b:v 1500k -deadline realtime -speed 4 -cpu-used -8 -y -f webm pipe:1" + } + ], + "mp4": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf scale=-2:720 -b:v 3000k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf scale=-2:480 -b:v 1500k -profile:v baseline -preset veryfast -tune fastdecode,zerolatency -movflags frag_keyframe+empty_moov+faststart+default_base_moof -y -f mp4 pipe:1" + } + ], + "hls": [ + { + "name": "720p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" + }, + { + "name": "480p", + "cmd": "%FFMPEG% -dual_mono_mode main -ss %SS% -i %INPUT% -sn -threads 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -hls_flags delete_segments -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf scale=-2:480 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" + } + ] } - ] - }, - "recordedHLS": [ - { - "name": "720p", - "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 192k -ac 2 -c:v libx264 -vf yadif,scale=-2:720 -b:v 3000k -preset veryfast -flags +loop-global_header %OUTPUT%" - }, - { - "name": "480p", - "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -threads 0 -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_filename %streamFileDir%/stream%streamNum%-%09d.ts -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx264 -vf yadif,scale=-2:480 -b:v 1500k -preset veryfast -flags +loop-global_header %OUTPUT%" - }, - { - "name": "480p(h265)", - "cmd": "%FFMPEG% -dual_mono_mode main -i %INPUT% -sn -map 0 -ignore_unknown -max_muxing_queue_size 1024 -f hls -hls_time 3 -hls_list_size 0 -hls_allow_cache 1 -hls_segment_type fmp4 -hls_fmp4_init_filename stream%streamNum%-init.mp4 -hls_segment_filename stream%streamNum%-%09d.m4s -c:a aac -ar 48000 -b:a 128k -ac 2 -c:v libx265 -vf yadif,scale=-2:480 -b:v 350k -preset veryfast -tag:v hvc1 %OUTPUT%" } - ], - "recordedViewer": { - "ios": "infuse://x-callback-url/play?url=http://ADDRESS", - "android": "intent://ADDRESS#Intent;package=com.mxtech.videoplayer.ad;type=video;scheme=http;end" } } diff --git a/nixos/modules/services/web-servers/pomerium.nix b/nixos/modules/services/web-servers/pomerium.nix index 2bc7d01c7c2..0b460755f50 100644 --- a/nixos/modules/services/web-servers/pomerium.nix +++ b/nixos/modules/services/web-servers/pomerium.nix @@ -69,11 +69,16 @@ in CERTIFICATE_KEY_FILE = "key.pem"; }; startLimitIntervalSec = 60; + script = '' + if [[ -v CREDENTIALS_DIRECTORY ]]; then + cd "$CREDENTIALS_DIRECTORY" + fi + exec "${pkgs.pomerium}/bin/pomerium" -config "${cfgFile}" + ''; serviceConfig = { DynamicUser = true; StateDirectory = [ "pomerium" ]; - ExecStart = "${pkgs.pomerium}/bin/pomerium -config ${cfgFile}"; PrivateUsers = false; # breaks CAP_NET_BIND_SERVICE MemoryDenyWriteExecute = false; # breaks LuaJIT @@ -99,7 +104,6 @@ in AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; - WorkingDirectory = mkIf (cfg.useACMEHost != null) "$CREDENTIALS_DIRECTORY"; LoadCredential = optionals (cfg.useACMEHost != null) [ "fullchain.pem:/var/lib/acme/${cfg.useACMEHost}/fullchain.pem" "key.pem:/var/lib/acme/${cfg.useACMEHost}/key.pem" @@ -124,7 +128,7 @@ in Type = "oneshot"; TimeoutSec = 60; ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active pomerium.service"; - ExecStart = "/run/current-system/systemd/bin/systemctl restart pomerium.service"; + ExecStart = "/run/current-system/systemd/bin/systemctl --no-block restart pomerium.service"; }; }; }); diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix index f9446fe125a..877097cf378 100644 --- a/nixos/modules/services/web-servers/tomcat.nix +++ b/nixos/modules/services/web-servers/tomcat.nix @@ -23,8 +23,8 @@ in package = mkOption { type = types.package; - default = pkgs.tomcat85; - defaultText = literalExpression "pkgs.tomcat85"; + default = pkgs.tomcat9; + defaultText = literalExpression "pkgs.tomcat9"; example = lib.literalExpression "pkgs.tomcat9"; description = '' Which tomcat package to use. @@ -127,7 +127,7 @@ in webapps = mkOption { type = types.listOf types.path; default = [ tomcat.webapps ]; - defaultText = literalExpression "[ pkgs.tomcat85.webapps ]"; + defaultText = literalExpression "[ config.services.tomcat.package.webapps ]"; description = "List containing WAR files or directories with WAR files which are web applications to be deployed on Tomcat"; }; @@ -201,6 +201,7 @@ in { uid = config.ids.uids.tomcat; description = "Tomcat user"; home = "/homeless-shelter"; + group = "tomcat"; extraGroups = cfg.extraGroups; }; diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl index a1653d451fe..07ee281feec 100644 --- a/nixos/modules/system/activation/switch-to-configuration.pl +++ b/nixos/modules/system/activation/switch-to-configuration.pl @@ -10,6 +10,8 @@ use Net::DBus; use Sys::Syslog qw(:standard :macros); use Cwd 'abs_path'; +## no critic(CodeLayout::ProhibitParensWithBuiltins) + my $out = "@out@"; my $curSystemd = abs_path("/run/current-system/sw/bin"); @@ -36,13 +38,13 @@ my $dryReloadByActivationFile = "/run/nixos/dry-activation-reload-list"; make_path("/run/nixos", { mode => oct(755) }); -my $action = shift @ARGV; +my $action = shift(@ARGV); if ("@localeArchive@" ne "") { $ENV{LOCALE_ARCHIVE} = "@localeArchive@"; } -if (!defined $action || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) { +if (!defined($action) || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) { print STDERR <<EOF; Usage: $0 [switch|boot|test] @@ -51,27 +53,27 @@ boot: make the configuration the boot default test: activate the configuration, but don\'t make it the boot default dry-activate: show what would be done if this configuration were activated EOF - exit 1; + exit(1); } $ENV{NIXOS_ACTION} = $action; # This is a NixOS installation if it has /etc/NIXOS or a proper # /etc/os-release. -die "This is not a NixOS installation!\n" unless +die("This is not a NixOS installation!\n") unless -f "/etc/NIXOS" || (read_file("/etc/os-release", err_mode => 'quiet') // "") =~ /ID="?nixos"?/s; openlog("nixos", "", LOG_USER); # Install or update the bootloader. if ($action eq "switch" || $action eq "boot") { - system("@installBootLoader@ $out") == 0 or exit 1; + system('@installBootLoader@', $out) == 0 or exit 1; } # Just in case the new configuration hangs the system, do a sync now. system("@coreutils@/bin/sync", "-f", "/nix/store") unless ($ENV{"NIXOS_NO_SYNC"} // "") eq "1"; -exit 0 if $action eq "boot"; +exit(0) if $action eq "boot"; # Check if we can activate the new configuration. my $oldVersion = read_file("/run/current-system/init-interface-version", err_mode => 'quiet') // ""; @@ -83,7 +85,7 @@ Warning: the new NixOS configuration has an ‘init’ that is incompatible with the current configuration. The new configuration won\'t take effect until you reboot the system. EOF - exit 100; + exit(100); } # Ignore SIGHUP so that we're not killed if we're running on (say) @@ -104,14 +106,27 @@ sub getActiveUnits { return $res; } +# Returns whether a systemd unit is active +sub unit_is_active { + my ($unit_name) = @_; + + my $mgr = Net::DBus->system->get_service('org.freedesktop.systemd1')->get_object('/org/freedesktop/systemd1'); + my $units = $mgr->ListUnitsByNames([$unit_name]); + if (scalar(@{$units}) == 0) { + return 0; + } + my $active_state = $units->[0]->[3]; ## no critic (ValuesAndExpressions::ProhibitMagicNumbers) + return $active_state eq 'active' || $active_state eq 'activating'; +} + sub parseFstab { my ($filename) = @_; my ($fss, $swaps); foreach my $line (read_file($filename, err_mode => 'quiet')) { - chomp $line; + chomp($line); $line =~ s/^\s*#.*//; next if $line =~ /^\s*$/; - my @xs = split / /, $line; + my @xs = split(/ /, $line); if ($xs[2] eq "swap") { $swaps->{$xs[0]} = { options => $xs[3] // "" }; } else { @@ -133,17 +148,16 @@ sub parseFstab { sub parseSystemdIni { my ($unitContents, $path) = @_; # Tie the ini file to a hash for easier access - my %fileContents; - tie %fileContents, "Config::IniFiles", (-file => $path, -allowempty => 1, -allowcontinue => 1); + tie(my %fileContents, 'Config::IniFiles', (-file => $path, -allowempty => 1, -allowcontinue => 1)); ## no critic(Miscellanea::ProhibitTies) # Copy over all sections - foreach my $sectionName (keys %fileContents) { + foreach my $sectionName (keys(%fileContents)) { if ($sectionName eq "Install") { # Skip the [Install] section because it has no relevant keys for us next; } # Copy over all keys - foreach my $iniKey (keys %{$fileContents{$sectionName}}) { + foreach my $iniKey (keys(%{$fileContents{$sectionName}})) { # Ensure the value is an array so it's easier to work with my $iniValue = $fileContents{$sectionName}{$iniKey}; my @iniValues; @@ -181,7 +195,7 @@ sub parse_unit { # Replace \ with \\ so glob() still works with units that have a \ in them # Valid characters in unit names are ASCII letters, digits, ":", "-", "_", ".", and "\" $unit_path =~ s/\\/\\\\/gmsx; - foreach (glob "${unit_path}{,.d/*.conf}") { + foreach (glob("${unit_path}{,.d/*.conf}")) { parseSystemdIni(\%unit_data, "$_") } return %unit_data; @@ -194,7 +208,7 @@ sub parseSystemdBool { my @values = @{$unitConfig->{$sectionName}{$boolName} // []}; # Return default if value is not set - if (scalar @values lt 1 || not defined $values[-1]) { + if (scalar(@values) lt 1 || not defined($values[-1])) { return $default; } # If value is defined multiple times, use the last definition @@ -211,7 +225,7 @@ sub recordUnit { # The opposite of recordUnit, removes a unit name from a file sub unrecord_unit { my ($fn, $unit) = @_; - edit_file { s/^$unit\n//msx } $fn if $action ne "dry-activate"; + edit_file(sub { s/^$unit\n//msx }, $fn) if $action ne "dry-activate"; } # Compare the contents of two unit files and return whether the unit @@ -226,6 +240,16 @@ sub unrecord_unit { sub compare_units { my ($old_unit, $new_unit) = @_; my $ret = 0; + # Keys to ignore in the [Unit] section + my %unit_section_ignores = map { $_ => 1 } qw( + X-Reload-Triggers + Description Documentation + OnFailure OnSuccess OnFailureJobMode + IgnoreOnIsolate StopWhenUnneeded + RefuseManualStart RefuseManualStop + AllowIsolate CollectMode + SourcePath + ); my $comp_array = sub { my ($a, $b) = @_; @@ -233,11 +257,23 @@ sub compare_units { }; # Comparison hash for the sections - my %section_cmp = map { $_ => 1 } keys %{$new_unit}; + my %section_cmp = map { $_ => 1 } keys(%{$new_unit}); # Iterate over the sections - foreach my $section_name (keys %{$old_unit}) { + foreach my $section_name (keys(%{$old_unit})) { # Missing section in the new unit? - if (not exists $section_cmp{$section_name}) { + if (not exists($section_cmp{$section_name})) { + # If the [Unit] section was removed, make sure that only keys + # were in it that are ignored + if ($section_name eq 'Unit') { + foreach my $ini_key (keys(%{$old_unit->{'Unit'}})) { + if (not defined($unit_section_ignores{$ini_key})) { + return 1; + } + } + next; # check the next section + } else { + return 1; + } if ($section_name eq 'Unit' and %{$old_unit->{'Unit'}} == 1 and defined(%{$old_unit->{'Unit'}}{'X-Reload-Triggers'})) { # If a new [Unit] section was removed that only contained X-Reload-Triggers, # do nothing. @@ -248,15 +284,15 @@ sub compare_units { } delete $section_cmp{$section_name}; # Comparison hash for the section contents - my %ini_cmp = map { $_ => 1 } keys %{$new_unit->{$section_name}}; + my %ini_cmp = map { $_ => 1 } keys(%{$new_unit->{$section_name}}); # Iterate over the keys of the section - foreach my $ini_key (keys %{$old_unit->{$section_name}}) { + foreach my $ini_key (keys(%{$old_unit->{$section_name}})) { delete $ini_cmp{$ini_key}; my @old_value = @{$old_unit->{$section_name}{$ini_key}}; # If the key is missing in the new unit, they are different... if (not $new_unit->{$section_name}{$ini_key}) { - # ... unless the key that is now missing was the reload trigger - if ($section_name eq 'Unit' and $ini_key eq 'X-Reload-Triggers') { + # ... unless the key that is now missing is one of the ignored keys + if ($section_name eq 'Unit' and defined($unit_section_ignores{$ini_key})) { next; } return 1; @@ -264,19 +300,30 @@ sub compare_units { my @new_value = @{$new_unit->{$section_name}{$ini_key}}; # If the contents are different, the units are different if (not $comp_array->(\@old_value, \@new_value)) { - # Check if only the reload triggers changed - if ($section_name eq 'Unit' and $ini_key eq 'X-Reload-Triggers') { - $ret = 2; - } else { - return 1; + # Check if only the reload triggers changed or one of the ignored keys + if ($section_name eq 'Unit') { + if ($ini_key eq 'X-Reload-Triggers') { + $ret = 2; + next; + } elsif (defined($unit_section_ignores{$ini_key})) { + next; + } } + return 1; } } # A key was introduced that was missing in the old unit if (%ini_cmp) { - if ($section_name eq 'Unit' and %ini_cmp == 1 and defined($ini_cmp{'X-Reload-Triggers'})) { - # If the newly introduced key was the reload triggers, reload the unit - $ret = 2; + if ($section_name eq 'Unit') { + foreach my $ini_key (keys(%ini_cmp)) { + if ($ini_key eq 'X-Reload-Triggers') { + $ret = 2; + } elsif (defined($unit_section_ignores{$ini_key})) { + next; + } else { + return 1; + } + } } else { return 1; } @@ -284,10 +331,14 @@ sub compare_units { } # A section was introduced that was missing in the old unit if (%section_cmp) { - if (%section_cmp == 1 and defined($section_cmp{'Unit'}) and %{$new_unit->{'Unit'}} == 1 and defined(%{$new_unit->{'Unit'}}{'X-Reload-Triggers'})) { - # If a new [Unit] section was introduced that only contains X-Reload-Triggers, - # reload instead of restarting - $ret = 2; + if (%section_cmp == 1 and defined($section_cmp{'Unit'})) { + foreach my $ini_key (keys(%{$new_unit->{'Unit'}})) { + if (not defined($unit_section_ignores{$ini_key})) { + return 1; + } elsif ($ini_key eq 'X-Reload-Triggers') { + $ret = 2; + } + } } else { return 1; } @@ -343,11 +394,11 @@ sub handleModifiedUnit { my $socket_activated = 0; if ($unit =~ /\.service$/) { my @sockets = split(/ /, join(" ", @{$unitInfo{Service}{Sockets} // []})); - if (scalar @sockets == 0) { + if (scalar(@sockets) == 0) { @sockets = ("$baseName.socket"); } foreach my $socket (@sockets) { - if (defined $activePrev->{$socket}) { + if (defined($activePrev->{$socket})) { # We can now be sure this is a socket-activate unit $unitsToStop->{$socket} = 1; @@ -355,7 +406,11 @@ sub handleModifiedUnit { # exist in new configuration: if (-e "$out/etc/systemd/system/$socket") { $unitsToStart->{$socket} = 1; - recordUnit($startListFile, $socket); + if ($unitsToStart eq $unitsToRestart) { + recordUnit($restartListFile, $socket); + } else { + recordUnit($startListFile, $socket); + } $socket_activated = 1; } # Remove from units to reload so we don't restart and reload @@ -373,7 +428,11 @@ sub handleModifiedUnit { # service gets restarted if we're interrupted. if (!$socket_activated) { $unitsToStart->{$unit} = 1; - recordUnit($startListFile, $unit); + if ($unitsToStart eq $unitsToRestart) { + recordUnit($restartListFile, $unit); + } else { + recordUnit($startListFile, $unit); + } } $unitsToStop->{$unit} = 1; @@ -401,8 +460,8 @@ $unitsToRestart{$_} = 1 foreach $unitsToReload{$_} = 1 foreach split('\n', read_file($reloadListFile, err_mode => 'quiet') // ""); -my $activePrev = getActiveUnits; -while (my ($unit, $state) = each %{$activePrev}) { +my $activePrev = getActiveUnits(); +while (my ($unit, $state) = each(%{$activePrev})) { my $baseUnit = $unit; my $prevUnitFile = "/etc/systemd/system/$baseUnit"; @@ -462,9 +521,9 @@ while (my ($unit, $state) = each %{$activePrev}) { my %old_unit_info = parse_unit($prevUnitFile); my %new_unit_info = parse_unit($newUnitFile); my $diff = compare_units(\%old_unit_info, \%new_unit_info); - if ($diff eq 1) { + if ($diff == 1) { handleModifiedUnit($unit, $baseName, $newUnitFile, \%new_unit_info, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip); - } elsif ($diff eq 2 and not $unitsToRestart{$unit}) { + } elsif ($diff == 2 and not $unitsToRestart{$unit}) { $unitsToReload{$unit} = 1; recordUnit($reloadListFile, $unit); } @@ -475,11 +534,11 @@ while (my ($unit, $state) = each %{$activePrev}) { sub pathToUnitName { my ($path) = @_; # Use current version of systemctl binary before daemon is reexeced. - open my $cmd, "-|", "$curSystemd/systemd-escape", "--suffix=mount", "-p", $path + open(my $cmd, "-|", "$curSystemd/systemd-escape", "--suffix=mount", "-p", $path) or die "Unable to escape $path!\n"; - my $escaped = join "", <$cmd>; - chomp $escaped; - close $cmd or die; + my $escaped = join("", <$cmd>); + chomp($escaped); + close($cmd) or die('Unable to close systemd-escape pipe'); return $escaped; } @@ -488,13 +547,13 @@ sub pathToUnitName { # automatically by starting local-fs.target. FIXME: might be nicer if # we generated units for all mounts; then we could unify this with the # unit checking code above. -my ($prevFss, $prevSwaps) = parseFstab "/etc/fstab"; -my ($newFss, $newSwaps) = parseFstab "$out/etc/fstab"; -foreach my $mountPoint (keys %$prevFss) { +my ($prevFss, $prevSwaps) = parseFstab("/etc/fstab"); +my ($newFss, $newSwaps) = parseFstab("$out/etc/fstab"); +foreach my $mountPoint (keys(%$prevFss)) { my $prev = $prevFss->{$mountPoint}; my $new = $newFss->{$mountPoint}; my $unit = pathToUnitName($mountPoint); - if (!defined $new) { + if (!defined($new)) { # Filesystem entry disappeared, so unmount it. $unitsToStop{$unit} = 1; } elsif ($prev->{fsType} ne $new->{fsType} || $prev->{device} ne $new->{device}) { @@ -510,10 +569,10 @@ foreach my $mountPoint (keys %$prevFss) { } # Also handles swap devices. -foreach my $device (keys %$prevSwaps) { +foreach my $device (keys(%$prevSwaps)) { my $prev = $prevSwaps->{$device}; my $new = $newSwaps->{$device}; - if (!defined $new) { + if (!defined($new)) { # Swap entry disappeared, so turn it off. Can't use # "systemctl stop" here because systemd has lots of alias # units that prevent a stop from actually calling @@ -544,8 +603,8 @@ if ($prevSystemdSystemConfig ne $newSystemdSystemConfig) { sub filterUnits { my ($units) = @_; my @res; - foreach my $unit (sort(keys %{$units})) { - push @res, $unit if !defined $unitsToFilter{$unit}; + foreach my $unit (sort(keys(%{$units}))) { + push(@res, $unit) if !defined($unitsToFilter{$unit}); } return @res; } @@ -556,9 +615,9 @@ my @unitsToStopFiltered = filterUnits(\%unitsToStop); # Show dry-run actions. if ($action eq "dry-activate") { print STDERR "would stop the following units: ", join(", ", @unitsToStopFiltered), "\n" - if scalar @unitsToStopFiltered > 0; - print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n" - if scalar(keys %unitsToSkip) > 0; + if scalar(@unitsToStopFiltered) > 0; + print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys(%unitsToSkip))), "\n" + if scalar(keys(%unitsToSkip)) > 0; print STDERR "would activate the configuration...\n"; system("$out/dry-activate", "$out"); @@ -579,7 +638,7 @@ if ($action eq "dry-activate") { $baseName =~ s/\.[a-z]*$//; # Start units if they were not active previously - if (not defined $activePrev->{$unit}) { + if (not defined($activePrev->{$unit})) { $unitsToStart{$unit} = 1; next; } @@ -599,28 +658,28 @@ if ($action eq "dry-activate") { unlink($dryReloadByActivationFile); print STDERR "would restart systemd\n" if $restartSystemd; - print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n" - if scalar(keys %unitsToReload) > 0; - print STDERR "would restart the following units: ", join(", ", sort(keys %unitsToRestart)), "\n" - if scalar(keys %unitsToRestart) > 0; + print STDERR "would reload the following units: ", join(", ", sort(keys(%unitsToReload))), "\n" + if scalar(keys(%unitsToReload)) > 0; + print STDERR "would restart the following units: ", join(", ", sort(keys(%unitsToRestart))), "\n" + if scalar(keys(%unitsToRestart)) > 0; my @unitsToStartFiltered = filterUnits(\%unitsToStart); print STDERR "would start the following units: ", join(", ", @unitsToStartFiltered), "\n" - if scalar @unitsToStartFiltered; + if scalar(@unitsToStartFiltered); exit 0; } syslog(LOG_NOTICE, "switching to system configuration $out"); -if (scalar (keys %unitsToStop) > 0) { +if (scalar(keys(%unitsToStop)) > 0) { print STDERR "stopping the following units: ", join(", ", @unitsToStopFiltered), "\n" - if scalar @unitsToStopFiltered; + if scalar(@unitsToStopFiltered); # Use current version of systemctl binary before daemon is reexeced. - system("$curSystemd/systemctl", "stop", "--", sort(keys %unitsToStop)); + system("$curSystemd/systemctl", "stop", "--", sort(keys(%unitsToStop))); } -print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n" - if scalar(keys %unitsToSkip) > 0; +print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys(%unitsToSkip))), "\n" + if scalar(keys(%unitsToSkip)) > 0; # Activate the new configuration (i.e., update /etc, make accounts, # and so on). @@ -644,7 +703,7 @@ foreach (split('\n', read_file($restartByActivationFile, err_mode => 'quiet') // $baseName =~ s/\.[a-z]*$//; # Start units if they were not active previously - if (not defined $activePrev->{$unit}) { + if (not defined($activePrev->{$unit})) { $unitsToStart{$unit} = 1; recordUnit($startListFile, $unit); next; @@ -681,7 +740,7 @@ system("@systemd@/bin/systemctl", "reset-failed"); system("@systemd@/bin/systemctl", "daemon-reload") == 0 or $res = 3; # Reload user units -open my $listActiveUsers, '-|', '@systemd@/bin/loginctl', 'list-users', '--no-legend'; +open(my $listActiveUsers, '-|', '@systemd@/bin/loginctl', 'list-users', '--no-legend'); while (my $f = <$listActiveUsers>) { next unless $f =~ /^\s*(?<uid>\d+)\s+(?<user>\S+)/; my ($uid, $name) = ($+{uid}, $+{user}); @@ -693,25 +752,43 @@ while (my $f = <$listActiveUsers>) { "@systemd@/bin/systemctl --user start nixos-activation.service"); } -close $listActiveUsers; +close($listActiveUsers); # Set the new tmpfiles print STDERR "setting up tmpfiles\n"; system("@systemd@/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3; +# Before reloading we need to ensure that the units are still active. They may have been +# deactivated because one of their requirements got stopped. If they are inactive +# but should have been reloaded, the user probably expects them to be started. +if (scalar(keys(%unitsToReload)) > 0) { + for my $unit (keys(%unitsToReload)) { + if (!unit_is_active($unit)) { + # Figure out if we need to start the unit + my %unit_info = parse_unit("$out/etc/systemd/system/$unit"); + if (!(parseSystemdBool(\%unit_info, 'Unit', 'RefuseManualStart', 0) || parseSystemdBool(\%unit_info, 'Unit', 'X-OnlyManualStart', 0))) { + $unitsToStart{$unit} = 1; + recordUnit($startListFile, $unit); + } + # Don't reload the unit, reloading would fail + delete %unitsToReload{$unit}; + unrecord_unit($reloadListFile, $unit); + } + } +} # Reload units that need it. This includes remounting changed mount # units. -if (scalar(keys %unitsToReload) > 0) { - print STDERR "reloading the following units: ", join(", ", sort(keys %unitsToReload)), "\n"; - system("@systemd@/bin/systemctl", "reload", "--", sort(keys %unitsToReload)) == 0 or $res = 4; +if (scalar(keys(%unitsToReload)) > 0) { + print STDERR "reloading the following units: ", join(", ", sort(keys(%unitsToReload))), "\n"; + system("@systemd@/bin/systemctl", "reload", "--", sort(keys(%unitsToReload))) == 0 or $res = 4; unlink($reloadListFile); } # Restart changed services (those that have to be restarted rather # than stopped and started). -if (scalar(keys %unitsToRestart) > 0) { - print STDERR "restarting the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"; - system("@systemd@/bin/systemctl", "restart", "--", sort(keys %unitsToRestart)) == 0 or $res = 4; +if (scalar(keys(%unitsToRestart)) > 0) { + print STDERR "restarting the following units: ", join(", ", sort(keys(%unitsToRestart))), "\n"; + system("@systemd@/bin/systemctl", "restart", "--", sort(keys(%unitsToRestart))) == 0 or $res = 4; unlink($restartListFile); } @@ -723,17 +800,17 @@ if (scalar(keys %unitsToRestart) > 0) { # systemd. my @unitsToStartFiltered = filterUnits(\%unitsToStart); print STDERR "starting the following units: ", join(", ", @unitsToStartFiltered), "\n" - if scalar @unitsToStartFiltered; -system("@systemd@/bin/systemctl", "start", "--", sort(keys %unitsToStart)) == 0 or $res = 4; + if scalar(@unitsToStartFiltered); +system("@systemd@/bin/systemctl", "start", "--", sort(keys(%unitsToStart))) == 0 or $res = 4; unlink($startListFile); # Print failed and new units. my (@failed, @new); -my $activeNew = getActiveUnits; -while (my ($unit, $state) = each %{$activeNew}) { +my $activeNew = getActiveUnits(); +while (my ($unit, $state) = each(%{$activeNew})) { if ($state->{state} eq "failed") { - push @failed, $unit; + push(@failed, $unit); next; } @@ -743,7 +820,7 @@ while (my ($unit, $state) = each %{$activeNew}) { chomp($main_status); if ($main_status ne "0") { - push @failed, $unit; + push(@failed, $unit); next; } } @@ -751,19 +828,19 @@ while (my ($unit, $state) = each %{$activeNew}) { # Ignore scopes since they are not managed by this script but rather # created and managed by third-party services via the systemd dbus API. # This only lists units that are not failed (including ones that are in auto-restart but have not failed previously) - if ($state->{state} ne "failed" && !defined $activePrev->{$unit} && $unit !~ /\.scope$/msx) { - push @new, $unit; + if ($state->{state} ne "failed" && !defined($activePrev->{$unit}) && $unit !~ /\.scope$/msx) { + push(@new, $unit); } } -if (scalar @new > 0) { +if (scalar(@new) > 0) { print STDERR "the following new units were started: ", join(", ", sort(@new)), "\n" } -if (scalar @failed > 0) { - my @failed_sorted = sort @failed; +if (scalar(@failed) > 0) { + my @failed_sorted = sort(@failed); print STDERR "warning: the following units failed: ", join(", ", @failed_sorted), "\n\n"; - system "@systemd@/bin/systemctl status --no-pager --full '" . join("' '", @failed_sorted) . "' >&2"; + system("@systemd@/bin/systemctl status --no-pager --full '" . join("' '", @failed_sorted) . "' >&2"); $res = 4; } @@ -773,4 +850,4 @@ if ($res == 0) { syslog(LOG_ERR, "switching to system configuration $out failed (status $res)"); } -exit $res; +exit($res); diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py index adc89306309..fa879437fd8 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py @@ -15,9 +15,12 @@ import re import datetime import glob import os.path -from typing import Tuple, List, Optional +from typing import NamedTuple, List, Optional -SystemIdentifier = Tuple[Optional[str], int, Optional[str]] +class SystemIdentifier(NamedTuple): + profile: Optional[str] + generation: int + specialisation: Optional[str] def copy_if_not_exists(source: str, dest: str) -> None: @@ -151,7 +154,14 @@ def get_generations(profile: Optional[str] = None) -> List[SystemIdentifier]: gen_lines.pop() configurationLimit = @configurationLimit@ - configurations: List[SystemIdentifier] = [ (profile, int(line.split()[0]), None) for line in gen_lines ] + configurations = [ + SystemIdentifier( + profile=profile, + generation=int(line.split()[0]), + specialisation=None + ) + for line in gen_lines + ] return configurations[-configurationLimit:] @@ -160,7 +170,7 @@ def get_specialisations(profile: Optional[str], generation: int, _: Optional[str system_dir(profile, generation, None), "specialisation") if not os.path.exists(specialisations_dir): return [] - return [(profile, generation, spec) for spec in os.listdir(specialisations_dir)] + return [SystemIdentifier(profile, generation, spec) for spec in os.listdir(specialisations_dir)] def remove_old_entries(gens: List[SystemIdentifier]) -> None: @@ -271,7 +281,8 @@ def main() -> None: if os.readlink(system_dir(*gen)) == args.default_config: write_loader_conf(*gen) except OSError as e: - print("ignoring generation '{}' in the list of boot entries because of the following error:\n{}".format(*gen, e), file=sys.stderr) + profile = f"profile '{gen.profile}'" if gen.profile else "default profile" + print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr) for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False): relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/") diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix index 5af9baff8bc..f4048172783 100644 --- a/nixos/modules/virtualisation/oci-containers.nix +++ b/nixos/modules/virtualisation/oci-containers.nix @@ -22,11 +22,13 @@ let type = with types; nullOr package; default = null; description = '' - Path to an image file to load instead of pulling from a registry. - If defined, do not pull from registry. + Path to an image file to load before running the image. This can + be used to bypass pulling the image from the registry. - You still need to set the <literal>image</literal> attribute, as it - will be used as the image name for docker to start a container. + The <literal>image</literal> attribute must match the name and + tag of the image contained in this file, as they will be used to + run the container with that image. If they do not match, the + image will be pulled from the registry as usual. ''; example = literalExpression "pkgs.dockerTools.buildImage {...};"; }; |