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