From c40dabc6552a3523c444559b75791aad74f34afe Mon Sep 17 00:00:00 2001
From: Zygmunt Krynicki <zygmunt.krynicki@huawei.com>
Date: Thu, 8 Jul 2021 17:44:57 +0000
Subject: [PATCH] WIP: to break up

---
 .gitignore                                    |   1 +
 Makefile                                      |   5 +
 .../tests/rauc-boot-backend-smoke/task.yaml   |  45 +++++
 cmd/sysotad/main.go                           |   2 +
 cmd/sysotad/rauc_boot_backend.go              |  37 ++++
 cmd/sysotad/sysotad.go                        |  11 ++
 .../introspection/introspection-expected.txt  |   6 +
 ota/ota.go                                    |   4 +
 rauc/tests/rauc-status/task.yaml              |  81 +++++++++
 service/boot_backend.go                       | 169 ++++++++++++++++++
 service/service.go                            |  92 ++++++++--
 spread.yaml                                   |  13 +-
 12 files changed, 445 insertions(+), 21 deletions(-)
 create mode 100644 boot/piboot/tests/rauc-boot-backend-smoke/task.yaml
 create mode 100644 cmd/sysotad/rauc_boot_backend.go
 create mode 100644 rauc/tests/rauc-status/task.yaml
 create mode 100644 service/boot_backend.go

diff --git a/.gitignore b/.gitignore
index 2a83ca6..032c0f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 
 .spread-reuse.*.yaml
 .spread-reuse.yaml
+cmd/sysotad/sysota-rauc-hook
 cmd/sysotad/sysotad
 config.sysota.mk
 configure
diff --git a/Makefile b/Makefile
index 7b63a9c..3c2981b 100644
--- a/Makefile
+++ b/Makefile
@@ -166,3 +166,8 @@ $(eval $(call ZMK.Expand,InstallUninstall,cmd/sysotad/sysotad))
 $(eval $(call ZMK.Expand,ManPage,man/sysotad.conf.5))
 
 include .zmk-hotfixes/zmk-hotfix-80.mk
+
+# Symlink: rauc-boot-backend -> sysotad
+cmd/sysotad/rauc-boot-backend.InstallDir = $(cmd/sysotad/sysotad.InstallDir)
+cmd/sysotad/rauc-boot-backend.SymlinkTarget = sysotad
+$(eval $(call ZMK.Expand,Symlink,cmd/sysotad/rauc-boot-backend))
diff --git a/boot/piboot/tests/rauc-boot-backend-smoke/task.yaml b/boot/piboot/tests/rauc-boot-backend-smoke/task.yaml
new file mode 100644
index 0000000..5ea390d
--- /dev/null
+++ b/boot/piboot/tests/rauc-boot-backend-smoke/task.yaml
@@ -0,0 +1,45 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Huawei Inc.
+
+summary: SystemOTA RAUC boot loader hook implements the "get-primary" command
+prepare: |
+    systemctl reset-failed sysotad.service
+    systemctl stop sysotad.service
+
+    mkdir -p /var/lib/sysota
+    cat <<STATE_INI >/var/lib/sysota/state.ini
+    [System]
+    BootMode=normal
+    PrimarySlot=A
+
+    [SlotA]
+    BootState=good
+
+    [SlotB]
+    BootState=bad
+    STATE_INI
+
+    systemctl start sysotad.service
+
+execute: |
+    # Primary slot can be read and written.
+    /usr/lib/sysota/rauc-boot-backend get-primary | MATCH A
+
+    /usr/lib/sysota/rauc-boot-backend set-primary B
+    /usr/lib/sysota/rauc-boot-backend get-primary | MATCH B
+
+    # State of each slot can be read and written.
+    /usr/lib/sysota/rauc-boot-backend get-state A | MATCH good
+    /usr/lib/sysota/rauc-boot-backend get-state B | MATCH bad
+
+    /usr/lib/sysota/rauc-boot-backend set-state B good
+    /usr/lib/sysota/rauc-boot-backend get-state B | MATCH good
+
+restore: |
+    systemctl stop sysotad.service
+
+    rm -f /var/lib/sysota/state.ini
+    rmdir /var/lib/sysota
+
+    systemctl reset-failed sysotad.service
+    systemctl restart sysotad.service
diff --git a/cmd/sysotad/main.go b/cmd/sysotad/main.go
index d7a1f52..8727c87 100644
--- a/cmd/sysotad/main.go
+++ b/cmd/sysotad/main.go
@@ -31,6 +31,8 @@ func dispatchCall() error {
 		return nil
 	case "sysotad":
 		return runSysotad(DefaultOptions())
+	case "rauc-boot-backend":
+		return runRaucBootBackend()
 	default:
 		return fmt.Errorf("cannot run sysotad with name: %s", execName)
 	}
