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