mirror of
https://github.com/espressif/openthread.git
synced 2026-08-14 22:57:46 +00:00
[thread-cert] Thread 1.2 CI with OTBR and Backbone link (#5489)
Thread 1.2 CI with OTBR and Backbone link:
- Run OTBR in Dockers with Backbone link
- Enhance node.py to work with OTBR Docker
- Add Packet Verification for 5.11.1
- incomplete, some steps can not pass yet, just to make sure
Backbone traffic verification works
- Support running multiple tests simultaneously (default: each test
run 3 instances)
- Build OTBR Docker using OpenThread PR code
- Upload code coverage in Docker
Some implementation details:
- Most existing code of node.py is shared by OTBR Docker
- Backbone related test scripts are found in
tests/scripts/thread-cert/backbone
- A new script tests/scripts/thread-cert/run_bbr_tests.py is added to
manage multiple running tests
- Test configuration differs according to PORT_OFFSET
- Backbone interface name: Backbone{PORT_OFFSET}
- Backbone network prefix: 91{PORT_OFFSET:02x}::/64
- Docker instance name: otbr_{PORT_OFFSET}_{nodeid}
- Output Files:
- Pcap:
- Thread: {test_name}_{PORT_OFFSET}.pcap
- Backbone: {test_name}_{PORT_OFFSET}_backbone.pcap
- Merged: {test_name}_{PORT_OFFSET}_merged.pcap
- Log: {test_name}_{PORT_OFFSET}.log
This commit is contained in:
@@ -28,20 +28,25 @@
|
||||
#
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import unittest
|
||||
from typing import Optional
|
||||
|
||||
import config
|
||||
import debug
|
||||
from node import Node
|
||||
from node import Node, OtbrNode, HostNode
|
||||
from pktverify import utils as pvutils
|
||||
|
||||
PACKET_VERIFICATION = int(os.getenv('PACKET_VERIFICATION', 0))
|
||||
|
||||
if PACKET_VERIFICATION:
|
||||
from pktverify.addrs import ExtAddr
|
||||
from pktverify.addrs import ExtAddr, EthAddr
|
||||
from pktverify.packet_verifier import PacketVerifier
|
||||
|
||||
PORT_OFFSET = int(os.getenv('PORT_OFFSET', "0"))
|
||||
@@ -51,6 +56,8 @@ ENV_THREAD_VERSION = os.getenv('THREAD_VERSION', '1.1')
|
||||
DEFAULT_PARAMS = {
|
||||
'is_mtd': False,
|
||||
'is_bbr': False,
|
||||
'is_otbr': False,
|
||||
'is_host': False,
|
||||
'mode': 'rsdn',
|
||||
'panid': 0xface,
|
||||
'allowlist': None,
|
||||
@@ -82,39 +89,73 @@ class TestCase(NcpSupportMixin, unittest.TestCase):
|
||||
The `topology` member of sub-class is used to create test topology.
|
||||
"""
|
||||
|
||||
USE_MESSAGE_FACTORY = True
|
||||
TOPOLOGY = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
self._start_time = None
|
||||
self._do_packet_verification = PACKET_VERIFICATION and hasattr(self, 'verify')
|
||||
|
||||
def setUp(self):
|
||||
try:
|
||||
self._setUp()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
for node in list(self.nodes.values()):
|
||||
try:
|
||||
node.destroy()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
raise
|
||||
|
||||
def _setUp(self):
|
||||
"""Create simulator, nodes and apply configurations.
|
||||
"""
|
||||
self._clean_up_tmp()
|
||||
|
||||
self.simulator = config.create_default_simulator()
|
||||
self.simulator = config.create_default_simulator(use_message_factory=self.USE_MESSAGE_FACTORY)
|
||||
self.nodes = {}
|
||||
|
||||
os.environ['LD_LIBRARY_PATH'] = '/tmp/thread-wireshark'
|
||||
|
||||
if self._has_backbone_traffic():
|
||||
self._prepare_backbone_network()
|
||||
self._start_backbone_sniffer()
|
||||
|
||||
self._initial_topology = initial_topology = {}
|
||||
|
||||
for i, params in self.TOPOLOGY.items():
|
||||
if params:
|
||||
params = dict(DEFAULT_PARAMS, **params)
|
||||
else:
|
||||
params = DEFAULT_PARAMS.copy()
|
||||
|
||||
params = self._parse_params(params)
|
||||
initial_topology[i] = params
|
||||
|
||||
self.nodes[i] = Node(
|
||||
logging.info("Creating node %d: %r", i, params)
|
||||
|
||||
if params['is_otbr']:
|
||||
nodeclass = OtbrNode
|
||||
elif params['is_host']:
|
||||
nodeclass = HostNode
|
||||
else:
|
||||
nodeclass = Node
|
||||
|
||||
node = nodeclass(
|
||||
i,
|
||||
params['is_mtd'],
|
||||
is_mtd=params['is_mtd'],
|
||||
simulator=self.simulator,
|
||||
name=params.get('name'),
|
||||
version=params['version'],
|
||||
is_bbr=params['is_bbr'],
|
||||
)
|
||||
|
||||
self.nodes[i] = node
|
||||
|
||||
if node.is_host:
|
||||
continue
|
||||
|
||||
self.nodes[i].set_panid(params['panid'])
|
||||
self.nodes[i].set_mode(params['mode'])
|
||||
|
||||
@@ -188,22 +229,31 @@ class TestCase(NcpSupportMixin, unittest.TestCase):
|
||||
"""
|
||||
if self._do_packet_verification and os.uname().sysname != "Linux":
|
||||
raise NotImplementedError(
|
||||
f'{self.testcase_name}: Packet Verification not available on {os.uname().sysname} (Linux only).')
|
||||
f'{self.test_name}: Packet Verification not available on {os.uname().sysname} (Linux only).')
|
||||
|
||||
if self._do_packet_verification:
|
||||
time.sleep(3)
|
||||
|
||||
if self._has_backbone_traffic():
|
||||
# Stop Backbone sniffer before stopping nodes so that we don't capture Codecov Uploading traffic
|
||||
self._stop_backbone_sniffer()
|
||||
|
||||
for node in list(self.nodes.values()):
|
||||
node.stop()
|
||||
node.destroy()
|
||||
|
||||
self.simulator.stop()
|
||||
|
||||
if self._has_backbone_traffic():
|
||||
self._remove_backbone_network()
|
||||
pcap_filename = self._merge_thread_backbone_pcaps()
|
||||
else:
|
||||
pcap_filename = self._get_thread_pcap_filename()
|
||||
|
||||
if self._do_packet_verification:
|
||||
self._test_info['pcap'] = self._get_pcap_filename()
|
||||
self._test_info['pcap'] = pcap_filename
|
||||
|
||||
test_info_path = self._output_test_info()
|
||||
os.environ['LD_LIBRARY_PATH'] = '/tmp/thread-wireshark'
|
||||
self._verify_packets(test_info_path)
|
||||
|
||||
def flush_all(self):
|
||||
@@ -236,8 +286,8 @@ class TestCase(NcpSupportMixin, unittest.TestCase):
|
||||
print("Packet verification passed: %s" % test_info_path, file=sys.stderr)
|
||||
|
||||
@property
|
||||
def testcase_name(self):
|
||||
return os.path.splitext(os.path.basename(sys.argv[0]))[0]
|
||||
def test_name(self):
|
||||
return os.getenv('TEST_NAME', 'current')
|
||||
|
||||
def collect_ipaddrs(self):
|
||||
if not self._do_packet_verification:
|
||||
@@ -248,8 +298,9 @@ class TestCase(NcpSupportMixin, unittest.TestCase):
|
||||
for i, node in self.nodes.items():
|
||||
ipaddrs = node.get_addrs()
|
||||
test_info['ipaddrs'][i] = ipaddrs
|
||||
mleid = node.get_mleid()
|
||||
test_info['mleids'][i] = mleid
|
||||
if not node.is_host:
|
||||
mleid = node.get_mleid()
|
||||
test_info['mleids'][i] = mleid
|
||||
|
||||
def collect_rloc16s(self):
|
||||
if not self._do_packet_verification:
|
||||
@@ -259,7 +310,8 @@ class TestCase(NcpSupportMixin, unittest.TestCase):
|
||||
test_info['rloc16s'] = {}
|
||||
|
||||
for i, node in self.nodes.items():
|
||||
test_info['rloc16s'][i] = '0x%04x' % node.get_addr16()
|
||||
if not node.is_host:
|
||||
test_info['rloc16s'][i] = '0x%04x' % node.get_addr16()
|
||||
|
||||
def collect_rlocs(self):
|
||||
if not self._do_packet_verification:
|
||||
@@ -296,7 +348,7 @@ class TestCase(NcpSupportMixin, unittest.TestCase):
|
||||
return
|
||||
|
||||
test_info = self._test_info = {
|
||||
'testcase': self.testcase_name,
|
||||
'testcase': self.test_name,
|
||||
'start_time': time.ctime(self._start_time),
|
||||
'pcap': '',
|
||||
'extaddrs': {},
|
||||
@@ -304,24 +356,38 @@ class TestCase(NcpSupportMixin, unittest.TestCase):
|
||||
'ipaddrs': {},
|
||||
'mleids': {},
|
||||
'topology': self._initial_topology,
|
||||
'backbone': {
|
||||
'interface': config.BACKBONE_DOCKER_NETWORK_NAME,
|
||||
'prefix': config.BACKBONE_PREFIX,
|
||||
},
|
||||
'otbr_commit': config.OTBR_COMMIT,
|
||||
'domain_prefix': config.DOMAIN_PREFIX,
|
||||
'env': {
|
||||
'PORT_OFFSET': config.PORT_OFFSET,
|
||||
},
|
||||
}
|
||||
|
||||
for i, node in self.nodes.items():
|
||||
extaddr = node.get_addr64()
|
||||
test_info['extaddrs'][i] = ExtAddr(extaddr).format_octets()
|
||||
if not node.is_host:
|
||||
extaddr = node.get_addr64()
|
||||
test_info['extaddrs'][i] = ExtAddr(extaddr).format_octets()
|
||||
|
||||
if node.is_host or node.is_otbr:
|
||||
ethaddr = node.get_ether_mac()
|
||||
test_info['ethaddrs'][i] = EthAddr(ethaddr).format_octets()
|
||||
|
||||
def _output_test_info(self):
|
||||
"""
|
||||
Output test info to json file after tearDown
|
||||
"""
|
||||
filename = f'{self.testcase_name}.json'
|
||||
filename = f'{self.test_name}.json'
|
||||
with open(filename, 'wt') as ofd:
|
||||
ofd.write(json.dumps(self._test_info, indent=1, sort_keys=True))
|
||||
|
||||
return filename
|
||||
|
||||
def _get_pcap_filename(self):
|
||||
current_pcap = os.getenv('TEST_NAME', 'current') + '.pcap'
|
||||
def _get_thread_pcap_filename(self):
|
||||
current_pcap = self.test_name + '.pcap'
|
||||
return os.path.abspath(current_pcap)
|
||||
|
||||
def assure_run_ok(self, cmd, shell=False):
|
||||
@@ -330,3 +396,78 @@ class TestCase(NcpSupportMixin, unittest.TestCase):
|
||||
proc = subprocess.run(cmd, stdout=sys.stdout, stderr=sys.stderr, shell=shell)
|
||||
print(">>> %s => %d" % (cmd, proc.returncode), file=sys.stderr)
|
||||
proc.check_returncode()
|
||||
|
||||
def _parse_params(self, params: Optional[dict]) -> dict:
|
||||
params = params or {}
|
||||
|
||||
if params.get('is_bbr') or params.get('is_otbr'):
|
||||
# BBRs must use thread version 1.2
|
||||
assert params.get('version', '1.2') == '1.2', params
|
||||
params['version'] = '1.2'
|
||||
elif params.get('is_host'):
|
||||
# Hosts must not specify thread version
|
||||
assert params.get('version', '') == '', params
|
||||
params['version'] = ''
|
||||
|
||||
if params:
|
||||
params = dict(DEFAULT_PARAMS, **params)
|
||||
else:
|
||||
params = DEFAULT_PARAMS.copy()
|
||||
|
||||
return params
|
||||
|
||||
def _has_backbone_traffic(self):
|
||||
for param in self.TOPOLOGY.values():
|
||||
if param and (param.get('is_otbr') or param.get('is_host')):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _prepare_backbone_network(self):
|
||||
network_name = config.BACKBONE_DOCKER_NETWORK_NAME
|
||||
self.assure_run_ok(
|
||||
f'docker network create --driver bridge --ipv6 --subnet {config.BACKBONE_PREFIX} -o "com.docker.network.bridge.name"="{network_name}" {network_name} || true',
|
||||
shell=True)
|
||||
|
||||
def _remove_backbone_network(self):
|
||||
network_name = config.BACKBONE_DOCKER_NETWORK_NAME
|
||||
self.assure_run_ok(f'docker network rm {network_name}', shell=True)
|
||||
|
||||
def _start_backbone_sniffer(self):
|
||||
# don't know why but I have to create the empty bbr.pcap first, otherwise tshark won't work
|
||||
# self.assure_run_ok("truncate --size 0 bbr.pcap && chmod 664 bbr.pcap", shell=True)
|
||||
pcap_file = self._get_backbone_pcap_filename()
|
||||
try:
|
||||
os.remove(pcap_file)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
dumpcap = pvutils.which_dumpcap()
|
||||
self._dumpcap_proc = subprocess.Popen([dumpcap, '-i', config.BACKBONE_DOCKER_NETWORK_NAME, '-w', pcap_file],
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr)
|
||||
time.sleep(0.2)
|
||||
assert self._dumpcap_proc.poll() is None, 'tshark terminated unexpectedly'
|
||||
logging.info('Backbone sniffer launched successfully: pid=%s', self._dumpcap_proc.pid)
|
||||
|
||||
def _get_backbone_pcap_filename(self):
|
||||
backbone_pcap = self.test_name + '_backbone.pcap'
|
||||
return os.path.abspath(backbone_pcap)
|
||||
|
||||
def _get_merged_pcap_filename(self):
|
||||
backbone_pcap = self.test_name + '_merged.pcap'
|
||||
return os.path.abspath(backbone_pcap)
|
||||
|
||||
def _stop_backbone_sniffer(self):
|
||||
self._dumpcap_proc.send_signal(signal.SIGTERM)
|
||||
self._dumpcap_proc.__exit__(None, None, None)
|
||||
logging.info('Backbone sniffer terminated successfully: pid=%s' % self._dumpcap_proc.pid)
|
||||
|
||||
def _merge_thread_backbone_pcaps(self):
|
||||
thread_pcap = self._get_thread_pcap_filename()
|
||||
backbone_pcap = self._get_backbone_pcap_filename()
|
||||
merged_pcap = self._get_merged_pcap_filename()
|
||||
|
||||
mergecap = pvutils.which_mergecap()
|
||||
self.assure_run_ok(f'{mergecap} -w {merged_pcap} {thread_pcap} {backbone_pcap}', shell=True)
|
||||
return merged_pcap
|
||||
|
||||
Reference in New Issue
Block a user