Unverified Commit 1014b658 authored by numinit's avatar numinit Committed by GitHub
Browse files

nixos/rauc: init module (#460905)

parents d57736bd a3c3bcff
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -158,6 +158,8 @@

- [pmount](https://salsa.debian.org/debian/pmount), a tool that allows normal users to mount removable devices without requiring root privileges Available at [programs.pmount](#opt-programs.pmount.enable).

- [rauc](https://rauc.io/) (the Robust Auto-Update Controller), a daemon that allows reliable and secure software updates in embedded Linux systems. Available at [services.rauc](#opt-services.rauc.enable).

- [lemurs](https://github.com/coastalwhite/lemurs), a customizable TUI display/login manager. Available at [services.displayManager.lemurs](#opt-services.displayManager.lemurs.enable).

- [docuseal](https://github.com/docusealco/docuseal), a DocuSign alternative. Create, fill, and sign digital documents. Available at [services.docuseal](#opt-services.docuseal.enable).
+1 −0
Original line number Diff line number Diff line
@@ -678,6 +678,7 @@
  ./services/hardware/powerstation.nix
  ./services/hardware/rasdaemon.nix
  ./services/hardware/ratbagd.nix
  ./services/hardware/rauc.nix
  ./services/hardware/sane.nix
  ./services/hardware/sane_extra_backends/brscan4.nix
  ./services/hardware/sane_extra_backends/brscan5.nix
+196 −0
Original line number Diff line number Diff line
{
  lib,
  pkgs,
  config,
  ...
}:

let
  inherit (lib.options) mkEnableOption mkPackageOption mkOption;
  inherit (lib.modules) mkMerge mkIf;
  inherit (lib.lists) flatten filter imap0;
  inherit (lib.attrsets)
    recursiveUpdate
    mapAttrsToList
    listToAttrs
    nameValuePair
    ;
  inherit (lib.strings) concatStringsSep;
  inherit (lib) types;

  cfg = config.services.rauc;
  format = pkgs.formats.ini { };

  mountDir = "${cfg.dataDir}/mnt";

  mkSlot =
    slot:
    if slot.enable then
      slot.settings
      // {
        inherit (slot) device type;
      }
    else
      null;

  slotSections = listToAttrs (
    filter (slot: slot != null) (
      flatten (
        mapAttrsToList (
          name: indexes: imap0 (idx: slot: nameValuePair "slot.${name}.${toString idx}" (mkSlot slot)) indexes
        ) cfg.slots
      )
    )
  );

  configFile = format.generate "rauc.conf" (
    recursiveUpdate cfg.settings (
      recursiveUpdate {
        system = {
          inherit (cfg) compatible bootloader;
          bundle-formats = concatStringsSep " " cfg.bundleFormats;
          data-directory = cfg.dataDir;
          mountprefix = mountDir;
        };
      } slotSections
    )
  );
in
{
  options = {
    services.rauc = {
      enable = mkEnableOption "RAUC A/B update service";
      mark-good.enable = mkEnableOption "RAUC Good-marking service";
      client.enable = mkEnableOption "RAUC client in the system environment";
      package = mkPackageOption pkgs "rauc" { };
      compatible = mkOption {
        description = "The compatibility string for this system. Can be any format so long as you are consistent.";
        type = types.str;
        example = "nix/appliance/foo";
      };
      bootloader = mkOption {
        description = "The bootloader backend for RAUC.";
        type = types.enum [
          "barebox"
          "grub"
          "uboot"
          "efi"
          "custom"
          "noop"
        ];
        example = "grub";
      };
      bundleFormats = mkOption {
        description = "Allowable formats for the RAUC bundle.";
        type = with types; listOf str;
        default = [
          "-plain"
          "+verity"
        ];
        example = [
          "-plain"
          "+verity"
        ];
      };
      dataDir = mkOption {
        description = "The state directory for RAUC.";
        default = "/var/lib/rauc";
        type = types.path;
      };
      slots = mkOption {
        description = "RAUC slot definitions. Every key is a slot class and every value is a list of slot indexes.";
        default = { };
        type = types.attrsOf (
          types.listOf (
            types.submodule {
              options = {
                enable = mkEnableOption "this RAUC slot";
                device = mkOption {
                  description = "The device to update.";
                  type = types.str;
                };
                type = mkOption {
                  description = "The type of the device.";
                  type = types.enum [
                    "raw"
                    "nand"
                    "nor"
                    "ubivol"
                    "ubifs"
                    "ext4"
                    "vfat"
                  ];
                  default = "raw";
                };
                settings = mkOption {
                  description = "Settings for this slot.";
                  type = types.attrs;
                  default = { };
                };
              };
            }
          )
        );
      };
      settings = mkOption {
        type = format.type;
        default = { };
        description = ''
          Rauc configuration that will be converted to INI. Refer to:
          <https://rauc.readthedocs.io/en/latest/reference.html#sec-ref-slot-config>
          for details on supported values.

          All module-specific options override these.
        '';
      };
    };
  };

  config = mkMerge [
    (mkIf cfg.enable {
      systemd.services.rauc = {
        description = "RAUC Update Service";
        documentation = [ "https://rauc.readthedocs.io" ];
        wants = [ "basic.target" ];
        wantedBy = [ "multi-user.target" ];
        after = [
          "dbus.service"
        ];
        serviceConfig = {
          Type = "dbus";
          BusName = "de.pengutronix.rauc";
          ExecStart = "${lib.getExe cfg.package} --conf=${configFile} --mount=/run/rauc/mnt service";
          RuntimeDirectory = "rauc/mnt";
          MountFlags = "slave";
          StateDirectory = baseNameOf cfg.dataDir;
          WorkingDirectory = cfg.dataDir;
        };
      };
      systemd.tmpfiles.rules = [
        "d ${cfg.dataDir} 0750 root root - -"
        "d ${mountDir} 0750 root root - -"
      ];
    })
    (mkIf (cfg.enable && cfg.client.enable) {
      services.dbus.packages = [ cfg.package ];
      environment.systemPackages = [ cfg.package ];
    })
    (mkIf (cfg.enable && cfg.mark-good.enable) {
      systemd.services.rauc-mark-good = {
        description = "RAUC Good-marking service";
        documentation = [ "https://rauc.readthedocs.io" ];
        wantedBy = [ "multi-user.target" ];
        after = [
          "rauc.service"
          "multi-user.target"
        ];
        serviceConfig = {
          Type = "oneshot";
          ExecStart = "${lib.getExe cfg.package} --conf=${configFile} status mark-good";
        };
      };
    })
  ];

  meta.maintainers = with lib.maintainers; [ numinit ];
}
+1 −0
Original line number Diff line number Diff line
@@ -1316,6 +1316,7 @@ in
  ragnarwm = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./ragnarwm.nix;
  rasdaemon = runTest ./rasdaemon.nix;
  rathole = runTest ./rathole.nix;
  rauc = runTest ./rauc.nix;
  readarr = runTest ./readarr.nix;
  readeck = runTest ./readeck.nix;
  realm = runTest ./realm.nix;

nixos/tests/rauc.nix

0 → 100644
+258 −0
Original line number Diff line number Diff line
{ pkgs, lib, ... }:

{
  name = "rauc";

  nodes.machine =
    {
      pkgs,
      lib,
      config,
      ...
    }:
    let
      inherit (import ./ssh-keys.nix pkgs)
        snakeOilPrivateKey
        ;

      snakeOilRaucCert =
        pkgs.runCommand "rauc.crt"
          {
            inherit snakeOilPrivateKey;
          }
          ''
            ${lib.getExe pkgs.openssl} req -x509 -key $snakeOilPrivateKey -out $out -days 3650 \
              -addext subjectKeyIdentifier=hash \
              -addext authorityKeyIdentifier=keyid:always,issuer:always \
              -addext basicConstraints=CA:FALSE \
              -addext extendedKeyUsage=critical,codeSigning \
              -subj "/OU=Smoke Test/OU=RAUC/CN=NixOS/"
          '';

      raucManifest = pkgs.writeText "manifest.raucm" (
        lib.generators.toINI { } {
          update = {
            inherit (config.services.rauc) compatible;
            version = "@VERSION@";
          };
          bundle.format = "verity";
          "image.rauc".filename = "rauc.img";
        }
      );
      rauc-mkimg = pkgs.writeShellApplication {
        name = "rauc-mkimg";
        runtimeInputs = with pkgs; [
          config.services.rauc.package
          e2fsprogs
          squashfsTools
        ];

        text = ''
          set -xeuo pipefail
          tmpdir="$(mktemp -dt rauc.XXXXX)"

          cleanup() {
            rm -rf "$tmpdir"
          }
          trap cleanup EXIT

          cd "$tmpdir"
          mkdir bundle target
          cat ${raucManifest} > bundle/manifest.raucm
          sed -i "s/@VERSION@/$2/g" bundle/manifest.raucm
          echo "$1" > target/label
          truncate -s128M bundle/rauc.img
          mkfs.ext4 -v -L "rauc_$1" -d target bundle/rauc.img
          cd bundle
          rauc bundle --cert=${snakeOilRaucCert} --key=${snakeOilPrivateKey} . "$3"
        '';
      };
      rauc-bundle-c = pkgs.runCommand "rauc_c.bundle" { } ''
        ${lib.getExe rauc-mkimg} c 42 $out
      '';

      rauc-do-update = pkgs.writeShellScriptBin "rauc-do-update" ''
        if [ "$1" == c ]; then
          bundle=${rauc-bundle-c}
        else
          exit 1
        fi
        exec rauc install "$bundle"
      '';
    in
    {
      environment.systemPackages = with pkgs; [
        jq
        rauc-do-update
      ];

      services.rauc = {
        enable = true;
        client.enable = true;
        mark-good.enable = true;
        compatible = "nix/widget/smoketest";
        bootloader = "custom";
        slots = {
          rauc =
            let
              slot = ab: {
                enable = true;
                device = "/dev/vd${lib.replaceStrings [ "a" "b" ] [ "b" "c" ] ab}";
                settings.bootname = ab;
              };
            in
            [
              (slot "a")
              (slot "b")
            ];
        };
        settings = {
          keyring = {
            path = toString snakeOilRaucCert;
            use-bundle-signing-time = true;
            check-purpose = "codesign";
          };
          handlers = {
            system-info = toString (
              pkgs.writeShellScript "rauc-sysinfo.sh" ''
                echo "RAUC_SYSTEM_SERIAL=nixos"
                echo "RAUC_SYSTEM_VARIANT=nix/widget/smoketest"
              ''
            );
            bootloader-custom-backend = toString (
              pkgs.writeShellScript "rauc-backend.sh" ''
                case "$1" in
                  get-primary)
                    cat /rauc.current || exit $?
                    ;;
                  set-primary)
                    echo "$2" > /rauc.current
                    ;;
                  get-state)
                    cat "/rauc.$2.status" || echo bad
                    ;;
                  set-state)
                    echo "$3" > "/rauc.$2.status"
                    ;;
                  get-current)
                    cat /rauc.booted
                    ;;
                  *)
                    exit 1
                    ;;
                esac
              ''
            );
          };
        };
      };

      boot.initrd = {
        postDeviceCommands = ''
          ensure_ext4() {
            x="$(echo "$1" | tr ab bc)"
            if [ "$(blkid -t TYPE=ext4 -l -o device "/dev/vd$x")" != "/dev/vd$x" ]; then
              ${pkgs.e2fsprogs}/bin/mkfs.ext4 -v -L "rauc_$1" "/dev/vd$x" || exit $?
            fi
          }
          ensure_ext4 a
          ensure_ext4 b
        '';

        postMountCommands = ''
          # Keep track of the current RAUC partition and the booted one.
          if [ ! -f /mnt-root/rauc.current ]; then
            echo a > /mnt-root/rauc.current
          fi
          cat /mnt-root/rauc.current > /mnt-root/rauc.booted

          # Initialize test rauc partitions using a custom backend for the test.
          init_ab() {
            mkdir -p "/mnt-root/rauc_$1"
            umount "/mnt-root/rauc_$1"
            mount -t ext4 "/dev/vd$(echo "$1" | tr ab bc)" "/mnt-root/rauc_$1" || exit $?
            if [ -d "/mnt-root/rauc_$1" ] && [ ! -f "/mnt-root/rauc_$1/label" ]; then
              echo "$1" > "/mnt-root/rauc_$1/label"
            fi
            echo bad > "/mnt-root/rauc.$1.status"
            umount "/mnt-root/rauc_$1"
            rm -rf "/mnt-root/rauc_$1"
          }
          init_ab a
          init_ab b

          # Mount the RAUC boot directory.
          mkdir -p /mnt-root/rauc
          booted="/dev/vd$(tr ab bc < /mnt-root/rauc.booted)"
          echo "Mounting $booted to /rauc" | tee /dev/stderr
          mount "$booted" /mnt-root/rauc | tee /dev/stderr
        '';
      };

      virtualisation = {
        emptyDiskImages = [
          256
          256
        ];
      };
    };

  testScript =
    { nodes, ... }:
    ''
      from shlex import quote, join

      def rauc(*rauc_args):
        rauc_cmd = join(rauc_args)
        ret = machine.succeed(f'rauc {rauc_cmd} --output-format=json')
        machine.succeed('rauc status >&2')
        return ret

      def wait_rauc(result, *jq_args):
        jq_cmd = join([*jq_args[:-1], '-r', *jq_args[-1:]])
        machine.wait_until_succeeds(f'[ "$(rauc status --output-format=json | jq {jq_cmd})" == {quote(result)} ]')
        machine.succeed("rauc status >&2")

      def name(slot):
        return 'rauc.%d' % (int(slot, 16) - 10)

      def wait_booted(slot, image=None):
        if image is None:
          image = slot
        wait_rauc(slot, '.booted')
        machine.wait_until_succeeds(f'[ "$(</rauc/label)" == {quote(image)} ]')
      def wait_current(slot):
        wait_rauc(name(slot), '.boot_primary')
      def wait_status(slot, status):
        wait_rauc(status, '--arg', 'slot', slot, '.slots[] | to_entries[] | select(.value.bootname == $slot) | .value.boot_status')
      def wait_state(slot, state):
        wait_rauc(state, '--arg', 'slot', slot, '.slots[] | to_entries[] | select(.value.bootname == $slot) | .value.state')
      def set_slot(slot):
        rauc('status', 'mark-active', name(slot))
        wait_rauc(slot, '--arg', 'slot', slot, '.boot_primary as $primary | .slots[] | to_entries[] | select(.key == $primary) | .value.bootname')

      machine.start(allow_reboot=True)
      machine.wait_for_unit('multi-user.target')

      for (a, b, image, update) in (('a', 'b', 'a', ""), ('b', 'a', 'b', 'c'), ('a', 'b', 'c', "")):
        machine.wait_for_unit('rauc.service')
        machine.succeed("mount >&2")
        wait_booted(a, image)
        wait_current(a)
        wait_status(a, 'good')
        wait_status(b, 'bad')
        wait_state(a, 'booted')
        wait_state(b, 'inactive')
        set_slot(b)
        wait_current(b)
        set_slot(a)
        wait_current(a)
        set_slot(b)
        wait_current(b)

        if update != "":
          machine.succeed(f'rauc-do-update {quote(update)}')

        machine.reboot()
    '';
}
Loading