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