diff --git a/include/openthread/instance.h b/include/openthread/instance.h index ef6e2d4f85b..2444f50b82c 100644 --- a/include/openthread/instance.h +++ b/include/openthread/instance.h @@ -53,7 +53,7 @@ extern "C" { * @note This number versions both OpenThread platform and user APIs. * */ -#define OPENTHREAD_API_VERSION (428) +#define OPENTHREAD_API_VERSION (429) /** * @addtogroup api-instance diff --git a/include/openthread/thread_ftd.h b/include/openthread/thread_ftd.h index 14aab130762..8c5491c760f 100644 --- a/include/openthread/thread_ftd.h +++ b/include/openthread/thread_ftd.h @@ -484,13 +484,24 @@ otError otThreadBecomeRouter(otInstance *aInstance); /** * Become a leader and start a new partition. * - * @note This API is reserved for testing and demo purposes only. Changing settings with - * this API will render a production application non-compliant with the Thread Specification. + * If the device is not attached, this API will force the device to start as the leader of the network. This use case + * is only intended for testing and demo purposes, and using the API while the device is detached can make a production + * application non-compliant with the Thread Specification. + * + * If the device is already attached, this API can be used to try to take over as the leader, creating a new partition. + * For this to work, the local leader weight (`otThreadGetLocalLeaderWeight()`) must be larger than the weight of the + * current leader (`otThreadGetLeaderWeight()`). If it is not, `OT_ERROR_NOT_CAPABLE` is returned to indicate to the + * caller that they need to adjust the weight. + * + * Taking over the leader role in this way is only allowed when triggered by an explicit user action. Using this API + * without such user action can make a production application non-compliant with the Thread Specification. * * @param[in] aInstance A pointer to an OpenThread instance. * - * @retval OT_ERROR_NONE Successfully became a leader and started a new partition. + * @retval OT_ERROR_NONE Successfully became a leader and started a new partition, or was leader already. * @retval OT_ERROR_INVALID_STATE Thread is disabled. + * @retval OT_ERROR_NOT_CAPABLE Device cannot override the current leader due to its local leader weight being same + * or smaller than current leader's weight, or device is not router eligible. */ otError otThreadBecomeLeader(otInstance *aInstance); diff --git a/src/cli/README.md b/src/cli/README.md index deb90c3d794..ee440b1345a 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -3573,12 +3573,45 @@ offline, disabled, detached, child, router or leader Done ``` -### state +### state leader + +Become a leader and start a new partition + +If the device is not attached, this command will force the device to start as the leader of the network. This use case is only intended for testing and demo purposes, and using the API while the device is detached can make a production application non-compliant with the Thread Specification. -Try to switch to state `detached`, `child`, `router` or `leader`. +If the device is already attached, this API can be used to try to take over as the leader, creating a new partition. For this to work, the local leader weight (`leaderweight`) must be larger than the weight of the current leader (from `leaderdata`). If it is not, error `NotCapable` is outputted to indicate to the caller that they need to adjust the weight. + +Taking over the leader role in this way is only allowed when triggered by an explicit user action. Using this API without such user action can make a production application non-compliant with the Thread Specification. ```bash +> leaderdata +Partition ID: 1886755069 +Weighting: 65 +Data Version: 178 +Stable Data Version: 48 +Leader Router ID: 59 +Done + +> leaderweight +64 +Done + > state leader +Error 27: NotCapable + +> leaderweight 66 +Done + +> state leader +Done +``` + +### state + +Try to switch to state `detached`, `child`, `router`. + +```bash +> state detached Done ``` diff --git a/src/core/api/thread_ftd_api.cpp b/src/core/api/thread_ftd_api.cpp index 6b3f554ee06..6f6d266cbcc 100644 --- a/src/core/api/thread_ftd_api.cpp +++ b/src/core/api/thread_ftd_api.cpp @@ -202,7 +202,7 @@ otError otThreadBecomeRouter(otInstance *aInstance) otError otThreadBecomeLeader(otInstance *aInstance) { - return AsCoreType(aInstance).Get().BecomeLeader(); + return AsCoreType(aInstance).Get().BecomeLeader(/* aCheckWeight */ true); } uint8_t otThreadGetRouterDowngradeThreshold(otInstance *aInstance) diff --git a/src/core/thread/mle.cpp b/src/core/thread/mle.cpp index c614ed0602d..efe9e593b2c 100644 --- a/src/core/thread/mle.cpp +++ b/src/core/thread/mle.cpp @@ -1548,7 +1548,7 @@ uint32_t Mle::Reattach(void) IgnoreError(BecomeDetached()); } #if OPENTHREAD_FTD - else if (IsFullThreadDevice() && Get().BecomeLeader() == kErrorNone) + else if (IsFullThreadDevice() && Get().BecomeLeader(/* aCheckWeight */ false) == kErrorNone) { // do nothing } diff --git a/src/core/thread/mle_router.cpp b/src/core/thread/mle_router.cpp index d4d1878d561..fcb4595de75 100644 --- a/src/core/thread/mle_router.cpp +++ b/src/core/thread/mle_router.cpp @@ -267,7 +267,7 @@ Error MleRouter::BecomeRouter(ThreadStatusTlv::Status aStatus) return error; } -Error MleRouter::BecomeLeader(void) +Error MleRouter::BecomeLeader(bool aCheckWeight) { Error error = kErrorNone; Router *router; @@ -283,6 +283,11 @@ Error MleRouter::BecomeLeader(void) VerifyOrExit(!IsLeader(), error = kErrorNone); VerifyOrExit(IsRouterEligible(), error = kErrorNotCapable); + if (aCheckWeight && IsAttached()) + { + VerifyOrExit(mLeaderWeight > mLeaderData.GetWeighting(), error = kErrorNotCapable); + } + mRouterTable.Clear(); #if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE diff --git a/src/core/thread/mle_router.hpp b/src/core/thread/mle_router.hpp index 8a43b1941fc..2e52cff3d64 100644 --- a/src/core/thread/mle_router.hpp +++ b/src/core/thread/mle_router.hpp @@ -135,14 +135,22 @@ class MleRouter : public Mle Error BecomeRouter(ThreadStatusTlv::Status aStatus); /** - * Causes the Thread interface to become a Leader and start a new partition. + * Becomes a leader and starts a new partition. + * + * If the device is already attached, this method can be used to attempt to take over as the leader, creating a new + * partition. For this to work, the local leader weight must be greater than the weight of the current leader. The + * @p aCheckWeight can be used to ensure that this check is performed. + * + * @param[in] aCheckWeight Check that the local leader weight is larger than the weight of the current leader. * * @retval kErrorNone Successfully become a Leader and started a new partition. - * @retval kErrorNotCapable Device is not capable of becoming a leader - * @retval kErrorInvalidState Thread is not enabled + * @retval kErrorInvalidState Thread is not enabled. + * @retval kErrorNotCapable Device is not capable of becoming a leader (not router eligible), or + * @p aCheckWeight is true and cannot override the current leader due to its local + * leader weight being same or smaller than current leader's weight. * */ - Error BecomeLeader(void); + Error BecomeLeader(bool aCheckWeight); #if OPENTHREAD_CONFIG_MLE_DEVICE_PROPERTY_LEADER_WEIGHT_ENABLE /** diff --git a/tests/toranj/cli/cli.py b/tests/toranj/cli/cli.py index 308a39867ae..06f7bd3c97b 100644 --- a/tests/toranj/cli/cli.py +++ b/tests/toranj/cli/cli.py @@ -339,6 +339,12 @@ def get_ip_maddrs(self): def add_ip_maddr(self, maddr): return self._cli_no_output('ipmaddr add', maddr) + def get_leader_weight(self): + return self._cli_single_output('leaderweight') + + def set_leader_weight(self, weight): + self._cli_no_output('leaderweight', weight) + def get_pollperiod(self): return self._cli_single_output('pollperiod') diff --git a/tests/toranj/cli/test-032-leader-take-over.py b/tests/toranj/cli/test-032-leader-take-over.py new file mode 100755 index 00000000000..f5930122747 --- /dev/null +++ b/tests/toranj/cli/test-032-leader-take-over.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2023, 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. + +from cli import verify +from cli import verify_within +import cli +import time + +# ----------------------------------------------------------------------------------------------------------------------- +# Test description: + +# This test covers behavior of leader take over (an already attached device +# trying to form their own partition and taking over the leader role). +# + +test_name = __file__[:-3] if __file__.endswith('.py') else __file__ +print('-' * 120) +print('Starting \'{}\''.format(test_name)) + +# ----------------------------------------------------------------------------------------------------------------------- +# Creating `cli.Nodes` instances + +speedup = 25 +cli.Node.set_time_speedup_factor(speedup) + +node1 = cli.Node() +node2 = cli.Node() +node3 = cli.Node() +child2 = cli.Node() + +# ----------------------------------------------------------------------------------------------------------------------- +# Form topology + +node1.form('lto') +node2.join(node1) +node3.join(node1) + +child2.allowlist_node(node2) +child2.join(node2, cli.JOIN_TYPE_REED) + +verify(node1.get_state() == 'leader') +verify(node2.get_state() == 'router') +verify(node3.get_state() == 'router') +verify(child2.get_state() == 'child') + +# ----------------------------------------------------------------------------------------------------------------------- +# Test Implementation + +node1.set_router_selection_jitter(1) + +n1_weight = int(node1.get_leader_weight()) + +node2.set_leader_weight(n1_weight) + +#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Make sure we get `NonCapable` if local leader weight same as current leader's weight + +error = None +try: + node2.cli('state leader') +except cli.CliError as e: + error = e + +verify(error.message == 'NotCapable') + +#- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Update local leader weight and try to take over the leader role on `node2`. + +node2.set_leader_weight(n1_weight + 1) + +old_partition_id = int(node2.get_partition_id()) + +node2.cli('state leader') + +new_partition_id = int(node2.get_partition_id()) +verify(new_partition_id != old_partition_id) + + +def check_leader_switch(): + for node in [node1, node2, node3, child2]: + verify(int(node.get_partition_id()) == new_partition_id) + verify(node1.get_state() == 'router') + verify(node2.get_state() == 'leader') + verify(node3.get_state() == 'router') + verify(child2.get_state() == 'child') + + +verify_within(check_leader_switch, 30) + +# ----------------------------------------------------------------------------------------------------------------------- +# Test finished + +cli.Node.finalize_all_nodes() + +print('\'{}\' passed.'.format(test_name)) diff --git a/tests/toranj/start.sh b/tests/toranj/start.sh index 495af398cea..d242752b9fd 100755 --- a/tests/toranj/start.sh +++ b/tests/toranj/start.sh @@ -195,7 +195,8 @@ if [ "$TORANJ_CLI" = 1 ]; then run cli/test-028-border-agent-ephemeral-key.py run cli/test-029-pending-dataset-key-change.py run cli/test-030-anycast-forwarding.py - run cli/./test-031-service-aloc-route-lookup.py + run cli/test-031-service-aloc-route-lookup.py + run cli/test-032-leader-take-over.py run cli/test-400-srp-client-server.py run cli/test-401-srp-server-address-cache-snoop.py run cli/test-500-two-brs-two-networks.py