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