• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#  Copyright (c) 2021, 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 sys
31import os
32import time
33import re
34import random
35import string
36import subprocess
37import pexpect
38import pexpect.popen_spawn
39import signal
40import inspect
41import weakref
42
43# ----------------------------------------------------------------------------------------------------------------------
44# Constants
45
46JOIN_TYPE_ROUTER = 'router'
47JOIN_TYPE_END_DEVICE = 'ed'
48JOIN_TYPE_SLEEPY_END_DEVICE = 'sed'
49JOIN_TYPE_REED = 'reed'
50
51# for use as `radios` parameter in `Node.__init__()`
52RADIO_15_4 = "-15.4"
53RADIO_TREL = "-trel"
54RADIO_15_4_TREL = "-15.4-trel"
55
56# ----------------------------------------------------------------------------------------------------------------------
57
58
59def _log(text, new_line=True, flush=True):
60    sys.stdout.write(text)
61    if new_line:
62        sys.stdout.write('\n')
63    if flush:
64        sys.stdout.flush()
65
66
67# ----------------------------------------------------------------------------------------------------------------------
68# CliError class
69
70
71class CliError(Exception):
72
73    def __init__(self, error_code, message):
74        self._error_code = error_code
75        self._message = message
76
77    @property
78    def error_code(self):
79        return self._error_code
80
81    @property
82    def message(self):
83        return self._message
84
85
86# ----------------------------------------------------------------------------------------------------------------------
87# Node class
88
89
90class Node(object):
91    """ An OT CLI instance """
92
93    # defines the default verbosity setting (can be changed per `Node`)
94    _VERBOSE = os.getenv('TORANJ_VERBOSE', 'no').lower() in ['true', '1', 't', 'y', 'yes', 'on']
95
96    _SPEED_UP_FACTOR = 1  # defines the default time speed up factor
97
98    # Determine whether to save logs in a file.
99    _SAVE_LOGS = True
100
101    # name of  log file (if _SAVE_LOGS is `True`)
102    _LOG_FNAME = 'ot-logs'
103
104    _OT_BUILDDIR = os.getenv('top_builddir', '../../..')
105
106    _OT_CLI_FTD = '%s/examples/apps/cli/ot-cli-ftd' % _OT_BUILDDIR
107
108    _WAIT_TIME = 10
109
110    _START_INDEX = 1
111    _cur_index = _START_INDEX
112
113    _all_nodes = weakref.WeakSet()
114
115    def __init__(self, radios='', index=None, verbose=_VERBOSE):
116        """Creates a new `Node` instance"""
117
118        if index is None:
119            index = Node._cur_index
120            Node._cur_index += 1
121
122        self._index = index
123        self._verbose = verbose
124
125        cmd = f'{self._OT_CLI_FTD}{radios} --time-speed={self._SPEED_UP_FACTOR} '
126
127        if Node._SAVE_LOGS:
128            log_file_name = self._LOG_FNAME + str(index) + '.log'
129            cmd = cmd + f'--log-file={log_file_name} '
130
131        cmd = cmd + f'{self._index}'
132
133        if self._verbose:
134            _log(f'$ Node{index}.__init__() cmd: `{cmd}`')
135
136        self._cli_process = pexpect.popen_spawn.PopenSpawn(cmd)
137        Node._all_nodes.add(self)
138
139    def __del__(self):
140        self._finalize()
141
142    def __repr__(self):
143        return f'Node(index={self._index})'
144
145    @property
146    def index(self):
147        return self._index
148
149    # ------------------------------------------------------------------------------------------------------------------
150    # Executing a `cli` command
151
152    def cli(self, *args):
153        """ Issues a CLI command on the given node and returns the resulting output.
154
155            The returned result is a list of strings (with `\r\n` removed) as outputted by the CLI.
156            If executing the command fails, `CliError` is raised with error code and error message.
157        """
158
159        cmd = ' '.join([f'{arg}' for arg in args if arg is not None]).strip()
160
161        if self._verbose:
162            _log(f'$ Node{self._index}.cli(\'{cmd}\')', new_line=False)
163
164        self._cli_process.send(cmd + '\n')
165        index = self._cli_process.expect(['(.*)Done\r\n', '.*Error (\d+):(.*)\r\n'])
166
167        if index == 0:
168            result = [
169                line for line in self._cli_process.match.group(1).decode().splitlines()
170                if not self._is_ot_logg_line(line) if not line.strip().endswith(cmd)
171            ]
172
173            if self._verbose:
174                if len(result) > 1:
175                    _log(':')
176                    for line in result:
177                        _log('     ' + line)
178                elif len(result) == 1:
179                    _log(f' -> {result[0]}')
180                else:
181                    _log('')
182
183            return result
184        else:
185            match = self._cli_process.match
186            e = CliError(int(match.group(1).decode()), match.group(2).decode().strip())
187            if self._verbose:
188                _log(f': Error {e.message} ({e.error_code})')
189            raise e
190
191    def _is_ot_logg_line(self, line):
192        return any(level in line for level in [' [D] ', ' [I] ', ' [N] ', ' [W] ', ' [C] ', ' [-] '])
193
194    def _cli_no_output(self, cmd, *args):
195        outputs = self.cli(cmd, *args)
196        verify(len(outputs) == 0)
197
198    def _cli_single_output(self, cmd, *args, expected_outputs=None):
199        outputs = self.cli(cmd, *args)
200        verify(len(outputs) == 1)
201        verify((expected_outputs is None) or (outputs[0] in expected_outputs))
202        return outputs[0]
203
204    def _finalize(self):
205        if self._cli_process.proc.poll() is None:
206            if self._verbose:
207                _log(f'$ Node{self.index} terminating')
208            self._cli_process.send('exit\n')
209            self._cli_process.wait()
210
211    # ------------------------------------------------------------------------------------------------------------------
212    # cli commands
213
214    def get_state(self):
215        return self._cli_single_output('state', expected_outputs=['detached', 'child', 'router', 'leader', 'disabled'])
216
217    def get_version(self):
218        return self._cli_single_output('version')
219
220    def get_channel(self):
221        return self._cli_single_output('channel')
222
223    def set_channel(self, channel):
224        self._cli_no_output('channel', channel)
225
226    def get_csl_config(self):
227        outputs = self.cli('csl')
228        result = {}
229        for line in outputs:
230            fields = line.split(':')
231            result[fields[0].strip()] = fields[1].strip()
232        return result
233
234    def set_csl_period(self, period):
235        self._cli_no_output('csl period', period)
236
237    def get_ext_addr(self):
238        return self._cli_single_output('extaddr')
239
240    def set_ext_addr(self, ext_addr):
241        self._cli_no_output('extaddr', ext_addr)
242
243    def get_ext_panid(self):
244        return self._cli_single_output('extpanid')
245
246    def set_ext_panid(self, ext_panid):
247        self._cli_no_output('extpanid', ext_panid)
248
249    def get_mode(self):
250        return self._cli_single_output('mode')
251
252    def set_mode(self, mode):
253        self._cli_no_output('mode', mode)
254
255    def get_network_key(self):
256        return self._cli_single_output('networkkey')
257
258    def set_network_key(self, networkkey):
259        self._cli_no_output('networkkey', networkkey)
260
261    def get_network_name(self):
262        return self._cli_single_output('networkname')
263
264    def set_network_name(self, network_name):
265        self._cli_no_output('networkname', network_name)
266
267    def get_panid(self):
268        return self._cli_single_output('panid')
269
270    def set_panid(self, panid):
271        self._cli_no_output('panid', panid)
272
273    def get_router_upgrade_threshold(self):
274        return self._cli_single_output('routerupgradethreshold')
275
276    def set_router_upgrade_threshold(self, threshold):
277        self._cli_no_output('routerupgradethreshold', threshold)
278
279    def get_router_selection_jitter(self):
280        return self._cli_single_output('routerselectionjitter')
281
282    def set_router_selection_jitter(self, jitter):
283        self._cli_no_output('routerselectionjitter', jitter)
284
285    def get_router_eligible(self):
286        return self._cli_single_output('routereligible')
287
288    def set_router_eligible(self, enable):
289        self._cli_no_output('routereligible', enable)
290
291    def get_context_reuse_delay(self):
292        return self._cli_single_output('contextreusedelay')
293
294    def set_context_reuse_delay(self, delay):
295        self._cli_no_output('contextreusedelay', delay)
296
297    def interface_up(self):
298        self._cli_no_output('ifconfig up')
299
300    def interface_down(self):
301        self._cli_no_output('ifconfig down')
302
303    def get_interface_state(self):
304        return self._cli_single_output('ifconfig')
305
306    def thread_start(self):
307        self._cli_no_output('thread start')
308
309    def thread_stop(self):
310        self._cli_no_output('thread stop')
311
312    def get_rloc16(self):
313        return self._cli_single_output('rloc16')
314
315    def get_mac_alt_short_addr(self):
316        return self._cli_single_output('mac altshortaddr')
317
318    def get_ip_addrs(self, verbose=None):
319        return self.cli('ipaddr', verbose)
320
321    def add_ip_addr(self, address):
322        self._cli_no_output('ipaddr add', address)
323
324    def remove_ip_addr(self, address):
325        self._cli_no_output('ipaddr del', address)
326
327    def get_mleid_ip_addr(self):
328        return self._cli_single_output('ipaddr mleid')
329
330    def get_linklocal_ip_addr(self):
331        return self._cli_single_output('ipaddr linklocal')
332
333    def get_rloc_ip_addr(self):
334        return self._cli_single_output('ipaddr rloc')
335
336    def get_mesh_local_prefix(self):
337        return self._cli_single_output('prefix meshlocal')
338
339    def get_ip_maddrs(self):
340        return self.cli('ipmaddr')
341
342    def add_ip_maddr(self, maddr):
343        return self._cli_no_output('ipmaddr add', maddr)
344
345    def get_leader_weight(self):
346        return self._cli_single_output('leaderweight')
347
348    def set_leader_weight(self, weight):
349        self._cli_no_output('leaderweight', weight)
350
351    def get_pollperiod(self):
352        return self._cli_single_output('pollperiod')
353
354    def set_pollperiod(self, period):
355        self._cli_no_output('pollperiod', period)
356
357    def get_child_timeout(self):
358        return self._cli_single_output('childtimeout')
359
360    def set_child_timeout(self, timeout):
361        self._cli_no_output('childtimeout', timeout)
362
363    def get_partition_id(self):
364        return self._cli_single_output('partitionid')
365
366    def get_nexthop(self, rloc16):
367        return self._cli_single_output('nexthop', rloc16)
368
369    def get_child_max(self):
370        return self._cli_single_output('childmax')
371
372    def set_child_max(self, childmax):
373        self._cli_no_output('childmax', childmax)
374
375    def get_parent_info(self):
376        outputs = self.cli('parent')
377        result = {}
378        for line in outputs:
379            fields = line.split(':')
380            result[fields[0].strip()] = fields[1].strip()
381        return result
382
383    def get_child_table(self):
384        return Node.parse_table(self.cli('child table'))
385
386    def get_child_ip(self):
387        return self.cli('childip')
388
389    def get_neighbor_table(self):
390        return Node.parse_table(self.cli('neighbor table'))
391
392    def get_router_table(self):
393        return Node.parse_table(self.cli('router table'))
394
395    def get_eidcache(self):
396        return self.cli('eidcache')
397
398    def get_vendor_name(self):
399        return self._cli_single_output('vendor name')
400
401    def set_vendor_name(self, name):
402        self._cli_no_output('vendor name', name)
403
404    def get_vendor_model(self):
405        return self._cli_single_output('vendor model')
406
407    def set_vendor_model(self, model):
408        self._cli_no_output('vendor model', model)
409
410    def get_vendor_sw_version(self):
411        return self._cli_single_output('vendor swversion')
412
413    def set_vendor_sw_version(self, version):
414        return self._cli_no_output('vendor swversion', version)
415
416    def get_vendor_app_url(self):
417        return self._cli_single_output('vendor appurl')
418
419    def set_vendor_app_url(self, url):
420        return self._cli_no_output('vendor appurl', url)
421
422    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
423    # netdata
424
425    def get_netdata(self, rloc16=None):
426        outputs = self.cli('netdata show', rloc16)
427        outputs = [line.strip() for line in outputs]
428        routes_index = outputs.index('Routes:')
429        services_index = outputs.index('Services:')
430        if rloc16 is None:
431            contexts_index = outputs.index('Contexts:')
432            commissioning_index = outputs.index('Commissioning:')
433        result = {}
434        result['prefixes'] = outputs[1:routes_index]
435        result['routes'] = outputs[routes_index + 1:services_index]
436        if rloc16 is None:
437            result['services'] = outputs[services_index + 1:contexts_index]
438            result['contexts'] = outputs[contexts_index + 1:commissioning_index]
439            result['commissioning'] = outputs[commissioning_index + 1:]
440        else:
441            result['services'] = outputs[services_index + 1:]
442
443        return result
444
445    def get_netdata_prefixes(self):
446        return self.get_netdata()['prefixes']
447
448    def get_netdata_routes(self):
449        return self.get_netdata()['routes']
450
451    def get_netdata_services(self):
452        return self.get_netdata()['services']
453
454    def get_netdata_contexts(self):
455        return self.get_netdata()['contexts']
456
457    def get_netdata_versions(self):
458        leaderdata = Node.parse_list(self.cli('leaderdata'))
459        return (int(leaderdata['Data Version']), int(leaderdata['Stable Data Version']))
460
461    def get_netdata_length(self):
462        return self._cli_single_output('netdata length')
463
464    def add_prefix(self, prefix, flags=None, prf=None):
465        return self._cli_no_output('prefix add', prefix, flags, prf)
466
467    def add_route(self, prefix, flags=None, prf=None):
468        return self._cli_no_output('route add', prefix, flags, prf)
469
470    def remove_prefix(self, prefix):
471        return self._cli_no_output('prefix remove', prefix)
472
473    def register_netdata(self):
474        self._cli_no_output('netdata register')
475
476    def get_netdata_full(self):
477        return self._cli_single_output('netdata full')
478
479    def reset_netdata_full(self):
480        self._cli_no_output('netdata full reset')
481
482    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
483    # ping and counters
484
485    def ping(self, address, size=0, count=1, verify_success=True):
486        outputs = self.cli('ping', address, size, count)
487        m = re.match(r'(\d+) packets transmitted, (\d+) packets received.', outputs[-1].strip())
488        verify(m is not None)
489        verify(int(m.group(1)) == count)
490        if verify_success:
491            verify(int(m.group(2)) == count)
492
493    def get_mle_counter(self):
494        return self.cli('counters mle')
495
496    def get_ip_counters(self):
497        return Node.parse_list(self.cli('counters ip'))
498
499    def get_mac_counters(self):
500        return Node.parse_list(self.cli('counters mac'))
501
502    def get_br_counter_unicast_outbound_packets(self):
503        outputs = self.cli('counters br')
504        for line in outputs:
505            m = re.match(r'Outbound Unicast: Packets (\d+) Bytes (\d+)', line.strip())
506            if m is not None:
507                counter = int(m.group(1))
508                break
509        else:
510            verify(False)
511        return counter
512
513    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
514    # Misc
515
516    def get_mle_adv_imax(self):
517        return self._cli_single_output('mleadvimax')
518
519    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
520    # Border Agent
521
522    def ba_get_state(self):
523        return self._cli_single_output('ba state')
524
525    def ba_get_port(self):
526        return self._cli_single_output('ba port')
527
528    def ba_ephemeral_key_get_state(self):
529        return self._cli_single_output('ba ephemeralkey')
530
531    def ba_ephemeral_key_set_enabled(self, enable):
532        self._cli_no_output('ba ephemeralkey', 'enable' if enable else 'disable')
533
534    def ba_ephemeral_key_start(self, keystring, timeout=None, port=None):
535        self._cli_no_output('ba ephemeralkey start', keystring, timeout, port)
536
537    def ba_ephemeral_key_stop(self):
538        self._cli_no_output('ba ephemeralkey stop')
539
540    def ba_ephemeral_key_get_port(self):
541        return self._cli_single_output('ba ephemeralkey port')
542
543    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
544    # UDP
545
546    def udp_open(self):
547        self._cli_no_output('udp open')
548
549    def udp_close(self):
550        self._cli_no_output('udp close')
551
552    def udp_bind(self, address, port):
553        self._cli_no_output('udp bind', address, port)
554
555    def udp_send(self, address, port, text):
556        self._cli_no_output('udp send', address, port, '-t', text)
557
558    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
559    # multiradio
560
561    def multiradio_get_radios(self):
562        return self._cli_single_output('multiradio')
563
564    def multiradio_get_neighbor_list(self):
565        return self.cli('multiradio neighbor list')
566
567    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
568    # SRP client
569
570    def srp_client_start(self, server_address, server_port):
571        self._cli_no_output('srp client start', server_address, server_port)
572
573    def srp_client_stop(self):
574        self._cli_no_output('srp client stop')
575
576    def srp_client_get_state(self):
577        return self._cli_single_output('srp client state', expected_outputs=['Enabled', 'Disabled'])
578
579    def srp_client_get_auto_start_mode(self):
580        return self._cli_single_output('srp client autostart', expected_outputs=['Enabled', 'Disabled'])
581
582    def srp_client_enable_auto_start_mode(self):
583        self._cli_no_output('srp client autostart enable')
584
585    def srp_client_disable_auto_start_mode(self):
586        self._cli_no_output('srp client autostart disable')
587
588    def srp_client_get_server_address(self):
589        return self._cli_single_output('srp client server address')
590
591    def srp_client_get_server_port(self):
592        return self._cli_single_output('srp client server port')
593
594    def srp_client_get_host_state(self):
595        return self._cli_single_output('srp client host state')
596
597    def srp_client_set_host_name(self, name):
598        self._cli_no_output('srp client host name', name)
599
600    def srp_client_get_host_name(self):
601        return self._cli_single_output('srp client host name')
602
603    def srp_client_remove_host(self, remove_key=False, send_unreg_to_server=False):
604        self._cli_no_output('srp client host remove', int(remove_key), int(send_unreg_to_server))
605
606    def srp_client_clear_host(self):
607        self._cli_no_output('srp client host clear')
608
609    def srp_client_enable_auto_host_address(self):
610        self._cli_no_output('srp client host address auto')
611
612    def srp_client_set_host_address(self, *addrs):
613        self._cli_no_output('srp client host address', *addrs)
614
615    def srp_client_get_host_address(self):
616        return self.cli('srp client host address')
617
618    def srp_client_add_service(self,
619                               instance_name,
620                               service_name,
621                               port,
622                               priority=0,
623                               weight=0,
624                               txt_entries=[],
625                               lease=0,
626                               key_lease=0):
627        txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries)
628        self._cli_no_output('srp client service add', instance_name, service_name, port, priority, weight, txt_record,
629                            lease, key_lease)
630
631    def srp_client_remove_service(self, instance_name, service_name):
632        self._cli_no_output('srp client service remove', instance_name, service_name)
633
634    def srp_client_clear_service(self, instance_name, service_name):
635        self._cli_no_output('srp client service clear', instance_name, service_name)
636
637    def srp_client_get_services(self):
638        outputs = self.cli('srp client service')
639        return [self._parse_srp_client_service(line) for line in outputs]
640
641    def _encode_txt_entry(self, entry):
642        """Encodes the TXT entry to the DNS-SD TXT record format as a HEX string.
643
644           Example usage:
645           self._encode_txt_entries(['abc'])     -> '03616263'
646           self._encode_txt_entries(['def='])    -> '046465663d'
647           self._encode_txt_entries(['xyz=XYZ']) -> '0778797a3d58595a'
648        """
649        return '{:02x}'.format(len(entry)) + "".join("{:02x}".format(ord(c)) for c in entry)
650
651    def _parse_srp_client_service(self, line):
652        """Parse one line of srp service list into a dictionary which
653           maps string keys to string values.
654
655           Example output for input
656           'instance:\"%s\", name:\"%s\", state:%s, port:%d, priority:%d, weight:%d"'
657           {
658               'instance': 'my-service',
659               'name': '_ipps._udp',
660               'state': 'ToAdd',
661               'port': '12345',
662               'priority': '0',
663               'weight': '0'
664           }
665
666           Note that value of 'port', 'priority' and 'weight' are represented
667           as strings but not integers.
668        """
669        key_values = [word.strip().split(':') for word in line.split(', ')]
670        return {key_value[0].strip(): key_value[1].strip('"') for key_value in key_values}
671
672    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
673    # SRP server
674
675    def srp_server_get_state(self):
676        return self._cli_single_output('srp server state', expected_outputs=['disabled', 'running', 'stopped'])
677
678    def srp_server_get_addr_mode(self):
679        return self._cli_single_output('srp server addrmode',
680                                       expected_outputs=['unicast', 'anycast', 'unicast-force-add'])
681
682    def srp_server_set_addr_mode(self, mode):
683        self._cli_no_output('srp server addrmode', mode)
684
685    def srp_server_get_anycast_seq_num(self):
686        return self._cli_single_output('srp server seqnum')
687
688    def srp_server_set_anycast_seq_num(self, seqnum):
689        self._cli_no_output('srp server seqnum', seqnum)
690
691    def srp_server_enable(self):
692        self._cli_no_output('srp server enable')
693
694    def srp_server_disable(self):
695        self._cli_no_output('srp server disable')
696
697    def srp_server_auto_enable(self):
698        self._cli_no_output('srp server auto enable')
699
700    def srp_server_auto_disable(self):
701        self._cli_no_output('srp server auto disable')
702
703    def srp_server_set_lease(self, min_lease, max_lease, min_key_lease, max_key_lease):
704        self._cli_no_output('srp server lease', min_lease, max_lease, min_key_lease, max_key_lease)
705
706    def srp_server_get_hosts(self):
707        """Returns the host list on the SRP server as a list of property
708           dictionary.
709
710           Example output:
711           [{
712               'fullname': 'my-host.default.service.arpa.',
713               'name': 'my-host',
714               'deleted': 'false',
715               'addresses': ['2001::1', '2001::2']
716           }]
717        """
718        outputs = self.cli('srp server host')
719        host_list = []
720        while outputs:
721            host = {}
722            host['fullname'] = outputs.pop(0).strip()
723            host['name'] = host['fullname'].split('.')[0]
724            host['deleted'] = outputs.pop(0).strip().split(':')[1].strip()
725            if host['deleted'] == 'true':
726                host_list.append(host)
727                continue
728            addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',')
729            map(str.strip, addresses)
730            host['addresses'] = [addr for addr in addresses if addr]
731            host_list.append(host)
732        return host_list
733
734    def srp_server_get_host(self, host_name):
735        """Returns host on the SRP server that matches given host name.
736
737           Example usage:
738           self.srp_server_get_host("my-host")
739        """
740        for host in self.srp_server_get_hosts():
741            if host_name == host['name']:
742                return host
743
744    def srp_server_get_services(self):
745        """Returns the service list on the SRP server as a list of property
746           dictionary.
747
748           Example output:
749           [{
750               'fullname': 'my-service._ipps._tcp.default.service.arpa.',
751               'instance': 'my-service',
752               'name': '_ipps._tcp',
753               'deleted': 'false',
754               'port': '12345',
755               'priority': '0',
756               'weight': '0',
757               'ttl': '7200',
758               'lease': '7200',
759               'key-lease', '1209600',
760               'TXT': ['abc=010203'],
761               'host_fullname': 'my-host.default.service.arpa.',
762               'host': 'my-host',
763               'addresses': ['2001::1', '2001::2']
764           }]
765
766           Note that the TXT data is output as a HEX string.
767        """
768        outputs = self.cli('srp server service')
769        service_list = []
770        while outputs:
771            service = {}
772            service['fullname'] = outputs.pop(0).strip()
773            name_labels = service['fullname'].split('.')
774            service['instance'] = name_labels[0]
775            service['name'] = '.'.join(name_labels[1:3])
776            service['deleted'] = outputs.pop(0).strip().split(':')[1].strip()
777            if service['deleted'] == 'true':
778                service_list.append(service)
779                continue
780            # 'subtypes', port', 'priority', 'weight', 'ttl', 'lease', 'key-lease'
781            for i in range(0, 7):
782                key_value = outputs.pop(0).strip().split(':')
783                service[key_value[0].strip()] = key_value[1].strip()
784            txt_entries = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',')
785            txt_entries = map(str.strip, txt_entries)
786            service['TXT'] = [txt for txt in txt_entries if txt]
787            service['host_fullname'] = outputs.pop(0).strip().split(':')[1].strip()
788            service['host'] = service['host_fullname'].split('.')[0]
789            addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',')
790            addresses = map(str.strip, addresses)
791            service['addresses'] = [addr for addr in addresses if addr]
792            service_list.append(service)
793        return service_list
794
795    def srp_server_get_service(self, instance_name, service_name):
796        """Returns service on the SRP server that matches given instance
797           name and service name.
798
799           Example usage:
800           self.srp_server_get_service("my-service", "_ipps._tcp")
801        """
802        for service in self.srp_server_get_services():
803            if (instance_name == service['instance'] and service_name == service['name']):
804                return service
805
806    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
807    # br
808
809    def br_init(self, if_inex, is_running):
810        self._cli_no_output('br init', if_inex, is_running)
811
812    def br_enable(self):
813        self._cli_no_output('br enable')
814
815    def br_disable(self):
816        self._cli_no_output('br disable')
817
818    def br_get_state(self):
819        return self._cli_single_output('br state')
820
821    def br_get_favored_omrprefix(self):
822        return self._cli_single_output('br omrprefix favored')
823
824    def br_get_local_omrprefix(self):
825        return self._cli_single_output('br omrprefix local')
826
827    def br_get_favored_onlinkprefix(self):
828        return self._cli_single_output('br onlinkprefix favored')
829
830    def br_get_local_onlinkprefix(self):
831        return self._cli_single_output('br onlinkprefix local')
832
833    def br_set_test_local_onlinkprefix(self, prefix):
834        self._cli_no_output('br onlinkprefix test', prefix)
835
836    def br_get_routeprf(self):
837        return self._cli_single_output('br routeprf')
838
839    def br_set_routeprf(self, prf):
840        self._cli_no_output('br routeprf', prf)
841
842    def br_clear_routeprf(self):
843        self._cli_no_output('br routeprf clear')
844
845    def br_get_routers(self):
846        return self.cli('br routers')
847
848    def br_get_peer_brs(self):
849        return self.cli('br peers')
850
851    def br_count_peers(self):
852        return self._cli_single_output('br peers count')
853
854    #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
855    # trel
856
857    def trel_get_peers(self):
858        peers = self.cli('trel peers ')
859        return Node.parse_table(peers)
860
861    def trel_test_get_sock_addr(self):
862        return self._cli_single_output('treltest sockaddr')
863
864    def trel_test_change_sock_addr(self):
865        return self._cli_no_output('treltest changesockaddr')
866
867    def trel_test_change_sock_port(self):
868        return self._cli_no_output('treltest changesockport')
869
870    def trel_test_get_notify_addr_counter(self):
871        return self._cli_single_output('treltest notifyaddrcounter')
872
873    # ------------------------------------------------------------------------------------------------------------------
874    # Helper methods
875
876    def form(self, network_name=None, network_key=None, channel=None, panid=0x1234, xpanid=None):
877        self._cli_no_output('dataset init new')
878        self._cli_no_output('dataset panid', panid)
879        if network_name is not None:
880            self._cli_no_output('dataset networkname', network_name)
881        if network_key is not None:
882            self._cli_no_output('dataset networkkey', network_key)
883        if channel is not None:
884            self._cli_no_output('dataset channel', channel)
885        if xpanid is not None:
886            self._cli_no_output('dataset extpanid', xpanid)
887        self._cli_no_output('dataset commit active')
888        self.set_mode('rdn')
889        self.interface_up()
890        self.thread_start()
891        verify_within(_check_node_is_leader, self._WAIT_TIME, arg=self)
892
893    def join(self, node, type=JOIN_TYPE_ROUTER):
894        self._cli_no_output('dataset clear')
895        self._cli_no_output('dataset networkname', node.get_network_name())
896        self._cli_no_output('dataset networkkey', node.get_network_key())
897        self._cli_no_output('dataset channel', node.get_channel())
898        self._cli_no_output('dataset panid', node.get_panid())
899        self._cli_no_output('dataset commit active')
900        if type == JOIN_TYPE_END_DEVICE:
901            self.set_mode('rn')
902        elif type == JOIN_TYPE_SLEEPY_END_DEVICE:
903            self.set_mode('-')
904        elif type == JOIN_TYPE_REED:
905            self.set_mode('rdn')
906            self.set_router_eligible('disable')
907        else:
908            self.set_mode('rdn')
909            self.set_router_selection_jitter(1)
910        self.interface_up()
911        self.thread_start()
912        if type == JOIN_TYPE_ROUTER:
913            verify_within(_check_node_is_router, self._WAIT_TIME, arg=self)
914        else:
915            verify_within(_check_node_is_child, self._WAIT_TIME, arg=self)
916
917    def allowlist_node(self, node):
918        """Adds a given node to the allowlist of `self` and enables allowlisting on `self`"""
919        self._cli_no_output('macfilter addr add', node.get_ext_addr())
920        self._cli_no_output('macfilter addr allowlist')
921
922    def un_allowlist_node(self, node):
923        """Removes a given node (of node `Node) from the allowlist"""
924        self._cli_no_output('macfilter addr remove', node.get_ext_addr())
925
926    def denylist_node(self, node):
927        """Adds a given node to the denylist of `self` and enables denylisting on `self`"""
928        self._cli_no_output('macfilter addr add', node.get_ext_addr())
929        self._cli_no_output('macfilter addr denylist')
930
931    def un_denylist_node(self, node):
932        """Removes a given node (of node `Node) from the denylist"""
933        self._cli_no_output('macfilter addr remove', node.get_ext_addr())
934
935    def set_macfilter_lqi_to_node(self, node, lqi):
936        self._cli_no_output('macfilter rss add-lqi', node.get_ext_addr(), lqi)
937
938    # ------------------------------------------------------------------------------------------------------------------
939    # Radio nodeidfilter
940
941    def nodeidfilter_clear(self, node):
942        self._cli_no_output('nodeidfilter clear')
943
944    def nodeidfilter_allow(self, node):
945        self._cli_no_output('nodeidfilter allow', node.index)
946
947    def nodeidfilter_deny(self, node):
948        self._cli_no_output('nodeidfilter deny', node.index)
949
950    # ------------------------------------------------------------------------------------------------------------------
951    # Parsing helpers
952
953    @classmethod
954    def parse_table(cls, table_lines):
955        verify(len(table_lines) >= 2)
956        headers = cls.split_table_row(table_lines[0])
957        info = []
958        for row in table_lines[2:]:
959            if row.strip() == '':
960                continue
961            fields = cls.split_table_row(row)
962            verify(len(fields) == len(headers))
963            info.append({headers[i]: fields[i] for i in range(len(fields))})
964        return info
965
966    @classmethod
967    def split_table_row(cls, row):
968        return [field.strip() for field in row.strip().split('|')[1:-1]]
969
970    @classmethod
971    def parse_list(cls, list_lines):
972        result = {}
973        for line in list_lines:
974            fields = line.split(':', 1)
975            result[fields[0].strip()] = fields[1].strip()
976        return result
977
978    @classmethod
979    def parse_multiradio_neighbor_entry(cls, line):
980        # Example: "ExtAddr:42aa94ad67229f14, RLOC16:0x9400, Radios:[15.4(245), TREL(255)]"
981        result = {}
982        for field in line.split(', ', 2):
983            key_value = field.split(':')
984            result[key_value[0]] = key_value[1]
985        radios = {}
986        for item in result['Radios'][1:-1].split(','):
987            name, prf = item.strip().split('(')
988            verify(prf.endswith(')'))
989            radios[name] = int(prf[:-1])
990        result['Radios'] = radios
991        return result
992
993    # ------------------------------------------------------------------------------------------------------------------
994    # class methods
995
996    @classmethod
997    def finalize_all_nodes(cls):
998        """Finalizes all previously created `Node` instances (stops the CLI process)"""
999        for node in Node._all_nodes:
1000            node._finalize()
1001
1002    @classmethod
1003    def set_time_speedup_factor(cls, factor):
1004        """Sets up the time speed up factor - should be set before creating any `Node` objects"""
1005        if len(Node._all_nodes) != 0:
1006            raise Node._NodeError('set_time_speedup_factor() cannot be called after creating a `Node`')
1007        Node._SPEED_UP_FACTOR = factor
1008
1009
1010def _check_node_is_leader(node):
1011    verify(node.get_state() == 'leader')
1012
1013
1014def _check_node_is_router(node):
1015    verify(node.get_state() == 'router')
1016
1017
1018def _check_node_is_child(node):
1019    verify(node.get_state() == 'child')
1020
1021
1022# ----------------------------------------------------------------------------------------------------------------------
1023
1024
1025class VerifyError(Exception):
1026    pass
1027
1028
1029_is_in_verify_within = False
1030
1031
1032def verify(condition):
1033    """Verifies that a `condition` is true, otherwise raises a VerifyError"""
1034    global _is_in_verify_within
1035    if not condition:
1036        calling_frame = inspect.currentframe().f_back
1037        error_message = 'verify() failed at line {} in "{}"'.format(calling_frame.f_lineno,
1038                                                                    calling_frame.f_code.co_filename)
1039        if not _is_in_verify_within:
1040            print(error_message)
1041        raise VerifyError(error_message)
1042
1043
1044def verify_within(condition_checker_func, wait_time, arg=None, delay_time=0.1):
1045    """Verifies that a given function `condition_checker_func` passes successfully within a given wait timeout.
1046       `wait_time` is maximum time waiting for condition_checker to pass (in seconds).
1047       `arg` is optional parameter and if it s not None, will be passed to `condition_checker_func()`
1048       `delay_time` specifies a delay interval added between failed attempts (in seconds).
1049    """
1050    global _is_in_verify_within
1051    start_time = time.time()
1052    old_is_in_verify_within = _is_in_verify_within
1053    _is_in_verify_within = True
1054    while True:
1055        try:
1056            if arg is None:
1057                condition_checker_func()
1058            else:
1059                condition_checker_func(arg)
1060        except VerifyError as e:
1061            if time.time() - start_time > wait_time:
1062                print('Took too long to pass the condition ({}>{} sec)'.format(time.time() - start_time, wait_time))
1063                if hasattr(e, 'message'):
1064                    print(e.message)
1065                raise e
1066        except BaseException:
1067            raise
1068        else:
1069            break
1070        if delay_time != 0:
1071            time.sleep(delay_time)
1072    _is_in_verify_within = old_is_in_verify_within
1073