diff --git a/.bazelrc b/.bazelrc index 234147d0b..bb45b59f6 100644 --- a/.bazelrc +++ b/.bazelrc @@ -50,20 +50,38 @@ common:x86_64-qnx --config=_toolchain_common common:x86_64-qnx --platforms=@score_bazel_platforms//:x86_64-qnx-sdp_8.0.0-posix common:x86_64-qnx --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_pc_nto_qnx800 common:x86_64-qnx --repo_env=TARGET_ARCH=x86_64 +common:x86_64-qnx --sandbox_writable_path=/var +common:x86_64-qnx --action_env=QNX_TARGET_ARCH=x86_64 +common:x86_64-qnx --spawn_strategy=local common:aarch64-qnx --config=_toolchain_common common:aarch64-qnx --platforms=@score_bazel_platforms//:aarch64-qnx-sdp_8.0.0-posix common:aarch64-qnx --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_aarch64_unknown_nto_qnx800 common:aarch64-qnx --repo_env=TARGET_ARCH=aarch64 +common:aarch64-qnx --sandbox_writable_path=/var/tmp +common:aarch64-qnx --action_env=QNX_TARGET_ARCH=aarch64le +common:aarch64-qnx --spawn_strategy=local common --credential_helper=*.qnx.com=%workspace%/.github/tools/qnx_credential_helper.py +common --action_env=QNXLM_LICENSE_FILE="6287@rb-lic-qnxlm.de.bosch.com" #to be removed + + test --test_output=errors build --incompatible_strict_action_env common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ common --registry=https://bcr.bazel.build +# ============================================================================ +# Integration Test Framework Configuration +# ============================================================================ + +build:qemu-linux --run_under=@score_itf//scripts:run_under_qemu +build:qemu-linux --test_arg="--qemu" +build:qemu-linux --test_arg="--os=linux" + + # ============================================================================ # Feature Flags & S-CORE Configuration # ============================================================================ diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..309338292 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +FROM ghcr.io/eclipse-score/devcontainer:latest + +# this basically allows QNX license to be checked for the authorized user inside the container . +# it cannot be done at runtime with for ex "sudo sed -i 's/^vscode:/:/' /etc/passwd" because docker will complain about "vscode" user not existing at next startup +ARG USERNAME=invalidusername + +# RENAME 'vscode' -> ${USERNAME} +RUN usermod -l ${USERNAME} vscode \ + && groupmod -n ${USERNAME} vscode \ + && usermod -d /home/${USERNAME} -m ${USERNAME} + +# GRANT SUDO , is someone using sudo in devcontainer ? +RUN echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} \ + && chmod 0440 /etc/sudoers.d/${USERNAME} diff --git a/.github/workflows/build_and_test_qemu.yml b/.github/workflows/build_and_test_qemu.yml new file mode 100644 index 000000000..13d27b664 --- /dev/null +++ b/.github/workflows/build_and_test_qemu.yml @@ -0,0 +1,55 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Workflow configuration for S-CORE CI - Bazel Build & Test module +# This workflow runs Bazel build and test when triggered by specific pull request events. + +name: Build & Test module (with qnx toolchain) +on: + pull_request: + types: [opened, reopened, synchronize] + merge_group: + types: [checks_requested] +jobs: + build_and_test_host: + # Starting with Ubuntu 24.04 apparmor breaks Bazels linux-sandbox: https://github.com/bazelbuild/bazel/issues/24081 + runs-on: ubuntu-22.04 + steps: + - name: Clean up disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc + sudo docker image prune --all --force + sudo docker builder prune -a + - name: Create Bazel output base directory + run: | + sudo mkdir -p /mnt/.bazel + sudo chown -R $USER:$USER /mnt/.bazel + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.15.0 + with: + output-base: /mnt/.bazel + # Avoid downloading Bazel every time. + bazelisk-cache: true + # Store build cache per workflow. + disk-cache: ${{ github.workflow }} + # Share repository cache between workflows. + repository-cache: true + # Cache external/ + external-cache: true + - name: Checkout repository + uses: actions/checkout@v4.2.2 + - run: df -h + - name: Bazel build targets + run: bazel test //tests/UT:test_ut --test_output=all --config=x86_64-qnx + - run: df -h + if: always() diff --git a/MODULE.bazel b/MODULE.bazel index 72869a643..e9f828cb8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -62,6 +62,15 @@ use_repo(pip, "someip_pip") bazel_dep(name = "googletest", version = "1.17.0", dev_dependency = True) bazel_dep(name = "google_benchmark", version = "1.9.4", dev_dependency = True) +bazel_dep(name = "score_itf", version = "0.1.0") +git_override( + module_name = "score_itf", + commit = "e994cb6", # Main branch with simplified QEMU plugin + patch_strip = 1, + patches = ["//third_party:score_itf_oci_conflict.patch"], + remote = "https://github.com/eclipse-score/itf.git", +) + # ============================================================================ # Build System Rules # ============================================================================ @@ -69,6 +78,9 @@ bazel_dep(name = "rules_cc", version = "0.2.14") bazel_dep(name = "rules_rust", version = "0.63.0") bazel_dep(name = "rules_pkg", version = "1.1.0") +# Required by score_toolchains_qnx qnx_ifs rule for tar extraction +bazel_dep(name = "tar.bzl", version = "0.7.0") + # Platforms: Standard constraint values and platform definitions # Required for select() expressions that use @platforms//cpu:* and @platforms//os:* bazel_dep(name = "platforms", version = "1.0.0") @@ -119,6 +131,7 @@ gcc.toolchain( use_default_package = True, version = "12.2.0", ) + gcc.toolchain( name = "score_qcc_aarch64_toolchain", sdp_version = "8.0.0", @@ -140,8 +153,31 @@ register_toolchains( "@score_gcc_aarch64_toolchain//:all", "@score_qcc_x86_64_toolchain//:all", "@score_qcc_aarch64_toolchain//:all", + "@toolchains_qnx_ifs//:all", dev_dependency = True, + "@score_toolchains_qnx+//toolchains/fs/ifs:all", +) + +# ============================================================================ +# QNX IFS Image Toolchain (for building QEMU filesystem images via mkifs) +# ============================================================================ +bazel_dep(name = "score_toolchains_qnx", version = "0.0.7", dev_dependency = True) + +toolchains_qnx = use_extension( + "@score_toolchains_qnx//:extensions.bzl", + "toolchains_qnx", + dev_dependency = True, +) +toolchains_qnx.sdp( + sha256 = "f2e0cb21c6baddbcb65f6a70610ce498e7685de8ea2e0f1648f01b327f6bac63", + strip_prefix = "installation", + url = "https://www.qnx.com/download/download/79858/installation.tgz", ) +use_repo(toolchains_qnx, "toolchains_qnx_ifs") +# register_toolchains( +# "@toolchains_qnx_ifs//:ifs_x86_64", +# dev_dependency = True, +# ) # ============================================================================ # LLVM C/C++ Toolchain Configuration @@ -263,7 +299,7 @@ http_archive( build_file = "@//:third_party/vsomeip.BUILD.bazel", integrity = "sha256-hMjkJs3fQ14MVtpwupfno/UbK+rxV+1zih9632bO5Sk=", patch_strip = 1, - patches = ["//:third_party/vsomeip-qnx8.patch"], + patches = ["//:third_party/vsomeip_qnx8.patch"], strip_prefix = "vsomeip-3.6.1", urls = [ "https://github.com/COVESA/vsomeip/archive/refs/tags/3.6.1.tar.gz", diff --git a/README.md b/README.md index 3b2d00bbe..d21cc2aa0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - # SOME/IP Gateway The gateway is divided into a gateway daemon (gatewayd) which contains the network-independent logic (payload serialization, etc.) and the SOME/IP daemon (someipd) which binds to the concrete SOME/IP stack. @@ -41,39 +40,32 @@ bazel run //examples/car_window_sim:car_window_controller If you type `open` or `close` the command will be sent via network. -### Dockerized integration test POC +### QEMU x86_64 - based integration test and unit tests -For integration tests, a docker based approach was taken. -As a proof of concept `docker compose` can be used to build, setup and run the containers. -In the future a pytest based setup can be implemented to orchestrate the containers. +For integration tests where the communication between two QEMU instances is required, a custom implementation is used to start and manage the QEMU instances within the test logic. This is because ITF does not support starting multiple QEMU instances in parallel yet. -Build the docker containers: +For the QEMU QNX x864 image to run on host please run the script deployment/qemu/setup_bridge.sh with sudo privileges to setup the required network bridge and tap interfaces. +It is stronly recommended to run all tests with `--nocache_test_results` which is the best way on development cycles to ensure you are always running the latest version of the tests and not accidentally running cached results. +Unit tests are defined by the bazel `test_ut` target: ```sh -docker compose --project-directory tests/integration/docker_setup/ build +bazel test //tests/UT:test_ut --test_output=all --config=x86_64-qnx ``` -Start up the containers: - +For Integration tests Host to QEMU communication: ```sh -docker compose --project-directory tests/integration/docker_setup/ up +bazel test //tests/integration:test_qemu_network_single --test_output=all --config=x86_64-qnx ``` -Those containers are pre-configured (IP adresses, multicast route, ...). -The someipd-1 container already starts up the `gatewayd` and the `someipd`. - -In Wireshark the network traffic can be seen by capturing on `any` with `ip.addr== 192.168.87.2 || ip.addr ==192.168.87.3`. - -On the client side, start up the `sample_client` in another shell: - +For integration tests QEMU to QEMU communication (dual instance test): ```sh -docker exec -it --env VSOMEIP_CONFIGURATION=/home/source/tests/integration/sample_client/vsomeip.json docker_setup-client-1 /home/source/bazel-bin/tests/integration/sample_client/sample_client +bazel test //tests/integration:test_qemu_network_dual --test_output=all --config=x86_64-qnx ``` -Finally start the benchmark on the someipd-1 container in a third shell: +For integration tests SOMEIP Service Discovery: ```sh -docker exec -it docker_setup-someipd-1 /home/source/bazel-bin/tests/performance_benchmarks/ipc_benchmarks +bazel test //tests/integration:test_someip_sd --test_output=all --config=x86_64-qnx ``` diff --git a/deployment/qemu/BUILD.bazel b/deployment/qemu/BUILD.bazel new file mode 100644 index 000000000..16392788e --- /dev/null +++ b/deployment/qemu/BUILD.bazel @@ -0,0 +1,164 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# ============================================================================= +# QNX QEMU Image for SOME/IP Gateway +# +# Builds a bootable QNX IFS (Image File System) containing the someipd and +# gatewayd daemons, then provides a target to run it under QEMU. +# +# Usage: +# # Build the IFS image +# bazel build //deployment/qemu:someip_gateway_ifs --config=x86_64-qnx +# +# # Run in QEMU (requires qemu-system-x86_64 + KVM on host) +# bazel run //deployment/qemu:run_qemu --config=x86_64-qnx +# ============================================================================= + +load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("@score_toolchains_qnx//rules/fs:ifs.bzl", "qnx_ifs") + +# Export scripts for use as data dependencies in tests +exports_files( + [ + "run_qemu.sh", + ], + visibility = ["//visibility:public"], +) + +# --------------------------------------------------------------------------- +# Package all gateway binaries + configs into a single tar for IFS inclusion +# --------------------------------------------------------------------------- +pkg_tar( + name = "gateway_binaries_pkg", + srcs = [ + "//src/gatewayd", + "//src/someipd", + "//tests/integration/sample_client", + "@vsomeip", + ], + include_runfiles = True, + package_dir = "/", +) + +# --------------------------------------------------------------------------- +# Package C++ unit test binaries for the IFS +# --------------------------------------------------------------------------- +pkg_tar( + name = "test_binaries_pkg", + srcs = [ + "//tests/UT/UnitTest_UT1:cpp_test_main1", + "//tests/UT/UnitTest_UT2:cpp_test_main2", + ], + include_runfiles = True, + package_dir = "/", + testonly = True, +) + +# --------------------------------------------------------------------------- +# Package gateway config files for the IFS +# --------------------------------------------------------------------------- +pkg_tar( + name = "gateway_configs_pkg", + srcs = [ + "//src/gatewayd:config_file", + "//src/gatewayd:etc/mw_com_config.json", + ], + strip_prefix = "/src/gatewayd/etc", +) + +# --------------------------------------------------------------------------- +# Package someipd config files for the IFS +# --------------------------------------------------------------------------- +pkg_tar( + name = "someipd_configs_pkg", + srcs = [ + "//src/someipd:etc/mw_com_config.json", + "//tests/integration:vsomeip.json", + ], + remap_paths = { + "/src/someipd/etc/mw_com_config.json": "mw_com_config.json", + "/tests/integration/vsomeip.json": "vsomeip.json", + }, +) + +# --------------------------------------------------------------------------- +# Package sample_client config files for the IFS +# --------------------------------------------------------------------------- +pkg_tar( + name = "sample_client_configs_pkg", + srcs = [ + "//tests/integration/sample_client:vsomeip.json", + ], + remap_paths = { + "/tests/integration/sample_client/vsomeip.json": "vsomeip.json", + }, +) + +# --------------------------------------------------------------------------- +# QNX IFS image: boots QNX 8.0 with someipd + gatewayd inside with networking +# --------------------------------------------------------------------------- +qnx_ifs( + name = "someip_gateway_ifs", + build_file = "init_x86_64.build", + out = "someip_gateway_x86_64.ifs", + srcs = [ + "//deployment/qemu/configs:network_configs", + "//deployment/qemu/configs:ssh_configs", + ], + tars = { + "GATEWAY_BINS": ":gateway_binaries_pkg", + "GATEWAY_CONFIGS": ":gateway_configs_pkg", + "SOMEIPD_CONFIGS": ":someipd_configs_pkg", + "SAMPLE_CLIENT_CONFIGS": ":sample_client_configs_pkg", + "TEST_BINS": ":test_binaries_pkg", + }, + testonly = True, + visibility = ["//visibility:public"], +) + +# --------------------------------------------------------------------------- +# Launch QEMU Instance 1 (SSH: 2222, qconn: 8000, dual networking) +# --------------------------------------------------------------------------- +sh_binary( + name = "run_qemu_1", + srcs = ["run_qemu.sh"], + args = [ + "$(location :someip_gateway_ifs)", + "1", + ], + data = [ + ":someip_gateway_ifs", + ], + env = {"QEMU_NET_MODE": "dual"}, + testonly = True, + visibility = ["//visibility:public"], +) + +# --------------------------------------------------------------------------- +# Launch QEMU Instance 2 (SSH: 2223, qconn: 8001, dual networking) +# --------------------------------------------------------------------------- +sh_binary( + name = "run_qemu_2", + srcs = ["run_qemu.sh"], + args = [ + "$(location :someip_gateway_ifs)", + "2", + ], + data = [ + ":someip_gateway_ifs", + ], + env = {"QEMU_NET_MODE": "dual"}, + testonly = True, + visibility = ["//visibility:public"], +) diff --git a/deployment/qemu/configs/BUILD.bazel b/deployment/qemu/configs/BUILD.bazel new file mode 100644 index 000000000..441e122d2 --- /dev/null +++ b/deployment/qemu/configs/BUILD.bazel @@ -0,0 +1,53 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +exports_files([ + "pci_server.cfg", + "pci_hw.cfg", + "dhcpcd.conf", + "network_setup.sh", + "startup.sh", + "qcrypto.conf", + "sshd_config", + "ssh_host_rsa_key", + "ssh_host_rsa_key.pub", + "passwd", + "group", + "profile", +]) + +filegroup( + name = "network_configs", + srcs = [ + "dhcpcd.conf", + "network_setup.sh", + "pci_hw.cfg", + "pci_server.cfg", + "qcrypto.conf", + "startup.sh", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "ssh_configs", + srcs = [ + "sshd_config", + "ssh_host_rsa_key", + "ssh_host_rsa_key.pub", + "passwd", + "group", + "profile", + ], + visibility = ["//visibility:public"], +) diff --git a/deployment/qemu/configs/dhcpcd.conf b/deployment/qemu/configs/dhcpcd.conf new file mode 100644 index 000000000..8a2ce0b86 --- /dev/null +++ b/deployment/qemu/configs/dhcpcd.conf @@ -0,0 +1,42 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + + +# ******************************************************************************* +# Minimal DHCP Client Configuration for QNX QEMU System +# ******************************************************************************* +# This is a minimal dhcpcd.conf file for basic DHCP functionality + +# Allow all interfaces starting with vtnet +allowinterfaces vtnet0 + +# Use the hardware address of the interface for the Client ID +duid + +# Request essential network configuration +option routers +option domain_name_servers +option subnet_mask +option broadcast_address + +# Set timeout for DHCP requests (30 seconds) +timeout 30 + +# Disable IPv6 to simplify configuration +noipv6 + +# Don't send hostname for privacy +nohook hostname + +# Background after getting lease +background diff --git a/deployment/qemu/configs/group b/deployment/qemu/configs/group new file mode 100644 index 000000000..5a496551b --- /dev/null +++ b/deployment/qemu/configs/group @@ -0,0 +1,3 @@ +root::0:root +qnxuser::1000:qnxuser +sshd::6:sshd diff --git a/deployment/qemu/configs/network_setup.sh b/deployment/qemu/configs/network_setup.sh new file mode 100644 index 000000000..21119cdfc --- /dev/null +++ b/deployment/qemu/configs/network_setup.sh @@ -0,0 +1,99 @@ +#!/bin/sh +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + + +# ******************************************************************************* +# Network Configuration Script for QEMU Bridge Networking +# +# This script configures the network interface for bridge mode. +# Both QEMU instances are on the same L2 network (virbr0 bridge). +# +# Network layout: +# Bridge (host): virbr0 - 192.168.122.1/24 +# vtnet0: +# Instance 1: 192.168.122.2 (MAC: 52:54:00:12:34:01) +# Instance 2: 192.168.122.3 (MAC: 52:54:00:12:34:02) +# Gateway: 192.168.122.1 +# Netmask: 255.255.255.0 +# +# Both instances can communicate directly on the same L2 network, +# including SOME/IP multicast traffic. +# ******************************************************************************* + +# Detect instance ID from MAC address +# QEMU sets unique MACs: Instance 1: 52:54:00:12:34:01, Instance 2: 52:54:00:12:34:02 +INSTANCE_ID=1 + +echo "---> Starting Networking" +io-sock -m phy -m pci -d vtnet_pci # Start network stack with PHY and PCI modules, load VirtIO network driver + +echo "---> Waiting for socket device..." +RETRY=0 +while [ $RETRY -lt 10 ] && [ ! -e /dev/socket ]; do + sleep 1 + RETRY=$((RETRY + 1)) +done + +if [ ! -e /dev/socket ]; then + echo "---> ERROR: /dev/socket not available after 10 seconds" + exit 1 +fi +echo "---> Socket device ready" + +# Bring up vtnet0 +echo "---> Bringing up vtnet0" +if_up -p vtnet0 + +# Detect instance ID from vtnet0 MAC address +VTNET0_MAC=$(ifconfig vtnet0 2>/dev/null | grep ether | awk '{print $2}') +case "$VTNET0_MAC" in + *:01) INSTANCE_ID=1 ;; + *:02) INSTANCE_ID=2 ;; + *:03) INSTANCE_ID=3 ;; +esac +echo "---> Detected instance ${INSTANCE_ID} from MAC: ${VTNET0_MAC}" + +# Calculate IP address: 192.168.87.(1 + INSTANCE_ID) +# Instance 1: 192.168.87.2, Instance 2: 192.168.87.3 +GUEST_IP="192.168.87.$((1 + INSTANCE_ID))" +GATEWAY_IP="192.168.87.1" + +echo "---> Configuring vtnet0: ${GUEST_IP}" +ifconfig vtnet0 ${GUEST_IP} netmask 255.255.255.0 + +echo "---> Adding default route via ${GATEWAY_IP}" +route add default ${GATEWAY_IP} + +# Enable multicast support for SOME/IP Service Discovery +echo "---> Enabling multicast support" +ifconfig vtnet0 multicast 2>/dev/null || true +ifconfig vtnet0 allmulti 2>/dev/null || true + +# Add multicast route for SOME/IP Service Discovery (224.0.0.0/4) +echo "---> Adding multicast route via vtnet0" +route add 224.0.0.0 -netmask 240.0.0.0 -iface vtnet0 2>/dev/null || true + +# Configure system network settings for multicast support +sysctl -w net.inet.icmp.bmcastecho=1 > /dev/null 2>&1 +sysctl -w net.inet.ip.mforwarding=1 > /dev/null 2>&1 +sysctl -w net.inet.udp.recvspace=262144 > /dev/null 2>&1 + +echo "---> Network configuration completed" +echo "---> Instance: ${INSTANCE_ID}" +echo "---> vtnet0: ${GUEST_IP}, Gateway: ${GATEWAY_IP}" + +# Save status +echo "INSTANCE_ID=${INSTANCE_ID}" > /tmp_discovery/network_status 2>/dev/null || true +echo "VTNET0_IP=${GUEST_IP}" >> /tmp_discovery/network_status 2>/dev/null || true +echo "GATEWAY_IP=${GATEWAY_IP}" >> /tmp_discovery/network_status 2>/dev/null || true diff --git a/deployment/qemu/configs/passwd b/deployment/qemu/configs/passwd new file mode 100644 index 000000000..450ce5908 --- /dev/null +++ b/deployment/qemu/configs/passwd @@ -0,0 +1,3 @@ +root::0:0:Superuser:/:/bin/sh +qnxuser::1000:1000:QNX User:/:/bin/sh +sshd::15:6:sshd:/:/bin/false diff --git a/deployment/qemu/configs/pci_hw.cfg b/deployment/qemu/configs/pci_hw.cfg new file mode 100644 index 000000000..4a619d5ee --- /dev/null +++ b/deployment/qemu/configs/pci_hw.cfg @@ -0,0 +1,9 @@ +[interrupts] +B0:D17:F0 A 18 +B0:D17:F0 B 19 +B0:D17:F0 C 16 +B0:D17:F0 D 17 +B3:D0:F0 A 18 +B3:D0:F0 B 19 +B3:D0:F0 C 16 +B3:D0:F0 D 17 diff --git a/deployment/qemu/configs/pci_server.cfg b/deployment/qemu/configs/pci_server.cfg new file mode 100644 index 000000000..899a56ba9 --- /dev/null +++ b/deployment/qemu/configs/pci_server.cfg @@ -0,0 +1,7 @@ +[buscfg] +DO_BUS_CONFIG=no + +[envars] +PCI_DEBUG_MODULE=pci_debug2.so +PCI_HW_MODULE=pci_hw-Intel_x86.so +PCI_HW_CONFIG_FILE=/proc/boot/pci_hw.cfg diff --git a/deployment/qemu/configs/profile b/deployment/qemu/configs/profile new file mode 100644 index 000000000..0f247b301 --- /dev/null +++ b/deployment/qemu/configs/profile @@ -0,0 +1,18 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# System-wide profile for interactive shells +export PATH=/proc/boot:/bin:/usr/bin:/sbin:/usr/sbin +export LD_LIBRARY_PATH=/proc/boot:/usr/lib +export TERM=qansi +export PS1='# ' diff --git a/deployment/qemu/configs/qcrypto.conf b/deployment/qemu/configs/qcrypto.conf new file mode 100644 index 000000000..b65b4c4a0 --- /dev/null +++ b/deployment/qemu/configs/qcrypto.conf @@ -0,0 +1 @@ +openssl-3 tags=* diff --git a/deployment/qemu/configs/ssh_host_rsa_key b/deployment/qemu/configs/ssh_host_rsa_key new file mode 100644 index 000000000..03f5f7517 --- /dev/null +++ b/deployment/qemu/configs/ssh_host_rsa_key @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEAtHES+7sXeeULpZMCKHqDTTCay5TpEKGgeW9knj+UHhXm+2d5jEBJ +LbwZi9mlmoZo9yLqMqezb1M5zIGT/Qr/Q5tD0jTRQKwaRVwDVYBsKhd+4/ISquzDffY+o8 +uahTKFxBtc41ubiC9Z1k74WV3eMztOMzC/BAwRJrEx5w6BkgyiC3T3uY3cHE7oeIO/cibi +2n86txOMOeEgA642xpgiHE8WEwmxQ4jZbyFKVb6V5KqqTow3SuPtNV8jtMagMyKhskjeRQ +V9NkmTM5YIXwYp0DL4U5HV7Ga5Xdf50fAeXo/ac8N9N5BwLHAa4ibd0RLs+XNTAeqp7eq5 +8R+Ww5zW8wAAA9g3Fhx/NxYcfwAAAAdzc2gtcnNhAAABAQC0cRL7uxd55QulkwIoeoNNMJ +rLlOkQoaB5b2SeP5QeFeb7Z3mMQEktvBmL2aWahmj3Iuoyp7NvUznMgZP9Cv9Dm0PSNNFA +rBpFXANVgGwqF37j8hKq7MN99j6jy5qFMoXEG1zjW5uIL1nWTvhZXd4zO04zML8EDBEmsT +HnDoGSDKILdPe5jdwcTuh4g79yJuLafzq3E4w54SADrjbGmCIcTxYTCbFDiNlvIUpVvpXk +qqpOjDdK4+01XyO0xqAzIqGySN5FBX02SZMzlghfBinQMvhTkdXsZrld1/nR8B5ej9pzw3 +03kHAscBriJt3REuz5c1MB6qnt6rnxH5bDnNbzAAAAAwEAAQAAAQANEjnhUXLIaj63PliE +LSfQ0ioMgFuH0GWLFnYv/oQQz95dLOhBsDB85yjkmB4mfQEikf1sMPjPIjTIK0TYqlDGiX +tWwWu9Jxp6FsnJClYLkIspIRKYHjGlMM7l16x4ZKoLBD5s2+h+64sy+qKD+shLZsrp62iS +9vrRFjn6tnLNbQyKm852vJFHvD4RqyC3UH2ZCf/BpWza9kVOfKyN5CtrJkwfmINdApFLv2 +EHZqCDBNjbTZxBtl30lY/odK8KXkG9XO9y2jACLzguLrWErB6Ridj+xwPfaha0XjaQuPj1 +Nv3zSbkA75rztIOo2lug+g4d2A1MEplO/Y1iaxbenmaxAAAAgCT1YkPQbJaScRXBsOH9il +3lsufjwzGA7pJQ6F44fUEJU6lJTzmzGmqxyvQpbomPqPPLhZq7MgkZXAY4ZU0uO0d8x2OR +5ilQXymFldjARQcxT4rw6BzQuTwPybVrlRQ9k7to8MA5Ymp20yxgMhAxkW3MtuwGtmutqY +HwOxVq7NmWAAAAgQDusWQCQdjJjLp+39J2tLL5/jQaAh1ZsyCHGfMoMzPWw1UbjJmcGLmT +rEOW3BgAGpBgfA/ifQ/i1EgerwPy/E/kS4zlhAdY1/WiZIkUB/NCFwElGkvDyAjCNKXDZR +s69kBvsDXqtdFAgtWKv65QjBS0HsGA1+XzC6OJuOw0z8kI0QAAAIEAwYZtHAwaH57yVIRv +C7qqIlBifMy7ZOWK2TPSdRR/pMMIF3zVB2pjhIK989E9jC7raK9iLOItdoZSqLSFNFe6LA +uKdBZmTVR5gWHIB6aoLac3Phe0vW7BjhgUekONKSyDHtM1Vp3col4BX+8+NT+5wvvZJndZ +irdPEWCJX919FIMAAAAccWVtdS1zb21laXAtZ2F0ZXdheS1ob3N0LWtleQECAwQFBgc= +-----END OPENSSH PRIVATE KEY----- diff --git a/deployment/qemu/configs/ssh_host_rsa_key.pub b/deployment/qemu/configs/ssh_host_rsa_key.pub new file mode 100644 index 000000000..09e4b2c04 --- /dev/null +++ b/deployment/qemu/configs/ssh_host_rsa_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0cRL7uxd55QulkwIoeoNNMJrLlOkQoaB5b2SeP5QeFeb7Z3mMQEktvBmL2aWahmj3Iuoyp7NvUznMgZP9Cv9Dm0PSNNFArBpFXANVgGwqF37j8hKq7MN99j6jy5qFMoXEG1zjW5uIL1nWTvhZXd4zO04zML8EDBEmsTHnDoGSDKILdPe5jdwcTuh4g79yJuLafzq3E4w54SADrjbGmCIcTxYTCbFDiNlvIUpVvpXkqqpOjDdK4+01XyO0xqAzIqGySN5FBX02SZMzlghfBinQMvhTkdXsZrld1/nR8B5ej9pzw303kHAscBriJt3REuz5c1MB6qnt6rnxH5bDnNbz qemu-someip-gateway-host-key diff --git a/deployment/qemu/configs/sshd_config b/deployment/qemu/configs/sshd_config new file mode 100644 index 000000000..e55bd03a0 --- /dev/null +++ b/deployment/qemu/configs/sshd_config @@ -0,0 +1,15 @@ +Port 22 +Protocol 2 +HostKey /var/ssh/ssh_host_rsa_key + +PasswordAuthentication yes +PermitEmptyPasswords yes +PubkeyAuthentication no +PermitRootLogin yes +PermitUserEnvironment yes +PidFile none +StrictModes no +UseDNS no +LogLevel DEBUG + +Subsystem sftp /usr/lib/ssh/sftp-server diff --git a/deployment/qemu/configs/startup.sh b/deployment/qemu/configs/startup.sh new file mode 100644 index 000000000..ba698a184 --- /dev/null +++ b/deployment/qemu/configs/startup.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + + +# ******************************************************************************* +# SOME/IP Gateway System Startup Script +# Executed during system initialization to start essential services +# ******************************************************************************* + +# Set PATH for system tools +export PATH=/proc/boot:/bin:/usr/bin:/sbin:/usr/sbin:$PATH + +echo "---> Starting slogger2" +slogger2 -s 4096 # Start system logger with 4KB buffer size for log messages +waitfor /dev/slog # Wait for system log device to become available + +echo "---> Starting PCI Services" +pci-server --config=/proc/boot/pci_server.cfg # Start PCI server with configuration file +waitfor /dev/pci # Wait for PCI device manager to initialize + +echo "---> Starting Pipe" +pipe # Start named pipe resource manager for IPC +waitfor /dev/pipe # Wait for pipe device to become available + +echo "---> Starting Random" +random # Start random number generator device +waitfor /dev/random # Wait for random device to become available + +echo "---> Starting fsevmgr" +fsevmgr # Start file system event manager for file notifications +waitfor /dev/fsnotify # Wait for filesystem notification device + +echo "---> Starting devb-ram" +devb-ram ram capacity=1 blk ramdisk=10m,cache=512k,vnode=256 2>/dev/null # Create 10MB RAM disk with 512KB cache +waitfor /dev/ram0 # Wait for RAM disk device to be ready + +echo "---> mounting ram disk" +mkqnx6fs -q /dev/ram0 # Create QNX6 filesystem on RAM disk (quiet mode) +waitfor /dev/ram0 # Wait for filesystem creation to complete +mount -w /dev/ram0 /tmp_discovery # Mount writable RAM disk at /tmp_discovery (required by LoLa) + +echo "---> Setting up writable /var for vsomeip" +# Save SSH configs before mounting over /var (IFS /var is read-only) +mkdir -p /tmp/var_backup +cp -r /var/ssh /tmp/var_backup/ 2>/dev/null || true +# Mount RAM disk for writable /var +devb-ram ram capacity=2 blk ramdisk=5m,cache=256k,vnode=64 2>/dev/null +waitfor /dev/ram1 2 +mkqnx6fs -q /dev/ram1 +mount -w /dev/ram1 /var +# Restore SSH configs +cp -r /tmp/var_backup/ssh /var/ 2>/dev/null || true +mkdir -p /var/run + +echo "---> Configuring network" +/etc/network_setup.sh # Execute network configuration script + +echo "---> Starting pseudo-terminal manager" +devc-pty # Start PTY manager for SSH terminal sessions +waitfor /dev/ptyp0 5 2>/dev/null || true # Wait up to 5 seconds for PTY device + +echo "---> Starting SSH daemon" +/proc/boot/sshd -f /var/ssh/sshd_config # Start SSH daemon for remote access + +echo "---> Starting qconn (QNX target agent)" +qconn & # Start QNX connection manager for IDE integration + +echo "---> SOME/IP Gateway system ready" +echo "---> You can now start:" +echo " /usr/bin/someipd --service_instance_manifest /etc/someipd/mw_com_config.json" +echo " /usr/bin/gatewayd -config_file /etc/gatewayd/gatewayd_config.bin --service_instance_manifest /etc/gatewayd/mw_com_config.json" +echo "" +echo "---> On QEMU instance 2 (192.168.87.3), run sample_client:" +echo " export VSOMEIP_CONFIGURATION=/etc/sample_client/vsomeip.json" +echo " /usr/bin/sample_client" diff --git a/deployment/qemu/init_x86_64.build b/deployment/qemu/init_x86_64.build new file mode 100644 index 000000000..bb996717a --- /dev/null +++ b/deployment/qemu/init_x86_64.build @@ -0,0 +1,243 @@ +############################################################################### +# +# QNX IFS image for SOME/IP Gateway — x86_64 (QEMU) with Networking +# +# This mkifs build file produces a bootable IFS containing: +# - QNX 8.0 kernel (procnto-smp-instr) +# - someipd daemon +# - gatewayd daemon +# - Minimal shell environment (ksh, pidin, toybox) +# - Network stack with VirtIO network driver for QEMU +# +# Build: +# bazel build //deployment/qemu:someip_gateway_ifs --config=x86_64-qnx +# +# Run in QEMU: +# bazel run //deployment/qemu:run_qemu --config=x86_64-qnx +# +############################################################################### + +[-optional] + +[image=0x3600000] +[virtual=x86_64,multiboot] boot = { + startup-x86 -v -D8250..115200 -zz + PATH=/proc/boot + LD_LIBRARY_PATH=/proc/boot:/usr/bin + [+keeplinked] procnto-smp-instr +} + +[+script] startup-script = { + procmgr_symlink /dev/shmem /tmp + + display_msg Welcome to QNX 8.0 — SOME/IP Gateway (x86_64) with Networking + + # Environment inherited by all subsequent processes + SYSNAME=nto + TERM=qansi + + # Start serial driver + devc-ser8250 & + waitfor /dev/ser1 + reopen /dev/ser1 + + display_msg Starting system services... + etc/startup.sh + + # Interactive shell + [+session] /bin/sh & +} + +# =========================================================================== +# QNX shared libraries (from SDP /proc/boot) +# =========================================================================== +/usr/lib/ldqnx-64.so.2=ldqnx-64.so.2 +libc.so.6 +[type=link] libc.so=libc.so.6 +libfsnotify.so.1 +libgcc_s.so.1 +libm.so +libsecpol.so.1 +libtracelog.so.1 +libz.so +libc++.so.2 +[type=link] libc++.so=libc++.so.2 +libsocket.so.4 +[type=link] libsocket.so=libsocket.so.4 +libxo.so.0 +[type=link] libxo.so=libxo.so.0 +libslog2.so.1 +[type=link] libslog2.so=libslog2.so.1 +libslog2parse.so.1 +[type=link] libslog2parse.so=libslog2parse.so.1 +libqh.so.1 +[type=link] libqh.so=libqh.so.1 +libcam.so.2 +cam-disk.so +io-blk.so +fs-qnx6.so + +# PCI and Network libraries +libpci.so.3.0 +libfdt.so.1 +libbz2.so.1 +liblzma.so.5 +libcatalog.so.1 +libiconv.so.1 +libregex.so.1 +libexpat.so.2 + +# Crypto libraries (required by random, networking, etc.) +libqcrypto.so.1.0 +libcrypto.so.3 +libssl.so.3 +qcrypto-openssl-3.so + +# System libraries +libjail.so.1 +libpam.so.2 + +# =========================================================================== +# SSH server components +# =========================================================================== +sshd # SSH daemon for remote access +# /usr/libexec/sshd-session=${QNX_TARGET}/${PROCESSOR}/usr/libexec/sshd-session # uncomment this for QNX > 8.0 +ssh # SSH client for remote connections +ssh-keygen # SSH key generation utility +/usr/lib/ssh/sftp-server=${QNX_TARGET}/${PROCESSOR}/usr/libexec/sftp-server # SFTP server for scp + +# Networking shared libraries and modules +mods-pci.so +mods-phy.so +devs-vtnet_pci.so + +# =========================================================================== +# Networking components +# =========================================================================== +io-sock # Network socket manager +if_up # Network interface configuration +ifconfig # Network interface configuration tool +route # Routing table management +dhcpcd # DHCP client daemon +sysctl # Configure kernel parameters at runtime +ping # Network connectivity test +netstat # Network statistics +tcpdump # Network packet capture +hostname # Set/display hostname +librpc.so.2 # RPC library (for tcpdump) + +# PCI server and components +pci-server # PCI bus server +pci/pci_hw-Intel_x86.so # Intel x86 PCI hardware support +pci/pci_slog2.so # PCI system logging support +pci/pci_cap-0x05.so # PCI capability handler for MSI +pci/pci_cap-0x10.so # PCI Express capability handler +pci/pci_cap-0x11.so # MSI-X capability handler +pci/pci_strings.so # PCI device string database +pci/pci_bkwd_compat.so # Backward compatibility support +pci/pci_debug2.so # Enhanced PCI debugging support + +# =========================================================================== +# Standard tools +# =========================================================================== +on +[type=link] waitfor=on +[type=link] ability=on +ksh +pidin +shutdown +qconn # QNX target agent for IDE debugging/deployment +toybox +awk +random +getconf +[type=link] setconf=getconf +slog2info + +# =========================================================================== +# Drivers +# =========================================================================== +devc-ser8250 +devc-pty # Pseudo-terminal driver (required for SSH sessions) +fsevmgr +slogger2 +pipe +devb-ram +mkqnx6fs +mount + +# =========================================================================== +# System directories +# =========================================================================== +[type=dir] /tmp_discovery +[type=dir] /tmp_ram +[type=dir] /var +[type=dir] /var/db +[type=dir] /var/run/dhcpcd +[gid=0 uid=0 dperms=755 type=dir] /var/chroot/sshd # SSH chroot directory (privilege separation) +[gid=0 uid=0 dperms=700 type=dir] /var/ssh # SSH configuration and key storage + +# =========================================================================== +# Symlinks +# =========================================================================== +[type=link] /bin/sh=/proc/boot/ksh +[type=link] /bin/ls=/proc/boot/ls +[type=link] cat=toybox +[type=link] chmod=toybox +[type=link] cp=toybox +[type=link] dd=toybox +[type=link] echo=toybox +[type=link] grep=toybox +[type=link] ln=toybox +[type=link] ls=toybox +[type=link] rm=toybox +[type=link] mkdir=toybox +[type=link] env=toybox +[type=link] mv=toybox +[type=link] pwd=toybox +[type=link] sleep=toybox +[type=link] tail=toybox +[type=link] head=toybox +[type=link] tee=toybox +[type=link] touch=toybox +[type=link] uname=toybox +[type=link] wc=toybox +[type=link] which=toybox +[type=link] whoami=toybox +[type=link] id=toybox +[type=link] sed=toybox +[type=link] sort=toybox +[type=link] find=toybox +[type=link] xargs=toybox +[type=link] date=toybox +[type=link] kill=toybox +[type=link] true=toybox +[type=link] false=toybox +[type=link] test=toybox + +# =========================================================================== +# Configuration files +# =========================================================================== +[perms=0444] pci_server.cfg = ${MAIN_BUILD_FILE_DIR}/configs/pci_server.cfg +[perms=0444] pci_hw.cfg = ${MAIN_BUILD_FILE_DIR}/configs/pci_hw.cfg +[perms=0444] qcrypto.conf = ${MAIN_BUILD_FILE_DIR}/configs/qcrypto.conf +[perms=0644] /etc/dhcpcd.conf = ${MAIN_BUILD_FILE_DIR}/configs/dhcpcd.conf +[perms=0700] /etc/startup.sh = ${MAIN_BUILD_FILE_DIR}/configs/startup.sh +[perms=0700] /etc/network_setup.sh = ${MAIN_BUILD_FILE_DIR}/configs/network_setup.sh + +# SSH configuration files +[perms=0444] /var/ssh/sshd_config = ${MAIN_BUILD_FILE_DIR}/configs/sshd_config +[uid=0 gid=0 perms=0400] /var/ssh/ssh_host_rsa_key = ${MAIN_BUILD_FILE_DIR}/configs/ssh_host_rsa_key +[uid=0 gid=0 perms=0444] /var/ssh/ssh_host_rsa_key.pub = ${MAIN_BUILD_FILE_DIR}/configs/ssh_host_rsa_key.pub +[perms=0644] /etc/passwd = ${MAIN_BUILD_FILE_DIR}/configs/passwd +[perms=0644] /etc/group = ${MAIN_BUILD_FILE_DIR}/configs/group +[perms=0644] /etc/profile = ${MAIN_BUILD_FILE_DIR}/configs/profile + +# =========================================================================== +# Application binaries (injected via pkg_tar → tars attribute) +# =========================================================================== +/usr/bin=${GATEWAY_BINS} +/etc/gatewayd=${GATEWAY_CONFIGS} +/etc/someipd=${SOMEIPD_CONFIGS} +/etc/sample_client=${SAMPLE_CLIENT_CONFIGS} +/usr/bin/tests=${TEST_BINS} diff --git a/deployment/qemu/run_qemu.sh b/deployment/qemu/run_qemu.sh new file mode 100755 index 000000000..dbd237da2 --- /dev/null +++ b/deployment/qemu/run_qemu.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +# +# Launch a QNX x86_64 IFS image under QEMU with bridge networking. +# +# Usage (via Bazel): +# bazel run //deployment/qemu:run_qemu --config=x86_64-qnx +# +# Environment variables: +# QEMU_INSTANCE_ID - Instance ID (1 or 2) for multi-instance setups (default: 1) +# +# Network layout (bridge mode): +# Bridge: virbr0 (192.168.87.1/24) +# Instance 1: 192.168.87.2 (MAC: 52:54:00:12:34:01) +# Instance 2: 192.168.87.3 (MAC: 52:54:00:12:34:02) +# Gateway: 192.168.87.1 (host) +# + +set -euo pipefail + +IFS_IMAGE=$1 +INSTANCE_ID=${2:-${QEMU_INSTANCE_ID:-1}} + +# Bridge configuration +BRIDGE_NAME="virbr0" + +# Calculate guest IP and MAC based on instance ID +GUEST_IP="192.168.87.$((1 + INSTANCE_ID))" # Instance 1: .2, Instance 2: .3 +GUEST_MAC="52:54:00:12:34:0${INSTANCE_ID}" + +echo "========================================" +echo " QNX SOME/IP Gateway — QEMU (x86_64)" +echo "========================================" +echo " IFS image: ${IFS_IMAGE}" +echo " Instance ID: ${INSTANCE_ID}" +echo " Network mode: bridge" +echo " Bridge: ${BRIDGE_NAME}" +echo " Guest IP: ${GUEST_IP}" +echo " Guest MAC: ${GUEST_MAC}" +echo " Gateway: 192.168.87.1" + +# Verify bridge exists +if ! ip link show "${BRIDGE_NAME}" &>/dev/null; then + echo "" + echo "ERROR: Bridge ${BRIDGE_NAME} does not exist!" + exit 1 +fi + +echo "========================================" +echo "" + +# Bridge networking - single NIC connected to virbr0 +# Both QEMU instances are on the same L2 network for direct communication +NET_ARGS="-netdev bridge,id=net0,br=${BRIDGE_NAME} -device virtio-net-pci,netdev=net0,mac=${GUEST_MAC}" + +# Pass instance ID to guest via kernel command line (QNX ignores unknown params) +exec qemu-system-x86_64 \ + -enable-kvm \ + -smp 2 \ + -cpu host \ + -m 1G \ + -pidfile "/tmp/qemu-someip-gateway-${INSTANCE_ID}.pid" \ + -nographic \ + -kernel "${IFS_IMAGE}" \ + -append "instance_id=${INSTANCE_ID}" \ + -chardev stdio,id=char0,signal=on,mux=on \ + -mon chardev=char0,mode=readline \ + -serial chardev:char0 \ + -object rng-random,filename=/dev/urandom,id=rng0 \ + -device virtio-rng-pci,rng=rng0 \ + ${NET_ARGS} diff --git a/deployment/qemu/setup_bridge.sh b/deployment/qemu/setup_bridge.sh new file mode 100755 index 000000000..60fef3812 --- /dev/null +++ b/deployment/qemu/setup_bridge.sh @@ -0,0 +1,255 @@ +#!/bin/bash +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Setup bridge networking for QEMU instances +# This script creates a bridge (virbr0) and configures it for QEMU networking. +# +# Usage: +# sudo ./setup_bridge.sh # Create bridge and configure it +# sudo ./setup_bridge.sh teardown # Remove bridge and clean up +# +# Prerequisites: +# - bridge-utils package (brctl command) +# - iproute2 package (ip command) +# - Linux kernel with bridging support +# - Root privileges +# +# Network layout: +# Bridge: virbr0 +# IP: 192.168.87.1/24 +# QEMU Instance 1: 192.168.87.2 +# QEMU Instance 2: 192.168.87.3 +# + + +#WORK IN PROGRESS - initial version with bridge setup and TAP device creation for ITF compatibility. +#USE IT at your own risk + +set -euo pipefail + +BRIDGE_NAME="virbr0" +BRIDGE_IP="192.168.87.1" +BRIDGE_NETMASK="255.255.255.0" + +# TAP devices for ITF compatibility (manual TAP mode) +# ITF expects pre-created tap devices passed via network_adapters +# Note: tap-qemu2 not used - dual instance tests use QEMU bridge helper (auto-creates tapN) +TAP_DEVICES=("tap-qemu1") +TAP_USER="${SUDO_USER:-$(whoami)}" + +# QEMU bridge helper path +QEMU_BRIDGE_HELPER="/usr/lib/qemu/qemu-bridge-helper" +QEMU_BRIDGE_ACL="/etc/qemu/bridge.conf" + +print_usage() { + echo "Usage: $0 [setup|teardown|status]" + echo "" + echo "Commands:" + echo " setup - Create and configure bridge (default)" + echo " teardown - Remove bridge and clean up" + echo " status - Show bridge status" + echo "" + echo "Network Configuration:" + echo " Bridge: ${BRIDGE_NAME}" + echo " Bridge IP: ${BRIDGE_IP}/${BRIDGE_NETMASK}" + echo " QEMU 1: 192.168.87.2" + echo " QEMU 2: 192.168.87.3" +} + +check_root() { + if [[ $EUID -ne 0 ]]; then + echo "ERROR: This script must be run as root (sudo)" + exit 1 + fi +} + +check_prerequisites() { + local missing=0 + + if ! command -v brctl &>/dev/null && ! command -v ip &>/dev/null; then + echo "ERROR: Neither brctl nor ip command found. Install bridge-utils or iproute2." + missing=1 + fi + + return $missing +} + +setup_bridge() { + echo "=== Setting up bridge network: ${BRIDGE_NAME} ===" + + setcap cap_net_raw,cap_net_admin=eip $(which tcpdump) + + # Check if bridge already exists + if ip link show "${BRIDGE_NAME}" &>/dev/null; then + echo "[INFO] Bridge ${BRIDGE_NAME} already exists, skipping creation" + else + echo "[INFO] Creating bridge ${BRIDGE_NAME}..." + ip link add name "${BRIDGE_NAME}" type bridge + ip link set "${BRIDGE_NAME}" up + fi + + # Configure bridge IP + if ! ip addr show "${BRIDGE_NAME}" | grep -q "${BRIDGE_IP}"; then + echo "[INFO] Configuring bridge IP: ${BRIDGE_IP}" + ip addr add "${BRIDGE_IP}/${BRIDGE_NETMASK}" dev "${BRIDGE_NAME}" + else + echo "[INFO] Bridge IP ${BRIDGE_IP} already configured" + fi + + # Configure QEMU bridge helper ACL + echo "[INFO] Configuring QEMU bridge helper..." + mkdir -p "$(dirname "${QEMU_BRIDGE_ACL}")" + if [[ ! -f "${QEMU_BRIDGE_ACL}" ]] || ! grep -q "allow ${BRIDGE_NAME}" "${QEMU_BRIDGE_ACL}" 2>/dev/null; then + echo "allow ${BRIDGE_NAME}" >> "${QEMU_BRIDGE_ACL}" + echo "[INFO] Added ${BRIDGE_NAME} to ${QEMU_BRIDGE_ACL}" + fi + + # Set bridge helper permissions (needs setuid for non-root QEMU) + if [[ -f "${QEMU_BRIDGE_HELPER}" ]]; then + chmod u+s "${QEMU_BRIDGE_HELPER}" + echo "[INFO] Set setuid on ${QEMU_BRIDGE_HELPER}" + else + echo "[WARNING] QEMU bridge helper not found at ${QEMU_BRIDGE_HELPER}" + echo " Install qemu-system-x86 package or run QEMU as root" + fi + + # Configure multicast support for SOME/IP Service Discovery + # Disable IGMP snooping to allow multicast flooding between guests + echo "[INFO] Configuring multicast support..." + echo 0 > /sys/devices/virtual/net/${BRIDGE_NAME}/bridge/multicast_snooping + echo "[INFO] Disabled IGMP snooping on ${BRIDGE_NAME}" + + # Add multicast route through the bridge + if ! ip route show | grep -q "224.0.0.0/4 dev ${BRIDGE_NAME}"; then + ip route add 224.0.0.0/4 dev "${BRIDGE_NAME}" 2>/dev/null || true + echo "[INFO] Added multicast route via ${BRIDGE_NAME}" + else + echo "[INFO] Multicast route already exists" + fi + + # Create persistent TAP devices for ITF compatibility + # ITF uses manual TAP mode: -netdev tap,ifname=,script=no,downscript=no + echo "[INFO] Creating persistent TAP devices for ITF..." + for tap in "${TAP_DEVICES[@]}"; do + if ip link show "${tap}" &>/dev/null; then + echo "[INFO] TAP ${tap} already exists" + else + ip tuntap add dev "${tap}" mode tap user "${TAP_USER}" + ip link set "${tap}" up + ip link set "${tap}" master "${BRIDGE_NAME}" + echo "[INFO] Created TAP ${tap} (owner: ${TAP_USER}) attached to ${BRIDGE_NAME}" + fi + done + + echo "" + echo "=== Bridge setup complete ===" + show_status +} + +teardown_bridge() { + echo "=== Tearing down bridge network: ${BRIDGE_NAME} ===" + + # Remove TAP devices + echo "[INFO] Removing TAP devices..." + for tap in "${TAP_DEVICES[@]}"; do + if ip link show "${tap}" &>/dev/null; then + ip link set "${tap}" down + ip tuntap del dev "${tap}" mode tap + echo "[INFO] Removed TAP ${tap}" + fi + done + + # Remove multicast route + ip route del 224.0.0.0/4 dev "${BRIDGE_NAME}" 2>/dev/null || true + + # Remove bridge + if ip link show "${BRIDGE_NAME}" &>/dev/null; then + echo "[INFO] Removing bridge ${BRIDGE_NAME}..." + ip link set "${BRIDGE_NAME}" down + ip link delete "${BRIDGE_NAME}" type bridge + else + echo "[INFO] Bridge ${BRIDGE_NAME} does not exist" + fi + + echo "=== Bridge teardown complete ===" +} + +show_status() { + echo "" + echo "=== Bridge Status ===" + + if ip link show "${BRIDGE_NAME}" &>/dev/null; then + echo "[OK] Bridge ${BRIDGE_NAME} exists" + ip addr show "${BRIDGE_NAME}" + echo "" + echo "Connected interfaces:" + brctl show "${BRIDGE_NAME}" 2>/dev/null || bridge link show master "${BRIDGE_NAME}" 2>/dev/null || echo " (none)" + else + echo "[NOT FOUND] Bridge ${BRIDGE_NAME} does not exist" + echo "Run: sudo $0 setup" + return 1 + fi + + echo "" + echo "QEMU bridge helper:" + if [[ -f "${QEMU_BRIDGE_ACL}" ]]; then + echo " ACL file: ${QEMU_BRIDGE_ACL}" + cat "${QEMU_BRIDGE_ACL}" + else + echo " [WARNING] ${QEMU_BRIDGE_ACL} not found" + fi + + echo "" + echo "TAP devices (for ITF):" + for tap in "${TAP_DEVICES[@]}"; do + if ip link show "${tap}" &>/dev/null; then + state=$(ip -br link show "${tap}" | awk '{print $2}') + echo " [OK] ${tap}: ${state}" + else + echo " [NOT FOUND] ${tap}" + fi + done + + echo "" + echo "To run QEMU with bridge networking:" + echo " ./run_qemu.sh 1" + echo " ./run_qemu.sh 2" + echo "" + echo "To use with ITF, configure network_adapters in your JSON:" + echo ' "networks": [{"name": "tap-qemu1", ...}]' +} + +# Main +case "${1:-setup}" in + setup) + check_root + check_prerequisites + setup_bridge + ;; + teardown) + check_root + teardown_bridge + ;; + status) + show_status + ;; + -h|--help|help) + print_usage + ;; + *) + echo "Unknown command: $1" + print_usage + exit 1 + ;; +esac diff --git a/deployment/qemu/setup_qemu_1.sh b/deployment/qemu/setup_qemu_1.sh new file mode 100755 index 000000000..e6d724c51 --- /dev/null +++ b/deployment/qemu/setup_qemu_1.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# Setup QEMU Instance 1 with gatewayd and someipd +# This script starts QEMU 1 and launches the gateway services +# +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR" +# Bridge mode: SSH directly to guest IP on bridge network +GUEST_IP="192.168.87.2" +SSH_HOST="${GUEST_IP}" +SSH_USER="root" + +echo "=== QEMU Instance 1 Setup (${GUEST_IP}) ===" +echo "" + +# Check if bridge exists +if ! ip link show virbr0 &>/dev/null; then + echo "[ERROR] Bridge virbr0 does not exist!" + exit 1 +fi + +# Check if QEMU is already running +if [[ -f "/tmp/qemu-someip-gateway-1.pid" ]] && kill -0 "$(cat /tmp/qemu-someip-gateway-1.pid)" 2>/dev/null; then + echo "[INFO] QEMU instance 1 already running" +else + echo "[INFO] Starting QEMU instance 1..." + cd "${WORKSPACE_ROOT}" + bazel run //deployment/qemu:run_qemu_1 --config=x86_64-qnx & + QEMU_PID=$! + echo "[INFO] QEMU started with PID ${QEMU_PID}" +fi + +# Wait for SSH to become available +echo "[INFO] Waiting for SSH on ${GUEST_IP}..." +RETRY=0 +MAX_RETRIES=60 +while [ $RETRY -lt $MAX_RETRIES ]; do + if ssh ${SSH_OPTS} ${SSH_USER}@${SSH_HOST} "echo ready" 2>/dev/null; then + echo "[INFO] SSH is ready" + break + fi + RETRY=$((RETRY + 1)) + sleep 2 +done + +if [ $RETRY -eq $MAX_RETRIES ]; then + echo "[ERROR] Timeout waiting for SSH" + exit 1 +fi + +echo "" +echo "[INFO] Starting gatewayd..." +ssh ${SSH_OPTS} ${SSH_USER}@${SSH_HOST} \ + '/usr/bin/gatewayd -config_file /etc/gatewayd/gatewayd_config.bin --service_instance_manifest /etc/gatewayd/mw_com_config.json &' & + +sleep 3 + +echo "[INFO] Starting someipd..." +ssh ${SSH_OPTS} ${SSH_USER}@${SSH_HOST} \ + 'export VSOMEIP_CONFIGURATION=/etc/someipd/vsomeip.json && /usr/bin/someipd --service_instance_manifest /etc/someipd/mw_com_config.json &' & + +sleep 2 diff --git a/deployment/qemu/setup_qemu_2.sh b/deployment/qemu/setup_qemu_2.sh new file mode 100755 index 000000000..e9a759d99 --- /dev/null +++ b/deployment/qemu/setup_qemu_2.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# This script starts QEMU 2 and runs the SOME/IP sample client + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR" +# Bridge mode: SSH directly to guest IP on bridge network +GUEST_IP="192.168.87.3" +SSH_HOST="${GUEST_IP}" +SSH_USER="root" + +echo "=== QEMU Instance 2 Setup (${GUEST_IP}) ===" +echo "" + +# Check if bridge exists +if ! ip link show virbr0 &>/dev/null; then + echo "[ERROR] Bridge virbr0 does not exist!" + exit 1 +fi + +# Check if QEMU is already running +if [[ -f "/tmp/qemu-someip-gateway-2.pid" ]] && kill -0 "$(cat /tmp/qemu-someip-gateway-2.pid)" 2>/dev/null; then + echo "[INFO] QEMU instance 2 already running" +else + echo "[INFO] Starting QEMU instance 2..." + cd "${WORKSPACE_ROOT}" + bazel run //deployment/qemu:run_qemu_2 --config=x86_64-qnx & + QEMU_PID=$! + echo "[INFO] QEMU started with PID ${QEMU_PID}" +fi + +# Wait for SSH to become available +echo "[INFO] Waiting for SSH on ${GUEST_IP}..." +RETRY=0 +MAX_RETRIES=10 +while [ $RETRY -lt $MAX_RETRIES ]; do + if ssh ${SSH_OPTS} ${SSH_USER}@${SSH_HOST} "echo ready" 2>/dev/null; then + echo "[INFO] SSH is ready" + break + fi + RETRY=$((RETRY + 1)) + sleep 2 +done + +if [ $RETRY -eq $MAX_RETRIES ]; then + echo "[ERROR] Timeout waiting for SSH" + exit 1 +fi + +# Run sample_client interactively (foreground) +ssh ${SSH_OPTS} ${SSH_USER}@${SSH_HOST} \ + 'export VSOMEIP_CONFIGURATION=/etc/sample_client/vsomeip.json && /usr/bin/sample_client' diff --git a/pyproject.toml b/pyproject.toml index 7a23e7a0a..b2789d237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ requires-python = ">= 3.10" classifiers = ["Private :: Do Not Upload"] # Run `bazel run //:python_requirements.update` to update python_requirements_lock.txt -dependencies = ["pytest~=8.0", "someip~=0.3.0"] +dependencies = ["pytest~=8.0", "someip~=0.3.0", "scapy~=2.7.0"] [tool.basedpyright] extends = "bazel-bin/ide_support.runfiles/score_tooling+/python_basics/pyproject.toml" diff --git a/src/gatewayd/BUILD.bazel b/src/gatewayd/BUILD.bazel index ce3bf9354..67e92b2d9 100644 --- a/src/gatewayd/BUILD.bazel +++ b/src/gatewayd/BUILD.bazel @@ -34,6 +34,16 @@ exports_files( cc_binary( name = "gatewayd", srcs = ["main.cpp"], + args = [ + "-config_file", + "$(rootpath :config_file)", + "-service_instance_manifest", + "$(rootpath etc/mw_com_config.json)", + ], + data = [ + "etc/mw_com_config.json", + ":config_file", + ], visibility = ["//visibility:public"], deps = [ ":config_flatbuffers", @@ -72,10 +82,17 @@ flatbuffer_cc_library( srcs = ["etc/gatewayd_config.fbs"], ) +exports_files( + ["etc/mw_com_config.json"], + visibility = ["//deployment:__subpackages__"], +) + + generate_someip_config_bin( name = "config_file", json = "etc/gatewayd_config.json", output = "etc/gatewayd_config.bin", + visibility = ["//deployment:__subpackages__"], ) validate_json_schema_test( diff --git a/src/gatewayd/main.cpp b/src/gatewayd/main.cpp index dd3a7ec04..bc57eac7e 100644 --- a/src/gatewayd/main.cpp +++ b/src/gatewayd/main.cpp @@ -45,14 +45,20 @@ int main(int argc, const char* argv[]) { std::signal(SIGINT, termination_handler); // Read config data - // TODO: Be more flexible with the path // TODO: Use memory mapped file instead of copying into buffer + std::string config_path = "src/gatewayd/etc/gatewayd_config.bin"; + for (int i = 1; i < argc; ++i) { + if (std::string(argv[i]) == "-config_file" && i + 1 < argc) { + config_path = argv[i + 1]; + break; + } + } + std::ifstream config_file; - config_file.open("src/gatewayd/etc/gatewayd_config.bin", std::ios::binary | std::ios::in); + config_file.open(config_path, std::ios::binary | std::ios::in); if (!config_file.is_open()) { - std::cerr << "Error: Could not open config file 'src/gatewayd/etc/gatewayd_config.bin'" - << std::endl; + std::cerr << "Error: Could not open config file '" << config_path << "'" << std::endl; return 1; } diff --git a/src/someipd/BUILD.bazel b/src/someipd/BUILD.bazel index f6828b9f0..907788f2c 100644 --- a/src/someipd/BUILD.bazel +++ b/src/someipd/BUILD.bazel @@ -18,6 +18,12 @@ This daemon contains the SOME/IP communication stack and is QM load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("@score_communication//bazel/tools:json_schema_validator.bzl", "validate_json_schema_test") +# Export config files for deployment +exports_files( + ["etc/mw_com_config.json"], + visibility = ["//visibility:public"], +) + # ============================================================================ # Main Binary # ============================================================================ diff --git a/tests/integration/test_dummy.py b/tests/BUILD.bazel similarity index 78% rename from tests/integration/test_dummy.py rename to tests/BUILD.bazel index cefb2471a..3d052b3d6 100644 --- a/tests/integration/test_dummy.py +++ b/tests/BUILD.bazel @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation +# Copyright (c) 2026 Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -11,8 +11,4 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -from someip.header import SOMEIPHeader - - -def test_dummy(someipd): - assert True +"""Tests package.""" diff --git a/tests/UT/BUILD.bazel b/tests/UT/BUILD.bazel new file mode 100644 index 000000000..0e5e8f57a --- /dev/null +++ b/tests/UT/BUILD.bazel @@ -0,0 +1,44 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_itf//:defs.bzl", "py_itf_test") +load("@score_itf//score/itf/plugins:plugins.bzl", "qemu") + +# --------------------------------------------------------------------------- +# C++ unit test execution on QNX QEMU image +# Uses ITF SSH to connect to a running QEMU instance +# +# Prerequisites: +# Start QEMU first: +# bazel run //deployment/qemu:run_qemu_1 --config=x86_64-qnx +# +# Then run: +# bazel test //tests/UT:test_ut --test_output=all --cache_test_results=no +# --------------------------------------------------------------------------- +py_itf_test( + name = "test_ut", + srcs = ["test_ut.py"], + args = [ + "--qemu-config=$(location qemu_config.json)", + "--qemu-image=$(location //deployment/qemu:someip_gateway_ifs)", + ], + data = [ + "qemu_config.json", + "//deployment/qemu:someip_gateway_ifs", + ], + plugins = [qemu], + tags = [ + "manual", # Requires pre-started QEMU + "qemu", + ], +) diff --git a/tests/cpp/BUILD.bazel b/tests/UT/UnitTest_UT1/BUILD.bazel similarity index 90% rename from tests/cpp/BUILD.bazel rename to tests/UT/UnitTest_UT1/BUILD.bazel index 4d8e08429..0ed402c71 100644 --- a/tests/cpp/BUILD.bazel +++ b/tests/UT/UnitTest_UT1/BUILD.bazel @@ -14,8 +14,9 @@ load("@rules_cc//cc:cc_test.bzl", "cc_test") cc_test( - name = "cpp_test_main", + name = "cpp_test_main1", srcs = ["test_main.cpp"], + visibility = ["//deployment/qemu:__pkg__"], deps = [ "@googletest//:gtest_main", # GoogleTest dependency via Bazel Modules ], diff --git a/tests/cpp/test_main.cpp b/tests/UT/UnitTest_UT1/test_main.cpp similarity index 79% rename from tests/cpp/test_main.cpp rename to tests/UT/UnitTest_UT1/test_main.cpp index d187a28c4..2298500bc 100644 --- a/tests/cpp/test_main.cpp +++ b/tests/UT/UnitTest_UT1/test_main.cpp @@ -21,17 +21,6 @@ TEST(AdditionTest, HandlesPositiveNumbers) { EXPECT_EQ(add(10, 20), 30); } -TEST(AdditionTest, HandlesNegativeNumbers) { - EXPECT_EQ(add(-2, -3), -5); - EXPECT_EQ(add(-10, 5), -5); -} - -TEST(AdditionTest, HandlesZero) { - EXPECT_EQ(add(0, 0), 0); - EXPECT_EQ(add(0, 5), 5); - EXPECT_EQ(add(5, 0), 5); -} - // Main function for running tests int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/tests/integration/docker_setup/entrypoint-slave b/tests/UT/UnitTest_UT2/BUILD.bazel old mode 100755 new mode 100644 similarity index 58% rename from tests/integration/docker_setup/entrypoint-slave rename to tests/UT/UnitTest_UT2/BUILD.bazel index de9e113e4..b8c3589d4 --- a/tests/integration/docker_setup/entrypoint-slave +++ b/tests/UT/UnitTest_UT2/BUILD.bazel @@ -1,5 +1,3 @@ -#!/bin/bash - # ******************************************************************************* # Copyright (c) 2025 Contributors to the Eclipse Foundation # @@ -13,15 +11,13 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -set -e - -echo "Container for sample_client started" - -# Add multicast route -route add -net 224.0.0.0/4 dev eth0 - -# Manually start up the sample_client application when the car_window_controller and gatewayd are already running -# export VSOMEIP_CONFIGURATION=$(pwd)/tests/integration/sample_client/vsomeip.json -# /home/source/bazel-bin/tests/integration/sample_client/sample_client +load("@rules_cc//cc:cc_test.bzl", "cc_test") -sleep infinity +cc_test( + name = "cpp_test_main2", + srcs = ["test_main.cpp"], + visibility = ["//deployment/qemu:__pkg__"], + deps = [ + "@googletest//:gtest_main", # GoogleTest dependency via Bazel Modules + ], +) diff --git a/tests/UT/UnitTest_UT2/test_main.cpp b/tests/UT/UnitTest_UT2/test_main.cpp new file mode 100644 index 000000000..897c796d8 --- /dev/null +++ b/tests/UT/UnitTest_UT2/test_main.cpp @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include + +// Function to be tested +int diff(int a, int b) { return a - b; } + + +TEST(DiffTest, HandlesNegativeNumbers) { + EXPECT_EQ(diff(-1, -2), 1); + EXPECT_EQ(diff(-10, -20), 10); + //test1 +} +// Main function for running tests +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} +//test123 +//sddd diff --git a/tests/UT/qemu_config.json b/tests/UT/qemu_config.json new file mode 100644 index 000000000..b9554d8ca --- /dev/null +++ b/tests/UT/qemu_config.json @@ -0,0 +1,12 @@ +{ + "networks": [ + { + "name": "tap-qemu1", + "ip_address": "192.168.87.2", + "gateway": "192.168.87.1" + } + ], + "ssh_port": 22, + "qemu_num_cores": 2, + "qemu_ram_size": "1G" +} diff --git a/tests/UT/test_ut.py b/tests/UT/test_ut.py new file mode 100644 index 000000000..9c7882656 --- /dev/null +++ b/tests/UT/test_ut.py @@ -0,0 +1,71 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Unit test execution on QNX QEMU image using ITF. + +Runs C++ GoogleTest binaries on the QEMU guest and validates results. + +""" + +import pytest +# ITF imports (score_itf main branch e994cb6 + OCI conflict patch) +from score.itf.core.com.ssh import execute_command_output + + + +@pytest.fixture +def ssh_client(target): + """Create ITF SSH connection to QEMU guest. + + Uses ITF's target fixture (from qemu plugin) which automatically + starts QEMU with the configured TAP devices. + """ + with target.ssh() as connection: + yield connection + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- +commands = [ + "/usr/bin/tests/cpp_test_main1", + "/usr/bin/tests/cpp_test_main2" + ] + + +def test_cpp_unit_tests_on_qemu(ssh_client): + """Run C++ GoogleTest binary on QEMU and verify all tests pass.""" + failed_tests = [] + run_logs = [] + + for cmd in commands: + exit_code, stdout_lines, stderr_lines = execute_command_output( + ssh_client, + cmd, + timeout=10, + max_exec_time=120, + + verbose=True, + ) + + output = "".join(stdout_lines) + error_output = "".join(stderr_lines) + + # Save the logs so you can see what happened later + run_logs.append(f"=== Logs for {cmd} ===\nSTDOUT:\n{output}\nSTDERR:\n{error_output}\n") + + # Record failures but keep the loop going + if exit_code != 0: + failed_tests.append(f"{cmd} (Exit Code: {exit_code})") + + # Now that all tests have run, assert if any of them failed + full_log_output = "\n".join(run_logs) + assert len(failed_tests) == 0, f"C++ tests failed!\nFailed binaries: {failed_tests}\n\n{full_log_output}" diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index ef68f4888..2ce1517e0 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -11,22 +11,73 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -""" -Integration Tests -""" load("@bazel_tools_python//quality:defs.bzl", "py_pytest") +load("@score_itf//:defs.bzl", "py_itf_test") +load("@score_itf//score/itf/plugins:plugins.bzl", "qemu") -py_pytest( - name = "integration", - size = "medium", - srcs = glob(["test_*.py"]) + ["conftest.py"], - data = ["vsomeip-local.json"], - # Restrict the test execution to Linux for the moment due to dependency on Python - target_compatible_with = ["@platforms//os:linux"], - deps = [ - "//src/someipd", - "@someip_pip//pytest", - "@someip_pip//someip", +# Export vsomeip config for QEMU deployment +exports_files( + ["vsomeip.json"], + visibility = ["//visibility:public"], +) + +py_itf_test( + name = "test_qemu_network_single", + srcs = [ + "conftest.py", + "test_qemu_network.py", + "//tests/itf_updates:qemu_utils.py", + ], + args = [ + "-k", "TestSingleInstanceNetwork", + "--qemu-config=$(location qemu_config.json)", + "--qemu-image=$(location //deployment/qemu:someip_gateway_ifs)", + ], + data = [ + "qemu_config.json", + "//deployment/qemu:someip_gateway_ifs", + ], + plugins = [qemu], + tags = ["qemu"], +) + +# Dual instance test - automatically starts two QEMU instances via qemu_utils.py +py_itf_test( + name = "test_qemu_network_dual", + srcs = [ + "conftest.py", + "test_qemu_network.py", + "//tests/itf_updates:qemu_utils.py", + ], + args = [ + "-k", "TestDualInstanceNetwork", + ], + data = [ + "//deployment/qemu:someip_gateway_ifs", + "//deployment/qemu:run_qemu.sh", + ], + plugins = [], # No qemu plugin - uses qemu_utils.py for dual instance management + tags = ["qemu"], +) + +# SOME/IP Service Discovery test - captures traffic and verifies SD offers/finds/subscriptions +py_itf_test( + name = "test_someip_sd", + srcs = [ + "conftest.py", + "test_qemu_network.py", + "//tests/itf_updates:qemu_utils.py", + ], + args = [ + "-k", "TestSomeIPSD", + "-s", + ], + data = [ + "analyze_pcap_someip.py", + "//deployment/qemu:someip_gateway_ifs", + "//deployment/qemu:run_qemu.sh", ], + plugins = [], # No qemu plugin - uses qemu_utils.py for dual instance management + tags = ["qemu"], ) diff --git a/tests/integration/analyze_pcap_someip.py b/tests/integration/analyze_pcap_someip.py new file mode 100755 index 000000000..45aa8af00 --- /dev/null +++ b/tests/integration/analyze_pcap_someip.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +""" +AI generated proof of concept, has been tested and verified to work correctly for the limited configuration in this project. +Scapy can simplify the parsing but requires additional dependencies and setup. +Targets: someipd (192.168.87.2) and sample_client (192.168.87.3) + +This script parses a pcap file for SOME/IP-SD messages and generates +a summary of Offers, Finds, and Subscriptions + +Usage: + python3 analyze_someip_sd.py [pcap_file] +""" + +import struct +import subprocess +import re +import sys +import os +from collections import defaultdict + +# --- Configuration --- +TARGET_HOSTS = { + "192.168.87.2": "someipd", + "192.168.87.3": "sample_client" +} + +# Constants for SOME/IP-SD parsing +SOMEIP_SD_SERVICE_ID = 0xFFFF +SOMEIP_SD_METHOD_ID = 0x8100 + +# SOME/IP-SD Entry Types +ENTRY_TYPE_FIND = 0x00 +ENTRY_TYPE_OFFER = 0x01 +ENTRY_TYPE_SUBSCRIBE = 0x06 +ENTRY_TYPE_STOP_SUBSCRIBE_ACK = 0x07 + +TYPE_NAMES = { + ENTRY_TYPE_FIND: "FindService", + ENTRY_TYPE_OFFER: "OfferService", + ENTRY_TYPE_SUBSCRIBE: "Subscribe", + ENTRY_TYPE_STOP_SUBSCRIBE_ACK: "StopSubscribe/Ack" +} + +# --- Parsing Functions --- + +def decode_someip_sd_entry(data, offset=0): + """Decode SOME/IP-SD entry at given offset.""" + if len(data) < offset + 16: + return None + + entry_type = data[offset] + service_id = struct.unpack('!H', data[offset+4:offset+6])[0] + instance_id = struct.unpack('!H', data[offset+6:offset+8])[0] + + entry_name = TYPE_NAMES.get(entry_type, f"Type-{entry_type:02x}") + + # For eventgroup entries (Subscribe), eventgroup is at bytes 14-15 + eventgroup = None + if entry_type in (ENTRY_TYPE_SUBSCRIBE, ENTRY_TYPE_STOP_SUBSCRIBE_ACK): + eventgroup = struct.unpack('!H', data[offset+14:offset+16])[0] + if eventgroup == 0xFFFF or eventgroup == 0: + eventgroup = None + + return { + 'type': entry_name, + 'type_id': entry_type, + 'service': service_id, + 'instance': instance_id, + 'eventgroup': eventgroup + } + +def parse_tcpdump_hex_output(output): + """Parse tcpdump -XX output and extract packet information.""" + packets = [] + current_packet = None + hex_lines = [] + + for line in output.split('\n'): + if re.match(r'^\d{2}:\d{2}:\d{2}', line): + if current_packet and hex_lines: + current_packet['raw_bytes'] = parse_hex_lines(hex_lines) + packets.append(current_packet) + hex_lines = [] + current_packet = parse_packet_header(line) + elif line.strip().startswith('0x'): + hex_lines.append(line) + + if current_packet and hex_lines: + current_packet['raw_bytes'] = parse_hex_lines(hex_lines) + packets.append(current_packet) + + return packets + +def parse_packet_header(line): + """Parse tcpdump packet header line.""" + packet = {'src_ip': None, 'dst_ip': None, 'timestamp': None} + ts_match = re.match(r'^(\d{2}:\d{2}:\d{2}\.\d+)', line) + if ts_match: + packet['timestamp'] = ts_match.group(1) + + ip_match = re.search(r'IP\s+(\d+\.\d+\.\d+\.\d+)\.(\d+)\s+>\s+(\d+\.\d+\.\d+\.\d+)\.(\d+)', line) + if ip_match: + packet['src_ip'] = ip_match.group(1) + packet['src_port'] = int(ip_match.group(2)) + packet['dst_ip'] = ip_match.group(3) + packet['dst_port'] = int(ip_match.group(4)) + return packet + +def parse_hex_lines(hex_lines): + """Parses tcpdump hex dump lines into raw bytes.""" + hex_data = [] + + for line in hex_lines: + # Ensure line has an offset (e.g., "0x0000:") + if ':' not in line: continue + + # Get content after the offset colon + content = line.split(':', 1)[1] + + # Collect hex chunks (2 or 4 chars), stop when we hit the ASCII visualization + for part in content.split(): + if len(part) in (2, 4) and all(c in "0123456789abcdefABCDEF" for c in part): + hex_data.append(part) + else: + break # Stop reading this line as we reached the ASCII column + + return bytes.fromhex("".join(hex_data)) + +def extract_someip_sd_entries(raw_bytes): + """Extract SOME/IP-SD entries from raw packet bytes.""" + if len(raw_bytes) < 58: return [] + + ip_header_start = 14 + ip_version_ihl = raw_bytes[ip_header_start] + ip_header_len = (ip_version_ihl & 0x0F) * 4 + + someip_start = ip_header_start + ip_header_len + 8 # +8 for UDP + if len(raw_bytes) < someip_start + 16: return [] + + service_id = struct.unpack('!H', raw_bytes[someip_start:someip_start+2])[0] + method_id = struct.unpack('!H', raw_bytes[someip_start+2:someip_start+4])[0] + + if service_id != SOMEIP_SD_SERVICE_ID or method_id != SOMEIP_SD_METHOD_ID: + return [] + + sd_start = someip_start + 16 + if len(raw_bytes) < sd_start + 8: return [] + + entries_length = struct.unpack('!I', raw_bytes[sd_start+4:sd_start+8])[0] + entries_start = sd_start + 8 + entries_end = min(entries_start + entries_length, len(raw_bytes)) + + entries = [] + offset = entries_start + while offset + 16 <= entries_end: + entry_data = raw_bytes[offset:offset+16] + entry = decode_someip_sd_entry(entry_data, 0) + if entry: + entries.append(entry) + offset += 16 + + return entries + +def run_tcpdump(pcap_file): + """Run tcpdump to capture all SOME/IP SD traffic.""" + # We capture all UDP 30490 traffic, sorting out IPs in Python + cmd = [ + 'tcpdump', '-r', pcap_file, '-n', + 'udp port 30490', + '-XX' + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + return result.stdout + result.stderr + except subprocess.TimeoutExpired: + print(f"Error: tcpdump timed out reading {pcap_file}") + return "" + except FileNotFoundError: + print("Error: tcpdump not found. Please install tcpdump.") + sys.exit(1) + +# --- Summary Logic --- + +def print_summary(ip, name, data): + """Prints the formatted summary for a specific IP.""" + print("=" * 80) + print(f"SUMMARY: {name} ({ip})") + print("=" * 80) + + # OFFERS + if data['offers']: + print("OFFERS:") + sorted_offers = sorted(list(data['offers'])) + for svc, inst, eg in sorted_offers: + eg_str = f" with eventgroup 0x{eg:04x}" if eg else "" + print(f" Service 0x{svc:04x}.0x{inst:04x}{eg_str}") + else: + print("OFFERS:") + print(" None detected") + print() + + # FINDS + if data['finds']: + print("FINDS:") + for svc, inst in sorted(list(data['finds'])): + print(f" Service 0x{svc:04x}.0x{inst:04x}") + else: + print("FINDS:") + print(" None detected") + print() + + # SUBSCRIBES + if data['subscribes']: + print("SUBSCRIBED SUCCESSFULLY TO:") + for svc, inst, eg in sorted(list(data['subscribes'])): + eg_str = f", eventgroup 0x{eg:04x}" if eg else "" + print(f" Service 0x{svc:04x}.0x{inst:04x}{eg_str}") + else: + print("SUBSCRIBED SUCCESSFULLY TO:") + print(" None detected") + print("\n") + +def main(): + if len(sys.argv) > 1: + pcap_file = sys.argv[1] + else: + print("Usage: python3 analyze_qemu1_qemu2_someip.py [pcap_file]") + sys.exit(1) + + if not os.path.exists(pcap_file): + print(f"Error: pcap file not found: {pcap_file}") + sys.exit(1) + + print(f"Analyzing {pcap_file} for SOME/IP-SD traffic...") + + # Data structure to hold unique entries per host + # Structure: host_data[ip] = { 'offers': set(), 'finds': set(), 'subscribes': set() } + host_data = defaultdict(lambda: {'offers': set(), 'finds': set(), 'subscribes': set()}) + + # Run processing + output = run_tcpdump(pcap_file) + packets = parse_tcpdump_hex_output(output) + + if not packets: + print("No SOME/IP-SD packets found.") + return + + # Process packets + for packet in packets: + src_ip = packet['src_ip'] + + # Only process if src_ip is one of our targets + if src_ip not in TARGET_HOSTS: + continue + + entries = extract_someip_sd_entries(packet['raw_bytes']) + + for entry in entries: + t_id = entry['type_id'] + svc = entry['service'] + inst = entry['instance'] + eg = entry['eventgroup'] + + if t_id == ENTRY_TYPE_OFFER: + host_data[src_ip]['offers'].add((svc, inst, eg)) + elif t_id == ENTRY_TYPE_FIND: + host_data[src_ip]['finds'].add((svc, inst)) + elif t_id == ENTRY_TYPE_SUBSCRIBE: + host_data[src_ip]['subscribes'].add((svc, inst, eg)) + + # Output Results + print("\n") + + # 1. Print someipd Summary + ip_someipd = "192.168.87.2" + if ip_someipd in host_data: + print_summary(ip_someipd, TARGET_HOSTS[ip_someipd], host_data[ip_someipd]) + else: + print(f"No data found for {TARGET_HOSTS[ip_someipd]} ({ip_someipd})") + + # 2. Print sample_client Summary + ip_client = "192.168.87.3" + if ip_client in host_data: + print_summary(ip_client, TARGET_HOSTS[ip_client], host_data[ip_client]) + else: + print(f"No data found for {TARGET_HOSTS[ip_client]} ({ip_client})") + +if __name__ == "__main__": + main() diff --git a/tests/integration/client.py b/tests/integration/client.py deleted file mode 100755 index 8de26939c..000000000 --- a/tests/integration/client.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 - -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -import asyncio -import ipaddress -import logging -import socket - -import someip.header -from someip.config import Eventgroup, _T_SOCKNAME -from someip.sd import SOMEIPDatagramProtocol, ServiceDiscoveryProtocol - -logging.getLogger("someip.sd").setLevel(logging.WARNING) -logging.getLogger("someip.sd.announce").setLevel(logging.WARNING) - - -def enhex(buf, sep=" "): - return sep.join("%02x" % b for b in buf) - - -class EventGroupReceiver(SOMEIPDatagramProtocol): - def __init__(self): - super().__init__(logger="notification") - - def message_received( - self, - someip_message: someip.header.SOMEIPHeader, - addr: _T_SOCKNAME, - multicast: bool, - ) -> None: - """ - called when a well-formed SOME/IP datagram was received - """ - if someip_message.message_type != someip.header.SOMEIPMessageType.NOTIFICATION: - self.log.warning("unexpected message type: %s", someip_message) - return - self.log.info( - "service=0x%04x method=0x%04x interface_version=0x%02x" - " returncode=%s payload=%s", - someip_message.service_id, - someip_message.method_id, - someip_message.interface_version, - someip_message.return_code.name, - enhex(someip_message.payload), - ) - - -async def run( - local_addr, - multicast_addr, - local_port, - service_id, - instance_id, - major_version, - evgid, -): - trsp_u, trsp_m, protocol = await ServiceDiscoveryProtocol.create_endpoints( - family=socket.AF_INET, - local_addr=str(local_addr), - multicast_addr=str(multicast_addr), - port=30490, - ) - - evgrp_receiver, _ = await EventGroupReceiver.create_unicast_endpoint( - local_addr=(str(local_addr), local_port) - ) - sockname = evgrp_receiver.get_extra_info("sockname") - - try: - protocol.start() - protocol.discovery.find_subscribe_eventgroup( - Eventgroup( - service_id=service_id, - instance_id=instance_id, - major_version=major_version, - eventgroup_id=evgid, - sockname=sockname, - protocol=someip.header.L4Protocols.UDP, - ) - ) - while True: - await asyncio.sleep(10) - finally: - protocol.stop() - evgrp_receiver.close() - trsp_u.close() - trsp_m.close() - - -def auto_int(s): - return int(s, 0) - - -def setup_log(fmt="", **kwargs): - try: - import coloredlogs # type: ignore[import] - - coloredlogs.install(fmt="%(asctime)s,%(msecs)03d " + fmt, **kwargs) - except ModuleNotFoundError: - logging.basicConfig(format="%(asctime)s " + fmt, **kwargs) - logging.info("install coloredlogs for colored logs :-)") - - -def main(): - setup_log(level=logging.DEBUG, fmt="%(levelname)-8s %(name)s: %(message)s") - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument("local_addr", type=ipaddress.ip_address) - parser.add_argument("multicast_addr", type=ipaddress.ip_address) - parser.add_argument("local_port", type=int) - parser.add_argument("service", type=auto_int) - parser.add_argument("eventgroup_id", type=auto_int) - parser.add_argument("--instance", type=auto_int, default=0xFFFF) - parser.add_argument("--major_version", type=auto_int, default=0xFF) - - args = parser.parse_args() - - try: - asyncio.get_event_loop().run_until_complete( - run( - args.local_addr, - args.multicast_addr, - args.local_port, - args.service, - args.instance, - args.major_version, - args.eventgroup_id, - ) - ) - except KeyboardInterrupt: - pass - - -if __name__ == "__main__": - main() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index c24eedc1a..70de068dc 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,5 +1,5 @@ # ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation +# Copyright (c) 2026 Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -10,48 +10,77 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +"""Pytest fixtures for integration tests using ITF. -""" -Pytest configuration and fixtures for integration tests. +Single instance tests: + Uses ITF's native QemuTarget with QEMU plugin for full QEMU lifecycle management. + ITF starts QEMU using pre-created TAP devices (tap-qemu1) attached to virbr0. + +Dual instance tests: + Uses qemu_utils.py to automatically start two QEMU instances via run_qemu.sh. + Both instances are started before tests and stopped after. + +Setup: + sudo ./deployment/qemu/setup_bridge.sh setup # Creates tap-qemu1 on virbr0 """ import pytest -from typing import Generator -import subprocess -from pathlib import Path +# ITF imports for SSH +from score.itf.core.com.ssh import Ssh + +# Import qemu_utils fixtures for dual instance tests +# These are re-exported so pytest can discover them +from tests.itf_updates.qemu_utils import ( + qemu_ifs_image, + qemu_run_script, + qemu_dual_instances, +) + + +# --------------------------------------------------------------------------- +# ITF SSH Fixtures - wrap ITF's target fixture for test compatibility +# --------------------------------------------------------------------------- -@pytest.fixture(scope="class") -def someipd_config() -> Path: - """Provide SOME/IP configuration parameters.""" - # TODO: We probably should have a way to add more configuration from fixtures in the tests - return Path("vsomeip-local.json") +@pytest.fixture +def ssh_client(target): + """Create ITF SSH connection to QEMU guest. -@pytest.fixture(scope="class") -def someipd(someipd_config) -> Generator[None, None, None]: - """Start someipd before tests and stop it after.""" - someipd = subprocess.Popen( - ["src/someipd/someipd"], - env={"VSOMEIP_CONFIGURATION": str(someipd_config.absolute())}, + Uses ITF's target fixture (from qemu plugin) which automatically + starts QEMU with the configured TAP devices. + """ + with target.ssh() as connection: + yield connection + + +@pytest.fixture +def dual_ssh_clients(qemu_dual_instances): + """Create ITF SSH connections to both QEMU instances. + + Uses qemu_utils.py to automatically start two QEMUs via run_qemu.sh. + Depends on qemu_dual_instances which handles QEMU lifecycle. + """ + instance1, instance2 = qemu_dual_instances + + ssh1 = Ssh( + target_ip=instance1.ssh_host, + port=22, + timeout=15, + n_retries=5, + retry_interval=2, + username="root", + password="", + ) + ssh2 = Ssh( + target_ip=instance2.ssh_host, + port=22, + timeout=15, + n_retries=5, + retry_interval=2, + username="root", + password="", ) - yield - someipd.terminate() - someipd.wait() - - -# Pytest configuration -def pytest_configure(config: pytest.Config) -> None: - """Pytest configuration hook.""" - config.addinivalue_line("markers", "integration: mark test as integration test") - config.addinivalue_line("markers", "slow: mark test as slow running") - config.addinivalue_line("markers", "network: mark test as requiring network access") - - -def pytest_collection_modifyitems( - config: pytest.Config, items: list[pytest.Item] -) -> None: - """Modify test items during collection.""" - for item in items: - # Auto-mark all tests in this directory as integration tests - item.add_marker(pytest.mark.integration) + + with ssh1 as client1, ssh2 as client2: + yield client1, client2 diff --git a/tests/integration/docker_setup/Dockerfile b/tests/integration/docker_setup/Dockerfile deleted file mode 100644 index 18a9e4837..000000000 --- a/tests/integration/docker_setup/Dockerfile +++ /dev/null @@ -1,61 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -FROM ubuntu:noble -SHELL ["/bin/bash", "-xec"] -RUN export DEBIAN_FRONTEND=noninteractive;\ - apt-get update;\ - apt-get dist-upgrade --purge --yes\ - bind9-dnsutils\ - gdb\ - iptables\ - iproute2\ - libnetfilter-queue-dev\ - libnfnetlink-dev\ - lsof\ - net-tools\ - openssh-server\ - pkg-config\ - python3-pip\ - python3-scapy\ - tcpdump\ - socat\ - iputils-ping\ - strace\ - ;\ - apt-get autoremove --purge --yes;\ - apt-get clean; - -# Pinned GCC version -ENV CC=gcc-12 -ENV CXX=g++-12 -ENV GCOV=gcov-12 - -# /etc configurations -RUN echo 'SendEnv *' > /etc/ssh/ssh_config.d/vsomeip.conf;\ - echo 'AcceptEnv *' > /etc/ssh/sshd_config.d/vsomeip.conf - -# SSH configurations -RUN ssh-keygen -q -b 1024 -f ~/.ssh/id_rsa -t rsa -N '';\ - install -DTm0400 ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys;\ - install -DTm0400 ~/.ssh/id_rsa /commonapi_main/lxc-config/.ssh/mgc_lxc/rsa_key_file.pub - -# Add ssh_config to the container -COPY tests/integration/docker_setup/ssh_config /root/.ssh/config -RUN chown root:root /root/.ssh/config && chmod 600 /root/.ssh/config - -# Place stuff in a known place -WORKDIR /home/source - -# Declare bind-mounts -VOLUME ["/home/build", "/home/logs", "/home/source"] diff --git a/tests/integration/docker_setup/docker-compose.yaml b/tests/integration/docker_setup/docker-compose.yaml deleted file mode 100644 index 6695d8375..000000000 --- a/tests/integration/docker_setup/docker-compose.yaml +++ /dev/null @@ -1,72 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -networks: - docker-network: - attachable: false - driver: macvlan - internal: true - ipam: - config: - - subnet: 192.168.87.0/24 - gateway: 192.168.87.254 - -x-common: &common - build: - args: - - http_proxy - - https_proxy - - no_proxy - context: ../../.. - dockerfile: tests/integration/docker_setup/Dockerfile - cap_add: - - NET_ADMIN - environment: - GLOBAL_TIMEOUT: 240 minutes - networks: - - docker-network - security_opt: - - seccomp:unconfined - ulimits: - core: -1 - volumes: - - ${SOURCE_DIR:-../../..}:/home/source - - bazel-cache:/var/cache/bazel - -volumes: - bazel-cache: - external: true - name: eclipse-s-core-bazel-cache - -services: - someipd: - !!merge <<: *common - networks: - docker-network: - ipv4_address: 192.168.87.2 - depends_on: - - client - entrypoint: setarch -R tests/integration/docker_setup/entrypoint-master - sysctls: - net.ipv4.tcp_tw_reuse: 1 # Enable TIME_WAIT socket reuse - net.ipv4.ip_local_port_range: 34600 60999 # Set local port range to exclude SOME/IP server port range - - client: - !!merge <<: *common - networks: - docker-network: - ipv4_address: 192.168.87.3 - entrypoint: setarch -R tests/integration/docker_setup/entrypoint-slave - sysctls: - net.ipv4.tcp_tw_reuse: 1 # Enable TIME_WAIT socket reuse - net.ipv4.ip_local_port_range: 34600 60999 # Set local port range to exclude SOME/IP server port range diff --git a/tests/integration/docker_setup/entrypoint-master b/tests/integration/docker_setup/entrypoint-master deleted file mode 100755 index 0feae5abd..000000000 --- a/tests/integration/docker_setup/entrypoint-master +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* - -set -e - -echo "Container using someipd started" - -# Add multicast route -route add -net 224.0.0.0/4 dev eth0 - -# copy the config bin file as temp solution, as the gatewayd loads it from a relative path -cp /home/source/bazel-bin/src/gatewayd/etc/gatewayd_config.bin /home/source/src/gatewayd/etc/ - -# Automatically startup gatewayd and someipd: -# gatewayd --> connects to car_window_controller, sets up IPC communication to someipd -# someipd --> connects to gatewayd, sends out SOME/IP messages on the network - -/home/source/bazel-bin/src/gatewayd/gatewayd -service_instance_manifest /home/source/src/gatewayd/etc/mw_com_config.json & -# Give gatewayd some time to start. TODO: improve with some sort of state check -sleep 2s - -export VSOMEIP_CONFIGURATION=$(pwd)/tests/integration/vsomeip.json -/home/source/bazel-bin/src/someipd/someipd -service_instance_manifest /home/source/src/someipd/etc/mw_com_config.json & - -# /home/source/bazel-bin/tests/performance_benchmarks/ipc_benchmarks - -sleep infinity diff --git a/tests/integration/docker_setup/readme.md b/tests/integration/docker_setup/readme.md deleted file mode 100644 index 0876f3981..000000000 --- a/tests/integration/docker_setup/readme.md +++ /dev/null @@ -1,11 +0,0 @@ -Create the containers (you must be within the top level of the score-someip-gateway repo). - - ```bash - docker compose --project-directory tests/integration/docker_setup/ build - ``` - -Start the containers, using the entrypoint files. - - ```bash - docker compose --project-directory tests/integration/docker_setup/ up - ``` diff --git a/tests/integration/docker_setup/ssh_config b/tests/integration/docker_setup/ssh_config deleted file mode 100644 index 45260a2ae..000000000 --- a/tests/integration/docker_setup/ssh_config +++ /dev/null @@ -1,5 +0,0 @@ -Host slave - ProxyCommand socat - TCP4:slave:%p,bind=:22,reuseaddr - User root - IdentityFile /commonapi_main/lxc-config/.ssh/mgc_lxc/rsa_key_file.pub - Port 22 diff --git a/tests/integration/qemu_config.json b/tests/integration/qemu_config.json new file mode 100644 index 000000000..b9554d8ca --- /dev/null +++ b/tests/integration/qemu_config.json @@ -0,0 +1,12 @@ +{ + "networks": [ + { + "name": "tap-qemu1", + "ip_address": "192.168.87.2", + "gateway": "192.168.87.1" + } + ], + "ssh_port": 22, + "qemu_num_cores": 2, + "qemu_ram_size": "1G" +} diff --git a/tests/integration/sample_client/BUILD.bazel b/tests/integration/sample_client/BUILD.bazel index e9bb0ffc4..fafc99f0d 100644 --- a/tests/integration/sample_client/BUILD.bazel +++ b/tests/integration/sample_client/BUILD.bazel @@ -13,6 +13,12 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +# Export vsomeip config for QEMU deployment +exports_files( + ["vsomeip.json"], + visibility = ["//visibility:public"], +) + cc_binary( name = "sample_client", srcs = ["sample_client.cpp"], diff --git a/tests/integration/sample_client/vsomeip.json b/tests/integration/sample_client/vsomeip.json index 6d70413a5..5fe2c2faf 100644 --- a/tests/integration/sample_client/vsomeip.json +++ b/tests/integration/sample_client/vsomeip.json @@ -1,5 +1,6 @@ { "unicast": "192.168.87.3", + "netmask": "255.255.255.0", "logging": { "level": "debug", "console": "true" diff --git a/tests/integration/test_qemu_network.py b/tests/integration/test_qemu_network.py new file mode 100644 index 000000000..0dcaec664 --- /dev/null +++ b/tests/integration/test_qemu_network.py @@ -0,0 +1,390 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""QEMU network connectivity tests. + +Tests for verifying bridge networking between QEMU instances. + +Single instance tests (bridge): + bazel test //tests/integration:test_qemu_network_single --test_output=all --config=x86_64-qnx + +Dual instance tests (bridge inter-QEMU): + bazel test //tests/integration:test_qemu_network_dual --test_output=all --config=x86_64-qnx +""" + +import os +import signal +import subprocess +import tempfile +import time + + +class TestSingleInstanceNetwork: + """Tests for single QEMU instance with bridge networking.""" + + def test_ssh_connection(self, ssh_client): + """Test basic SSH connectivity to QNX QEMU as root user.""" + # Use full path since SSH session may not have /proc/boot in PATH + _, stdout, stderr = ssh_client.exec_command("/proc/boot/uname -a && /proc/boot/whoami") + output = stdout.read().decode().strip() + + assert stdout.channel.recv_exit_status() == 0, f"Command failed: {stderr.read().decode()}" + assert "QNX" in output and "root" in output, f"Expected QNX and root, got: {output}" + + def test_bridge_interface_configured(self, ssh_client): + """Verify vtnet0 (bridge) interface is configured with 192.168.87.2.""" + _, stdout, stderr = ssh_client.exec_command("/proc/boot/ifconfig vtnet0") + exit_code = stdout.channel.recv_exit_status() + output = stdout.read().decode().strip() + + assert exit_code == 0, f"ifconfig vtnet0 failed: {stderr.read().decode()}" + assert "192.168.87.2" in output, f"Expected 192.168.87.2, got: {output}" + + def test_bridge_gateway_reachable(self, ssh_client): + """Verify guest can ping the bridge gateway (host at 192.168.87.1).""" + _, stdout, stderr = ssh_client.exec_command("/proc/boot/ping -c 3 192.168.87.1") + exit_code = stdout.channel.recv_exit_status() + output = stdout.read().decode().strip() + + assert exit_code == 0, f"Cannot ping gateway: {stderr.read().decode()}" + assert "3 packets transmitted" in output or "3 received" in output.lower() or "0% packet loss" in output + + def test_bridge_default_route(self, ssh_client): + """Verify default route points to 192.168.87.1.""" + # QNX route uses 'get default' to show default route info + _, stdout, stderr = ssh_client.exec_command("/proc/boot/route get default 2>&1") + exit_code = stdout.channel.recv_exit_status() + output = stdout.read().decode().strip() + + # Default route should go through gateway 192.168.87.1 + assert "192.168.87.1" in output, f"Default route not via 192.168.87.1: {output}" + + +class TestDualInstanceNetwork: + """Tests for two QEMU instances communicating via bridge networking.""" + + def test_both_instances_have_bridge_interface(self, dual_ssh_clients): + """Verify both instances have vtnet0 (bridge) interface configured.""" + client1, client2 = dual_ssh_clients + + # Check instance 1 + _, stdout, _ = client1.exec_command("/proc/boot/ifconfig vtnet0 2>/dev/null || echo 'NO_VTNET0'") + output1 = stdout.read().decode().strip() + + # Check instance 2 + _, stdout, _ = client2.exec_command("/proc/boot/ifconfig vtnet0 2>/dev/null || echo 'NO_VTNET0'") + output2 = stdout.read().decode().strip() + + assert "NO_VTNET0" not in output1, f"Instance 1 missing vtnet0: {output1}" + assert "NO_VTNET0" not in output2, f"Instance 2 missing vtnet0: {output2}" + assert "192.168.87.2" in output1, f"Instance 1 wrong IP: {output1}" + assert "192.168.87.3" in output2, f"Instance 2 wrong IP: {output2}" + + def test_instance1_can_ping_instance2(self, dual_ssh_clients): + """Verify instance 1 can ping instance 2 via bridge network.""" + client1, _ = dual_ssh_clients + + _, stdout, stderr = client1.exec_command("/proc/boot/ping -c 3 192.168.87.3") + exit_code = stdout.channel.recv_exit_status() + output = stdout.read().decode().strip() + + assert exit_code == 0, f"Instance 1 cannot ping instance 2: {stderr.read().decode()}\n{output}" + + def test_instance2_can_ping_instance1(self, dual_ssh_clients): + """Verify instance 2 can ping instance 1 via bridge network.""" + _, client2 = dual_ssh_clients + + _, stdout, stderr = client2.exec_command("/proc/boot/ping -c 3 192.168.87.2") + exit_code = stdout.channel.recv_exit_status() + output = stdout.read().decode().strip() + + assert exit_code == 0, f"Instance 2 cannot ping instance 1: {stderr.read().decode()}\n{output}" + + def test_both_instances_can_reach_host(self, dual_ssh_clients): + """Verify both instances can reach host via bridge (192.168.87.1).""" + client1, client2 = dual_ssh_clients + + for i, client in enumerate([client1, client2], 1): + _, stdout, stderr = client.exec_command("/proc/boot/ping -c 1 192.168.87.1") + exit_code = stdout.channel.recv_exit_status() + + assert exit_code == 0, f"Instance {i} cannot reach host via bridge" + + +class TestSomeIPSD: + """Tests for SOME/IP Service Discovery between two QEMU instances. + + Starts tcpdump to capture traffic, launches both QEMU instances with + their respective services (gatewayd/someipd on QEMU 1, sample_client + on QEMU 2), waits for service discovery to complete, then analyzes + the captured pcap to verify offers, finds, and subscriptions. + + OFFERS: + What the service is offered by the host's someipd configuration. + + FINDS: + What service is looking for. + + SUBSCRIBED SUCCESSFULLY TO:: + A subscription succeeds only if the remote side offers the + matching service. + """ + + TCPDUMP_INTERFACE = "virbr0" + QEMU1_IP = "192.168.87.2" + QEMU2_IP = "192.168.87.3" + SERVICE_SETTLE_TIME = 20 # seconds to let SD exchange complete + + SSH_OPTS = [ + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "LogLevel=ERROR", + ] + + # Expected SOME/IP-SD state per host + EXPECTED = { + "192.168.87.2": { + "name": "someipd", + "offers": {"Service 0x1234.0x5678"}, + "finds": {"Service 0x4321.0x5678"}, + "subscribed": {"Service 0x4321.0x5678, eventgroup 0x4465"}, + }, + "192.168.87.3": { + "name": "sample_client", + "offers": {"Service 0x4321.0x5678"}, + "finds": {"Service 0x1234.0x5678"}, + "subscribed": {"Service 0x1234.0x5678, eventgroup 0x4465"}, + }, + } + + @staticmethod + def _get_script_path(relative_path: str) -> str: + """Resolve a path relative to the workspace root.""" + workspace_root = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..") + ) + return os.path.join(workspace_root, relative_path) + + @staticmethod + def _wait_for_ssh(host: str, timeout: int = 60) -> bool: + """Poll until SSH is accepting connections on *host*.""" + deadline = time.time() + timeout + while time.time() < deadline: + try: + result = subprocess.run( + ["ssh"] + TestSomeIPSD.SSH_OPTS + [f"root@{host}", "echo ready"], + capture_output=True, + timeout=5, + ) + if result.returncode == 0: + return True + except subprocess.TimeoutExpired: + pass + time.sleep(2) + return False + + @staticmethod + def _ssh_run_bg(host: str, cmd: str) -> None: + """Fire-and-forget a command on *host* via SSH.""" + subprocess.Popen( + ["ssh"] + TestSomeIPSD.SSH_OPTS + [f"root@{host}", cmd], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + @staticmethod + def _parse_analyze_output(output: str) -> dict: + """Parse the output of analyze_pcap_someip.py into a per-IP dict. + + Returns a dict keyed by IP with sub-keys 'offers', 'finds', + 'subscribed', each a set of stripped entry strings. + """ + result = {} + current_ip = None + current_section = None + + for line in output.splitlines(): + line_stripped = line.strip() + + # Detect SUMMARY header → "SUMMARY: someipd (192.168.87.2)" + if line_stripped.startswith("SUMMARY:"): + ip_start = line_stripped.rfind("(") + ip_end = line_stripped.rfind(")") + if ip_start != -1 and ip_end != -1: + current_ip = line_stripped[ip_start + 1 : ip_end] + result[current_ip] = { + "offers": set(), + "finds": set(), + "subscribed": set(), + } + current_section = None + continue + + if current_ip is None: + continue + + # Detect section headers + if line_stripped == "OFFERS:": + current_section = "offers" + continue + elif line_stripped == "FINDS:": + current_section = "finds" + continue + elif line_stripped.startswith("SUBSCRIBED SUCCESSFULLY TO:"): + current_section = "subscribed" + continue + + # Collect entry lines (indented with "Service …") + if current_section and line_stripped.startswith("Service"): + result[current_ip][current_section].add(line_stripped) + + return result + + def test_someip_sd_offers_finds_subscriptions(self, qemu_dual_instances): + """Verify SOME/IP-SD offers, finds, and subscriptions between QEMUs. + + 1. Start tcpdump on virbr0 capturing SOME/IP traffic. + 2. Start gatewayd + someipd on QEMU 1 and sample_client on QEMU 2. + 3. Wait for service discovery to settle. + 4. Stop the services and tcpdump. + 5. Run analyze_pcap_someip.py on the capture. + 6. Assert expected offers / finds / subscriptions per host. + """ + instance1, instance2 = qemu_dual_instances + + # Use TEST_TMPDIR (Bazel sandbox) if available, else /tmp + tmp_dir = os.environ.get("TEST_TMPDIR", tempfile.gettempdir()) + pcap_file = os.path.join(tmp_dir, "someip_sd_test.pcap") + analyze_script = self._get_script_path( + "tests/integration/analyze_pcap_someip.py" + ) + + # --- 1. Start tcpdump ------------------------------------------------ + tcpdump_log = os.path.join(tmp_dir, "tcpdump_stderr.log") + tcpdump_log_fh = open(tcpdump_log, "w") + tcpdump_proc = subprocess.Popen( + [ + "tcpdump", + "-i", self.TCPDUMP_INTERFACE, + "-w", pcap_file, + f"host {self.QEMU1_IP} or host {self.QEMU2_IP}", + ], + stdout=subprocess.DEVNULL, + stderr=tcpdump_log_fh, + ) + # Give tcpdump a moment to initialise the capture + time.sleep(2) + assert tcpdump_proc.poll() is None, ( + f"tcpdump exited early (rc={tcpdump_proc.returncode}). " + f"stderr: {open(tcpdump_log).read()}" + ) + + try: + # --- 2. Launch services on both QEMUs ----------------------------- + + # QEMU 1 – gatewayd then someipd (mirrors setup_qemu_1.sh) + self._ssh_run_bg( + instance1.ssh_host, + "/usr/bin/gatewayd -config_file /etc/gatewayd/gatewayd_config.bin " + "--service_instance_manifest /etc/gatewayd/mw_com_config.json", + ) + time.sleep(3) + self._ssh_run_bg( + instance1.ssh_host, + "export VSOMEIP_CONFIGURATION=/etc/someipd/vsomeip.json && " + "/usr/bin/someipd --service_instance_manifest /etc/someipd/mw_com_config.json", + ) + time.sleep(2) + + # QEMU 2 – sample_client (mirrors setup_qemu_2.sh) + self._ssh_run_bg( + instance2.ssh_host, + "export VSOMEIP_CONFIGURATION=/etc/sample_client/vsomeip.json && " + "/usr/bin/sample_client", + ) + + # --- 3. Wait for service discovery to settle ---------------------- + time.sleep(self.SERVICE_SETTLE_TIME) + + finally: + # --- 4. Stop tcpdump --------------------------------------------- + tcpdump_proc.send_signal(signal.SIGINT) + try: + tcpdump_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + tcpdump_proc.kill() + tcpdump_proc.wait() + tcpdump_log_fh.close() + + # --- 5. Analyse the pcap file ----------------------------------------- + result = subprocess.run( + ["python3", analyze_script, pcap_file], + capture_output=True, + text=True, + timeout=30, + ) + output = result.stdout + assert os.path.exists(pcap_file), ( + f"pcap file was not created at {pcap_file}. " + f"tcpdump log: {open(tcpdump_log).read()}" + ) + assert result.returncode == 0, ( + f"analyze_pcap_someip.py failed (rc={result.returncode}):\n" + f"{result.stderr}\n{output}" + ) + + parsed = self._parse_analyze_output(output) + + # --- 6. Verify per host ----------------------------------------------- + for ip, expected in self.EXPECTED.items(): + host_name = expected["name"] + + print("-" * 80) + print(f"CHECKING: {host_name} ({ip})") + print("-" * 80) + + assert ip in parsed, ( + f"No SOME/IP-SD data found for {host_name} ({ip}).\n" + f"Full output:\n{output}" + ) + + actual = parsed[ip] + + print(f" OFFERS:") + print(f" expected: {sorted(expected['offers'])}") + print(f" actual: {sorted(actual['offers'])}") + assert expected["offers"] == actual["offers"], ( + f"[{host_name}] OFFERS mismatch.\n" + f" expected: {expected['offers']}\n" + f" actual: {actual['offers']}" + ) + print(f" -> OK") + + print(f" FINDS:") + print(f" expected: {sorted(expected['finds'])}") + print(f" actual: {sorted(actual['finds'])}") + assert expected["finds"] == actual["finds"], ( + f"[{host_name}] FINDS mismatch.\n" + f" expected: {expected['finds']}\n" + f" actual: {actual['finds']}" + ) + print(f" -> OK") + + print(f" SUBSCRIBED SUCCESSFULLY TO:") + print(f" expected: {sorted(expected['subscribed'])}") + print(f" actual: {sorted(actual['subscribed'])}") + assert expected["subscribed"] == actual["subscribed"], ( + f"[{host_name}] SUBSCRIBED SUCCESSFULLY TO mismatch.\n" + f" expected: {expected['subscribed']}\n" + f" actual: {actual['subscribed']}" + ) + print(f" -> OK") diff --git a/tests/integration/vsomeip.json b/tests/integration/vsomeip.json index 85bb8fc3c..cb15f10ba 100644 --- a/tests/integration/vsomeip.json +++ b/tests/integration/vsomeip.json @@ -1,5 +1,6 @@ { "unicast": "192.168.87.2", + "netmask": "255.255.255.0", "logging": { "level": "debug", "console": "true", diff --git a/tests/itf_updates/BUILD.bazel b/tests/itf_updates/BUILD.bazel new file mode 100644 index 000000000..d26f05b94 --- /dev/null +++ b/tests/itf_updates/BUILD.bazel @@ -0,0 +1,29 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Common test utilities for UT and integration tests.""" + +exports_files( + [ + "qemu_utils.py", + ], + visibility = ["//tests:__subpackages__"], +) + +py_library( + name = "itf_updates", + srcs = [ + "qemu_utils.py", + ], + visibility = ["//tests:__subpackages__"], +) diff --git a/tests/itf_updates/qemu_utils.py b/tests/itf_updates/qemu_utils.py new file mode 100644 index 000000000..ad66b86a4 --- /dev/null +++ b/tests/itf_updates/qemu_utils.py @@ -0,0 +1,344 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Common QEMU utilities for UT and integration tests. + +This module provides shared functionality for managing QEMU instances, +SSH connections, and Bazel runfile discovery. +""" + +import logging +import os +import signal +import socket +import subprocess +import tempfile +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Generator, Optional + +import paramiko +import pytest + +# Suppress noisy paramiko logs during SSH retry attempts +logging.getLogger("paramiko").setLevel(logging.CRITICAL) + +# --------------------------------------------------------------------------- +# QEMU Configuration Constants +# --------------------------------------------------------------------------- + +# Bridge networking configuration +# Guests are directly accessible on the bridge network +BRIDGE_SUBNET = "192.168.87" +SSH_USER = "root" +SSH_PORT = 22 # Direct SSH to guest (no port forwarding) +SSH_TIMEOUT = 10 # SSH connection timeout in seconds + +# Boot timeout +QEMU_BOOT_TIMEOUT = 60 # seconds + + +def get_guest_ip(instance_id: int) -> str: + """Get the bridge IP address for a QEMU instance. + + Args: + instance_id: Instance ID (1 or 2). + + Returns: + IP address string (e.g., '192.168.87.2' for instance 1). + """ + return f"{BRIDGE_SUBNET}.{1 + instance_id}" + +# Bazel workspace name in runfiles (bzlmod uses "_main" for the root module) +WORKSPACE_NAME = "_main" + + +# --------------------------------------------------------------------------- +# Bazel Runfiles Utilities +# --------------------------------------------------------------------------- + + +def get_runfile_path(relative_path: str) -> Optional[Path]: + """Get the path to a file in Bazel runfiles. + + This uses standard Bazel environment variables to locate runfiles. + Bazel sets RUNFILES_DIR when running tests via 'bazel test'. + + Args: + relative_path: Path relative to workspace root (e.g., "deployment/qemu/run_qemu.sh") + + Returns: + Absolute path to the file, or None if not found. + """ + # Primary method: RUNFILES_DIR (set by Bazel test runner) + runfiles_dir = os.environ.get("RUNFILES_DIR") + if runfiles_dir: + candidate = Path(runfiles_dir) / WORKSPACE_NAME / relative_path + if candidate.exists(): + return candidate + + return None + + +# --------------------------------------------------------------------------- +# SSH Connection Utilities +# --------------------------------------------------------------------------- + + +def wait_for_ssh(host: str, port: int, user: str, timeout: int = 60) -> bool: + """Wait for SSH to be fully ready (not just port open). + + + Args: + host: SSH hostname to connect to. + port: SSH port number. + user: SSH username. + timeout: Maximum time to wait in seconds. + + Returns: + True if SSH is ready, False if timeout exceeded. + """ + start_time = time.time() + last_error = None + attempt = 0 + + while time.time() - start_time < timeout: + attempt += 1 + try: + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect( + hostname=host, + port=port, + username=user, + password="", + timeout=5, + allow_agent=False, + look_for_keys=False, + banner_timeout=10, + ) + # Successfully connected - run a quick command to verify + _, stdout, _ = client.exec_command("echo ready") + stdout.channel.recv_exit_status() + client.close() + return True + except (paramiko.SSHException, socket.error, EOFError, ConnectionResetError) as e: + last_error = e + # Exponential backoff: 1s, 2s, 3s... up to 5s max + backoff = min(attempt, 5) + time.sleep(backoff) + + print(f"SSH not ready after {timeout}s. Last error: {last_error}") + return False + + +# --------------------------------------------------------------------------- +# QEMU Instance Management +# --------------------------------------------------------------------------- + + +@dataclass +class QEMUInstance: + """Represents a running QEMU instance. + + Attributes: + process: The subprocess.Popen object for the QEMU process. + instance_id: Unique identifier for this instance (1, 2, etc.). + ssh_host: SSH hostname/IP for this instance (192.168.87.2 for id=1, etc.). + qconn_port: QConn port for this instance (8000 for id=1, 8001 for id=2). + log_file: Path to the QEMU log file. + pid_file: Path to the PID file for cleanup. + """ + + process: subprocess.Popen + instance_id: int + ssh_host: str + qconn_port: int + log_file: Path + pid_file: Path + + def get_ssh_client(self, retries: int = 3) -> paramiko.SSHClient: + """Create SSH connection to this QEMU instance with retry logic. + + Args: + retries: Number of connection attempts before failing. + + Returns: + Connected paramiko.SSHClient. + + Raises: + paramiko.SSHException: If all connection attempts fail. + """ + last_error = None + for attempt in range(retries): + try: + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect( + hostname=self.ssh_host, + port=SSH_PORT, + username=SSH_USER, + password="", + timeout=SSH_TIMEOUT, + allow_agent=False, + look_for_keys=False, + banner_timeout=15, + ) + return client + except (paramiko.SSHException, socket.error, EOFError, ConnectionResetError) as e: + last_error = e + if attempt < retries - 1: + time.sleep(2) + raise paramiko.SSHException(f"Failed to connect after {retries} attempts: {last_error}") + + def stop(self): + """Stop this QEMU instance gracefully.""" + if self.process and self.process.poll() is None: + self.process.terminate() + try: + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.wait() + + +def start_qemu( + ifs_image: Path, + run_script: Path, + instance_id: int = 1, + net_mode: str = "slirp", +) -> QEMUInstance: + """Start a QEMU instance and wait for it to boot. + + Args: + ifs_image: Path to the QNX IFS image. + run_script: Path to the run_qemu.sh script. + instance_id: Unique identifier for this instance (default: 1). + net_mode: Network mode - "bridge" for bridge networking. + + Returns: + QEMUInstance representing the running QEMU. + + Raises: + pytest.fail: If QEMU fails to boot or SSH is not ready. + """ + ssh_host = get_guest_ip(instance_id) + qconn_port = 7999 + instance_id + + # Create log file + log_dir = Path(tempfile.gettempdir()) / "qemu-test" + log_dir.mkdir(exist_ok=True) + log_file = log_dir / f"qemu-instance-{instance_id}.log" + pid_file = Path(f"/tmp/qemu-someip-gateway-{instance_id}.pid") + + # Clean up any existing instance on same ports + if pid_file.exists(): + try: + old_pid = int(pid_file.read_text().strip()) + os.kill(old_pid, signal.SIGTERM) + time.sleep(1) + except (ProcessLookupError, ValueError): + pass + pid_file.unlink(missing_ok=True) + + # Start QEMU + env = os.environ.copy() + env["QEMU_NET_MODE"] = net_mode + env["QEMU_INSTANCE_ID"] = str(instance_id) + + with open(log_file, "w") as log: + process = subprocess.Popen( + [str(run_script), str(ifs_image), str(instance_id)], + stdout=log, + stderr=subprocess.STDOUT, + env=env, + start_new_session=True, # Prevent signals from propagating + ) + + instance = QEMUInstance( + process=process, + instance_id=instance_id, + ssh_host=ssh_host, + qconn_port=qconn_port, + log_file=log_file, + pid_file=pid_file, + ) + + # Wait for SSH to be fully ready (not just port open) + if not wait_for_ssh(ssh_host, SSH_PORT, SSH_USER, timeout=QEMU_BOOT_TIMEOUT): + # Read log for debugging + log_content = log_file.read_text() if log_file.exists() else "No log" + instance.stop() + pytest.fail( + f"QEMU instance {instance_id} failed to boot within {QEMU_BOOT_TIMEOUT}s. " + f"SSH not ready at {ssh_host}:{SSH_PORT}.\nLog:\n{log_content[-2000:]}" + ) + + return instance + + +# --------------------------------------------------------------------------- +# Common Pytest Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="session") +def qemu_ifs_image() -> Path: + """Locate the QNX IFS image from Bazel runfiles.""" + path = "deployment/qemu/someip_gateway_x86_64.ifs" + found = get_runfile_path(path) + if found: + return found + pytest.fail( + f"Cannot find QNX IFS image at '{WORKSPACE_NAME}/{path}'. " + "Build it first: bazel build //deployment/qemu:someip_gateway_ifs --config=x86_64-qnx" + ) + + +@pytest.fixture(scope="session") +def qemu_run_script() -> Path: + """Locate the QEMU run script from Bazel runfiles.""" + path = "deployment/qemu/run_qemu.sh" + found = get_runfile_path(path) + if found: + return found + pytest.fail(f"Cannot find run_qemu.sh at '{WORKSPACE_NAME}/{path}'.") + + +@pytest.fixture(scope="module") +def qemu_dual_instances( + qemu_ifs_image: Path, qemu_run_script: Path +) -> Generator[tuple[QEMUInstance, QEMUInstance], None, None]: + """Start two QEMU instances for inter-QEMU communication tests. + + Instance 1: 192.168.87.2:22 (direct bridge access) + Instance 2: 192.168.87.3:22 (direct bridge access) + """ + instance1 = start_qemu( + ifs_image=qemu_ifs_image, + run_script=qemu_run_script, + instance_id=1, + net_mode="dual", + ) + + instance2 = start_qemu( + ifs_image=qemu_ifs_image, + run_script=qemu_run_script, + instance_id=2, + net_mode="dual", + ) + + yield instance1, instance2 + + instance1.stop() + instance2.stop() diff --git a/third_party/BUILD.bazel b/third_party/BUILD.bazel new file mode 100644 index 000000000..c6e870e96 --- /dev/null +++ b/third_party/BUILD.bazel @@ -0,0 +1,20 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +exports_files([ + "baselibs-multiarch.patch", + "boost-asio_qnx.patch", + "score_itf_oci_conflict.patch", + "vsomeip.BUILD.bazel", + "vsomeip_qnx8.patch", +]) diff --git a/third_party/score_itf_oci_conflict.patch b/third_party/score_itf_oci_conflict.patch new file mode 100644 index 000000000..543ad32f8 --- /dev/null +++ b/third_party/score_itf_oci_conflict.patch @@ -0,0 +1,21 @@ +--- a/MODULE.bazel ++++ b/MODULE.bazel +@@ -87,17 +87,7 @@ bazel_dep(name = "rules_oci", version = "1.8.0") + + oci = use_extension("@rules_oci//oci:extensions.bzl", "oci") + +-# Declare external images you need to pull, for example: +-oci.pull( +- name = "ubuntu_24_04", +- digest = "sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30", +- image = "ubuntu", +- platforms = ["linux/amd64"], +- tag = "24.04", +-) +- +-# For each oci.pull call, repeat the "name" here to expose them as dependencies. +-use_repo(oci, "ubuntu_24_04") ++# OCI images disabled - conflicts with score_tooling ubuntu_24_04_linux_amd64 + + ############################################################################### + # diff --git a/third_party/vsomeip-qnx8.patch b/third_party/vsomeip_qnx8.patch similarity index 66% rename from third_party/vsomeip-qnx8.patch rename to third_party/vsomeip_qnx8.patch index 9ad093571..7c9b84030 100644 --- a/third_party/vsomeip-qnx8.patch +++ b/third_party/vsomeip_qnx8.patch @@ -43,3 +43,17 @@ index 77c261cc..6e5a71b7 100644 /* * These definitions MUST remain in the global namespace. +diff --git a/implementation/routing/src/routing_manager_impl.cpp b/implementation/routing/src/routing_manager_impl.cpp +--- a/implementation/routing/src/routing_manager_impl.cpp ++++ b/implementation/routing/src/routing_manager_impl.cpp +@@ -3453,6 +3453,10 @@ void routing_manager_impl::on_net_interface_or_route_state_changed(bool _is_inte + void routing_manager_impl::start_ip_routing() { + #if defined(_WIN32) || defined(__QNX__) + if_state_running_ = true; ++ sd_route_set_ = true; ++ // QNX: Process any pending service offers now that routing is ready ++ // (On Linux, this is done via on_net_interface_or_route_state_changed) ++ init_pending_services(); + #endif + + if (routing_ready_handler_) {