1#!/usr/bin/env python3 2# 3# Copyright (c) 2016, The OpenThread Authors. 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 1. Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# 2. Redistributions in binary form must reproduce the above copyright 11# notice, this list of conditions and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# 3. Neither the name of the copyright holder nor the 14# names of its contributors may be used to endorse or promote products 15# derived from this software without specific prior written permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27# POSSIBILITY OF SUCH DAMAGE. 28# 29 30import json 31import binascii 32import ipaddress 33import logging 34import os 35import re 36import shlex 37import socket 38import subprocess 39import sys 40import time 41import traceback 42import typing 43import unittest 44from ipaddress import IPv6Address, IPv6Network 45from typing import Union, Dict, Optional, List, Any 46 47import pexpect 48import pexpect.popen_spawn 49 50import config 51import simulator 52import thread_cert 53 54PORT_OFFSET = int(os.getenv('PORT_OFFSET', "0")) 55 56INFRA_DNS64 = int(os.getenv('NAT64', 0)) 57 58 59class OtbrDocker: 60 RESET_DELAY = 3 61 62 _socat_proc = None 63 _ot_rcp_proc = None 64 _docker_proc = None 65 _border_routing_counters = None 66 67 def __init__(self, nodeid: int, **kwargs): 68 self.verbose = int(float(os.getenv('VERBOSE', 0))) 69 try: 70 self._docker_name = config.OTBR_DOCKER_NAME_PREFIX + str(nodeid) 71 self._prepare_ot_rcp_sim(nodeid) 72 self._launch_docker() 73 except Exception: 74 traceback.print_exc() 75 self.destroy() 76 raise 77 78 def _prepare_ot_rcp_sim(self, nodeid: int): 79 self._socat_proc = subprocess.Popen(['socat', '-d', '-d', 'pty,raw,echo=0', 'pty,raw,echo=0'], 80 stderr=subprocess.PIPE, 81 stdin=subprocess.DEVNULL, 82 stdout=subprocess.DEVNULL) 83 84 line = self._socat_proc.stderr.readline().decode('ascii').strip() 85 self._rcp_device_pty = rcp_device_pty = line[line.index('PTY is /dev') + 7:] 86 line = self._socat_proc.stderr.readline().decode('ascii').strip() 87 self._rcp_device = rcp_device = line[line.index('PTY is /dev') + 7:] 88 logging.info(f"socat running: device PTY: {rcp_device_pty}, device: {rcp_device}") 89 90 ot_rcp_path = self._get_ot_rcp_path() 91 self._ot_rcp_proc = subprocess.Popen(f"{ot_rcp_path} {nodeid} > {rcp_device_pty} < {rcp_device_pty}", 92 shell=True, 93 stdin=subprocess.DEVNULL, 94 stdout=subprocess.DEVNULL, 95 stderr=subprocess.DEVNULL) 96 97 try: 98 self._ot_rcp_proc.wait(1) 99 except subprocess.TimeoutExpired: 100 # We expect ot-rcp not to quit in 1 second. 101 pass 102 else: 103 raise Exception(f"ot-rcp {nodeid} exited unexpectedly!") 104 105 def _get_ot_rcp_path(self) -> str: 106 srcdir = os.environ['top_builddir'] 107 path = '%s/examples/apps/ncp/ot-rcp' % srcdir 108 logging.info("ot-rcp path: %s", path) 109 return path 110 111 def _launch_docker(self): 112 logging.info(f'Docker image: {config.OTBR_DOCKER_IMAGE}') 113 subprocess.check_call(f"docker rm -f {self._docker_name} || true", shell=True) 114 CI_ENV = os.getenv('CI_ENV', '').split() 115 dns = ['--dns=127.0.0.1'] if INFRA_DNS64 == 1 else ['--dns=8.8.8.8'] 116 nat64_prefix = ['--nat64-prefix', '2001:db8:1:ffff::/96'] if INFRA_DNS64 == 1 else [] 117 os.makedirs('/tmp/coverage/', exist_ok=True) 118 119 cmd = ['docker', 'run'] + CI_ENV + [ 120 '--rm', 121 '--name', 122 self._docker_name, 123 '--network', 124 config.BACKBONE_DOCKER_NETWORK_NAME, 125 ] + dns + [ 126 '-i', 127 '--sysctl', 128 'net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1', 129 '--privileged', 130 '--cap-add=NET_ADMIN', 131 '--volume', 132 f'{self._rcp_device}:/dev/ttyUSB0', 133 '-v', 134 '/tmp/coverage/:/tmp/coverage/', 135 config.OTBR_DOCKER_IMAGE, 136 '-B', 137 config.BACKBONE_IFNAME, 138 '--trel-url', 139 f'trel://{config.BACKBONE_IFNAME}', 140 ] + nat64_prefix 141 logging.info(' '.join(cmd)) 142 self._docker_proc = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=sys.stdout, stderr=sys.stderr) 143 144 launch_docker_deadline = time.time() + 300 145 launch_ok = False 146 147 while time.time() < launch_docker_deadline: 148 try: 149 subprocess.check_call(f'docker exec -i {self._docker_name} ot-ctl state', shell=True) 150 launch_ok = True 151 logging.info("OTBR Docker %s Is Ready!", self._docker_name) 152 break 153 except subprocess.CalledProcessError: 154 time.sleep(5) 155 continue 156 157 assert launch_ok 158 159 self.start_ot_ctl() 160 161 def __repr__(self): 162 return f'OtbrDocker<{self.nodeid}>' 163 164 def start_otbr_service(self): 165 self.bash('service otbr-agent start') 166 self.simulator.go(3) 167 self.start_ot_ctl() 168 169 def stop_otbr_service(self): 170 self.stop_ot_ctl() 171 self.bash('service otbr-agent stop') 172 173 def stop_mdns_service(self): 174 self.bash('service avahi-daemon stop; service mdns stop; !(cat /proc/net/udp | grep -i :14E9)') 175 176 def start_mdns_service(self): 177 self.bash('service avahi-daemon start; service mdns start; cat /proc/net/udp | grep -i :14E9') 178 179 def start_ot_ctl(self): 180 cmd = f'docker exec -i {self._docker_name} ot-ctl' 181 self.pexpect = pexpect.popen_spawn.PopenSpawn(cmd, timeout=30) 182 if self.verbose: 183 self.pexpect.logfile_read = sys.stdout.buffer 184 185 # Add delay to ensure that the process is ready to receive commands. 186 timeout = 0.4 187 while timeout > 0: 188 self.pexpect.send('\r\n') 189 try: 190 self.pexpect.expect('> ', timeout=0.1) 191 break 192 except pexpect.TIMEOUT: 193 timeout -= 0.1 194 195 def stop_ot_ctl(self): 196 self.pexpect.sendeof() 197 self.pexpect.wait() 198 self.pexpect.proc.kill() 199 200 def destroy(self): 201 logging.info("Destroying %s", self) 202 self._shutdown_docker() 203 self._shutdown_ot_rcp() 204 self._shutdown_socat() 205 206 def _shutdown_docker(self): 207 if self._docker_proc is None: 208 return 209 210 try: 211 COVERAGE = int(os.getenv('COVERAGE', '0')) 212 OTBR_COVERAGE = int(os.getenv('OTBR_COVERAGE', '0')) 213 test_name = os.getenv('TEST_NAME') 214 unique_node_id = f'{test_name}-{PORT_OFFSET}-{self.nodeid}' 215 216 if COVERAGE or OTBR_COVERAGE: 217 self.bash('service otbr-agent stop') 218 219 cov_file_path = f'/tmp/coverage/coverage-{unique_node_id}.info' 220 # Upload OTBR code coverage if OTBR_COVERAGE=1, otherwise OpenThread code coverage. 221 if OTBR_COVERAGE: 222 codecov_cmd = f'lcov --directory . --capture --output-file {cov_file_path}' 223 else: 224 codecov_cmd = ('lcov --directory build/otbr/third_party/openthread/repo --capture ' 225 f'--output-file {cov_file_path}') 226 227 self.bash(codecov_cmd) 228 229 copyCore = subprocess.run(f'docker cp {self._docker_name}:/core ./coredump_{unique_node_id}', shell=True) 230 if copyCore.returncode == 0: 231 subprocess.check_call( 232 f'docker cp {self._docker_name}:/usr/sbin/otbr-agent ./otbr-agent_{unique_node_id}', shell=True) 233 234 finally: 235 subprocess.check_call(f"docker rm -f {self._docker_name}", shell=True) 236 self._docker_proc.wait() 237 del self._docker_proc 238 239 def _shutdown_ot_rcp(self): 240 if self._ot_rcp_proc is not None: 241 self._ot_rcp_proc.kill() 242 self._ot_rcp_proc.wait() 243 del self._ot_rcp_proc 244 245 def _shutdown_socat(self): 246 if self._socat_proc is not None: 247 self._socat_proc.stderr.close() 248 self._socat_proc.kill() 249 self._socat_proc.wait() 250 del self._socat_proc 251 252 def bash(self, cmd: str, encoding='ascii') -> List[str]: 253 logging.info("%s $ %s", self, cmd) 254 proc = subprocess.Popen(['docker', 'exec', '-i', self._docker_name, 'bash', '-c', cmd], 255 stdin=subprocess.DEVNULL, 256 stdout=subprocess.PIPE, 257 stderr=sys.stderr, 258 encoding=encoding) 259 260 with proc: 261 262 lines = [] 263 264 while True: 265 line = proc.stdout.readline() 266 267 if not line: 268 break 269 270 lines.append(line) 271 logging.info("%s $ %r", self, line.rstrip('\r\n')) 272 273 proc.wait() 274 275 if proc.returncode != 0: 276 raise subprocess.CalledProcessError(proc.returncode, cmd, ''.join(lines)) 277 else: 278 return lines 279 280 def dns_dig(self, server: str, name: str, qtype: str): 281 """ 282 Run dig command to query a DNS server. 283 284 Args: 285 server: the server address. 286 name: the name to query. 287 qtype: the query type (e.g. AAAA, PTR, TXT, SRV). 288 289 Returns: 290 The dig result similar as below: 291 { 292 "opcode": "QUERY", 293 "status": "NOERROR", 294 "id": "64144", 295 "QUESTION": [ 296 ('google.com.', 'IN', 'AAAA') 297 ], 298 "ANSWER": [ 299 ('google.com.', 107, 'IN', 'AAAA', '2404:6800:4008:c00::71'), 300 ('google.com.', 107, 'IN', 'AAAA', '2404:6800:4008:c00::8a'), 301 ('google.com.', 107, 'IN', 'AAAA', '2404:6800:4008:c00::66'), 302 ('google.com.', 107, 'IN', 'AAAA', '2404:6800:4008:c00::8b'), 303 ], 304 "ADDITIONAL": [ 305 ], 306 } 307 """ 308 output = self.bash(f'dig -6 @{server} \'{name}\' {qtype}', encoding='raw_unicode_escape') 309 310 section = None 311 dig_result = { 312 'QUESTION': [], 313 'ANSWER': [], 314 'ADDITIONAL': [], 315 } 316 317 for line in output: 318 line = line.strip() 319 320 if line.startswith(';; ->>HEADER<<- '): 321 headers = line[len(';; ->>HEADER<<- '):].split(', ') 322 for header in headers: 323 key, val = header.split(': ') 324 dig_result[key] = val 325 326 continue 327 328 if line == ';; QUESTION SECTION:': 329 section = 'QUESTION' 330 continue 331 elif line == ';; ANSWER SECTION:': 332 section = 'ANSWER' 333 continue 334 elif line == ';; ADDITIONAL SECTION:': 335 section = 'ADDITIONAL' 336 continue 337 elif section and not line: 338 section = None 339 continue 340 341 if section: 342 assert line 343 344 if section == 'QUESTION': 345 assert line.startswith(';') 346 line = line[1:] 347 record = list(line.split()) 348 349 if section == 'QUESTION': 350 if record[2] in ('SRV', 'TXT'): 351 record[0] = self.__unescape_dns_instance_name(record[0]) 352 else: 353 record[1] = int(record[1]) 354 if record[3] == 'SRV': 355 record[0] = self.__unescape_dns_instance_name(record[0]) 356 record[4], record[5], record[6] = map(int, [record[4], record[5], record[6]]) 357 elif record[3] == 'TXT': 358 record[0] = self.__unescape_dns_instance_name(record[0]) 359 record[4:] = [self.__parse_dns_dig_txt(line)] 360 elif record[3] == 'PTR': 361 record[4] = self.__unescape_dns_instance_name(record[4]) 362 363 dig_result[section].append(tuple(record)) 364 365 return dig_result 366 367 def call_dbus_method(self, *args): 368 args = shlex.join([args[0], args[1], json.dumps(args[2:])]) 369 return json.loads( 370 self.bash(f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/call_dbus_method.py {args}') 371 [0]) 372 373 def get_dbus_property(self, property_name): 374 return self.call_dbus_method('org.freedesktop.DBus.Properties', 'Get', 'io.openthread.BorderRouter', 375 property_name) 376 377 def set_dbus_property(self, property_name, property_value): 378 return self.call_dbus_method('org.freedesktop.DBus.Properties', 'Set', 'io.openthread.BorderRouter', 379 property_name, property_value) 380 381 def get_border_routing_counters(self): 382 counters = self.get_dbus_property('BorderRoutingCounters') 383 counters = { 384 'inbound_unicast': counters[0], 385 'inbound_multicast': counters[1], 386 'outbound_unicast': counters[2], 387 'outbound_multicast': counters[3], 388 'ra_rx': counters[4], 389 'ra_tx_success': counters[5], 390 'ra_tx_failure': counters[6], 391 'rs_rx': counters[7], 392 'rs_tx_success': counters[8], 393 'rs_tx_failure': counters[9], 394 } 395 logging.info(f'border routing counters: {counters}') 396 return counters 397 398 def _process_traffic_counters(self, counter): 399 return { 400 '4to6': { 401 'packets': counter[0], 402 'bytes': counter[1], 403 }, 404 '6to4': { 405 'packets': counter[2], 406 'bytes': counter[3], 407 } 408 } 409 410 def _process_packet_counters(self, counter): 411 return {'4to6': {'packets': counter[0]}, '6to4': {'packets': counter[1]}} 412 413 def nat64_set_enabled(self, enable): 414 return self.call_dbus_method('io.openthread.BorderRouter', 'SetNat64Enabled', enable) 415 416 @property 417 def nat64_cidr(self): 418 self.send_command('nat64 cidr') 419 cidr = self._expect_command_output()[0].strip() 420 return ipaddress.IPv4Network(cidr, strict=False) 421 422 @nat64_cidr.setter 423 def nat64_cidr(self, cidr: ipaddress.IPv4Network): 424 if not isinstance(cidr, ipaddress.IPv4Network): 425 raise ValueError("cidr is expected to be an instance of ipaddress.IPv4Network") 426 self.send_command(f'nat64 cidr {cidr}') 427 self._expect_done() 428 429 @property 430 def nat64_state(self): 431 state = self.get_dbus_property('Nat64State') 432 return {'PrefixManager': state[0], 'Translator': state[1]} 433 434 @property 435 def nat64_mappings(self): 436 return [{ 437 'id': row[0], 438 'ip4': row[1], 439 'ip6': row[2], 440 'expiry': row[3], 441 'counters': { 442 'total': self._process_traffic_counters(row[4][0]), 443 'ICMP': self._process_traffic_counters(row[4][1]), 444 'UDP': self._process_traffic_counters(row[4][2]), 445 'TCP': self._process_traffic_counters(row[4][3]), 446 } 447 } for row in self.get_dbus_property('Nat64Mappings')] 448 449 @property 450 def nat64_counters(self): 451 res_error = self.get_dbus_property('Nat64ErrorCounters') 452 res_proto = self.get_dbus_property('Nat64ProtocolCounters') 453 return { 454 'protocol': { 455 'Total': self._process_traffic_counters(res_proto[0]), 456 'ICMP': self._process_traffic_counters(res_proto[1]), 457 'UDP': self._process_traffic_counters(res_proto[2]), 458 'TCP': self._process_traffic_counters(res_proto[3]), 459 }, 460 'errors': { 461 'Unknown': self._process_packet_counters(res_error[0]), 462 'Illegal Pkt': self._process_packet_counters(res_error[1]), 463 'Unsup Proto': self._process_packet_counters(res_error[2]), 464 'No Mapping': self._process_packet_counters(res_error[3]), 465 } 466 } 467 468 @property 469 def nat64_traffic_counters(self): 470 res = self.get_dbus_property('Nat64TrafficCounters') 471 return { 472 'Total': self._process_traffic_counters(res[0]), 473 'ICMP': self._process_traffic_counters(res[1]), 474 'UDP': self._process_traffic_counters(res[2]), 475 'TCP': self._process_traffic_counters(res[3]), 476 } 477 478 @property 479 def dns_upstream_query_state(self): 480 return bool(self.get_dbus_property('DnsUpstreamQueryState')) 481 482 @dns_upstream_query_state.setter 483 def dns_upstream_query_state(self, value): 484 if type(value) is not bool: 485 raise ValueError("dns_upstream_query_state must be a bool") 486 return self.set_dbus_property('DnsUpstreamQueryState', value) 487 488 def read_border_routing_counters_delta(self): 489 old_counters = self._border_routing_counters 490 new_counters = self.get_border_routing_counters() 491 self._border_routing_counters = new_counters 492 delta_counters = {} 493 if old_counters is None: 494 delta_counters = new_counters 495 else: 496 for i in ('inbound', 'outbound'): 497 for j in ('unicast', 'multicast'): 498 key = f'{i}_{j}' 499 assert (key in old_counters) 500 assert (key in new_counters) 501 value = [new_counters[key][0] - old_counters[key][0], new_counters[key][1] - old_counters[key][1]] 502 delta_counters[key] = value 503 delta_counters = { 504 key: value for key, value in delta_counters.items() if not isinstance(value, int) and value[0] and value[1] 505 } 506 507 return delta_counters 508 509 @staticmethod 510 def __unescape_dns_instance_name(name: str) -> str: 511 new_name = [] 512 i = 0 513 while i < len(name): 514 c = name[i] 515 516 if c == '\\': 517 assert i + 1 < len(name), name 518 if name[i + 1].isdigit(): 519 assert i + 3 < len(name) and name[i + 2].isdigit() and name[i + 3].isdigit(), name 520 new_name.append(chr(int(name[i + 1:i + 4]))) 521 i += 3 522 else: 523 new_name.append(name[i + 1]) 524 i += 1 525 else: 526 new_name.append(c) 527 528 i += 1 529 530 return ''.join(new_name) 531 532 def __parse_dns_dig_txt(self, line: str): 533 # Example TXT entry: 534 # "xp=\\000\\013\\184\\000\\000\\000\\000\\000" 535 txt = {} 536 for entry in re.findall(r'"((?:[^\\]|\\.)*?)"', line): 537 if entry == "": 538 continue 539 540 k, v = entry.split('=', 1) 541 txt[k] = v 542 543 return txt 544 545 def _setup_sysctl(self): 546 self.bash(f'sysctl net.ipv6.conf.{self.ETH_DEV}.accept_ra=2') 547 self.bash(f'sysctl net.ipv6.conf.{self.ETH_DEV}.accept_ra_rt_info_max_plen=64') 548 549 550class OtCli: 551 RESET_DELAY = 0.1 552 553 def __init__(self, nodeid, is_mtd=False, version=None, is_bbr=False, **kwargs): 554 self.verbose = int(float(os.getenv('VERBOSE', 0))) 555 self.node_type = os.getenv('NODE_TYPE', 'sim') 556 self.env_version = os.getenv('THREAD_VERSION', '1.1') 557 self.is_bbr = is_bbr 558 self._initialized = False 559 if os.getenv('COVERAGE', 0) and os.getenv('CC', 'gcc') == 'gcc': 560 self._cmd_prefix = '/usr/bin/env GCOV_PREFIX=%s/ot-run/%s/ot-gcda.%d ' % (os.getenv( 561 'top_srcdir', '.'), sys.argv[0], nodeid) 562 else: 563 self._cmd_prefix = '' 564 565 if version is not None: 566 self.version = version 567 else: 568 self.version = self.env_version 569 570 mode = os.environ.get('USE_MTD') == '1' and is_mtd and 'mtd' or 'ftd' 571 572 if self.node_type == 'soc': 573 self.__init_soc(nodeid) 574 elif self.node_type == 'ncp-sim': 575 # TODO use mode after ncp-mtd is available. 576 self.__init_ncp_sim(nodeid, 'ftd') 577 else: 578 self.__init_sim(nodeid, mode) 579 580 if self.verbose: 581 self.pexpect.logfile_read = sys.stdout.buffer 582 583 self._initialized = True 584 585 def __init_sim(self, nodeid, mode): 586 """ Initialize a simulation node. """ 587 588 # Default command if no match below, will be overridden if below conditions are met. 589 cmd = './ot-cli-%s' % (mode) 590 591 # For Thread 1.2 MTD node, use ot-cli-mtd build regardless of OT_CLI_PATH 592 if self.version != '1.1' and mode == 'mtd' and 'top_builddir' in os.environ: 593 srcdir = os.environ['top_builddir'] 594 cmd = '%s/examples/apps/cli/ot-cli-%s %d' % (srcdir, mode, nodeid) 595 596 # If Thread version of node matches the testing environment version. 597 elif self.version == self.env_version: 598 # Load Thread 1.2 BBR device when testing Thread 1.2 scenarios 599 # which requires device with Backbone functionality. 600 if self.version != '1.1' and self.is_bbr: 601 if 'OT_CLI_PATH_BBR' in os.environ: 602 cmd = os.environ['OT_CLI_PATH_BBR'] 603 elif 'top_builddir_1_3_bbr' in os.environ: 604 srcdir = os.environ['top_builddir_1_3_bbr'] 605 cmd = '%s/examples/apps/cli/ot-cli-%s' % (srcdir, mode) 606 607 # Load Thread device of the testing environment version (may be 1.1 or 1.2) 608 else: 609 if 'OT_CLI_PATH' in os.environ: 610 cmd = os.environ['OT_CLI_PATH'] 611 elif 'top_builddir' in os.environ: 612 srcdir = os.environ['top_builddir'] 613 cmd = '%s/examples/apps/cli/ot-cli-%s' % (srcdir, mode) 614 615 if 'RADIO_DEVICE' in os.environ: 616 cmd += ' --real-time-signal=+1 -v spinel+hdlc+uart://%s?forkpty-arg=%d' % (os.environ['RADIO_DEVICE'], 617 nodeid) 618 self.is_posix = True 619 else: 620 cmd += ' %d' % nodeid 621 622 # Load Thread 1.1 node when testing Thread 1.2 scenarios for interoperability 623 elif self.version == '1.1': 624 # Posix app 625 if 'OT_CLI_PATH_1_1' in os.environ: 626 cmd = os.environ['OT_CLI_PATH_1_1'] 627 elif 'top_builddir_1_1' in os.environ: 628 srcdir = os.environ['top_builddir_1_1'] 629 cmd = '%s/examples/apps/cli/ot-cli-%s' % (srcdir, mode) 630 631 if 'RADIO_DEVICE_1_1' in os.environ: 632 cmd += ' --real-time-signal=+1 -v spinel+hdlc+uart://%s?forkpty-arg=%d' % ( 633 os.environ['RADIO_DEVICE_1_1'], nodeid) 634 self.is_posix = True 635 else: 636 cmd += ' %d' % nodeid 637 638 print("%s" % cmd) 639 640 self.pexpect = pexpect.popen_spawn.PopenSpawn(self._cmd_prefix + cmd, timeout=10) 641 642 # Add delay to ensure that the process is ready to receive commands. 643 timeout = 0.4 644 while timeout > 0: 645 self.pexpect.send('\r\n') 646 try: 647 self.pexpect.expect('> ', timeout=0.1) 648 break 649 except pexpect.TIMEOUT: 650 timeout -= 0.1 651 652 def __init_ncp_sim(self, nodeid, mode): 653 """ Initialize an NCP simulation node. """ 654 655 # Default command if no match below, will be overridden if below conditions are met. 656 cmd = 'spinel-cli.py -p ./ot-ncp-%s -n' % mode 657 658 # If Thread version of node matches the testing environment version. 659 if self.version == self.env_version: 660 if 'RADIO_DEVICE' in os.environ: 661 args = ' --real-time-signal=+1 spinel+hdlc+uart://%s?forkpty-arg=%d' % (os.environ['RADIO_DEVICE'], 662 nodeid) 663 self.is_posix = True 664 else: 665 args = '' 666 667 # Load Thread 1.2 BBR device when testing Thread 1.2 scenarios 668 # which requires device with Backbone functionality. 669 if self.version != '1.1' and self.is_bbr: 670 if 'OT_NCP_PATH_1_3_BBR' in os.environ: 671 cmd = 'spinel-cli.py -p "%s%s" -n' % ( 672 os.environ['OT_NCP_PATH_1_3_BBR'], 673 args, 674 ) 675 elif 'top_builddir_1_3_bbr' in os.environ: 676 srcdir = os.environ['top_builddir_1_3_bbr'] 677 cmd = '%s/examples/apps/ncp/ot-ncp-%s' % (srcdir, mode) 678 cmd = 'spinel-cli.py -p "%s%s" -n' % ( 679 cmd, 680 args, 681 ) 682 683 # Load Thread device of the testing environment version (may be 1.1 or 1.2). 684 else: 685 if 'OT_NCP_PATH' in os.environ: 686 cmd = 'spinel-cli.py -p "%s%s" -n' % ( 687 os.environ['OT_NCP_PATH'], 688 args, 689 ) 690 elif 'top_builddir' in os.environ: 691 srcdir = os.environ['top_builddir'] 692 cmd = '%s/examples/apps/ncp/ot-ncp-%s' % (srcdir, mode) 693 cmd = 'spinel-cli.py -p "%s%s" -n' % ( 694 cmd, 695 args, 696 ) 697 698 # Load Thread 1.1 node when testing Thread 1.2 scenarios for interoperability. 699 elif self.version == '1.1': 700 if 'RADIO_DEVICE_1_1' in os.environ: 701 args = ' --real-time-signal=+1 spinel+hdlc+uart://%s?forkpty-arg=%d' % (os.environ['RADIO_DEVICE_1_1'], 702 nodeid) 703 self.is_posix = True 704 else: 705 args = '' 706 707 if 'OT_NCP_PATH_1_1' in os.environ: 708 cmd = 'spinel-cli.py -p "%s%s" -n' % ( 709 os.environ['OT_NCP_PATH_1_1'], 710 args, 711 ) 712 elif 'top_builddir_1_1' in os.environ: 713 srcdir = os.environ['top_builddir_1_1'] 714 cmd = '%s/examples/apps/ncp/ot-ncp-%s' % (srcdir, mode) 715 cmd = 'spinel-cli.py -p "%s%s" -n' % ( 716 cmd, 717 args, 718 ) 719 720 cmd += ' %d' % nodeid 721 print("%s" % cmd) 722 723 self.pexpect = pexpect.spawn(self._cmd_prefix + cmd, timeout=10) 724 725 # Add delay to ensure that the process is ready to receive commands. 726 time.sleep(0.2) 727 self._expect('spinel-cli >') 728 self.debug(int(os.getenv('DEBUG', '0'))) 729 730 def __init_soc(self, nodeid): 731 """ Initialize a System-on-a-chip node connected via UART. """ 732 import fdpexpect 733 734 serialPort = '/dev/ttyUSB%d' % ((nodeid - 1) * 2) 735 self.pexpect = fdpexpect.fdspawn(os.open(serialPort, os.O_RDWR | os.O_NONBLOCK | os.O_NOCTTY)) 736 737 def destroy(self): 738 if not self._initialized: 739 return 740 741 if (hasattr(self.pexpect, 'proc') and self.pexpect.proc.poll() is None or 742 not hasattr(self.pexpect, 'proc') and self.pexpect.isalive()): 743 print("%d: exit" % self.nodeid) 744 self.pexpect.send('exit\n') 745 self.pexpect.expect(pexpect.EOF) 746 self.pexpect.wait() 747 self._initialized = False 748 749 750class NodeImpl: 751 is_host = False 752 is_otbr = False 753 754 def __init__(self, nodeid, name=None, simulator=None, **kwargs): 755 self.nodeid = nodeid 756 self.name = name or ('Node%d' % nodeid) 757 self.is_posix = False 758 759 self.simulator = simulator 760 if self.simulator: 761 self.simulator.add_node(self) 762 763 super().__init__(nodeid, **kwargs) 764 765 self.set_addr64('%016x' % (thread_cert.EXTENDED_ADDRESS_BASE + nodeid)) 766 767 def _expect(self, pattern, timeout=-1, *args, **kwargs): 768 """ Process simulator events until expected the pattern. """ 769 if timeout == -1: 770 timeout = self.pexpect.timeout 771 772 assert timeout > 0 773 774 while timeout > 0: 775 try: 776 return self.pexpect.expect(pattern, 0.1, *args, **kwargs) 777 except pexpect.TIMEOUT: 778 timeout -= 0.1 779 self.simulator.go(0) 780 if timeout <= 0: 781 raise 782 783 def _expect_done(self, timeout=-1): 784 self._expect('Done', timeout) 785 786 def _expect_result(self, pattern, *args, **kwargs): 787 """Expect a single matching result. 788 789 The arguments are identical to pexpect.expect(). 790 791 Returns: 792 The matched line. 793 """ 794 results = self._expect_results(pattern, *args, **kwargs) 795 assert len(results) == 1, results 796 return results[0] 797 798 def _expect_results(self, pattern, *args, **kwargs): 799 """Expect multiple matching results. 800 801 The arguments are identical to pexpect.expect(). 802 803 Returns: 804 The matched lines. 805 """ 806 output = self._expect_command_output() 807 results = [line for line in output if self._match_pattern(line, pattern)] 808 return results 809 810 @staticmethod 811 def _match_pattern(line, pattern): 812 if isinstance(pattern, str): 813 pattern = re.compile(pattern) 814 815 if isinstance(pattern, typing.Pattern): 816 return pattern.match(line) 817 else: 818 return any(NodeImpl._match_pattern(line, p) for p in pattern) 819 820 def _expect_command_output(self, ignore_logs=True): 821 lines = [] 822 823 while True: 824 line = self.__readline(ignore_logs=ignore_logs) 825 826 if line == 'Done': 827 break 828 elif line.startswith('Error '): 829 raise Exception(line) 830 else: 831 lines.append(line) 832 833 print(f'_expect_command_output() returns {lines!r}') 834 return lines 835 836 def __is_logging_line(self, line: str) -> bool: 837 return len(line) >= 3 and line[:3] in {'[D]', '[I]', '[N]', '[W]', '[C]', '[-]'} 838 839 def read_cert_messages_in_commissioning_log(self, timeout=-1): 840 """Get the log of the traffic after DTLS handshake. 841 """ 842 format_str = br"=+?\[\[THCI\].*?type=%s.*?\].*?=+?[\s\S]+?-{40,}" 843 join_fin_req = format_str % br"JOIN_FIN\.req" 844 join_fin_rsp = format_str % br"JOIN_FIN\.rsp" 845 dummy_format_str = br"\[THCI\].*?type=%s.*?" 846 join_ent_ntf = dummy_format_str % br"JOIN_ENT\.ntf" 847 join_ent_rsp = dummy_format_str % br"JOIN_ENT\.rsp" 848 pattern = (b"(" + join_fin_req + b")|(" + join_fin_rsp + b")|(" + join_ent_ntf + b")|(" + join_ent_rsp + b")") 849 850 messages = [] 851 # There are at most 4 cert messages both for joiner and commissioner 852 for _ in range(0, 4): 853 try: 854 self._expect(pattern, timeout=timeout) 855 log = self.pexpect.match.group(0) 856 messages.append(self._extract_cert_message(log)) 857 except BaseException: 858 break 859 return messages 860 861 def _extract_cert_message(self, log): 862 res = re.search(br"direction=\w+", log) 863 assert res 864 direction = res.group(0).split(b'=')[1].strip() 865 866 res = re.search(br"type=\S+", log) 867 assert res 868 type = res.group(0).split(b'=')[1].strip() 869 870 payload = bytearray([]) 871 payload_len = 0 872 if type in [b"JOIN_FIN.req", b"JOIN_FIN.rsp"]: 873 res = re.search(br"len=\d+", log) 874 assert res 875 payload_len = int(res.group(0).split(b'=')[1].strip()) 876 877 hex_pattern = br"\|(\s([0-9a-fA-F]{2}|\.\.))+?\s+?\|" 878 while True: 879 res = re.search(hex_pattern, log) 880 if not res: 881 break 882 data = [int(hex, 16) for hex in res.group(0)[1:-1].split(b' ') if hex and hex != b'..'] 883 payload += bytearray(data) 884 log = log[res.end() - 1:] 885 assert len(payload) == payload_len 886 return (direction, type, payload) 887 888 def send_command(self, cmd, go=True, expect_command_echo=True): 889 print("%d: %s" % (self.nodeid, cmd)) 890 self.pexpect.send(cmd + '\n') 891 if go: 892 self.simulator.go(0, nodeid=self.nodeid) 893 sys.stdout.flush() 894 895 if expect_command_echo: 896 self._expect_command_echo(cmd) 897 898 def _expect_command_echo(self, cmd): 899 cmd = cmd.strip() 900 while True: 901 line = self.__readline() 902 if line == cmd: 903 break 904 905 logging.warning("expecting echo %r, but read %r", cmd, line) 906 907 def __readline(self, ignore_logs=True): 908 PROMPT = 'spinel-cli > ' if self.node_type == 'ncp-sim' else '> ' 909 while True: 910 self._expect(r"[^\n]+\n") 911 line = self.pexpect.match.group(0).decode('utf8').strip() 912 while line.startswith(PROMPT): 913 line = line[len(PROMPT):] 914 915 if line == '': 916 continue 917 918 if ignore_logs and self.__is_logging_line(line): 919 continue 920 921 return line 922 923 def get_commands(self): 924 self.send_command('?') 925 self._expect('Commands:') 926 return self._expect_results(r'\S+') 927 928 def set_mode(self, mode): 929 cmd = 'mode %s' % mode 930 self.send_command(cmd) 931 self._expect_done() 932 933 def debug(self, level): 934 # `debug` command will not trigger interaction with simulator 935 self.send_command('debug %d' % level, go=False) 936 937 def start(self): 938 self.interface_up() 939 self.thread_start() 940 941 def stop(self): 942 self.thread_stop() 943 self.interface_down() 944 945 def set_log_level(self, level: int): 946 self.send_command(f'log level {level}') 947 self._expect_done() 948 949 def interface_up(self): 950 self.send_command('ifconfig up') 951 self._expect_done() 952 953 def interface_down(self): 954 self.send_command('ifconfig down') 955 self._expect_done() 956 957 def thread_start(self): 958 self.send_command('thread start') 959 self._expect_done() 960 961 def thread_stop(self): 962 self.send_command('thread stop') 963 self._expect_done() 964 965 def detach(self, is_async=False): 966 cmd = 'detach' 967 if is_async: 968 cmd += ' async' 969 970 self.send_command(cmd) 971 972 if is_async: 973 self._expect_done() 974 return 975 976 end = self.simulator.now() + 4 977 while True: 978 self.simulator.go(1) 979 try: 980 self._expect_done(timeout=0.1) 981 return 982 except (pexpect.TIMEOUT, socket.timeout): 983 if self.simulator.now() > end: 984 raise 985 986 def expect_finished_detaching(self): 987 self._expect('Finished detaching') 988 989 def commissioner_start(self): 990 cmd = 'commissioner start' 991 self.send_command(cmd) 992 self._expect_done() 993 994 def commissioner_stop(self): 995 cmd = 'commissioner stop' 996 self.send_command(cmd) 997 self._expect_done() 998 999 def commissioner_state(self): 1000 states = [r'disabled', r'petitioning', r'active'] 1001 self.send_command('commissioner state') 1002 return self._expect_result(states) 1003 1004 def commissioner_add_joiner(self, addr, psk): 1005 cmd = 'commissioner joiner add %s %s' % (addr, psk) 1006 self.send_command(cmd) 1007 self._expect_done() 1008 1009 def commissioner_set_provisioning_url(self, provisioning_url=''): 1010 cmd = 'commissioner provisioningurl %s' % provisioning_url 1011 self.send_command(cmd) 1012 self._expect_done() 1013 1014 def joiner_start(self, pskd='', provisioning_url=''): 1015 cmd = 'joiner start %s %s' % (pskd, provisioning_url) 1016 self.send_command(cmd) 1017 self._expect_done() 1018 1019 def clear_allowlist(self): 1020 cmd = 'macfilter addr clear' 1021 self.send_command(cmd) 1022 self._expect_done() 1023 1024 def enable_allowlist(self): 1025 cmd = 'macfilter addr allowlist' 1026 self.send_command(cmd) 1027 self._expect_done() 1028 1029 def disable_allowlist(self): 1030 cmd = 'macfilter addr disable' 1031 self.send_command(cmd) 1032 self._expect_done() 1033 1034 def add_allowlist(self, addr, rssi=None): 1035 cmd = 'macfilter addr add %s' % addr 1036 1037 if rssi is not None: 1038 cmd += ' %s' % rssi 1039 1040 self.send_command(cmd) 1041 self._expect_done() 1042 1043 def radiofilter_is_enabled(self) -> bool: 1044 states = [r'Disabled', r'Enabled'] 1045 self.send_command('radiofilter') 1046 return self._expect_result(states) == 'Enabled' 1047 1048 def radiofilter_enable(self): 1049 cmd = 'radiofilter enable' 1050 self.send_command(cmd) 1051 self._expect_done() 1052 1053 def radiofilter_disable(self): 1054 cmd = 'radiofilter disable' 1055 self.send_command(cmd) 1056 self._expect_done() 1057 1058 def get_bbr_registration_jitter(self): 1059 self.send_command('bbr jitter') 1060 return int(self._expect_result(r'\d+')) 1061 1062 def set_bbr_registration_jitter(self, jitter): 1063 cmd = 'bbr jitter %d' % jitter 1064 self.send_command(cmd) 1065 self._expect_done() 1066 1067 def get_rcp_version(self) -> str: 1068 self.send_command('rcp version') 1069 rcp_version = self._expect_command_output()[0].strip() 1070 return rcp_version 1071 1072 def srp_server_get_state(self): 1073 states = ['disabled', 'running', 'stopped'] 1074 self.send_command('srp server state') 1075 return self._expect_result(states) 1076 1077 def srp_server_get_addr_mode(self): 1078 modes = [r'unicast', r'anycast'] 1079 self.send_command(f'srp server addrmode') 1080 return self._expect_result(modes) 1081 1082 def srp_server_set_addr_mode(self, mode): 1083 self.send_command(f'srp server addrmode {mode}') 1084 self._expect_done() 1085 1086 def srp_server_get_anycast_seq_num(self): 1087 self.send_command(f'srp server seqnum') 1088 return int(self._expect_result(r'\d+')) 1089 1090 def srp_server_set_anycast_seq_num(self, seqnum): 1091 self.send_command(f'srp server seqnum {seqnum}') 1092 self._expect_done() 1093 1094 def srp_server_set_enabled(self, enable): 1095 cmd = f'srp server {"enable" if enable else "disable"}' 1096 self.send_command(cmd) 1097 self._expect_done() 1098 1099 def srp_server_set_lease_range(self, min_lease, max_lease, min_key_lease, max_key_lease): 1100 self.send_command(f'srp server lease {min_lease} {max_lease} {min_key_lease} {max_key_lease}') 1101 self._expect_done() 1102 1103 def srp_server_set_ttl_range(self, min_ttl, max_ttl): 1104 self.send_command(f'srp server ttl {min_ttl} {max_ttl}') 1105 self._expect_done() 1106 1107 def srp_server_get_hosts(self): 1108 """Returns the host list on the SRP server as a list of property 1109 dictionary. 1110 1111 Example output: 1112 [{ 1113 'fullname': 'my-host.default.service.arpa.', 1114 'name': 'my-host', 1115 'deleted': 'false', 1116 'addresses': ['2001::1', '2001::2'] 1117 }] 1118 """ 1119 1120 cmd = 'srp server host' 1121 self.send_command(cmd) 1122 lines = self._expect_command_output() 1123 host_list = [] 1124 while lines: 1125 host = {} 1126 1127 host['fullname'] = lines.pop(0).strip() 1128 host['name'] = host['fullname'].split('.')[0] 1129 1130 host['deleted'] = lines.pop(0).strip().split(':')[1].strip() 1131 if host['deleted'] == 'true': 1132 host_list.append(host) 1133 continue 1134 1135 addresses = lines.pop(0).strip().split('[')[1].strip(' ]').split(',') 1136 map(str.strip, addresses) 1137 host['addresses'] = [addr.strip() for addr in addresses if addr] 1138 1139 host_list.append(host) 1140 1141 return host_list 1142 1143 def srp_server_get_host(self, host_name): 1144 """Returns host on the SRP server that matches given host name. 1145 1146 Example usage: 1147 self.srp_server_get_host("my-host") 1148 """ 1149 1150 for host in self.srp_server_get_hosts(): 1151 if host_name == host['name']: 1152 return host 1153 1154 def srp_server_get_services(self): 1155 """Returns the service list on the SRP server as a list of property 1156 dictionary. 1157 1158 Example output: 1159 [{ 1160 'fullname': 'my-service._ipps._tcp.default.service.arpa.', 1161 'instance': 'my-service', 1162 'name': '_ipps._tcp', 1163 'deleted': 'false', 1164 'port': '12345', 1165 'priority': '0', 1166 'weight': '0', 1167 'ttl': '7200', 1168 'lease': '7200', 1169 'key-lease': '7200', 1170 'TXT': ['abc=010203'], 1171 'host_fullname': 'my-host.default.service.arpa.', 1172 'host': 'my-host', 1173 'addresses': ['2001::1', '2001::2'] 1174 }] 1175 1176 Note that the TXT data is output as a HEX string. 1177 """ 1178 1179 cmd = 'srp server service' 1180 self.send_command(cmd) 1181 lines = self._expect_command_output() 1182 1183 service_list = [] 1184 while lines: 1185 service = {} 1186 1187 service['fullname'] = lines.pop(0).strip() 1188 name_labels = service['fullname'].split('.') 1189 service['instance'] = name_labels[0] 1190 service['name'] = '.'.join(name_labels[1:3]) 1191 1192 service['deleted'] = lines.pop(0).strip().split(':')[1].strip() 1193 if service['deleted'] == 'true': 1194 service_list.append(service) 1195 continue 1196 1197 # 'subtypes', port', 'priority', 'weight', 'ttl', 'lease', and 'key-lease' 1198 for i in range(0, 7): 1199 key_value = lines.pop(0).strip().split(':') 1200 service[key_value[0].strip()] = key_value[1].strip() 1201 1202 txt_entries = lines.pop(0).strip().split('[')[1].strip(' ]').split(',') 1203 txt_entries = map(str.strip, txt_entries) 1204 service['TXT'] = [txt for txt in txt_entries if txt] 1205 1206 service['host_fullname'] = lines.pop(0).strip().split(':')[1].strip() 1207 service['host'] = service['host_fullname'].split('.')[0] 1208 1209 addresses = lines.pop(0).strip().split('[')[1].strip(' ]').split(',') 1210 addresses = map(str.strip, addresses) 1211 service['addresses'] = [addr for addr in addresses if addr] 1212 1213 service_list.append(service) 1214 1215 return service_list 1216 1217 def srp_server_get_service(self, instance_name, service_name): 1218 """Returns service on the SRP server that matches given instance 1219 name and service name. 1220 1221 Example usage: 1222 self.srp_server_get_service("my-service", "_ipps._tcp") 1223 """ 1224 1225 for service in self.srp_server_get_services(): 1226 if (instance_name == service['instance'] and service_name == service['name']): 1227 return service 1228 1229 def get_srp_server_port(self): 1230 """Returns the SRP server UDP port by parsing 1231 the SRP Server Data in Network Data. 1232 """ 1233 1234 for service in self.get_services(): 1235 # TODO: for now, we are using 0xfd as the SRP service data. 1236 # May use a dedicated bit flag for SRP server. 1237 if int(service[1], 16) == 0x5d: 1238 # The SRP server data contains IPv6 address (16 bytes) 1239 # followed by UDP port number. 1240 return int(service[2][2 * 16:], 16) 1241 1242 def srp_client_start(self, server_address, server_port): 1243 self.send_command(f'srp client start {server_address} {server_port}') 1244 self._expect_done() 1245 1246 def srp_client_stop(self): 1247 self.send_command(f'srp client stop') 1248 self._expect_done() 1249 1250 def srp_client_get_state(self): 1251 cmd = 'srp client state' 1252 self.send_command(cmd) 1253 return self._expect_command_output()[0] 1254 1255 def srp_client_get_auto_start_mode(self): 1256 cmd = 'srp client autostart' 1257 self.send_command(cmd) 1258 return self._expect_command_output()[0] 1259 1260 def srp_client_enable_auto_start_mode(self): 1261 self.send_command(f'srp client autostart enable') 1262 self._expect_done() 1263 1264 def srp_client_disable_auto_start_mode(self): 1265 self.send_command(f'srp client autostart disable') 1266 self._expect_done() 1267 1268 def srp_client_get_server_address(self): 1269 cmd = 'srp client server address' 1270 self.send_command(cmd) 1271 return self._expect_command_output()[0] 1272 1273 def srp_client_get_server_port(self): 1274 cmd = 'srp client server port' 1275 self.send_command(cmd) 1276 return int(self._expect_command_output()[0]) 1277 1278 def srp_client_get_host_state(self): 1279 cmd = 'srp client host state' 1280 self.send_command(cmd) 1281 return self._expect_command_output()[0] 1282 1283 def srp_client_set_host_name(self, name): 1284 self.send_command(f'srp client host name {name}') 1285 self._expect_done() 1286 1287 def srp_client_get_host_name(self): 1288 self.send_command(f'srp client host name') 1289 self._expect_done() 1290 1291 def srp_client_remove_host(self, remove_key=False, send_unreg_to_server=False): 1292 self.send_command(f'srp client host remove {int(remove_key)} {int(send_unreg_to_server)}') 1293 self._expect_done() 1294 1295 def srp_client_clear_host(self): 1296 self.send_command(f'srp client host clear') 1297 self._expect_done() 1298 1299 def srp_client_enable_auto_host_address(self): 1300 self.send_command(f'srp client host address auto') 1301 self._expect_done() 1302 1303 def srp_client_set_host_address(self, *addrs: str): 1304 self.send_command(f'srp client host address {" ".join(addrs)}') 1305 self._expect_done() 1306 1307 def srp_client_get_host_address(self): 1308 self.send_command(f'srp client host address') 1309 self._expect_done() 1310 1311 def srp_client_add_service(self, 1312 instance_name, 1313 service_name, 1314 port, 1315 priority=0, 1316 weight=0, 1317 txt_entries=[], 1318 lease=0, 1319 key_lease=0): 1320 txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries) 1321 if txt_record == '': 1322 txt_record = '-' 1323 instance_name = self._escape_escapable(instance_name) 1324 self.send_command( 1325 f'srp client service add {instance_name} {service_name} {port} {priority} {weight} {txt_record} {lease} {key_lease}' 1326 ) 1327 self._expect_done() 1328 1329 def srp_client_remove_service(self, instance_name, service_name): 1330 self.send_command(f'srp client service remove {instance_name} {service_name}') 1331 self._expect_done() 1332 1333 def srp_client_clear_service(self, instance_name, service_name): 1334 self.send_command(f'srp client service clear {instance_name} {service_name}') 1335 self._expect_done() 1336 1337 def srp_client_get_services(self): 1338 cmd = 'srp client service' 1339 self.send_command(cmd) 1340 service_lines = self._expect_command_output() 1341 return [self._parse_srp_client_service(line) for line in service_lines] 1342 1343 def srp_client_set_lease_interval(self, leaseinterval: int): 1344 cmd = f'srp client leaseinterval {leaseinterval}' 1345 self.send_command(cmd) 1346 self._expect_done() 1347 1348 def srp_client_get_lease_interval(self) -> int: 1349 cmd = 'srp client leaseinterval' 1350 self.send_command(cmd) 1351 return int(self._expect_result('\d+')) 1352 1353 def srp_client_set_key_lease_interval(self, leaseinterval: int): 1354 cmd = f'srp client keyleaseinterval {leaseinterval}' 1355 self.send_command(cmd) 1356 self._expect_done() 1357 1358 def srp_client_get_key_lease_interval(self) -> int: 1359 cmd = 'srp client keyleaseinterval' 1360 self.send_command(cmd) 1361 return int(self._expect_result('\d+')) 1362 1363 def srp_client_set_ttl(self, ttl: int): 1364 cmd = f'srp client ttl {ttl}' 1365 self.send_command(cmd) 1366 self._expect_done() 1367 1368 def srp_client_get_ttl(self) -> int: 1369 cmd = 'srp client ttl' 1370 self.send_command(cmd) 1371 return int(self._expect_result('\d+')) 1372 1373 # 1374 # TREL utilities 1375 # 1376 1377 def enable_trel(self): 1378 cmd = 'trel enable' 1379 self.send_command(cmd) 1380 self._expect_done() 1381 1382 def is_trel_enabled(self) -> Union[None, bool]: 1383 states = [r'Disabled', r'Enabled'] 1384 self.send_command('trel') 1385 try: 1386 return self._expect_result(states) == 'Enabled' 1387 except Exception as ex: 1388 if 'InvalidCommand' in str(ex): 1389 return None 1390 1391 raise 1392 1393 def get_trel_counters(self): 1394 cmd = 'trel counters' 1395 self.send_command(cmd) 1396 result = self._expect_command_output() 1397 1398 counters = {} 1399 for line in result: 1400 m = re.match(r'(\w+)\:[^\d]+(\d+)[^\d]+(\d+)(?:[^\d]+(\d+))?', line) 1401 if m: 1402 groups = m.groups() 1403 sub_counters = { 1404 'packets': int(groups[1]), 1405 'bytes': int(groups[2]), 1406 } 1407 if groups[3]: 1408 sub_counters['failures'] = int(groups[3]) 1409 counters[groups[0]] = sub_counters 1410 return counters 1411 1412 def reset_trel_counters(self): 1413 cmd = 'trel counters reset' 1414 self.send_command(cmd) 1415 self._expect_done() 1416 1417 def _encode_txt_entry(self, entry): 1418 """Encodes the TXT entry to the DNS-SD TXT record format as a HEX string. 1419 1420 Example usage: 1421 self._encode_txt_entries(['abc']) -> '03616263' 1422 self._encode_txt_entries(['def=']) -> '046465663d' 1423 self._encode_txt_entries(['xyz=XYZ']) -> '0778797a3d58595a' 1424 """ 1425 return '{:02x}'.format(len(entry)) + "".join("{:02x}".format(ord(c)) for c in entry) 1426 1427 def _parse_srp_client_service(self, line: str): 1428 """Parse one line of srp service list into a dictionary which 1429 maps string keys to string values. 1430 1431 Example output for input 1432 'instance:\"%s\", name:\"%s\", state:%s, port:%d, priority:%d, weight:%d"' 1433 { 1434 'instance': 'my-service', 1435 'name': '_ipps._udp', 1436 'state': 'ToAdd', 1437 'port': '12345', 1438 'priority': '0', 1439 'weight': '0' 1440 } 1441 1442 Note that value of 'port', 'priority' and 'weight' are represented 1443 as strings but not integers. 1444 """ 1445 key_values = [word.strip().split(':') for word in line.split(', ')] 1446 keys = [key_value[0] for key_value in key_values] 1447 values = [key_value[1].strip('"') for key_value in key_values] 1448 return dict(zip(keys, values)) 1449 1450 def locate(self, anycast_addr): 1451 cmd = 'locate ' + anycast_addr 1452 self.send_command(cmd) 1453 self.simulator.go(5) 1454 return self._parse_locate_result(self._expect_command_output()[0]) 1455 1456 def _parse_locate_result(self, line: str): 1457 """Parse anycast locate result as list of ml-eid and rloc16. 1458 1459 Example output for input 1460 'fd00:db8:0:0:acf9:9d0:7f3c:b06e 0xa800' 1461 1462 [ 'fd00:db8:0:0:acf9:9d0:7f3c:b06e', '0xa800' ] 1463 """ 1464 return line.split(' ') 1465 1466 def enable_backbone_router(self): 1467 cmd = 'bbr enable' 1468 self.send_command(cmd) 1469 self._expect_done() 1470 1471 def disable_backbone_router(self): 1472 cmd = 'bbr disable' 1473 self.send_command(cmd) 1474 self._expect_done() 1475 1476 def register_backbone_router(self): 1477 cmd = 'bbr register' 1478 self.send_command(cmd) 1479 self._expect_done() 1480 1481 def get_backbone_router_state(self): 1482 states = [r'Disabled', r'Primary', r'Secondary'] 1483 self.send_command('bbr state') 1484 return self._expect_result(states) 1485 1486 @property 1487 def is_primary_backbone_router(self) -> bool: 1488 return self.get_backbone_router_state() == 'Primary' 1489 1490 def get_backbone_router(self): 1491 cmd = 'bbr config' 1492 self.send_command(cmd) 1493 self._expect(r'(.*)Done') 1494 g = self.pexpect.match.groups() 1495 output = g[0].decode("utf-8") 1496 lines = output.strip().split('\n') 1497 lines = [l.strip() for l in lines] 1498 ret = {} 1499 for l in lines: 1500 z = re.search(r'seqno:\s+([0-9]+)', l) 1501 if z: 1502 ret['seqno'] = int(z.groups()[0]) 1503 1504 z = re.search(r'delay:\s+([0-9]+)', l) 1505 if z: 1506 ret['delay'] = int(z.groups()[0]) 1507 1508 z = re.search(r'timeout:\s+([0-9]+)', l) 1509 if z: 1510 ret['timeout'] = int(z.groups()[0]) 1511 1512 return ret 1513 1514 def set_backbone_router(self, seqno=None, reg_delay=None, mlr_timeout=None): 1515 cmd = 'bbr config' 1516 1517 if seqno is not None: 1518 cmd += ' seqno %d' % seqno 1519 1520 if reg_delay is not None: 1521 cmd += ' delay %d' % reg_delay 1522 1523 if mlr_timeout is not None: 1524 cmd += ' timeout %d' % mlr_timeout 1525 1526 self.send_command(cmd) 1527 self._expect_done() 1528 1529 def set_domain_prefix(self, prefix, flags='prosD'): 1530 self.add_prefix(prefix, flags) 1531 self.register_netdata() 1532 1533 def remove_domain_prefix(self, prefix): 1534 self.remove_prefix(prefix) 1535 self.register_netdata() 1536 1537 def set_next_dua_response(self, status: Union[str, int], iid=None): 1538 # Convert 5.00 to COAP CODE 160 1539 if isinstance(status, str): 1540 assert '.' in status 1541 status = status.split('.') 1542 status = (int(status[0]) << 5) + int(status[1]) 1543 1544 cmd = 'bbr mgmt dua {}'.format(status) 1545 if iid is not None: 1546 cmd += ' ' + str(iid) 1547 self.send_command(cmd) 1548 self._expect_done() 1549 1550 def set_dua_iid(self, iid: str): 1551 assert len(iid) == 16 1552 int(iid, 16) 1553 1554 cmd = 'dua iid {}'.format(iid) 1555 self.send_command(cmd) 1556 self._expect_done() 1557 1558 def clear_dua_iid(self): 1559 cmd = 'dua iid clear' 1560 self.send_command(cmd) 1561 self._expect_done() 1562 1563 def multicast_listener_list(self) -> Dict[IPv6Address, int]: 1564 cmd = 'bbr mgmt mlr listener' 1565 self.send_command(cmd) 1566 1567 table = {} 1568 for line in self._expect_results("\S+ \d+"): 1569 line = line.split() 1570 assert len(line) == 2, line 1571 ip = IPv6Address(line[0]) 1572 timeout = int(line[1]) 1573 assert ip not in table 1574 1575 table[ip] = timeout 1576 1577 return table 1578 1579 def multicast_listener_clear(self): 1580 cmd = f'bbr mgmt mlr listener clear' 1581 self.send_command(cmd) 1582 self._expect_done() 1583 1584 def multicast_listener_add(self, ip: Union[IPv6Address, str], timeout: int = 0): 1585 if not isinstance(ip, IPv6Address): 1586 ip = IPv6Address(ip) 1587 1588 cmd = f'bbr mgmt mlr listener add {ip.compressed} {timeout}' 1589 self.send_command(cmd) 1590 self._expect(r"(Done|Error .*)") 1591 1592 def set_next_mlr_response(self, status: int): 1593 cmd = 'bbr mgmt mlr response {}'.format(status) 1594 self.send_command(cmd) 1595 self._expect_done() 1596 1597 def register_multicast_listener(self, *ipaddrs: Union[IPv6Address, str], timeout=None): 1598 assert len(ipaddrs) > 0, ipaddrs 1599 1600 ipaddrs = map(str, ipaddrs) 1601 cmd = f'mlr reg {" ".join(ipaddrs)}' 1602 if timeout is not None: 1603 cmd += f' {int(timeout)}' 1604 self.send_command(cmd) 1605 self.simulator.go(3) 1606 lines = self._expect_command_output() 1607 m = re.match(r'status (\d+), (\d+) failed', lines[0]) 1608 assert m is not None, lines 1609 status = int(m.group(1)) 1610 failed_num = int(m.group(2)) 1611 assert failed_num == len(lines) - 1 1612 failed_ips = list(map(IPv6Address, lines[1:])) 1613 print(f"register_multicast_listener {ipaddrs} => status: {status}, failed ips: {failed_ips}") 1614 return status, failed_ips 1615 1616 def set_link_quality(self, addr, lqi): 1617 cmd = 'macfilter rss add-lqi %s %s' % (addr, lqi) 1618 self.send_command(cmd) 1619 self._expect_done() 1620 1621 def set_outbound_link_quality(self, lqi): 1622 cmd = 'macfilter rss add-lqi * %s' % (lqi) 1623 self.send_command(cmd) 1624 self._expect_done() 1625 1626 def remove_allowlist(self, addr): 1627 cmd = 'macfilter addr remove %s' % addr 1628 self.send_command(cmd) 1629 self._expect_done() 1630 1631 def get_addr16(self): 1632 self.send_command('rloc16') 1633 rloc16 = self._expect_result(r'[0-9a-fA-F]{4}') 1634 return int(rloc16, 16) 1635 1636 def get_router_id(self): 1637 rloc16 = self.get_addr16() 1638 return rloc16 >> 10 1639 1640 def get_addr64(self): 1641 self.send_command('extaddr') 1642 return self._expect_result('[0-9a-fA-F]{16}') 1643 1644 def set_addr64(self, addr64: str): 1645 # Make sure `addr64` is a hex string of length 16 1646 assert len(addr64) == 16 1647 int(addr64, 16) 1648 self.send_command('extaddr %s' % addr64) 1649 self._expect_done() 1650 1651 def get_eui64(self): 1652 self.send_command('eui64') 1653 return self._expect_result('[0-9a-fA-F]{16}') 1654 1655 def set_extpanid(self, extpanid): 1656 self.send_command('extpanid %s' % extpanid) 1657 self._expect_done() 1658 1659 def get_extpanid(self): 1660 self.send_command('extpanid') 1661 return self._expect_result('[0-9a-fA-F]{16}') 1662 1663 def get_mesh_local_prefix(self): 1664 self.send_command('prefix meshlocal') 1665 return self._expect_command_output()[0] 1666 1667 def set_mesh_local_prefix(self, mesh_local_prefix): 1668 self.send_command('prefix meshlocal %s' % mesh_local_prefix) 1669 self._expect_done() 1670 1671 def get_joiner_id(self): 1672 self.send_command('joiner id') 1673 return self._expect_result('[0-9a-fA-F]{16}') 1674 1675 def get_channel(self): 1676 self.send_command('channel') 1677 return int(self._expect_result(r'\d+')) 1678 1679 def set_channel(self, channel): 1680 cmd = 'channel %d' % channel 1681 self.send_command(cmd) 1682 self._expect_done() 1683 1684 def get_networkkey(self): 1685 self.send_command('networkkey') 1686 return self._expect_result('[0-9a-fA-F]{32}') 1687 1688 def set_networkkey(self, networkkey): 1689 cmd = 'networkkey %s' % networkkey 1690 self.send_command(cmd) 1691 self._expect_done() 1692 1693 def get_key_sequence_counter(self): 1694 self.send_command('keysequence counter') 1695 result = self._expect_result(r'\d+') 1696 return int(result) 1697 1698 def set_key_sequence_counter(self, key_sequence_counter): 1699 cmd = 'keysequence counter %d' % key_sequence_counter 1700 self.send_command(cmd) 1701 self._expect_done() 1702 1703 def set_key_switch_guardtime(self, key_switch_guardtime): 1704 cmd = 'keysequence guardtime %d' % key_switch_guardtime 1705 self.send_command(cmd) 1706 self._expect_done() 1707 1708 def set_network_id_timeout(self, network_id_timeout): 1709 cmd = 'networkidtimeout %d' % network_id_timeout 1710 self.send_command(cmd) 1711 self._expect_done() 1712 1713 def _escape_escapable(self, string): 1714 """Escape CLI escapable characters in the given string. 1715 1716 Args: 1717 string (str): UTF-8 input string. 1718 1719 Returns: 1720 [str]: The modified string with escaped characters. 1721 """ 1722 escapable_chars = '\\ \t\r\n' 1723 for char in escapable_chars: 1724 string = string.replace(char, '\\%s' % char) 1725 return string 1726 1727 def get_network_name(self): 1728 self.send_command('networkname') 1729 return self._expect_result([r'\S+']) 1730 1731 def set_network_name(self, network_name): 1732 cmd = 'networkname %s' % self._escape_escapable(network_name) 1733 self.send_command(cmd) 1734 self._expect_done() 1735 1736 def get_panid(self): 1737 self.send_command('panid') 1738 result = self._expect_result('0x[0-9a-fA-F]{4}') 1739 return int(result, 16) 1740 1741 def set_panid(self, panid=config.PANID): 1742 cmd = 'panid %d' % panid 1743 self.send_command(cmd) 1744 self._expect_done() 1745 1746 def set_parent_priority(self, priority): 1747 cmd = 'parentpriority %d' % priority 1748 self.send_command(cmd) 1749 self._expect_done() 1750 1751 def get_partition_id(self): 1752 self.send_command('partitionid') 1753 return self._expect_result(r'\d+') 1754 1755 def get_preferred_partition_id(self): 1756 self.send_command('partitionid preferred') 1757 return self._expect_result(r'\d+') 1758 1759 def set_preferred_partition_id(self, partition_id): 1760 cmd = 'partitionid preferred %d' % partition_id 1761 self.send_command(cmd) 1762 self._expect_done() 1763 1764 def get_pollperiod(self): 1765 self.send_command('pollperiod') 1766 return self._expect_result(r'\d+') 1767 1768 def set_pollperiod(self, pollperiod): 1769 self.send_command('pollperiod %d' % pollperiod) 1770 self._expect_done() 1771 1772 def get_child_supervision_interval(self): 1773 self.send_command('childsupervision interval') 1774 return self._expect_result(r'\d+') 1775 1776 def set_child_supervision_interval(self, interval): 1777 self.send_command('childsupervision interval %d' % interval) 1778 self._expect_done() 1779 1780 def get_child_supervision_check_timeout(self): 1781 self.send_command('childsupervision checktimeout') 1782 return self._expect_result(r'\d+') 1783 1784 def set_child_supervision_check_timeout(self, timeout): 1785 self.send_command('childsupervision checktimeout %d' % timeout) 1786 self._expect_done() 1787 1788 def get_child_supervision_check_failure_counter(self): 1789 self.send_command('childsupervision failcounter') 1790 return self._expect_result(r'\d+') 1791 1792 def reset_child_supervision_check_failure_counter(self): 1793 self.send_command('childsupervision failcounter reset') 1794 self._expect_done() 1795 1796 def get_csl_info(self): 1797 self.send_command('csl') 1798 self._expect_done() 1799 1800 def set_csl_channel(self, csl_channel): 1801 self.send_command('csl channel %d' % csl_channel) 1802 self._expect_done() 1803 1804 def set_csl_period(self, csl_period): 1805 self.send_command('csl period %d' % csl_period) 1806 self._expect_done() 1807 1808 def set_csl_timeout(self, csl_timeout): 1809 self.send_command('csl timeout %d' % csl_timeout) 1810 self._expect_done() 1811 1812 def send_mac_emptydata(self): 1813 self.send_command('mac send emptydata') 1814 self._expect_done() 1815 1816 def send_mac_datarequest(self): 1817 self.send_command('mac send datarequest') 1818 self._expect_done() 1819 1820 def set_router_upgrade_threshold(self, threshold): 1821 cmd = 'routerupgradethreshold %d' % threshold 1822 self.send_command(cmd) 1823 self._expect_done() 1824 1825 def set_router_downgrade_threshold(self, threshold): 1826 cmd = 'routerdowngradethreshold %d' % threshold 1827 self.send_command(cmd) 1828 self._expect_done() 1829 1830 def get_router_downgrade_threshold(self) -> int: 1831 self.send_command('routerdowngradethreshold') 1832 return int(self._expect_result(r'\d+')) 1833 1834 def set_router_eligible(self, enable: bool): 1835 cmd = f'routereligible {"enable" if enable else "disable"}' 1836 self.send_command(cmd) 1837 self._expect_done() 1838 1839 def get_router_eligible(self) -> bool: 1840 states = [r'Disabled', r'Enabled'] 1841 self.send_command('routereligible') 1842 return self._expect_result(states) == 'Enabled' 1843 1844 def prefer_router_id(self, router_id): 1845 cmd = 'preferrouterid %d' % router_id 1846 self.send_command(cmd) 1847 self._expect_done() 1848 1849 def release_router_id(self, router_id): 1850 cmd = 'releaserouterid %d' % router_id 1851 self.send_command(cmd) 1852 self._expect_done() 1853 1854 def get_state(self): 1855 states = [r'detached', r'child', r'router', r'leader', r'disabled'] 1856 self.send_command('state') 1857 return self._expect_result(states) 1858 1859 def set_state(self, state): 1860 cmd = 'state %s' % state 1861 self.send_command(cmd) 1862 self._expect_done() 1863 1864 def get_timeout(self): 1865 self.send_command('childtimeout') 1866 return self._expect_result(r'\d+') 1867 1868 def set_timeout(self, timeout): 1869 cmd = 'childtimeout %d' % timeout 1870 self.send_command(cmd) 1871 self._expect_done() 1872 1873 def set_max_children(self, number): 1874 cmd = 'childmax %d' % number 1875 self.send_command(cmd) 1876 self._expect_done() 1877 1878 def get_weight(self): 1879 self.send_command('leaderweight') 1880 return self._expect_result(r'\d+') 1881 1882 def set_weight(self, weight): 1883 cmd = 'leaderweight %d' % weight 1884 self.send_command(cmd) 1885 self._expect_done() 1886 1887 def add_ipaddr(self, ipaddr): 1888 cmd = 'ipaddr add %s' % ipaddr 1889 self.send_command(cmd) 1890 self._expect_done() 1891 1892 def del_ipaddr(self, ipaddr): 1893 cmd = 'ipaddr del %s' % ipaddr 1894 self.send_command(cmd) 1895 self._expect_done() 1896 1897 def add_ipmaddr(self, ipmaddr): 1898 cmd = 'ipmaddr add %s' % ipmaddr 1899 self.send_command(cmd) 1900 self._expect_done() 1901 1902 def del_ipmaddr(self, ipmaddr): 1903 cmd = 'ipmaddr del %s' % ipmaddr 1904 self.send_command(cmd) 1905 self._expect_done() 1906 1907 def get_addrs(self, verbose=False): 1908 self.send_command('ipaddr' + (' -v' if verbose else '')) 1909 1910 return self._expect_results(r'\S+(:\S*)+') 1911 1912 def get_mleid(self): 1913 self.send_command('ipaddr mleid') 1914 return self._expect_result(r'\S+(:\S*)+') 1915 1916 def get_linklocal(self): 1917 self.send_command('ipaddr linklocal') 1918 return self._expect_result(r'\S+(:\S*)+') 1919 1920 def get_rloc(self): 1921 self.send_command('ipaddr rloc') 1922 return self._expect_result(r'\S+(:\S*)+') 1923 1924 def get_addr(self, prefix): 1925 network = ipaddress.ip_network(u'%s' % str(prefix)) 1926 addrs = self.get_addrs() 1927 1928 for addr in addrs: 1929 if isinstance(addr, bytearray): 1930 addr = bytes(addr) 1931 ipv6_address = ipaddress.ip_address(addr) 1932 if ipv6_address in network: 1933 return ipv6_address.exploded 1934 1935 return None 1936 1937 def has_ipaddr(self, address): 1938 ipaddr = ipaddress.ip_address(address) 1939 ipaddrs = self.get_addrs() 1940 for addr in ipaddrs: 1941 if isinstance(addr, bytearray): 1942 addr = bytes(addr) 1943 if ipaddress.ip_address(addr) == ipaddr: 1944 return True 1945 return False 1946 1947 def get_ipmaddrs(self): 1948 self.send_command('ipmaddr') 1949 return self._expect_results(r'\S+(:\S*)+') 1950 1951 def has_ipmaddr(self, address): 1952 ipmaddr = ipaddress.ip_address(address) 1953 ipmaddrs = self.get_ipmaddrs() 1954 for addr in ipmaddrs: 1955 if isinstance(addr, bytearray): 1956 addr = bytes(addr) 1957 if ipaddress.ip_address(addr) == ipmaddr: 1958 return True 1959 return False 1960 1961 def get_addr_leader_aloc(self): 1962 addrs = self.get_addrs() 1963 for addr in addrs: 1964 segs = addr.split(':') 1965 if (segs[4] == '0' and segs[5] == 'ff' and segs[6] == 'fe00' and segs[7] == 'fc00'): 1966 return addr 1967 return None 1968 1969 def get_mleid_iid(self): 1970 ml_eid = IPv6Address(self.get_mleid()) 1971 return ml_eid.packed[8:].hex() 1972 1973 def get_eidcaches(self): 1974 eidcaches = [] 1975 self.send_command('eidcache') 1976 for line in self._expect_results(r'([a-fA-F0-9\:]+) ([a-fA-F0-9]+)'): 1977 eidcaches.append(line.split()) 1978 1979 return eidcaches 1980 1981 def add_service(self, enterpriseNumber, serviceData, serverData): 1982 cmd = 'service add %s %s %s' % ( 1983 enterpriseNumber, 1984 serviceData, 1985 serverData, 1986 ) 1987 self.send_command(cmd) 1988 self._expect_done() 1989 1990 def remove_service(self, enterpriseNumber, serviceData): 1991 cmd = 'service remove %s %s' % (enterpriseNumber, serviceData) 1992 self.send_command(cmd) 1993 self._expect_done() 1994 1995 def get_child_table(self) -> Dict[int, Dict[str, Any]]: 1996 """Get the table of attached children.""" 1997 cmd = 'child table' 1998 self.send_command(cmd) 1999 output = self._expect_command_output() 2000 2001 # 2002 # Example output: 2003 # | ID | RLOC16 | Timeout | Age | LQ In | C_VN |R|D|N|Ver|CSL|QMsgCnt|Suprvsn| Extended MAC | 2004 # +-----+--------+------------+------------+-------+------+-+-+-+---+---+-------+-------+------------------+ 2005 # | 1 | 0xc801 | 240 | 24 | 3 | 131 |1|0|0| 3| 0 | 0 | 129 | 4ecede68435358ac | 2006 # | 2 | 0xc802 | 240 | 2 | 3 | 131 |0|0|0| 3| 1 | 0 | 0 | a672a601d2ce37d8 | 2007 # Done 2008 # 2009 2010 headers = self.__split_table_row(output[0]) 2011 2012 table = {} 2013 for line in output[2:]: 2014 line = line.strip() 2015 if not line: 2016 continue 2017 2018 fields = self.__split_table_row(line) 2019 col = lambda colname: self.__get_table_col(colname, headers, fields) 2020 2021 id = int(col("ID")) 2022 r, d, n = int(col("R")), int(col("D")), int(col("N")) 2023 mode = f'{"r" if r else ""}{"d" if d else ""}{"n" if n else ""}' 2024 2025 table[int(id)] = { 2026 'id': int(id), 2027 'rloc16': int(col('RLOC16'), 16), 2028 'timeout': int(col('Timeout')), 2029 'age': int(col('Age')), 2030 'lq_in': int(col('LQ In')), 2031 'c_vn': int(col('C_VN')), 2032 'mode': mode, 2033 'extaddr': col('Extended MAC'), 2034 'ver': int(col('Ver')), 2035 'csl': bool(int(col('CSL'))), 2036 'qmsgcnt': int(col('QMsgCnt')), 2037 'suprvsn': int(col('Suprvsn')) 2038 } 2039 2040 return table 2041 2042 def __split_table_row(self, row: str) -> List[str]: 2043 if not (row.startswith('|') and row.endswith('|')): 2044 raise ValueError(row) 2045 2046 fields = row.split('|') 2047 fields = [x.strip() for x in fields[1:-1]] 2048 return fields 2049 2050 def __get_table_col(self, colname: str, headers: List[str], fields: List[str]) -> str: 2051 return fields[headers.index(colname)] 2052 2053 def __getOmrAddress(self): 2054 prefixes = [prefix.split('::')[0] for prefix in self.get_prefixes()] 2055 omr_addrs = [] 2056 for addr in self.get_addrs(): 2057 for prefix in prefixes: 2058 if (addr.startswith(prefix)) and (addr != self.__getDua()): 2059 omr_addrs.append(addr) 2060 break 2061 2062 return omr_addrs 2063 2064 def __getLinkLocalAddress(self): 2065 for ip6Addr in self.get_addrs(): 2066 if re.match(config.LINK_LOCAL_REGEX_PATTERN, ip6Addr, re.I): 2067 return ip6Addr 2068 2069 return None 2070 2071 def __getGlobalAddress(self): 2072 global_address = [] 2073 for ip6Addr in self.get_addrs(): 2074 if ((not re.match(config.LINK_LOCAL_REGEX_PATTERN, ip6Addr, re.I)) and 2075 (not re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, re.I)) and 2076 (not re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I))): 2077 global_address.append(ip6Addr) 2078 2079 return global_address 2080 2081 def __getRloc(self): 2082 for ip6Addr in self.get_addrs(): 2083 if (re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, re.I) and 2084 re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I) and 2085 not (re.match(config.ALOC_FLAG_REGEX_PATTERN, ip6Addr, re.I))): 2086 return ip6Addr 2087 return None 2088 2089 def __getAloc(self): 2090 aloc = [] 2091 for ip6Addr in self.get_addrs(): 2092 if (re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, re.I) and 2093 re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I) and 2094 re.match(config.ALOC_FLAG_REGEX_PATTERN, ip6Addr, re.I)): 2095 aloc.append(ip6Addr) 2096 2097 return aloc 2098 2099 def __getMleid(self): 2100 for ip6Addr in self.get_addrs(): 2101 if re.match(config.MESH_LOCAL_PREFIX_REGEX_PATTERN, ip6Addr, 2102 re.I) and not (re.match(config.ROUTING_LOCATOR_REGEX_PATTERN, ip6Addr, re.I)): 2103 return ip6Addr 2104 2105 return None 2106 2107 def __getDua(self) -> Optional[str]: 2108 for ip6Addr in self.get_addrs(): 2109 if re.match(config.DOMAIN_PREFIX_REGEX_PATTERN, ip6Addr, re.I): 2110 return ip6Addr 2111 2112 return None 2113 2114 def get_ip6_address_by_prefix(self, prefix: Union[str, IPv6Network]) -> List[IPv6Address]: 2115 """Get addresses matched with given prefix. 2116 2117 Args: 2118 prefix: the prefix to match against. 2119 Can be either a string or ipaddress.IPv6Network. 2120 2121 Returns: 2122 The IPv6 address list. 2123 """ 2124 if isinstance(prefix, str): 2125 prefix = IPv6Network(prefix) 2126 addrs = map(IPv6Address, self.get_addrs()) 2127 2128 return [addr for addr in addrs if addr in prefix] 2129 2130 def get_ip6_address(self, address_type): 2131 """Get specific type of IPv6 address configured on thread device. 2132 2133 Args: 2134 address_type: the config.ADDRESS_TYPE type of IPv6 address. 2135 2136 Returns: 2137 IPv6 address string. 2138 """ 2139 if address_type == config.ADDRESS_TYPE.LINK_LOCAL: 2140 return self.__getLinkLocalAddress() 2141 elif address_type == config.ADDRESS_TYPE.GLOBAL: 2142 return self.__getGlobalAddress() 2143 elif address_type == config.ADDRESS_TYPE.RLOC: 2144 return self.__getRloc() 2145 elif address_type == config.ADDRESS_TYPE.ALOC: 2146 return self.__getAloc() 2147 elif address_type == config.ADDRESS_TYPE.ML_EID: 2148 return self.__getMleid() 2149 elif address_type == config.ADDRESS_TYPE.DUA: 2150 return self.__getDua() 2151 elif address_type == config.ADDRESS_TYPE.BACKBONE_GUA: 2152 return self._getBackboneGua() 2153 elif address_type == config.ADDRESS_TYPE.OMR: 2154 return self.__getOmrAddress() 2155 else: 2156 return None 2157 2158 def get_context_reuse_delay(self): 2159 self.send_command('contextreusedelay') 2160 return self._expect_result(r'\d+') 2161 2162 def set_context_reuse_delay(self, delay): 2163 cmd = 'contextreusedelay %d' % delay 2164 self.send_command(cmd) 2165 self._expect_done() 2166 2167 def add_prefix(self, prefix, flags='paosr', prf='med'): 2168 cmd = 'prefix add %s %s %s' % (prefix, flags, prf) 2169 self.send_command(cmd) 2170 self._expect_done() 2171 2172 def remove_prefix(self, prefix): 2173 cmd = 'prefix remove %s' % prefix 2174 self.send_command(cmd) 2175 self._expect_done() 2176 2177 def enable_br(self): 2178 self.send_command('br enable') 2179 self._expect_done() 2180 2181 def disable_br(self): 2182 self.send_command('br disable') 2183 self._expect_done() 2184 2185 def get_br_omr_prefix(self): 2186 cmd = 'br omrprefix local' 2187 self.send_command(cmd) 2188 return self._expect_command_output()[0] 2189 2190 def get_netdata_omr_prefixes(self): 2191 omr_prefixes = [] 2192 for prefix in self.get_prefixes(): 2193 prefix, flags = prefix.split()[:2] 2194 if 'a' in flags and 'o' in flags and 's' in flags and 'D' not in flags: 2195 omr_prefixes.append(prefix) 2196 2197 return omr_prefixes 2198 2199 def get_br_on_link_prefix(self): 2200 cmd = 'br onlinkprefix local' 2201 self.send_command(cmd) 2202 return self._expect_command_output()[0] 2203 2204 def get_netdata_non_nat64_routes(self): 2205 nat64_routes = [] 2206 routes = self.get_routes() 2207 for route in routes: 2208 if 'n' not in route.split(' ')[1]: 2209 nat64_routes.append(route.split(' ')[0]) 2210 return nat64_routes 2211 2212 def get_netdata_nat64_routes(self): 2213 nat64_routes = [] 2214 routes = self.get_routes() 2215 for route in routes: 2216 if 'n' in route.split(' ')[1]: 2217 nat64_routes.append(route.split(' ')[0]) 2218 return nat64_routes 2219 2220 def get_br_nat64_prefix(self): 2221 cmd = 'br nat64prefix local' 2222 self.send_command(cmd) 2223 return self._expect_command_output()[0] 2224 2225 def get_br_favored_nat64_prefix(self): 2226 cmd = 'br nat64prefix favored' 2227 self.send_command(cmd) 2228 return self._expect_command_output()[0].split(' ')[0] 2229 2230 def enable_nat64(self): 2231 self.send_command(f'nat64 enable') 2232 self._expect_done() 2233 2234 def disable_nat64(self): 2235 self.send_command(f'nat64 disable') 2236 self._expect_done() 2237 2238 def get_nat64_state(self): 2239 self.send_command('nat64 state') 2240 res = {} 2241 for line in self._expect_command_output(): 2242 state = line.split(':') 2243 res[state[0].strip()] = state[1].strip() 2244 return res 2245 2246 def get_nat64_mappings(self): 2247 cmd = 'nat64 mappings' 2248 self.send_command(cmd) 2249 result = self._expect_command_output() 2250 session = None 2251 session_counters = None 2252 sessions = [] 2253 2254 for line in result: 2255 m = re.match( 2256 r'\|\s+([a-f0-9]+)\s+\|\s+(.+)\s+\|\s+(.+)\s+\|\s+(\d+)s\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', 2257 line) 2258 if m: 2259 groups = m.groups() 2260 if session: 2261 session['counters'] = session_counters 2262 sessions.append(session) 2263 session = { 2264 'id': groups[0], 2265 'ip6': groups[1], 2266 'ip4': groups[2], 2267 'expiry': int(groups[3]), 2268 } 2269 session_counters = {} 2270 session_counters['total'] = { 2271 '4to6': { 2272 'packets': int(groups[4]), 2273 'bytes': int(groups[5]), 2274 }, 2275 '6to4': { 2276 'packets': int(groups[6]), 2277 'bytes': int(groups[7]), 2278 }, 2279 } 2280 continue 2281 if not session: 2282 continue 2283 m = re.match(r'\|\s+\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line) 2284 if m: 2285 groups = m.groups() 2286 session_counters[groups[0]] = { 2287 '4to6': { 2288 'packets': int(groups[1]), 2289 'bytes': int(groups[2]), 2290 }, 2291 '6to4': { 2292 'packets': int(groups[3]), 2293 'bytes': int(groups[4]), 2294 }, 2295 } 2296 if session: 2297 session['counters'] = session_counters 2298 sessions.append(session) 2299 return sessions 2300 2301 def get_nat64_counters(self): 2302 cmd = 'nat64 counters' 2303 self.send_command(cmd) 2304 result = self._expect_command_output() 2305 2306 protocol_counters = {} 2307 error_counters = {} 2308 for line in result: 2309 m = re.match(r'\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line) 2310 if m: 2311 groups = m.groups() 2312 protocol_counters[groups[0]] = { 2313 '4to6': { 2314 'packets': int(groups[1]), 2315 'bytes': int(groups[2]), 2316 }, 2317 '6to4': { 2318 'packets': int(groups[3]), 2319 'bytes': int(groups[4]), 2320 }, 2321 } 2322 continue 2323 m = re.match(r'\|\s+(.+)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|', line) 2324 if m: 2325 groups = m.groups() 2326 error_counters[groups[0]] = { 2327 '4to6': { 2328 'packets': int(groups[1]), 2329 }, 2330 '6to4': { 2331 'packets': int(groups[2]), 2332 }, 2333 } 2334 continue 2335 return {'protocol': protocol_counters, 'errors': error_counters} 2336 2337 def get_prefixes(self): 2338 return self.get_netdata()['Prefixes'] 2339 2340 def get_routes(self): 2341 return self.get_netdata()['Routes'] 2342 2343 def get_services(self): 2344 netdata = self.netdata_show() 2345 services = [] 2346 services_section = False 2347 2348 for line in netdata: 2349 if line.startswith('Services:'): 2350 services_section = True 2351 elif line.startswith('Contexts'): 2352 services_section = False 2353 elif services_section: 2354 services.append(line.strip().split(' ')) 2355 return services 2356 2357 def netdata_show(self): 2358 self.send_command('netdata show') 2359 return self._expect_command_output() 2360 2361 def get_netdata(self): 2362 raw_netdata = self.netdata_show() 2363 netdata = {'Prefixes': [], 'Routes': [], 'Services': [], 'Contexts': [], 'Commissioning': []} 2364 key_list = ['Prefixes', 'Routes', 'Services', 'Contexts', 'Commissioning'] 2365 key = None 2366 2367 for i in range(0, len(raw_netdata)): 2368 keys = list(filter(raw_netdata[i].startswith, key_list)) 2369 if keys != []: 2370 key = keys[0] 2371 elif key is not None: 2372 netdata[key].append(raw_netdata[i]) 2373 2374 return netdata 2375 2376 def add_route(self, prefix, stable=False, nat64=False, prf='med'): 2377 cmd = 'route add %s ' % prefix 2378 if stable: 2379 cmd += 's' 2380 if nat64: 2381 cmd += 'n' 2382 cmd += ' %s' % prf 2383 self.send_command(cmd) 2384 self._expect_done() 2385 2386 def remove_route(self, prefix): 2387 cmd = 'route remove %s' % prefix 2388 self.send_command(cmd) 2389 self._expect_done() 2390 2391 def register_netdata(self): 2392 self.send_command('netdata register') 2393 self._expect_done() 2394 2395 def netdata_publish_dnssrp_anycast(self, seqnum): 2396 self.send_command(f'netdata publish dnssrp anycast {seqnum}') 2397 self._expect_done() 2398 2399 def netdata_publish_dnssrp_unicast(self, address, port): 2400 self.send_command(f'netdata publish dnssrp unicast {address} {port}') 2401 self._expect_done() 2402 2403 def netdata_publish_dnssrp_unicast_mleid(self, port): 2404 self.send_command(f'netdata publish dnssrp unicast {port}') 2405 self._expect_done() 2406 2407 def netdata_unpublish_dnssrp(self): 2408 self.send_command('netdata unpublish dnssrp') 2409 self._expect_done() 2410 2411 def netdata_publish_prefix(self, prefix, flags='paosr', prf='med'): 2412 self.send_command(f'netdata publish prefix {prefix} {flags} {prf}') 2413 self._expect_done() 2414 2415 def netdata_publish_route(self, prefix, flags='s', prf='med'): 2416 self.send_command(f'netdata publish route {prefix} {flags} {prf}') 2417 self._expect_done() 2418 2419 def netdata_publish_replace(self, old_prefix, prefix, flags='s', prf='med'): 2420 self.send_command(f'netdata publish replace {old_prefix} {prefix} {flags} {prf}') 2421 self._expect_done() 2422 2423 def netdata_unpublish_prefix(self, prefix): 2424 self.send_command(f'netdata unpublish {prefix}') 2425 self._expect_done() 2426 2427 def send_network_diag_get(self, addr, tlv_types): 2428 self.send_command('networkdiagnostic get %s %s' % (addr, ' '.join([str(t.value) for t in tlv_types]))) 2429 2430 if isinstance(self.simulator, simulator.VirtualTime): 2431 self.simulator.go(8) 2432 timeout = 1 2433 else: 2434 timeout = 8 2435 2436 self._expect_done(timeout=timeout) 2437 2438 def send_network_diag_reset(self, addr, tlv_types): 2439 self.send_command('networkdiagnostic reset %s %s' % (addr, ' '.join([str(t.value) for t in tlv_types]))) 2440 2441 if isinstance(self.simulator, simulator.VirtualTime): 2442 self.simulator.go(8) 2443 timeout = 1 2444 else: 2445 timeout = 8 2446 2447 self._expect_done(timeout=timeout) 2448 2449 def energy_scan(self, mask, count, period, scan_duration, ipaddr): 2450 cmd = 'commissioner energy %d %d %d %d %s' % ( 2451 mask, 2452 count, 2453 period, 2454 scan_duration, 2455 ipaddr, 2456 ) 2457 self.send_command(cmd) 2458 2459 if isinstance(self.simulator, simulator.VirtualTime): 2460 self.simulator.go(8) 2461 timeout = 1 2462 else: 2463 timeout = 8 2464 2465 self._expect('Energy:', timeout=timeout) 2466 2467 def panid_query(self, panid, mask, ipaddr): 2468 cmd = 'commissioner panid %d %d %s' % (panid, mask, ipaddr) 2469 self.send_command(cmd) 2470 2471 if isinstance(self.simulator, simulator.VirtualTime): 2472 self.simulator.go(8) 2473 timeout = 1 2474 else: 2475 timeout = 8 2476 2477 self._expect('Conflict:', timeout=timeout) 2478 2479 def scan(self, result=1, timeout=10): 2480 self.send_command('scan') 2481 2482 self.simulator.go(timeout) 2483 2484 if result == 1: 2485 networks = [] 2486 for line in self._expect_command_output()[2:]: 2487 _, panid, extaddr, channel, dbm, lqi, _ = map(str.strip, line.split('|')) 2488 panid = int(panid, 16) 2489 channel, dbm, lqi = map(int, (channel, dbm, lqi)) 2490 2491 networks.append({ 2492 'panid': panid, 2493 'extaddr': extaddr, 2494 'channel': channel, 2495 'dbm': dbm, 2496 'lqi': lqi, 2497 }) 2498 return networks 2499 2500 def scan_energy(self, timeout=10): 2501 self.send_command('scan energy') 2502 self.simulator.go(timeout) 2503 rssi_list = [] 2504 for line in self._expect_command_output()[2:]: 2505 _, channel, rssi, _ = line.split('|') 2506 rssi_list.append({ 2507 'channel': int(channel.strip()), 2508 'rssi': int(rssi.strip()), 2509 }) 2510 return rssi_list 2511 2512 def ping(self, ipaddr, num_responses=1, size=8, timeout=5, count=1, interval=1, hoplimit=64, interface=None): 2513 args = f'{ipaddr} {size} {count} {interval} {hoplimit} {timeout}' 2514 if interface is not None: 2515 args = f'-I {interface} {args}' 2516 cmd = f'ping {args}' 2517 2518 self.send_command(cmd) 2519 2520 wait_allowance = 3 2521 end = self.simulator.now() + timeout + wait_allowance 2522 2523 responders = {} 2524 2525 result = True 2526 # ncp-sim doesn't print Done 2527 done = (self.node_type == 'ncp-sim') 2528 while len(responders) < num_responses or not done: 2529 self.simulator.go(1) 2530 try: 2531 i = self._expect([r'from (\S+):', r'Done'], timeout=0.1) 2532 except (pexpect.TIMEOUT, socket.timeout): 2533 if self.simulator.now() < end: 2534 continue 2535 result = False 2536 if isinstance(self.simulator, simulator.VirtualTime): 2537 self.simulator.sync_devices() 2538 break 2539 else: 2540 if i == 0: 2541 responders[self.pexpect.match.groups()[0]] = 1 2542 elif i == 1: 2543 done = True 2544 return result 2545 2546 def reset(self): 2547 self._reset('reset') 2548 2549 def factory_reset(self): 2550 self._reset('factoryreset') 2551 2552 def _reset(self, cmd): 2553 self.send_command(cmd, expect_command_echo=False) 2554 time.sleep(self.RESET_DELAY) 2555 # Send a "version" command and drain the CLI output after reset 2556 self.send_command('version', expect_command_echo=False) 2557 while True: 2558 try: 2559 self._expect(r"[^\n]+\n", timeout=0.1) 2560 continue 2561 except pexpect.TIMEOUT: 2562 break 2563 2564 if self.is_otbr: 2565 self.set_log_level(5) 2566 2567 def set_router_selection_jitter(self, jitter): 2568 cmd = 'routerselectionjitter %d' % jitter 2569 self.send_command(cmd) 2570 self._expect_done() 2571 2572 def set_active_dataset( 2573 self, 2574 timestamp=None, 2575 channel=None, 2576 channel_mask=None, 2577 extended_panid=None, 2578 mesh_local_prefix=None, 2579 network_key=None, 2580 network_name=None, 2581 panid=None, 2582 pskc=None, 2583 security_policy=[], 2584 updateExisting=False, 2585 ): 2586 2587 if updateExisting: 2588 self.send_command('dataset init active', go=False) 2589 else: 2590 self.send_command('dataset clear', go=False) 2591 self._expect_done() 2592 2593 if timestamp is not None: 2594 cmd = 'dataset activetimestamp %d' % timestamp 2595 self.send_command(cmd, go=False) 2596 self._expect_done() 2597 2598 if channel is not None: 2599 cmd = 'dataset channel %d' % channel 2600 self.send_command(cmd, go=False) 2601 self._expect_done() 2602 2603 if channel_mask is not None: 2604 cmd = 'dataset channelmask %d' % channel_mask 2605 self.send_command(cmd, go=False) 2606 self._expect_done() 2607 2608 if extended_panid is not None: 2609 cmd = 'dataset extpanid %s' % extended_panid 2610 self.send_command(cmd, go=False) 2611 self._expect_done() 2612 2613 if mesh_local_prefix is not None: 2614 cmd = 'dataset meshlocalprefix %s' % mesh_local_prefix 2615 self.send_command(cmd, go=False) 2616 self._expect_done() 2617 2618 if network_key is not None: 2619 cmd = 'dataset networkkey %s' % network_key 2620 self.send_command(cmd, go=False) 2621 self._expect_done() 2622 2623 if network_name is not None: 2624 cmd = 'dataset networkname %s' % network_name 2625 self.send_command(cmd, go=False) 2626 self._expect_done() 2627 2628 if panid is not None: 2629 cmd = 'dataset panid %d' % panid 2630 self.send_command(cmd, go=False) 2631 self._expect_done() 2632 2633 if pskc is not None: 2634 cmd = 'dataset pskc %s' % pskc 2635 self.send_command(cmd, go=False) 2636 self._expect_done() 2637 2638 if security_policy is not None: 2639 if len(security_policy) >= 2: 2640 cmd = 'dataset securitypolicy %s %s' % ( 2641 str(security_policy[0]), 2642 security_policy[1], 2643 ) 2644 if len(security_policy) >= 3: 2645 cmd += ' %s' % (str(security_policy[2])) 2646 self.send_command(cmd, go=False) 2647 self._expect_done() 2648 2649 self.send_command('dataset commit active', go=False) 2650 self._expect_done() 2651 2652 def set_pending_dataset(self, pendingtimestamp, activetimestamp, panid=None, channel=None, delay=None): 2653 self.send_command('dataset clear') 2654 self._expect_done() 2655 2656 cmd = 'dataset pendingtimestamp %d' % pendingtimestamp 2657 self.send_command(cmd) 2658 self._expect_done() 2659 2660 cmd = 'dataset activetimestamp %d' % activetimestamp 2661 self.send_command(cmd) 2662 self._expect_done() 2663 2664 if panid is not None: 2665 cmd = 'dataset panid %d' % panid 2666 self.send_command(cmd) 2667 self._expect_done() 2668 2669 if channel is not None: 2670 cmd = 'dataset channel %d' % channel 2671 self.send_command(cmd) 2672 self._expect_done() 2673 2674 if delay is not None: 2675 cmd = 'dataset delay %d' % delay 2676 self.send_command(cmd) 2677 self._expect_done() 2678 2679 # Set the meshlocal prefix in config.py 2680 self.send_command('dataset meshlocalprefix %s' % config.MESH_LOCAL_PREFIX.split('/')[0]) 2681 self._expect_done() 2682 2683 self.send_command('dataset commit pending') 2684 self._expect_done() 2685 2686 def start_dataset_updater(self, panid=None, channel=None): 2687 self.send_command('dataset clear') 2688 self._expect_done() 2689 2690 if panid is not None: 2691 cmd = 'dataset panid %d' % panid 2692 self.send_command(cmd) 2693 self._expect_done() 2694 2695 if channel is not None: 2696 cmd = 'dataset channel %d' % channel 2697 self.send_command(cmd) 2698 self._expect_done() 2699 2700 self.send_command('dataset updater start') 2701 self._expect_done() 2702 2703 def announce_begin(self, mask, count, period, ipaddr): 2704 cmd = 'commissioner announce %d %d %d %s' % ( 2705 mask, 2706 count, 2707 period, 2708 ipaddr, 2709 ) 2710 self.send_command(cmd) 2711 self._expect_done() 2712 2713 def send_mgmt_active_set( 2714 self, 2715 active_timestamp=None, 2716 channel=None, 2717 channel_mask=None, 2718 extended_panid=None, 2719 panid=None, 2720 network_key=None, 2721 mesh_local=None, 2722 network_name=None, 2723 security_policy=None, 2724 binary=None, 2725 ): 2726 cmd = 'dataset mgmtsetcommand active ' 2727 2728 if active_timestamp is not None: 2729 cmd += 'activetimestamp %d ' % active_timestamp 2730 2731 if channel is not None: 2732 cmd += 'channel %d ' % channel 2733 2734 if channel_mask is not None: 2735 cmd += 'channelmask %d ' % channel_mask 2736 2737 if extended_panid is not None: 2738 cmd += 'extpanid %s ' % extended_panid 2739 2740 if panid is not None: 2741 cmd += 'panid %d ' % panid 2742 2743 if network_key is not None: 2744 cmd += 'networkkey %s ' % network_key 2745 2746 if mesh_local is not None: 2747 cmd += 'localprefix %s ' % mesh_local 2748 2749 if network_name is not None: 2750 cmd += 'networkname %s ' % self._escape_escapable(network_name) 2751 2752 if security_policy is not None: 2753 cmd += 'securitypolicy %d %s ' % (security_policy[0], security_policy[1]) 2754 if (len(security_policy) >= 3): 2755 cmd += '%d ' % (security_policy[2]) 2756 2757 if binary is not None: 2758 cmd += '-x %s ' % binary 2759 2760 self.send_command(cmd) 2761 self._expect_done() 2762 2763 def send_mgmt_active_get(self, addr='', tlvs=[]): 2764 cmd = 'dataset mgmtgetcommand active' 2765 2766 if addr != '': 2767 cmd += ' address ' 2768 cmd += addr 2769 2770 if len(tlvs) != 0: 2771 tlv_str = ''.join('%02x' % tlv for tlv in tlvs) 2772 cmd += ' -x ' 2773 cmd += tlv_str 2774 2775 self.send_command(cmd) 2776 self._expect_done() 2777 2778 def send_mgmt_pending_get(self, addr='', tlvs=[]): 2779 cmd = 'dataset mgmtgetcommand pending' 2780 2781 if addr != '': 2782 cmd += ' address ' 2783 cmd += addr 2784 2785 if len(tlvs) != 0: 2786 tlv_str = ''.join('%02x' % tlv for tlv in tlvs) 2787 cmd += ' -x ' 2788 cmd += tlv_str 2789 2790 self.send_command(cmd) 2791 self._expect_done() 2792 2793 def send_mgmt_pending_set( 2794 self, 2795 pending_timestamp=None, 2796 active_timestamp=None, 2797 delay_timer=None, 2798 channel=None, 2799 panid=None, 2800 network_key=None, 2801 mesh_local=None, 2802 network_name=None, 2803 ): 2804 cmd = 'dataset mgmtsetcommand pending ' 2805 if pending_timestamp is not None: 2806 cmd += 'pendingtimestamp %d ' % pending_timestamp 2807 2808 if active_timestamp is not None: 2809 cmd += 'activetimestamp %d ' % active_timestamp 2810 2811 if delay_timer is not None: 2812 cmd += 'delaytimer %d ' % delay_timer 2813 2814 if channel is not None: 2815 cmd += 'channel %d ' % channel 2816 2817 if panid is not None: 2818 cmd += 'panid %d ' % panid 2819 2820 if network_key is not None: 2821 cmd += 'networkkey %s ' % network_key 2822 2823 if mesh_local is not None: 2824 cmd += 'localprefix %s ' % mesh_local 2825 2826 if network_name is not None: 2827 cmd += 'networkname %s ' % self._escape_escapable(network_name) 2828 2829 self.send_command(cmd) 2830 self._expect_done() 2831 2832 def coap_cancel(self): 2833 """ 2834 Cancel a CoAP subscription. 2835 """ 2836 cmd = 'coap cancel' 2837 self.send_command(cmd) 2838 self._expect_done() 2839 2840 def coap_delete(self, ipaddr, uri, con=False, payload=None): 2841 """ 2842 Send a DELETE request via CoAP. 2843 """ 2844 return self._coap_rq('delete', ipaddr, uri, con, payload) 2845 2846 def coap_get(self, ipaddr, uri, con=False, payload=None): 2847 """ 2848 Send a GET request via CoAP. 2849 """ 2850 return self._coap_rq('get', ipaddr, uri, con, payload) 2851 2852 def coap_get_block(self, ipaddr, uri, size=16, count=0): 2853 """ 2854 Send a GET request via CoAP. 2855 """ 2856 return self._coap_rq_block('get', ipaddr, uri, size, count) 2857 2858 def coap_observe(self, ipaddr, uri, con=False, payload=None): 2859 """ 2860 Send a GET request via CoAP with Observe set. 2861 """ 2862 return self._coap_rq('observe', ipaddr, uri, con, payload) 2863 2864 def coap_post(self, ipaddr, uri, con=False, payload=None): 2865 """ 2866 Send a POST request via CoAP. 2867 """ 2868 return self._coap_rq('post', ipaddr, uri, con, payload) 2869 2870 def coap_post_block(self, ipaddr, uri, size=16, count=0): 2871 """ 2872 Send a POST request via CoAP. 2873 """ 2874 return self._coap_rq_block('post', ipaddr, uri, size, count) 2875 2876 def coap_put(self, ipaddr, uri, con=False, payload=None): 2877 """ 2878 Send a PUT request via CoAP. 2879 """ 2880 return self._coap_rq('put', ipaddr, uri, con, payload) 2881 2882 def coap_put_block(self, ipaddr, uri, size=16, count=0): 2883 """ 2884 Send a PUT request via CoAP. 2885 """ 2886 return self._coap_rq_block('put', ipaddr, uri, size, count) 2887 2888 def _coap_rq(self, method, ipaddr, uri, con=False, payload=None): 2889 """ 2890 Issue a GET/POST/PUT/DELETE/GET OBSERVE request. 2891 """ 2892 cmd = 'coap %s %s %s' % (method, ipaddr, uri) 2893 if con: 2894 cmd += ' con' 2895 else: 2896 cmd += ' non' 2897 2898 if payload is not None: 2899 cmd += ' %s' % payload 2900 2901 self.send_command(cmd) 2902 return self.coap_wait_response() 2903 2904 def _coap_rq_block(self, method, ipaddr, uri, size=16, count=0): 2905 """ 2906 Issue a GET/POST/PUT/DELETE/GET OBSERVE BLOCK request. 2907 """ 2908 cmd = 'coap %s %s %s' % (method, ipaddr, uri) 2909 2910 cmd += ' block-%d' % size 2911 2912 if count != 0: 2913 cmd += ' %d' % count 2914 2915 self.send_command(cmd) 2916 return self.coap_wait_response() 2917 2918 def coap_wait_response(self): 2919 """ 2920 Wait for a CoAP response, and return it. 2921 """ 2922 if isinstance(self.simulator, simulator.VirtualTime): 2923 self.simulator.go(5) 2924 timeout = 1 2925 else: 2926 timeout = 5 2927 2928 self._expect(r'coap response from ([\da-f:]+)(?: OBS=(\d+))?' 2929 r'(?: with payload: ([\da-f]+))?\b', 2930 timeout=timeout) 2931 (source, observe, payload) = self.pexpect.match.groups() 2932 source = source.decode('UTF-8') 2933 2934 if observe is not None: 2935 observe = int(observe, base=10) 2936 2937 if payload is not None: 2938 try: 2939 payload = binascii.a2b_hex(payload).decode('UTF-8') 2940 except UnicodeDecodeError: 2941 pass 2942 2943 # Return the values received 2944 return dict(source=source, observe=observe, payload=payload) 2945 2946 def coap_wait_request(self): 2947 """ 2948 Wait for a CoAP request to be made. 2949 """ 2950 if isinstance(self.simulator, simulator.VirtualTime): 2951 self.simulator.go(5) 2952 timeout = 1 2953 else: 2954 timeout = 5 2955 2956 self._expect(r'coap request from ([\da-f:]+)(?: OBS=(\d+))?' 2957 r'(?: with payload: ([\da-f]+))?\b', 2958 timeout=timeout) 2959 (source, observe, payload) = self.pexpect.match.groups() 2960 source = source.decode('UTF-8') 2961 2962 if observe is not None: 2963 observe = int(observe, base=10) 2964 2965 if payload is not None: 2966 payload = binascii.a2b_hex(payload).decode('UTF-8') 2967 2968 # Return the values received 2969 return dict(source=source, observe=observe, payload=payload) 2970 2971 def coap_wait_subscribe(self): 2972 """ 2973 Wait for a CoAP client to be subscribed. 2974 """ 2975 if isinstance(self.simulator, simulator.VirtualTime): 2976 self.simulator.go(5) 2977 timeout = 1 2978 else: 2979 timeout = 5 2980 2981 self._expect(r'Subscribing client\b', timeout=timeout) 2982 2983 def coap_wait_ack(self): 2984 """ 2985 Wait for a CoAP notification ACK. 2986 """ 2987 if isinstance(self.simulator, simulator.VirtualTime): 2988 self.simulator.go(5) 2989 timeout = 1 2990 else: 2991 timeout = 5 2992 2993 self._expect(r'Received ACK in reply to notification from ([\da-f:]+)\b', timeout=timeout) 2994 (source,) = self.pexpect.match.groups() 2995 source = source.decode('UTF-8') 2996 2997 return source 2998 2999 def coap_set_resource_path(self, path): 3000 """ 3001 Set the path for the CoAP resource. 3002 """ 3003 cmd = 'coap resource %s' % path 3004 self.send_command(cmd) 3005 self._expect_done() 3006 3007 def coap_set_resource_path_block(self, path, count=0): 3008 """ 3009 Set the path for the CoAP resource and how many blocks can be received from this resource. 3010 """ 3011 cmd = 'coap resource %s %d' % (path, count) 3012 self.send_command(cmd) 3013 self._expect('Done') 3014 3015 def coap_set_content(self, content): 3016 """ 3017 Set the content of the CoAP resource. 3018 """ 3019 cmd = 'coap set %s' % content 3020 self.send_command(cmd) 3021 self._expect_done() 3022 3023 def coap_start(self): 3024 """ 3025 Start the CoAP service. 3026 """ 3027 cmd = 'coap start' 3028 self.send_command(cmd) 3029 self._expect_done() 3030 3031 def coap_stop(self): 3032 """ 3033 Stop the CoAP service. 3034 """ 3035 cmd = 'coap stop' 3036 self.send_command(cmd) 3037 3038 if isinstance(self.simulator, simulator.VirtualTime): 3039 self.simulator.go(5) 3040 timeout = 1 3041 else: 3042 timeout = 5 3043 3044 self._expect_done(timeout=timeout) 3045 3046 def coaps_start_psk(self, psk, pskIdentity): 3047 cmd = 'coaps psk %s %s' % (psk, pskIdentity) 3048 self.send_command(cmd) 3049 self._expect_done() 3050 3051 cmd = 'coaps start' 3052 self.send_command(cmd) 3053 self._expect_done() 3054 3055 def coaps_start_x509(self): 3056 cmd = 'coaps x509' 3057 self.send_command(cmd) 3058 self._expect_done() 3059 3060 cmd = 'coaps start' 3061 self.send_command(cmd) 3062 self._expect_done() 3063 3064 def coaps_set_resource_path(self, path): 3065 cmd = 'coaps resource %s' % path 3066 self.send_command(cmd) 3067 self._expect_done() 3068 3069 def coaps_stop(self): 3070 cmd = 'coaps stop' 3071 self.send_command(cmd) 3072 3073 if isinstance(self.simulator, simulator.VirtualTime): 3074 self.simulator.go(5) 3075 timeout = 1 3076 else: 3077 timeout = 5 3078 3079 self._expect_done(timeout=timeout) 3080 3081 def coaps_connect(self, ipaddr): 3082 cmd = 'coaps connect %s' % ipaddr 3083 self.send_command(cmd) 3084 3085 if isinstance(self.simulator, simulator.VirtualTime): 3086 self.simulator.go(5) 3087 timeout = 1 3088 else: 3089 timeout = 5 3090 3091 self._expect('coaps connected', timeout=timeout) 3092 3093 def coaps_disconnect(self): 3094 cmd = 'coaps disconnect' 3095 self.send_command(cmd) 3096 self._expect_done() 3097 self.simulator.go(5) 3098 3099 def coaps_get(self): 3100 cmd = 'coaps get test' 3101 self.send_command(cmd) 3102 3103 if isinstance(self.simulator, simulator.VirtualTime): 3104 self.simulator.go(5) 3105 timeout = 1 3106 else: 3107 timeout = 5 3108 3109 self._expect('coaps response', timeout=timeout) 3110 3111 def commissioner_mgmtget(self, tlvs_binary=None): 3112 cmd = 'commissioner mgmtget' 3113 if tlvs_binary is not None: 3114 cmd += ' -x %s' % tlvs_binary 3115 self.send_command(cmd) 3116 self._expect_done() 3117 3118 def commissioner_mgmtset(self, tlvs_binary): 3119 cmd = 'commissioner mgmtset -x %s' % tlvs_binary 3120 self.send_command(cmd) 3121 self._expect_done() 3122 3123 def bytes_to_hex_str(self, src): 3124 return ''.join(format(x, '02x') for x in src) 3125 3126 def commissioner_mgmtset_with_tlvs(self, tlvs): 3127 payload = bytearray() 3128 for tlv in tlvs: 3129 payload += tlv.to_hex() 3130 self.commissioner_mgmtset(self.bytes_to_hex_str(payload)) 3131 3132 def udp_start(self, local_ipaddr, local_port, bind_unspecified=False): 3133 cmd = 'udp open' 3134 self.send_command(cmd) 3135 self._expect_done() 3136 3137 cmd = 'udp bind %s %s %s' % ("-u" if bind_unspecified else "", local_ipaddr, local_port) 3138 self.send_command(cmd) 3139 self._expect_done() 3140 3141 def udp_stop(self): 3142 cmd = 'udp close' 3143 self.send_command(cmd) 3144 self._expect_done() 3145 3146 def udp_send(self, bytes, ipaddr, port, success=True): 3147 cmd = 'udp send %s %d -s %d ' % (ipaddr, port, bytes) 3148 self.send_command(cmd) 3149 if success: 3150 self._expect_done() 3151 else: 3152 self._expect('Error') 3153 3154 def udp_check_rx(self, bytes_should_rx): 3155 self._expect('%d bytes' % bytes_should_rx) 3156 3157 def set_routereligible(self, enable: bool): 3158 cmd = f'routereligible {"enable" if enable else "disable"}' 3159 self.send_command(cmd) 3160 self._expect_done() 3161 3162 def router_list(self): 3163 cmd = 'router list' 3164 self.send_command(cmd) 3165 self._expect([r'(\d+)((\s\d+)*)']) 3166 3167 g = self.pexpect.match.groups() 3168 router_list = g[0].decode('utf8') + ' ' + g[1].decode('utf8') 3169 router_list = [int(x) for x in router_list.split()] 3170 self._expect_done() 3171 return router_list 3172 3173 def router_table(self): 3174 cmd = 'router table' 3175 self.send_command(cmd) 3176 3177 self._expect(r'(.*)Done') 3178 g = self.pexpect.match.groups() 3179 output = g[0].decode('utf8') 3180 lines = output.strip().split('\n') 3181 lines = [l.strip() for l in lines] 3182 router_table = {} 3183 for i, line in enumerate(lines): 3184 if not line.startswith('|') or not line.endswith('|'): 3185 if i not in (0, 2): 3186 # should not happen 3187 print("unexpected line %d: %s" % (i, line)) 3188 3189 continue 3190 3191 line = line[1:][:-1] 3192 line = [x.strip() for x in line.split('|')] 3193 if len(line) < 9: 3194 print("unexpected line %d: %s" % (i, line)) 3195 continue 3196 3197 try: 3198 int(line[0]) 3199 except ValueError: 3200 if i != 1: 3201 print("unexpected line %d: %s" % (i, line)) 3202 continue 3203 3204 id = int(line[0]) 3205 rloc16 = int(line[1], 16) 3206 nexthop = int(line[2]) 3207 pathcost = int(line[3]) 3208 lqin = int(line[4]) 3209 lqout = int(line[5]) 3210 age = int(line[6]) 3211 emac = str(line[7]) 3212 link = int(line[8]) 3213 3214 router_table[id] = { 3215 'rloc16': rloc16, 3216 'nexthop': nexthop, 3217 'pathcost': pathcost, 3218 'lqin': lqin, 3219 'lqout': lqout, 3220 'age': age, 3221 'emac': emac, 3222 'link': link, 3223 } 3224 3225 return router_table 3226 3227 def link_metrics_query_single_probe(self, dst_addr: str, linkmetrics_flags: str, block: str = ""): 3228 cmd = 'linkmetrics query %s single %s %s' % (dst_addr, linkmetrics_flags, block) 3229 self.send_command(cmd) 3230 self.simulator.go(5) 3231 return self._parse_linkmetrics_query_result(self._expect_command_output()) 3232 3233 def link_metrics_query_forward_tracking_series(self, dst_addr: str, series_id: int, block: str = ""): 3234 cmd = 'linkmetrics query %s forward %d %s' % (dst_addr, series_id, block) 3235 self.send_command(cmd) 3236 self.simulator.go(5) 3237 return self._parse_linkmetrics_query_result(self._expect_command_output()) 3238 3239 def _parse_linkmetrics_query_result(self, lines): 3240 """Parse link metrics query result""" 3241 3242 # Example of command output: 3243 # ['Received Link Metrics Report from: fe80:0:0:0:146e:a00:0:1', 3244 # '- PDU Counter: 1 (Count/Summation)', 3245 # '- LQI: 0 (Exponential Moving Average)', 3246 # '- Margin: 80 (dB) (Exponential Moving Average)', 3247 # '- RSSI: -20 (dBm) (Exponential Moving Average)'] 3248 # 3249 # Or 'Link Metrics Report, status: {status}' 3250 3251 result = {} 3252 for line in lines: 3253 if line.startswith('- '): 3254 k, v = line[2:].split(': ') 3255 result[k] = v.split(' ')[0] 3256 elif line.startswith('Link Metrics Report, status: '): 3257 result['Status'] = line[29:] 3258 return result 3259 3260 def link_metrics_mgmt_req_enhanced_ack_based_probing(self, 3261 dst_addr: str, 3262 enable: bool, 3263 metrics_flags: str, 3264 ext_flags=''): 3265 cmd = "linkmetrics mgmt %s enhanced-ack" % (dst_addr) 3266 if enable: 3267 cmd = cmd + (" register %s %s" % (metrics_flags, ext_flags)) 3268 else: 3269 cmd = cmd + " clear" 3270 self.send_command(cmd) 3271 self._expect_done() 3272 3273 def link_metrics_mgmt_req_forward_tracking_series(self, dst_addr: str, series_id: int, series_flags: str, 3274 metrics_flags: str): 3275 cmd = "linkmetrics mgmt %s forward %d %s %s" % (dst_addr, series_id, series_flags, metrics_flags) 3276 self.send_command(cmd) 3277 self._expect_done() 3278 3279 def link_metrics_send_link_probe(self, dst_addr: str, series_id: int, length: int): 3280 cmd = "linkmetrics probe %s %d %d" % (dst_addr, series_id, length) 3281 self.send_command(cmd) 3282 self._expect_done() 3283 3284 def link_metrics_mgr_set_enabled(self, enable: bool): 3285 op_str = "enable" if enable else "disable" 3286 cmd = f'linkmetricsmgr {op_str}' 3287 self.send_command(cmd) 3288 self._expect_done() 3289 3290 def send_address_notification(self, dst: str, target: str, mliid: str): 3291 cmd = f'fake /a/an {dst} {target} {mliid}' 3292 self.send_command(cmd) 3293 self._expect_done() 3294 3295 def send_proactive_backbone_notification(self, target: str, mliid: str, ltt: int): 3296 cmd = f'fake /b/ba {target} {mliid} {ltt}' 3297 self.send_command(cmd) 3298 self._expect_done() 3299 3300 def dns_get_config(self): 3301 """ 3302 Returns the DNS config as a list of property dictionary (string key and string value). 3303 3304 Example output: 3305 { 3306 'Server': '[fd00:0:0:0:0:0:0:1]:1234' 3307 'ResponseTimeout': '5000 ms' 3308 'MaxTxAttempts': '2' 3309 'RecursionDesired': 'no' 3310 } 3311 """ 3312 cmd = f'dns config' 3313 self.send_command(cmd) 3314 output = self._expect_command_output() 3315 config = {} 3316 for line in output: 3317 k, v = line.split(': ') 3318 config[k] = v 3319 return config 3320 3321 def dns_set_config(self, config): 3322 cmd = f'dns config {config}' 3323 self.send_command(cmd) 3324 self._expect_done() 3325 3326 def dns_resolve(self, hostname, server=None, port=53): 3327 cmd = f'dns resolve {hostname}' 3328 if server is not None: 3329 cmd += f' {server} {port}' 3330 3331 self.send_command(cmd) 3332 self.simulator.go(10) 3333 output = self._expect_command_output() 3334 dns_resp = output[0] 3335 # example output: "DNS response for host1.default.service.arpa. - fd00:db8:0:0:fd3d:d471:1e8c:b60 TTL:7190 " 3336 # " fd00:db8:0:0:0:ff:fe00:9000 TTL:7190" 3337 addrs = dns_resp.strip().split(' - ')[1].split(' ') 3338 ip = [item.strip() for item in addrs[::2]] 3339 ttl = [int(item.split('TTL:')[1]) for item in addrs[1::2]] 3340 3341 return list(zip(ip, ttl)) 3342 3343 def _parse_dns_service_info(self, output): 3344 # Example of `output` 3345 # Port:22222, Priority:2, Weight:2, TTL:7155 3346 # Host:host2.default.service.arpa. 3347 # HostAddress:0:0:0:0:0:0:0:0 TTL:0 3348 # TXT:[a=00, b=02bb] TTL:7155 3349 3350 m = re.match( 3351 r'.*Port:(\d+), Priority:(\d+), Weight:(\d+), TTL:(\d+)\s+Host:(.*?)\s+HostAddress:(\S+) TTL:(\d+)\s+TXT:\[(.*?)\] TTL:(\d+)', 3352 '\r'.join(output)) 3353 if not m: 3354 return {} 3355 port, priority, weight, srv_ttl, hostname, address, aaaa_ttl, txt_data, txt_ttl = m.groups() 3356 return { 3357 'port': int(port), 3358 'priority': int(priority), 3359 'weight': int(weight), 3360 'host': hostname, 3361 'address': address, 3362 'txt_data': txt_data, 3363 'srv_ttl': int(srv_ttl), 3364 'txt_ttl': int(txt_ttl), 3365 'aaaa_ttl': int(aaaa_ttl), 3366 } 3367 3368 def dns_resolve_service(self, instance, service, server=None, port=53): 3369 """ 3370 Resolves the service instance and returns the instance information as a dict. 3371 3372 Example return value: 3373 { 3374 'port': 12345, 3375 'priority': 0, 3376 'weight': 0, 3377 'host': 'ins1._ipps._tcp.default.service.arpa.', 3378 'address': '2001::1', 3379 'txt_data': 'a=00, b=02bb', 3380 'srv_ttl': 7100, 3381 'txt_ttl': 7100, 3382 'aaaa_ttl': 7100, 3383 } 3384 """ 3385 instance = self._escape_escapable(instance) 3386 cmd = f'dns service {instance} {service}' 3387 if server is not None: 3388 cmd += f' {server} {port}' 3389 3390 self.send_command(cmd) 3391 self.simulator.go(10) 3392 output = self._expect_command_output() 3393 info = self._parse_dns_service_info(output) 3394 if not info: 3395 raise Exception('dns resolve service failed: %s.%s' % (instance, service)) 3396 return info 3397 3398 @staticmethod 3399 def __parse_hex_string(hexstr: str) -> bytes: 3400 assert (len(hexstr) % 2 == 0) 3401 return bytes(int(hexstr[i:i + 2], 16) for i in range(0, len(hexstr), 2)) 3402 3403 def dns_browse(self, service_name, server=None, port=53): 3404 """ 3405 Browse the service and returns the instances. 3406 3407 Example return value: 3408 { 3409 'ins1': { 3410 'port': 12345, 3411 'priority': 1, 3412 'weight': 1, 3413 'host': 'ins1._ipps._tcp.default.service.arpa.', 3414 'address': '2001::1', 3415 'txt_data': 'a=00, b=11cf', 3416 'srv_ttl': 7100, 3417 'txt_ttl': 7100, 3418 'aaaa_ttl': 7100, 3419 }, 3420 'ins2': { 3421 'port': 12345, 3422 'priority': 2, 3423 'weight': 2, 3424 'host': 'ins2._ipps._tcp.default.service.arpa.', 3425 'address': '2001::2', 3426 'txt_data': 'a=01, b=23dd', 3427 'srv_ttl': 7100, 3428 'txt_ttl': 7100, 3429 'aaaa_ttl': 7100, 3430 } 3431 } 3432 """ 3433 cmd = f'dns browse {service_name}' 3434 if server is not None: 3435 cmd += f' {server} {port}' 3436 3437 self.send_command(cmd) 3438 self.simulator.go(10) 3439 output = self._expect_command_output() 3440 3441 # Example output: 3442 # DNS browse response for _ipps._tcp.default.service.arpa. 3443 # ins2 3444 # Port:22222, Priority:2, Weight:2, TTL:7175 3445 # Host:host2.default.service.arpa. 3446 # HostAddress:fd00:db8:0:0:3205:28dd:5b87:6a63 TTL:7175 3447 # TXT:[a=00, b=11cf] TTL:7175 3448 # ins1 3449 # Port:11111, Priority:1, Weight:1, TTL:7170 3450 # Host:host1.default.service.arpa. 3451 # HostAddress:fd00:db8:0:0:39f4:d9:eb4f:778 TTL:7170 3452 # TXT:[a=01, b=23dd] TTL:7170 3453 # Done 3454 3455 result = {} 3456 index = 1 # skip first line 3457 while index < len(output): 3458 ins = output[index].strip() 3459 result[ins] = self._parse_dns_service_info(output[index + 1:index + 6]) 3460 index = index + (5 if result[ins] else 1) 3461 return result 3462 3463 def set_mliid(self, mliid: str): 3464 cmd = f'mliid {mliid}' 3465 self.send_command(cmd) 3466 self._expect_command_output() 3467 3468 def history_netinfo(self, num_entries=0): 3469 """ 3470 Get the `netinfo` history list, parse each entry and return 3471 a list of dictionary (string key and string value) entries. 3472 3473 Example of return value: 3474 [ 3475 { 3476 'age': '00:00:00.000 ago', 3477 'role': 'disabled', 3478 'mode': 'rdn', 3479 'rloc16': '0x7400', 3480 'partition-id': '1318093703' 3481 }, 3482 { 3483 'age': '00:00:02.588 ago', 3484 'role': 'leader', 3485 'mode': 'rdn', 3486 'rloc16': '0x7400', 3487 'partition-id': '1318093703' 3488 } 3489 ] 3490 """ 3491 cmd = f'history netinfo list {num_entries}' 3492 self.send_command(cmd) 3493 output = self._expect_command_output() 3494 netinfos = [] 3495 for entry in output: 3496 netinfo = {} 3497 age, info = entry.split(' -> ') 3498 netinfo['age'] = age 3499 for item in info.split(' '): 3500 k, v = item.split(':') 3501 netinfo[k] = v 3502 netinfos.append(netinfo) 3503 return netinfos 3504 3505 def history_rx(self, num_entries=0): 3506 """ 3507 Get the IPv6 RX history list, parse each entry and return 3508 a list of dictionary (string key and string value) entries. 3509 3510 Example of return value: 3511 [ 3512 { 3513 'age': '00:00:01.999', 3514 'type': 'ICMP6(EchoReqst)', 3515 'len': '16', 3516 'sec': 'yes', 3517 'prio': 'norm', 3518 'rss': '-20', 3519 'from': '0xac00', 3520 'radio': '15.4', 3521 'src': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0', 3522 'dst': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0', 3523 } 3524 ] 3525 """ 3526 cmd = f'history rx list {num_entries}' 3527 self.send_command(cmd) 3528 return self._parse_history_rx_tx_ouput(self._expect_command_output()) 3529 3530 def history_tx(self, num_entries=0): 3531 """ 3532 Get the IPv6 TX history list, parse each entry and return 3533 a list of dictionary (string key and string value) entries. 3534 3535 Example of return value: 3536 [ 3537 { 3538 'age': '00:00:01.999', 3539 'type': 'ICMP6(EchoReply)', 3540 'len': '16', 3541 'sec': 'yes', 3542 'prio': 'norm', 3543 'to': '0xac00', 3544 'tx-success': 'yes', 3545 'radio': '15.4', 3546 'src': '[fd00:db8:0:0:ed7e:2d04:e543:eba5]:0', 3547 'dst': '[fd00:db8:0:0:2cfa:fd61:58a9:f0aa]:0', 3548 3549 } 3550 ] 3551 """ 3552 cmd = f'history tx list {num_entries}' 3553 self.send_command(cmd) 3554 return self._parse_history_rx_tx_ouput(self._expect_command_output()) 3555 3556 def _parse_history_rx_tx_ouput(self, lines): 3557 rxtx_list = [] 3558 for line in lines: 3559 if line.strip().startswith('type:'): 3560 for item in line.strip().split(' '): 3561 k, v = item.split(':') 3562 entry[k] = v 3563 elif line.strip().startswith('src:'): 3564 entry['src'] = line[4:] 3565 elif line.strip().startswith('dst:'): 3566 entry['dst'] = line[4:] 3567 rxtx_list.append(entry) 3568 else: 3569 entry = {} 3570 entry['age'] = line 3571 3572 return rxtx_list 3573 3574 def set_router_id_range(self, min_router_id: int, max_router_id: int): 3575 cmd = f'routeridrange {min_router_id} {max_router_id}' 3576 self.send_command(cmd) 3577 self._expect_command_output() 3578 3579 def get_router_id_range(self): 3580 cmd = 'routeridrange' 3581 self.send_command(cmd) 3582 line = self._expect_command_output()[0] 3583 return [int(item) for item in line.split()] 3584 3585 3586class Node(NodeImpl, OtCli): 3587 pass 3588 3589 3590class LinuxHost(): 3591 PING_RESPONSE_PATTERN = re.compile(r'\d+ bytes from .*:.*') 3592 ETH_DEV = config.BACKBONE_IFNAME 3593 3594 def enable_ether(self): 3595 """Enable the ethernet interface. 3596 """ 3597 3598 self.bash(f'ip link set {self.ETH_DEV} up') 3599 3600 def disable_ether(self): 3601 """Disable the ethernet interface. 3602 """ 3603 3604 self.bash(f'ip link set {self.ETH_DEV} down') 3605 3606 def get_ether_addrs(self): 3607 output = self.bash(f'ip -6 addr list dev {self.ETH_DEV}') 3608 3609 addrs = [] 3610 for line in output: 3611 # line example: "inet6 fe80::42:c0ff:fea8:903/64 scope link" 3612 line = line.strip().split() 3613 3614 if line and line[0] == 'inet6': 3615 addr = line[1] 3616 if '/' in addr: 3617 addr = addr.split('/')[0] 3618 addrs.append(addr) 3619 3620 logging.debug('%s: get_ether_addrs: %r', self, addrs) 3621 return addrs 3622 3623 def get_ether_mac(self): 3624 output = self.bash(f'ip addr list dev {self.ETH_DEV}') 3625 for line in output: 3626 # link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 3627 line = line.strip().split() 3628 if line and line[0] == 'link/ether': 3629 return line[1] 3630 3631 assert False, output 3632 3633 def add_ipmaddr_ether(self, ip: str): 3634 cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/mcast6.py {self.ETH_DEV} {ip} &' 3635 self.bash(cmd) 3636 3637 def ping_ether(self, ipaddr, num_responses=1, size=None, timeout=5, ttl=None, interface='eth0') -> int: 3638 3639 cmd = f'ping -6 {ipaddr} -I {interface} -c {num_responses} -W {timeout}' 3640 if size is not None: 3641 cmd += f' -s {size}' 3642 3643 if ttl is not None: 3644 cmd += f' -t {ttl}' 3645 3646 resp_count = 0 3647 3648 try: 3649 for line in self.bash(cmd): 3650 if self.PING_RESPONSE_PATTERN.match(line): 3651 resp_count += 1 3652 except subprocess.CalledProcessError: 3653 pass 3654 3655 return resp_count 3656 3657 def get_ip6_address(self, address_type: config.ADDRESS_TYPE): 3658 """Get specific type of IPv6 address configured on thread device. 3659 3660 Args: 3661 address_type: the config.ADDRESS_TYPE type of IPv6 address. 3662 3663 Returns: 3664 IPv6 address string. 3665 """ 3666 if address_type == config.ADDRESS_TYPE.BACKBONE_GUA: 3667 return self._getBackboneGua() 3668 elif address_type == config.ADDRESS_TYPE.ONLINK_ULA: 3669 return self._getInfraUla() 3670 elif address_type == config.ADDRESS_TYPE.ONLINK_GUA: 3671 return self._getInfraGua() 3672 else: 3673 raise ValueError(f'unsupported address type: {address_type}') 3674 3675 def _getBackboneGua(self) -> Optional[str]: 3676 for addr in self.get_ether_addrs(): 3677 if re.match(config.BACKBONE_PREFIX_REGEX_PATTERN, addr, re.I): 3678 return addr 3679 3680 return None 3681 3682 def _getInfraUla(self) -> Optional[str]: 3683 """ Returns the ULA addresses autoconfigured on the infra link. 3684 """ 3685 addrs = [] 3686 for addr in self.get_ether_addrs(): 3687 if re.match(config.ONLINK_PREFIX_REGEX_PATTERN, addr, re.I): 3688 addrs.append(addr) 3689 3690 return addrs 3691 3692 def _getInfraGua(self) -> Optional[str]: 3693 """ Returns the GUA addresses autoconfigured on the infra link. 3694 """ 3695 3696 gua_prefix = config.ONLINK_GUA_PREFIX.split('::/')[0] 3697 return [addr for addr in self.get_ether_addrs() if addr.startswith(gua_prefix)] 3698 3699 def ping(self, *args, **kwargs): 3700 backbone = kwargs.pop('backbone', False) 3701 if backbone: 3702 return self.ping_ether(*args, **kwargs) 3703 else: 3704 return super().ping(*args, **kwargs) 3705 3706 def udp_send_host(self, ipaddr, port, data, hop_limit=None): 3707 if hop_limit is None: 3708 if ipaddress.ip_address(ipaddr).is_multicast: 3709 hop_limit = 10 3710 else: 3711 hop_limit = 64 3712 cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/udp_send_host.py {ipaddr} {port} "{data}" {hop_limit}' 3713 self.bash(cmd) 3714 3715 def add_ipmaddr(self, *args, **kwargs): 3716 backbone = kwargs.pop('backbone', False) 3717 if backbone: 3718 return self.add_ipmaddr_ether(*args, **kwargs) 3719 else: 3720 return super().add_ipmaddr(*args, **kwargs) 3721 3722 def ip_neighbors_flush(self): 3723 # clear neigh cache on linux 3724 self.bash(f'ip -6 neigh list dev {self.ETH_DEV}') 3725 self.bash(f'ip -6 neigh flush nud all nud failed nud noarp dev {self.ETH_DEV}') 3726 self.bash('ip -6 neigh list nud all dev %s | cut -d " " -f1 | sudo xargs -I{} ip -6 neigh delete {} dev %s' % 3727 (self.ETH_DEV, self.ETH_DEV)) 3728 self.bash(f'ip -6 neigh list dev {self.ETH_DEV}') 3729 3730 def publish_mdns_service(self, instance_name, service_type, port, host_name, txt): 3731 """Publish an mDNS service on the Ethernet. 3732 3733 :param instance_name: the service instance name. 3734 :param service_type: the service type in format of '<service_type>.<protocol>'. 3735 :param port: the port the service is at. 3736 :param host_name: the host name this service points to. The domain 3737 should not be included. 3738 :param txt: a dictionary containing the key-value pairs of the TXT record. 3739 """ 3740 txt_string = ' '.join([f'{key}={value}' for key, value in txt.items()]) 3741 self.bash(f'avahi-publish -s {instance_name} {service_type} {port} -H {host_name}.local {txt_string} &') 3742 3743 def publish_mdns_host(self, hostname, addresses): 3744 """Publish an mDNS host on the Ethernet 3745 3746 :param host_name: the host name this service points to. The domain 3747 should not be included. 3748 :param addresses: a list of strings representing the addresses to 3749 be registered with the host. 3750 """ 3751 for address in addresses: 3752 self.bash(f'avahi-publish -a {hostname}.local {address} &') 3753 3754 def browse_mdns_services(self, name, timeout=2): 3755 """ Browse mDNS services on the ethernet. 3756 3757 :param name: the service type name in format of '<service-name>.<protocol>'. 3758 :param timeout: timeout value in seconds before returning. 3759 :return: A list of service instance names. 3760 """ 3761 3762 self.bash(f'dns-sd -Z {name} local. > /tmp/{name} 2>&1 &') 3763 time.sleep(timeout) 3764 self.bash('pkill dns-sd') 3765 3766 instances = [] 3767 for line in self.bash(f'cat /tmp/{name}', encoding='raw_unicode_escape'): 3768 elements = line.split() 3769 if len(elements) >= 3 and elements[0] == name and elements[1] == 'PTR': 3770 instances.append(elements[2][:-len('.' + name)]) 3771 return instances 3772 3773 def discover_mdns_service(self, instance, name, host_name, timeout=2): 3774 """ Discover/resolve the mDNS service on ethernet. 3775 3776 :param instance: the service instance name. 3777 :param name: the service name in format of '<service-name>.<protocol>'. 3778 :param host_name: the host name this service points to. The domain 3779 should not be included. 3780 :param timeout: timeout value in seconds before returning. 3781 :return: a dict of service properties or None. 3782 3783 The return value is a dict with the same key/values of srp_server_get_service 3784 except that we don't have a `deleted` field here. 3785 """ 3786 host_name_file = self.bash('mktemp')[0].strip() 3787 service_data_file = self.bash('mktemp')[0].strip() 3788 3789 self.bash(f'dns-sd -Z {name} local. > {service_data_file} 2>&1 &') 3790 time.sleep(timeout) 3791 3792 full_service_name = f'{instance}.{name}' 3793 # When hostname is unspecified, extract hostname from browse result 3794 if host_name is None: 3795 for line in self.bash(f'cat {service_data_file}', encoding='raw_unicode_escape'): 3796 elements = line.split() 3797 if len(elements) >= 6 and elements[0] == full_service_name and elements[1] == 'SRV': 3798 host_name = elements[5].split('.')[0] 3799 break 3800 3801 assert (host_name is not None) 3802 self.bash(f'dns-sd -G v6 {host_name}.local. > {host_name_file} 2>&1 &') 3803 time.sleep(timeout) 3804 3805 self.bash('pkill dns-sd') 3806 addresses = [] 3807 service = {} 3808 3809 logging.debug(self.bash(f'cat {host_name_file}', encoding='raw_unicode_escape')) 3810 logging.debug(self.bash(f'cat {service_data_file}', encoding='raw_unicode_escape')) 3811 3812 # example output in the host file: 3813 # Timestamp A/R Flags if Hostname Address TTL 3814 # 9:38:09.274 Add 23 48 my-host.local. 2001:0000:0000:0000:0000:0000:0000:0002%<0> 120 3815 # 3816 for line in self.bash(f'cat {host_name_file}', encoding='raw_unicode_escape'): 3817 elements = line.split() 3818 fullname = f'{host_name}.local.' 3819 if fullname not in elements: 3820 continue 3821 if 'Add' not in elements: 3822 continue 3823 addresses.append(elements[elements.index(fullname) + 1].split('%')[0]) 3824 3825 logging.debug(f'addresses of {host_name}: {addresses}') 3826 3827 # example output of in the service file: 3828 # _ipps._tcp PTR my-service._ipps._tcp 3829 # my-service._ipps._tcp SRV 0 0 12345 my-host.local. ; Replace with unicast FQDN of target host 3830 # my-service._ipps._tcp TXT "" 3831 # 3832 is_txt = False 3833 txt = '' 3834 for line in self.bash(f'cat {service_data_file}', encoding='raw_unicode_escape'): 3835 elements = line.split() 3836 if len(elements) >= 2 and elements[0] == full_service_name and elements[1] == 'TXT': 3837 is_txt = True 3838 if is_txt: 3839 txt += line.strip() 3840 if line.strip().endswith('"'): 3841 is_txt = False 3842 txt_dict = self.__parse_dns_sd_txt(txt) 3843 logging.info(f'txt = {txt_dict}') 3844 service['txt'] = txt_dict 3845 3846 if not elements or elements[0] != full_service_name: 3847 continue 3848 if elements[1] == 'SRV': 3849 service['fullname'] = elements[0] 3850 service['instance'] = instance 3851 service['name'] = name 3852 service['priority'] = int(elements[2]) 3853 service['weight'] = int(elements[3]) 3854 service['port'] = int(elements[4]) 3855 service['host_fullname'] = elements[5] 3856 assert (service['host_fullname'] == f'{host_name}.local.') 3857 service['host'] = host_name 3858 service['addresses'] = addresses 3859 return service or None 3860 3861 def start_radvd_service(self, prefix, slaac): 3862 self.bash("""cat >/etc/radvd.conf <<EOF 3863interface eth0 3864{ 3865 AdvSendAdvert on; 3866 3867 AdvReachableTime 200; 3868 AdvRetransTimer 200; 3869 AdvDefaultLifetime 1800; 3870 MinRtrAdvInterval 1200; 3871 MaxRtrAdvInterval 1800; 3872 AdvDefaultPreference low; 3873 3874 prefix %s 3875 { 3876 AdvOnLink on; 3877 AdvAutonomous %s; 3878 AdvRouterAddr off; 3879 AdvPreferredLifetime 40; 3880 AdvValidLifetime 60; 3881 }; 3882}; 3883EOF 3884""" % (prefix, 'on' if slaac else 'off')) 3885 self.bash('service radvd start') 3886 self.bash('service radvd status') # Make sure radvd service is running 3887 3888 def stop_radvd_service(self): 3889 self.bash('service radvd stop') 3890 3891 def kill_radvd_service(self): 3892 self.bash('pkill radvd') 3893 3894 def __parse_dns_sd_txt(self, line: str): 3895 # Example TXT entry: 3896 # "xp=\\000\\013\\184\\000\\000\\000\\000\\000" 3897 txt = {} 3898 for entry in re.findall(r'"((?:[^\\]|\\.)*?)"', line): 3899 if '=' not in entry: 3900 continue 3901 3902 k, v = entry.split('=', 1) 3903 txt[k] = v 3904 3905 return txt 3906 3907 3908class OtbrNode(LinuxHost, NodeImpl, OtbrDocker): 3909 TUN_DEV = config.THREAD_IFNAME 3910 is_otbr = True 3911 is_bbr = True # OTBR is also BBR 3912 node_type = 'otbr-docker' 3913 3914 def __repr__(self): 3915 return f'Otbr<{self.nodeid}>' 3916 3917 def start(self): 3918 self._setup_sysctl() 3919 self.set_log_level(5) 3920 super().start() 3921 3922 def add_ipaddr(self, addr): 3923 cmd = f'ip -6 addr add {addr}/64 dev {self.TUN_DEV}' 3924 self.bash(cmd) 3925 3926 def add_ipmaddr_tun(self, ip: str): 3927 cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/mcast6.py {self.TUN_DEV} {ip} &' 3928 self.bash(cmd) 3929 3930 def get_ip6_address(self, address_type: config.ADDRESS_TYPE): 3931 try: 3932 return super(OtbrNode, self).get_ip6_address(address_type) 3933 except Exception as e: 3934 return super(LinuxHost, self).get_ip6_address(address_type) 3935 3936 3937class HostNode(LinuxHost, OtbrDocker): 3938 is_host = True 3939 3940 def __init__(self, nodeid, name=None, **kwargs): 3941 self.nodeid = nodeid 3942 self.name = name or ('Host%d' % nodeid) 3943 super().__init__(nodeid, **kwargs) 3944 self.bash('service otbr-agent stop') 3945 3946 def start(self, start_radvd=True, prefix=config.DOMAIN_PREFIX, slaac=False): 3947 self._setup_sysctl() 3948 if start_radvd: 3949 self.start_radvd_service(prefix, slaac) 3950 else: 3951 self.stop_radvd_service() 3952 3953 def stop(self): 3954 self.stop_radvd_service() 3955 3956 def get_addrs(self) -> List[str]: 3957 return self.get_ether_addrs() 3958 3959 def __repr__(self): 3960 return f'Host<{self.nodeid}>' 3961 3962 def get_matched_ula_addresses(self, prefix): 3963 """Get the IPv6 addresses that matches given prefix. 3964 """ 3965 3966 addrs = [] 3967 for addr in self.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA): 3968 if IPv6Address(addr) in IPv6Network(prefix): 3969 addrs.append(addr) 3970 3971 return addrs 3972 3973 3974if __name__ == '__main__': 3975 unittest.main() 3976