diff --git a/cmd/sysotad/rauc_boot_backend.go b/cmd/sysotad/rauc_boot_backend.go
new file mode 100644
index 0000000..afd3dff
--- /dev/null
+++ b/cmd/sysotad/rauc_boot_backend.go
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Huawei Inc.
+
+// Package main implements the sysotad system service.
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"git.ostc-eu.org/OSTC/OHOS/components/sysota/dbusutil"
+	"git.ostc-eu.org/OSTC/OHOS/components/sysota/ota/raucbackend"
+	"git.ostc-eu.org/OSTC/OHOS/components/sysota/rauc"
+)
+
+func runRaucBootBackend() (err error) {
+	conn, err := dbusutil.SystemBus()
+	if err != nil {
+		return err
+	}
+
+	defer func() {
+		e := conn.Close()
+		if err == nil {
+			err = e
+		}
+	}()
+
+	if len(os.Args) < 2 {
+		return fmt.Errorf("expected RAUC boot backend command")
+	}
+
+	cmd := os.Args[1]
+	args := os.Args[2:]
+
+	return rauc.BootCommand(raucbackend.NewClient(conn), cmd, args...)
+}
diff --git a/cmd/sysotad/sysotad.go b/cmd/sysotad/sysotad.go
index a1db1ed..ceda92e 100644
--- a/cmd/sysotad/sysotad.go
+++ b/cmd/sysotad/sysotad.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"os"
 	"os/signal"
+	"path/filepath"
 	"syscall"
 	"time"
 
@@ -42,6 +43,12 @@ func runSysotad(opts *Options) (err error) {
 		return err
 	}
 
