Skip to content

Commit 9a0be3b

Browse files
committed
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 <felix.moessbauer@siemens.com>
1 parent 481cac6 commit 9a0be3b

File tree

5 files changed

+314
-0
lines changed

5 files changed

+314
-0
lines changed

mtda-cli

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,37 @@ class Application:
452452
client.monitor_remote(self.remote, None)
453453
return result
454454

455+
def system_cmd(self, args):
456+
cmds = {
457+
'update': self.system_update
458+
}
459+
460+
return cmds[args.subcommand](args)
461+
462+
def system_update(self, args=None):
463+
result = 0
464+
client = self.agent
465+
self.imgname = os.path.basename(args.image)
466+
# TODO: there is currently no way back!
467+
client.storage_to_sysupdate()
468+
469+
try:
470+
client.monitor_remote(self.remote, self.screen)
471+
472+
client.system_update_image(args.image)
473+
sys.stdout.write("\n")
474+
sys.stdout.flush()
475+
except Exception as e:
476+
import traceback
477+
traceback.print_exc()
478+
msg = e.msg if hasattr(e, 'msg') else str(e)
479+
print(f"\n'system update' failed! ({msg})",
480+
file=sys.stderr)
481+
result = 1
482+
finally:
483+
client.monitor_remote(self.remote, None)
484+
return result
485+
455486
def target_uptime(self):
456487
result = ""
457488
uptime = self.client().target_uptime()
@@ -833,6 +864,25 @@ class Application:
833864
help="Path to image file"
834865
)
835866

