From 44aed2fb352b08737c467d6fe11f371146e1e97d Mon Sep 17 00:00:00 2001 From: Manjunatha D Date: Sun, 26 Apr 2026 18:30:15 +0000 Subject: [PATCH] feat: add block device read/write check and ethtool test scripts in SR - add read_write_check_blk_devices.py for block device validation - add ethtool-test.py for ethernet interface testing - add required packages and configurations for SR image - integrate both scripts in Systemready band image flow - fix bashate errors - update file paths and documents to reflect the changes Signed-off-by: Manjunatha D Change-Id: I71b471bd31216a0fb2897f62fa0192a3f704dfe2 --- .../build-scripts/build-buildroot.sh | 8 + SystemReady-band/build-scripts/build-linux.sh | 5 - .../Yocto/build-scripts/get_source.sh | 12 +- .../files/read_write_check_blk_devices.py | 346 ------------ common/config/buildroot_defconfig | 20 + common/config/srband_defconfig | 42 +- common/config/system_config.txt | 3 + .../linux_scripts}/ethtool-test.py | 293 ++++++---- common/linux_scripts/init.sh | 60 +- common/linux_scripts/linux_dump.sh | 115 ++++ common/linux_scripts/linux_init.sh | 228 ++++++++ .../read_write_check_blk_devices.py | 524 ++++++++++++++++++ common/log_parser/main_log_parser.sh | 12 +- common/log_parser/test_categoryDT.json | 2 +- docs/ethtool_test_guide.md | 4 +- 15 files changed, 1146 insertions(+), 528 deletions(-) delete mode 100644 SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/install-files/files/read_write_check_blk_devices.py rename {SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/install-files/files => common/linux_scripts}/ethtool-test.py (74%) create mode 100644 common/linux_scripts/linux_dump.sh create mode 100644 common/linux_scripts/linux_init.sh create mode 100644 common/linux_scripts/read_write_check_blk_devices.py diff --git a/SystemReady-band/build-scripts/build-buildroot.sh b/SystemReady-band/build-scripts/build-buildroot.sh index 3e955dfe..7909dfc4 100755 --- a/SystemReady-band/build-scripts/build-buildroot.sh +++ b/SystemReady-band/build-scripts/build-buildroot.sh @@ -74,6 +74,14 @@ do_build () chmod +x root_fs_overlay/usr/bin/secure_init.sh cp $TOP_DIR/ramdisk/device_driver_sr.sh root_fs_overlay/usr/bin/ chmod +x root_fs_overlay/usr/bin/device_driver_sr.sh + cp $TOP_DIR/ramdisk/linux_init.sh root_fs_overlay/usr/bin/ + chmod +x root_fs_overlay/usr/bin/linux_init.sh + cp $TOP_DIR/ramdisk/linux_dump.sh root_fs_overlay/usr/bin/ + chmod +x root_fs_overlay/usr/bin/linux_dump.sh + cp $TOP_DIR/ramdisk/ethtool-test.py root_fs_overlay/usr/bin/ + chmod +x root_fs_overlay/usr/bin/ethtool-test.py + cp $TOP_DIR/ramdisk/read_write_check_blk_devices.py root_fs_overlay/usr/bin/ + chmod +x root_fs_overlay/usr/bin/read_write_check_blk_devices.py cp $TOP_DIR/ramdisk/bsa.sh root_fs_overlay/usr/bin/ chmod +x root_fs_overlay/usr/bin/bsa.sh cp $TOP_DIR/ramdisk/sbsa.sh root_fs_overlay/usr/bin/ diff --git a/SystemReady-band/build-scripts/build-linux.sh b/SystemReady-band/build-scripts/build-linux.sh index 6e6762d2..625d3ffa 100755 --- a/SystemReady-band/build-scripts/build-linux.sh +++ b/SystemReady-band/build-scripts/build-linux.sh @@ -77,11 +77,6 @@ do_build () sed -i 's/# CONFIG_TEE is not set/CONFIG_TEE=y/g' $LINUX_OUT_DIR/.config sed -i 's/# CONFIG_OPTEE is not set/CONFIG_OPTEE=y/g' $LINUX_OUT_DIR/.config sed -i 's/# CONFIG_ARM_PSCI_CHECKER is not set/CONFIG_ARM_PSCI_CHECKER=y/g' $LINUX_OUT_DIR/.config - #Configurations to enable rshim support - echo "CONFIG_MLXBF_TMFIFO=y" >> $LINUX_OUT_DIR/.config - #Configurations to increase serial ports - echo "CONFIG_SERIAL_8250_NR_UARTS=32" >> $LINUX_OUT_DIR/.config - echo "CONFIG_SERIAL_8250_RUNTIME_UARTS=32" >> $LINUX_OUT_DIR/.config cat $SRBAND_DEFCONFIG >> $LINUX_OUT_DIR/.config if [[ $arch = "aarch64" ]]; then echo "arm64 machine" diff --git a/SystemReady-devicetree-band/Yocto/build-scripts/get_source.sh b/SystemReady-devicetree-band/Yocto/build-scripts/get_source.sh index 0a6ea486..60254efe 100755 --- a/SystemReady-devicetree-band/Yocto/build-scripts/get_source.sh +++ b/SystemReady-devicetree-band/Yocto/build-scripts/get_source.sh @@ -42,10 +42,10 @@ else fi sudo apt install git curl mtools gdisk gcc liblz4-tool zstd \ - openssl automake autotools-dev libtool bison flex \ - bc uuid-dev python3 libglib2.0-dev libssl-dev autopoint \ - make gcc g++ gnu-efi libfile-slurp-perl help2man \ - python3-pip chrpath diffstat lz4 cpio gawk wget efitools -y + openssl automake autotools-dev libtool bison flex \ + bc uuid-dev python3 libglib2.0-dev libssl-dev autopoint \ + make gcc g++ gnu-efi libfile-slurp-perl help2man \ + python3-pip chrpath diffstat lz4 cpio gawk wget efitools -y sudo pip3 install kas @@ -143,7 +143,7 @@ copy_recipes() echo "SystemReady DT ACS" >> "$SYSTEMREADY_COMMIT_LOG" echo " URL(systemready-acs) = $(git remote get-url origin)" >> "$SYSTEMREADY_COMMIT_LOG" - echo " commit(systemready-acs) = $(git rev-parse HEAD)" >> "$SYSTEMREADY_COMMIT_LOG" + echo " commit(systemready-acs) = $(git rev-parse HEAD)" >> "$SYSTEMREADY_COMMIT_LOG" echo "" >> "${SYSTEMREADY_COMMIT_LOG}" pushd $TOP_DIR/meta-woden/recipes-acs/bootfs-files/files @@ -155,6 +155,8 @@ copy_recipes() cp $TOP_DIR/../../common/linux_scripts/extract_capsule_fw_version.py $TOP_DIR/meta-woden/recipes-acs/install-files/files cp $TOP_DIR/../../common/linux_scripts/capsule_ondisk_reporting_vars_check.py $TOP_DIR/meta-woden/recipes-acs/install-files/files cp $TOP_DIR/../../common/linux_scripts/runtime_device_mapping_conflict_checker.py $TOP_DIR/meta-woden/recipes-acs/install-files/files + cp $TOP_DIR/../../common/linux_scripts/ethtool-test.py $TOP_DIR/meta-woden/recipes-acs/install-files/files + cp $TOP_DIR/../../common/linux_scripts/read_write_check_blk_devices.py $TOP_DIR/meta-woden/recipes-acs/install-files/files cp -r $TOP_DIR/../../common/log_parser $TOP_DIR/meta-woden/recipes-acs/install-files/files/ popd diff --git a/SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/install-files/files/read_write_check_blk_devices.py b/SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/install-files/files/read_write_check_blk_devices.py deleted file mode 100644 index acac39c7..00000000 --- a/SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/install-files/files/read_write_check_blk_devices.py +++ /dev/null @@ -1,346 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2025, Arm Limited or its affiliates. All rights reserved. -# SPDX-License-Identifier : Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This script parses for block devices and perform block read and write operation if the partition doesn't belong precious partitions set. - -import subprocess -import re -import hashlib -import threading -import os - -# Note: Precious partitions dictionary, this is a set of partition types which might have firmware -# and to be refrained from read/write operations. The list is not exhaustive might see additions in future. -precious_parts_mbr = { - "Protective partition":"0xF8", - "EFI system partition":"0xEF" -} - -precious_parts_gpt = { - "EFI System partition":"C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - "BIOS boot partition":"21686148-6449-6E6F-744E-656564454649", - "U-Boot environment partition":"3DE21764-95BD-54BD-A5C3-4ABE786F38A8" -} - -def input_with_timeout(prompt, timeout=5): - print(prompt, end='', flush=True) - input_queue = [] - def get_user_input(): - try: - input_queue.append(input()) - except EOFError: - pass - user_input_thread = threading.Thread(target=get_user_input) - user_input_thread.daemon = True - user_input_thread.start() - user_input_thread.join(timeout) - return input_queue[0] if input_queue else "no" - -def calculate_sha256(file_path): - sha256_hash = hashlib.sha256() - with open(file_path, "rb") as f: - # Read and update hash string value in blocks of 4K - for byte_block in iter(lambda: f.read(4096), b""): - sha256_hash.update(byte_block) - return sha256_hash.hexdigest() - -def get_partition_space(partition_path): #to calculate used blocks and available blocks - command = f"df -B 512 {partition_path} --output=used,avail" - result = subprocess.run(command, shell=True, text=True, check=True, capture_output=True) - lines = result.stdout.strip().split('\n') - if len(lines) > 1: - parts = lines[1].split() - used_blocks = int(parts[0]) - available_blocks = int(parts[1]) - return used_blocks, available_blocks - else: - print(f"WARNING: Unable to parse partition space for {partition_path}.") - return 0, 0 # Default to 0 if parsing fails - -def is_mounted(device): - command = f"findmnt -n {device}" - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - return result.returncode == 0 - -def is_mtd_block_device(device): - return device.startswith('mtdblock') - -def is_ram_disk(device): - return device.startswith('ram') - -def perform_write_check(partition_label, partition_id, precious_parts): - - if is_mounted(f"/dev/{partition_label}"): - print(f"INFO: /dev/{partition_label} is mounted, skipping write test.") - return - - # Check if partition is precious - user_input = 'no' if partition_id in precious_parts.values() else input_with_timeout( - f"Do you want to perform a write check on /dev/{partition_label}? (yes/no): ", 5).lower() - - if user_input == 'yes' and partition_id not in precious_parts.values(): - used_blocks, available_blocks = get_partition_space(f"/dev/{partition_label}") - if available_blocks > 0: - - # Prepare hello.txt content with padding to 512 bytes - hello_content = "Hello!".ljust(512, '\x00') # Pad the content to 512 bytes - with open("hello.txt", "wb") as f: - f.write(hello_content.encode('utf-8')) - original_sha256 = calculate_sha256("hello.txt") - - # Backup filename specific to partition - backup_filename = f"{partition_label}_backup.bin" - # backup command - print("INFO: Creating backup of the current block before write check...") - backup_command = f"dd if=/dev/{partition_label} of={backup_filename} bs=512 count=1 skip={used_blocks}" - subprocess.run(backup_command, shell=True, check=True) - - # Write padded hello.txt to the device - print("INFO: Writing test data to the device for write check...") - write_command = f"dd if=hello.txt of=/dev/{partition_label} bs=512 count=1 seek={used_blocks}" - subprocess.run(write_command, shell=True, check=True) - - # Read back the 512-byte block - read_back_file = "read_hello.txt" - print("INFO: Reading back the test data for verification...") - read_command = f"dd if=/dev/{partition_label} of={read_back_file} bs=512 count=1 skip={used_blocks}" - subprocess.run(read_command, shell=True, check=True) - - # Calculate SHA256 for the padded content to ensure a fair comparison - read_back_sha256 = calculate_sha256(read_back_file) - - # Verify checksums - print(f"Original SHA256: {original_sha256}") - print(f"Read-back SHA256: {read_back_sha256}") - if original_sha256 == read_back_sha256: - print(f"INFO: write check passed on /dev/{partition_label}.") - - # Restore the backup - print("INFO: Restoring the backup to the device after write check...") - restore_command = f"dd if={backup_filename} of=/dev/{partition_label} bs=512 count=1 seek={used_blocks}" - subprocess.run(restore_command, shell=True, check=True) - print(f"INFO: Backup restored for /dev/{partition_label}.") - else: - print(f"WARNING: Data integrity check failed for /dev/{partition_label}. Possible data corruption.") - - # Clean up - os.remove("hello.txt") - os.remove(read_back_file) - os.remove(backup_filename) - - else: - print(f"WARNING: No available space for write check on /dev/{partition_label}. Skipping write check.") - -def get_partition_labels(disk): - """ - Return a list of partition labels from lsblk *without* relying on box/Unicode characters. - """ - command = f"lsblk -rn -o NAME,TYPE /dev/{disk} | awk '/part/ {{print $1}}'" - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - labels = result.stdout.strip().split() - return labels - -if __name__ == "__main__": - try: - # find all disk block devices - command = "lsblk -e 7 -d | grep disk | awk '{print $1}'" - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - disks = result.stdout.split() - - print("\n********************************************************************************************************************************\n") - print(" Read block devices tool\n") - print("********************************************************************************************************************************") - - print("INFO: Detected following block devices with lsblk command :") - for num, disk in enumerate(disks): - print(f"{num}: {disk}") - - print("\n********************************************************************************************************************************\n") - - for disk in disks: - # Skip MTD block devices - if is_mtd_block_device(disk): - print(f"INFO: Skipping MTD block device /dev/{disk}") - continue - - # Skip RAM disks - if is_ram_disk(disk): - print(f"INFO: Skipping RAM disk /dev/{disk}") - continue - - print(f"INFO: Block device : /dev/{disk}") - - # check whether disk uses MBR or GPT partition table - command = f"timeout 10 gdisk -l /dev/{disk}" - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - - if "MBR: MBR only" in result.stdout: - part_table = "MBR" - elif "GPT: present" in result.stdout: - part_table = "GPT" - else: - print(f"INFO: No valid partition table found for {disk}, treating as raw device.") - part_table = "RAW" - - print(f"INFO: Partition table type : {part_table}\n") - - # get number of partitions available for given disk - command = f"lsblk -rn -o NAME,TYPE /dev/{disk} | grep -c part" - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - num_parts_str = result.stdout.strip() - num_parts = int(num_parts_str) if num_parts_str else 0 - - if part_table == "RAW" or num_parts == 0: - print(f"INFO: No partitions detected for {disk}, treating as raw device.") - part_lables = [disk] # Treat the whole disk as a 'partition' - - # Process the raw disk - for part_label in part_lables: - print(f"INFO: Performing block read on /dev/{part_label}") - command = f"dd if=/dev/{part_label} bs=1M count=1 > /dev/null" - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - if result.returncode == 0: - print(f"INFO: Block read on /dev/{part_label} successful") - # Since we don't have partition IDs, pass empty string to perform_write_check - perform_write_check(part_label, '', {}) - else: - print(f"INFO: Block read on /dev/{part_label} failed") - - print("\n********************************************************************************************************************************\n") - continue # Continue to next disk - - # get partition labels with the safer method - part_lables = get_partition_labels(disk) - - # If there's a mismatch, just warn. We do not skip any existing partitions. - if len(part_lables) < num_parts: - print(f"WARNING: Mismatch in partition count. Found {len(part_lables)} partition labels, " - f"but lsblk reported {num_parts} partitions for {disk}. Proceeding with the ones we have...") - - # ----------------------------------- - # MBR scheme - # ----------------------------------- - if part_table == "MBR": - table_header_row = ["Device", "Boot", "Start", "End", "Sectors", "Size", "Id", "Type"] - command = f"fdisk -l /dev/{disk}" - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - - # get MBR partition type Ids - lines = result.stdout.strip().split('\n') - collect_lines = False - mbr_part_ids = [] - - for line in lines: - if collect_lines: - columns = line.split() - if len(columns) >= 6: - # Because "Boot" might be an extra column, handle if columns[1] == '*' - if columns[1] == '*' and len(columns) >= 7: - # Then columns[6] is the "Id" (typical fdisk layout) - mbr_part_ids.append("0x" + columns[6].upper()) - else: - # columns[5] is the "Id" in normal case - mbr_part_ids.append("0x" + columns[5].upper()) - elif all(substring in line for substring in table_header_row): - collect_lines = True - - if len(mbr_part_ids) < num_parts: - print(f"WARNING: Could not parse enough MBR partition IDs. Found {len(mbr_part_ids)}, expected {num_parts}.") - - # Process up to min of (parsed labels, parsed IDs, num_parts) - process_count = min(len(part_lables), len(mbr_part_ids), num_parts) - - for index in range(process_count): - print(f"\nINFO: Partition : /dev/{part_lables[index]} Partition type : {mbr_part_ids[index]}") - - if mbr_part_ids[index] in precious_parts_mbr.values(): - # skip read/write for precious - for key, value in precious_parts_mbr.items(): - if value == mbr_part_ids[index]: - print(f"INFO: {part_lables[index]} partition is PRECIOUS") - used_blocks, _ = get_partition_space(f"/dev/{part_lables[index]}") - print(f"INFO: Number of 512B blocks used on /dev/{part_lables[index]}: {used_blocks}") - print(f" {key} : {value}") - print(" Skipping block read/write...") - break - else: - command = f"dd if=/dev/{part_lables[index]} bs=1M count=1 > /dev/null" - print(f"INFO: Performing block read on /dev/{part_lables[index]} mbr_part_id = {mbr_part_ids[index]}") - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - if result.returncode == 0: - print(f"INFO: Block read on /dev/{part_lables[index]} mbr_part_id = {mbr_part_ids[index]} successful") - perform_write_check(part_lables[index], mbr_part_ids[index], precious_parts_mbr) - else: - print(f"INFO: Block read on /dev/{part_lables[index]} mbr_part_id = {mbr_part_ids[index]} failed") - print("\n********************************************************************************************************************************\n") - - # if disk follows GPT scheme - elif part_table == "GPT": - # We'll parse partition GUID code + attribute flags with sgdisk - process_count = min(len(part_lables), num_parts) - for index in range(process_count): - command = f"sgdisk -i={index+1} /dev/{disk}" - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - - # regex to match part GUIDs and attribute flags - guid_regex = r"Partition GUID code: ([\w-]+) \(" - attr_flag_regex = r"Attribute flags: ([0-9A-Fa-f]+)" - guid_code_match = re.search(guid_regex, result.stdout) - attribute_flags_match = re.search(attr_flag_regex, result.stdout) - - if not guid_code_match or not attribute_flags_match: - print(f"INFO: Unable to parse sgdisk info for {part_lables[index]}. Skipping.") - continue - - partition_guid_code = guid_code_match.group(1) - attribute_flags_hex = attribute_flags_match.group(1) - attribute_flags_int = int(attribute_flags_hex, 16) - lsb = attribute_flags_int & 1 # "Platform required" bit - - print(f"\nINFO: Partition : /dev/{part_lables[index]} Partition type GUID : {partition_guid_code} \"Platform required bit\" : {lsb}") - - # skip block read if "Platform required" bit is set - if lsb == 1: - print(f"INFO: Platform required attribute set for {part_lables[index]} partition, skipping block read/write...") - continue - # check if the partition is precious - if partition_guid_code in precious_parts_gpt.values(): - used_blocks, _ = get_partition_space(f"/dev/{part_lables[index]}") - for key, value in precious_parts_gpt.items(): - if value == partition_guid_code: - print(f"INFO: {part_lables[index]} partition is PRECIOUS.") - print(f"INFO: Number of 512B blocks used on /dev/{part_lables[index]}: {used_blocks}") - print(f" {key} : {value}") - print(" Skipping block read/write...") - break - else: - command = f"dd if=/dev/{part_lables[index]} bs=1M count=1 > /dev/null" - print(f"INFO: Performing block read on /dev/{part_lables[index]} part_guid = {partition_guid_code}") - result = subprocess.run(command, shell=True, text=True, check=False, capture_output=True) - - if result.returncode == 0: - print(f"INFO: Block read on /dev/{part_lables[index]} part_guid = {partition_guid_code} successful") - perform_write_check(part_lables[index], partition_guid_code, precious_parts_gpt) - else: - print(f"INFO: Block read on /dev/{part_lables[index]} part_guid = {partition_guid_code} failed") - - print("\n********************************************************************************************************************************\n") - else: - print(f"INFO: Invalid partition table, expected MBR or GPT reported type = {part_table}") - - except Exception as e: - print(f"Error occurred: {e}") - exit(1) diff --git a/common/config/buildroot_defconfig b/common/config/buildroot_defconfig index ada80c99..727c7e2b 100644 --- a/common/config/buildroot_defconfig +++ b/common/config/buildroot_defconfig @@ -39,6 +39,26 @@ BR2_PACKAGE_PYTHON_PIP=y BR2_PACKAGE_PYTHON_PILLOW=y BR2_PACKAGE_LSHW=y +# Ethtool and Block devices packages +BR2_PACKAGE_OPENSSL=y +BR2_PACKAGE_LIBOPENSSL=y + +BR2_PACKAGE_LIBCURL=y +BR2_PACKAGE_LIBCURL_CURL=y +BR2_PACKAGE_LIBCURL_OPENSSL=y +BR2_PACKAGE_LIBCURL_PROXY_SUPPORT=y +BR2_PACKAGE_LIBCURL_COOKIES_SUPPORT=y +BR2_PACKAGE_LIBCURL_EXTRA_PROTOCOLS_FEATURES=y + +BR2_PACKAGE_CA_CERTIFICATES=y +BR2_PACKAGE_BUSYBOX=y +BR2_USE_WCHAR=y +BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y + +BR2_PACKAGE_GPTFDISK=y +BR2_PACKAGE_GPTFDISK_GDISK=y +BR2_PACKAGE_GPTFDISK_SGDISK=y + # Target filesystem BR2_TARGET_ROOTFS_CPIO=y diff --git a/common/config/srband_defconfig b/common/config/srband_defconfig index 533d0598..fd4bccb4 100644 --- a/common/config/srband_defconfig +++ b/common/config/srband_defconfig @@ -37,4 +37,44 @@ CONFIG_AUTOFS4_FS=y CONFIG_USB_NET_CDC_EEM=y CONFIG_PREEMPT=y CONFIG_PREEMPT_VOLUNTARY=y -CONFIG_RCU_NOCB_CPU=y \ No newline at end of file +CONFIG_RCU_NOCB_CPU=y + +# Configurations to enable rshim support +CONFIG_MLXBF_TMFIFO=y + +# Configurations to increase serial ports +CONFIG_SERIAL_8250_NR_UARTS=32 +CONFIG_SERIAL_8250_RUNTIME_UARTS=32 + +# Configurations for ethtool and block device testing +CONFIG_BLK_DEV_RAM=y +CONFIG_BLK_DEV_RAM_COUNT=16 +CONFIG_BLK_DEV_RAM_SIZE=4096 +CONFIG_INET=y +CONFIG_IPV6=y +CONFIG_IPV6_ROUTER_PREF=y +CONFIG_NETDEVICES=y +CONFIG_DUMMY=m +CONFIG_TUN=y +CONFIG_VETH=y +CONFIG_VLAN_8021Q=y +CONFIG_BRIDGE=y +CONFIG_MACVLAN=y +CONFIG_IPVLAN=y +CONFIG_MLX5_CORE=y +CONFIG_MLX5_CORE_EN=y +CONFIG_NET_VENDOR_MELLANOX=y +CONFIG_USB_NET_DRIVERS=y +CONFIG_USB_USBNET=y +CONFIG_USB_NET_CDCETHER=y +CONFIG_USB_NET_CDC_EEM=y +CONFIG_USB_NET_CDC_NCM=y +CONFIG_USB_NET_RNDIS_HOST=y +CONFIG_USB_NET_AX88179_178A=y +CONFIG_USB_NET_SMSC75XX=y +CONFIG_USB_NET_SMSC95XX=y +CONFIG_USB=y +CONFIG_USB_SUPPORT=y +CONFIG_USB_EHCI_HCD=y +CONFIG_USB_XHCI_HCD=y +CONFIG_USB_COMMON=y diff --git a/common/config/system_config.txt b/common/config/system_config.txt index db1c20e8..036b486a 100644 --- a/common/config/system_config.txt +++ b/common/config/system_config.txt @@ -3,3 +3,6 @@ Flashing instructions: Unknown product website: Unknown Tested operated Systems: Unknown Testlab assistance: Unknown + +#Add the number of interfaces to be compliant, if commented or "0", all existing interfaces must be compliant +Total_number_of_network_controllers= 0 diff --git a/SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/install-files/files/ethtool-test.py b/common/linux_scripts/ethtool-test.py similarity index 74% rename from SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/install-files/files/ethtool-test.py rename to common/linux_scripts/ethtool-test.py index 6583d157..ac89863e 100755 --- a/SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/install-files/files/ethtool-test.py +++ b/common/linux_scripts/ethtool-test.py @@ -14,24 +14,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -# This script parses for ethernet interfaces using ip tool and runs ethtool -# self-test if the interface supports. It also performs link detection, -# DHCP verification, IPv6 testing, and network connectivity checks via ping, -# wget, and curl. +"""This script parses for ethernet interfaces using ip tool and runs ethtool +self-test if the interface supports. It also performs link detection, +DHCP verification, IPv6 testing, and network connectivity checks via ping, +wget, and curl. +""" - -import subprocess import re -import time +import shutil import signal +import subprocess # nosec B404 import sys -import shutil -from pathlib import Path +import time from collections import OrderedDict from fnmatch import fnmatch +from pathlib import Path +from typing import Dict # To print coloured output on the console def print_color(text, level="INFO"): + """Print coloured log messages with a severity prefix.""" colors = { "INFO": "\033[92m", # Green "DEBUG": "\033[94m", # Blue @@ -49,6 +51,16 @@ def print_color(text, level="INFO"): SKIPPED = "SKIPPED" WARNING = "WARNING" +# Command paths +IP_CMD = shutil.which("ip") or "ip" +ETHTOOL_CMD = shutil.which("ethtool") or "ethtool" +PING_CMD = shutil.which("ping") or "ping" +PING6_CMD = shutil.which("ping6") or "ping6" +UDHCPC_CMD = shutil.which("udhcpc") or "udhcpc" +DHCLIENT_CMD = shutil.which("dhclient") or "dhclient" +WGET_CMD = shutil.which("wget") or "wget" +CURL_CMD = shutil.which("curl") or "curl" + # Order and names of tests shown in the summary TEST_ORDER = [ "Detect interface", @@ -69,13 +81,15 @@ def print_color(text, level="INFO"): # Parsing the summary results = {} -SYSTEM_CONFIG_PATH = None # Get the ethtool compliant interface value from system_config.txt -def get_required_compliant_ifaces(): +def get_required_compliant_ifaces(system_config_path): + """Read required Ethernet controller count from system_config.txt.""" + if not system_config_path: + return 0 try: - cfg = Path(SYSTEM_CONFIG_PATH) - text = cfg.read_text() - except Exception: + cfg = Path(system_config_path) + text = cfg.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): return 0 for line in text.splitlines(): @@ -95,31 +109,38 @@ def get_required_compliant_ifaces(): return 0 def init_iface_results(iface): + """Initialise all test results for an interface as skipped.""" od = OrderedDict() for t in TEST_ORDER: od[t] = {"status": SKIPPED, "detail": "Not run"} results[iface] = od def set_result(iface, test_name, status, detail=""): + """Set test result for an interface.""" if iface not in results: init_iface_results(iface) results[iface][test_name] = {"status": status, "detail": detail or ""} def skip_many(iface, test_names, reason): + """Mark multiple tests as skipped for an interface.""" for t in test_names: if results[iface][t]["status"] == SKIPPED or results[iface][t]["detail"] == "Not run": results[iface][t] = {"status": SKIPPED, "detail": reason} -def print_summary(): +def print_summary(system_config_path=None): + """Print per-interface test results and the final compliance summary.""" print("\n================================================================") print(" SUMMARY") print("================================================================") - c = {"PASSED":"\033[92m", "FAILED":"\033[91m", "SKIPPED":"\033[93m", "WARNING":"\033[36m", "reset":"\033[0m"} + c = {"PASSED":"\033[92m", "FAILED":"\033[91m", "SKIPPED":"\033[93m", + "WARNING":"\033[36m", "reset":"\033[0m"} # One-line detected interfaces summary - detected_ifaces = [iface for iface in results if results[iface]["Detect interface"]["status"] == PASSED] + detected_ifaces = [iface for iface, iface_results in results.items() + if iface_results["Detect interface"]["status"] == PASSED] if detected_ifaces: - print(f"\nDetected Interfaces : {c['PASSED']}PASSED{c['reset']} ({', '.join(detected_ifaces)})") + print(f"\nDetected Interfaces : {c['PASSED']}PASSED{c['reset']} " + f"({', '.join(detected_ifaces)})") printable_tests = [t for t in TEST_ORDER if t != "Detect interface"] max_name = max(len(t) for t in printable_tests) @@ -229,7 +250,7 @@ def link_detected_status(iface): def join_names(names): return ", ".join(names) if names else "None" - required = get_required_compliant_ifaces() + required = get_required_compliant_ifaces(system_config_path) # total testable interfaces excluding virtual total_testable = len(compliant_ifaces) + len(non_compliant_ifaces) @@ -265,9 +286,8 @@ def join_names(names): if len(compliant_ifaces) >= required: extra_msgs = [] if non_compliant_ifaces: - extra_msgs.append( - f"{len(non_compliant_ifaces)} interface(s) failed: {join_names(non_compliant_ifaces)}" - ) + extra_msgs.append(f"{len(non_compliant_ifaces)} interface(s) failed: " + f"{join_names(non_compliant_ifaces)}") extra = f"; {'; '.join(extra_msgs)}" if extra_msgs else "" print( f"\nEthtool Compliance : {green}PASSED{reset} " @@ -289,28 +309,29 @@ def join_names(names): f"({detail})\n" ) -original_states = {} +original_states: Dict[str, str] = {} #To check if a tool is from BusyBox def is_busybox_tool(tool_name): + """Return True if the given tool appears to be provided by BusyBox.""" tool_path = shutil.which(tool_name) if not tool_path: return False try: r = subprocess.run( - f"{tool_path} --help", shell=True, - capture_output=True, text=True, timeout=2 - ) + [tool_path, "--help"], + capture_output=True, text=True, timeout=2, check=False) return "BusyBox" in r.stdout or "BusyBox" in r.stderr - except Exception: + except (OSError, subprocess.SubprocessError): return False def is_virtual_iface(iface): + """Return True if the interface appears to be virtual.""" try: target_abs = str(Path(f"/sys/class/net/{iface}/device").resolve()) if "/devices/virtual/" in target_abs: return True - except Exception: + except OSError: pass virt_prefixes = ( @@ -331,15 +352,16 @@ def is_virtual_iface(iface): # To renew DHCP when doesn’t exist. def renew_dhcp(intrf, busybox_env): + """Try to restore a default route for an interface using DHCP.""" if has_default_route(intrf): return False print_color(f"Default route via {intrf} is missing; attempting DHCP restore", "INFO") try: if busybox_env and shutil.which("udhcpc"): - subprocess.run(f"udhcpc -n -q -i {intrf}", shell=True, timeout=30) + subprocess.run([UDHCPC_CMD, "-n", "-q", "-i", intrf], timeout=30, check=False) elif shutil.which("dhclient"): - subprocess.run(f"dhclient -r {intrf}", shell=True, timeout=20) - subprocess.run(f"dhclient -1 {intrf}", shell=True, timeout=35) + subprocess.run([DHCLIENT_CMD, "-r", intrf], timeout=20, check=False) + subprocess.run([DHCLIENT_CMD, "-1", intrf], timeout=35, check=False) else: print_color("No DHCP client found (udhcpc/dhclient). Skipping restore.", "WARN") except subprocess.TimeoutExpired: @@ -353,12 +375,15 @@ def renew_dhcp(intrf, busybox_env): # To check if the default route already exist. def has_default_route(dev): - r = subprocess.run("ip route show default", shell=True, capture_output=True, text=True) + """Return True if the given interface has a default route.""" + r = subprocess.run([IP_CMD, "route", "show", "default"], capture_output=True, + text=True, check=False) if r.returncode == 0: for line in r.stdout.splitlines(): if f" dev {dev} " in f" {line} ": return True - r2 = subprocess.run("ip -o route show table all default", shell=True, capture_output=True, text=True) + r2 = subprocess.run([IP_CMD, "-o", "route", "show", "table", "all", "default"], + capture_output=True, text=True, check=False) if r2.returncode == 0: for line in r2.stdout.splitlines(): if f" dev {dev} " in f" {line} ": @@ -367,23 +392,31 @@ def has_default_route(dev): #Restoring the interfaces to their original states on exit def cleanup(): + """Restore Ethernet interfaces to their original up/down states.""" print_color("Cleaning up... restoring interface states", "INFO") for iface, state in original_states.items(): - subprocess.run(f"ip link set dev {iface} {state}", shell=True) + subprocess.run([IP_CMD, "link", "set", "dev", iface, state], + check=False) print_color("Cleanup complete.", "INFO") -signal.signal(signal.SIGINT, lambda sig, frame: (print_summary(), cleanup(), sys.exit(0))) -signal.signal(signal.SIGTERM, lambda sig, frame: (print_summary(), cleanup(), sys.exit(0))) +def handle_exit_signal(_signum, _frame): + """Print summary, clean up interfaces, and exit on termination signal.""" + print_summary() + cleanup() + sys.exit(0) -if __name__ == "__main__": +signal.signal(signal.SIGINT, handle_exit_signal) +signal.signal(signal.SIGTERM, handle_exit_signal) + +def main(): + """Run Ethernet validation checks.""" try: - if len(sys.argv) > 1: - SYSTEM_CONFIG_PATH = sys.argv[1] + system_config_path = sys.argv[1] if len(sys.argv) > 1 else None have_ethtool = shutil.which("ethtool") is not None busybox_env = shutil.which("udhcpc") is not None # Discovering ethernet interfaces - output = subprocess.check_output("ip -o link", shell=True).decode("utf-8").split('\n') + output = subprocess.check_output([IP_CMD, "-o", "link"]).decode("utf-8").split('\n') ether_interfaces = [] for line in output: parts = line.split() @@ -400,12 +433,12 @@ def cleanup(): if not ether_interfaces: print_color("No ethernet interfaces detected via ip linux command, Exiting ...", "WARN") - sys.exit(1) - else: - print_color("Detected following ethernet interfaces via ip command :", "INFO") - for index, intrf in enumerate(ether_interfaces): - print(f"{index}: {intrf}") - set_result(intrf, "Detect interface", PASSED) + return 1 + + print_color("Detected following ethernet interfaces via ip command :", "INFO") + for index, intrf in enumerate(ether_interfaces): + print(f"{index}: {intrf}") + set_result(intrf, "Detect interface", PASSED) #Classify interfaces as virtual or physical virtual_ifaces = [i for i in ether_interfaces if is_virtual_iface(i)] @@ -416,7 +449,8 @@ def cleanup(): # Recording initial states print_color("Capturing original interface states", "INFO") for intrf in ether_interfaces: - result = subprocess.run(f"ip link show {intrf}", shell=True, capture_output=True, text=True) + result = subprocess.run([IP_CMD, "link", "show", intrf], capture_output=True, + text=True, check=False) flags_match = re.search(r'<([^>]+)>', result.stdout) flags = flags_match.group(1).split(',') if flags_match else [] state = "up" if "UP" in flags else "down" @@ -425,11 +459,13 @@ def cleanup(): # Bringing down all the available interfaces print_color("Bringing down all ethernet interfaces using ip", "INFO") for intrf in physical_ifaces: - cmd = f"ip link set dev {intrf} down" - print(cmd) - rc = subprocess.run(cmd, shell=True).returncode + cmd = [IP_CMD, "link", "set", "dev", intrf, "down"] + print(f"ip link set dev {intrf} down") + rc = subprocess.run(cmd, check=False).returncode if rc != 0: - print_color(f"Unable to bring down ethernet interface {intrf} using ip, Exiting ...", "WARN") + print_color( + f"Unable to bring down ethernet interface {intrf} using ip, Exiting ...", + "WARN") print("\n****************************************************************\n") time.sleep(20) @@ -438,13 +474,14 @@ def cleanup(): for intrf in physical_ifaces: if previous_eth_intrf: print_color(f"Bringing down ethernet interface: {previous_eth_intrf}", "INFO") - subprocess.run(f"ip link set dev {previous_eth_intrf} down", shell=True) + subprocess.run([IP_CMD, "link", "set", "dev", previous_eth_intrf, "down"], + check=False) time.sleep(20) previous_eth_intrf = intrf # Bring up the current interface print_color(f"Bringing up ethernet interface: {intrf}", "INFO") - result_up = subprocess.run(f"ip link set dev {intrf} up", shell=True) + result_up = subprocess.run([IP_CMD, "link", "set", "dev", intrf, "up"], check=False) if result_up.returncode != 0: print_color(f"Unable to bring up ethernet interface {intrf} using ip", "WARN") set_result(intrf, "Bring up", FAILED, "ip link set up failed") @@ -453,40 +490,46 @@ def cleanup(): skip_many(intrf, remaining, "Interface could not be brought up") print("\n****************************************************************\n") continue - else: - set_result(intrf, "Bring up", PASSED) + + set_result(intrf, "Bring up", PASSED) time.sleep(20) # Check for ethtool availability if have_ethtool: set_result(intrf, "ethtool present", PASSED) print_color(f"Running \"ethtool {intrf}\"", "INFO") - result_ethdump = subprocess.run(f"ethtool {intrf}", shell=True, capture_output=True, text=True) + result_ethdump = subprocess.run([ETHTOOL_CMD, intrf], capture_output=True, + text=True, check=False) print(result_ethdump.stdout) - result_test = subprocess.run(f"ethtool -i {intrf}", shell=True, capture_output=True, text=True) + result_test = subprocess.run([ETHTOOL_CMD, "-i", intrf], capture_output=True, + text=True, check=False) print(result_test.stdout) if "supports-test: yes" in result_test.stdout: print_color(f"Ethernet interface {intrf} supports ethtool self test.", "CHECK") set_result(intrf, "Self-test supported", PASSED) print_color(f"Running ethtool -t {intrf}", "INFO") try: - t = subprocess.run(f"ethtool -t {intrf}",shell=True, capture_output=True, text=True, timeout=60) + t = subprocess.run([ETHTOOL_CMD, "-t", intrf], capture_output=True, + text=True, timeout=60, check=False) print_color(t.stdout, "DEBUG") if t.returncode == 0: set_result(intrf, "ethtool self tests", PASSED) else: - first_line = next((ln for ln in (t.stdout + "\n" + t.stderr).splitlines() if ln.strip()), "") - set_result(intrf, "ethtool self tests", WARNING, first_line or f"returncode={t.returncode}") + output_lines = (t.stdout + "\n" + t.stderr).splitlines() + first_line = next((ln for ln in output_lines if ln.strip()), "") + set_result(intrf, "ethtool self tests", WARNING, + first_line or f"returncode={t.returncode}") except subprocess.TimeoutExpired: print_color("ethtool -t timed out (60s)", "WARN") set_result(intrf, "ethtool self tests", WARNING, "timeout") - subprocess.run(["ip", "link", "set", "dev", intrf, "up"]) + subprocess.run([IP_CMD, "link", "set", "dev", intrf, "up"], check=False) for _ in range(10): try: - carrier = Path(f"/sys/class/net/{intrf}/carrier").read_text().strip() - except Exception: + carrier = Path(f"/sys/class/net/{intrf}/carrier").read_text( + encoding="utf-8").strip() + except OSError: carrier = "0" if carrier == "1": print_color(f"Link restored on {intrf}", "CHECK") @@ -494,7 +537,8 @@ def cleanup(): time.sleep(1) else: - print_color(f"Ethernet interface {intrf} does not support ethtool self test", "WARN") + print_color(f"Ethernet interface {intrf} does not support ethtool self test", + "WARN") set_result(intrf, "Self-test supported", SKIPPED, "supports-test: no") set_result(intrf, "ethtool self tests", SKIPPED, "Self-test not supported") @@ -521,17 +565,21 @@ def cleanup(): set_result(intrf, "ethtool self tests", SKIPPED, "No ethtool") print_color("ethtool not found; using sysfs for link detection", "WARN") try: - carrier = Path(f"/sys/class/net/{intrf}/carrier").read_text().strip() - except Exception: + carrier = Path(f"/sys/class/net/{intrf}/carrier").read_text( + encoding="utf-8").strip() + except OSError: carrier = "0" if carrier != "1": try: - oper = Path(f"/sys/class/net/{intrf}/operstate").read_text().strip() - except Exception: + oper = Path(f"/sys/class/net/{intrf}/operstate").read_text( + encoding="utf-8").strip() + except OSError: oper = "down" if oper != "up": - print_color(f"Link not detected for {intrf} (carrier={carrier}, operstate={oper})", "WARN") - set_result(intrf, "Link detected", FAILED, f"carrier={carrier}, operstate={oper}") + print_color(f"Link not detected for {intrf} " + f"(carrier={carrier}, operstate={oper})", "WARN") + set_result(intrf, "Link detected", FAILED, + f"carrier={carrier}, operstate={oper}") skip_many(intrf, [ "Gateway Address present", "Ping gateway (IPv4)", @@ -540,15 +588,16 @@ def cleanup(): "Ping ipv6.google.com (IPv6)", "wget and curl", ], "Link not detected") - print("\n****************************************************************\n") + print("\n**************************************************************\n") continue print_color(f"Link detected on {intrf} (sysfs)", "CHECK") set_result(intrf, "Link detected", PASSED) # Check IPv4 and IPv6 address configuration - command = f"ip address show dev {intrf}" - print_color(f"Running {command}", "INFO") - result_addr = subprocess.run(command, shell=True, capture_output=True, text=True) + command = [IP_CMD, "address", "show", "dev", intrf] + print_color(f"Running ip address show dev {intrf}", "INFO") + result_addr = subprocess.run(command, capture_output=True, + text=True, check=False) print(result_addr.stdout) has_dhcp = "dynamic" in result_addr.stdout @@ -562,8 +611,9 @@ def cleanup(): # Default route to evaluate whenever we have any IPv4 if not has_default_route(intrf): renew_dhcp(intrf, busybox_env) - command = f"ip address show dev {intrf}" - result_addr = subprocess.run(command, shell=True, capture_output=True, text=True) + command = [IP_CMD, "address", "show", "dev", intrf] + result_addr = subprocess.run(command, capture_output=True, + text=True, check=False) print(result_addr.stdout) has_dhcp = "dynamic" in result_addr.stdout has_ipv6 = re.search(r'inet6 (?!fe80)', result_addr.stdout) @@ -589,7 +639,8 @@ def cleanup(): # IPv4 address present (independent from DHCP) if has_ipv4: ip_type = "dynamic" if has_dhcp else "static" - set_result(intrf, "IPv4 address present", PASSED, f"{ip_type} {', '.join(ipv4_list)}") + set_result(intrf, "IPv4 address present", PASSED, + f"{ip_type} {', '.join(ipv4_list)}") else: set_result(intrf, "IPv4 address present", FAILED, "No IPv4 address") @@ -598,20 +649,25 @@ def cleanup(): # Run ping6 if global IPv6 address is found if has_ipv6: set_result(intrf, "IPv6 address present", PASSED) - ipv6_addresses = re.findall(r'inet6 ([\da-f:]+)/\d+ scope global', result_addr.stdout) + ipv6_addresses = re.findall(r'inet6 ([\da-f:]+)/\d+ scope global', + result_addr.stdout) for ip6 in ipv6_addresses: print_color(f"Found global IPv6 address on {intrf} → {ip6}", "CHECK") ping6_bin = shutil.which("ping") or shutil.which("ping6") if "ping6" in (ping6_bin or ""): - ping6_command = f"ping6 -c 3 -I {intrf} ipv6.google.com" + ping6_command = [PING6_CMD, "-c", "3", "-I", intrf, "ipv6.google.com"] + ping6_command_display = f"ping6 -c 3 -I {intrf} ipv6.google.com" else: - ping6_command = f"ping -6 -c 3 -I {intrf} ipv6.google.com" - print_color(f"Running {ping6_command}", "INFO") - result_ping6 = subprocess.run(ping6_command, shell=True, capture_output=True, text=True) + ping6_command = [PING_CMD, "-6", "-c", "3", "-I", intrf, "ipv6.google.com"] + ping6_command_display = f"ping -6 -c 3 -I {intrf} ipv6.google.com" + print_color(f"Running {ping6_command_display}", "INFO") + result_ping6 = subprocess.run(ping6_command, capture_output=True, + text=True, check=False) print(result_ping6.stdout) if result_ping6.returncode != 0 or "100% packet loss" in result_ping6.stdout: print_color(f"Failed to ping ipv6.google.com via {intrf}", "WARN") - set_result(intrf, "Ping ipv6.google.com (IPv6)", WARNING, "Packet loss or ping failed") + set_result(intrf, "Ping ipv6.google.com (IPv6)", WARNING, + "Packet loss or ping failed") else: print_color(f"Ping to ipv6.google.com via {intrf} is successful", "CHECK") set_result(intrf, "Ping ipv6.google.com (IPv6)", PASSED) @@ -633,10 +689,12 @@ def cleanup(): # Determine default router/gateway and verify the route path print_color("Running ip route get 8.8.8.8", "INFO") - r = subprocess.run("ip route get 8.8.8.8", shell=True, capture_output=True, text=True) + r = subprocess.run([IP_CMD, "route", "get", "8.8.8.8"], capture_output=True, + text=True, check=False) print(r.stdout) if r.returncode != 0: - print_color(f"No default route available for {intrf} (route get failed), skipping further tests for this interface", "WARN") + print_color(f"No default route available for {intrf} (route get failed), " + "skipping further tests for this interface", "WARN") skip_many(intrf, [ "Ping gateway (IPv4)", "Ping www.arm.com (IPv4)", @@ -647,7 +705,8 @@ def cleanup(): m = re.search(r'\bvia\s+(\d{1,3}(?:\.\d{1,3}){3}).*?\bdev\s+(\S+)', r.stdout) if not m: - print_color(f"Unable to parse gateway/dev from route output, skipping further tests for {intrf}", "WARN") + print_color("Unable to parse gateway/dev from route output, " + f"skipping further tests for {intrf}", "WARN") skip_many(intrf, [ "Ping gateway (IPv4)", "Ping www.arm.com (IPv4)", @@ -658,7 +717,8 @@ def cleanup(): gw, dev_on_path = m.group(1), m.group(2) if dev_on_path != intrf: - print_color(f"Default route to 8.8.8.8 is via {dev_on_path}, not {intrf}; skipping further tests for {intrf}", "WARN") + print_color(f"Default route to 8.8.8.8 is via {dev_on_path}, " + f"not {intrf}; skipping further tests for {intrf}", "WARN") skip_many(intrf, [ "Ping gateway (IPv4)", "Ping www.arm.com (IPv4)", @@ -672,33 +732,35 @@ def cleanup(): set_result(intrf, "Gateway Address present", PASSED, f"gateway {gw}") - subprocess.run(f"ip link set dev {intrf} up", shell=True) + subprocess.run([IP_CMD, "link", "set", "dev", intrf, "up"], check=False) time.sleep(20) # Run IPv4 ping test to the router/gateway - cmd = f"ping -c 3 -W 10 -I {intrf} {ip_address}" - print_color(f"Running {cmd}", "INFO") - rping = subprocess.run(cmd, shell=True, capture_output=True, text=True) + cmd = [PING_CMD, "-c", "3", "-W", "10", "-I", intrf, ip_address] + print_color(f"Running ping -c 3 -W 10 -I {intrf} {ip_address}", "INFO") + rping = subprocess.run(cmd, capture_output=True, text=True, check=False) print(rping.stdout) if rping.returncode != 0 or "100% packet loss" in rping.stdout: print_color(f"Failed to ping router/gateway[{ip_address}] for {intrf}", "WARN") set_result(intrf, "Ping gateway (IPv4)", WARNING, "Packet loss or ping failed") else: - print_color(f"Ping to router/gateway[{ip_address}] for {intrf} is successful", "CHECK") + print_color(f"Ping to router/gateway[{ip_address}] for {intrf} is successful", + "CHECK") set_result(intrf, "Ping gateway (IPv4)", PASSED) # Ping www.arm.com to verify DNS resolution and external connectivity - cmd = f"ping -c 3 -W 10 -I {intrf} www.arm.com" - print_color(f"Running {cmd}", "INFO") - rp2 = subprocess.run(cmd, shell=True, capture_output=True, text=True) + cmd = [PING_CMD, "-c", "3", "-W", "10", "-I", intrf, "www.arm.com"] + print_color(f"Running ping -c 3 -W 10 -I {intrf} www.arm.com", "INFO") + rp2 = subprocess.run(cmd, capture_output=True, text=True, check=False) print(rp2.stdout) if "bad address" in rp2.stderr: - print_color(f"Unable to resolve www.arm.com, DNS not configured correctly for {intrf}", "WARN") + print_color(f"Unable to resolve www.arm.com, DNS not configured correctly " + f"for {intrf}", "WARN") if rp2.returncode != 0 or "100% packet loss" in rp2.stdout: print_color(f"Failed to ping www.arm.com via {intrf}", "WARN") set_result(intrf, "Ping www.arm.com (IPv4)", WARNING, "Ping failed or DNS issue") else: - print_color(f"Ping to www.arm.com is successful", "CHECK") + print_color("Ping to www.arm.com is successful", "CHECK") set_result(intrf, "Ping www.arm.com (IPv4)", PASSED) # wget and curl connectivity check @@ -711,9 +773,10 @@ def cleanup(): # wget check if wget_available: - wget_command = f"wget --spider --timeout=10 https://www.arm.com" - print_color(f"Running {wget_command}", "INFO") - rwget = subprocess.run(wget_command, shell=True, capture_output=True, text=True) + wget_command = [WGET_CMD, "--spider", "--timeout=10", "https://www.arm.com"] + print_color("Running wget --spider --timeout=10 https://www.arm.com", "INFO") + rwget = subprocess.run(wget_command, capture_output=True, + text=True, check=False) if rwget.stdout.strip(): print_color(rwget.stdout.strip(), "DEBUG") if rwget.stderr.strip(): @@ -731,9 +794,19 @@ def cleanup(): # curl check if curl_available: - curl_command = f"curl -Is --connect-timeout 20 --interface {intrf} https://www.arm.com" - print_color(f"Running {curl_command}", "INFO") - rcurl = subprocess.run(curl_command, shell=True, capture_output=True, text=True) + curl_command = [ + CURL_CMD, + "-Is", + "--connect-timeout", + "20", + "--interface", + intrf, + "https://www.arm.com", + ] + print_color(f"Running curl -Is --connect-timeout 20 --interface {intrf} " + "https://www.arm.com", "INFO") + rcurl = subprocess.run(curl_command, capture_output=True, + text=True, check=False) lines = rcurl.stdout.strip().splitlines() if rcurl.stdout else [] first_line = lines[0] if lines else "" if rcurl.stderr.strip(): @@ -762,13 +835,17 @@ def cleanup(): print("\n****************************************************************\n") # Restore all original interface states and print summary - print_summary() - print("\033[91mPlease update 'system_config.txt' with the correct value for total_number_of_network_controllers\033[0m") + print_summary(system_config_path) + print("\033[91mPlease update 'system_config.txt' with the correct value " + "for total_number_of_network_controllers\033[0m") cleanup() - sys.exit(0) + return 0 except Exception as e: print_color(f"Error occurred: {e}", "ERROR") - print_summary() + print_summary(system_config_path) cleanup() - sys.exit(1) + return 1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/common/linux_scripts/init.sh b/common/linux_scripts/init.sh index dab2a3bd..ae3c7693 100644 --- a/common/linux_scripts/init.sh +++ b/common/linux_scripts/init.sh @@ -106,66 +106,18 @@ if [ $ADDITIONAL_CMD_OPTION != "noacs" ]; then exec sh +m fi - - #Linux debug dump - echo "Collecting Linux Debug Dump" - mkdir -p /mnt/acs_results/linux_dump - dmesg > /mnt/acs_results/linux_dump/dmesg.log - lspci > /mnt/acs_results/linux_dump/lspci.log - lspci -vvv &> /mnt/acs_results/linux_dump/lspci-vvv.log - cat /proc/interrupts > /mnt/acs_results/linux_dump/interrupts.log - cat /proc/cpuinfo > /mnt/acs_results/linux_dump/cpuinfo.log - cat /proc/meminfo > /mnt/acs_results/linux_dump/meminfo.log - cat /proc/iomem > /mnt/acs_results/linux_dump/iomem.log - lscpu > /mnt/acs_results/linux_dump/lscpu.log - lsblk > /mnt/acs_results/linux_dump/lsblk.log - lsusb > /mnt/acs_results/linux_dump/lsusb.log - lshw > /mnt/acs_results/linux_dump/lshw.log - dmidecode > /mnt/acs_results/linux_dump/dmidecode.log - dmidecode --dump-bin /mnt/acs_results/linux_dump/dmidecode.bin >> /mnt/acs_results/linux_dump/dmidecode.log 2>&1 - uname -a > /mnt/acs_results/linux_dump/uname.log - cat /etc/os-release > /mnt/acs_results/linux_dump/cat-etc-os-release.log - date > /mnt/acs_results/linux_dump/date.log - cat /proc/driver/rtc > /mnt/acs_results/linux_dump/rtc.log - hwclock > /mnt/acs_results/linux_dump/hwclock.log - efibootmgr > /mnt/acs_results/linux_dump/efibootmgr.log - efibootmgr -t 20 > /mnt/acs_results/linux_dump/efibootmgr-t-20.log - efibootmgr -t 5 > /mnt/acs_results/linux_dump/efibootmgr-t-5.log - efibootmgr -c > /mnt/acs_results/linux_dump/efibootmgr-c.txt 2>&1 - ifconfig > /mnt/acs_results/linux_dump/ifconfig.log - ip addr show > /mnt/acs_results/linux_dump/ip-addr-show.log - ping -c 5 www.arm.com > /mnt/acs_results/linux_dump/ping-c-5-www-arm-com.log - acpidump > /mnt/acs_results/linux_dump/acpi.log - acpidump > /mnt/acs_results/linux_dump/acpi.dat - cd /mnt/acs_results/linux_dump - acpixtract -a acpi.dat > acpixtract.log 2>&1 - iasl -d *.dat > iasl.log 2>&1 + # Linux dump with ethtool and blk devices script run + cd /usr/bin + ./linux_init.sh --mode acs cd - - ORIG_SYS_TIME="$(date '+%Y-%m-%d %H:%M:%S')" - date --set="20221215 05:30" > /mnt/acs_results/linux_dump/date-set-202212150530.log - date > /mnt/acs_results/linux_dump/date-after-set.log - hwclock --set --date "2023-01-01 09:10:15" > /mnt/acs_results/linux_dump/hw-clock-set-20230101091015.log - hwclock > /mnt/acs_results/linux_dump/hwclock-after-set.log - date --set="$ORIG_SYS_TIME" 2>/dev/null || true - hwclock --systohc 2>/dev/null || true - ls -lR /sys/firmware > /mnt/acs_results/linux_dump/firmware.log - cp -r /sys/firmware /mnt/acs_results/linux_dump/ >> firmware.log 2>&1 - ipmitool -C 17 -N 3 -p 623 mc info > /mnt/acs_results/linux_dump/ipmitool.log 2>&1 - # Capturing System PSCI command output - mkdir -p /mnt/acs_results/linux_tools/psci - mount -t debugfs none /sys/kernel/debug - cat /sys/kernel/debug/psci > /mnt/acs_results/linux_tools/psci/psci.log - dmesg | grep psci > /mnt/acs_results/linux_tools/psci/psci_kernel.log - sync /mnt - sleep 5 - echo "Linux Debug Dump - Completed" + sleep 2 # Linux Device Driver script run echo "Running Device Driver Matching Script" - cd /usr/bin/ + cd /usr/bin ./device_driver_sr.sh > /mnt/acs_results/linux_dump/device_driver.log cd - - echo "Device Driver script run completed" + echo "Device Driver script run - Completed" sync /mnt sleep 5 diff --git a/common/linux_scripts/linux_dump.sh b/common/linux_scripts/linux_dump.sh new file mode 100644 index 00000000..182aa95a --- /dev/null +++ b/common/linux_scripts/linux_dump.sh @@ -0,0 +1,115 @@ +#!/bin/sh + +# @file +# Copyright (c) 2026, Arm Limited or its affiliates. All rights reserved. +# SPDX-License-Identifier : Apache-2.0 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +MODE="${1:-os}" +LOG_DIR="${2:-}" + +if [ -z "$LOG_DIR" ]; then + echo "ERROR: LOG_DIR argument missing" + exit 1 +fi + +mkdir -p "$LOG_DIR" +cd "$LOG_DIR" || exit 1 + +echo "Collecting Linux Debug Dump" + +if [ "$MODE" = "acs" ]; then + ORIG_SYS_TIME="$(date '+%Y-%m-%d %H:%M:%S')" +fi + +dmesg > dmesg.txt 2>&1 +lspci > lspci.txt 2>&1 +lspci -vvv > lspci-vvv.txt 2>&1 +cat /proc/interrupts > cat-proc-interrupts.txt 2>&1 +cat /proc/cpuinfo > cat-proc-cpuinfo.txt 2>&1 +cat /proc/meminfo > cat-proc-meminfo.txt 2>&1 +cat /proc/iomem > cat-proc-iomem.txt 2>&1 +lscpu > lscpu.txt 2>&1 +lsblk > lsblk.txt 2>&1 +lsusb > lsusb.txt 2>&1 +lshw > lshw.txt 2>&1 + +dmidecode > dmidecode.txt 2>&1 +dmidecode --dump-bin dmidecode.bin >> dmidecode.txt 2>&1 + +uname -a > uname-a.txt 2>&1 +cat /etc/os-release > cat-etc-os-release.txt 2>&1 +date > date.txt 2>&1 + +if [ "$MODE" = "os" ]; then + timedatectl > timedatectl.txt 2>&1 +fi + +cat /proc/driver/rtc > cat-proc-driver-rtc.txt 2>&1 +hwclock > hwclock.txt 2>&1 + +efibootmgr > efibootmgr.txt 2>&1 +efibootmgr -t 20 > efibootmgr-t-20.txt 2>&1 +efibootmgr -t 5 > efibootmgr-t-5.txt 2>&1 +efibootmgr -c > efibootmgr-c.txt 2>&1 + +ifconfig > ifconfig.txt 2>&1 +ip addr show > ip-addr-show.txt 2>&1 +ping -c 5 www.arm.com > ping-c-5-www-arm-com.txt 2>&1 + +cat /proc/cmdline > cat-proc-cmdline.txt 2>&1 +df -h > df-h.txt 2>&1 +mount > mount.txt 2>&1 +lsmod > lsmod.txt 2>&1 + +acpidump > acpi.log 2>&1 +acpixtract -a acpi.log > acpixtract.txt 2>&1 +iasl -d *.dat > iasl.txt 2>&1 + +date --set="20221215 05:30" > date-set-202212150530.txt 2>&1 +date > date-after-set.txt 2>&1 + +hwclock --set --date "2023-01-01 09:10:15" > hw-clock-set-20230101091015.txt 2>&1 +hwclock > hwclock-after-set.txt 2>&1 + +ls -lR /sys/firmware > firmware.txt 2>&1 +cp -r /sys/firmware . >> firmware.txt 2>&1 + +if [ "$MODE" = "acs" ]; then + ipmitool -C 17 -N 3 -p 623 mc info > ipmitool.txt 2>&1 + + mount -t debugfs none /sys/kernel/debug > debugfs-mount.txt 2>&1 || true + cat /sys/kernel/debug/psci > psci.txt 2>&1 + dmesg | grep -i psci > psci-kernel.txt 2>&1 + + date --set="$ORIG_SYS_TIME" > date-restore-original.txt 2>&1 || true + hwclock --systohc > hwclock-systohc.txt 2>&1 || true +fi + +if [ "$MODE" = "os" ]; then + echo "Restoring time sync..." > time-sync-restore.txt + + if systemctl list-unit-files 2>/dev/null | grep -q chronyd; then + systemctl restart chronyd >> time-sync-restore.txt 2>&1 + chronyc -a makestep >> time-sync-restore.txt 2>&1 + elif systemctl list-unit-files 2>/dev/null | grep -q systemd-timesyncd; then + systemctl restart systemd-timesyncd >> time-sync-restore.txt 2>&1 + else + echo "No known time sync service found" >> time-sync-restore.txt + fi + + sleep 10 +fi + +echo "Linux Debug Dump - Completed" diff --git a/common/linux_scripts/linux_init.sh b/common/linux_scripts/linux_init.sh new file mode 100644 index 00000000..82a7505e --- /dev/null +++ b/common/linux_scripts/linux_init.sh @@ -0,0 +1,228 @@ +#!/bin/sh + +# @file +# Copyright (c) 2026, Arm Limited or its affiliates. All rights reserved. +# SPDX-License-Identifier : Apache-2.0 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +MODE="auto" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LINUX_DUMP_SH="$SCRIPT_DIR/linux_dump.sh" + +usage() { + cat </dev/null 2>&1; then + apt-get update + apt-get install -y acpica-tools pciutils usbutils dmidecode lshw efibootmgr net-tools iproute2 iputils-ping util-linux ethtool python3 gdisk wget curl + elif command -v dnf >/dev/null 2>&1; then + dnf check-update || true + dnf install -y acpica-tools pciutils usbutils dmidecode lshw efibootmgr net-tools iproute iputils util-linux ethtool python3 gdisk wget curl + elif command -v yum >/dev/null 2>&1; then + yum check-update || true + yum install -y acpica-tools pciutils usbutils dmidecode lshw efibootmgr net-tools iproute iputils util-linux ethtool python3 gdisk wget curl --nogpgcheck + elif command -v zypper >/dev/null 2>&1; then + zypper modifyrepo --all -e + zypper refresh + zypper install -y acpica pciutils usbutils dmidecode lshw efibootmgr net-tools iproute2 iputils util-linux ethtool python3 gdisk wget curl + else + echo "Unknown package manager. Continuing without installing packages." + fi +} + +run_linux_dump() { + if [ ! -f "$LINUX_DUMP_SH" ]; then + if [ -f /usr/bin/linux_dump.sh ]; then + LINUX_DUMP_SH="/usr/bin/linux_dump.sh" + else + echo "linux_dump.sh not found" + exit 1 + fi + fi + + sh "$LINUX_DUMP_SH" "$MODE" "$LOG_DIR" +} + +run_block_device_check() { + echo "Running BLK devices read and write check" + + if [ "$MODE" = "acs" ]; then + python3 /usr/bin/read_write_check_blk_devices.py "$LOG_DIR/ethtool-test.log" + rm -f "$LOG_DIR/ethtool-test-temp.log" + + echo "Ethtool script run - Completed" +} + +create_os_archive() { + if [ "$MODE" = "os" ]; then + OLD_PWD=$(pwd) + cd "$SCRIPT_DIR" || exit 1 + + tar -czvf systemready-band-compliance-logs.tar.gz \ + -C "$REAL_HOME" systemready-band-compliance-logs + + cd "$OLD_PWD" || true + echo "Created $SCRIPT_DIR/systemready-band-compliance-logs.tar.gz" + fi +} + +print_os_copy_instructions() { + if [ "$MODE" = "os" ]; then + echo "" + echo "============================================================" + echo "OS run completed." + echo "" + echo "Generated OS logs directory:" + echo " $LOG_DIR" + echo "" + echo "Generated archive:" + echo " $SCRIPT_DIR/systemready-band-compliance-logs.tar.gz" + echo "" + echo "Copy the generated OS logs into the ACS results template at:" + echo " acs_results_template/os-logs//systemready-band-compliance-logs/" + echo "" + echo "Examples:" + echo " acs_results_template/os-logs/linux-redhat/systemready-band-compliance-logs/" + echo " acs_results_template/os-logs/linux-opensuse/systemready-band-compliance-logs/" + echo "" + echo "The ACS parser expects OS logs under:" + echo " os-logs/linux-*/systemready-band-compliance-logs/" + echo "============================================================" + echo "" + fi +} + +sync_results() { + if [ "$MODE" = "acs" ]; then + sync /mnt 2>/dev/null || sync + sleep 5 + else + sync + fi +} + +echo "linux_init.sh run started" +echo "Mode: $MODE" +echo "Log directory: $LOG_DIR" + +if [ "$MODE" = "os" ]; then + install_os_tools +fi + +run_linux_dump +sync_results + +run_block_device_check +sync_results + +run_ethtool_check +sync_results + +create_os_archive +print_os_copy_instructions + +echo "linux_init.sh run completed" diff --git a/common/linux_scripts/read_write_check_blk_devices.py b/common/linux_scripts/read_write_check_blk_devices.py new file mode 100644 index 00000000..dc697cff --- /dev/null +++ b/common/linux_scripts/read_write_check_blk_devices.py @@ -0,0 +1,524 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025-2026, Arm Limited or its affiliates. All rights reserved. +# SPDX-License-Identifier : Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Read block devices and optionally perform safe block write checks. + +The script detects block devices, identifies MBR/GPT/raw devices, skips known +precious partitions, performs a block read test, and optionally performs a +single-block write/restore verification on non-precious partitions. +""" + +import hashlib +import os +import re +import subprocess +import sys +import threading + + +# Precious partitions dictionary. This is a set of partition types that might +# contain firmware and should be skipped for read/write operations. The list is +# not exhaustive and may see additions in future. +PRECIOUS_PARTS_MBR = { + "Protective partition": "0xF8", + "EFI system partition": "0xEF", +} + +PRECIOUS_PARTS_GPT = { + "EFI System partition": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "BIOS boot partition": "21686148-6449-6E6F-744E-656564454649", + "U-Boot environment partition": "3DE21764-95BD-54BD-A5C3-4ABE786F38A8", +} + +SEPARATOR = ( + "\n" + "****************************************************************" + "****************************************************************" + "\n" +) + + +def run_command(command_args, check=False): + """Run a command and return the completed process.""" + return subprocess.run( + command_args, + text=True, + check=check, + capture_output=True, + ) + + +def input_with_timeout(prompt, timeout=5): + """Read user input with a timeout and return 'no' when no input is given.""" + print(prompt, end="", flush=True) + input_queue = [] + + def get_user_input(): + try: + input_queue.append(sys.stdin.readline().strip()) + except EOFError: + pass + + user_input_thread = threading.Thread(target=get_user_input) + user_input_thread.daemon = True + user_input_thread.start() + user_input_thread.join(timeout) + + return input_queue[0] if input_queue else "no" + + +def calculate_sha256(file_path): + """Calculate and return the SHA256 checksum of a file.""" + sha256_hash = hashlib.sha256() + + with open(file_path, "rb") as file_obj: + for byte_block in iter(lambda: file_obj.read(4096), b""): + sha256_hash.update(byte_block) + + return sha256_hash.hexdigest() + + +def get_partition_space(partition_path): + """Return used and available 512-byte blocks for a partition.""" + command_result = run_command( + ["df", "-B", "512", partition_path, "--output=used,avail"], + check=True, + ) + output_lines = command_result.stdout.strip().split("\n") + + if len(output_lines) > 1: + fields = output_lines[1].split() + used_block_count = int(fields[0]) + available_block_count = int(fields[1]) + return used_block_count, available_block_count + + print(f"WARNING: Unable to parse partition space for {partition_path}.") + return 0, 0 + + +def is_mounted(device): + """Return True if the given device is mounted.""" + command_result = run_command(["findmnt", "-n", device]) + return command_result.returncode == 0 + + +def is_mtd_block_device(device): + """Return True if the device is an MTD block device.""" + return device.startswith("mtdblock") + + +def is_ram_disk(device): + """Return True if the device is a RAM disk.""" + return device.startswith("ram") + + +def create_hello_file(file_path): + """Create a 512-byte test file and return its SHA256 checksum.""" + hello_content = "Hello!".ljust(512, "\x00") + + with open(file_path, "wb") as file_obj: + file_obj.write(hello_content.encode("utf-8")) + + return calculate_sha256(file_path) + + +def cleanup_files(file_paths): + """Remove temporary files if they exist.""" + for file_path in file_paths: + if os.path.exists(file_path): + os.remove(file_path) + + +def restore_backup(partition_label, backup_filename, used_blocks): + """Restore the backed-up block to the target partition.""" + print("INFO: Restoring the backup to the device after write check...") + run_command(["dd", f"if={backup_filename}", f"of=/dev/{partition_label}", + "bs=512", "count=1", f"seek={used_blocks}"], check=True) + print(f"INFO: Backup restored for /dev/{partition_label}.") + + +def perform_write_check(partition_label, partition_id, precious_parts): + """Optionally perform a single-block write/read/restore check.""" + device_path = f"/dev/{partition_label}" + + if is_mounted(device_path): + print(f"INFO: {device_path} is mounted, skipping write test.") + return + + user_input = "no" + if partition_id not in precious_parts.values(): + user_input = input_with_timeout( + f"Do you want to perform a write check on {device_path}? (yes/no): ", + 5, + ).lower() + + if user_input != "yes" or partition_id in precious_parts.values(): + return + + used_blocks, available_blocks = get_partition_space(device_path) + if available_blocks <= 0: + print( + f"WARNING: No available space for write check on {device_path}. " + "Skipping write check." + ) + return + + hello_file = "hello.txt" + read_back_file = "read_hello.txt" + backup_filename = f"{partition_label}_backup.bin" + + original_sha256 = create_hello_file(hello_file) + + print("INFO: Creating backup of the current block before write check...") + run_command(["dd", f"if={device_path}", f"of={backup_filename}", + "bs=512", "count=1", f"skip={used_blocks}"], check=True) + + print("INFO: Writing test data to the device for write check...") + run_command(["dd", f"if={hello_file}", f"of={device_path}", + "bs=512", "count=1", f"seek={used_blocks}"], check=True) + + print("INFO: Reading back the test data for verification...") + run_command(["dd", f"if={device_path}", f"of={read_back_file}", + "bs=512", "count=1", f"skip={used_blocks}"], check=True) + + read_back_sha256 = calculate_sha256(read_back_file) + + print(f"Original SHA256: {original_sha256}") + print(f"Read-back SHA256: {read_back_sha256}") + + if original_sha256 == read_back_sha256: + print(f"INFO: write check passed on {device_path}.") + else: + print(f"INFO: write check failed on {device_path}.") + print( + f"WARNING: Data integrity check failed for {device_path}. " + "Possible data corruption." + ) + + restore_backup(partition_label, backup_filename, used_blocks) + cleanup_files([hello_file, read_back_file, backup_filename]) + + +def get_partition_labels(disk): + """Return partition labels from lsblk without relying on Unicode output.""" + command_result = run_command( + ["lsblk", "-rn", "-o", "NAME,TYPE", f"/dev/{disk}"] + ) + labels = [] + + for line in command_result.stdout.splitlines(): + columns = line.split() + if len(columns) >= 2 and columns[1] == "part": + labels.append(columns[0]) + + return labels + + +def get_disks(): + """Return detected disk block devices.""" + command_result = run_command( + ["lsblk", "-e", "7", "-d", "-n", "-o", "NAME,TYPE"] + ) + disks = [] + + for line in command_result.stdout.splitlines(): + columns = line.split() + if len(columns) >= 2 and columns[1] == "disk": + disks.append(columns[0]) + + return disks + + +def get_partition_table_type(disk): + """Return MBR, GPT, or RAW for the given disk.""" + command_result = run_command( + ["timeout", "10", "gdisk", "-l", f"/dev/{disk}"] + ) + + if "MBR: MBR only" in command_result.stdout: + return "MBR" + + if "GPT: present" in command_result.stdout: + return "GPT" + + print(f"INFO: No valid partition table found for {disk}, treating as raw device.") + return "RAW" + + +def get_partition_count(disk): + """Return the number of partitions detected on the disk.""" + return len(get_partition_labels(disk)) + + +def read_block(partition_label): + """Perform a block read test for the given partition label.""" + command_result = run_command(["dd", f"if=/dev/{partition_label}", + "of=/dev/null", "bs=1M", "count=1"]) + return command_result.returncode == 0 + + +def process_raw_device(disk): + """Process a raw block device with no partition table.""" + print(f"INFO: No partitions detected for {disk}, treating as raw device.") + + print(f"INFO: Performing block read on /dev/{disk}") + if read_block(disk): + print(f"INFO: Block read on /dev/{disk} successful") + perform_write_check(disk, "", {}) + else: + print(f"INFO: Block read on /dev/{disk} failed") + + print(SEPARATOR) + + +def parse_mbr_partition_ids(disk): + """Parse MBR partition IDs from fdisk output.""" + table_header_row = [ + "Device", "Boot", "Start", "End", "Sectors", "Size", "Id", "Type", + ] + command_result = run_command(["fdisk", "-l", f"/dev/{disk}"]) + output_lines = command_result.stdout.strip().split("\n") + should_collect_lines = False + mbr_part_ids = [] + + for line in output_lines: + if should_collect_lines: + columns = line.split() + if len(columns) >= 6: + if columns[1] == "*" and len(columns) >= 7: + mbr_part_ids.append("0x" + columns[6].upper()) + else: + mbr_part_ids.append("0x" + columns[5].upper()) + elif all(substring in line for substring in table_header_row): + should_collect_lines = True + + return mbr_part_ids + + +def print_precious_partition_info(partition_label, partition_id, precious_parts): + """Print details for a precious partition.""" + used_blocks, _ = get_partition_space(f"/dev/{partition_label}") + + for key, value in precious_parts.items(): + if value == partition_id: + print(f"INFO: {partition_label} partition is PRECIOUS") + print( + f"INFO: Number of 512B blocks used on /dev/{partition_label}: " + f"{used_blocks}" + ) + print(f" {key} : {value}") + print(" Skipping block read/write...") + break + + +def process_mbr_disk(disk, partition_labels, num_parts): + """Process all MBR partitions on a disk.""" + mbr_part_ids = parse_mbr_partition_ids(disk) + + if len(mbr_part_ids) < num_parts: + print( + "WARNING: Could not parse enough MBR partition IDs. " + f"Found {len(mbr_part_ids)}, expected {num_parts}." + ) + + process_count = min(len(partition_labels), len(mbr_part_ids), num_parts) + + for index in range(process_count): + partition_label = partition_labels[index] + partition_id = mbr_part_ids[index] + + print(f"\nINFO: Partition : /dev/{partition_label} Partition type : {partition_id}") + + if partition_id in PRECIOUS_PARTS_MBR.values(): + print_precious_partition_info( + partition_label, + partition_id, + PRECIOUS_PARTS_MBR, + ) + continue + + print( + f"INFO: Performing block read on /dev/{partition_label} " + f"mbr_part_id = {partition_id}" + ) + + if read_block(partition_label): + print( + f"INFO: Block read on /dev/{partition_label} " + f"mbr_part_id = {partition_id} successful" + ) + perform_write_check(partition_label, partition_id, PRECIOUS_PARTS_MBR) + else: + print( + f"INFO: Block read on /dev/{partition_label} " + f"mbr_part_id = {partition_id} failed" + ) + + print(SEPARATOR) + + +def parse_gpt_partition_info(disk, partition_index): + """Return GPT partition GUID and platform-required bit.""" + command_result = run_command( + ["sgdisk", f"-i={partition_index + 1}", f"/dev/{disk}"] + ) + + guid_regex = r"Partition GUID code: ([\w-]+) \(" + attr_flag_regex = r"Attribute flags: ([0-9A-Fa-f]+)" + + guid_code_match = re.search(guid_regex, command_result.stdout) + attribute_flags_match = re.search(attr_flag_regex, command_result.stdout) + + if not guid_code_match or not attribute_flags_match: + return None, None + + partition_guid_code = guid_code_match.group(1) + attribute_flags_hex = attribute_flags_match.group(1) + attribute_flags_int = int(attribute_flags_hex, 16) + platform_required_bit = attribute_flags_int & 1 + + return partition_guid_code, platform_required_bit + + +def process_gpt_disk(disk, partition_labels, num_parts): + """Process all GPT partitions on a disk.""" + process_count = min(len(partition_labels), num_parts) + + for index in range(process_count): + partition_label = partition_labels[index] + partition_guid_code, platform_required_bit = parse_gpt_partition_info( + disk, index + ) + + if not partition_guid_code: + print(f"INFO: Unable to parse sgdisk info for {partition_label}. Skipping.") + continue + + print( + f"\nINFO: Partition : /dev/{partition_label} " + f"Partition type GUID : {partition_guid_code} " + f'"Platform required bit" : {platform_required_bit}' + ) + + if platform_required_bit == 1: + print( + f"INFO: Platform required attribute set for {partition_label} " + "partition, skipping block read/write..." + ) + continue + + if partition_guid_code in PRECIOUS_PARTS_GPT.values(): + print_precious_partition_info( + partition_label, + partition_guid_code, + PRECIOUS_PARTS_GPT, + ) + continue + + print( + f"INFO: Performing block read on /dev/{partition_label} " + f"part_guid = {partition_guid_code}" + ) + + if read_block(partition_label): + print( + f"INFO: Block read on /dev/{partition_label} " + f"part_guid = {partition_guid_code} successful" + ) + perform_write_check( + partition_label, + partition_guid_code, + PRECIOUS_PARTS_GPT, + ) + else: + print( + f"INFO: Block read on /dev/{partition_label} " + f"part_guid = {partition_guid_code} failed" + ) + + print(SEPARATOR) + + +def print_detected_disks(disks): + """Print detected block devices.""" + print(SEPARATOR) + print(" Read block devices tool") + print(SEPARATOR) + + print("INFO: Detected following block devices with lsblk command :") + for num, disk in enumerate(disks): + print(f"{num}: {disk}") + + print(SEPARATOR) + + +def process_disk(disk): + """Process a single disk block device.""" + if is_mtd_block_device(disk): + print(f"INFO: Skipping MTD block device /dev/{disk}") + return + + if is_ram_disk(disk): + print(f"INFO: Skipping RAM disk /dev/{disk}") + return + + print(f"INFO: Block device : /dev/{disk}") + + partition_table = get_partition_table_type(disk) + print(f"INFO: Partition table type : {partition_table}\n") + + num_parts = get_partition_count(disk) + + if partition_table == "RAW" or num_parts == 0: + process_raw_device(disk) + return + + partition_labels = get_partition_labels(disk) + + if len(partition_labels) < num_parts: + print( + "WARNING: Mismatch in partition count. " + f"Found {len(partition_labels)} partition labels, " + f"but lsblk reported {num_parts} partitions for {disk}. " + "Proceeding with the ones we have..." + ) + + if partition_table == "MBR": + process_mbr_disk(disk, partition_labels, num_parts) + elif partition_table == "GPT": + process_gpt_disk(disk, partition_labels, num_parts) + else: + print( + "INFO: Invalid partition table, expected MBR or GPT " + f"reported type = {partition_table}" + ) + + +def main(): + """Main entry point.""" + disks = get_disks() + print_detected_disks(disks) + + for disk in disks: + process_disk(disk) + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError, subprocess.SubprocessError) as error: + print(f"Error occurred: {error}") + sys.exit(1) diff --git a/common/log_parser/main_log_parser.sh b/common/log_parser/main_log_parser.sh index fcf5f868..82230ebf 100755 --- a/common/log_parser/main_log_parser.sh +++ b/common/log_parser/main_log_parser.sh @@ -81,12 +81,12 @@ mkdir -p "$ACS_SUMMARY_DIR" mkdir -p "$JSONS_DIR" #echo "Gathering ACS info into acs_info.txt and acs_info.json..." -IPMITOOL_LOG="$LOGS_PATH/linux_dump/ipmitool.log" +IPMITOOL_LOG="$LOGS_PATH/linux_dump/ipmitool.txt" python3 "$SCRIPTS_PATH/acs_info.py" \ --acs_config_path "$ACS_CONFIG_PATH" \ --system_config_path "$SYSTEM_CONFIG_PATH" \ --uefi_version_log "$LOGS_PATH/uefi_dump/uefi_version.log" \ - --dmidecode_log "$LOGS_PATH/linux_dump/dmidecode.log" \ + --dmidecode_log "$LOGS_PATH/linux_dump/dmidecode.txt" \ --ipmitool_log "$IPMITOOL_LOG" \ --output_dir "$JSONS_DIR" echo "" @@ -311,10 +311,10 @@ if check_file "$BBSR_SCT_LOG"; then if [ $YOCTO_FLAG_PRESENT -eq 1 ]; then - # EDK2 Log Parsing: Process the edk2-test-parser.log - if check_file "$BBSR_EDK2_PARSER_LOG"; then - python3 "$SCRIPTS_PATH/bbr/sct/logs_to_json_edk2.py" "$BBSR_EDK2_PARSER_LOG" "$BBSR_EDK2_PARSER_JSON" - fi + # EDK2 Log Parsing: Process the edk2-test-parser.log + if check_file "$BBSR_EDK2_PARSER_LOG"; then + python3 "$SCRIPTS_PATH/bbr/sct/logs_to_json_edk2.py" "$BBSR_EDK2_PARSER_LOG" "$BBSR_EDK2_PARSER_JSON" + fi fi python3 "$SCRIPTS_PATH/bbr/sct/logs_to_json.py" "$BBSR_SCT_LOG" "$BBSR_SCT_JSON" apply_waivers "BBSR-SCT" "$BBSR_SCT_JSON" diff --git a/common/log_parser/test_categoryDT.json b/common/log_parser/test_categoryDT.json index 751397cb..a55764d4 100644 --- a/common/log_parser/test_categoryDT.json +++ b/common/log_parser/test_categoryDT.json @@ -494,7 +494,7 @@ "Description": [ "The SystemReady Devicetree band recommends network boot support.", "Failures may have only minor effects, or may prevent an OS to boot from the network.", - "https://github.com/ARM-software/arm-systemready/blob/main/SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/install-files/files/ethtool-test.py" + "https://github.com/ARM-software/arm-systemready/blob/main/common/linux_scripts/ethtool-test.py" ], "specName": "SRS", "rel Import. to main readiness": "Minor", diff --git a/docs/ethtool_test_guide.md b/docs/ethtool_test_guide.md index 77bb9591..8ca0ddf9 100644 --- a/docs/ethtool_test_guide.md +++ b/docs/ethtool_test_guide.md @@ -4,7 +4,7 @@ This script automates testing and diagnostics of Ethernet interfaces on Linux-based systems. It evaluates the status, configuration, and connectivity of each interface using standard tools such as `ip`, `ethtool`, `ping`, `wget`, and `curl`. -When executed with a **SystemReady DT image**, the script runs automatically as part of the ACS (Architecture Compliance Suite) test framework with all dependencies. Output logs are generated at `linux_tools/ethtool-test.log` within the `acs_results` directory. +When executed with a **SystemReady image**, the script runs automatically as part of the ACS (Architecture Compliance Suite) test framework with all dependencies. Output logs are generated at `linux_tools/ethtool-test.log` within the `acs_results` directory. --- @@ -141,4 +141,4 @@ These messages indicate that the current interface failed validation and is bein - `WARN: Default route to 8.8.8.8 is via , not ; skipping further tests for ` -------------- -*Copyright (c) 2025, Arm Limited and Contributors. All rights reserved.* +*Copyright (c) 2025-2026, Arm Limited and Contributors. All rights reserved.*