mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-06-05 21:03:40 +00:00
nixos/rauc: init module
Add the Robust Auto-Update Controller as a NixOS module, with tests.
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -680,6 +680,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
|
||||
|
||||
191
nixos/modules/services/hardware/rauc.nix
Normal file
191
nixos/modules/services/hardware/rauc.nix
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
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 A/B update service";
|
||||
wants = [ "basic.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [
|
||||
"dbus.service"
|
||||
];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${lib.getExe cfg.package} --conf=${configFile} service";
|
||||
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";
|
||||
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 ];
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
258
nixos/tests/rauc.nix
Normal file
258
nixos/tests/rauc.nix
Normal file
@@ -0,0 +1,258 @@
|
||||
{ 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()
|
||||
'';
|
||||
}
|
||||
@@ -14,7 +14,8 @@
|
||||
ninja,
|
||||
util-linux,
|
||||
libnl,
|
||||
systemd,
|
||||
systemdLibs,
|
||||
nixosTests,
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation rec {
|
||||
@@ -46,7 +47,7 @@ stdenv.mkDerivation rec {
|
||||
openssl
|
||||
util-linux
|
||||
libnl
|
||||
systemd
|
||||
systemdLibs
|
||||
];
|
||||
|
||||
mesonFlags = [
|
||||
@@ -61,6 +62,7 @@ stdenv.mkDerivation rec {
|
||||
|
||||
passthru = {
|
||||
updateScript = nix-update-script { };
|
||||
tests.rauc = nixosTests.rauc;
|
||||
};
|
||||
|
||||
meta = {
|
||||
|
||||
Reference in New Issue
Block a user