[thci] add support for OTBR simulations (#8015)

It can run all Thread 1.2 test cases with simulations now except those
requiring multiple versions support. It has been tested with the DUT
being a role of Router, Leader, Border Router, BR_1 and BR_2.
This commit is contained in:
Jiachen Dong
2022-08-22 21:32:45 -07:00
committed by GitHub
parent 142b8cf58c
commit 0b8e9745e5
21 changed files with 950 additions and 164 deletions
+30 -9
View File
@@ -4,21 +4,39 @@ THCI (Thread Host Controller Interface) is an implementation of the Python abstr
SI (Sniffer Interface) is an implementation of the sniffer abstract class template `ISniffer`, which is used by the Thread Test Harness Software to sniff all packets sent by devices.
Both OpenThread simulation and sniffer simulation are required to run on a POSIX environment. However, Harness has to be run on Windows, which is a non-POSIX environment. So both two systems are needed, and their setup procedures in detail are listed in the following sections. Either two machines or one machine running two (sub)systems (for example, VM, WSL) is feasible.
Both OpenThread simulation and sniffer simulation are required to run on a POSIX environment. However, Harness has to be run on Windows, which is a non-POSIX environment. So two systems are needed, and their setup procedures in detail are listed in the following sections. Either two machines or one machine running two (sub)systems (for example, VM, WSL) is feasible.
Platform developers should modify the THCI implementation and/or the SI implementation directly to match their platform (for example, the path of the OpenThread repository).
## POSIX Environment Setup
1. Build OpenThread to generate standalone OpenThread simulation `ot-cli-ftd`. For example, run the following command in the top directory of OpenThread.
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:
```bash
$ script/cmake-build simulation
$ 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
```
Then `ot-cli-ftd` is built in the directory `build/simulation/examples/apps/cli/`.
2. Run the installation script.
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/<username>/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.
```bash
$ tools/harness-simulation/posix/install.sh
@@ -30,6 +48,8 @@ Platform developers should modify the THCI implementation and/or the SI implemen
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.
3. Add the additional simulation device information in `harness\Web\data\deviceInputFields.xml` to `C:\GRL\Thread1.2\Web\data\deviceInputFields.xml`.
@@ -40,16 +60,17 @@ Platform developers should modify the THCI implementation and/or the SI implemen
```bash
$ cd tools/harness-simulation/posix
$ python launch_testbed.py \
--interface=eth0 \
--ot1.1=24 \
$ python3 launch_testbed.py \
--interface=eth0 \
--ot=6 \
--otbr=4 \
--sniffer=2
```
It starts 24 OT FTD simulations and 2 sniffer simulations and can be discovered on eth0.
This example starts 6 OT FTD simulations, 4 OTBR simulations, and 2 sniffer simulations and can be discovered on `eth0`.
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 `<node_id>@<ip_addr>`. Choose the proper device as the DUT accordingly.
2. Run Test Harness. The information field of the device is encoded as `<node_id>@<ip_addr>` for FTDs and `otbr_<node_id>@<ip_addr>` for BRs. Choose the proper device as the DUT accordingly.
3. Select one or more test cases to start the test.
@@ -34,20 +34,17 @@ import netifaces
import select
import socket
import struct
import subprocess
import time
import win32api
import winreg as wr
from ISniffer import ISniffer
from Sniffer.ISniffer import ISniffer
from THCI.OpenThread import watched
from simulation.config import EDITCAP_PATH
from simulation.Sniffer.proto import sniffer_pb2
from simulation.Sniffer.proto import sniffer_pb2_grpc
DISCOVERY_ADDR = ('ff02::114', 12345)
IFNAME = 'WLAN'
IFNAME = ISniffer.ethernet_interface_name
SCAN_TIME = 3
@@ -67,12 +64,13 @@ class SimSniffer(ISniffer):
self.addr_port = kwargs.get('addressofDevice')
self.is_active = False
self._local_pcapng_location = None
if self.addr_port is not None:
self._sniffer = grpc.insecure_channel(self.addr_port)
self._stub = sniffer_pb2_grpc.SnifferStub(self._sniffer)
# Close the sniffer only when Harness exits
win32api.SetConsoleCtrlHandler(self.__disconnect, True)
# Close the sniffer only when Harness exits
win32api.SetConsoleCtrlHandler(self.__disconnect, True)
def __repr__(self):
return '%r' % self.__dict__
@@ -146,9 +144,9 @@ class SimSniffer(ISniffer):
self.channel = channelToCapture
self._local_pcapng_location = captureFileLocation
response = self._stub.Start(sniffer_pb2.StartRequest(channel=self.channel))
response = self._stub.Start(sniffer_pb2.StartRequest(channel=self.channel, includeEthernet=includeEthernet))
if response.status != sniffer_pb2.OK:
raise RuntimeError(f'startSniffer error: {sniffer_pb2.Status.Name(response.status)}')
raise RuntimeError('startSniffer error: %s' % sniffer_pb2.Status.Name(response.status))
self.is_active = True
@@ -159,19 +157,11 @@ class SimSniffer(ISniffer):
response = self._stub.Stop(sniffer_pb2.StopRequest())
if response.status != sniffer_pb2.OK:
raise RuntimeError(f'stopSniffer error: {sniffer_pb2.Status.Name(response.status)}')
raise RuntimeError('stopSniffer error: %s' % sniffer_pb2.Status.Name(response.status))
# Truncate suffix from .pcapng to .pcap
local_pcap_location = self._local_pcapng_location[:-2]
with open(local_pcap_location, 'wb') as f:
with open(self._local_pcapng_location, 'wb') as f:
f.write(response.pcap_content)
cmd = [EDITCAP_PATH, '-F', 'pcapng', local_pcap_location, self._local_pcapng_location]
self.log('running editcap: %r', cmd)
subprocess.Popen(cmd).wait()
self.log('editcap done')
self.is_active = False
def __disconnect(self, dwCtrlType):
@@ -0,0 +1,139 @@
#!/usr/bin/env python
#
# 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.
#
"""
>> Thread Host Controller Interface
>> Device : OpenThread_BR_Sim THCI
>> Class : OpenThread_BR_Sim
"""
import ipaddress
import logging
import paramiko
import pipes
import sys
import time
from THCI.OpenThread import watched
from THCI.OpenThread_BR import OpenThread_BR
from simulation.config import (REMOTE_USERNAME, REMOTE_PASSWORD, REMOTE_PORT)
logging.getLogger('paramiko').setLevel(logging.WARNING)
class SSHHandle(object):
# Unit: second
KEEPALIVE_INTERVAL = 30
def __init__(self, ip, port, username, password, docker_name):
self.ip = ip
self.port = int(port)
self.username = username
self.password = password
self.docker_name = docker_name
self.__handle = None
self.__connect()
def __connect(self):
self.close()
self.__handle = paramiko.SSHClient()
self.__handle.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
self.__handle.connect(self.ip, port=self.port, username=self.username, password=self.password)
except paramiko.AuthenticationException:
if not self.password:
self.__handle.get_transport().auth_none(self.username)
else:
raise Exception('Password error')
# Avoid SSH disconnection after idle for a long time
self.__handle.get_transport().set_keepalive(self.KEEPALIVE_INTERVAL)
def close(self):
if self.__handle is not None:
self.__handle.close()
self.__handle = None
def bash(self, cmd, timeout):
# It is necessary to quote the command when there is stdin/stdout redirection
cmd = pipes.quote(cmd)
retry = 3
for i in range(retry):
try:
stdin, stdout, stderr = self.__handle.exec_command('docker exec %s bash -c %s' %
(self.docker_name, cmd),
timeout=timeout)
stdout._set_mode('rb')
sys.stderr.write(stderr.read())
output = [r.rstrip() for r in stdout.readlines()]
return output
except paramiko.SSHException:
if i < retry - 1:
print('SSH connection is lost, try reconnect after 1 second.')
time.sleep(1)
self.__connect()
else:
raise ConnectionError('SSH connection is lost')
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)
@watched
def _parseConnectionParams(self, params):
discovery_add = params.get('SerialPort')
if '@' not in discovery_add:
raise ValueError('%r in the field `add` is invalid' % discovery_add)
docker_name, ssh_ip = discovery_add.split('@')
# Let it crash if it is an invalid IP address
ipaddress.ip_address(ssh_ip)
self.connectType = 'ip'
self.telnetIp = self.port = discovery_add
self.telnetPort = REMOTE_PORT
self.telnetUsername = REMOTE_USERNAME
self.telnetPassword = REMOTE_PASSWORD
self.extraParams = {
'cmd-start-otbr-agent': 'service otbr-agent start',
'cmd-stop-otbr-agent': 'service otbr-agent stop',
'cmd-restart-otbr-agent': 'service otbr-agent restart',
'cmd-restart-radvd': 'service radvd stop; service radvd start',
}
@@ -33,36 +33,41 @@
"""
import ipaddress
import os
import paramiko
import socket
import time
import win32api
from IThci import IThci
from OpenThread import OpenThreadTHCI, watched
from simulation.config import REMOTE_OT_PATH
from THCI.IThci import IThci
from THCI.OpenThread import OpenThreadTHCI, watched
from simulation.config import (
REMOTE_USERNAME,
REMOTE_PASSWORD,
REMOTE_PORT,
REMOTE_OT_PATH,
)
class SSHHandle(object):
KEEPALIVE_INTERVAL = 30
def __init__(self, ip, port, username, password, device, node_id):
ipaddress.ip_address(ip)
self.ip = ip
self.port = int(port)
self.username = username
self.password = password
self.node_id = node_id
self.__handle = None
self.__stdin = None
self.__stdout = None
self.__connect(device, node_id)
self.__connect(device)
# Close the SSH connection only when Harness exits
win32api.SetConsoleCtrlHandler(self.__disconnect, True)
@watched
def __connect(self, device, node_id):
def __connect(self, device):
if self.__handle is not None:
return
@@ -80,7 +85,7 @@ class SSHHandle(object):
# Avoid SSH connection lost after inactivity for a while
self.__handle.get_transport().set_keepalive(self.KEEPALIVE_INTERVAL)
self.__stdin, self.__stdout, _ = self.__handle.exec_command(device + ' ' + str(node_id))
self.__stdin, self.__stdout, _ = self.__handle.exec_command(device + ' ' + str(self.node_id))
# Receive the output in non-blocking mode
self.__stdout.channel.setblocking(0)
@@ -120,7 +125,7 @@ class SSHHandle(object):
def log(self, fmt, *args):
try:
msg = fmt % args
print('%s - %s - %s' % (self.port, time.strftime('%b %d %H:%M:%S'), msg))
print('%d@%s - %s - %s' % (self.node_id, self.ip, time.strftime('%b %d %H:%M:%S'), msg))
except Exception:
pass
@@ -128,7 +133,8 @@ class SSHHandle(object):
class OpenThread_Sim(OpenThreadTHCI, IThci):
__handle = None
device = os.path.join(REMOTE_OT_PATH, 'build/simulation/examples/apps/cli/ot-cli-ftd')
# 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):
@@ -149,6 +155,22 @@ class OpenThread_Sim(OpenThreadTHCI, IThci):
def _disconnect(self):
pass
@watched
def _parseConnectionParams(self, params):
discovery_add = params.get('SerialPort')
if '@' not in discovery_add:
raise ValueError('%r in the field `add` is invalid' % discovery_add)
node_id, ssh_ip = discovery_add.split('@')
# Let it crash if it is an invalid IP address
ipaddress.ip_address(ssh_ip)
self.connectType = 'ip'
self.telnetIp = self.port = discovery_add
self.telnetPort = REMOTE_PORT
self.telnetUsername = REMOTE_USERNAME
self.telnetPassword = REMOTE_PASSWORD
def _cliReadLine(self):
if len(self.__lines) > 1:
return self.__lines.pop(0)
@@ -27,6 +27,8 @@
# POSSIBILITY OF SUCH DAMAGE.
#
REMOTE_OT_PATH = '/home/pi/openthread/'
REMOTE_USERNAME = 'pi'
REMOTE_PASSWORD = 'raspberry'
REMOTE_PORT = 22
EDITCAP_PATH = r'C:\Program Files (x86)\Wireshark_Thread\editcap.exe'
REMOTE_OT_PATH = '/home/pi/repo/openthread'
@@ -1,10 +1,8 @@
<DEVICE_FIELDS>
<DEVICE name="OpenThread_Sim" thumbnail="OpenThread.png" description = "OpenThread Simulation" THCI="OpenThread_Sim">
<ITEM label="Serial Line"
type="text"
forParam="SerialPort"
validation="COM"
hint="eg: COM1">COM1
</ITEM>
<ITEM type="text" forParam="SerialPort">UNSPECIFIED</ITEM>
</DEVICE>
<DEVICE name="OpenThread_BR_Sim" thumbnail="OpenThread_BR.png" description = "OpenThread BR Simulation" THCI="OpenThread_BR_Sim">
<ITEM type="text" forParam="SerialPort">UNSPECIFIED</ITEM>
</DEVICE>
</DEVICE_FIELDS>
@@ -27,6 +27,9 @@
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
%systemdrive%\GRL\Thread1.2\Python27\python.exe -m pip install --upgrade pip
+79
View File
@@ -0,0 +1,79 @@
#!/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
if [[ $OT_PATH == "" ]]; then
OT_PATH="/home/pi/repo/openthread"
fi
if [[ $OTBR_DOCKER_IMAGE == "" ]]; then
OTBR_DOCKER_IMAGE="otbr-reference-device-1.2"
fi
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
+36
View File
@@ -0,0 +1,36 @@
#!/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_'
@@ -0,0 +1,119 @@
# 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.
#
ARG BASE_IMAGE=ubuntu:focal
FROM ${BASE_IMAGE}
ARG INFRA_IF_NAME
ARG BORDER_ROUTING
ARG BACKBONE_ROUTER
ARG OTBR_OPTIONS
ARG EXTERNAL_COMMISSIONER
ARG DNS64
ARG NAT64
ARG NAT64_SERVICE
ARG REFERENCE_DEVICE
ARG REST_API
ARG WEB_GUI
ARG MDNS
ENV INFRA_IF_NAME=${INFRA_IF_NAME:-eth0}
ENV BORDER_ROUTING=${BORDER_ROUTING:-1}
ENV BACKBONE_ROUTER=${BACKBONE_ROUTER:-1}
ENV OTBR_MDNS=${MDNS:-mDNSResponder}
ENV OTBR_OPTIONS=${OTBR_OPTIONS}
ENV EXTERNAL_COMMISSIONER=${EXTERNAL_COMMISSIONER:-1}
ENV DEBIAN_FRONTEND noninteractive
ENV PLATFORM ubuntu
ENV REFERENCE_DEVICE=${REFERENCE_DEVICE:-0}
ENV NAT64=${NAT64:-1}
ENV NAT64_SERVICE=${NAT64_SERVICE:-tayga}
ENV DNS64=${DNS64:-0}
ENV WEB_GUI=${WEB_GUI:-1}
ENV REST_API=${REST_API:-1}
ENV DOCKER 1
RUN env
COPY . /app
WORKDIR /app
# Required during build or run
ENV OTBR_DOCKER_REQS sudo python2 python3 python-is-python2
# Required during build, could be removed
ENV OTBR_DOCKER_DEPS git ca-certificates python3-pip wget
# Required during run python scripts
ENV OTBR_PYTHON_REQS zeroconf
# Required and installed during build (script/bootstrap), could be removed
ENV OTBR_BUILD_DEPS apt-utils build-essential psmisc ninja-build cmake ca-certificates \
libreadline-dev libncurses-dev libcpputest-dev libdbus-1-dev libavahi-common-dev \
libavahi-client-dev libboost-dev libboost-filesystem-dev libboost-system-dev \
libnetfilter-queue-dev
RUN apt-get update \
&& cp -r ./root/. / \
&& rm -rf ./root \
&& apt-get install --no-install-recommends -y $OTBR_DOCKER_REQS $OTBR_DOCKER_DEPS \
&& wget -P /tmp "https://bootstrap.pypa.io/pip/2.7/get-pip.py" \
&& python2 /tmp/get-pip.py \
&& pip2 install -r /tmp/requirements.txt \
&& pip3 install $OTBR_PYTHON_REQS \
&& ln -fs /usr/share/zoneinfo/UTC /etc/localtime \
&& ([ "${EXTERNAL_COMMISSIONER}" != "1" ] || ( \
git clone https://github.com/openthread/ot-commissioner.git --recurse-submodules --shallow-submodules --depth=1 \
&& cd ot-commissioner \
&& ./script/bootstrap.sh \
&& mkdir -p build \
&& cd build \
&& cmake -GNinja -DCMAKE_INSTALL_PREFIX="/usr/local" -DOT_COMM_REFERENCE_DEVICE=ON .. \
&& ninja \
&& ninja install \
&& cd /app \
&& rm -rf ot-commissioner \
)) \
&& ./script/bootstrap \
&& ./script/setup \
&& ([ "${DNS64}" = "0" ] || chmod 644 /etc/bind/named.conf.options) \
&& mv ./script /tmp \
&& mv ./etc /tmp \
&& find . -delete \
&& rm -rf /usr/include \
&& mv /tmp/script . \
&& mv /tmp/etc . \
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $OTBR_DOCKER_DEPS \
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $OTBR_BUILD_DEPS \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* \
&& sed -i "s/\/root/\/home\/pi/g" /etc/passwd
# The command above changes root home directory to /home/pi
ENTRYPOINT ["/app/etc/docker/docker_entrypoint.sh"]
EXPOSE 80
+100
View File
@@ -0,0 +1,100 @@
#!/bin/sh
#
# 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.
#
### BEGIN INIT INFO
# Provides: commissionerd
# Required-Start:
# Required-Stop:
# Should-Start:
# Should-Stop:
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: OT-commissioner daemon
# Description: OT-commissioner daemon
### END INIT INFO
set -e
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DESC="OT-commissioner daemon"
NAME=commissionerd
DAEMON=/usr/bin/python2
PIDFILE=/var/run/commissionerd.pid
# shellcheck source=/dev/null
. /lib/lsb/init-functions
# shellcheck source=/dev/null
. /lib/init/vars.sh
start_commissionerd()
{
if [ -e $PIDFILE ]; then
if $0 status >/dev/null; then
log_success_msg "$DESC already started; not starting."
return
else
log_success_msg "Removing stale PID file $PIDFILE."
rm -f $PIDFILE
fi
fi
log_daemon_msg "Starting $DESC" "$NAME"
start-stop-daemon --start --quiet \
--pidfile $PIDFILE --make-pidfile \
-b --exec $DAEMON -- \
-u /usr/local/bin/commissionerd.py -c /usr/local/bin/commissioner-cli
log_end_msg $?
}
stop_commissionerd()
{
log_daemon_msg "Stopping $DESC" "$NAME"
start-stop-daemon --stop --retry 5 --quiet --oknodo \
--pidfile $PIDFILE --remove-pidfile
log_end_msg $?
}
case "$1" in
start)
start_commissionerd
;;
restart | reload | force-reload)
stop_commissionerd
start_commissionerd
;;
stop | force-stop)
stop_commissionerd
;;
status)
status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $?
;;
*)
log_action_msg "Usage: /etc/init.d/$NAME {start | stop | status | restart | reload | force-reload}"
exit 2
;;
esac
@@ -0,0 +1,3 @@
pexpect==4.7.0
ptyprocess==0.6.0
pyserial==3.4
@@ -0,0 +1,18 @@
--- script/server 2022-08-15 11:41:53.915673348 +0800
+++ script/server2 2022-08-15 11:43:12.387100651 +0800
@@ -50,6 +50,7 @@
systemctl is-active avahi-daemon || sudo systemctl start avahi-daemon || die 'Failed to start avahi!'
without WEB_GUI || systemctl is-active otbr-web || sudo systemctl start otbr-web || die 'Failed to start otbr-web!'
systemctl is-active otbr-agent || sudo systemctl start otbr-agent || die 'Failed to start otbr-agent!'
+ systemctl is-active commissionerd || sudo systemctl start commissionerd || die 'Failed to start commissionerd!'
elif have service; then
sudo service rsyslog status || sudo service rsyslog start || die 'Failed to start rsyslog!'
sudo service dbus status || sudo service dbus start || die 'Failed to start dbus!'
@@ -58,6 +59,7 @@
sudo service avahi-daemon status || sudo service avahi-daemon start || die 'Failed to start avahi!'
sudo service otbr-agent status || sudo service otbr-agent start || die 'Failed to start otbr-agent!'
without WEB_GUI || sudo service otbr-web status || sudo service otbr-web start || die 'Failed to start otbr-web!'
+ sudo service commissionerd status || sudo service commissionerd start || die 'Failed to start commissionerd!'
else
die 'Unable to find service manager. Try script/console to start in console mode!'
fi
@@ -39,12 +39,18 @@ import socket
import struct
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'
PORT = 12345
MAX_OT11_NUM = 33
MAX_SNIFFER_NUM = 4
SNIFFER_SERVER_PORT_BASE = 50051
def if_nametoindex(ifname: str) -> int:
@@ -88,20 +94,19 @@ def _advertise(s: socket.socket, dst, info):
s.sendto(json.dumps(info).encode('utf-8'), dst)
def advertise_ftd(s: socket.socket, dst, ven: str, ver: str, add: str, por: int, number: int):
# Node ID of ot-cli-ftd is 1-indexed
for i in range(1, number + 1):
def advertise_devices(s: socket.socket, dst, ven: str, add: str, nodeids: Iterable[int], prefix: str = ''):
for nodeid in nodeids:
info = {
'ven': ven,
'mod': f'{ven}_{i}',
'ver': ver,
'add': f'{i}@{add}',
'por': por,
'mod': 'OpenThread',
'ver': '4',
'add': f'{prefix}{nodeid}@{add}',
'por': 22,
}
_advertise(s, dst, info)
def advertise_sniffer(s: socket.socket, dst, add: str, number: int):
def advertise_sniffers(s: socket.socket, dst, add: str, number: int):
for i in range(number):
info = {
'add': add,
@@ -135,17 +140,24 @@ def main():
required=True,
help='the interface used for discovery')
# Determine the number of OpenThread 1.1 FTD simulations to be "detected" and then started
parser.add_argument('--ot1.1',
dest='ot11_num',
# 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, no more than {MAX_OT11_NUM}')
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('-s',
'--sniffer',
parser.add_argument('--sniffer',
dest='sniffer_num',
type=int,
required=False,
@@ -155,15 +167,24 @@ def main():
args = parser.parse_args()
# Check validation of arguments
if not 0 <= args.ot11_num <= MAX_OT11_NUM:
raise ValueError(f'The number of FTDs should be between 0 and {MAX_OT11_NUM}')
if args.ot_num < 0:
raise ValueError(f'The number of FTDs should be non-negative')
if args.otbr_num < 0:
raise ValueError(f'The number of OTBRs should be non-negative')
if not 0 <= args.sniffer_num <= MAX_SNIFFER_NUM:
raise ValueError(f'The number of FTDs should be between 0 and {MAX_SNIFFER_NUM}')
raise ValueError(f'The number of sniffers should be between 0 and {MAX_SNIFFER_NUM}')
if args.ot11_num == args.sniffer_num == 0:
if args.ot_num == args.otbr_num == 0:
raise ValueError('At least one device is required')
if args.sniffer_num == 0:
raise ValueError('At least one sniffer is required')
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}')
# Get the local IP address on the specified interface
addr = get_ipaddr(args.ifname)
@@ -172,6 +193,11 @@ def main():
for i in range(args.sniffer_num):
sniffer_procs.append(start_sniffer(addr, i + SNIFFER_SERVER_PORT_BASE))
# 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)))
s = init_socket(args.ifname, GROUP, PORT)
logging.info('Advertising on interface %s group %s ...', args.ifname, GROUP)
@@ -183,6 +209,10 @@ def main():
for sniffer_proc in sniffer_procs:
sniffer_proc.terminate()
ret = max(ret, sniffer_proc.wait())
for otbr in otbr_dockers:
otbr.close()
sys.exit(ret)
signal.signal(signal.SIGINT, exit_handler)
@@ -194,11 +224,17 @@ def main():
if data == b'BBR':
logging.info('Received OpenThread simulation query, advertising')
advertise_ftd(s, src, ven='OpenThread_Sim', ver='4', add=addr, por=22, number=args.ot11_num)
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)
elif data == b'Sniffer':
logging.info('Received sniffer simulation query, advertising')
advertise_sniffer(s, src, add=addr, number=args.sniffer_num)
advertise_sniffers(s, src, add=addr, number=args.sniffer_num)
else:
logging.warning('Received %r, but ignored', data)
@@ -0,0 +1,170 @@
#!/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.
#
import logging
import os
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):
self.nodeid = nodeid
self.docker_name = docker_name
self.logger = logging.getLogger('otbr_docker.OtbrDocker')
self.logger.setLevel(logging.INFO)
self._socat_proc = None
self._ot_rcp_proc = None
self._rcp_device_pty = None
self._rcp_device = None
self._launch()
def __repr__(self) -> str:
return f'OTBR<{self.nodeid}>'
def _launch(self):
self.logger.info('Launching %r ...', self)
self._launch_socat()
self._launch_ot_rcp()
self._launch_docker()
self.logger.info('Launched %r successfully', self)
def close(self):
self.logger.info('Shutting down %r ...', self)
self._shutdown_docker()
self._shutdown_ot_rcp()
self._shutdown_socat()
self.logger.info('Shut down %r successfully', self)
def _launch_socat(self):
self._socat_proc = subprocess.Popen(['socat', '-d', '-d', 'pty,raw,echo=0', 'pty,raw,echo=0'],
stderr=subprocess.PIPE,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL)
line = self._socat_proc.stderr.readline().decode('ascii').strip()
self._rcp_device_pty = self.device_pattern.findall(line)[0]
line = self._socat_proc.stderr.readline().decode('ascii').strip()
self._rcp_device = self.device_pattern.findall(line)[0]
self.logger.info(f"socat running: device PTY: {self._rcp_device_pty}, device: {self._rcp_device}")
def _shutdown_socat(self):
if self._socat_proc is None:
return
self._socat_proc.stderr.close()
self._socat_proc.terminate()
self._socat_proc.wait()
self._socat_proc = None
self._rcp_device_pty = None
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}',
shell=True,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
try:
self._ot_rcp_proc.wait(1)
except subprocess.TimeoutExpired:
# We expect ot-rcp not to quit in 1 second.
pass
else:
raise Exception(f"ot-rcp {self.nodeid} exited unexpectedly!")
def _shutdown_ot_rcp(self):
if self._ot_rcp_proc is None:
return
self._ot_rcp_proc.terminate()
self._ot_rcp_proc.wait()
self._ot_rcp_proc = None
def _launch_docker(self):
local_cmd_path = f'/tmp/{self.docker_name}'
os.makedirs(local_cmd_path, exist_ok=True)
cmd = [
'docker',
'run',
'--rm',
'--name',
self.docker_name,
'-d',
'--sysctl',
'net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1',
'--privileged',
'-v',
f'{self._rcp_device}:/dev/ttyUSB0',
'-v',
f'{OT_PATH.rstrip("/")}:/home/pi/repo/openthread',
OTBR_DOCKER_IMAGE,
]
self.logger.info('Launching docker: %s', ' '.join(cmd))
launch_proc = subprocess.Popen(cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
launch_docker_deadline = time.time() + 60
launch_ok = False
time.sleep(5)
while time.time() < launch_docker_deadline:
try:
subprocess.check_call(['docker', 'exec', self.docker_name, 'ot-ctl', 'state'],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
launch_ok = True
logging.info("OTBR Docker %s is ready!", self.docker_name)
break
except subprocess.CalledProcessError:
time.sleep(5)
continue
if not launch_ok:
raise RuntimeError('Cannot start OTBR Docker %s!' % self.docker_name)
launch_proc.wait()
def _shutdown_docker(self):
subprocess.run(['docker', 'stop', self.docker_name])
@@ -42,11 +42,17 @@ PCAP_VERSION_MINOR = 4
class PcapCodec(object):
""" Utility class for .pcap formatters. """
def __init__(self, channel):
def __init__(self, channel, filename):
self._dlt = DLT_IEEE802_15_4_WITHFCS
self._pcap_contents = [self._encode_header()]
self._channel = channel
self._pcap_writer = open(filename, 'wb')
self._write(self._encode_header())
def _write(self, content):
self._pcap_writer.write(content)
self._pcap_writer.flush()
def _encode_header(self):
""" Return a pcap file header. """
@@ -88,7 +94,7 @@ class PcapCodec(object):
return
timestamp = self._get_timestamp()
self._pcap_contents.append(self._encode_frame(frame, *timestamp))
self._write(self._encode_frame(frame, *timestamp))
def pop_all(self) -> bytes:
return b''.join(self._pcap_contents)
def close(self):
self._pcap_writer.close()
@@ -32,6 +32,9 @@ enum Status {
message StartRequest {
// Specify the channel that the sniffer is going to sniff
int32 channel = 1;
// Specify whether to include Ethernet packets between OTBR and infra
bool includeEthernet = 2;
}
message StartResponse {
@@ -32,71 +32,76 @@ from concurrent import futures
import enum
import grpc
import logging
import os
import signal
import pcap_codec
import socket
import subprocess
import tempfile
import threading
import pcap_codec
from proto import sniffer_pb2
from proto import sniffer_pb2_grpc
import sniffer_transport
class CaptureState(enum.Flag):
NONE = 0
THREAD = enum.auto()
ETHERNET = enum.auto()
class SnifferServicer(sniffer_pb2_grpc.Sniffer):
""" Class representing the Sniffing node, whose main task is listening. """
logger = logging.getLogger('sniffer.SnifferServicer')
RECV_BUFFER_SIZE = 4096
MAX_NODES_NUM = 33
class State(enum.Enum):
STOPPED = 0
RUNNING = 1
TIMEOUT = 0.1
MAX_NODES_NUM = 64
def _reset(self):
self._state = SnifferServicer.State.STOPPED
self._state = CaptureState.NONE
self._pcap = None
self._allowed_nodeids = None
self._transport = None
self._thread = None
self._thread_alive.clear()
self._pcapng_filename = None
self._tshark_proc = None
def __init__(self):
self._thread_alive = threading.Event()
self._mutex = threading.Lock() # for self._allowed_nodeids
self._reset()
def _sniffer_main_loop(self):
""" Sniffer main loop. """
self.logger.debug('Sniffer started.')
while self._thread_alive.is_set():
# Avoid being blocked endlessly when there is no data
if not self._transport.ready(0.1):
continue
data, nodeid = self._transport.recv(self.RECV_BUFFER_SIZE)
with self._mutex:
allowed_nodeids = self._allowed_nodeids
# Equivalent to RF enclosure
if allowed_nodeids is None or nodeid in allowed_nodeids:
self._pcap.append(data)
self.logger.debug('Sniffer stopped.')
def Start(self, request, context):
""" Start sniffing. """
self.logger.debug('call Start')
# Validate and change the state
if self._state != SnifferServicer.State.STOPPED:
if self._state != CaptureState.NONE:
return sniffer_pb2.StartResponse(status=sniffer_pb2.OPERATION_ERROR)
self._state = SnifferServicer.State.RUNNING
self._state = CaptureState.THREAD
self._pcap = pcap_codec.PcapCodec(request.channel)
# Create a temporary named pipe
tempdir = tempfile.mkdtemp()
fifo_name = os.path.join(tempdir, 'pcap.fifo')
os.mkfifo(fifo_name)
cmd = ['tshark', '-i', fifo_name]
if request.includeEthernet:
self._state |= CaptureState.ETHERNET
cmd += ['-i', 'docker0']
self._pcapng_filename = os.path.join(tempdir, 'sim.pcapng')
cmd += ['-w', self._pcapng_filename, '-q', 'not ip and not tcp and not arp and not ether proto 0x8899']
self.logger.debug('Running command: %s', ' '.join(cmd))
self._tshark_proc = subprocess.Popen(cmd)
# Construct pcap codec after initiating tshark to avoid blocking
self._pcap = pcap_codec.PcapCodec(request.channel, fifo_name)
# Sniffer all nodes in default, i.e. there is no RF enclosure
# In this case, self._allowed_nodeids is set to None
@@ -115,13 +120,29 @@ class SnifferServicer(sniffer_pb2_grpc.Sniffer):
return sniffer_pb2.StartResponse(status=sniffer_pb2.OK)
def _sniffer_main_loop(self):
""" Sniffer main loop. """
while self._thread_alive.is_set():
try:
data, nodeid = self._transport.recv(self.RECV_BUFFER_SIZE, self.TIMEOUT)
except socket.timeout:
continue
with self._mutex:
allowed_nodeids = self._allowed_nodeids
# Equivalent to RF enclosure
if allowed_nodeids is None or nodeid in allowed_nodeids:
self._pcap.append(data)
def FilterNodes(self, request, context):
""" Only sniffer the specified nodes. """
self.logger.debug('call FilterNodes')
# Validate the state
if self._state != SnifferServicer.State.RUNNING:
if not (self._state & CaptureState.THREAD):
return sniffer_pb2.FilterNodesResponse(status=sniffer_pb2.OPERATION_ERROR)
allowed_nodeids = set(request.nodeids)
@@ -141,15 +162,21 @@ class SnifferServicer(sniffer_pb2_grpc.Sniffer):
self.logger.debug('call Stop')
# Validate and change the state
if self._state != SnifferServicer.State.RUNNING:
if self._state == CaptureState.NONE:
return sniffer_pb2.StopResponse(status=sniffer_pb2.OPERATION_ERROR, pcap_content=b'')
self._state = SnifferServicer.State.STOPPED
self._state = CaptureState.NONE
self._thread_alive.clear()
self._thread.join(timeout=1)
self._transport.close()
self._pcap.close()
self._tshark_proc.terminate()
self._tshark_proc.wait()
with open(self._pcapng_filename, 'rb') as f:
pcap_content = f.read()
pcap_content = self._pcap.pop_all()
self._reset()
return sniffer_pb2.StopResponse(status=sniffer_pb2.OK, pcap_content=pcap_content)
@@ -28,7 +28,6 @@
#
import os
import select
import socket
@@ -72,17 +71,21 @@ class SnifferTransport(object):
"""
raise NotImplementedError
def recv(self, bufsize):
def recv(self, bufsize, timeout):
""" Receive data sent by other node.
Args:
bufsize (int): size of buffer for incoming data.
timeout (float | None): socket timeout.
Returns:
A tuple contains data and node id.
For example:
(bytearray([0x00, 0x01...], 1)
Raises:
socket.timeout: when receiving the packets times out.
"""
raise NotImplementedError
@@ -140,16 +143,14 @@ class SnifferSocketTransport(SnifferTransport):
return self._socket.sendto(data, address)
def recv(self, bufsize):
def recv(self, bufsize, timeout):
self._socket.settimeout(timeout)
data, address = self._socket.recvfrom(bufsize)
nodeid = self._port_to_nodeid(address[1])
return bytearray(data), nodeid
def ready(self, timeout):
return select.select([self._socket], [], [], timeout)[0]
class SnifferTransportFactory(object):
+53 -44
View File
@@ -254,13 +254,29 @@ class OpenThreadTHCI(object):
line str: data send to device
"""
@abstractmethod
# Override the following empty methods in the dervied classes when needed
def _onCommissionStart(self):
"""Called when commissioning starts."""
"""Called when commissioning starts"""
@abstractmethod
def _onCommissionStop(self):
"""Called when commissioning stops."""
"""Called when commissioning stops"""
def _deviceBeforeReset(self):
"""Called before the device resets"""
def _deviceAfterReset(self):
"""Called after the device resets"""
def _restartAgentService(self):
"""Restart the agent service"""
def _beforeRegisterMulticast(self, sAddr, timeout):
"""Called before the ipv6 address being subscribed in interface
Args:
sAddr : str : Multicast address to be subscribed and notified OTA
timeout : int : The allowed maximal time to end normally
"""
def __sendCommand(self, cmd, expectEcho=True):
cmd = self._cmdPrefix + cmd
@@ -384,31 +400,13 @@ class OpenThreadTHCI(object):
@API
def intialize(self, params):
"""initialize the serial port with baudrate, timeout parameters"""
self.port = params.get('SerialPort', '')
# params example: {'EUI': 1616240311388864514L, 'SerialBaudRate': None, 'TelnetIP': '192.168.8.181', 'SerialPort': None, 'Param7': None, 'Param6': None, 'Param5': 'ip', 'TelnetPort': '22', 'Param9': None, 'Param8': None}
try:
ipaddress.ip_address(self.port)
# handle TestHarness Discovery Protocol
self.connectType = 'ip'
self.telnetIp = self.port
self.telnetPort = 22
self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
except ValueError:
self.connectType = (params.get('Param5') or 'usb').lower()
self.telnetIp = params.get('TelnetIP')
self.telnetPort = int(params.get('TelnetPort')) if params.get('TelnetPort') else 22
# username for SSH
self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
# password for SSH
self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
self.mac = params.get('EUI')
self.backboneNetif = params.get('Param8') or 'eth0'
self.extraParams = self.__parseExtraParams(params.get('Param9'))
# Potentially changes `self.extraParams`
self._parseConnectionParams(params)
self.UIStatusMsg = ''
self.AutoDUTEnable = False
self.isPowerDown = False
@@ -449,6 +447,33 @@ class OpenThreadTHCI(object):
else:
return '[%s]' % self.port
def _parseConnectionParams(self, params):
"""Parse parameters related to connection to the device
Args:
params: Arbitrary keyword arguments including 'EUI' and 'SerialPort'
"""
self.port = params.get('SerialPort', '')
# params example: {'EUI': 1616240311388864514L, 'SerialBaudRate': None, 'TelnetIP': '192.168.8.181', 'SerialPort': None, 'Param7': None, 'Param6': None, 'Param5': 'ip', 'TelnetPort': '22', 'Param9': None, 'Param8': None}
self.log('All parameters: %r', params)
try:
ipaddress.ip_address(self.port)
# handle TestHarness Discovery Protocol
self.connectType = 'ip'
self.telnetIp = self.port
self.telnetPort = 22
self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
except ValueError:
self.connectType = (params.get('Param5') or 'usb').lower()
self.telnetIp = params.get('TelnetIP')
self.telnetPort = int(params.get('TelnetPort')) if params.get('TelnetPort') else 22
# username for SSH
self.telnetUsername = 'pi' if params.get('Param6') is None else params.get('Param6')
# password for SSH
self.telnetPassword = 'raspberry' if params.get('Param7') is None else params.get('Param7')
@watched
def __parseExtraParams(self, Param9):
"""
@@ -460,12 +485,14 @@ class OpenThreadTHCI(object):
- "cmd-start-otbr-agent" : The command to start otbr-agent (default: systemctl start otbr-agent)
- "cmd-stop-otbr-agent" : The command to stop otbr-agent (default: systemctl stop otbr-agent)
- "cmd-restart-otbr-agent" : The command to restart otbr-agent (default: systemctl restart otbr-agent)
- "cmd-restart-radvd" : The command to restart radvd (default: service radvd restart)
For example, Param9 can be generated as below:
Param9 = base64.urlsafe_b64encode(json.dumps({
"cmd-start-otbr-agent": "service otbr-agent start",
"cmd-stop-otbr-agent": "service otbr-agent stop",
"cmd-restart-otbr-agent": "service otbr-agent restart",
"cmd-restart-radvd": "service radvd stop; service radvd start",
}))
:param Param9: A JSON string encoded in URL-safe base64 encoding.
@@ -1354,7 +1381,7 @@ class OpenThreadTHCI(object):
self.__executeCommand('state', timeout=0.1)
break
except Exception:
self.__restartAgentService()
self._restartAgentService()
time.sleep(2)
self.__sendCommand('factoryreset', expectEcho=False)
time.sleep(0.5)
@@ -3278,18 +3305,6 @@ class OpenThread(OpenThreadTHCI, IThci):
self.__handle.close()
self.__handle = None
def _deviceBeforeReset(self):
pass
def _deviceAfterReset(self):
pass
def __restartAgentService(self):
pass
def _beforeRegisterMulticast(self, sAddr, timeout):
pass
def __socRead(self, size=512):
if self._is_net:
return self.__handle.recv(size)
@@ -3327,9 +3342,3 @@ class OpenThread(OpenThreadTHCI, IThci):
self.__socWrite(line + '\r')
else:
self.__socWrite(line + '\r\n')
def _onCommissionStart(self):
pass
def _onCommissionStop(self):
pass
+10 -6
View File
@@ -298,17 +298,21 @@ class OpenThread_BR(OpenThreadTHCI, IThci):
IsBorderRouter = True
__is_root = False
def _getHandle(self):
if self.connectType == 'ip':
return SSHHandle(self.telnetIp, self.telnetPort, self.telnetUsername, self.telnetPassword)
else:
return SerialHandle(self.port, 115200)
def _connect(self):
self.log("logging in to Raspberry Pi ...")
self.__cli_output_lines = []
self.__syslog_skip_lines = None
self.__syslog_last_read_ts = 0
self.__handle = self._getHandle()
if self.connectType == 'ip':
self.__handle = SSHHandle(self.telnetIp, self.telnetPort, self.telnetUsername, self.telnetPassword)
self.__is_root = self.telnetUsername == 'root'
else:
self.__handle = SerialHandle(self.port, 115200)
def _disconnect(self):
if self.__handle:
@@ -331,7 +335,7 @@ class OpenThread_BR(OpenThreadTHCI, IThci):
self.__truncateSyslog()
self.__enableAcceptRa()
if not self.IsHost:
self.__restartAgentService()
self._restartAgentService()
time.sleep(2)
def __enableAcceptRa(self):
@@ -587,7 +591,7 @@ class OpenThread_BR(OpenThreadTHCI, IThci):
cmd = 'sh -c "cat >/etc/radvd.conf <<%s"' % conf
self.bash(cmd)
self.bash('service radvd restart')
self.bash(self.extraParams.get('cmd-restart-radvd', 'service radvd restart'))
self.bash('service radvd status')
@watched
@@ -624,7 +628,7 @@ class OpenThread_BR(OpenThreadTHCI, IThci):
for line in output:
self.__cli_output_lines.append(line)
def __restartAgentService(self):
def _restartAgentService(self):
restart_cmd = self.extraParams.get('cmd-restart-otbr-agent', 'systemctl restart otbr-agent')
self.bash(restart_cmd)