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