From 785f9e04b123b8fd3b5a6fd0901f4ac5177fd752 Mon Sep 17 00:00:00 2001 From: Malte Voos Date: Thu, 12 May 2022 17:39:22 +0200 Subject: nixos module --- flake.nix | 5 + module.nix | 371 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 376 insertions(+) create mode 100644 flake.nix create mode 100644 module.nix diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..8c4dc5c --- /dev/null +++ b/flake.nix @@ -0,0 +1,5 @@ +{ + outputs = { ... }: { + nixosModule = import ./module.nix; + }; +} diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..bd607c8 --- /dev/null +++ b/module.nix @@ -0,0 +1,371 @@ +{ config, lib, pkgs, ... }: +with lib; +let + writeExeclineScript = name: text: pkgs.writeScript name '' + #!${pkgs.execline}/bin/execlineb -P + ${text} + ''; + + cfg = config.s6; + + # -------- configuration for s6-rc -------- # + + loggerNames = flatten (mapAttrsToList (_: srv: optional (srv.logger != null) srv.logger) cfg.longruns); + + loggingRelations = genAttrs loggerNames + (loggerName: filter + (serviceName: cfg.longruns.${serviceName}.logger == loggerName) + (attrNames cfg.longruns)); + + loggerLongruns = mapAttrs' + (loggerName: producers: + nameValuePair "${loggerName}-log" { + run = '' + s6-setuidgid ${cfg.logUser} + exec -c + s6-log -d 3 -- ${cfg.loggingScript loggerName} + ''; + notificationFd = 3; + consumerFor = producers; + pipelineName = "${loggerName}-pipeline"; + flagEssential = false; + timeoutUp = null; + timeoutDown = null; + dependencies = null; + finish = null; + timeoutKill = null; + timeoutFinish = null; + maxDeathTally = null; + downSignal = null; + producerFor = null; + logger = null; + }) + loggingRelations; + + allLongruns = cfg.longruns // loggerLongruns; + + makeCommonConfig = srv: '' + mkdir -p $out + '' + optionalString srv.flagEssential '' + touch $out/flagEssential + ''; + + makeBundleConfig = srv: makeCommonConfig srv + '' + echo bundle > $out/type + mkdir $out/contents.d + '' + concatMapStringsSep "\n" + (srvName: ''touch "$out/contents.d/${srvName}"'') + srv.contents + "\n"; + + makeAtomicConfig = srv: makeCommonConfig srv + + optionalString (srv.timeoutUp != null) '' + echo "${toString srv.timeoutUp}" > $out/timeout-up + '' + optionalString (srv.timeoutDown != null) '' + echo "${toString srv.timeoutDown}" > $out/timeout-down + '' + optionalString (srv.dependencies != null) ( + '' + mkdir $out/dependencies.d + '' + concatMapStringsSep "\n" + (srvName: ''touch "$out/dependencies.d/${srvName}"'') + srv.dependencies + "\n" + ); + + makeOneshotConfig = srv: + let + upScript = pkgs.writeText "up-script" srv.up; + downScript = mapNullable (pkgs.writeText "down-script") srv.down; + in + makeAtomicConfig srv + '' + echo oneshot > $out/type + cp ${upScript} $out/up + '' + optionalString (downScript != null) '' + cp ${downScript} $out/down + ''; + + makeLongrunConfig = srv: + let + runScript = writeExeclineScript "run-script" srv.run; + finishScript = mapNullable (writeExeclineScript "finish-script") srv.finish; + in + makeAtomicConfig srv + '' + echo longrun > $out/type + cp ${runScript} $out/run + '' + optionalString (finishScript != null) '' + cp ${finishScript} $out/finish + '' + optionalString (srv.notificationFd != null) '' + echo "${toString srv.notificationFd}" > $out/notification-fd + '' + optionalString (srv.timeoutKill != null) '' + echo "${toString srv.timeoutKill}" > $out/timeout-kill + '' + optionalString (srv.timeoutFinish != null) '' + echo "${toString srv.timeoutFinish}" > $out/timeout-finish + '' + optionalString (srv.maxDeathTally != null) '' + echo "${toString srv.maxDeathTally}" > $out/max-death-tally + '' + optionalString (srv.downSignal != null) '' + echo "${srv.downSignal}" > $out/down-signal + '' + optionalString (srv.producerFor != null) '' + echo "${srv.producerFor}" > $out/producer-for + '' + optionalString (srv.consumerFor != null) '' + echo "${concatStringsSep "\n" srv.consumerFor}" > $out/consumer-for + '' + optionalString (srv.pipelineName != null) '' + echo "${srv.pipelineName}" > $out/pipeline-name + '' + optionalString (srv.logger != null) '' + echo "${srv.logger}-log" > $out/producer-for + ''; + + makeBundleDefinitionDir = name: srv: pkgs.runCommand + "s6-rc-bundle-definition-${name}" + { } + (makeBundleConfig srv); + makeOneshotDefinitionDir = name: srv: pkgs.runCommand + "s6-rc-oneshot-definition-${name}" + { } + (makeOneshotConfig srv); + makeLongrunDefinitionDir = name: srv: pkgs.runCommand + "s6-rc-longrun-definition-${name}" + { } + (makeLongrunConfig srv); + + bundleDefinitionDirs = mapAttrs makeBundleDefinitionDir cfg.bundles; + oneshotDefinitionDirs = mapAttrs makeOneshotDefinitionDir cfg.oneshots; + longrunDefinitionDirs = mapAttrs makeLongrunDefinitionDir allLongruns; + + allDefinitionDirs = bundleDefinitionDirs // oneshotDefinitionDirs // longrunDefinitionDirs; + + serviceSourceDir = pkgs.linkFarm "s6-rc-service-source" + (mapAttrsToList + (srvName: definitionDir: { name = srvName; path = definitionDir; }) + allDefinitionDirs); + + compiledDatabase = pkgs.runCommand "s6-rc-compiled-database" { } '' + ${pkgs.s6-rc}/bin/s6-rc-compile $out ${serviceSourceDir} + ''; + + # -------- init script -------- # + + initialPath = lib.makeBinPath (with pkgs; [ + coreutils + execline + s6 + s6-rc + ]); + + catchAllLoggerServiceDir = + let + runScript = writeExeclineScript "catch-all-logger-run-script" '' + s6-setuidgid "${cfg.logUser}" + redirfd -w 1 /dev/null + redirfd -rnb 0 "${cfg.logFifo}" + s6-log -bpd3 -- ${cfg.loggingScript "uncaught"} + ''; + in + pkgs.runCommand "catch-all-logger-srvdir" { } '' + mkdir -p $out + cp ${runScript} $out/run + echo 3 > $out/notification-fd + ''; + + # this runs after NixOS' stage-2-init.sh + initScript = writeExeclineScript "init-script" '' + ${pkgs.execline}/bin/export PATH "${initialPath}" + ${pkgs.execline}/bin/exec + + # stage-2-init.sh prints the line "starting systemd..." + foreground { echo "...NOT! starting s6-svscan on ${cfg.scanDir}..." } + + if { mkdir -p "${cfg.scanDir}" } + if { cp -r "${catchAllLoggerServiceDir}" "${cfg.scanDir}/catch-all-log" } + + if { mkdir -p "${cfg.logDir}" } + if { chown "${cfg.logUser}:${cfg.logGroup}" "${cfg.logDir}" } + if { chmod 700 "${cfg.logDir}" } + + if { mkfifo "${cfg.logFifo}" } + if { chown "${cfg.logUser}:${cfg.logGroup}" "${cfg.logFifo}" } + if { chmod 600 "${cfg.logFifo}" } + + redirfd -r 0 /dev/null # don't use /dev/console as stdin + redirfd -wnb 1 "${cfg.logFifo}" # redirect stdout to fifo + fdmove -c 2 1 # redirect stderr to fifo + + background { + redirfd -w 3 "${cfg.logFifo}" # this blocks until the catch-all logger is running + if { s6-rc-init -c "${compiledDatabase}" "${cfg.scanDir}" } + s6-rc -v2 change "${cfg.allBundle}" + } + + exec -a s6-svscan # name it 's6-svscan' instead of /nix/store/... + s6-svscan "${cfg.scanDir}" + ''; +in +{ + options = with types; + let + mkNullableOption = { type, ... } @ o: + mkOption o // { type = nullOr type; default = null; }; + + # see: https://skarnet.org/software/s6-rc/s6-rc-compile.html#source + + commonOptions = { + flagEssential = mkOption { + type = bool; + default = false; + }; + }; + + bundleOptions = { + contents = mkOption { + type = listOf str; + }; + } // commonOptions; + + atomicOptions = { + timeoutUp = mkNullableOption { + type = ints.unsigned; + }; + timeoutDown = mkNullableOption { + type = ints.unsigned; + }; + dependencies = mkNullableOption { + type = listOf str; + }; + } // commonOptions; + + oneshotOptions = { + up = mkOption { + type = str; + }; + down = mkNullableOption { + type = str; + }; + } // atomicOptions; + + longrunOptions = { + run = mkOption { + type = str; + }; + finish = mkNullableOption { + type = str; + }; + notificationFd = mkNullableOption { + type = ints.unsigned; + }; + timeoutKill = mkNullableOption { + type = ints.unsigned; + }; + timeoutFinish = mkNullableOption { + type = ints.unsigned; + }; + maxDeathTally = mkNullableOption { + type = ints.between 0 4096; + }; + downSignal = mkNullableOption { + type = str; + }; + producerFor = mkNullableOption { + type = str; + }; + consumerFor = mkNullableOption { + type = listOf str; + }; + pipelineName = mkNullableOption { + type = str; + }; + # this is not an actual option in s6-rc; it creates a logger service + # with the name "-log" and adds the necessary "producer-for" + # and "consumer-for" files. + logger = mkNullableOption { + type = str; + }; + } // atomicOptions; + in + { + s6 = { + enable = mkEnableOption "s6 init system"; + allBundle = mkOption { + type = str; + }; + logUser = mkOption { + type = str; + default = "log"; + }; + logGroup = mkOption { + type = str; + default = "log"; + }; + logDir = mkOption { + type = path; + default = "/log"; + }; + logFifo = mkOption { + type = path; + default = "/run/log-fifo"; + }; + loggingScript = mkOption { + type = anything; + default = name: "T s1000000 n20 ${cfg.logDir}/${name}"; + }; + scanDir = mkOption { + type = path; + default = "/run/service"; + }; + + bundles = mkOption { + type = with types; attrsOf (submodule { + options = bundleOptions; + }); + }; + + oneshots = mkOption { + type = with types; attrsOf (submodule { + options = oneshotOptions; + }); + default = { }; + }; + + longruns = mkOption { + type = with types; attrsOf (submodule { + options = longrunOptions; + }); + default = { }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = hasAttr cfg.allBundle cfg.bundles; + message = "the bundle '${cfg.allBundle}' does not exist!"; + } + { + assertion = !(elem "uncaught" loggerNames); + message = "the logger name 'uncaught' is reserved"; + } + { + assertion = length (attrNames allDefinitionDirs) + == length (attrNames cfg.bundles) + + length (attrNames cfg.oneshots) + + length (attrNames cfg.longruns) + + length (attrNames loggerLongruns); + message = "no two services can have the same name, even if they are of different types"; + } + ] ++ mapAttrsToList + (name: def: { + assertion = def.producerFor == null || def.logger == null; + message = "in s6.longruns.${name}: 'producerFor' and 'logger' are mutually exclusive"; + }) + cfg.longruns; + + users = { + users.${cfg.logUser} = { + isSystemUser = true; + group = cfg.logGroup; + home = cfg.logDir; + }; + groups.${cfg.logGroup} = { }; + }; + + # checkmate! + boot.systemdExecutable = "${initScript}"; + }; +} -- cgit 1.4.1