diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index d7fc35da9..b7dd78944 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -122,8 +122,8 @@ run test-006-traffic-router-end-device.py run test-007-traffic-router-sleepy.py run test-008-permit-join.py run test-009-insecure-traffic-join.py +run test-010-on-mesh-prefix-config-gateway.py run test-100-mcu-power-state.py - run test-600-channel-manager-properties.py run test-601-channel-manager-channel-change.py run test-602-channel-manager-channel-select.py diff --git a/tests/toranj/test-010-on-mesh-prefix-config-gateway.py b/tests/toranj/test-010-on-mesh-prefix-config-gateway.py new file mode 100644 index 000000000..bae612bd7 --- /dev/null +++ b/tests/toranj/test-010-on-mesh-prefix-config-gateway.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# +# Copyright (c) 2018, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import time +import wpan +from wpan import verify + +#----------------------------------------------------------------------------------------------------------------------- +# Test description: +# +# Adding on-mesh prefix and `config-gateway` command +# +# - Verifies adding an on-mesh prefix using wpantund/wpanctl `config-gateway` command. +# - Verifies prefixes with different flags/priorities added on routers and/or end-devices. +# - Verifies `wpantund` adding SLAAC based IPv6 address based on prefix. +# - Verified `wpantund` retaining user-added prefixes and adding them back after an NCP reset. +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print '-' * 120 +print 'Starting \'{}\''.format(test_name) + +#----------------------------------------------------------------------------------------------------------------------- +# Utility functions + +def verify_prefix(node_list, prefix, on_mesh=True, slaac=True, def_route=False, priority='med'): + """ + This function verifies that all the nodes in the given `node_list` contain an IPv6 address with the given prefix, + and that the given prefix is present in the on-mesh prefixes list with the given flags (on-mesh, slaac, etc). + """ + for node in node_list: + all_addrs = wpan.parse_list(node.get(wpan.WPAN_IP6_ALL_ADDRESSES)) + verify(any([addr.startswith(prefix[:-1]) for addr in all_addrs])) + + prefixes = wpan.parse_on_mesh_prefix_result(node.get(wpan.WPAN_THREAD_ON_MESH_PREFIXES)) + for p in prefixes: + if p.prefix == prefix: + verify(p.prefix_len == '64') + verify(p.is_stable()) + verify(p.is_on_mesh() == on_mesh) + verify(p.is_def_route() == def_route) + verify(p.is_slaac() == slaac) + verify(p.is_dhcp() == False) + verify(p.priority == priority) + + +#----------------------------------------------------------------------------------------------------------------------- +# Creating `wpan.Nodes` instances + +speedup = 4 +wpan.Node.set_time_speedup_factor(speedup) + +r1 = wpan.Node() +r2 = wpan.Node() +sc1 = wpan.Node() +sc2 = wpan.Node() + +all_nodes = [r1, r2, sc1, sc2] + +#----------------------------------------------------------------------------------------------------------------------- +# Init all nodes + +wpan.Node.init_all_nodes() + +for node in all_nodes: + node.set(wpan.WPAN_OT_LOG_LEVEL, '0') + +#----------------------------------------------------------------------------------------------------------------------- +# Build network topology + +r1.whitelist_node(r2) +r2.whitelist_node(r1) + +r1.whitelist_node(sc1) +r2.whitelist_node(sc2) + +r1.form('ipv6-address') +r2.join_node(r1, node_type=wpan.JOIN_TYPE_ROUTER) +sc1.join_node(r1, node_type=wpan.JOIN_TYPE_SLEEPY_END_DEVICE) +sc2.join_node(r2, node_type=wpan.JOIN_TYPE_SLEEPY_END_DEVICE) + +sc1.set(wpan.WPAN_POLL_INTERVAL, '200') +sc2.set(wpan.WPAN_POLL_INTERVAL, '200') + +#----------------------------------------------------------------------------------------------------------------------- +# Test implementation + +prefix1 = 'fd00:abba:cafe::' +prefix2 = 'fd00:1234::' +prefix3 = 'fd00:deed::' + +# Add on-mesh prefix1 on router r1 +r1.config_gateway(prefix1) +time.sleep(0.5) + +# Verify that the prefix1 and its corresponding address are present on all nodes +verify_prefix(all_nodes, prefix1, def_route=False) + +# Now add prefix2 with priority `high` on router r2 and check all nodes for the new prefix/address +r2.config_gateway(prefix2, default_route=True, priority='1') +time.sleep(0.5) +verify_prefix(all_nodes, prefix2, def_route=True, priority='high') + +# Add prefix3 on sleepy end-device and check for it on all nodes +sc1.config_gateway(prefix3, priority='-1') +time.sleep(0.5) +verify_prefix(all_nodes, prefix3, def_route=False, priority='low') + +# Verify that prefix1 is retained by `wpantund` and pushed to NCP after a reset +r1.reset() + +# Wait for r1 to recover after reset +start_time = time.time() +wait_time = 5 +while not r1.is_associated(): + if time.time() - start_time > wait_time: + print 'Took too long for node to recover after reset ({}>{} sec)'.format(time.time() - start_time, wait_time) + exit(1) + time.sleep(0.25) + +# Wait for on-mesh prefix to be updated +time.sleep(0.5) +verify_prefix(all_nodes, prefix1, def_route=False) + +#----------------------------------------------------------------------------------------------------------------------- +# Test finished + +print '\'{}\' passed.'.format(test_name) diff --git a/tests/toranj/wpan.py b/tests/toranj/wpan.py index cf504bbea..7ace9ef04 100644 --- a/tests/toranj/wpan.py +++ b/tests/toranj/wpan.py @@ -29,6 +29,7 @@ import sys import time +import re import random import weakref import subprocess @@ -355,9 +356,10 @@ class Node(object): (' {}'.format(port) if port is not None else '') + traffic_type) - def config_gateway(self, prefix, default_route=False): + def config_gateway(self, prefix, default_route=False, priority=None): return self.wpanctl('config-gateway ' + prefix + - (' -d' if default_route else '')) + (' -d' if default_route else '') + + (' -P {}'.format(priority) if priority is not None else '')) def add_route(self, route_prefix, prefix_len_in_bytes=None, priority=None): """route priority [(>0 for high, 0 for medium, <0 for low)]""" @@ -540,7 +542,7 @@ def _is_ipv6_addr_link_local(ip_addr): def _create_socket_address(ip_address, port): """Convert a given IPv6 address (string) and port number into a socket address""" - # `socket.getaddrinfo() returns a list of `(family, socktype, proto, canonname, sockaddr)` where `sockaddr` + # `socket.getaddrinfo()` returns a list of `(family, socktype, proto, canonname, sockaddr)` where `sockaddr` # (at index 4) can be used as input in socket methods (like `sendto()`, `bind()`, etc.). return socket.getaddrinfo(ip_address, port)[0][4] @@ -829,4 +831,93 @@ def parse_scan_result(scan_result): """ Parses scan result string and returns an array of `ScanResult` objects""" return [ ScanResult(item) for item in scan_result.split('\n')[2:] ] # skip first two lines which are table headers +def parse_list(list_string): + """ + Parses IPv6/prefix/route list string (output of wpanctl get for properties WPAN_IP6_ALL_ADDRESSES, + IP6_MULTICAST_ADDRESSES, WPAN_THREAD_ON_MESH_PREFIXES, ...) + Returns an array of strings each containing an IPv6/prefix/route entry. + """ + # List string example (get(WPAN_IP6_ALL_ADDRESSES) output): + # + # '[\n + # \t"fdf4:5632:4940:0:8798:8701:85d4:e2be prefix_len:64 origin:ncp valid:forever preferred:forever"\n + # \t"fe80::2092:9358:97ea:71c6 prefix_len:64 origin:ncp valid:forever preferred:forever"\n + # ]' + # + # We split the lines ('\n' as separator) and skip the first and last lines which are '[' and ']'. + # For each line, skip the first two characters (which are '\t"') and last character ('"'), then split the string + # using whitespace as separator. The first entry is the IPv6 address. + # + return [line[2:-1].split()[0] for line in list_string.split('\n')[1:-1]] +class OnMeshPrefix(object): + """ This object encapsulates an on-mesh prefix""" + + def __init__(self, text): + + # Example of expected text: + # + # '\t"fd00:abba:cafe:: prefix_len:64 origin:user stable:yes flags:0x31' + # ' [on-mesh:1 def-route:0 config:0 dhcp:0 slaac:1 pref:1 prio:med]"' + + m = re.match('\t"([0-9a-fA-F:]+)\s*prefix_len:(\d+)\s+origin:(\w*)\s+stable:(\w*).*' + + '\[on-mesh:(\d)\s+def-route:(\d)\s+config:(\d)\s+dhcp:(\d)\s+slaac:(\d)\s+pref:(\d)\s+prio:(\w*)\]', + text) + verify(m is not None) + data = m.groups() + + self._prefix = data[0] + self._prefix_len = data[1] + self._origin = data[2] + self._stable = (data[3] == 'yes') + self._on_mesh = (data[4] == '1') + self._def_route = (data[5] == '1') + self._config = (data[6] == '1') + self._dhcp = (data[7] == '1') + self._slaac = (data[8] == '1') + self._preffered = (data[9] == '1') + self._priority = (data[10]) + + @property + def prefix(self): + return self._prefix + + @property + def prefix_len(self): + return self._prefix_len + + @property + def origin(self): + return self._origin + + @property + def priority(self): + return self._priority + + def is_stable(self): + return self._stable + + def is_on_mesh(self): + return self._on_mesh + + def is_def_route(self): + return self._def_route + + def is_config(self): + return self._config + + def is_dhcp(self): + return self._dhcp + + def is_slaac(self): + return self._slaac + + def is_preffered(self): + return self._preffered + + def __repr__(self): + return 'OnMeshPrefix({})'.format(self.__dict__) + +def parse_on_mesh_prefix_result(on_mesh_prefix_list): + """ Parses on-mesh prefix list string and returns an array of `OnMeshPrefix` objects""" + return [ OnMeshPrefix(item) for item in on_mesh_prefix_list.split('\n')[1:-1] ] \ No newline at end of file