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_is_ephemeral_key_active(self): 529 return self._cli_single_output('ba ephemeralkey') 530 531 def ba_set_ephemeral_key(self, keystring, timeout=None, port=None): 532 self._cli_no_output('ba ephemeralkey set', keystring, timeout, port) 533 534 def ba_clear_ephemeral_key(self): 535 self._cli_no_output('ba ephemeralkey clear') 536 537 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 538 # UDP 539 540 def udp_open(self): 541 self._cli_no_output('udp open') 542 543 def udp_close(self): 544 self._cli_no_output('udp close') 545 546 def udp_bind(self, address, port): 547 self._cli_no_output('udp bind', address, port) 548 549 def udp_send(self, address, port, text): 550 self._cli_no_output('udp send', address, port, '-t', text) 551 552 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 553 # multiradio 554 555 def multiradio_get_radios(self): 556 return self._cli_single_output('multiradio') 557 558 def multiradio_get_neighbor_list(self): 559 return self.cli('multiradio neighbor list') 560 561 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 562 # SRP client 563 564 def srp_client_start(self, server_address, server_port): 565 self._cli_no_output('srp client start', server_address, server_port) 566 567 def srp_client_stop(self): 568 self._cli_no_output('srp client stop') 569 570 def srp_client_get_state(self): 571 return self._cli_single_output('srp client state', expected_outputs=['Enabled', 'Disabled']) 572 573 def srp_client_get_auto_start_mode(self): 574 return self._cli_single_output('srp client autostart', expected_outputs=['Enabled', 'Disabled']) 575 576 def srp_client_enable_auto_start_mode(self): 577 self._cli_no_output('srp client autostart enable') 578 579 def srp_client_disable_auto_start_mode(self): 580 self._cli_no_output('srp client autostart disable') 581 582 def srp_client_get_server_address(self): 583 return self._cli_single_output('srp client server address') 584 585 def srp_client_get_server_port(self): 586 return self._cli_single_output('srp client server port') 587 588 def srp_client_get_host_state(self): 589 return self._cli_single_output('srp client host state') 590 591 def srp_client_set_host_name(self, name): 592 self._cli_no_output('srp client host name', name) 593 594 def srp_client_get_host_name(self): 595 return self._cli_single_output('srp client host name') 596 597 def srp_client_remove_host(self, remove_key=False, send_unreg_to_server=False): 598 self._cli_no_output('srp client host remove', int(remove_key), int(send_unreg_to_server)) 599 600 def srp_client_clear_host(self): 601 self._cli_no_output('srp client host clear') 602 603 def srp_client_enable_auto_host_address(self): 604 self._cli_no_output('srp client host address auto') 605 606 def srp_client_set_host_address(self, *addrs): 607 self._cli_no_output('srp client host address', *addrs) 608 609 def srp_client_get_host_address(self): 610 return self.cli('srp client host address') 611 612 def srp_client_add_service(self, 613 instance_name, 614 service_name, 615 port, 616 priority=0, 617 weight=0, 618 txt_entries=[], 619 lease=0, 620 key_lease=0): 621 txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries) 622 self._cli_no_output('srp client service add', instance_name, service_name, port, priority, weight, txt_record, 623 lease, key_lease) 624 625 def srp_client_remove_service(self, instance_name, service_name): 626 self._cli_no_output('srp client service remove', instance_name, service_name) 627 628 def srp_client_clear_service(self, instance_name, service_name): 629 self._cli_no_output('srp client service clear', instance_name, service_name) 630 631 def srp_client_get_services(self): 632 outputs = self.cli('srp client service') 633 return [self._parse_srp_client_service(line) for line in outputs] 634 635 def _encode_txt_entry(self, entry): 636 """Encodes the TXT entry to the DNS-SD TXT record format as a HEX string. 637 638 Example usage: 639 self._encode_txt_entries(['abc']) -> '03616263' 640 self._encode_txt_entries(['def=']) -> '046465663d' 641 self._encode_txt_entries(['xyz=XYZ']) -> '0778797a3d58595a' 642 """ 643 return '{:02x}'.format(len(entry)) + "".join("{:02x}".format(ord(c)) for c in entry) 644 645 def _parse_srp_client_service(self, line): 646 """Parse one line of srp service list into a dictionary which 647 maps string keys to string values. 648 649 Example output for input 650 'instance:\"%s\", name:\"%s\", state:%s, port:%d, priority:%d, weight:%d"' 651 { 652 'instance': 'my-service', 653 'name': '_ipps._udp', 654 'state': 'ToAdd', 655 'port': '12345', 656 'priority': '0', 657 'weight': '0' 658 } 659 660 Note that value of 'port', 'priority' and 'weight' are represented 661 as strings but not integers. 662 """ 663 key_values = [word.strip().split(':') for word in line.split(', ')] 664 return {key_value[0].strip(): key_value[1].strip('"') for key_value in key_values} 665 666 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 667 # SRP server 668 669 def srp_server_get_state(self): 670 return self._cli_single_output('srp server state', expected_outputs=['disabled', 'running', 'stopped']) 671 672 def srp_server_get_addr_mode(self): 673 return self._cli_single_output('srp server addrmode', expected_outputs=['unicast', 'anycast']) 674 675 def srp_server_set_addr_mode(self, mode): 676 self._cli_no_output('srp server addrmode', mode) 677 678 def srp_server_get_anycast_seq_num(self): 679 return self._cli_single_output('srp server seqnum') 680 681 def srp_server_set_anycast_seq_num(self, seqnum): 682 self._cli_no_output('srp server seqnum', seqnum) 683 684 def srp_server_enable(self): 685 self._cli_no_output('srp server enable') 686 687 def srp_server_disable(self): 688 self._cli_no_output('srp server disable') 689 690 def srp_server_auto_enable(self): 691 self._cli_no_output('srp server auto enable') 692 693 def srp_server_auto_disable(self): 694 self._cli_no_output('srp server auto disable') 695 696 def srp_server_set_lease(self, min_lease, max_lease, min_key_lease, max_key_lease): 697 self._cli_no_output('srp server lease', min_lease, max_lease, min_key_lease, max_key_lease) 698 699 def srp_server_get_hosts(self): 700 """Returns the host list on the SRP server as a list of property 701 dictionary. 702 703 Example output: 704 [{ 705 'fullname': 'my-host.default.service.arpa.', 706 'name': 'my-host', 707 'deleted': 'false', 708 'addresses': ['2001::1', '2001::2'] 709 }] 710 """ 711 outputs = self.cli('srp server host') 712 host_list = [] 713 while outputs: 714 host = {} 715 host['fullname'] = outputs.pop(0).strip() 716 host['name'] = host['fullname'].split('.')[0] 717 host['deleted'] = outputs.pop(0).strip().split(':')[1].strip() 718 if host['deleted'] == 'true': 719 host_list.append(host) 720 continue 721 addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') 722 map(str.strip, addresses) 723 host['addresses'] = [addr for addr in addresses if addr] 724 host_list.append(host) 725 return host_list 726 727 def srp_server_get_host(self, host_name): 728 """Returns host on the SRP server that matches given host name. 729 730 Example usage: 731 self.srp_server_get_host("my-host") 732 """ 733 for host in self.srp_server_get_hosts(): 734 if host_name == host['name']: 735 return host 736 737 def srp_server_get_services(self): 738 """Returns the service list on the SRP server as a list of property 739 dictionary. 740 741 Example output: 742 [{ 743 'fullname': 'my-service._ipps._tcp.default.service.arpa.', 744 'instance': 'my-service', 745 'name': '_ipps._tcp', 746 'deleted': 'false', 747 'port': '12345', 748 'priority': '0', 749 'weight': '0', 750 'ttl': '7200', 751 'lease': '7200', 752 'key-lease', '1209600', 753 'TXT': ['abc=010203'], 754 'host_fullname': 'my-host.default.service.arpa.', 755 'host': 'my-host', 756 'addresses': ['2001::1', '2001::2'] 757 }] 758 759 Note that the TXT data is output as a HEX string. 760 """ 761 outputs = self.cli('srp server service') 762 service_list = [] 763 while outputs: 764 service = {} 765 service['fullname'] = outputs.pop(0).strip() 766 name_labels = service['fullname'].split('.') 767 service['instance'] = name_labels[0] 768 service['name'] = '.'.join(name_labels[1:3]) 769 service['deleted'] = outputs.pop(0).strip().split(':')[1].strip() 770 if service['deleted'] == 'true': 771 service_list.append(service) 772 continue 773 # 'subtypes', port', 'priority', 'weight', 'ttl', 'lease', 'key-lease' 774 for i in range(0, 7): 775 key_value = outputs.pop(0).strip().split(':') 776 service[key_value[0].strip()] = key_value[1].strip() 777 txt_entries = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') 778 txt_entries = map(str.strip, txt_entries) 779 service['TXT'] = [txt for txt in txt_entries if txt] 780 service['host_fullname'] = outputs.pop(0).strip().split(':')[1].strip() 781 service['host'] = service['host_fullname'].split('.')[0] 782 addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') 783 addresses = map(str.strip, addresses) 784 service['addresses'] = [addr for addr in addresses if addr] 785 service_list.append(service) 786 return service_list 787 788 def srp_server_get_service(self, instance_name, service_name): 789 """Returns service on the SRP server that matches given instance 790 name and service name. 791 792 Example usage: 793 self.srp_server_get_service("my-service", "_ipps._tcp") 794 """ 795 for service in self.srp_server_get_services(): 796 if (instance_name == service['instance'] and service_name == service['name']): 797 return service 798 799 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 800 # br 801 802 def br_init(self, if_inex, is_running): 803 self._cli_no_output('br init', if_inex, is_running) 804 805 def br_enable(self): 806 self._cli_no_output('br enable') 807 808 def br_disable(self): 809 self._cli_no_output('br disable') 810 811 def br_get_state(self): 812 return self._cli_single_output('br state') 813 814 def br_get_favored_omrprefix(self): 815 return self._cli_single_output('br omrprefix favored') 816 817 def br_get_local_omrprefix(self): 818 return self._cli_single_output('br omrprefix local') 819 820 def br_get_favored_onlinkprefix(self): 821 return self._cli_single_output('br onlinkprefix favored') 822 823 def br_get_local_onlinkprefix(self): 824 return self._cli_single_output('br onlinkprefix local') 825 826 def br_set_test_local_onlinkprefix(self, prefix): 827 self._cli_no_output('br onlinkprefix test', prefix) 828 829 def br_get_routeprf(self): 830 return self._cli_single_output('br routeprf') 831 832 def br_set_routeprf(self, prf): 833 self._cli_no_output('br routeprf', prf) 834 835 def br_clear_routeprf(self): 836 self._cli_no_output('br routeprf clear') 837 838 def br_get_routers(self): 839 return self.cli('br routers') 840 841 def br_get_peer_brs(self): 842 return self.cli('br peers') 843 844 def br_count_peers(self): 845 return self._cli_single_output('br peers count') 846 847 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 848 # trel 849 850 def trel_get_peers(self): 851 peers = self.cli('trel peers ') 852 return Node.parse_table(peers) 853 854 def trel_test_get_sock_addr(self): 855 return self._cli_single_output('treltest sockaddr') 856 857 def trel_test_change_sock_addr(self): 858 return self._cli_no_output('treltest changesockaddr') 859 860 def trel_test_change_sock_port(self): 861 return self._cli_no_output('treltest changesockport') 862 863 def trel_test_get_notify_addr_counter(self): 864 return self._cli_single_output('treltest notifyaddrcounter') 865 866 # ------------------------------------------------------------------------------------------------------------------ 867 # Helper methods 868 869 def form(self, network_name=None, network_key=None, channel=None, panid=0x1234, xpanid=None): 870 self._cli_no_output('dataset init new') 871 self._cli_no_output('dataset panid', panid) 872 if network_name is not None: 873 self._cli_no_output('dataset networkname', network_name) 874 if network_key is not None: 875 self._cli_no_output('dataset networkkey', network_key) 876 if channel is not None: 877 self._cli_no_output('dataset channel', channel) 878 if xpanid is not None: 879 self._cli_no_output('dataset extpanid', xpanid) 880 self._cli_no_output('dataset commit active') 881 self.set_mode('rdn') 882 self.interface_up() 883 self.thread_start() 884 verify_within(_check_node_is_leader, self._WAIT_TIME, arg=self) 885 886 def join(self, node, type=JOIN_TYPE_ROUTER): 887 self._cli_no_output('dataset clear') 888 self._cli_no_output('dataset networkname', node.get_network_name()) 889 self._cli_no_output('dataset networkkey', node.get_network_key()) 890 self._cli_no_output('dataset channel', node.get_channel()) 891 self._cli_no_output('dataset panid', node.get_panid()) 892 self._cli_no_output('dataset commit active') 893 if type == JOIN_TYPE_END_DEVICE: 894 self.set_mode('rn') 895 elif type == JOIN_TYPE_SLEEPY_END_DEVICE: 896 self.set_mode('-') 897 elif type == JOIN_TYPE_REED: 898 self.set_mode('rdn') 899 self.set_router_eligible('disable') 900 else: 901 self.set_mode('rdn') 902 self.set_router_selection_jitter(1) 903 self.interface_up() 904 self.thread_start() 905 if type == JOIN_TYPE_ROUTER: 906 verify_within(_check_node_is_router, self._WAIT_TIME, arg=self) 907 else: 908 verify_within(_check_node_is_child, self._WAIT_TIME, arg=self) 909 910 def allowlist_node(self, node): 911 """Adds a given node to the allowlist of `self` and enables allowlisting on `self`""" 912 self._cli_no_output('macfilter addr add', node.get_ext_addr()) 913 self._cli_no_output('macfilter addr allowlist') 914 915 def un_allowlist_node(self, node): 916 """Removes a given node (of node `Node) from the allowlist""" 917 self._cli_no_output('macfilter addr remove', node.get_ext_addr()) 918 919 def denylist_node(self, node): 920 """Adds a given node to the denylist of `self` and enables denylisting on `self`""" 921 self._cli_no_output('macfilter addr add', node.get_ext_addr()) 922 self._cli_no_output('macfilter addr denylist') 923 924 def un_denylist_node(self, node): 925 """Removes a given node (of node `Node) from the denylist""" 926 self._cli_no_output('macfilter addr remove', node.get_ext_addr()) 927 928 def set_macfilter_lqi_to_node(self, node, lqi): 929 self._cli_no_output('macfilter rss add-lqi', node.get_ext_addr(), lqi) 930 931 # ------------------------------------------------------------------------------------------------------------------ 932 # Radio nodeidfilter 933 934 def nodeidfilter_clear(self, node): 935 self._cli_no_output('nodeidfilter clear') 936 937 def nodeidfilter_allow(self, node): 938 self._cli_no_output('nodeidfilter allow', node.index) 939 940 def nodeidfilter_deny(self, node): 941 self._cli_no_output('nodeidfilter deny', node.index) 942 943 # ------------------------------------------------------------------------------------------------------------------ 944 # Parsing helpers 945 946 @classmethod 947 def parse_table(cls, table_lines): 948 verify(len(table_lines) >= 2) 949 headers = cls.split_table_row(table_lines[0]) 950 info = [] 951 for row in table_lines[2:]: 952 if row.strip() == '': 953 continue 954 fields = cls.split_table_row(row) 955 verify(len(fields) == len(headers)) 956 info.append({headers[i]: fields[i] for i in range(len(fields))}) 957 return info 958 959 @classmethod 960 def split_table_row(cls, row): 961 return [field.strip() for field in row.strip().split('|')[1:-1]] 962 963 @classmethod 964 def parse_list(cls, list_lines): 965 result = {} 966 for line in list_lines: 967 fields = line.split(':', 1) 968 result[fields[0].strip()] = fields[1].strip() 969 return result 970 971 @classmethod 972 def parse_multiradio_neighbor_entry(cls, line): 973 # Example: "ExtAddr:42aa94ad67229f14, RLOC16:0x9400, Radios:[15.4(245), TREL(255)]" 974 result = {} 975 for field in line.split(', ', 2): 976 key_value = field.split(':') 977 result[key_value[0]] = key_value[1] 978 radios = {} 979 for item in result['Radios'][1:-1].split(','): 980 name, prf = item.strip().split('(') 981 verify(prf.endswith(')')) 982 radios[name] = int(prf[:-1]) 983 result['Radios'] = radios 984 return result 985 986 # ------------------------------------------------------------------------------------------------------------------ 987 # class methods 988 989 @classmethod 990 def finalize_all_nodes(cls): 991 """Finalizes all previously created `Node` instances (stops the CLI process)""" 992 for node in Node._all_nodes: 993 node._finalize() 994 995 @classmethod 996 def set_time_speedup_factor(cls, factor): 997 """Sets up the time speed up factor - should be set before creating any `Node` objects""" 998 if len(Node._all_nodes) != 0: 999 raise Node._NodeError('set_time_speedup_factor() cannot be called after creating a `Node`') 1000 Node._SPEED_UP_FACTOR = factor 1001 1002 1003def _check_node_is_leader(node): 1004 verify(node.get_state() == 'leader') 1005 1006 1007def _check_node_is_router(node): 1008 verify(node.get_state() == 'router') 1009 1010 1011def _check_node_is_child(node): 1012 verify(node.get_state() == 'child') 1013 1014 1015# ---------------------------------------------------------------------------------------------------------------------- 1016 1017 1018class VerifyError(Exception): 1019 pass 1020 1021 1022_is_in_verify_within = False 1023 1024 1025def verify(condition): 1026 """Verifies that a `condition` is true, otherwise raises a VerifyError""" 1027 global _is_in_verify_within 1028 if not condition: 1029 calling_frame = inspect.currentframe().f_back 1030 error_message = 'verify() failed at line {} in "{}"'.format(calling_frame.f_lineno, 1031 calling_frame.f_code.co_filename) 1032 if not _is_in_verify_within: 1033 print(error_message) 1034 raise VerifyError(error_message) 1035 1036 1037def verify_within(condition_checker_func, wait_time, arg=None, delay_time=0.1): 1038 """Verifies that a given function `condition_checker_func` passes successfully within a given wait timeout. 1039 `wait_time` is maximum time waiting for condition_checker to pass (in seconds). 1040 `arg` is optional parameter and if it s not None, will be passed to `condition_checker_func()` 1041 `delay_time` specifies a delay interval added between failed attempts (in seconds). 1042 """ 1043 global _is_in_verify_within 1044 start_time = time.time() 1045 old_is_in_verify_within = _is_in_verify_within 1046 _is_in_verify_within = True 1047 while True: 1048 try: 1049 if arg is None: 1050 condition_checker_func() 1051 else: 1052 condition_checker_func(arg) 1053 except VerifyError as e: 1054 if time.time() - start_time > wait_time: 1055 print('Took too long to pass the condition ({}>{} sec)'.format(time.time() - start_time, wait_time)) 1056 if hasattr(e, 'message'): 1057 print(e.message) 1058 raise e 1059 except BaseException: 1060 raise 1061 else: 1062 break 1063 if delay_time != 0: 1064 time.sleep(delay_time) 1065 _is_in_verify_within = old_is_in_verify_within 1066