From 9a0be3b60c20e1d03e9c117b8afd79f7b8050710 Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Tue, 5 Aug 2025 16:56:34 +0200 Subject: [PATCH] feat(mtda): add command to apply firmware update This is a first and rough attempt to allow updating a ab-rootfs installed MTDA image from the mtda-cli. We implement this around the image streaming API, as it is semantically quite similar. However, due to the MTDA internals at some locations we need to cut corners. Further, there is currently no mechanism to confirm an update after the reboot (will be added, once the other points are clarified). Signed-off-by: Felix Moessbauer --- mtda-cli | 50 +++++++++ mtda/client.py | 15 +++ mtda/main.py | 14 +++ mtda/storage/helpers/swupdate_ipc.py | 162 +++++++++++++++++++++++++++ mtda/storage/swupdate.py | 73 ++++++++++++ 5 files changed, 314 insertions(+) create mode 100644 mtda/storage/helpers/swupdate_ipc.py create mode 100644 mtda/storage/swupdate.py diff --git a/mtda-cli b/mtda-cli index ab799303..923d9f56 100755 --- a/mtda-cli +++ b/mtda-cli @@ -452,6 +452,37 @@ class Application: client.monitor_remote(self.remote, None) return result + def system_cmd(self, args): + cmds = { + 'update': self.system_update + } + + return cmds[args.subcommand](args) + + def system_update(self, args=None): + result = 0 + client = self.agent + self.imgname = os.path.basename(args.image) + # TODO: there is currently no way back! + client.storage_to_sysupdate() + + try: + client.monitor_remote(self.remote, self.screen) + + client.system_update_image(args.image) + sys.stdout.write("\n") + sys.stdout.flush() + except Exception as e: + import traceback + traceback.print_exc() + msg = e.msg if hasattr(e, 'msg') else str(e) + print(f"\n'system update' failed! ({msg})", + file=sys.stderr) + result = 1 + finally: + client.monitor_remote(self.remote, None) + return result + def target_uptime(self): result = "" uptime = self.client().target_uptime() @@ -833,6 +864,25 @@ class Application: help="Path to image file" ) + cmd = self.system_cmd + p = subparsers.add_parser( + "system", + help="Interact with the mtda system", + ) + p.set_defaults(func=cmd) + subsub = p.add_subparsers(dest="subcommand") + subsub.required = True + s = subsub.add_parser( + "update", + help="Update the mtda system" + ) + s.add_argument( + "image", + metavar="image", + type=str, + help="Path to swu file" + ) + # subcommand: target cmd = self.target_cmd p = subparsers.add_parser( diff --git a/mtda/client.py b/mtda/client.py index 63f1e084..04147319 100644 --- a/mtda/client.py +++ b/mtda/client.py @@ -274,6 +274,21 @@ def parseBmap(self, bmap, bmap_path): return None return bmapDict + def system_update_image(self, path, callback=None): + blksz = self._agent.blksz + impl = self._impl + session = self._session + + # Get file handler from specified path + file = ImageFile.new(path, impl, session, blksz, callback) + self.storage_open(file.size) + try: + file.prepare(self._data, file.size) + file.copy() + file.flush() + finally: + self.storage_close() + def start(self): return self._agent.start() diff --git a/mtda/main.py b/mtda/main.py index b5e04a97..3f6e60c9 100644 --- a/mtda/main.py +++ b/mtda/main.py @@ -1041,6 +1041,20 @@ def storage_to_target(self, **kwargs): self.mtda.debug(3, f"main.storage_to_target(): {str(result)}") return result + @Pyro4.expose + def storage_to_sysupdate(self, **kwags): + # TODO: currently there is no way to go back! + from mtda.storage.swupdate import SWUpdate + from mtda.storage.writer import AsyncImageWriter + + # TODO: we need to overwrite the global storage object, + # as internal calls rely on mtda.storage_status() + self.storage = SWUpdate(self) + self._writer = AsyncImageWriter(self, self.storage) + # TODO: this is technically not true, but this value + # is checked all over the place + self._storage_event(CONSTS.STORAGE.ON_HOST) + @Pyro4.expose def storage_swap(self, **kwargs): self.mtda.debug(3, "main.storage_swap()") diff --git a/mtda/storage/helpers/swupdate_ipc.py b/mtda/storage/helpers/swupdate_ipc.py new file mode 100644 index 00000000..73aafeea --- /dev/null +++ b/mtda/storage/helpers/swupdate_ipc.py @@ -0,0 +1,162 @@ +# --------------------------------------------------------------------------- +# Helper class for images +# --------------------------------------------------------------------------- +# +# This software is a part of MTDA. +# Copyright (C) 2025 Siemens AG +# +# --------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# --------------------------------------------------------------------------- +# +# The type definitions need to match the upstream protocol definitions in +# https://github.com/sbabic/swupdate/blob/master/include/network_ipc.h + +import ctypes + +# Constants +IPC_MAGIC = 0x14052001 +SWUPDATE_API_VERSION = 0x1 + + +# Enums +class msgtype(ctypes.c_int): + REQ_INSTALL = 0 + ACK = 1 + NACK = 2 + GET_STATUS = 3 + POST_UPDATE = 4 + SWUPDATE_SUBPROCESS = 5 + SET_AES_KEY = 6 + SET_UPDATE_STATE = 7 + GET_UPDATE_STATE = 8 + REQ_INSTALL_EXT = 9 + SET_VERSIONS_RANGE = 10 + NOTIFY_STREAM = 11 + GET_HW_REVISION = 12 + SET_SWUPDATE_VARS = 13 + GET_SWUPDATE_VARS = 14 + + +class CMD_TYPE(ctypes.c_int): + CMD_ACTIVATION = 0 + CMD_CONFIG = 1 + CMD_ENABLE = 2 + CMD_GET_STATUS = 3 + CMD_SET_DOWNLOAD_URL = 4 + + +class run_type(ctypes.c_int): + RUN_DEFAULT = 0 + RUN_DRYRUN = 1 + RUN_INSTALL = 2 + + +# Structures +class sourcetype(ctypes.c_int): + SOURCE_UNKNOWN = 0 + SOURCE_FILE = 1 + SOURCE_NETWORK = 2 + SOURCE_USB = 3 + + +class swupdate_request(ctypes.Structure): + _fields_ = [ + ("apiversion", ctypes.c_uint), + ("source", sourcetype), + ("dry_run", run_type), + ("len", ctypes.c_size_t), + ("info", ctypes.c_char * 512), + ("software_set", ctypes.c_char * 256), + ("running_mode", ctypes.c_char * 256), + ("disable_store_swu", ctypes.c_bool) + ] + + +class status(ctypes.Structure): + _fields_ = [ + ("current", ctypes.c_int), + ("last_result", ctypes.c_int), + ("error", ctypes.c_int), + ("desc", ctypes.c_char * 2048) + ] + + +class notify(ctypes.Structure): + _fields_ = [ + ("status", ctypes.c_int), + ("error", ctypes.c_int), + ("level", ctypes.c_int), + ("msg", ctypes.c_char * 2048) + ] + + +class instmsg(ctypes.Structure): + _fields_ = [ + ("req", swupdate_request), + ("len", ctypes.c_uint), + ("buf", ctypes.c_char * 2048) + ] + + +class procmsg(ctypes.Structure): + _fields_ = [ + ("source", sourcetype), + ("cmd", ctypes.c_int), + ("timeout", ctypes.c_int), + ("len", ctypes.c_uint), + ("buf", ctypes.c_char * 2048) + ] + + +class aeskeymsg(ctypes.Structure): + _fields_ = [ + ("key_ascii", ctypes.c_char * 65), + ("ivt_ascii", ctypes.c_char * 33) + ] + + +class versions(ctypes.Structure): + _fields_ = [ + ("minimum_version", ctypes.c_char * 256), + ("maximum_version", ctypes.c_char * 256), + ("current_version", ctypes.c_char * 256), + ("update_type", ctypes.c_char * 256) + ] + + +class revisions(ctypes.Structure): + _fields_ = [ + ("boardname", ctypes.c_char * 256), + ("revision", ctypes.c_char * 256) + ] + + +class vars(ctypes.Structure): + _fields_ = [ + ("varnamespace", ctypes.c_char * 256), + ("varname", ctypes.c_char * 256), + ("varvalue", ctypes.c_char * 256) + ] + + +class msgdata(ctypes.Union): + _fields_ = [ + ("msg", ctypes.c_char * 128), + ("status", status), + ("notify", notify), + ("instmsg", instmsg), + ("procmsg", procmsg), + ("aeskeymsg", aeskeymsg), + ("versions", versions), + ("revisions", revisions), + ("vars", vars) + ] + + +class ipc_message(ctypes.Structure): + _fields_ = [ + ("magic", ctypes.c_int), + ("type", msgtype), + ("data", msgdata) + ] diff --git a/mtda/storage/swupdate.py b/mtda/storage/swupdate.py new file mode 100644 index 00000000..9ab62ccb --- /dev/null +++ b/mtda/storage/swupdate.py @@ -0,0 +1,73 @@ +# --------------------------------------------------------------------------- +# swupdate storage driver for MTDA +# --------------------------------------------------------------------------- +# +# This software is a part of MTDA. +# Copyright (C) 2025 Siemens +# +# --------------------------------------------------------------------------- +# SPDX-License-Identifier: MIT +# --------------------------------------------------------------------------- + +import mtda.constants as CONSTS +from mtda.storage.controller import StorageController +import mtda.storage.helpers.swupdate_ipc as IPC +import socket +import ctypes + + +class SWUpdate(StorageController): + def __init__(self, mtda): + self.mtda = mtda + self.writtenBytes = 0 + self._ipc_socket = None + + def open(self): + """ Open the shared storage device for I/O operations""" + self.mtda.debug(2, "swupdate open") + self.writtenBytes = 0 + + self._ipc_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + # TODO: should come from a constant + self._ipc_socket.connect("/var/run/swupdate/sockinstctrl") + self._perform_handshake() + return True + + def close(self): + self._ipc_socket.close() + return True + + def status(self): + return CONSTS.STORAGE.ON_HOST + + def tell(self): + return self.writtenBytes + + def write(self, data): + self._ipc_socket.sendall(data) + self.writtenBytes += len(data) + self.mtda.notify_write() + return len(data) + + def _perform_handshake(self): + sock = self._ipc_socket + sock.sendall(self._create_ipc_header_msg()) + response = sock.recv(ctypes.sizeof(IPC.ipc_message)) + ack = IPC.ipc_message.from_buffer_copy(response) + if ack.type.value != IPC.msgtype.ACK: + raise Exception("SWupdate error") + + def _create_ipc_header_msg(self): + # TODO: for testing we create a dryrun message + req = IPC.swupdate_request( + apiversion=IPC.SWUPDATE_API_VERSION, + disable_store_swu=True, + source=IPC.sourcetype.SOURCE_NETWORK, + dry_run=IPC.run_type.RUN_DRYRUN) + instmsg = IPC.instmsg(req=req) + msgdata = IPC.msgdata(instmsg=instmsg) + + return IPC.ipc_message( + magic=IPC.IPC_MAGIC, + type=IPC.msgtype.REQ_INSTALL, + data=msgdata)