Skip to content

Commit

Permalink
[mle] update otThreadBecomeLeader() to allow leader take over (open…
Browse files Browse the repository at this point in the history
…thread#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.
  • Loading branch information
abtink authored Jul 18, 2024
1 parent 171f94c commit 6f30a3f
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 14 deletions.
2 changes: 1 addition & 1 deletion include/openthread/instance.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 14 additions & 3 deletions include/openthread/thread_ftd.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
37 changes: 35 additions & 2 deletions src/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3573,12 +3573,45 @@ offline, disabled, detached, child, router or leader
Done
```
### state <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 <state>
Try to switch to state `detached`, `child`, `router`.
```bash
> state detached
Done
```
Expand Down
2 changes: 1 addition & 1 deletion src/core/api/thread_ftd_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/core/thread/mle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
7 changes: 6 additions & 1 deletion src/core/thread/mle_router.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
16 changes: 12 additions & 4 deletions src/core/thread/mle_router.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
/**
Expand Down
6 changes: 6 additions & 0 deletions tests/toranj/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
120 changes: 120 additions & 0 deletions tests/toranj/cli/test-032-leader-take-over.py
Original file line number Diff line number Diff line change
@@ -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))
3 changes: 2 additions & 1 deletion tests/toranj/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6f30a3f

Please sign in to comment.