From 7f572dc94114f1fcb71c4e5be82e2268335624ea Mon Sep 17 00:00:00 2001 From: Maarten De Decker Date: Thu, 9 Jul 2026 22:41:22 +0200 Subject: [PATCH] ci: EasyMesh R1: flash Freedom via FEAT-29 update_prpl Align the Easymesh R1 smoke test with the DUT flashing procedure from FEAT-29. This commit aligns the DUT deploy scripts from the prplMesh repository. Reference: PPM-4139 Signed-off-by: Maarten De Decker --- .gitlab/prplmesh.yml | 6 +- .gitlab/scripts/prplMesh/deploy_firmware.py | 24 ++++++- .gitlab/scripts/prplMesh/device/freedom.py | 67 ++++++++++++++----- .gitlab/scripts/prplMesh/device/generic.py | 33 ++++++++- .gitlab/scripts/prplMesh/device/get_device.py | 23 ++++--- .gitlab/scripts/prplMesh/device/logs.py | 28 ++++---- .gitlab/scripts/prplMesh/device/prplos.py | 27 +++----- .gitlab/scripts/prplMesh/device/serial.py | 12 +++- .gitlab/scripts/prplMesh/device/urx_osp.py | 63 +++++++++++------ 9 files changed, 192 insertions(+), 91 deletions(-) diff --git a/.gitlab/prplmesh.yml b/.gitlab/prplmesh.yml index a77198cc6f..d45978e5c8 100644 --- a/.gitlab/prplmesh.yml +++ b/.gitlab/prplmesh.yml @@ -13,8 +13,8 @@ EasyMesh-Smoke-test-R1-Freedom: EASYMESH_TESTS=$EASYMESH_TESTS_SIMPLE echo -e "${TXT_RED}Executing Easymesh tests: ${TXT_PRPL}$EASYMESH_TESTS${TXT_CLEAR}" - mkdir -p build/freedom/ - - cp bin/targets/ipq95xx/generic/prplos-ipq95xx-generic-prpl_freedom-squashfs-sysupgrade.bin build/freedom/prplos-ipq95xx-generic-prpl_freedom-squashfs-sysupgrade.bin - - .gitlab/scripts/prplMesh/deploy_firmware.py --device "$RUN_ON_DEVICE" --full --target-name "$RUN_ON_DEVICE" --image "prplos-ipq95xx-generic-prpl_freedom-squashfs-sysupgrade.bin" --whm --configuration ".gitlab/scripts/prplMesh/dut-configuration/R1/freedom.sh" + - cp bin/targets/ipq95xx/generic/{prplos-ipq95xx-generic-prpl_freedom-update_script.itb,prplos-ipq95xx-generic-prpl_freedom-kernel.itb,prplos-ipq95xx-generic-prpl_freedom-rootfs.itb} build/freedom/ + - .gitlab/scripts/prplMesh/deploy_firmware.py --device "$RUN_ON_DEVICE" --full --target-name "$RUN_ON_DEVICE" --image "prplos-ipq95xx-generic-prpl_freedom-kernel.itb" --rootfs "prplos-ipq95xx-generic-prpl_freedom-rootfs.itb" --whm --configuration ".gitlab/scripts/prplMesh/dut-configuration/R1/freedom.sh" - .gitlab/scripts/prplMesh/git-clean-reset.sh ~/easymesh_cert "$(<".gitlab/scripts/prplMesh/easymesh_cert_version")" - ~/easymesh_cert/run_test_file.py --verbose --log-folder logs --easymesh-revision "$EASYMESH_REVISION" --device-under-test $RUN_ON_DEVICE "$EASYMESH_TESTS" after_script: @@ -33,6 +33,6 @@ EasyMesh-Smoke-test-R1-Freedom: - logs when: always tags: - - dut-prplmesh-freedom + - dut-prplmesh-freedom-SMD - prplmesh-R1 diff --git a/.gitlab/scripts/prplMesh/deploy_firmware.py b/.gitlab/scripts/prplMesh/deploy_firmware.py index 05cd8dcd52..8e2b48af49 100755 --- a/.gitlab/scripts/prplMesh/deploy_firmware.py +++ b/.gitlab/scripts/prplMesh/deploy_firmware.py @@ -23,7 +23,7 @@ def main(): or using sysupgrade, depending on the target device.""") parser.add_argument('-d', '--device', help="""Device to upgrade. Currently supported targets are: - turris-omnia haze urx_osp urx_ospv2""", required=True) + freedom urx_osp urx_ospv2 mozart""", required=True) parser.add_argument( '-t', '--target-name', @@ -35,6 +35,22 @@ def main(): help="Name of the image to use for the upgrade (should exist in the artifacts folder).", required=True) + parser.add_argument( + '-s', + '--rootfs', + help="Name of the rootfs FIT image to use for the upgrade " + "(should exist in the artifacts folder).", required=False) + + parser.add_argument( + '-a', + '--ipaddr', + help="Bootloader IP address of the DUT", required=False) + + parser.add_argument( + '-v', + '--serverip', + help="Bootloader IP address of the TFTP server", required=False) + parser.add_argument( '-f', '--full', @@ -54,7 +70,11 @@ def main(): args = parser.parse_args() - dev = device_from_name(args.device, args.target_name, args.image) + if args.device == "freedom" and args.rootfs is None: + raise ValueError("--rootfs is required when upgrading Freedom") + + dev = device_from_name(args.device, args.target_name, args.image, args.rootfs, + args.ipaddr, args.serverip) def do_upgrade(dev): try: diff --git a/.gitlab/scripts/prplMesh/device/freedom.py b/.gitlab/scripts/prplMesh/device/freedom.py index 09a863c5bf..ca603b7f2f 100644 --- a/.gitlab/scripts/prplMesh/device/freedom.py +++ b/.gitlab/scripts/prplMesh/device/freedom.py @@ -25,6 +25,15 @@ class Freedom(GenericPrplOS): bootloader_prompt = r"IPQ9574# " """The u-boot prompt on the target.""" + update_script = "prplos-ipq95xx-generic-prpl_freedom-update_script.itb" + """The name of the u-boot update script file""" + + default_ipaddr = "192.168.250.150" + """The bootloader IP address of the DUT, used when none is given.""" + + default_serverip = "192.168.250.199" + """The bootloader IP address of the TFTP server, used when none is given.""" + def upgrade_from_u_boot(self, shell: pexpect.fdpexpect.fdspawn): """Upgrade from u-boot and remove the overlay. @@ -37,29 +46,55 @@ class Freedom(GenericPrplOS): shell.sendline("") shell.expect(self.bootloader_prompt) # Give the ethernet interfaces some time to initialize: - time.sleep(10) + time.sleep(5) - shell.sendline(f"tftpboot 0x44000000 {self.image}") - shell.sendline("") - shell.expect("Loading: ") + shell.sendline(f"setenv serverip {self.serverip}") + shell.expect(self.bootloader_prompt) + shell.sendline(f"setenv ipaddr {self.ipaddr}") + shell.expect(self.bootloader_prompt) + shell.sendline("setenv loadaddr 0x50000000") + shell.expect(self.bootloader_prompt) + time.sleep(5) + + shell.sendline(f"tftpboot ${{loadaddr}} {self.update_script}") shell.expect("done") shell.expect(self.bootloader_prompt) - - shell.sendline("setenv untar_addr_kernel; setenv untar_addr_root") + shell.sendline("source ${loadaddr}:update-script") + shell.expect("run update_prpl") + shell.expect("========================================================") shell.expect(self.bootloader_prompt) - shell.sendline("untar 0x$fileaddr 0x$filesize kernel root") - shell.expect("filename: sysupgrade-prpl_freedom/CONTROL") - shell.expect("filename: sysupgrade-prpl_freedom/kernel") - shell.expect("filename: sysupgrade-prpl_freedom/root") + shell.sendline(f"setenv img_kernel {self.image}") + shell.expect(self.bootloader_prompt) + if self.rootfs is not None: + shell.sendline(f"setenv img_rootfs {self.rootfs}") + else: + shell.sendline(f"setenv img_rootfs") shell.expect(self.bootloader_prompt) - shell.sendline("if test -n $untar_addr_kernel; then flash '0:HLOS' 0x$untar_addr_kernel 0x$untar_size_kernel; else echo 'kernel not found'; fi") # noqa E501 - shell.expect("blocks erased: OK", timeout=10) - shell.expect("blocks written: OK", timeout=10) + shell.sendline("setenv img_uboot") + shell.expect(self.bootloader_prompt) + shell.sendline("setenv img_xbl") + shell.expect(self.bootloader_prompt) + shell.sendline("setenv img_smd") + shell.expect(self.bootloader_prompt) + shell.sendline("setenv update_rescue_bank yes") shell.expect(self.bootloader_prompt) - shell.sendline("if test -n $untar_addr_root; then flash 'rootfs' 0x$untar_addr_root 0x$untar_size_root; else echo 'rootfs not found'; fi") # noqa E501 - shell.expect("blocks erased: OK", timeout=10) - shell.expect("blocks written: OK", timeout=10) + shell.sendline("mmc dev 0") shell.expect(self.bootloader_prompt) + shell.sendline("mmc erase 0x534022 0x40000") + shell.expect("blocks erased: OK") + shell.expect(self.bootloader_prompt) + + shell.sendline("run update_prpl") + shell.expect("kernel.itb written successfully to kernel-active", timeout=45) + shell.expect("kernel.itb written successfully to kernel-inactive", timeout=45) + shell.expect("rootfs.itb written successfully to rootfs-active", timeout=45) + shell.expect("rootfs.itb written successfully to rootfs-inactive", timeout=45) + shell.expect("img_uboot not set") + shell.expect("img_xbl not set") + shell.expect("img_smd not set") + shell.expect("Done.") + shell.expect(self.bootloader_prompt) + print("Flashing completed successfully") diff --git a/.gitlab/scripts/prplMesh/device/generic.py b/.gitlab/scripts/prplMesh/device/generic.py index 054750c37b..af4178cf60 100644 --- a/.gitlab/scripts/prplMesh/device/generic.py +++ b/.gitlab/scripts/prplMesh/device/generic.py @@ -7,6 +7,7 @@ # Standard library import difflib +import ipaddress import os import re import time @@ -41,11 +42,11 @@ class GenericDevice(): baudrate = 115200 """The baudrate of the serial connection to the device.""" - initialization_time = 200 + initialization_time = 180 """The time (in seconds) the device needs to initialize when it boots for the first time after flashing a new image.""" - configuration_initialization_time = 30 + configuration_initialization_time = 5 """The time (in seconds) the device needs to initialize after configuring it""" bootloader_prompt = "=> " @@ -63,8 +64,21 @@ class GenericDevice(): tftp_dir = "/srv/tftp" """The root directory of the tftp server. OS images will be copied there.""" + update_script = None + """The name of the u-boot update script file""" + + rootfs = None + """The name of the rootfs image to use""" + + uboot_ipaddr = "192.165.100.155" + """The default bootloader IP address of the DUT""" + + uboot_serverip = "192.165.100.199" + """The default bootloader IP address of the TFTP server""" + def __init__(self, device: str, name: str, image: Union[str, None] = None, - username: str = "root"): + rootfs: Union[str, None] = None, ipaddr: Union[str, None] = None, + serverip: Union[str, None] = None, username: str = "root"): """ Parameters @@ -75,12 +89,25 @@ class GenericDevice(): The name of the device (it should ne reachable through ssh without a password). image: Union[str, None] The name of the image that can be used to upgrade the device. + rootfs: Union[str, None], optional + The name of the rootfs FIT image that can be used to upgrade the device. + ipaddr: Union[str, None], optional + The bootloader IP address of the DUT (defaults to `default_ipaddr`). + serverip: Union[str, None], optional + The bootloader IP address of the TFTP server (defaults to `default_serverip`). username: str, optional The username to use when connecting to the device over SSH. """ self.device = device self.name = name self.image = image + self.rootfs = rootfs + if ipaddr is not None: + ipaddress.ip_address(ipaddr) + if serverip is not None: + ipaddress.ip_address(serverip) + self.ipaddr = ipaddr if ipaddr is not None else self.uboot_ipaddr + self.serverip = serverip if serverip is not None else self.uboot_serverip self.username = username self.rootdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../../..") diff --git a/.gitlab/scripts/prplMesh/device/get_device.py b/.gitlab/scripts/prplMesh/device/get_device.py index 81471a636c..936a509d4c 100644 --- a/.gitlab/scripts/prplMesh/device/get_device.py +++ b/.gitlab/scripts/prplMesh/device/get_device.py @@ -12,14 +12,14 @@ from typing import Union # Third party import device.generic import device.prplos -import device.turris_prplos import device.turris_rdk_b -import device.haze import device.urx_osp import device.freedom -def device_from_name(name: str, target_name: str, image: Union[str, None] = None +def device_from_name(name: str, target_name: str, image: Union[str, None] = None, + rootfs: Union[str, None] = None, ipaddr: Union[str, None] = None, + serverip: Union[str, None] = None ) -> device.generic.GenericDevice: """Construct a device based on its name and type. @@ -31,17 +31,22 @@ def device_from_name(name: str, target_name: str, image: Union[str, None] = None The name of the target. image: image: Union[str, None] The name of the image (optional, defaults to None). + rootfs: rootfs: Union[str, None] + The name of the rootfs FIT image (optional, defaults to None). + ipaddr: Union[str, None] + The bootloader IP address of the DUT (optional, defaults to None). + serverip: Union[str, None] + The bootloader IP address of the TFTP server (optional, defaults to None). """ if name == "turris-omnia-rdk": dev = device.turris_rdk_b.TurrisRdkb(name, target_name, image) - elif name == "turris-omnia": - dev = device.turris_prplos.TurrisPrplOS(name, target_name, image) - elif name == "haze": - dev = device.haze.Haze(name, target_name, image) elif name == "urx_osp" or name == "urx_ospv2": - dev = device.urx_osp.URXOSP(name, target_name, image) + dev = device.urx_osp.URXOSP(name, target_name, image, ipaddr=ipaddr, serverip=serverip) elif name == "freedom": - dev = device.freedom.Freedom(name, target_name, image) + dev = device.freedom.Freedom(name, target_name, image, rootfs, ipaddr=ipaddr, + serverip=serverip) + elif name == "mozart": + dev = device.mozart.Mozart(name, target_name, image) else: # if no device matched, try the generic prplOS (sysupgrade) print("No specific device matched, using GenericPrplOS.") diff --git a/.gitlab/scripts/prplMesh/device/logs.py b/.gitlab/scripts/prplMesh/device/logs.py index f765f18078..1dabcec9cd 100644 --- a/.gitlab/scripts/prplMesh/device/logs.py +++ b/.gitlab/scripts/prplMesh/device/logs.py @@ -27,16 +27,7 @@ LOG_COMMANDS = \ LogCommand('ubus call IP _get \'{ "depth": "10" }\'', "dm-IP.txt"), LogCommand('ubus call Bridging _get \'{ "depth": "10" }\'', "dm-Bridge.txt"), LogCommand('ubus call Ethernet _get \'{ "depth": "10" }\'', "dm-Ethernet.txt"), - LogCommand('ubus call DHCPv4 _get \'{ "depth": "10" }\'', "dm-DHCPv4.txt"), - LogCommand('ubus call DHCPv6 _get \'{ "depth": "10" }\'', "dm-DHCPv6.txt"), - LogCommand('ubus call Firewall _get \'{ "depth": "10" }\'', "dm-Firewall.txt"), - LogCommand('ubus call WiFi _get \'{ "depth": "10" }\'', "dm-WiFi.txt"), - LogCommand('logread', "syslog.txt"), LogCommand('iptables -L', "iptables-filter.txt"), - LogCommand('find /etc/config/tr181-bridging/ -type f -print -exec cat {} \';\'', - "config-tr181-bridging.txt"), - LogCommand('find /etc/amx/tr181-bridging/ -type f -print -exec cat {} \';\'', - "amx-tr181-bridging.txt"), ] @@ -72,18 +63,23 @@ def capture_logs(device: GenericDevice, path: str): with open(f"{path}/all-console-{timestamp}.txt", "wb") as logfile: with SerialDevice(device.baudrate, device.name, device.serial_prompt, expect_prompt_on_connect=False, logfile=logfile) as shell: + shell.logfile = open(f"{path}/ci-expect-debug.txt", "wb") shell.sendline("") time.sleep(1) + shell.flush() # Interrupt any running command: shell.send("\003") shell.expect(device.serial_prompt) # There might have been two prompts, if no command had to be interrupted: shell.expect([device.serial_prompt, pexpect.TIMEOUT]) - for log in LOG_COMMANDS: - shell.sendline(log.cmd) - shell.expect(log.cmd) - shell.expect(device.serial_prompt) - cmd_output = shell.before.decode("utf-8") - with open(f"{path}/{log.filename}", "w", encoding="utf-8") as output_file: - output_file.writelines(cmd_output) + try: + for log in LOG_COMMANDS: + print(f"send command {log.cmd}") + shell.sendline(log.cmd) + shell.expect(device.serial_prompt) + cmd_output = shell.before.decode("utf-8", errors="replace") + with open(f"{path}/{log.filename}", "w", encoding="utf-8") as output_file: + output_file.writelines(cmd_output) + except pexpect.exceptions.TIMEOUT: + print(f"Warning: I/O error hitted, refer to ci-expect-debug.txt") print("Done") diff --git a/.gitlab/scripts/prplMesh/device/prplos.py b/.gitlab/scripts/prplMesh/device/prplos.py index ad9aaf5e0a..d4f38cc833 100644 --- a/.gitlab/scripts/prplMesh/device/prplos.py +++ b/.gitlab/scripts/prplMesh/device/prplos.py @@ -41,24 +41,6 @@ class GenericPrplOS(GenericDevice): with open(os.path.join(self.artifacts_dir, "prplwrt-version")) as version_file: return version_file.read().splitlines() - def read_remote_prplos_version(self) -> List[str]: - """Read prplos build.version on a remote device - - Returns - ------- - List[str] - The content of the build.version file on the device as a list of strings. - """ - version = None - with pexpect.pxssh.pxssh(logfile=sys.stdout.buffer) as shell: - shell.login(self.name, self.username) - shell.sendline("cat /etc/build.version") - shell.expect("cat /etc/build.version") - shell.prompt() - # remove the first (empty) line: - version = shell.before.decode().splitlines()[1:] - return version - def read_remote_version(self) -> List[str]: """Read prplwrt-version on a remote device @@ -89,7 +71,14 @@ class GenericPrplOS(GenericDevice): + "Please make sure you have an appropriate udev rule for it.") print("Copying image '{}' to '{}'".format(self.image, self.tftp_dir)) shutil.copy(os.path.join(self.artifacts_dir, self.image), self.tftp_dir) - print("Image copied to {}.".format(self.tftp_dir)) + if self.update_script is not None: + print("Copying U-Boot update script '{}' to '{}'".format( + self.update_script, self.tftp_dir)) + shutil.copy(os.path.join(self.artifacts_dir, self.update_script), self.tftp_dir) + if self.rootfs is not None: + print("Copying rootfs '{}' to '{}'".format(self.rootfs, self.tftp_dir)) + shutil.copy(os.path.join(self.artifacts_dir, self.rootfs), self.tftp_dir) + print("Image(s) copied to {}.".format(self.tftp_dir)) with SerialDevice(self.baudrate, self.name, self.serial_prompt, expect_prompt_on_connect=False) as shell: self.reboot(self.check_serial_type(), diff --git a/.gitlab/scripts/prplMesh/device/serial.py b/.gitlab/scripts/prplMesh/device/serial.py index 8b9c659ea4..dbf70c6dad 100644 --- a/.gitlab/scripts/prplMesh/device/serial.py +++ b/.gitlab/scripts/prplMesh/device/serial.py @@ -62,7 +62,9 @@ class SerialDevice(pexpect.fdpexpect.fdspawn): + "Please make sure you have an appropriate udev rule for it.") if self.serial is not None: raise ValueError("Serial already connected!") - self.serial = serial.Serial(self.serial_path, self.baudrate, xonxoff=True) + """Setting xonxoff (software flow control) + will not work for the serial connection to Freedom in R6""" + self.serial = serial.Serial(self.serial_path, self.baudrate) self.serial.flushInput() super().__init__(self.serial, logfile=self.logfile) @@ -104,3 +106,11 @@ class SerialDevice(pexpect.fdpexpect.fdspawn): for char in s: self.send(char) time.sleep(self.send_delay / 1000.) + + def flush(self): + """Flush the serial read buffer""" + if self.serial.is_open: + if hasattr(self.serial, "reset_input_buffer"): + self.serial.reset_input_buffer() + else: + self.serial.flushInput() diff --git a/.gitlab/scripts/prplMesh/device/urx_osp.py b/.gitlab/scripts/prplMesh/device/urx_osp.py index cfb2203bb2..7ad2200962 100644 --- a/.gitlab/scripts/prplMesh/device/urx_osp.py +++ b/.gitlab/scripts/prplMesh/device/urx_osp.py @@ -46,28 +46,47 @@ class URXOSP(GenericPrplOS): # Give the ethernet interfaces some time to initialize: time.sleep(5) - # Changes the default image name to upgrade - # shell.sendline(f"setenv fullimage {self.image}") - # shell.sendline("") - # shell.sendline("saveenv") - # shell.expect("OK") - - shell.sendline("run update_fullimage") - shell.expect("done", timeout=30) - # Image transfer successful - shell.expect("Found device tree image", timeout=15) - shell.expect("Saving Environment to MMC", timeout=15) - shell.expect("OK", timeout=15) - # Image written to MMC - shell.sendline("") + shell.sendline(f"setenv serverip {self.serverip}") + shell.expect(self.bootloader_prompt) + shell.sendline(f"setenv ipaddr {self.ipaddr}") + shell.expect(self.bootloader_prompt) + shell.sendline("setenv loadaddr 0x8200000") shell.expect(self.bootloader_prompt) - - time.sleep(10) - shell.sendline( - "mmc erase ${overlay_container_a_block_start} ${overlay_container_a_block_size}") - shell.expect("blocks erased: OK", timeout=15) time.sleep(5) - shell.sendline( - "mmc erase ${overlay_container_a_block_start} ${overlay_container_a_block_size}") - shell.expect("blocks erased: OK", timeout=15) + + shell.sendline(f"tftpboot ${{loadaddr}} {self.update_script}") + shell.expect("done") shell.expect(self.bootloader_prompt) + shell.sendline("source ${loadaddr}:update-script") + shell.expect("run update_prpl") + shell.expect("========================================================") + shell.expect(self.bootloader_prompt) + + shell.sendline(f"setenv img_kernel {self.image}") + shell.expect(self.bootloader_prompt) + shell.sendline(f"setenv img_rootfs") + shell.expect(self.bootloader_prompt) + shell.sendline("setenv img_uboot") + shell.expect(self.bootloader_prompt) + shell.sendline("setenv img_rbe") + shell.expect(self.bootloader_prompt) + shell.sendline("setenv img_smd") + shell.expect(self.bootloader_prompt) + shell.sendline("setenv update_rescue_bank yes") + shell.expect(self.bootloader_prompt) + + shell.sendline("mmc dev 0") + shell.expect(self.bootloader_prompt) + shell.sendline("mmc erase 0x86c00 0x10000") + shell.expect("blocks erased: OK") + shell.expect(self.bootloader_prompt) + + shell.sendline("run update_prpl") + shell.expect("image.ext4 written successfully to kernel-active.", timeout=45) + shell.expect("image.ext4 written successfully to kernel-inactive", timeout=45) + shell.expect("img_uboot not set") + shell.expect("img_rbe not set") + shell.expect("img_smd not set") + shell.expect("Done.") + shell.expect(self.bootloader_prompt) + print("Flashing completed successfully")