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