From 4311d1931a97561366a6ab96b12e8005d3495620 Mon Sep 17 00:00:00 2001 From: Jiachen Dong Date: Sat, 10 Sep 2022 04:32:17 +0800 Subject: [PATCH] [harness-simulation] add multiple-version support (#8075) This commit adds multiple-version support, and uses a unified configuration file to specify all the parameters. --- tools/harness-simulation/README.md | 56 ++----- .../Thread_Harness/THCI/OpenThread_BR_Sim.py | 27 ++-- .../Thread_Harness/THCI/OpenThread_Sim.py | 37 +++-- .../Thread_Harness/simulation/config.py | 13 +- tools/harness-simulation/harness/install.bat | 21 +-- .../posix/build_docker_image.sh | 74 ---------- tools/harness-simulation/posix/config.py | 36 ----- tools/harness-simulation/posix/install.sh | 120 ++++++++++++++- .../posix/launch_testbed.py | 138 +++++++++--------- .../posix/otbr_sim/otbr_docker.py | 14 +- .../harness-simulation/posix/simulation.conf | 120 +++++++++++++++ 11 files changed, 381 insertions(+), 275 deletions(-) delete mode 100755 tools/harness-simulation/posix/build_docker_image.sh delete mode 100644 tools/harness-simulation/posix/config.py mode change 100644 => 100755 tools/harness-simulation/posix/launch_testbed.py create mode 100644 tools/harness-simulation/posix/simulation.conf diff --git a/tools/harness-simulation/README.md b/tools/harness-simulation/README.md index 61e1a95c3..0471685ae 100644 --- a/tools/harness-simulation/README.md +++ b/tools/harness-simulation/README.md @@ -10,33 +10,19 @@ Platform developers should modify the THCI implementation and/or the SI implemen ## POSIX Environment Setup -1. Build OpenThread to generate standalone OpenThread simulation `ot-cli-ftd`. For example, to run OpenThread 1.2 test cases, run the following command in the top directory of OpenThread: +1. Open the JSON format configuration file `tools/harness-simulation/posix/simulation.conf`: - ```bash - $ CFLAGS='-DOPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS=8' \ - CXXFLAGS='-DOPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS=8' \ - script/cmake-build simulation \ - -DOT_THREAD_VERSION=1.2 \ - -DOT_DUA=ON \ - -DOT_MLR=ON \ - -DOT_COMMISSIONER=ON \ - -DOT_CSL_RECEIVER=ON \ - -DOT_SIMULATION_MAX_NETWORK_SIZE=64 - ``` + - Edit the value of `ot_path` to the absolute path where the top directory of the OpenThread repository is located. For example, change the value of `ot_path` to `/home//repo/openthread`. + - For each entry in `ot_build.ot`, update the value of `number` to be the number of OT FTD simulations needed with the corresponding version. + - For each entry in `ot_build.otbr`, update the value of `number` to be the number of OTBR simulations needed with the corresponding version. + - The numbers above can be adjusted according to the requirement of test cases. + - Edit the value of `ssh.username` to the username to be used for connecting to the remote POSIX environment. + - Edit the value of `ssh.password` to the password corresponding to the username above. + - Edit the value of `discovery_ifname` to the network interface that the Harness will connect to. - Then `ot-cli-ftd` is built in the directory `build/simulation/examples/apps/cli/`. + Note that it may be time-consuming to build all versions of `ot-cli-ftd`s and OTBR Docker images especially on devices such as Raspberry Pis. -2. Open the configuration file `config.py`: - - - Edit the value of `OT_PATH` to the absolute path where the top directory of the OpenThread repository is located. For example, change the value of `OT_PATH` to `/home//repo/openthread`. - -3. Run the `build_docker_image.sh` with the environment variable `OT_PATH` set properly. `OT_PATH` should be the same as that in the previous step. For example run the following command: - - ```bash - $ OT_PATH=~/repo/openthread ./build_docker_image.sh - ``` - -4. Run the installation script. +2. Run the installation script. ```bash $ tools/harness-simulation/posix/install.sh @@ -44,33 +30,23 @@ Platform developers should modify the THCI implementation and/or the SI implemen ## Test Harness Environment Setup -1. Double click the file `harness\install.bat` on the machine which installed Harness. +1. Copy the directory `tools/harness-simulation` from the POSIX machine to the Windows machine, and then switch to that directory. -2. Check the configuration file `C:\GRL\Thread1.2\Thread_Harness\simulation\config.py` - - - Edit the value of `REMOTE_USERNAME` to the username expected to connect to on the remote POSIX environment. - - Edit the value of `REMOTE_PASSWORD` to the password corresponding to the username above. - - Edit the value of `REMOTE_OT_PATH` to the absolute path where the top directory of the OpenThread repository is located. +2. Double click the file `harness\install.bat` on Windows. 3. Add the additional simulation device information in `harness\Web\data\deviceInputFields.xml` to `C:\GRL\Thread1.2\Web\data\deviceInputFields.xml`. ## Run Test Harness on Simulation -1. On POSIX machine, change directory to the top of OpenThread repository, and run the following commands. +1. On the POSIX machine, change directory to the top of the OpenThread repository, and run the following commands. ```bash $ cd tools/harness-simulation/posix - $ python3 launch_testbed.py \ - --interface=eth0 \ - --ot=6 \ - --otbr=4 \ - --sniffer=2 + $ ./launch_testbed.py -c simulation.conf ``` - This example starts 6 OT FTD simulations, 4 OTBR simulations, and 2 sniffer simulations and can be discovered on `eth0`. + This example starts several OT FTD simulations, OTBR simulations, and sniffer simulations and can be discovered on `eth0`. The number of each type of simulation is specified in the configuration file `simulation.conf`. - The arguments can be adjusted according to the requirement of test cases. - -2. Run Test Harness. The information field of the device is encoded as `@` for FTDs and `otbr_@` for BRs. Choose the proper device as the DUT accordingly. +2. Run the Test Harness. The information field of the device is encoded as `_@`. Choose the desired device as the DUT. 3. Select one or more test cases to start the test. diff --git a/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_BR_Sim.py b/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_BR_Sim.py index e2b04565e..0f68e6761 100644 --- a/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_BR_Sim.py +++ b/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_BR_Sim.py @@ -39,12 +39,15 @@ import pipes import sys import time +from THCI.IThci import IThci from THCI.OpenThread import watched from THCI.OpenThread_BR import OpenThread_BR -from simulation.config import (REMOTE_USERNAME, REMOTE_PASSWORD, REMOTE_PORT) +from simulation.config import load_config logging.getLogger('paramiko').setLevel(logging.WARNING) +config = load_config() + class SSHHandle(object): # Unit: second @@ -109,11 +112,8 @@ class SSHHandle(object): class OpenThread_BR_Sim(OpenThread_BR): def _getHandle(self): - assert self.connectType == 'ip' - assert '@' in self.telnetIp self.log('SSH connecting ...') - docker_name, ssh_ip = self.telnetIp.split('@') - return SSHHandle(ssh_ip, self.telnetPort, self.telnetUsername, self.telnetPassword, docker_name) + return SSHHandle(self.ssh_ip, self.telnetPort, self.telnetUsername, self.telnetPassword, self.docker_name) @watched def _parseConnectionParams(self, params): @@ -121,15 +121,19 @@ class OpenThread_BR_Sim(OpenThread_BR): if '@' not in discovery_add: raise ValueError('%r in the field `add` is invalid' % discovery_add) - docker_name, ssh_ip = discovery_add.split('@') + self.docker_name, self.ssh_ip = discovery_add.split('@') + self.tag, self.node_id = self.docker_name.split('_') # Let it crash if it is an invalid IP address - ipaddress.ip_address(ssh_ip) + ipaddress.ip_address(self.ssh_ip) self.connectType = 'ip' self.telnetIp = self.port = discovery_add - self.telnetPort = REMOTE_PORT - self.telnetUsername = REMOTE_USERNAME - self.telnetPassword = REMOTE_PASSWORD + + global config + ssh = config['ssh'] + self.telnetPort = ssh['port'] + self.telnetUsername = ssh['username'] + self.telnetPassword = ssh['password'] self.extraParams = { 'cmd-start-otbr-agent': 'service otbr-agent start', @@ -137,3 +141,6 @@ class OpenThread_BR_Sim(OpenThread_BR): 'cmd-restart-otbr-agent': 'service otbr-agent restart', 'cmd-restart-radvd': 'service radvd stop; service radvd start', } + + +assert issubclass(OpenThread_BR_Sim, IThci) diff --git a/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py b/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py index 00f876487..59eebcbc7 100644 --- a/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py +++ b/tools/harness-simulation/harness/Thread_Harness/THCI/OpenThread_Sim.py @@ -38,14 +38,12 @@ import socket import time import win32api +from simulation.config import load_config from THCI.IThci import IThci from THCI.OpenThread import OpenThreadTHCI, watched -from simulation.config import ( - REMOTE_USERNAME, - REMOTE_PASSWORD, - REMOTE_PORT, - REMOTE_OT_PATH, -) + +config = load_config() +ot_subpath = {item['tag']: item['subpath'] for item in config['ot_build']['ot']} class SSHHandle(object): @@ -133,21 +131,15 @@ class SSHHandle(object): class OpenThread_Sim(OpenThreadTHCI, IThci): __handle = None - # Do not use `os.path.join` as it uses backslash as the separator on Windows - device = REMOTE_OT_PATH + '/build/simulation/examples/apps/cli/ot-cli-ftd' - @watched def _connect(self): self.__lines = [] # Only actually connect once. if self.__handle is None: - assert self.connectType == 'ip' - assert '@' in self.telnetIp self.log('SSH connecting ...') - node_id, ssh_ip = self.telnetIp.split('@') - self.__handle = SSHHandle(ssh_ip, self.telnetPort, self.telnetUsername, self.telnetPassword, self.device, - node_id) + self.__handle = SSHHandle(self.ssh_ip, self.telnetPort, self.telnetUsername, self.telnetPassword, + self.device, self.node_id) self.log('connected to %s successfully', self.telnetIp) @@ -161,15 +153,22 @@ class OpenThread_Sim(OpenThreadTHCI, IThci): if '@' not in discovery_add: raise ValueError('%r in the field `add` is invalid' % discovery_add) - node_id, ssh_ip = discovery_add.split('@') + prefix, self.ssh_ip = discovery_add.split('@') + self.tag, self.node_id = prefix.split('_') # Let it crash if it is an invalid IP address - ipaddress.ip_address(ssh_ip) + ipaddress.ip_address(self.ssh_ip) + + # Do not use `os.path.join` as it uses backslash as the separator on Windows + self.device = '/'.join([config['ot_path'], ot_subpath[self.tag], 'examples/apps/cli/ot-cli-ftd']) self.connectType = 'ip' self.telnetIp = self.port = discovery_add - self.telnetPort = REMOTE_PORT - self.telnetUsername = REMOTE_USERNAME - self.telnetPassword = REMOTE_PASSWORD + + global config + ssh = config['ssh'] + self.telnetPort = ssh['port'] + self.telnetUsername = ssh['username'] + self.telnetPassword = ssh['password'] def _cliReadLine(self): if len(self.__lines) > 1: diff --git a/tools/harness-simulation/harness/Thread_Harness/simulation/config.py b/tools/harness-simulation/harness/Thread_Harness/simulation/config.py index de24adacc..63605d0a1 100644 --- a/tools/harness-simulation/harness/Thread_Harness/simulation/config.py +++ b/tools/harness-simulation/harness/Thread_Harness/simulation/config.py @@ -27,8 +27,13 @@ # POSSIBILITY OF SUCH DAMAGE. # -REMOTE_USERNAME = 'pi' -REMOTE_PASSWORD = 'raspberry' -REMOTE_PORT = 22 +import json +import os -REMOTE_OT_PATH = '/home/pi/repo/openthread' +CONFIG_PATH = r'%s\GRL\Thread1.2\Thread_Harness\simulation\simulation.conf' % os.environ['systemdrive'] + + +def load_config(): + with open(CONFIG_PATH, 'rt') as f: + config = json.load(f) + return config diff --git a/tools/harness-simulation/harness/install.bat b/tools/harness-simulation/harness/install.bat index bcf81e508..133f1e1f5 100644 --- a/tools/harness-simulation/harness/install.bat +++ b/tools/harness-simulation/harness/install.bat @@ -25,18 +25,19 @@ :: POSSIBILITY OF SUCH DAMAGE. :: -xcopy /E /Y Thread_Harness %systemdrive%\GRL\Thread1.2\Thread_Harness -copy /Y ..\..\harness-thci\OpenThread.py %systemdrive%\GRL\Thread1.2\Thread_Harness\THCI -copy /Y ..\..\harness-thci\OpenThread_BR.py %systemdrive%\GRL\Thread1.2\Thread_Harness\THCI -copy /Y ..\..\harness-thci\OpenThread.png %systemdrive%\GRL\Thread1.2\Web\images -copy /Y ..\..\harness-thci\OpenThread_BR.png %systemdrive%\GRL\Thread1.2\Web\images -xcopy /E /Y ..\posix\sniffer_sim\proto %systemdrive%\GRL\Thread1.2\Thread_Harness\simulation\Sniffer\proto +set THREADDIR=%systemdrive%\GRL\Thread1.2 +xcopy /E /Y Thread_Harness %THREADDIR%\Thread_Harness +copy /Y ..\..\harness-thci\OpenThread.py %THREADDIR%\Thread_Harness\THCI +copy /Y ..\..\harness-thci\OpenThread_BR.py %THREADDIR%\Thread_Harness\THCI +copy /Y ..\..\harness-thci\OpenThread.png %THREADDIR%\Web\images +copy /Y ..\..\harness-thci\OpenThread_BR.png %THREADDIR%\Web\images +copy /Y ..\posix\simulation.conf %THREADDIR%\Thread_Harness\simulation +xcopy /E /Y ..\posix\sniffer_sim\proto %THREADDIR%\Thread_Harness\simulation\Sniffer\proto -%systemdrive%\GRL\Thread1.2\Python27\python.exe -m pip install --upgrade pip -%systemdrive%\GRL\Thread1.2\Python27\python.exe -m pip install -r requirements.txt - -set BASEDIR=%systemdrive%\GRL\Thread1.2\Thread_Harness +%THREADDIR%\Python27\python.exe -m pip install --upgrade pip +%THREADDIR%\Python27\python.exe -m pip install -r requirements.txt +set BASEDIR=%THREADDIR%\Thread_Harness %systemdrive%\GRL\Thread1.2\Python27\python.exe -m grpc_tools.protoc -I%BASEDIR% --python_out=%BASEDIR% --grpc_python_out=%BASEDIR% simulation/Sniffer/proto/sniffer.proto pause diff --git a/tools/harness-simulation/posix/build_docker_image.sh b/tools/harness-simulation/posix/build_docker_image.sh deleted file mode 100755 index 634b04f15..000000000 --- a/tools/harness-simulation/posix/build_docker_image.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash -# -# Copyright (c) 2022, The OpenThread Authors. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# - -# NOTE: This script has not been fully tested up to now - -set -euxo pipefail - -OT_PATH=${OT_PATH:-"/home/pi/repo/openthread"} -OTBR_DOCKER_IMAGE=${OTBR_DOCKER_IMAGE:-"otbr-reference-device-1.2"} - -DOCKER_BUILD_OTBR_OPTIONS=( - "-DOTBR_DUA_ROUTING=ON" - "-DOT_DUA=ON" - "-DOT_MLR=ON" - "-DOT_THREAD_VERSION=1.2" - "-DOT_SIMULATION_MAX_NETWORK_SIZE=64" -) - -git clone https://github.com/openthread/ot-br-posix.git --recurse-submodules --shallow-submodules --depth=1 -ETC_PATH="${OT_PATH}/tools/harness-simulation/posix/etc" - -( - cd ot-br-posix - # Use system V `service` command instead - mkdir -p root/etc/init.d - cp "${ETC_PATH}/commissionerd" root/etc/init.d/commissionerd - sudo chown root:root root/etc/init.d/commissionerd - sudo chmod +x root/etc/init.d/commissionerd - - cp "${ETC_PATH}/server.patch" script/server.patch - patch script/server script/server.patch - mkdir -p root/tmp - cp "${ETC_PATH}/requirements.txt" root/tmp/requirements.txt - - docker build . \ - -t "${OTBR_DOCKER_IMAGE}" \ - -f "${ETC_PATH}/Dockerfile" \ - --build-arg REFERENCE_DEVICE=1 \ - --build-arg BORDER_ROUTING=0 \ - --build-arg BACKBONE_ROUTER=1 \ - --build-arg NAT64=0 \ - --build-arg WEB_GUI=0 \ - --build-arg REST_API=0 \ - --build-arg EXTERNAL_COMMISSIONER=1 \ - --build-arg OTBR_OPTIONS="${DOCKER_BUILD_OTBR_OPTIONS[*]}" -) - -rm -rf ot-br-posix diff --git a/tools/harness-simulation/posix/config.py b/tools/harness-simulation/posix/config.py deleted file mode 100644 index 8e12d8d84..000000000 --- a/tools/harness-simulation/posix/config.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2022, The OpenThread Authors. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. Neither the name of the copyright holder nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# - -MAX_NODES_NUM = 64 -MAX_SNIFFER_NUM = 2 -SNIFFER_SERVER_PORT_BASE = 50051 - -OT_PATH = '/home/pi/work/src/openthread-pr' -OTBR_DOCKER_IMAGE = 'otbr-reference-device-1.2' -OTBR_DOCKER_NAME_PREFIX = 'otbr_' diff --git a/tools/harness-simulation/posix/install.sh b/tools/harness-simulation/posix/install.sh index f82fbbd8b..cc23a5bb2 100755 --- a/tools/harness-simulation/posix/install.sh +++ b/tools/harness-simulation/posix/install.sh @@ -29,9 +29,123 @@ set -euxo pipefail -BASE_DIR=$(dirname "$0") -SNIFFER_DIR="${BASE_DIR}/sniffer_sim" +POSIX_DIR="$(cd "$(dirname "$0")" && pwd)" +OT_DIR="${POSIX_DIR}/../../.." +ETC_DIR="${POSIX_DIR}/etc" +SNIFFER_DIR="${POSIX_DIR}/sniffer_sim" -pip3 install -r "${BASE_DIR}/requirements.txt" +CONFIG=${1:-"${POSIX_DIR}/simulation.conf"} +# Use absolute path in case of changing current working directory +if [[ ${CONFIG:0:1} != '/' ]]; then + CONFIG="${POSIX_DIR}/${CONFIG}" +fi +MAX_NETWORK_SIZE=$(jq -r '.ot_build.max_number' "$CONFIG") + +PACKAGES=( + "docker.io" + "git" + "jq" + "socat" + "tshark" +) + +sudo apt install -y "${PACKAGES[@]}" + +pip3 install -r "${POSIX_DIR}/requirements.txt" python3 -m grpc_tools.protoc -I"${SNIFFER_DIR}" --python_out="${SNIFFER_DIR}" --grpc_python_out="${SNIFFER_DIR}" proto/sniffer.proto + +build_ot() +{ + # SC2155: Declare and assign separately to avoid masking return values + local target build_dir cflags version options + target="ot-cli-ftd" + build_dir=$(jq -r '.subpath' <<<"$1") + cflags=$(jq -r '.cflags | join(" ")' <<<"$1") + version=$(jq -r '.version' <<<"$1") + options=$(jq -r '.options | join(" ")' <<<"$1") + # Intended splitting of options + read -ra options <<<"$options" + + ( + cd "$OT_DIR" + + OT_CMAKE_NINJA_TARGET="$target" \ + OT_CMAKE_BUILD_DIR="$build_dir" \ + CFLAGS="$cflags" \ + CXXFLAGS="$cflags" \ + script/cmake-build \ + simulation \ + "${options[@]}" \ + -DOT_THREAD_VERSION="$version" \ + -DOT_SIMULATION_MAX_NETWORK_SIZE="$MAX_NETWORK_SIZE" + ) +} + +build_otbr() +{ + # SC2155: Declare and assign separately to avoid masking return values + local target build_dir version rcp_options + target="ot-rcp" + build_dir=$(jq -r '.rcp_subpath' <<<"$1") + version=$(jq -r '.version' <<<"$1") + rcp_options=$(jq -r '.rcp_options | join(" ")' <<<"$1") + # Intended splitting of rcp_options + read -ra rcp_options <<<"$rcp_options" + + ( + cd "$OT_DIR" + + OT_CMAKE_NINJA_TARGET="$target" \ + OT_CMAKE_BUILD_DIR="$build_dir" \ + script/cmake-build \ + simulation \ + "${rcp_options[@]}" \ + -DOT_THREAD_VERSION="$version" \ + -DOT_SIMULATION_MAX_NETWORK_SIZE="$MAX_NETWORK_SIZE" + ) + + # SC2155: Declare and assign separately to avoid masking return values + local otbr_docker_image build_args options + otbr_docker_image=$(jq -r '.docker_image' <<<"$1") + build_args=$(jq -r '.build_args | map("--build-arg " + .) | join(" ")' <<<"$1") + # Intended splitting of build_args + read -ra build_args <<<"$build_args" + options=$(jq -r '.options | join(" ")' <<<"$1") + + local otbr_options=( + "$options" + "-DOT_THREAD_VERSION=$version" + "-DOT_SIMULATION_MAX_NETWORK_SIZE=$MAX_NETWORK_SIZE" + ) + + docker build . \ + -t "${otbr_docker_image}" \ + -f "${ETC_DIR}/Dockerfile" \ + "${build_args[@]}" \ + --build-arg OTBR_OPTIONS="${otbr_options[*]}" +} + +for item in $(jq -c '.ot_build.ot | .[]' "$CONFIG"); do + build_ot "$item" +done + +git clone https://github.com/openthread/ot-br-posix.git --recurse-submodules --shallow-submodules --depth=1 +( + cd ot-br-posix + # Use system V `service` command instead + mkdir -p root/etc/init.d + cp "${ETC_DIR}/commissionerd" root/etc/init.d/commissionerd + sudo chown root:root root/etc/init.d/commissionerd + sudo chmod +x root/etc/init.d/commissionerd + + cp "${ETC_DIR}/server.patch" script/server.patch + patch script/server script/server.patch + mkdir -p root/tmp + cp "${ETC_DIR}/requirements.txt" root/tmp/requirements.txt + + for item in $(jq -c '.ot_build.otbr | .[]' "$CONFIG"); do + build_otbr "$item" + done +) +rm -rf ot-br-posix diff --git a/tools/harness-simulation/posix/launch_testbed.py b/tools/harness-simulation/posix/launch_testbed.py old mode 100644 new mode 100755 index 0038135d3..c7363362c --- a/tools/harness-simulation/posix/launch_testbed.py +++ b/tools/harness-simulation/posix/launch_testbed.py @@ -41,12 +41,6 @@ import subprocess import sys from typing import Iterable -from config import ( - MAX_NODES_NUM, - MAX_SNIFFER_NUM, - SNIFFER_SERVER_PORT_BASE, - OTBR_DOCKER_NAME_PREFIX, -) from otbr_sim import otbr_docker GROUP = 'ff02::114' @@ -94,34 +88,37 @@ def _advertise(s: socket.socket, dst, info): s.sendto(json.dumps(info).encode('utf-8'), dst) -def advertise_devices(s: socket.socket, dst, ven: str, add: str, nodeids: Iterable[int], prefix: str = ''): +def advertise_devices(s: socket.socket, dst, ven: str, add: str, nodeids: Iterable[int], tag: str): for nodeid in nodeids: info = { 'ven': ven, 'mod': 'OpenThread', 'ver': '4', - 'add': f'{prefix}{nodeid}@{add}', + 'add': f'{tag}_{nodeid}@{add}', 'por': 22, } _advertise(s, dst, info) -def advertise_sniffers(s: socket.socket, dst, add: str, number: int): - for i in range(number): +def advertise_sniffers(s: socket.socket, dst, add: str, ports: Iterable[int]): + for port in ports: info = { 'add': add, - 'por': i + SNIFFER_SERVER_PORT_BASE, + 'por': port, } _advertise(s, dst, info) -def start_sniffer(addr: str, port: int) -> subprocess.Popen: +def start_sniffer(addr: str, port: int, ot_path: str) -> subprocess.Popen: if isinstance(ipaddress.ip_address(addr), ipaddress.IPv6Address): server = f'[{addr}]:{port}' else: server = f'{addr}:{port}' - cmd = ['python3', 'sniffer_sim/sniffer.py', '--grpc-server', server] + cmd = [ + 'python3', + os.path.join(ot_path, 'tools/harness-simulation/posix/sniffer_sim/sniffer.py'), '--grpc-server', server + ] logging.info('Executing command: %s', ' '.join(cmd)) return subprocess.Popen(cmd) @@ -131,79 +128,74 @@ def main(): # Parse arguments parser = argparse.ArgumentParser() - - # Determine the interface - parser.add_argument('-i', - '--interface', - dest='ifname', + parser.add_argument('-c', + '--config', + dest='config', type=str, required=True, - help='the interface used for discovery') - - # Determine the number of OpenThread FTD simulations to be "detected" and then started - parser.add_argument('--ot', - dest='ot_num', - type=int, - required=False, - default=0, - help=f'the number of OpenThread FTD simulations') - - # Determine the number of OpenThread BR simulations to be initiated and then detected - parser.add_argument('--otbr', - dest='otbr_num', - type=int, - required=False, - default=0, - help=f'the number of OpenThread BR simulations') - - # Determine the number of sniffer simulations to be started and then detected - parser.add_argument('--sniffer', - dest='sniffer_num', - type=int, - required=False, - default=0, - help=f'the number of sniffer simulations, no more than {MAX_SNIFFER_NUM}') - + help='the path of the configuration JSON file') args = parser.parse_args() + with open(args.config, 'rt') as f: + config = json.load(f) - # Check validation of arguments - if args.ot_num < 0: - raise ValueError(f'The number of FTDs should be non-negative') + ot_path = config['ot_path'] + ot_build = config['ot_build'] + max_nodes_num = ot_build['max_number'] + # No test case requires more than 2 sniffers + MAX_SNIFFER_NUM = 2 - if args.otbr_num < 0: - raise ValueError(f'The number of OTBRs should be non-negative') + ot_devices = [(item['tag'], item['number']) for item in ot_build['ot']] + otbr_devices = [(item['tag'], item['number']) for item in ot_build['otbr']] + ot_nodes_num = sum(x[1] for x in ot_devices) + otbr_nodes_num = sum(x[1] for x in otbr_devices) + nodes_num = ot_nodes_num + otbr_nodes_num + sniffer_num = config['sniffer']['number'] - if not 0 <= args.sniffer_num <= MAX_SNIFFER_NUM: - raise ValueError(f'The number of sniffers should be between 0 and {MAX_SNIFFER_NUM}') + # Check validation of numbers + if not all(0 <= x[1] <= max_nodes_num for x in ot_devices): + raise ValueError(f'The number of devices of each OT version should be between 0 and {max_nodes_num}') - if args.ot_num == args.otbr_num == 0: - raise ValueError('At least one device is required') + if not all(0 <= x[1] <= max_nodes_num for x in otbr_devices): + raise ValueError(f'The number of devices of each OTBR version should be between 0 and {max_nodes_num}') - if args.sniffer_num == 0: - raise ValueError('At least one sniffer is required') + if not 1 <= nodes_num <= max_nodes_num: + raise ValueError(f'The number of devices should be between 1 and {max_nodes_num}') - if args.ot_num + args.otbr_num > MAX_NODES_NUM: - raise ValueError(f'The number of all devices should be no more than {MAX_NODES_NUM}') + if not 1 <= sniffer_num <= MAX_SNIFFER_NUM: + raise ValueError(f'The number of sniffers should be between 1 and {MAX_SNIFFER_NUM}') # Get the local IP address on the specified interface - addr = get_ipaddr(args.ifname) + ifname = config['discovery_ifname'] + addr = get_ipaddr(ifname) # Start the sniffer + sniffer_server_port_base = config['sniffer']['server_port_base'] sniffer_procs = [] - for i in range(args.sniffer_num): - sniffer_procs.append(start_sniffer(addr, i + SNIFFER_SERVER_PORT_BASE)) + for i in range(sniffer_num): + sniffer_procs.append(start_sniffer(addr, i + sniffer_server_port_base, ot_path)) # OTBR firewall scripts create rules inside the Docker container # Run modprobe to load the kernel modules for iptables subprocess.run(['sudo', 'modprobe', 'ip6table_filter']) # Start the BRs otbr_dockers = [] - for nodeid in range(args.ot_num + 1, args.ot_num + args.otbr_num + 1): - otbr_dockers.append(otbr_docker.OtbrDocker(nodeid, OTBR_DOCKER_NAME_PREFIX + str(nodeid))) + nodeid = ot_nodes_num + for item in ot_build['otbr']: + tag = item['tag'] + ot_rcp_path = os.path.join(ot_path, item['rcp_subpath'], 'examples/apps/ncp/ot-rcp') + docker_image = item['docker_image'] + for _ in range(item['number']): + nodeid += 1 + otbr_dockers.append( + otbr_docker.OtbrDocker(nodeid=nodeid, + ot_path=ot_path, + ot_rcp_path=ot_rcp_path, + docker_image=docker_image, + docker_name=f'{tag}_{nodeid}')) - s = init_socket(args.ifname, GROUP, PORT) + s = init_socket(ifname, GROUP, PORT) - logging.info('Advertising on interface %s group %s ...', args.ifname, GROUP) + logging.info('Advertising on interface %s group %s ...', ifname, GROUP) # Terminate all sniffer simulation server processes and then exit def exit_handler(signum, context): @@ -227,17 +219,19 @@ def main(): if data == b'BBR': logging.info('Received OpenThread simulation query, advertising') - advertise_devices(s, src, ven='OpenThread_Sim', add=addr, nodeids=range(1, args.ot_num + 1)) - advertise_devices(s, - src, - ven='OpenThread_BR_Sim', - add=addr, - nodeids=range(args.ot_num + 1, args.ot_num + args.otbr_num + 1), - prefix=OTBR_DOCKER_NAME_PREFIX) + + nodeid = 1 + for ven, devices in [('OpenThread_Sim', ot_devices), ('OpenThread_BR_Sim', otbr_devices)]: + for tag, number in devices: + advertise_devices(s, src, ven=ven, add=addr, nodeids=range(nodeid, nodeid + number), tag=tag) + nodeid += number elif data == b'Sniffer': logging.info('Received sniffer simulation query, advertising') - advertise_sniffers(s, src, add=addr, number=args.sniffer_num) + advertise_sniffers(s, + src, + add=addr, + ports=range(sniffer_server_port_base, sniffer_server_port_base + sniffer_num)) else: logging.warning('Received %r, but ignored', data) diff --git a/tools/harness-simulation/posix/otbr_sim/otbr_docker.py b/tools/harness-simulation/posix/otbr_sim/otbr_docker.py index b116163f7..cc7f73b47 100644 --- a/tools/harness-simulation/posix/otbr_sim/otbr_docker.py +++ b/tools/harness-simulation/posix/otbr_sim/otbr_docker.py @@ -33,14 +33,15 @@ import re import subprocess import time -from config import (OT_PATH, OTBR_DOCKER_IMAGE) - class OtbrDocker: device_pattern = re.compile('(?<=PTY is )/dev/.+$') - def __init__(self, nodeid: int, docker_name: str): + def __init__(self, nodeid: int, ot_path: str, ot_rcp_path: str, docker_image: str, docker_name: str): self.nodeid = nodeid + self.ot_path = ot_path + self.ot_rcp_path = ot_rcp_path + self.docker_image = docker_image self.docker_name = docker_name self.logger = logging.getLogger('otbr_docker.OtbrDocker') @@ -96,9 +97,8 @@ class OtbrDocker: self._rcp_device = None def _launch_ot_rcp(self): - ot_rcp_path = os.path.join(OT_PATH, 'build/simulation/examples/apps/ncp/ot-rcp') self._ot_rcp_proc = subprocess.Popen( - f'{ot_rcp_path} {self.nodeid} > {self._rcp_device_pty} < {self._rcp_device_pty}', + f'{self.ot_rcp_path} {self.nodeid} > {self._rcp_device_pty} < {self._rcp_device_pty}', shell=True, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, @@ -136,8 +136,8 @@ class OtbrDocker: '-v', f'{self._rcp_device}:/dev/ttyUSB0', '-v', - f'{OT_PATH.rstrip("/")}:/home/pi/repo/openthread', - OTBR_DOCKER_IMAGE, + f'{self.ot_path.rstrip("/")}:/home/pi/repo/openthread', + self.docker_image, ] self.logger.info('Launching docker: %s', ' '.join(cmd)) launch_proc = subprocess.Popen(cmd, diff --git a/tools/harness-simulation/posix/simulation.conf b/tools/harness-simulation/posix/simulation.conf new file mode 100644 index 000000000..c506f92c9 --- /dev/null +++ b/tools/harness-simulation/posix/simulation.conf @@ -0,0 +1,120 @@ +{ + "ot_path": "/home/pi/work/src/openthread-pr", + "ot_build": { + "max_number": 64, + "ot": [ + { + "tag": "OT11", + "version": "1.1", + "number": 33, + "subpath": "build/ot11/simulation", + "cflags": [ + "-DOPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS=8" + ], + "options": [ + "-DOT_REFERENCE_DEVICE=ON", + "-DOT_COMMISSIONER=ON", + "-DOT_JOINER=ON" + ] + }, + { + "tag": "OT12", + "version": "1.2", + "number": 10, + "subpath": "build/ot12/simulation", + "cflags": [ + "-DOPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS=8" + ], + "options": [ + "-DOT_REFERENCE_DEVICE=ON", + "-DOT_DUA=ON", + "-DOT_MLR=ON", + "-DOT_COMMISSIONER=ON", + "-DOT_JOINER=ON", + "-DOT_CSL_RECEIVER=ON", + "-DOT_LINK_METRICS_SUBJECT=ON", + "-DOT_LINK_METRICS_INITIATOR=ON" + ] + }, + { + "tag": "OT13", + "version": "1.3", + "number": 10, + "subpath": "build/ot13/simulation", + "cflags": [ + "-DOPENTHREAD_CONFIG_IP6_MAX_EXT_MCAST_ADDRS=8" + ], + "options": [ + "-DOT_REFERENCE_DEVICE=ON", + "-DOT_DUA=ON", + "-DOT_MLR=ON", + "-DOT_COMMISSIONER=ON", + "-DOT_JOINER=ON", + "-DOT_CSL_RECEIVER=ON", + "-DOT_LINK_METRICS_SUBJECT=ON", + "-DOT_LINK_METRICS_INITIATOR=ON" + ] + } + ], + "otbr": [ + { + "tag": "OTBR12", + "version": "1.2", + "number": 4, + "docker_image": "otbr-reference-device-1.2", + "build_args": [ + "REFERENCE_DEVICE=1", + "BORDER_ROUTING=0", + "BACKBONE_ROUTER=1", + "NAT64=0", + "WEB_GUI=0", + "REST_API=0", + "OT_COMMISSIONER=1" + ], + "options": [ + "-DOTBR_DUA_ROUTING=ON", + "-DOT_DUA=ON", + "-DOT_MLR=ON" + ], + "rcp_subpath": "build/ot12/simulation", + "rcp_options": [ + "-DOT_LINK_METRICS_SUBJECT=ON" + ] + }, + { + "tag": "OTBR13", + "version": "1.3", + "number": 4, + "docker_image": "otbr-reference-device-1.3", + "build_args": [ + "REFERENCE_DEVICE=1", + "BORDER_ROUTING=1", + "BACKBONE_ROUTER=1", + "NAT64=0", + "WEB_GUI=0", + "REST_API=0", + "EXTERNAL_COMMISSIONER=1" + ], + "options": [ + "-DOTBR_DUA_ROUTING=ON", + "-DOT_DUA=ON", + "-DOT_MLR=ON" + ], + "rcp_subpath": "build/ot13/simulation", + "rcp_options": [ + "-DOT_LINK_METRICS_SUBJECT=ON" + ] + } + ] + }, + "ssh": { + "username": "pi", + "password": "raspberry", + "port": 22 + }, + "sniffer": { + "number": 2, + "server_port_base": 50051 + }, + "discovery_ifname": "eth0" +}