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