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' 49 50# ---------------------------------------------------------------------------------------------------------------------- 51 52 53def _log(text, new_line=True, flush=True): 54 sys.stdout.write(text) 55 if new_line: 56 sys.stdout.write('\n') 57 if flush: 58 sys.stdout.flush() 59 60 61# ---------------------------------------------------------------------------------------------------------------------- 62# CliError class 63 64 65class CliError(Exception): 66 67 def __init__(self, error_code, message): 68 self._error_code = error_code 69 self._message = message 70 71 @property 72 def error_code(self): 73 return self._error_code 74 75 @property 76 def message(self): 77 return self._message 78 79 80# ---------------------------------------------------------------------------------------------------------------------- 81# Node class 82 83 84class Node(object): 85 """ An OT CLI instance """ 86 87 # defines the default verbosity setting (can be changed per `Node`) 88 _VERBOSE = os.getenv('TORANJ_VERBOSE', 'no').lower() in ['true', '1', 't', 'y', 'yes', 'on'] 89 90 _SPEED_UP_FACTOR = 1 # defines the default time speed up factor 91 92 # Determine whether to save logs in a file. 93 _SAVE_LOGS = True 94 95 # name of log file (if _SAVE_LOGS is `True`) 96 _LOG_FNAME = 'ot-logs' 97 98 _OT_BUILDDIR = os.getenv('top_builddir', '../../..') 99 100 _OT_CLI_FTD = '%s/examples/apps/cli/ot-cli-ftd' % _OT_BUILDDIR 101 102 _WAIT_TIME = 10 103 104 _START_INDEX = 1 105 _cur_index = _START_INDEX 106 107 _all_nodes = weakref.WeakSet() 108 109 def __init__(self, verbose=_VERBOSE): 110 """Creates a new `Node` instance""" 111 112 index = Node._cur_index 113 Node._cur_index += 1 114 115 self._index = index 116 self._verbose = verbose 117 118 if Node._SAVE_LOGS: 119 self._log_file = open(self._LOG_FNAME + str(index) + '.log', 'wb') 120 else: 121 self._log_file = None 122 123 cmd = f'{self._OT_CLI_FTD} --time-speed={self._SPEED_UP_FACTOR} {self._index}' 124 125 if self._verbose: 126 _log(f'$ Node{index}.__init__() cmd: `{cmd}`') 127 128 self._cli_process = pexpect.popen_spawn.PopenSpawn(cmd, logfile=self._log_file) 129 Node._all_nodes.add(self) 130 131 def __del__(self): 132 self._finalize() 133 134 def __repr__(self): 135 return f'Node(index={self._index})' 136 137 @property 138 def index(self): 139 return self._index 140 141 # ------------------------------------------------------------------------------------------------------------------ 142 # Executing a `cli` command 143 144 def cli(self, *args): 145 """ Issues a CLI command on the given node and returns the resulting output. 146 147 The returned result is a list of strings (with `\r\n` removed) as outputted by the CLI. 148 If executing the command fails, `CliError` is raised with error code and error message. 149 """ 150 151 cmd = ' '.join([f'{arg}' for arg in args if arg is not None]).strip() 152 153 if self._verbose: 154 _log(f'$ Node{self._index}.cli(\'{cmd}\')', new_line=False) 155 156 self._cli_process.send(cmd + '\n') 157 index = self._cli_process.expect(['(.*)Done\r\n', '.*Error (\d+):(.*)\r\n']) 158 159 if index == 0: 160 result = [ 161 line for line in self._cli_process.match.group(1).decode().splitlines() 162 if not self._is_ot_logg_line(line) if not line.strip().endswith(cmd) 163 ] 164 165 if self._verbose: 166 if len(result) > 1: 167 _log(':') 168 for line in result: 169 _log(' ' + line) 170 elif len(result) == 1: 171 _log(f' -> {result[0]}') 172 else: 173 _log('') 174 175 return result 176 else: 177 match = self._cli_process.match 178 e = CliError(int(match.group(1).decode()), match.group(2).decode().strip()) 179 if self._verbose: 180 _log(f': Error {e.message} ({e.error_code})') 181 raise e 182 183 def _is_ot_logg_line(self, line): 184 return any(level in line for level in [' [D] ', ' [I] ', ' [N] ', ' [W] ', ' [C] ', ' [-] ']) 185 186 def _cli_no_output(self, cmd, *args): 187 outputs = self.cli(cmd, *args) 188 verify(len(outputs) == 0) 189 190 def _cli_single_output(self, cmd, expected_outputs=None): 191 outputs = self.cli(cmd) 192 verify(len(outputs) == 1) 193 verify((expected_outputs is None) or (outputs[0] in expected_outputs)) 194 return outputs[0] 195 196 def _finalize(self): 197 if self._cli_process.proc.poll() is None: 198 if self._verbose: 199 _log(f'$ Node{self.index} terminating') 200 self._cli_process.send('exit\n') 201 self._cli_process.wait() 202 203 # ------------------------------------------------------------------------------------------------------------------ 204 # cli commands 205 206 def get_state(self): 207 return self._cli_single_output('state', ['detached', 'child', 'router', 'leader', 'disabled']) 208 209 def get_channel(self): 210 return self._cli_single_output('channel') 211 212 def set_channel(self, channel): 213 self._cli_no_output('channel', channel) 214 215 def get_ext_addr(self): 216 return self._cli_single_output('extaddr') 217 218 def set_ext_addr(self, ext_addr): 219 self._cli_no_output('extaddr', ext_addr) 220 221 def get_ext_panid(self): 222 return self._cli_single_output('extpanid') 223 224 def set_ext_panid(self, ext_panid): 225 self._cli_no_output('extpanid', ext_panid) 226 227 def get_mode(self): 228 return self._cli_single_output('mode') 229 230 def set_mode(self, mode): 231 self._cli_no_output('mode', mode) 232 233 def get_network_key(self): 234 return self._cli_single_output('networkkey') 235 236 def set_network_key(self, networkkey): 237 self._cli_no_output('networkkey', networkkey) 238 239 def get_network_name(self): 240 return self._cli_single_output('networkname') 241 242 def set_network_name(self, network_name): 243 self._cli_no_output('networkname', network_name) 244 245 def get_panid(self): 246 return self._cli_single_output('panid') 247 248 def set_panid(self, panid): 249 self._cli_no_output('panid', panid) 250 251 def get_router_upgrade_threshold(self): 252 return self._cli_single_output('routerupgradethreshold') 253 254 def set_router_upgrade_threshold(self, threshold): 255 self._cli_no_output('routerupgradethreshold', threshold) 256 257 def get_router_selection_jitter(self): 258 return self._cli_single_output('routerselectionjitter') 259 260 def set_router_selection_jitter(self, jitter): 261 self._cli_no_output('routerselectionjitter', jitter) 262 263 def interface_up(self): 264 self._cli_no_output('ifconfig up') 265 266 def interface_down(self): 267 self._cli_no_output('ifconfig down') 268 269 def get_interface_state(self): 270 return self._cli_single_output('ifconfig') 271 272 def thread_start(self): 273 self._cli_no_output('thread start') 274 275 def thread_stop(self): 276 self._cli_no_output('thread stop') 277 278 def get_ip_addrs(self): 279 return self.cli('ipaddr') 280 281 def get_mleid_ip_addr(self): 282 return self._cli_single_output('ipaddr mleid') 283 284 def get_linklocal_ip_addr(self): 285 return self._cli_single_output('ipaddr linklocal') 286 287 def get_rloc_ip_addr(self): 288 return self._cli_single_output('ipaddr rloc') 289 290 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 291 # SRP client 292 293 def srp_client_start(self, server_address, server_port): 294 self._cli_no_output('srp client start', server_address, server_port) 295 296 def srp_client_stop(self): 297 self._cli_no_output('srp client stop') 298 299 def srp_client_get_state(self): 300 return self._cli_single_output('srp client state', ['Enabled', 'Disabled']) 301 302 def srp_client_get_auto_start_mode(self): 303 return self._cli_single_output('srp client autostart', ['Enabled', 'Disabled']) 304 305 def srp_client_enable_auto_start_mode(self): 306 self._cli_no_output('srp client autostart enable') 307 308 def srp_client_disable_auto_start_mode(self): 309 self._cli_no_output('srp client autostart disable') 310 311 def srp_client_get_server_address(self): 312 return self._cli_single_output('srp client server address') 313 314 def srp_client_get_server_port(self): 315 return self._cli_single_output('srp client server port') 316 317 def srp_client_get_host_state(self): 318 return self._cli_single_output('srp client host state') 319 320 def srp_client_set_host_name(self, name): 321 self._cli_no_output('srp client host name', name) 322 323 def srp_client_get_host_name(self): 324 return self._cli_single_output('srp client host name') 325 326 def srp_client_remove_host(self, remove_key=False, send_unreg_to_server=False): 327 self._cli_no_output('srp client host remove', int(remove_key), int(send_unreg_to_server)) 328 329 def srp_client_clear_host(self): 330 self._cli_no_output('srp client host clear') 331 332 def srp_client_enable_auto_host_address(self): 333 self._cli_no_output('srp client host address auto') 334 335 def srp_client_set_host_address(self, *addrs): 336 self._cli_no_output('srp client host address', *addrs) 337 338 def srp_client_get_host_address(self): 339 return self.cli('srp client host address') 340 341 def srp_client_add_service(self, instance_name, service_name, port, priority=0, weight=0, txt_entries=[]): 342 txt_record = "".join(self._encode_txt_entry(entry) for entry in txt_entries) 343 self._cli_no_output('srp client service add', instance_name, service_name, port, priority, weight, txt_record) 344 345 def srp_client_remove_service(self, instance_name, service_name): 346 self._cli_no_output('srp client service remove', instance_name, service_name) 347 348 def srp_client_clear_service(self, instance_name, service_name): 349 self._cli_no_output('srp client service clear', instance_name, service_name) 350 351 def srp_client_get_services(self): 352 outputs = self.cli('srp client service') 353 return [self._parse_srp_client_service(line) for line in outputs] 354 355 def _encode_txt_entry(self, entry): 356 """Encodes the TXT entry to the DNS-SD TXT record format as a HEX string. 357 358 Example usage: 359 self._encode_txt_entries(['abc']) -> '03616263' 360 self._encode_txt_entries(['def=']) -> '046465663d' 361 self._encode_txt_entries(['xyz=XYZ']) -> '0778797a3d58595a' 362 """ 363 return '{:02x}'.format(len(entry)) + "".join("{:02x}".format(ord(c)) for c in entry) 364 365 def _parse_srp_client_service(self, line): 366 """Parse one line of srp service list into a dictionary which 367 maps string keys to string values. 368 369 Example output for input 370 'instance:\"%s\", name:\"%s\", state:%s, port:%d, priority:%d, weight:%d"' 371 { 372 'instance': 'my-service', 373 'name': '_ipps._udp', 374 'state': 'ToAdd', 375 'port': '12345', 376 'priority': '0', 377 'weight': '0' 378 } 379 380 Note that value of 'port', 'priority' and 'weight' are represented 381 as strings but not integers. 382 """ 383 key_values = [word.strip().split(':') for word in line.split(', ')] 384 return {key_value[0].strip(): key_value[1].strip('"') for key_value in key_values} 385 386 #- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 387 # SRP server 388 389 def srp_server_get_state(self): 390 return self._cli_single_output('srp server state', ['disabled', 'running', 'stopped']) 391 392 def srp_server_get_addr_mode(self): 393 return self._cli_single_output('srp server addrmode', ['unicast', 'anycast']) 394 395 def srp_server_set_addr_mode(self, mode): 396 self._cli_no_output('srp server addrmode', mode) 397 398 def srp_server_get_anycast_seq_num(self): 399 return self._cli_single_output('srp server seqnum') 400 401 def srp_server_set_anycast_seq_num(self, seqnum): 402 self._cli_no_output('srp server seqnum', seqnum) 403 404 def srp_server_enable(self): 405 self._cli_no_output('srp server enable') 406 407 def srp_server_disable(self): 408 self._cli_no_output('srp server disable') 409 410 def srp_server_set_lease(self, min_lease, max_lease, min_key_lease, max_key_lease): 411 self._cli_no_output('srp server lease', min_lease, max_lease, min_key_lease, max_key_lease) 412 413 def srp_server_get_hosts(self): 414 """Returns the host list on the SRP server as a list of property 415 dictionary. 416 417 Example output: 418 [{ 419 'fullname': 'my-host.default.service.arpa.', 420 'name': 'my-host', 421 'deleted': 'false', 422 'addresses': ['2001::1', '2001::2'] 423 }] 424 """ 425 outputs = self.cli('srp server host') 426 host_list = [] 427 while outputs: 428 host = {} 429 host['fullname'] = outputs.pop(0).strip() 430 host['name'] = host['fullname'].split('.')[0] 431 host['deleted'] = outputs.pop(0).strip().split(':')[1].strip() 432 if host['deleted'] == 'true': 433 host_list.append(host) 434 continue 435 addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') 436 map(str.strip, addresses) 437 host['addresses'] = [addr for addr in addresses if addr] 438 host_list.append(host) 439 return host_list 440 441 def srp_server_get_host(self, host_name): 442 """Returns host on the SRP server that matches given host name. 443 444 Example usage: 445 self.srp_server_get_host("my-host") 446 """ 447 for host in self.srp_server_get_hosts(): 448 if host_name == host['name']: 449 return host 450 451 def srp_server_get_services(self): 452 """Returns the service list on the SRP server as a list of property 453 dictionary. 454 455 Example output: 456 [{ 457 'fullname': 'my-service._ipps._tcp.default.service.arpa.', 458 'instance': 'my-service', 459 'name': '_ipps._tcp', 460 'deleted': 'false', 461 'port': '12345', 462 'priority': '0', 463 'weight': '0', 464 'ttl': '7200', 465 'TXT': ['abc=010203'], 466 'host_fullname': 'my-host.default.service.arpa.', 467 'host': 'my-host', 468 'addresses': ['2001::1', '2001::2'] 469 }] 470 471 Note that the TXT data is output as a HEX string. 472 """ 473 outputs = self.cli('srp server service') 474 service_list = [] 475 while outputs: 476 service = {} 477 service['fullname'] = outputs.pop(0).strip() 478 name_labels = service['fullname'].split('.') 479 service['instance'] = name_labels[0] 480 service['name'] = '.'.join(name_labels[1:3]) 481 service['deleted'] = outputs.pop(0).strip().split(':')[1].strip() 482 if service['deleted'] == 'true': 483 service_list.append(service) 484 continue 485 # 'subtypes', port', 'priority', 'weight', 'ttl' 486 for i in range(0, 5): 487 key_value = outputs.pop(0).strip().split(':') 488 service[key_value[0].strip()] = key_value[1].strip() 489 txt_entries = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') 490 txt_entries = map(str.strip, txt_entries) 491 service['TXT'] = [txt for txt in txt_entries if txt] 492 service['host_fullname'] = outputs.pop(0).strip().split(':')[1].strip() 493 service['host'] = service['host_fullname'].split('.')[0] 494 addresses = outputs.pop(0).strip().split('[')[1].strip(' ]').split(',') 495 addresses = map(str.strip, addresses) 496 service['addresses'] = [addr for addr in addresses if addr] 497 service_list.append(service) 498 return service_list 499 500 def srp_server_get_service(self, instance_name, service_name): 501 """Returns service on the SRP server that matches given instance 502 name and service name. 503 504 Example usage: 505 self.srp_server_get_service("my-service", "_ipps._tcp") 506 """ 507 for service in self.srp_server_get_services(): 508 if (instance_name == service['instance'] and service_name == service['name']): 509 return service 510 511 # ------------------------------------------------------------------------------------------------------------------ 512 # Helper methods 513 514 def form(self, network_name=None, network_key=None, channel=None, panid=0x1234, xpanid=None): 515 if network_name is not None: 516 self.set_network_name(network_name) 517 if network_key is not None: 518 self.set_network_key(network_key) 519 if channel is not None: 520 self.set_channel(channel) 521 if xpanid is not None: 522 self.set_ext_panid(xpanid) 523 self.set_panid(panid) 524 self.interface_up() 525 self.thread_start() 526 verify_within(_check_node_is_leader, self._WAIT_TIME, arg=self) 527 528 def join(self, node, type=JOIN_TYPE_ROUTER): 529 self.set_network_name(node.get_network_name()) 530 self.set_network_key(node.get_network_key()) 531 self.set_channel(node.get_channel()) 532 self.set_panid(node.get_panid()) 533 if type == JOIN_TYPE_END_DEVICE: 534 self.set_mode('rn') 535 elif type == JOIN_TYPE_SLEEPY_END_DEVICE: 536 self.set_mode('-') 537 else: 538 self.set_mode('rdn') 539 self.set_router_selection_jitter(1) 540 self.interface_up() 541 self.thread_start() 542 if type == JOIN_TYPE_ROUTER: 543 verify_within(_check_node_is_router, self._WAIT_TIME, arg=self) 544 else: 545 verify_within(_check_node_is_child, self._WAIT_TIME, arg=self) 546 547 def allowlist_node(self, node): 548 """Adds a given node to the allowlist of `self` and enables allowlisting on `self`""" 549 self._cli_no_output('macfilter addr add', node.get_ext_addr()) 550 self._cli_no_output('macfilter addr allowlist') 551 552 def un_allowlist_node(self, node): 553 """Removes a given node (of node `Node) from the allowlist""" 554 self._cli_no_output('macfilter addr remove', node.get_ext_addr()) 555 556 # ------------------------------------------------------------------------------------------------------------------ 557 # class methods 558 559 @classmethod 560 def finalize_all_nodes(cls): 561 """Finalizes all previously created `Node` instances (stops the CLI process)""" 562 for node in Node._all_nodes: 563 node._finalize() 564 565 @classmethod 566 def set_time_speedup_factor(cls, factor): 567 """Sets up the time speed up factor - should be set before creating any `Node` objects""" 568 if len(Node._all_nodes) != 0: 569 raise Node._NodeError('set_time_speedup_factor() cannot be called after creating a `Node`') 570 Node._SPEED_UP_FACTOR = factor 571 572 573def _check_node_is_leader(node): 574 verify(node.get_state() == 'leader') 575 576 577def _check_node_is_router(node): 578 verify(node.get_state() == 'router') 579 580 581def _check_node_is_child(node): 582 verify(node.get_state() == 'child') 583 584 585# ---------------------------------------------------------------------------------------------------------------------- 586 587 588class VerifyError(Exception): 589 pass 590 591 592_is_in_verify_within = False 593 594 595def verify(condition): 596 """Verifies that a `condition` is true, otherwise raises a VerifyError""" 597 global _is_in_verify_within 598 if not condition: 599 calling_frame = inspect.currentframe().f_back 600 error_message = 'verify() failed at line {} in "{}"'.format(calling_frame.f_lineno, 601 calling_frame.f_code.co_filename) 602 if not _is_in_verify_within: 603 print(error_message) 604 raise VerifyError(error_message) 605 606 607def verify_within(condition_checker_func, wait_time, arg=None, delay_time=0.1): 608 """Verifies that a given function `condition_checker_func` passes successfully within a given wait timeout. 609 `wait_time` is maximum time waiting for condition_checker to pass (in seconds). 610 `arg` is optional parameter and if it s not None, will be passed to `condition_checker_func()` 611 `delay_time` specifies a delay interval added between failed attempts (in seconds). 612 """ 613 global _is_in_verify_within 614 start_time = time.time() 615 old_is_in_verify_within = _is_in_verify_within 616 _is_in_verify_within = True 617 while True: 618 try: 619 if arg is None: 620 condition_checker_func() 621 else: 622 condition_checker_func(arg) 623 except VerifyError as e: 624 if time.time() - start_time > wait_time: 625 print('Took too long to pass the condition ({}>{} sec)'.format(time.time() - start_time, wait_time)) 626 print(e.message) 627 raise e 628 except BaseException: 629 raise 630 else: 631 break 632 if delay_time != 0: 633 time.sleep(delay_time) 634 _is_in_verify_within = old_is_in_verify_within 635