summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/mail/postfix.nix6
-rw-r--r--nixos/modules/system/boot/binfmt.nix139
-rw-r--r--nixos/tests/home-assistant.nix36
4 files changed, 178 insertions, 4 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 35f5e87d7e5e..01bd2960517b 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -694,6 +694,7 @@
   ./services/x11/xserver.nix
   ./system/activation/activation-script.nix
   ./system/activation/top-level.nix
+  ./system/boot/binfmt.nix
   ./system/boot/coredump.nix
   ./system/boot/emergency-mode.nix
   ./system/boot/grow-partition.nix
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index 22af7e876af2..5ab331ac067f 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -414,7 +414,10 @@ in
       postmasterAlias = mkOption {
         type = types.str;
         default = "root";
-        description = "Who should receive postmaster e-mail.";
+        description = "
+          Who should receive postmaster e-mail. Multiple values can be added by
+          separating values with comma.
+        ";
       };
 
       rootAlias = mkOption {
@@ -422,6 +425,7 @@ in
         default = "";
         description = "
           Who should receive root e-mail. Blank for no redirection.
+          Multiple values can be added by separating values with comma.
         ";
       };
 
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
new file mode 100644
index 000000000000..15e84dc021e2
--- /dev/null
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -0,0 +1,139 @@
+{ config, lib, ... }:
+let
+  inherit (lib) mkOption types optionalString;
+
+  cfg = config.boot.binfmtMiscRegistrations;
+
+  makeBinfmtLine = name: { recognitionType, offset, magicOrExtension
+                         , mask, preserveArgvZero, openBinary
+                         , matchCredentials, fixBinary, ...
+                         }: let
+    type = if recognitionType == "magic" then "M" else "E";
+    offset' = toString offset;
+    mask' = toString mask;
+    interpreter = "/run/binfmt/${name}";
+    flags = if !(matchCredentials -> openBinary)
+              then throw "boot.binfmtMiscRegistrations.${name}: you can't specify openBinary = false when matchCredentials = true."
+            else optionalString preserveArgvZero "P" +
+                 optionalString (openBinary && !matchCredentials) "O" +
+                 optionalString matchCredentials "C" +
+                 optionalString fixBinary "F";
+  in ":${name}:${type}:${offset'}:${magicOrExtension}:${mask'}:${interpreter}:${flags}";
+
+  binfmtFile = builtins.toFile "binfmt_nixos.conf"
+    (lib.concatStringsSep "\n" (lib.mapAttrsToList makeBinfmtLine cfg));
+
+  activationSnippet = name: { interpreter, ... }:
+    "ln -sf ${interpreter} /run/binfmt/${name}";
+  activationScript = ''
+    mkdir -p -m 0755 /run/binfmt
+    ${lib.concatStringsSep "\n" (lib.mapAttrsToList activationSnippet cfg)}
+  '';
+in {
+  options = {
+    boot.binfmtMiscRegistrations = mkOption {
+      default = {};
+
+      description = ''
+        Extra binary formats to register with the kernel.
+        See https://www.kernel.org/doc/html/latest/admin-guide/binfmt-misc.html for more details.
+      '';
+
+      type = types.attrsOf (types.submodule ({ config, ... }: {
+        options = {
+          recognitionType = mkOption {
+            default = "magic";
+            description = "Whether to recognize executables by magic number or extension.";
+            type = types.enum [ "magic" "extension" ];
+          };
+
+          offset = mkOption {
+            default = null;
+            description = "The byte offset of the magic number used for recognition.";
+            type = types.nullOr types.int;
+          };
+
+          magicOrExtension = mkOption {
+            description = "The magic number or extension to match on.";
+            type = types.str;
+          };
+
+          mask = mkOption {
+            default = null;
+            description =
+              "A mask to be ANDed with the byte sequence of the file before matching";
+            type = types.nullOr types.str;
+          };
+
+          interpreter = mkOption {
+            description = ''
+              The interpreter to invoke to run the program.
+
+              Note that the actual registration will point to
+              /run/binfmt/''${name}, so the kernel interpreter length
+              limit doesn't apply.
+            '';
+            type = types.path;
+          };
+
+          preserveArgvZero = mkOption {
+            default = false;
+            description = ''
+              Whether to pass the original argv[0] to the interpreter.
+
+              See the description of the 'P' flag in the kernel docs
+              for more details;
+            '';
+            type = types.bool;
+          };
+
+          openBinary = mkOption {
+            default = config.matchCredentials;
+            description = ''
+              Whether to pass the binary to the interpreter as an open
+              file descriptor, instead of a path.
+            '';
+            type = types.bool;
+          };
+
+          matchCredentials = mkOption {
+            default = false;
+            description = ''
+              Whether to launch with the credentials and security
+              token of the binary, not the interpreter (e.g. setuid
+              bit).
+
+              See the description of the 'C' flag in the kernel docs
+              for more details.
+
+              Implies/requires openBinary = true.
+            '';
+            type = types.bool;
+          };
+
+          fixBinary = mkOption {
+            default = false;
+            description = ''
+              Whether to open the interpreter file as soon as the
+              registration is loaded, rather than waiting for a
+              relevant file to be invoked.
+
+              See the description of the 'F' flag in the kernel docs
+              for more details.
+            '';
+            type = types.bool;
+          };
+        };
+      }));
+    };
+  };
+
+  config = lib.mkIf (cfg != {}) {
+    environment.etc."binfmt.d/nixos.conf".source = binfmtFile;
+    system.activationScripts.binfmt = activationScript;
+    systemd.additionalUpstreamSystemUnits =
+      [ "proc-sys-fs-binfmt_misc.automount"
+        "proc-sys-fs-binfmt_misc.mount"
+      ];
+  };
+}
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index 5d7e0ec65e73..2e45dc78471f 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -2,17 +2,27 @@ import ./make-test.nix ({ pkgs, ... }:
 
 let
   configDir = "/var/lib/foobar";
+  apiPassword = "secret";
 
 in {
   name = "home-assistant";
+  meta = with pkgs.stdenv.lib; {
+    maintainers = with maintainers; [ dotlambda ];
+  };
 
   nodes = {
     hass =
       { config, pkgs, ... }:
       {
+        environment.systemPackages = with pkgs; [
+          mosquitto
+        ];
         services.home-assistant = {
           inherit configDir;
           enable = true;
+          package = pkgs.home-assistant.override {
+            extraPackages = ps: with ps; [ hbmqtt ];
+          };
           config = {
             homeassistant = {
               name = "Home";
@@ -22,7 +32,16 @@ in {
               elevation = 0;
             };
             frontend = { };
-            http = { };
+            http.api_password = apiPassword;
+            mqtt = { }; # Use hbmqtt as broker
+            binary_sensor = [
+              {
+                platform = "mqtt";
+                state_topic = "home-assistant/test";
+                payload_on = "let_there_be_light";
+                payload_off = "off";
+              }
+            ];
           };
         };
       };
@@ -31,7 +50,7 @@ in {
   testScript = ''
     startAll;
     $hass->waitForUnit("home-assistant.service");
-    
+
     # Since config is specified using a Nix attribute set,
     # configuration.yaml is a link to the Nix store
     $hass->succeed("test -L ${configDir}/configuration.yaml");
@@ -39,8 +58,19 @@ in {
     # Check that Home Assistant's web interface and API can be reached
     $hass->waitForOpenPort(8123);
     $hass->succeed("curl --fail http://localhost:8123/states");
-    $hass->succeed("curl --fail http://localhost:8123/api/ | grep 'API running'");
+    $hass->succeed("curl --fail -H 'x-ha-access: ${apiPassword}' http://localhost:8123/api/ | grep -qF 'API running'");
 
+    # Toggle a binary sensor using MQTT
+    $hass->succeed("curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}' | grep -qF '\"state\": \"off\"'");
+    $hass->waitUntilSucceeds("mosquitto_pub -V mqttv311 -t home-assistant/test -u homeassistant -P '${apiPassword}' -m let_there_be_light");
+    $hass->succeed("curl http://localhost:8123/api/states/binary_sensor.mqtt_binary_sensor -H 'x-ha-access: ${apiPassword}' | grep -qF '\"state\": \"on\"'");
+
+    # Check that no errors were logged
     $hass->fail("cat ${configDir}/home-assistant.log | grep -qF ERROR");
+
+    # Print log to ease debugging
+    my $log = $hass->succeed("cat ${configDir}/home-assistant.log");
+    print "\n### home-assistant.log ###\n";
+    print "$log\n";
   '';
 })