1#!/usr/bin/env python
2#
3# Copyright (c) 2020, 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>> Thread Host Controller Interface
30>> Device : OpenThread_BR THCI
31>> Class : OpenThread_BR
32"""
33import logging
34import re
35import sys
36import time
37import ipaddress
38
39import serial
40from IThci import IThci
41from THCI.OpenThread import OpenThreadTHCI, watched, API
42
43RPI_FULL_PROMPT = 'pi@raspberrypi:~$ '
44RPI_USERNAME_PROMPT = 'raspberrypi login: '
45RPI_PASSWORD_PROMPT = 'Password: '
46"""regex: used to split lines"""
47LINESEPX = re.compile(r'\r\n|\n')
48
49LOGX = re.compile(r'.*Under-voltage detected!')
50"""regex: used to filter logging"""
51
52assert LOGX.match('[57522.618196] Under-voltage detected! (0x00050005)')
53
54OTBR_AGENT_SYSLOG_PATTERN = re.compile(r'raspberrypi otbr-agent\[\d+\]: (.*)')
55assert OTBR_AGENT_SYSLOG_PATTERN.search(
56    'Jun 23 05:21:22 raspberrypi otbr-agent[323]: =========[[THCI] direction=send | type=JOIN_FIN.req | len=039]==========]'
57).group(1) == '=========[[THCI] direction=send | type=JOIN_FIN.req | len=039]==========]'
58
59logging.getLogger('paramiko').setLevel(logging.WARNING)
60
61
62class SSHHandle(object):
63    # Unit: second
64    KEEPALIVE_INTERVAL = 30
65
66    def __init__(self, ip, port, username, password):
67        self.ip = ip
68        self.port = int(port)
69        self.username = username
70        self.password = password
71        self.__handle = None
72
73        self.__connect()
74
75    def __connect(self):
76        import paramiko
77
78        self.close()
79
80        self.__handle = paramiko.SSHClient()
81        self.__handle.set_missing_host_key_policy(paramiko.AutoAddPolicy())
82        try:
83            self.__handle.connect(self.ip, port=self.port, username=self.username, password=self.password)
84        except paramiko.ssh_exception.AuthenticationException:
85            if not self.password:
86                self.__handle.get_transport().auth_none(self.username)
87            else:
88                raise
89
90        # Avoid SSH disconnection after idle for a long time
91        self.__handle.get_transport().set_keepalive(self.KEEPALIVE_INTERVAL)
92
93    def close(self):
94        if self.__handle is not None:
95            self.__handle.close()
96            self.__handle = None
97
98    def bash(self, cmd, timeout):
99        from paramiko import SSHException
100        retry = 3
101        for i in range(retry):
102            try:
103                stdin, stdout, stderr = self.__handle.exec_command(cmd, timeout=timeout)
104
105                sys.stderr.write(stderr.read())
106                output = [r.encode('utf8').rstrip('\r\n') for r in stdout.readlines()]
107                return output
108
109            except Exception:
110                if i < retry - 1:
111                    print('SSH connection is lost, try reconnect after 1 second.')
112                    time.sleep(1)
113                    self.__connect()
114                else:
115                    raise
116
117    def log(self, fmt, *args):
118        try:
119            msg = fmt % args
120            print('%s - %s - %s' % (self.port, time.strftime('%b %d %H:%M:%S'), msg))
121        except Exception:
122            pass
123
124
125class SerialHandle:
126
127    def __init__(self, port, baudrate):
128        self.port = port
129        self.__handle = serial.Serial(port, baudrate, timeout=0)
130
131        self.__lines = ['']
132        assert len(self.__lines) >= 1, self.__lines
133
134        self.log("inputing username ...")
135        self.__bashWriteLine('pi')
136        deadline = time.time() + 20
137        loginOk = False
138        while time.time() < deadline:
139            time.sleep(1)
140
141            lastLine = None
142            while True:
143                line = self.__bashReadLine(timeout=1)
144
145                if not line:
146                    break
147
148                lastLine = line
149
150            if lastLine == RPI_FULL_PROMPT:
151                self.log("prompt found, login success!")
152                loginOk = True
153                break
154
155            if lastLine == RPI_PASSWORD_PROMPT:
156                self.log("inputing password ...")
157                self.__bashWriteLine('raspberry')
158            elif lastLine == RPI_USERNAME_PROMPT:
159                self.log("inputing username ...")
160                self.__bashWriteLine('pi')
161            elif not lastLine:
162                self.log("inputing username ...")
163                self.__bashWriteLine('pi')
164
165        if not loginOk:
166            raise Exception('login fail')
167
168        self.bash('stty cols 256')
169
170    def log(self, fmt, *args):
171        try:
172            msg = fmt % args
173            print('%s - %s - %s' % (self.port, time.strftime('%b %d %H:%M:%S'), msg))
174        except Exception:
175            pass
176
177    def close(self):
178        self.__handle.close()
179
180    def bash(self, cmd, timeout=10):
181        """
182        Execute the command in bash.
183        """
184        self.__bashClearLines()
185        self.__bashWriteLine(cmd)
186        self.__bashExpect(cmd, timeout=timeout, endswith=True)
187
188        response = []
189
190        deadline = time.time() + timeout
191        while time.time() < deadline:
192            line = self.__bashReadLine()
193            if line is None:
194                time.sleep(0.01)
195                continue
196
197            if line == RPI_FULL_PROMPT:
198                # return response lines without prompt
199                return response
200
201            response.append(line)
202
203        self.__bashWrite('\x03')
204        raise Exception('%s: failed to find end of response' % self.port)
205
206    def __bashExpect(self, expected, timeout=20, endswith=False):
207        self.log('Expecting [%r]' % (expected))
208
209        deadline = time.time() + timeout
210        while time.time() < deadline:
211            line = self.__bashReadLine()
212            if line is None:
213                time.sleep(0.01)
214                continue
215
216            print('[%s] Got line [%r]' % (self.port, line))
217
218            if endswith:
219                matched = line.endswith(expected)
220            else:
221                matched = line == expected
222
223            if matched:
224                print('[%s] Expected [%r]' % (self.port, expected))
225                return
226
227        # failed to find the expected string
228        # send Ctrl+C to terminal
229        self.__bashWrite('\x03')
230        raise Exception('failed to find expected string[%s]' % expected)
231
232    def __bashRead(self, timeout=1):
233        deadline = time.time() + timeout
234        data = ''
235        while True:
236            piece = self.__handle.read()
237            data = data + piece.decode('utf8')
238            if piece:
239                continue
240
241            if data or time.time() >= deadline:
242                break
243
244        if data:
245            self.log('>>> %r', data)
246
247        return data
248
249    def __bashReadLine(self, timeout=1):
250        line = self.__bashGetNextLine()
251        if line is not None:
252            return line
253
254        assert len(self.__lines) == 1, self.__lines
255        tail = self.__lines.pop()
256
257        try:
258            tail += self.__bashRead(timeout=timeout)
259            tail = tail.replace(RPI_FULL_PROMPT, RPI_FULL_PROMPT + '\r\n')
260            tail = tail.replace(RPI_USERNAME_PROMPT, RPI_USERNAME_PROMPT + '\r\n')
261            tail = tail.replace(RPI_PASSWORD_PROMPT, RPI_PASSWORD_PROMPT + '\r\n')
262        finally:
263            self.__lines += [l.rstrip('\r') for l in LINESEPX.split(tail)]
264            assert len(self.__lines) >= 1, self.__lines
265
266        return self.__bashGetNextLine()
267
268    def __bashGetNextLine(self):
269        assert len(self.__lines) >= 1, self.__lines
270        while len(self.__lines) > 1:
271            line = self.__lines.pop(0)
272            assert len(self.__lines) >= 1, self.__lines
273            if LOGX.match(line):
274                logging.info('LOG: %s', line)
275                continue
276            else:
277                return line
278        assert len(self.__lines) >= 1, self.__lines
279        return None
280
281    def __bashWrite(self, data):
282        self.__handle.write(data)
283        self.log("<<< %r", data)
284
285    def __bashClearLines(self):
286        assert len(self.__lines) >= 1, self.__lines
287        while self.__bashReadLine(timeout=0) is not None:
288            pass
289        assert len(self.__lines) >= 1, self.__lines
290
291    def __bashWriteLine(self, line):
292        self.__bashWrite(line + '\n')
293
294
295class OpenThread_BR(OpenThreadTHCI, IThci):
296    DEFAULT_COMMAND_TIMEOUT = 20
297
298    IsBorderRouter = True
299    __is_root = False
300
301    def _getHandle(self):
302        if self.connectType == 'ip':
303            return SSHHandle(self.telnetIp, self.telnetPort, self.telnetUsername, self.telnetPassword)
304        else:
305            return SerialHandle(self.port, 115200)
306
307    def _connect(self):
308        self.log("logging in to Raspberry Pi ...")
309        self.__cli_output_lines = []
310        self.__syslog_skip_lines = None
311        self.__syslog_last_read_ts = 0
312
313        self.__handle = self._getHandle()
314        if self.connectType == 'ip':
315            self.__is_root = self.telnetUsername == 'root'
316
317    def _disconnect(self):
318        if self.__handle:
319            self.__handle.close()
320            self.__handle = None
321
322    def _deviceBeforeReset(self):
323        if self.isPowerDown:
324            self.log('Powering up the device')
325            self.powerUp()
326        if self.IsHost:
327            self.__stopRadvdService()
328            self.bash('ip -6 addr del 910b::1 dev %s || true' % self.backboneNetif)
329            self.bash('ip -6 addr del fd00:7d03:7d03:7d03::1 dev %s || true' % self.backboneNetif)
330
331        self.stopListeningToAddrAll()
332
333    def _deviceAfterReset(self):
334        self.__dumpSyslog()
335        self.__truncateSyslog()
336        self.__enableAcceptRa()
337        if not self.IsHost:
338            self._restartAgentService()
339            time.sleep(2)
340
341    def __enableAcceptRa(self):
342        self.bash('sysctl net.ipv6.conf.%s.accept_ra=2' % self.backboneNetif)
343
344    def _beforeRegisterMulticast(self, sAddr='ff04::1234:777a:1', timeout=300):
345        """subscribe to the given ipv6 address (sAddr) in interface and send MLR.req OTA
346
347        Args:
348            sAddr   : str : Multicast address to be subscribed and notified OTA.
349        """
350
351        if self.externalCommissioner is not None:
352            self.externalCommissioner.MLR([sAddr], timeout)
353            return True
354
355        cmd = 'nohup ~/repo/openthread/tests/scripts/thread-cert/mcast6.py wpan0 %s' % sAddr
356        cmd = cmd + ' > /dev/null 2>&1 &'
357        self.bash(cmd)
358
359    @API
360    def setupHost(self, setDp=False, setDua=False):
361        self.IsHost = True
362
363        self.bash('ip -6 addr add 910b::1 dev %s' % self.backboneNetif)
364
365        if setDua:
366            self.bash('ip -6 addr add fd00:7d03:7d03:7d03::1 dev %s' % self.backboneNetif)
367
368        self.__startRadvdService(setDp)
369
370    def _deviceEscapeEscapable(self, string):
371        """Escape CLI escapable characters in the given string.
372
373        Args:
374            string (str): UTF-8 input string.
375
376        Returns:
377            [str]: The modified string with escaped characters.
378        """
379        return '"' + string + '"'
380
381    @watched
382    def bash(self, cmd, timeout=DEFAULT_COMMAND_TIMEOUT, sudo=True):
383        return self.bash_unwatched(cmd, timeout=timeout, sudo=sudo)
384
385    def bash_unwatched(self, cmd, timeout=DEFAULT_COMMAND_TIMEOUT, sudo=True):
386        if sudo and not self.__is_root:
387            cmd = 'sudo ' + cmd
388
389        return self.__handle.bash(cmd, timeout=timeout)
390
391    # Override send_udp
392    @API
393    def send_udp(self, interface, dst, port, payload):
394        if interface == 0:  # Thread Interface
395            super(OpenThread_BR, self).send_udp(interface, dst, port, payload)
396            return
397
398        if interface == 1:
399            ifname = self.backboneNetif
400        else:
401            raise AssertionError('Invalid interface set to send UDP: {} '
402                                 'Available interface options: 0 - Thread; 1 - Ethernet'.format(interface))
403        cmd = '/home/pi/reference-device/send_udp.py %s %s %s %s' % (ifname, dst, port, payload)
404        self.bash(cmd)
405
406    @API
407    def mldv2_query(self):
408        ifname = self.backboneNetif
409        dst = 'ff02::1'
410
411        cmd = '/home/pi/reference-device/send_mld_query.py %s %s' % (ifname, dst)
412        self.bash(cmd)
413
414    @API
415    def ip_neighbors_flush(self):
416        # clear neigh cache on linux
417        cmd1 = 'sudo ip -6 neigh flush nud all nud failed nud noarp dev %s' % self.backboneNetif
418        cmd2 = ('sudo ip -6 neigh list nud all dev %s '
419                '| cut -d " " -f1 '
420                '| sudo xargs -I{} ip -6 neigh delete {} dev %s') % (self.backboneNetif, self.backboneNetif)
421        cmd = '%s ; %s' % (cmd1, cmd2)
422        self.bash(cmd, sudo=False)
423
424    @API
425    def ip_neighbors_add(self, addr, lladdr, nud='noarp'):
426        cmd1 = 'sudo ip -6 neigh delete %s dev %s' % (addr, self.backboneNetif)
427        cmd2 = 'sudo ip -6 neigh add %s dev %s lladdr %s nud %s' % (addr, self.backboneNetif, lladdr, nud)
428        cmd = '%s ; %s' % (cmd1, cmd2)
429        self.bash(cmd, sudo=False)
430
431    @API
432    def get_eth_ll(self):
433        cmd = "ip -6 addr list dev %s | grep 'inet6 fe80' | awk '{print $2}'" % self.backboneNetif
434        ret = self.bash(cmd)[0].split('/')[0]
435        return ret
436
437    @API
438    def ping(self, strDestination, ilength=0, hop_limit=5, timeout=5):
439        """ send ICMPv6 echo request with a given length to a unicast destination
440            address
441
442        Args:
443            strDestination: the unicast destination address of ICMPv6 echo request
444            ilength: the size of ICMPv6 echo request payload
445            hop_limit: the hop limit
446            timeout: time before ping() stops
447        """
448        if hop_limit is None:
449            hop_limit = 5
450
451        if self.IsHost or self.IsBorderRouter:
452            ifName = self.backboneNetif
453        else:
454            ifName = 'wpan0'
455
456        cmd = 'ping -6 -I %s %s -c 1 -s %d -W %d -t %d' % (
457            ifName,
458            strDestination,
459            int(ilength),
460            int(timeout),
461            int(hop_limit),
462        )
463
464        self.bash(cmd, sudo=False)
465        time.sleep(timeout)
466
467    def multicast_Ping(self, destination, length=20):
468        """send ICMPv6 echo request with a given length to a multicast destination
469           address
470
471        Args:
472            destination: the multicast destination address of ICMPv6 echo request
473            length: the size of ICMPv6 echo request payload
474        """
475        hop_limit = 5
476
477        if self.IsHost or self.IsBorderRouter:
478            ifName = self.backboneNetif
479        else:
480            ifName = 'wpan0'
481
482        cmd = 'ping -6 -I %s %s -c 1 -s %d -t %d' % (ifName, destination, str(length), hop_limit)
483
484        self.bash(cmd, sudo=False)
485
486    @API
487    def getGUA(self, filterByPrefix=None, eth=False):
488        """get expected global unicast IPv6 address of Thread device
489
490        note: existing filterByPrefix are string of in lowercase. e.g.
491        '2001' or '2001:0db8:0001:0000".
492
493        Args:
494            filterByPrefix: a given expected global IPv6 prefix to be matched
495
496        Returns:
497            a global IPv6 address
498        """
499        # get global addrs set if multiple
500        if eth:
501            return self.__getEthGUA(filterByPrefix=filterByPrefix)
502        else:
503            return super(OpenThread_BR, self).getGUA(filterByPrefix=filterByPrefix)
504
505    def __getEthGUA(self, filterByPrefix=None):
506        globalAddrs = []
507
508        cmd = 'ip -6 addr list dev %s | grep inet6' % self.backboneNetif
509        output = self.bash(cmd, sudo=False)
510        for line in output:
511            # example: inet6 2401:fa00:41:23:274a:1329:3ab9:d953/64 scope global dynamic noprefixroute
512            line = line.strip().split()
513
514            if len(line) < 4 or line[2] != 'scope':
515                continue
516
517            if line[3] != 'global':
518                continue
519
520            addr = line[1].split('/')[0]
521            addr = str(ipaddress.IPv6Address(addr.decode()).exploded)
522            globalAddrs.append(addr)
523
524        if not filterByPrefix:
525            return globalAddrs[0]
526        else:
527            if filterByPrefix[-2:] != '::':
528                filterByPrefix = '%s::' % filterByPrefix
529            prefix = ipaddress.IPv6Network((filterByPrefix + '/64').decode())
530            for fullIp in globalAddrs:
531                address = ipaddress.IPv6Address(fullIp.decode())
532                if address in prefix:
533                    return fullIp
534
535    def _cliReadLine(self):
536        # read commissioning log if it's commissioning
537        if not self.__cli_output_lines:
538            self.__readSyslogToCli()
539
540        if self.__cli_output_lines:
541            return self.__cli_output_lines.pop(0)
542
543        return None
544
545    @watched
546    def _deviceGetEtherMac(self):
547        # Harness wants it in string. Because wireshark filter for eth
548        # cannot be applies in hex
549        return self.bash('ip addr list dev %s | grep ether' % self.backboneNetif, sudo=False)[0].strip().split()[1]
550
551    @watched
552    def _onCommissionStart(self):
553        assert self.__syslog_skip_lines is None
554        self.__syslog_skip_lines = int(self.bash('wc -l /var/log/syslog', sudo=False)[0].split()[0])
555        self.__syslog_last_read_ts = 0
556
557    @watched
558    def _onCommissionStop(self):
559        assert self.__syslog_skip_lines is not None
560        self.__syslog_skip_lines = None
561
562    @watched
563    def __startRadvdService(self, setDp=False):
564        assert self.IsHost, "radvd service runs on Host only"
565
566        conf = "EOF"
567        conf += "\ninterface %s" % self.backboneNetif
568        conf += "\n{"
569        conf += "\n    AdvSendAdvert on;"
570        conf += "\n"
571        conf += "\n    MinRtrAdvInterval 3;"
572        conf += "\n    MaxRtrAdvInterval 30;"
573        conf += "\n    AdvDefaultPreference low;"
574        conf += "\n"
575        conf += "\n    prefix 910b::/64"
576        conf += "\n    {"
577        conf += "\n        AdvOnLink on;"
578        conf += "\n        AdvAutonomous on;"
579        conf += "\n        AdvRouterAddr on;"
580        conf += "\n    };"
581        if setDp:
582            conf += "\n"
583            conf += "\n    prefix fd00:7d03:7d03:7d03::/64"
584            conf += "\n    {"
585            conf += "\n        AdvOnLink on;"
586            conf += "\n        AdvAutonomous off;"
587            conf += "\n        AdvRouterAddr off;"
588            conf += "\n    };"
589        conf += "\n};"
590        conf += "\nEOF"
591        cmd = 'sh -c "cat >/etc/radvd.conf <<%s"' % conf
592
593        self.bash(cmd)
594        self.bash(self.extraParams.get('cmd-restart-radvd', 'service radvd restart'))
595        self.bash('service radvd status')
596
597    @watched
598    def __stopRadvdService(self):
599        assert self.IsHost, "radvd service runs on Host only"
600        self.bash('service radvd stop')
601
602    def __readSyslogToCli(self):
603        if self.__syslog_skip_lines is None:
604            return 0
605
606        # read syslog once per second
607        if time.time() < self.__syslog_last_read_ts + 1:
608            return 0
609
610        self.__syslog_last_read_ts = time.time()
611
612        lines = self.bash_unwatched('tail +%d /var/log/syslog' % self.__syslog_skip_lines, sudo=False)
613        for line in lines:
614            m = OTBR_AGENT_SYSLOG_PATTERN.search(line)
615            if not m:
616                continue
617
618            self.__cli_output_lines.append(m.group(1))
619
620        self.__syslog_skip_lines += len(lines)
621        return len(lines)
622
623    def _cliWriteLine(self, line):
624        cmd = 'ot-ctl -- %s' % line
625        output = self.bash(cmd)
626        # fake the line echo back
627        self.__cli_output_lines.append(line)
628        for line in output:
629            self.__cli_output_lines.append(line)
630
631    def _restartAgentService(self):
632        restart_cmd = self.extraParams.get('cmd-restart-otbr-agent', 'systemctl restart otbr-agent')
633        self.bash(restart_cmd)
634
635    def __truncateSyslog(self):
636        self.bash('truncate -s 0 /var/log/syslog')
637
638    def __dumpSyslog(self):
639        cmd = self.extraParams.get('cmd-dump-otbr-log', 'grep "otbr-agent" /var/log/syslog')
640        output = self.bash_unwatched(cmd)
641        for line in output:
642            self.log('%s', line)
643
644    @API
645    def get_eth_addrs(self):
646        cmd = "ip -6 addr list dev %s | grep 'inet6 ' | awk '{print $2}'" % self.backboneNetif
647        addrs = self.bash(cmd)
648        return [addr.split('/')[0] for addr in addrs]
649
650    @API
651    def mdns_query(self, service='_meshcop._udp.local', addrs_allowlist=(), addrs_denylist=()):
652        try:
653            for deny_addr in addrs_denylist:
654                self.bash('ip6tables -A INPUT -p udp --dport 5353 -s %s -j DROP' % deny_addr)
655
656            if addrs_allowlist:
657                for allow_addr in addrs_allowlist:
658                    self.bash('ip6tables -A INPUT -p udp --dport 5353 -s %s -j ACCEPT' % allow_addr)
659
660                self.bash('ip6tables -A INPUT -p udp --dport 5353 -j DROP')
661
662            return self._mdns_query_impl(service, find_active=(addrs_allowlist or addrs_denylist))
663
664        finally:
665            self.bash('ip6tables -F INPUT')
666            time.sleep(1)
667
668    def _mdns_query_impl(self, service, find_active):
669        # For BBR-TC-03 or DH test cases (empty arguments) just send a query
670        output = self.bash('python3 ~/repo/openthread/tests/scripts/thread-cert/find_border_agents.py')
671
672        if not find_active:
673            return
674
675        # For MATN-TC-17 and MATN-TC-18 use Zeroconf to get the BBR address and border agent port
676        for line in output:
677            print(line)
678            alias, addr, port, thread_status = eval(line)
679            if thread_status == 2 and addr:
680                if ipaddress.IPv6Address(addr.decode()).is_link_local:
681                    addr = '%s%%%s' % (addr, self.backboneNetif)
682                return addr, port
683
684        raise Exception('No active Border Agents found')
685
686    # Override powerDown
687    @API
688    def powerDown(self):
689        self.log('Powering down BBR')
690        super(OpenThread_BR, self).powerDown()
691        stop_cmd = self.extraParams.get('cmd-stop-otbr-agent', 'systemctl stop otbr-agent')
692        self.bash(stop_cmd)
693
694    # Override powerUp
695    @API
696    def powerUp(self):
697        self.log('Powering up BBR')
698        start_cmd = self.extraParams.get('cmd-start-otbr-agent', 'systemctl start otbr-agent')
699        self.bash(start_cmd)
700        super(OpenThread_BR, self).powerUp()
701
702    # Override forceSetSlaac
703    @API
704    def forceSetSlaac(self, slaacAddress):
705        self.bash('ip -6 addr add %s/64 dev wpan0' % slaacAddress)
706
707    # Override stopListeningToAddr
708    @API
709    def stopListeningToAddr(self, sAddr):
710        """
711        Unsubscribe to a given IPv6 address which was subscribed earlier wiht `registerMulticast`.
712
713        Args:
714            sAddr   : str : Multicast address to be unsubscribed. Use an empty string to unsubscribe
715                            all the active multicast addresses.
716        """
717        cmd = 'pkill -f mcast6.*%s' % sAddr
718        self.bash(cmd)
719
720    def stopListeningToAddrAll(self):
721        return self.stopListeningToAddr('')
722
723    @API
724    def deregisterMulticast(self, sAddr):
725        """
726        Unsubscribe to a given IPv6 address.
727        Only used by External Commissioner.
728
729        Args:
730            sAddr   : str : Multicast address to be unsubscribed.
731        """
732        self.externalCommissioner.MLR([sAddr], 0)
733        return True
734
735    @watched
736    def _waitBorderRoutingStabilize(self):
737        """
738        Wait for Network Data to stabilize if BORDER_ROUTING is enabled.
739        """
740        if not self.isBorderRoutingEnabled():
741            return
742
743        MAX_TIMEOUT = 30
744        MIN_TIMEOUT = 15
745        CHECK_INTERVAL = 3
746
747        time.sleep(MIN_TIMEOUT)
748
749        lastNetData = self.getNetworkData()
750        for i in range((MAX_TIMEOUT - MIN_TIMEOUT) // CHECK_INTERVAL):
751            time.sleep(CHECK_INTERVAL)
752            curNetData = self.getNetworkData()
753
754            # Wait until the Network Data is not changing, and there is OMR Prefix and External Routes available
755            if curNetData == lastNetData and len(curNetData['Prefixes']) > 0 and len(curNetData['Routes']) > 0:
756                break
757
758            lastNetData = curNetData
759
760        return lastNetData
761