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