+	// If the state directory exists, run in stateful mode.
+	stateful := false
+	if fi, err := os.Stat(filepath.Dir(opts.StateFile)); err == nil && fi.IsDir() {
+		stateful = true
+	}
+
 	// Load state and defer state save on return.
 	state, err := ota.LoadState(opts.StateFile)
 	if err != nil {
@@ -90,6 +97,10 @@ func runSysotad(opts *Options) (err error) {
 	// Create the SystemOTA service
 	svc := service.New(conn)
 
+	if stateful {
+		svc.SetSystemState(state)
+	}
+
 	// Set the boot loader protocol implementation based on configuration.
 	switch conf.BootLoaderType {
 	case ota.InertBootLoader:
diff --git a/cmd/sysotad/tests/introspection/introspection-expected.txt b/cmd/sysotad/tests/introspection/introspection-expected.txt
index faf3cee..098d4c2 100644
--- a/cmd/sysotad/tests/introspection/introspection-expected.txt
+++ b/cmd/sysotad/tests/introspection/introspection-expected.txt
@@ -6,9 +6,15 @@ dev.ostc.sysota1.BootLoader         interface -          -              -
 .QueryInactive                      method    -          s              -
 .Reboot                             method    u          -              -
 .TrySwitch                          method    s          -              -
+dev.ostc.sysota1.RaucBootBackend    interface -          -              -
+.GetPrimary                         method    -          s              -
+.GetState                           method    s          s              -
+.SetPrimary                         method    s          -              -
+.SetState                           method    ss         -              -
 dev.ostc.sysota1.Service            interface -          -              -
 .UpdateDevice                       method    a{ss}      o              -
 .UpdateStreams                      method    -          -              -
+.BootMode                           property  s          "normal"       emits-change
 .Maker                              property  s          "dummy-maker"  const
 .Model                              property  s          "dummy-model"  const
 .Stream                             property  s          "dummy-stream" emits-change writable
diff --git a/ota/ota.go b/ota/ota.go
index a44c679..860efd0 100644
--- a/ota/ota.go
+++ b/ota/ota.go
@@ -167,4 +167,8 @@ const (
 	ServiceBusName = "dev.ostc.sysota1"
 	// ServiceObjectPath is the D-Bus object path used by the SystemOTA service.
 	ServiceObjectPath = "/dev/ostc/sysota1/Service"
+	// ServiceInterfaceName is the name of the SystemOTA Service interface.
+	ServiceInterfaceName = "dev.ostc.sysota1.Service"
+	// BootLoaderInterfaceName is the name of the SystemOTA BootLoader interface.
+	BootLoaderInterfaceName = "dev.ostc.sysota1.BootLoader"
 )
diff --git a/rauc/tests/rauc-status/task.yaml b/rauc/tests/rauc-status/task.yaml
new file mode 100644
index 0000000..f6ffcf7
--- /dev/null
+++ b/rauc/tests/rauc-status/task.yaml
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Huawei Inc.
+
+summary: RAUC custom boot loader hooks are accepted
+environment:
+    FAKE: /var/tmp/sysota-fake
+prepare: |
+    mkdir -p "$FAKE/app-data"
+    mkdir -p "$FAKE/boot"
+    mkdir -p "$FAKE/dev/by-name/"
+    mkdir -p "$FAKE/sys-data"
+    mkdir -p "$FAKE/sys-data/common"
+
+    mkdir -p /etc/sysota
+    mkdir -p /etc/rauc
+    mkdir -p /var/lib/sysota
+
+    systemctl stop rauc.service || true
+    systemctl reset-failed rauc.service || true
+
+    systemctl stop sysotad.service || true
+    systemctl reset-failed sysotad.service
+
+    # Create fake pi-boot configuration
+    cat <<BOOT_CONFIG_TXT >$FAKE/boot/config.txt
+    # TODO(zyga): use test boot interface
+    kernel=slot-a/kernel
+    cmdline=slot-a/cmdline.txt
+    initramfs slot-a/initrd.cpio
+    BOOT_CONFIG_TXT
+
+    # Configure sysotad for pi-boot in a fake directory
+    cat <<SYSOTA_SYSOTAD_CONF >/etc/sysota/sysotad.conf
+    [OTA]
+    BootLoaderType=pi-boot
+    BootPartitionMountDir=$FAKE/boot
+    SYSOTA_SYSOTAD_CONF
+
+    cat <<RAUC_SYSTEM_CONF >/etc/rauc/system.conf
+    [system]
+    compatible=OSTC SystemOTA Test Environment
+    bootloader=custom
+    statusfile=$FAKE/sys-data/common/central-status.raucs
+    bundle-formats=-plain
+
+    [slot.image.0]
+    device=$FAKE/dev/by-name/slot-a
+    bootname=A
+
+    [slot.image.1]
+    device=$FAKE/dev/by-name/slot-b
+    bootname=B
+
+    [handlers]
+    bootloader-custom-backend=/usr/lib/sysota/rauc-boot-backend
+    RAUC_SYSTEM_CONF
+
+    # Create create SystemOTA state file
+    cat <<STATE_INI >/var/lib/sysota/state.ini
+    [System]
+    BootMode=normal
+    PrimarySlot=A
+
+    [SlotA]
+    BootState=good
+
+    [SlotB]
+    BootState=bad
+    STATE_INI
+execute: |
+    rauc status
+restore: |
+    systemctl reset-failed sysotad.service
+    systemctl stop sysotad.service
+
+    systemctl reset-failed rauc.service
+    systemctl stop rauc.service
+
+    rm -rf /etc/rauc
+    rm -rf /etc/sysota
+    rm -rf "$FAKE"
diff --git a/service/boot_backend.go b/service/boot_backend.go
new file mode 100644
index 0000000..8bab279
--- /dev/null
+++ b/service/boot_backend.go
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: Huawei Inc.
+
+// Package service implements the sysotad D-Bus service.
+package service
+
+import (
+	dbus "github.com/godbus/dbus/v5"
+	"github.com/godbus/dbus/v5/introspect"
+
+	"git.ostc-eu.org/OSTC/OHOS/components/sysota/boot"
+	"git.ostc-eu.org/OSTC/OHOS/components/sysota/ota/raucbackend"
+)
+
+// RaucBootBackendIntrospectData is the D-Bus introspection data for the RaucHook interface of the service object.
+var RaucBootBackendIntrospectData = introspect.Interface{
+	Name: raucbackend.InterfaceName,
+	Methods: []introspect.Method{
+		{
+			Name: "GetPrimary",
+			Args: []introspect.Arg{
+				{
+					Name:      "slot",
+					Type:      "s",
+					Direction: "out",
+				},
+			},
+		},
+		{
+			Name: "SetPrimary",
+			Args: []introspect.Arg{
+				{
+					Name:      "slot",
+					Type:      "s",
+					Direction: "in",
+				},
+			},
+		},
+		{
+			Name: "GetState",
+			Args: []introspect.Arg{
+				{
+					Name:      "slot",
+					Type:      "s",
+					Direction: "in",
+				},
+				{
+					Name:      "state",
+					Type:      "s",
+					Direction: "out",
+				},
+			},
+		},
+		{
+			Name: "SetState",
+			Args: []introspect.Arg{
+				{
+					Name:      "slot",
+					Type:      "s",
+					Direction: "in",
+				},
+				{
+					Name:      "state",
+					Type:      "s",
+					Direction: "in",
+				},
+			},
+		},
+	},
+}
+
+func (svc *Service) GetPrimary() (slotName string, dbusErr *dbus.Error) {
+	svc.m.RLock()
+	defer svc.m.RUnlock()
+
+	if svc.systemState == nil {
+		return "", dbus.MakeFailedError(errEphemeralMode)
+	}
+
+	if svc.systemState.PrimarySlot == boot.InvalidSlot {
+		return "", dbus.MakeFailedError(boot.ErrInvalidSlot)
+	}
+
+	return svc.systemState.PrimarySlot.String(), nil
+}
+
+func (svc *Service) SetPrimary(slotName string) (dbusErr *dbus.Error) {
+	svc.m.Lock()
+	defer svc.m.Unlock()
+
+	if svc.systemState == nil {
+		return dbus.MakeFailedError(errEphemeralMode)
+	}
+
+	var slot boot.Slot
+
+	if err := slot.UnmarshalText([]byte(slotName)); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	if slot == boot.InvalidSlot {
+		return dbus.MakeFailedError(boot.ErrInvalidSlot)
+	}
+
+	// TODO(zyga): use the boot protocol to TrySwitch if slot changes
+
+	svc.systemState.PrimarySlot = slot
+
+	return nil
+}
+
+func (svc *Service) GetState(slotName string) (bootState string, dbusErr *dbus.Error) {
+	svc.m.RLock()
+	defer svc.m.RUnlock()
+
+	if svc.systemState == nil {
+		return "", dbus.MakeFailedError(errEphemeralMode)
+	}
+
+	var slot boot.Slot
+
+	if err := slot.UnmarshalText([]byte(slotName)); err != nil {
+		return "", dbus.MakeFailedError(err)
+	}
+
+	// TODO(zyga): verify the expected behavior when RAUC performs a complete update.
+	// In particular, check if the expected state of an inactive slot needs to be faked.
+
+	switch slot {
+	case boot.SlotA:
+		return svc.systemState.SlotAState.String(), nil
+	case boot.SlotB:
+		return svc.systemState.SlotBState.String(), nil
+	}
+
+	return "", nil
+}
+
+func (svc *Service) SetState(slotName string, stateName string) (dbusErr *dbus.Error) {
+	svc.m.Lock()
+	defer svc.m.Unlock()
+
+	if svc.systemState == nil {
+		return dbus.MakeFailedError(errEphemeralMode)
+	}
+
+	var slot boot.Slot
+
+	if err := slot.UnmarshalText([]byte(slotName)); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	var state boot.SlotState
+
+	if err := state.UnmarshalText([]byte(stateName)); err != nil {
+		return dbus.MakeFailedError(err)
+	}
+
+	// TODO(zyga): use the boot protocol to CommitSwitch or Rollback switch if boot.Mode is boot.Try.
+
+	switch slot {
+	case boot.SlotA:
+		svc.systemState.SlotAState = state
+	case boot.SlotB:
+		svc.systemState.SlotBState = state
+	}
+
+	return nil
+}
diff --git a/service/service.go b/service/service.go
index e46bbc2..c25a23b 100644
--- a/service/service.go
+++ b/service/service.go
@@ -17,6 +17,7 @@ import (
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/dbusutil"
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/dbusutil/objmgr"
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/ota"
+	"git.ostc-eu.org/OSTC/OHOS/components/sysota/ota/raucbackend"
 	"git.ostc-eu.org/OSTC/OHOS/components/sysota/service/oper"
 )
 
@@ -24,13 +25,8 @@ const (
 	makerProperty  = "Maker"
 	modelProperty  = "Model"
 	streamProperty = "Stream"
-)
 
-const (
-	// ServiceInterfaceName is the name of the SystemOTA Service interface.
-	ServiceInterfaceName = "dev.ostc.sysota1.Service"
-	// BootLoaderInterfaceName is the name of the SystemOTA BootLoader interface.
-	BootLoaderInterfaceName = "dev.ostc.sysota1.BootLoader"
+	bootModeProperty = "BootMode"
 )
 
 // ServiceIntrospectData is the D-Bus introspection data for Service interface of the service object.
@@ -39,7 +35,7 @@ const (
 // over the provided data, given that we implement the properties interface
 // ourselves.
 var ServiceIntrospectData = introspect.Interface{
-	Name: ServiceInterfaceName,
+	Name: ota.ServiceInterfaceName,
 	Methods: []introspect.Method{
 		{
 			Name: "UpdateStreams",
@@ -94,12 +90,23 @@ var ServiceIntrospectData = introspect.Interface{
 				},
 			},
 		},
+		{
+			Name:   bootModeProperty,
+			Type:   "s",
+			Access: "read",
+			Annotations: []introspect.Annotation{
+				{
+					Name:  dbusutil.PropertyAnnotationEmitsChangedSignal,
+					Value: "true",
+				},
+			},
+		},
 	},
 }
 
 // BootLoaderIntrospectData is the D-Bus introspection data for the BootLoader interface of the service object.
 var BootLoaderIntrospectData = introspect.Interface{
-	Name: BootLoaderInterfaceName,
+	Name: ota.BootLoaderInterfaceName,
 	Methods: []introspect.Method{
 		{
 			Name: "QueryActive",
@@ -188,6 +195,8 @@ type Service struct {
 
 	bootProto boot.Protocol
 
+	systemState *ota.SystemState
+
 	lastOperationID int
 }
 
@@ -210,6 +219,18 @@ func (svc *Service) SetBootProtocol(bootProto boot.Protocol) {
 	svc.bootProto = bootProto
 }
 
+// SetSystemState sets the system state associated with the service.
+//
+// Setting the state makes the service stateful. A stateful service will allow
+// state to be read, as a D-Bus property. It will also update the state when
+// certain operations are invoked.
+func (svc *Service) SetSystemState(systemState *ota.SystemState) {
+	svc.m.Lock()
+	defer svc.m.Unlock()
+
+	svc.systemState = systemState
+}
+
 // Export exports the service object on the bus.
 func (svc *Service) Export() (err error) {
 	svc.m.Lock()
@@ -227,7 +248,7 @@ func (svc *Service) Export() (err error) {
 		"UpdateStreams": svc.UpdateStreams,
 		"UpdateDevice":  svc.UpdateDevice,
 	}
-	if err := svc.conn.ExportMethodTable(svcMethods, path, ServiceInterfaceName); err != nil {
+	if err := svc.conn.ExportMethodTable(svcMethods, path, ota.ServiceInterfaceName); err != nil {
 		return err
 	}
 
@@ -255,7 +276,17 @@ func (svc *Service) Export() (err error) {
 		"CommitSwitch":  svc.CommitSwitch,
 		"CancelSwitch":  svc.CancelSwitch,
 	}
-	if err := svc.conn.ExportMethodTable(bootProtoMethods, path, BootLoaderInterfaceName); err != nil {
+	if err := svc.conn.ExportMethodTable(bootProtoMethods, path, ota.BootLoaderInterfaceName); err != nil {
+		return err
+	}
+
+	raucHookProtoMethods := map[string]interface{}{
+		"GetPrimary": svc.GetPrimary,
+		"SetPrimary": svc.SetPrimary,
+		"GetState":   svc.GetState,
+		"SetState":   svc.SetState,
+	}
+	if err := svc.conn.ExportMethodTable(raucHookProtoMethods, path, raucbackend.InterfaceName); err != nil {
 		return err
 	}
 
@@ -267,6 +298,7 @@ func (svc *Service) Export() (err error) {
 			objmgr.IntrospectData,
 			ServiceIntrospectData,
 			BootLoaderIntrospectData,
+			RaucBootBackendIntrospectData,
 		},
 	}
 
@@ -277,7 +309,10 @@ func (svc *Service) Export() (err error) {
 	return nil
 }
 
-var errNoBootProtocol = errors.New("boot protocol is not set")
+var (
+	errNoBootProtocol = errors.New("boot protocol is not set")
+	errEphemeralMode  = errors.New("ephemeral mode")
+)
 
 // QueryActive exposes the boot.Protocol.QueryActive method over D-Bus.
 func (svc *Service) QueryActive() (string, *dbus.Error) {
@@ -395,7 +430,15 @@ func (svc *Service) Unexport() error {
 func (svc *Service) unexportUnlocked() error {
 	const path = ota.ServiceObjectPath
 
-	if err := svc.conn.Export(nil, path, ServiceInterfaceName); err != nil {
+	if err := svc.conn.Export(nil, path, ota.ServiceInterfaceName); err != nil {
+		return err
+	}
+
+	if err := svc.conn.Export(nil, path, ota.BootLoaderInterfaceName); err != nil {
+		return err
+	}
+
+	if err := svc.conn.Export(nil, path, raucbackend.InterfaceName); err != nil {
 		return err
 	}
 
@@ -458,7 +501,7 @@ func (svc *Service) setStreamUnlocked(stream string) error {
 	}
 	invalidated := []string{}
 
-	return svc.conn.Emit(ota.ServiceObjectPath, dbusutil.PropertiesChangedSignal, ServiceInterfaceName, updated, invalidated)
+	return svc.conn.Emit(ota.ServiceObjectPath, dbusutil.PropertiesChangedSignal, ota.ServiceInterfaceName, updated, invalidated)
 }
 
 // IsIdle returns true if the OTA service is idle and can be shut down.
@@ -545,7 +588,7 @@ func (svc *Service) Get(iface, property string) (dbus.Variant, *dbus.Error) {
 	defer svc.m.RUnlock()
 
 	switch iface {
-	case ServiceInterfaceName:
+	case ota.ServiceInterfaceName:
 		switch property {
 		case makerProperty:
 			return dbus.MakeVariant(svc.maker), nil
@@ -553,6 +596,12 @@ func (svc *Service) Get(iface, property string) (dbus.Variant, *dbus.Error) {
 			return dbus.MakeVariant(svc.model), nil
 		case streamProperty:
 			return dbus.MakeVariant(svc.stream), nil
+		case bootModeProperty:
+			if svc.systemState != nil {
+				return dbus.MakeVariant(svc.systemState.BootMode.String()), nil
+			}
+
+			return dbus.Variant{}, dbus.MakeFailedError(errEphemeralMode)
 		default:
 			return dbus.Variant{}, prop.ErrPropNotFound
 		}
@@ -567,12 +616,17 @@ func (svc *Service) GetAll(iface string) (map[string]dbus.Variant, *dbus.Error)
 	defer svc.m.RUnlock()
 
 	switch iface {
-	case ServiceInterfaceName:
-		return map[string]dbus.Variant{
+	case ota.ServiceInterfaceName:
+		all := map[string]dbus.Variant{
 			makerProperty:  dbus.MakeVariant(svc.maker),
 			modelProperty:  dbus.MakeVariant(svc.model),
 			streamProperty: dbus.MakeVariant(svc.stream),
-		}, nil
+		}
+		if svc.systemState != nil {
+			all[bootModeProperty] = dbus.MakeVariant(svc.systemState.BootMode.String())
+		}
+
+		return all, nil
 	default:
 		return nil, prop.ErrIfaceNotFound
 	}
@@ -584,9 +638,9 @@ func (svc *Service) Set(iface, property string, newValue dbus.Variant) (err *dbu
 	defer svc.m.Unlock()
 
 	switch iface {
-	case ServiceInterfaceName:
+	case ota.ServiceInterfaceName:
 		switch property {
-		case makerProperty, modelProperty:
+		case makerProperty, modelProperty, bootModeProperty:
 			return prop.ErrReadOnly
 		case streamProperty:
 			if newValue.Signature() != dbus.SignatureOf(svc.stream) {
diff --git a/spread.yaml b/spread.yaml
index 3c9fd4f..c783b61 100644
--- a/spread.yaml
+++ b/spread.yaml
@@ -7,6 +7,13 @@ backends:
     lxd:
         systems:
             - ubuntu-21.04:
+        prepare: |
+            apt-get remove -y --purge snapd
+            apt-get remove -y --purge fwupd
+            apt-get remove -y --purge udisks
+            apt-get remove -y --purge packagekit
+            apt-get autoremove -y --purge
+
     qemu:
         memory: 1G
         systems:
@@ -23,8 +30,8 @@ exclude:
 
 prepare: |
     # Install build dependencies if missing.
-    test -n "$(command -v go)" && test -f /usr/include/z.mk || (
-        apt-get update && apt-get install -y golang-go zmk
+    test -n "$(command -v go)" && test -f /usr/include/z.mk && test -f /lib/systemd/system/rauc.service || (
+        apt-get update && apt-get install -y golang-go zmk rauc-service
     )
 
     # Current directory is $SPREAD_PATH which is conveniently the same as $GOPATH
@@ -176,3 +183,5 @@ suites:
             rm -rf /etc/sysota
     man/tests/:
         summary: tests for manual pages
+    rauc/tests/:
+        summary: tests for RAUC integration
-- 
GitLab