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_ip_addrs(self, verbose=None): 316 return self.cli('ipaddr', verbose) 317 318 def add_ip_addr(self, address): 319 self._cli_no_output('ipaddr add', address) 320 321 def remove_ip_addr(self, address): 322 self._cli_no_output('ipaddr del', address) 323 324 def get_mleid_ip_addr(self): 325 return self._cli_single_output('ipaddr mleid') 326 327 def get_linklocal_ip_addr(self): 328 return self._cli_single_output('ipaddr linklocal') 329 330 def get_rloc_ip_addr(self): 331 return self._cli_single_output('ipaddr rloc') 332 333 def get_mesh_local_prefix(self): 334 return self._cli_single_output('prefix meshlocal') 335 336 def get_ip_maddrs(self): 337 return self.cli('ipmaddr') 338 339 def add_ip_maddr(self, maddr): 340 return self._cli_no_output('ipmaddr add', maddr) 341 342 def get_leader_weight(self): 343 return self._cli_single_output('leaderweight') 344 345 def set_leader_weight(self, weight): 346 self._cli_no_output('leaderweight', weight) 347 348 def get_pollperiod(self): 349 return self._cli_single_output('pollperiod') 350 351 def set_pollperiod(self, period): 352 self._cli_no_output('pollperiod', period) 353 354 def get_partition_id(self): 355 return self._cli_single_output('partitionid') 356 357 def get_nexthop(self, rloc16): 358 return self._cli_single_output('nexthop', rloc16) 359 360 def get_parent_info(self): 361 outputs = self.cli('parent') 362 result = {} 363 for line in outputs: 364 fields = line.split(':') 365 result[fields[0].strip()] = fields[1].strip() 366 return result 367 368 def get_child_table(self): 369 return Node.parse_table(self.cli('child table')) 370 371 def get_neighbor_table(self): 372 return Node.parse_table(self.cli('neighbor table')) 373 374 def get_router_table(self): 375 return Node.parse_table(self.cli('router table')) 376 377 def get_eidcache(self): 378 return self.cli('eidcache') 379 380 def get_vendor_name(self): 381 return self._cli_single_output('vendor name') 382 383 def set_vendor_name(self, name): 384 self._cli_no_output('vendor name', name) 385 386 def get_vendor_model(self): 387 return self._cli_single_output('vendor model') 388 389 def set_vendor_model(self, model): 390 self._cli_no_output('vendor model', model) 391 392 def get_vendor_sw_version(self): 393 return self._cli_single_output('vendor swversion') 394 395 def set_vendor_sw_version(self, version): 396 return self._cli_no_output('vendor swversion', version) 397 398 def get_vendor_app_url(self): 399 return self._cli_single_output('vendor appurl') 400 401 def set_vendor_app_url(self, url): 402 return self._cli_no_output('vendor appurl', url) 403 404 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 405 # netdata 406 407 def get_netdata(self, rloc16=None): 408 outputs = self.cli('netdata show', rloc16) 409 outputs = [line.strip() for line in outputs] 410 routes_index = outputs.index('Routes:') 411 services_index = outputs.index('Services:') 412 if rloc16 is None: 413 contexts_index = outputs.index('Contexts:') 414 commissioning_index = outputs.index('Commissioning:') 415 result = {} 416 result['prefixes'] = outputs[1:routes_index] 417 result['routes'] = outputs[routes_index + 1:services_index] 418 if rloc16 is None: 419 result['services'] = outputs[services_index + 1:contexts_index] 420 result['contexts'] = outputs[contexts_index + 1:commissioning_index] 421 result['commissioning'] = outputs[commissioning_index + 1:] 422 else: 423 result['services'] = outputs[services_index + 1:] 424 425 return result 426 427 def get_netdata_prefixes(self): 428 return self.get_netdata()['prefixes'] 429 430 def get_netdata_routes(self): 431 return self.get_netdata()['routes'] 432 433 def get_netdata_services(self): 434 return self.get_netdata()['services'] 435 436 def get_netdata_contexts(self): 437 return self.get_netdata()['contexts'] 438 439 def get_netdata_versions(self): 440 leaderdata = Node.parse_list(self.cli('leaderdata')) 441 return (int(leaderdata['Data Version']), int(leaderdata['Stable Data Version'])) 442 443 def get_netdata_length(self): 444 return self._cli_single_output('netdata length') 445 446 def add_prefix(self, prefix, flags=None, prf=None): 447 return self._cli_no_output('prefix add', prefix, flags, prf) 448 449 def add_route(self, prefix, flags=None, prf=None): 450 return self._cli_no_output('route add', prefix, flags, prf) 451 452 def remove_prefix(self, prefix): 453 return self._cli_no_output('prefix remove', prefix) 454 455 def register_netdata(self): 456 self._cli_no_output('netdata register') 457 458 def get_netdata_full(self): 459 return self._cli_single_output('netdata full') 460 461 def reset_netdata_full(self): 462 self._cli_no_output('netdata full reset') 463 464 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 465 # ping and counters 466 467 def ping(self, address, size=0, count=1, verify_success=True): 468 outputs = self.cli('ping', address, size, count) 469 m = re.match(r'(\d+) packets transmitted, (\d+) packets received.', outputs[-1].strip()) 470 verify(m is not None) 471 verify(int(m.group(1)) == count) 472 if verify_success: 473 verify(int(m.group(2)) == count) 474 475 def get_mle_counter(self): 476 return self.cli('counters mle') 477 478 def get_ip_counters(self): 479 return Node.parse_list(self.cli('counters ip')) 480 481 def get_br_counter_unicast_outbound_packets(self): 482 outputs = self.cli('counters br') 483 for line in outputs: 484 m = re.match(r'Outbound Unicast: Packets (\d+) Bytes (\d+)', line.strip()) 485 if m is not None: 486 counter = int(m.group(1)) 487 break 488 else: 489 verify(False) 490 return counter 491 492 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 493 # Misc 494 495 def get_mle_adv_imax(self): 496 return self._cli_single_output('mleadvimax') 497 498 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 499 # Border Agent 500 501 def ba_get_state(self): 502 return self._cli_single_output('ba state') 503 504 def ba_get_port(self): 505 return self._cli_single_output('ba port') 506 507 def ba_is_ephemeral_key_active(self): 508 return self._cli_single_output('ba ephemeralkey') 509 510 def ba_set_ephemeral_key(self, keystring, timeout=None, port=None): 511 self._cli_no_output('ba ephemeralkey set', keystring, timeout, port) 512 513 def ba_clear_ephemeral_key(self): 514 self._cli_no_output('ba ephemeralkey clear') 515 516 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 517 # UDP 518 519 def udp_open(self): 520 self._cli_no_output('udp open') 521 522 def udp_close(self): 523 self._cli_no_output('udp close') 524 525 def udp_bind(self, address, port): 526 self._cli_no_output('udp bind', address, port) 527 528 def udp_send(self, address, port, text): 529 self._cli_no_output('udp send', address, port, '-t', text) 530 531 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 532 # multiradio 533 534 def multiradio_get_radios(self): 535 return self._cli_single_output('multiradio') 536 537 def multiradio_get_neighbor_list(self): 538 return self.cli('multiradio neighbor list') 539 540 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 541 # SRP client 542 543 def srp_client_start(self, server_address, server_port): 544 self._cli_no_output('srp client start', server_address, server_port) 545 546 def srp_client_stop(self): 547 self._cli_no_output('srp client stop') 548 549 def srp_client_get_state(self): 550 return self._cli_single_output('srp client state', expected_outputs=['Enabled', 'Disabled']) 551 552 def srp_client_get_auto_start_mode(self): 553 return self._cli_single_output('srp client autostart', expected_outputs=['Enabled', 'Disabled']) 554 555 def srp_client_enable_auto_start_mode(self): 556 self._cli_no_output('srp client autostart enable') 557 558 def srp_client_disable_auto_start_mode(self): 559 self._cli_no_output('srp client autostart disable') 560 561 def srp_client_get_server_address(self): 562 return self._cli_single_output('srp client server address') 563 564 def srp_client_get_server_port(self): 565 return self._cli_single_output('srp client server port') 566 567 def srp_client_get_host_state(self): 568 return self._cli_single_output('srp client host state') 569 570 def srp_client_set_host_name(self, name): 571 self._cli_no_output('srp client host name', name) 572 573 def srp_client_get_host_name(self): 574 return self._cli_single_output('srp client host name') 575 576 def srp_client_remove_host(self, remove_key=False, send_unreg_to_server=False): 577 self._cli_no_output('srp client host remove', int(remove_key), int(send_unreg_to_server)) 578 579 def srp_client_clear_host(self): 580 self._cli_no_output('srp client host clear') 581 582 def srp_client_enable_auto_host_address(self): 583 self._cli_no_output('srp client host address auto') 584 585 def srp_client_set_host_address(self, *addrs): 586 self._cli_no_output('srp client host address', *addrs) 587 588 def srp_client_get_host_address(self): 589 return self.cli('srp client host address') 590 591 def srp_client_add_service(self, 592 instance_name, 593 service_name, 594 port, 595 priority=0, 596 weight=0, 597 txt_entries=[], 598 lease=0, 599 key_lease=0): 600 txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries) 601 self._cli_no_output('srp client service add', instance_name, service_name, port, priority, weight, txt_record, 602 lease, key_lease) 603 604 def srp_client_remove_service(self, instance_name, service_name): 605 self._cli_no_output('srp client service remove', instance_name, service_name) 606 607 def srp_client_clear_service(self, instance_name, service_name): 608 self._cli_no_output('srp client service clear', instance_name, service_name) 609 610 def srp_client_get_services(self): 611 outputs = self.cli('srp client service') 612 return [self._parse_srp_client_service(line) for line in outputs] 613 614 def _encode_txt_entry(self, entry): 615 """Encodes the TXT entry to the DNS-SD TXT record format as a HEX string. 616 617 Example usage: 618 self._encode_txt_entries(['abc']) -> '03616263' 619 self._encode_txt_entries(['def=']) -> '046465663d' 620 self._encode_txt_entries(['xyz=XYZ']) -> '0778797a3d58595a' 621 """ 622 return '{:02x}'.format(len(entry)) + "".join("{:02x}".format(ord(c)) for c in entry) 623 624 def _parse_srp_client_service(self, line): 625 """Parse one line of srp service list into a dictionary which 626 maps string keys to string values. 627 628 Example output for input 629 'instance:\"%s\", name:\"%s\", state:%s, port:%d, priority:%d, weight:%d"' 630 { 631 'instance': 'my-service', 632 'name': '_ipps._udp', 633 'state': 'ToAdd', 634 'port': '12345', 635 'priority': '0', 636 'weight': '0' 637 } 638 639 Note that value of 'port', 'priority' and 'weight' are represented 640 as strings but not integers. 641 """ 642 key_values = [word.strip().split(':') for word in line.split(', ')] 643 return {key_value[0].strip(): key_value[1].strip('"') for key_value in key_values} 644 645 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 646 # SRP server 647 648 def srp_server_get_state(self): 649 return self._cli_single_output('srp server state', expected_outputs=['disabled', 'running', 'stopped']) 650 651 def srp_server_get_addr_mode(self): 652 return self._cli_single_output('srp server addrmode', expected_outputs=['unicast', 'anycast']) 653 654 def srp_server_set_addr_mode(self, mode): 655 self._cli_no_output('srp server addrmode', mode) 656 657 def srp_server_get_anycast_seq_num(self): 658 return self._cli_single_output('srp server seqnum') 659 660 def srp_server_set_anycast_seq_num(self, seqnum): 661 self._cli_no_output('srp server seqnum', seqnum) 662 663 def srp_server_enable(self): 664 self._cli_no_output('srp server enable') 665 666 def srp_server_disable(self): 667 self._cli_no_output('srp server disable') 668 669 def srp_server_auto_enable(self): 670 self._cli_no_output('srp server auto enable') 671 672 def srp_server_auto_disable(self): 673 self._cli_no_output('srp server auto disable') 674 675 def srp_server_set_lease(self, min_lease, max_lease, min_key_lease, max_key_lease): 676 self._cli_no_output('srp server lease', min_lease, max_lease, min_key_lease, max_key_lease) 677 678 def srp_server_get_hosts(self): 679 """Returns the host list on the SRP server as a list of property 680 dictionary. 681 682 Example output: 683 [{ 684 'fullname': 'my-host.default.service.arpa.', 685 'name': 'my-host', 686 'deleted': 'false', 687 'addresses': ['2001::1', '2001::2'] 688 }] 689 """ 690 outputs = self.cli('srp server host') 691 host_list = [] 692 while outputs: 693 host = {} 694 host['fullname'] = outputs.pop(0).strip() 695 host['name'] = host['fullname'].split('.')[0] 696 host['deleted'] = outputs.pop(0).strip().split(':')[1].strip() 697 if host['deleted'] == 'true': 698 host_list.append(host) 699 continue 700 addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') 701 map(str.strip, addresses) 702 host['addresses'] = [addr for addr in addresses if addr] 703 host_list.append(host) 704 return host_list 705 706 def srp_server_get_host(self, host_name): 707 """Returns host on the SRP server that matches given host name. 708 709 Example usage: 710 self.srp_server_get_host("my-host") 711 """ 712 for host in self.srp_server_get_hosts(): 713 if host_name == host['name']: 714 return host 715 716 def srp_server_get_services(self): 717 """Returns the service list on the SRP server as a list of property 718 dictionary. 719 720 Example output: 721 [{ 722 'fullname': 'my-service._ipps._tcp.default.service.arpa.', 723 'instance': 'my-service', 724 'name': '_ipps._tcp', 725 'deleted': 'false', 726 'port': '12345', 727 'priority': '0', 728 'weight': '0', 729 'ttl': '7200', 730 'lease': '7200', 731 'key-lease', '1209600', 732 'TXT': ['abc=010203'], 733 'host_fullname': 'my-host.default.service.arpa.', 734 'host': 'my-host', 735 'addresses': ['2001::1', '2001::2'] 736 }] 737 738 Note that the TXT data is output as a HEX string. 739 """ 740 outputs = self.cli('srp server service') 741 service_list = [] 742 while outputs: 743 service = {} 744 service['fullname'] = outputs.pop(0).strip() 745 name_labels = service['fullname'].split('.') 746 service['instance'] = name_labels[0] 747 service['name'] = '.'.join(name_labels[1:3]) 748 service['deleted'] = outputs.pop(0).strip().split(':')[1].strip() 749 if service['deleted'] == 'true': 750 service_list.append(service) 751 continue 752 # 'subtypes', port', 'priority', 'weight', 'ttl', 'lease', 'key-lease' 753 for i in range(0, 7): 754 key_value = outputs.pop(0).strip().split(':') 755 service[key_value[0].strip()] = key_value[1].strip() 756 txt_entries = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') 757 txt_entries = map(str.strip, txt_entries) 758 service['TXT'] = [txt for txt in txt_entries if txt] 759 service['host_fullname'] = outputs.pop(0).strip().split(':')[1].strip() 760 service['host'] = service['host_fullname'].split('.')[0] 761 addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') 762 addresses = map(str.strip, addresses) 763 service['addresses'] = [addr for addr in addresses if addr] 764 service_list.append(service) 765 return service_list 766 767 def srp_server_get_service(self, instance_name, service_name): 768 """Returns service on the SRP server that matches given instance 769 name and service name. 770 771 Example usage: 772 self.srp_server_get_service("my-service", "_ipps._tcp") 773 """ 774 for service in self.srp_server_get_services(): 775 if (instance_name == service['instance'] and service_name == service['name']): 776 return service 777 778 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 779 # br 780 781 def br_init(self, if_inex, is_running): 782 self._cli_no_output('br init', if_inex, is_running) 783 784 def br_enable(self): 785 self._cli_no_output('br enable') 786 787 def br_disable(self): 788 self._cli_no_output('br disable') 789 790 def br_get_state(self): 791 return self._cli_single_output('br state') 792 793 def br_get_favored_omrprefix(self): 794 return self._cli_single_output('br omrprefix favored') 795 796 def br_get_local_omrprefix(self): 797 return self._cli_single_output('br omrprefix local') 798 799 def br_get_favored_onlinkprefix(self): 800 return self._cli_single_output('br onlinkprefix favored') 801 802 def br_get_local_onlinkprefix(self): 803 return self._cli_single_output('br onlinkprefix local') 804 805 def br_set_test_local_onlinkprefix(self, prefix): 806 self._cli_no_output('br onlinkprefix test', prefix) 807 808 def br_get_routeprf(self): 809 return self._cli_single_output('br routeprf') 810 811 def br_set_routeprf(self, prf): 812 self._cli_no_output('br routeprf', prf) 813 814 def br_clear_routeprf(self): 815 self._cli_no_output('br routeprf clear') 816 817 def br_get_routers(self): 818 return self.cli('br routers') 819 820 def br_get_peer_brs(self): 821 return self.cli('br peers') 822 823 def br_count_peers(self): 824 return self._cli_single_output('br peers count') 825 826 # ------------------------------------------------------------------------------------------------------------------ 827 # Helper methods 828 829 def form(self, network_name=None, network_key=None, channel=None, panid=0x1234, xpanid=None): 830 self._cli_no_output('dataset init new') 831 self._cli_no_output('dataset panid', panid) 832 if network_name is not None: 833 self._cli_no_output('dataset networkname', network_name) 834 if network_key is not None: 835 self._cli_no_output('dataset networkkey', network_key) 836 if channel is not None: 837 self._cli_no_output('dataset channel', channel) 838 if xpanid is not None: 839 self._cli_no_output('dataset extpanid', xpanid) 840 self._cli_no_output('dataset commit active') 841 self.set_mode('rdn') 842 self.interface_up() 843 self.thread_start() 844 verify_within(_check_node_is_leader, self._WAIT_TIME, arg=self) 845 846 def join(self, node, type=JOIN_TYPE_ROUTER): 847 self._cli_no_output('dataset clear') 848 self._cli_no_output('dataset networkname', node.get_network_name()) 849 self._cli_no_output('dataset networkkey', node.get_network_key()) 850 self._cli_no_output('dataset channel', node.get_channel()) 851 self._cli_no_output('dataset panid', node.get_panid()) 852 self._cli_no_output('dataset commit active') 853 if type == JOIN_TYPE_END_DEVICE: 854 self.set_mode('rn') 855 elif type == JOIN_TYPE_SLEEPY_END_DEVICE: 856 self.set_mode('-') 857 elif type == JOIN_TYPE_REED: 858 self.set_mode('rdn') 859 self.set_router_eligible('disable') 860 else: 861 self.set_mode('rdn') 862 self.set_router_selection_jitter(1) 863 self.interface_up() 864 self.thread_start() 865 if type == JOIN_TYPE_ROUTER: 866 verify_within(_check_node_is_router, self._WAIT_TIME, arg=self) 867 else: 868 verify_within(_check_node_is_child, self._WAIT_TIME, arg=self) 869 870 def allowlist_node(self, node): 871 """Adds a given node to the allowlist of `self` and enables allowlisting on `self`""" 872 self._cli_no_output('macfilter addr add', node.get_ext_addr()) 873 self._cli_no_output('macfilter addr allowlist') 874 875 def un_allowlist_node(self, node): 876 """Removes a given node (of node `Node) from the allowlist""" 877 self._cli_no_output('macfilter addr remove', node.get_ext_addr()) 878 879 def set_macfilter_lqi_to_node(self, node, lqi): 880 self._cli_no_output('macfilter rss add-lqi', node.get_ext_addr(), lqi) 881 882 # ------------------------------------------------------------------------------------------------------------------ 883 # Radio nodeidfilter 884 885 def nodeidfilter_clear(self, node): 886 self._cli_no_output('nodeidfilter clear') 887 888 def nodeidfilter_allow(self, node): 889 self._cli_no_output('nodeidfilter allow', node.index) 890 891 def nodeidfilter_deny(self, node): 892 self._cli_no_output('nodeidfilter deny', node.index) 893 894 # ------------------------------------------------------------------------------------------------------------------ 895 # Parsing helpers 896 897 @classmethod 898 def parse_table(cls, table_lines): 899 verify(len(table_lines) >= 2) 900 headers = cls.split_table_row(table_lines[0]) 901 info = [] 902 for row in table_lines[2:]: 903 if row.strip() == '': 904 continue 905 fields = cls.split_table_row(row) 906 verify(len(fields) == len(headers)) 907 info.append({headers[i]: fields[i] for i in range(len(fields))}) 908 return info 909 910 @classmethod 911 def split_table_row(cls, row): 912 return [field.strip() for field in row.strip().split('|')[1:-1]] 913 914 @classmethod 915 def parse_list(cls, list_lines): 916 result = {} 917 for line in list_lines: 918 fields = line.split(':', 1) 919 result[fields[0].strip()] = fields[1].strip() 920 return result 921 922 @classmethod 923 def parse_multiradio_neighbor_entry(cls, line): 924 # Example: "ExtAddr:42aa94ad67229f14, RLOC16:0x9400, Radios:[15.4(245), TREL(255)]" 925 result = {} 926 for field in line.split(', ', 2): 927 key_value = field.split(':') 928 result[key_value[0]] = key_value[1] 929 radios = {} 930 for item in result['Radios'][1:-1].split(','): 931 name, prf = item.strip().split('(') 932 verify(prf.endswith(')')) 933 radios[name] = int(prf[:-1]) 934 result['Radios'] = radios 935 return result 936 937 # ------------------------------------------------------------------------------------------------------------------ 938 # class methods 939 940 @classmethod 941 def finalize_all_nodes(cls): 942 """Finalizes all previously created `Node` instances (stops the CLI process)""" 943 for node in Node._all_nodes: 944 node._finalize() 945 946 @classmethod 947 def set_time_speedup_factor(cls, factor): 948 """Sets up the time speed up factor - should be set before creating any `Node` objects""" 949 if len(Node._all_nodes) != 0: 950 raise Node._NodeError('set_time_speedup_factor() cannot be called after creating a `Node`') 951 Node._SPEED_UP_FACTOR = factor 952 953 954def _check_node_is_leader(node): 955 verify(node.get_state() == 'leader') 956 957 958def _check_node_is_router(node): 959 verify(node.get_state() == 'router') 960 961 962def _check_node_is_child(node): 963 verify(node.get_state() == 'child') 964 965 966# ---------------------------------------------------------------------------------------------------------------------- 967 968 969class VerifyError(Exception): 970 pass 971 972 973_is_in_verify_within = False 974 975 976def verify(condition): 977 """Verifies that a `condition` is true, otherwise raises a VerifyError""" 978 global _is_in_verify_within 979 if not condition: 980 calling_frame = inspect.currentframe().f_back 981 error_message = 'verify() failed at line {} in "{}"'.format(calling_frame.f_lineno, 982 calling_frame.f_code.co_filename) 983 if not _is_in_verify_within: 984 print(error_message) 985 raise VerifyError(error_message) 986 987 988def verify_within(condition_checker_func, wait_time, arg=None, delay_time=0.1): 989 """Verifies that a given function `condition_checker_func` passes successfully within a given wait timeout. 990 `wait_time` is maximum time waiting for condition_checker to pass (in seconds). 991 `arg` is optional parameter and if it s not None, will be passed to `condition_checker_func()` 992 `delay_time` specifies a delay interval added between failed attempts (in seconds). 993 """ 994 global _is_in_verify_within 995 start_time = time.time() 996 old_is_in_verify_within = _is_in_verify_within 997 _is_in_verify_within = True 998 while True: 999 try: 1000 if arg is None: 1001 condition_checker_func() 1002 else: 1003 condition_checker_func(arg) 1004 except VerifyError as e: 1005 if time.time() - start_time > wait_time: 1006 print('Took too long to pass the condition ({}>{} sec)'.format(time.time() - start_time, wait_time)) 1007 if hasattr(e, 'message'): 1008 print(e.message) 1009 raise e 1010 except BaseException: 1011 raise 1012 else: 1013 break 1014 if delay_time != 0: 1015 time.sleep(delay_time) 1016 _is_in_verify_within = old_is_in_verify_within 1017