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