867+
cmd = self.system_cmd
868+
p = subparsers.add_parser(
869+
"system",
870+
help="Interact with the mtda system",
871+
)
872+
p.set_defaults(func=cmd)
873+
subsub = p.add_subparsers(dest="subcommand")
874+
subsub.required = True
875+
s = subsub.add_parser(
876+
"update",
877+
help="Update the mtda system"
878+
)
879+
s.add_argument(
880+
"image",
881+
metavar="image",
882+
type=str,
883+
help="Path to swu file"
884+
)
885+
836886
# subcommand: target
837887
cmd = self.target_cmd
838888
p = subparsers.add_parser(

mtda/client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,21 @@ def parseBmap(self, bmap, bmap_path):
274274
return None
275275
return bmapDict
276276

277+
def system_update_image(self, path, callback=None):
278+
blksz = self._agent.blksz
279+
impl = self._impl
280+
session = self._session
281+
282+
# Get file handler from specified path
283+
file = ImageFile.new(path, impl, session, blksz, callback)
284+
self.storage_open(file.size)
285+
try:
286+
file.prepare(self._data, file.size)
287+
file.copy()
288+
file.flush()
289+
finally:
290+
self.storage_close()
291+
277292
def start(self):
278293
return self._agent.start()
279294

mtda/main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,20 @@ def storage_to_target(self, **kwargs):
10411041
self.mtda.debug(3, f"main.storage_to_target(): {str(result)}")
10421042
return result
10431043

1044+
@Pyro4.expose
1045+
def storage_to_sysupdate(self, **kwags):
1046+
# TODO: currently there is no way to go back!
1047+
from mtda.storage.swupdate import SWUpdate
1048+
from mtda.storage.writer import AsyncImageWriter
1049+
1050+
# TODO: we need to overwrite the global storage object,
1051+
# as internal calls rely on mtda.storage_status()
1052+
self.storage = SWUpdate(self)
1053+
self._writer = AsyncImageWriter(self, self.storage)
1054+
# TODO: this is technically not true, but this value
1055+
# is checked all over the place
1056+
self._storage_event(CONSTS.STORAGE.ON_HOST)
1057+
10441058
@Pyro4.expose
10451059
def storage_swap(self, **kwargs):
10461060
self.mtda.debug(3, "main.storage_swap()")
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# ---------------------------------------------------------------------------
2+
# Helper class for images
3+
# ---------------------------------------------------------------------------
4+
#
5+
# This software is a part of MTDA.
6+
# Copyright (C) 2025 Siemens AG
7+
#
8+
# ---------------------------------------------------------------------------
9+
# SPDX-License-Identifier: MIT
10+
# ---------------------------------------------------------------------------
11+
#
12+
# The type definitions need to match the upstream protocol definitions in
13+
# https://github.com/sbabic/swupdate/blob/master/include/network_ipc.h
14+
15+
import ctypes
16+
17+
# Constants
18+
IPC_MAGIC = 0x14052001
19+
SWUPDATE_API_VERSION = 0x1
20+
21+
22+
# Enums
23+
class msgtype(ctypes.c_int):
24+
REQ_INSTALL = 0
25+
ACK = 1
26+
NACK = 2
27+
GET_STATUS = 3
28+
POST_UPDATE = 4
29+
SWUPDATE_SUBPROCESS = 5
30+
SET_AES_KEY = 6
31+
SET_UPDATE_STATE = 7
32+
GET_UPDATE_STATE = 8
33+
REQ_INSTALL_EXT = 9
34+
SET_VERSIONS_RANGE = 10
35+
NOTIFY_STREAM = 11
36+
GET_HW_REVISION = 12
37+
SET_SWUPDATE_VARS = 13
38+
GET_SWUPDATE_VARS = 14
39+
40+
41+
class CMD_TYPE(ctypes.c_int):
42+
CMD_ACTIVATION = 0
43+
CMD_CONFIG = 1
44+
CMD_ENABLE = 2
45+
CMD_GET_STATUS = 3
46+
CMD_SET_DOWNLOAD_URL = 4
47+
48+
49+
class run_type(ctypes.c_int):
50+
RUN_DEFAULT = 0
51+
RUN_DRYRUN = 1
52+
RUN_INSTALL = 2
53+
54+
55+
# Structures
56+
class sourcetype(ctypes.c_int):
57+
SOURCE_UNKNOWN = 0
58+
SOURCE_FILE = 1
59+
SOURCE_NETWORK = 2
60+
SOURCE_USB = 3
61+
62+
63+
class swupdate_request(ctypes.Structure):
64+
_fields_ = [
65+
("apiversion", ctypes.c_uint),
66+
("source", sourcetype),
67+
("dry_run", run_type),
68+
("len", ctypes.c_size_t),
69+
("info", ctypes.c_char * 512),
70+
("software_set", ctypes.c_char * 256),
71+
("running_mode", ctypes.c_char * 256),
72+
("disable_store_swu", ctypes.c_bool)
73+
]
74+
75+
76+
class status(ctypes.Structure):
77+
_fields_ = [
78+
("current", ctypes.c_int),
79+
("last_result", ctypes.c_int),
80+
("error", ctypes.c_int),
81+
("desc", ctypes.c_char * 2048)
82+
]
83+
84+
85+
class notify(ctypes.Structure):
86+
_fields_ = [
87+
("status", ctypes.c_int),
88+
("error", ctypes.c_int),
89+
("level", ctypes.c_int),
90+
("msg", ctypes.c_char * 2048)
91+
]
92+
93+
94+
class instmsg(ctypes.Structure):
95+
_fields_ = [
96+
("req", swupdate_request),
97+
("len", ctypes.c_uint),
98+
("buf", ctypes.c_char * 2048)
99+
]
100+
101+
102+
class procmsg(ctypes.Structure):
103+
_fields_ = [
104+
("source", sourcetype),
105+
("cmd", ctypes.c_int),
106+
("timeout", ctypes.c_int),
107+
("len", ctypes.c_uint),
108+
("buf", ctypes.c_char * 2048)
109+
]
110+
111+
112+
class aeskeymsg(ctypes.Structure):
113+
_fields_ = [
114+
("key_ascii", ctypes.c_char * 65),
115+
("ivt_ascii", ctypes.c_char * 33)
116+
]
117+
118+
119+
class versions(ctypes.Structure):
120+
_fields_ = [
121+
("minimum_version", ctypes.c_char * 256),
122+
("maximum_version", ctypes.c_char * 256),
123+
("current_version", ctypes.c_char * 256),
124+
("update_type", ctypes.c_char * 256)
125+
]
126+
127+
128+
class revisions(ctypes.Structure):
129+
_fields_ = [
130+
("boardname", ctypes.c_char * 256),
131+
("revision", ctypes.c_char * 256)
132+
]
133+
134+
135+
class vars(ctypes.Structure):
136+
_fields_ = [
137+
("varnamespace", ctypes.c_char * 256),
138+
("varname", ctypes.c_char * 256),
139+
("varvalue", ctypes.c_char * 256)
140+
]
141+
142+
143+
class msgdata(ctypes.Union):
144+
_fields_ = [
145+
("msg", ctypes.c_char * 128),
146+
("status", status),
147+
("notify", notify),
148+
("instmsg", instmsg),
149+
("procmsg", procmsg),
150+
("aeskeymsg", aeskeymsg),
151+
("versions", versions),
152+
("revisions", revisions),
153+
("vars", vars)
154+
]
155+
156+
157+
class ipc_message(ctypes.Structure):
158+
_fields_ = [
159+
("magic", ctypes.c_int),
160+
("type", msgtype),
161+
("data", msgdata)
162+
]

mtda/storage/swupdate.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# ---------------------------------------------------------------------------
2+
# swupdate storage driver for MTDA
3+
# ---------------------------------------------------------------------------
4+
#
5+
# This software is a part of MTDA.
6+
# Copyright (C) 2025 Siemens
7+
#
8+
# ---------------------------------------------------------------------------
9+
# SPDX-License-Identifier: MIT
10+
# ---------------------------------------------------------------------------
11+
12+
import mtda.constants as CONSTS
13+
from mtda.storage.controller import StorageController
14+
import mtda.storage.helpers.swupdate_ipc as IPC
15+
import socket
16+
import ctypes
17+
18+
19+
class SWUpdate(StorageController):
20+
def __init__(self, mtda):
21+
self.mtda = mtda
22+
self.writtenBytes = 0
23+
self._ipc_socket = None
24+
25+
def open(self):
26+
""" Open the shared storage device for I/O operations"""
27+
self.mtda.debug(2, "swupdate open")
28+
self.writtenBytes = 0
29+
30+
self._ipc_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
31+
# TODO: should come from a constant
32+
self._ipc_socket.connect("/var/run/swupdate/sockinstctrl")
33+
self._perform_handshake()
34+
return True
35+
36+
def close(self):
37+
self._ipc_socket.close()
38+
return True
39+
40+
def status(self):
41+
return CONSTS.STORAGE.ON_HOST
42+
43+
def tell(self):
44+
return self.writtenBytes
45+
46+
def write(self, data):
47+
self._ipc_socket.sendall(data)
48+
self.writtenBytes += len(data)
49+
self.mtda.notify_write()
50+
return len(data)
51+
52+
def _perform_handshake(self):
53+
sock = self._ipc_socket
54+
sock.sendall(self._create_ipc_header_msg())
55+
response = sock.recv(ctypes.sizeof(IPC.ipc_message))
56+
ack = IPC.ipc_message.from_buffer_copy(response)
57+
if ack.type.value != IPC.msgtype.ACK:
58+
raise Exception("SWupdate error")
59+
60+
def _create_ipc_header_msg(self):
61+
# TODO: for testing we create a dryrun message
62+
req = IPC.swupdate_request(
63+
apiversion=IPC.SWUPDATE_API_VERSION,
64+
disable_store_swu=True,
65+
source=IPC.sourcetype.SOURCE_NETWORK,
66+
dry_run=IPC.run_type.RUN_DRYRUN)
67+
instmsg = IPC.instmsg(req=req)
68+
msgdata = IPC.msgdata(instmsg=instmsg)
69+
70+
return IPC.ipc_message(
71+
magic=IPC.IPC_MAGIC,
72+
type=IPC.msgtype.REQ_INSTALL,
73+
data=msgdata)

0 commit comments

Comments
 (0)