[mle] update otThreadBecomeLeader() to allow leader take over (#9186)

This commit updates the `otThreadBecomeLeader()` API (and its related
core `MleRouter::BecomeLeader()` method) to allow an already attached
device to take over as leader, creating a new partition. For this to
work, the local leader weight (`otThreadGetLocalLeaderWeight()`) must
be greater than the weight of the current leader (which can be
retrieved using `otThreadGetLeaderWeight()`). If it is not, error code
`OT_ERROR_NOT_CAPABLE` is returned to indicate to the caller that they
need to adjust the local weight.

This commit also updates the related CLI command and adds a new test,
`test-032-leader-take-over.py`, to validate the newly added
mechanism.
This commit is contained in:
Abtin Keshavarzian
2024-07-17 19:43:09 -07:00
committed by GitHub
parent 171f94c307
commit 6f30a3f913
10 changed files with 198 additions and 14 deletions
+1 -1
View File
@@ -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
+14 -3
View File
@@ -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);
+35 -2
View File
@@ -3573,12 +3573,45 @@ offline, disabled, detached, child, router or leader
Done
```
### state <state>
### state leader
Try to switch to state `detached`, `child`, `router` or `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.
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 <state>
Try to switch to state `detached`, `child`, `router`.
```bash
> state detached
Done
```
+1 -1
View File
@@ -202,7 +202,7 @@ otError otThreadBecomeRouter(otInstance *aInstance)
otError otThreadBecomeLeader(otInstance *aInstance)
{
return AsCoreType(aInstance).Get<Mle::MleRouter>().BecomeLeader();
return AsCoreType(aInstance).Get<Mle::MleRouter>().BecomeLeader(/* aCheckWeight */ true);
}
uint8_t otThreadGetRouterDowngradeThreshold(otInstance *aInstance)
+1 -1
View File
@@ -1548,7 +1548,7 @@ uint32_t Mle::Reattach(void)
IgnoreError(BecomeDetached());
}
#if OPENTHREAD_FTD
else if (IsFullThreadDevice() && Get<MleRouter>().BecomeLeader() == kErrorNone)
else if (IsFullThreadDevice() && Get<MleRouter>().BecomeLeader(/* aCheckWeight */ false) == kErrorNone)
{
// do nothing
}
+6 -1
View File
@@ -267,7 +267,7 @@ exit:
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
+12 -4
View File
@@ -135,14 +135,22 @@ public:
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
/**
+6
View File
@@ -339,6 +339,12 @@ class Node(object):
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')
+120
View File
@@ -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))
+2 -1
View File
@@ -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