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