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