examples: add "examples/python/gi/nm-up-many.py"
It's an example for how to use libnm and asynchronous API. But it's also a script I will use to test activating many profiles in parallel. Also add a test script that creates many veth interfaces and connection profiles. So now you can do: sudo NUM_DEVS=100 contrib/scripts/test-create-many-device-setup.sh setup ./examples/python/gi/nm-up-many.py c-a{1..100} and cleanup with nmcli connection down c-a{1..100} sudo contrib/scripts/test-create-many-device-setup.sh cleanup Of course, be careful to do this on your production machine.
This commit is contained in:
@@ -187,6 +187,7 @@ EXTRA_DIST += \
|
|||||||
examples/python/gi/nm-add-connection2.py \
|
examples/python/gi/nm-add-connection2.py \
|
||||||
examples/python/gi/nm-connection-update-stable-id.py \
|
examples/python/gi/nm-connection-update-stable-id.py \
|
||||||
examples/python/gi/nm-keyfile.py \
|
examples/python/gi/nm-keyfile.py \
|
||||||
|
examples/python/gi/nm-up-many.py \
|
||||||
examples/python/gi/nm-update2.py \
|
examples/python/gi/nm-update2.py \
|
||||||
examples/python/gi/nm-wg-set \
|
examples/python/gi/nm-wg-set \
|
||||||
examples/python/gi/ovs-external-ids.py \
|
examples/python/gi/ovs-external-ids.py \
|
||||||
|
117
contrib/scripts/test-create-many-device-setup.sh
Executable file
117
contrib/scripts/test-create-many-device-setup.sh
Executable file
@@ -0,0 +1,117 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf '%s\n' "$*" >&1
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ARG_OP="$1"
|
||||||
|
shift
|
||||||
|
test -n "$ARG_OP" || die "specify the operation (setup, cleanup)"
|
||||||
|
|
||||||
|
test "$USER" = root || die "must run as root"
|
||||||
|
|
||||||
|
NUM_DEVS="${NUM_DEVS:-50}"
|
||||||
|
|
||||||
|
|
||||||
|
DNSMASQ_PIDFILE="/tmp/nm-test-create-many-device-setup.dnsmasq.pid"
|
||||||
|
NM_TEST_CONF="/etc/NetworkManager/conf.d/99-my-test.conf"
|
||||||
|
TEST_NETNS="T"
|
||||||
|
|
||||||
|
|
||||||
|
_dnsmasq_kill() {
|
||||||
|
pkill -F "$DNSMASQ_PIDFILE"
|
||||||
|
rm -rf "$DNSMASQ_PIDFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
_link_delete_all() {
|
||||||
|
ip link | sed -n 's/^[0-9]\+:.*\(t-[^@:]\+\)@.*/\1/p' | xargs -n 1 ip link delete
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_base() {
|
||||||
|
ip netns delete "$TEST_NETNS"
|
||||||
|
_dnsmasq_kill
|
||||||
|
_link_delete_all
|
||||||
|
rm -rf "$NM_TEST_CONF"
|
||||||
|
rm -rf /run/NetworkManager/system-connections/c-*.nmconnection
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_cleanup() {
|
||||||
|
systemctl stop NetworkManager
|
||||||
|
cleanup_base
|
||||||
|
systemctl unmask NetworkManager-dispatcher
|
||||||
|
systemctl enable NetworkManager-dispatcher
|
||||||
|
systemctl start NetworkManager
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_setup() {
|
||||||
|
|
||||||
|
systemctl stop NetworkManager
|
||||||
|
systemctl mask NetworkManager-dispatcher
|
||||||
|
systemctl stop NetworkManager-dispatcher
|
||||||
|
|
||||||
|
cleanup_base
|
||||||
|
|
||||||
|
ip netns add "$TEST_NETNS"
|
||||||
|
ip --netns "$TEST_NETNS" link add t-br0 type bridge
|
||||||
|
ip --netns "$TEST_NETNS" link set t-br0 type bridge stp_state 0
|
||||||
|
ip --netns "$TEST_NETNS" link set t-br0 up
|
||||||
|
ip --netns "$TEST_NETNS" addr add 172.16.0.1/16 dev t-br0
|
||||||
|
ip netns exec "$TEST_NETNS" \
|
||||||
|
dnsmasq \
|
||||||
|
--conf-file=/dev/null \
|
||||||
|
--pid-file="$DNSMASQ_PIDFILE" \
|
||||||
|
--no-hosts \
|
||||||
|
--keep-in-foreground \
|
||||||
|
--bind-interfaces \
|
||||||
|
--except-interface=lo \
|
||||||
|
--clear-on-reload \
|
||||||
|
--listen-address=172.16.0.1 \
|
||||||
|
--dhcp-range=172.16.1.1,172.16.20.1,60 \
|
||||||
|
--no-ping \
|
||||||
|
&
|
||||||
|
disown
|
||||||
|
for i in `seq "$NUM_DEVS"`; do
|
||||||
|
ip --netns "$TEST_NETNS" link add t-a$i type veth peer t-b$i
|
||||||
|
ip --netns "$TEST_NETNS" link set t-a$i up
|
||||||
|
ip --netns "$TEST_NETNS" link set t-b$i up master t-br0
|
||||||
|
done
|
||||||
|
|
||||||
|
cat <<EOF > "$NM_TEST_CONF"
|
||||||
|
[main]
|
||||||
|
dhcp=internal
|
||||||
|
no-auto-default=interface-name:t-a*
|
||||||
|
[device-99-my-test]
|
||||||
|
match-device=interface-name:t-a*
|
||||||
|
managed=1
|
||||||
|
[logging]
|
||||||
|
level=INFO
|
||||||
|
[connectivity]
|
||||||
|
enabled=0
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl start NetworkManager
|
||||||
|
|
||||||
|
for i in `seq "$NUM_DEVS"`; do
|
||||||
|
ip --netns "$TEST_NETNS" link set t-a$i netns $$
|
||||||
|
done
|
||||||
|
|
||||||
|
for i in `seq "$NUM_DEVS"`; do
|
||||||
|
nmcli connection add save no type ethernet con-name c-a$i ifname t-a$i autoconnect no ipv4.method auto ipv6.method auto
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
case "$ARG_OP" in
|
||||||
|
"setup")
|
||||||
|
cmd_setup
|
||||||
|
;;
|
||||||
|
"cleanup")
|
||||||
|
cmd_cleanup
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unknown command \"$ARG_OP\""
|
||||||
|
;;
|
||||||
|
esac
|
373
examples/python/gi/nm-up-many.py
Executable file
373
examples/python/gi/nm-up-many.py
Executable file
@@ -0,0 +1,373 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
|
||||||
|
# A example script to activate many profiles in parallel.
|
||||||
|
#
|
||||||
|
# It uses entirely asynchronous API. At various points the
|
||||||
|
# script explicitly iterates the main context, which is unlike
|
||||||
|
# a more complex application that uses the GMainContext, which
|
||||||
|
# probably would run the context only at one point as long as
|
||||||
|
# the application is running (from the main function).
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import gi
|
||||||
|
import time
|
||||||
|
|
||||||
|
gi.require_version("NM", "1.0")
|
||||||
|
from gi.repository import NM, GLib, Gio
|
||||||
|
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
class MyError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg):
|
||||||
|
# use nm_utils_print(), so that the log messages are in synch with
|
||||||
|
# LIBNM_CLIENT_DEBUG=trace messages.
|
||||||
|
NM.utils_print(0, "[%015.10f] %s\n" % (time.monotonic() - start_time, msg))
|
||||||
|
|
||||||
|
|
||||||
|
def nmc_new(io_priority=GLib.PRIORITY_DEFAULT, cancellable=None):
|
||||||
|
# create a NMClient instance using the async initialization
|
||||||
|
# (but the function itself iterates the main context until
|
||||||
|
# the initialization completes).
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def cb(source_object, res):
|
||||||
|
|
||||||
|
try:
|
||||||
|
source_object.init_finish(res)
|
||||||
|
except Exception as e:
|
||||||
|
result.append(e)
|
||||||
|
else:
|
||||||
|
result.append(None)
|
||||||
|
|
||||||
|
nmc = NM.Client()
|
||||||
|
nmc.init_async(io_priority, cancellable, cb)
|
||||||
|
while not result:
|
||||||
|
nmc.get_main_context().iteration(may_block=True)
|
||||||
|
|
||||||
|
if result[0]:
|
||||||
|
raise result[0]
|
||||||
|
|
||||||
|
log("initialized NMClient cache")
|
||||||
|
|
||||||
|
return nmc
|
||||||
|
|
||||||
|
|
||||||
|
def nmc_destroy(nmc_transfer_ref):
|
||||||
|
|
||||||
|
# Just for fun, show how to completely cleanup a NMClient instance.
|
||||||
|
# An NMClient instance registers D-Bus signals and unrefing the instance
|
||||||
|
# will cancel/unsubscribe those signals, but there might still be some
|
||||||
|
# pending operations scheduled on the main context. That means, after
|
||||||
|
# unrefing the NMClient instance, we may need to iterate the GMainContext
|
||||||
|
# a bit longer, go get rid of all resources (otherwise, the GMainContext
|
||||||
|
# itself cannot be destroyed and leaks).
|
||||||
|
#
|
||||||
|
# We can use nm_client_get_context_busy_watcher() for that, by subscribing
|
||||||
|
# a weak reference and iterating the context as long as the object is
|
||||||
|
# alive.
|
||||||
|
|
||||||
|
nmc = nmc_transfer_ref[0]
|
||||||
|
del nmc_transfer_ref[0]
|
||||||
|
|
||||||
|
alive = [1]
|
||||||
|
|
||||||
|
def weak_ref_cb(alive):
|
||||||
|
del alive[0]
|
||||||
|
|
||||||
|
nmc.get_context_busy_watcher().weak_ref(weak_ref_cb, alive)
|
||||||
|
main_context = nmc.get_main_context()
|
||||||
|
|
||||||
|
del nmc
|
||||||
|
|
||||||
|
while alive:
|
||||||
|
main_context.iteration(may_block=True)
|
||||||
|
|
||||||
|
log("NMClient instance cleaned up")
|
||||||
|
|
||||||
|
|
||||||
|
def find_connections(nmc, argv):
|
||||||
|
|
||||||
|
# parse the inpurt argv and select the connection profiles to activate.
|
||||||
|
# The arguments are either "connection.id" or "connection.uuid", possibly
|
||||||
|
# qualified by "id" or "uuid".
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if not argv:
|
||||||
|
break
|
||||||
|
arg_type = argv.pop(0)
|
||||||
|
if arg_type in ["id", "uuid"]:
|
||||||
|
if not argv:
|
||||||
|
raise MyError('missing specifier after "%s"' % (arg_type))
|
||||||
|
arg_param = argv.pop(0)
|
||||||
|
else:
|
||||||
|
arg_param = arg_type
|
||||||
|
arg_type = "*"
|
||||||
|
|
||||||
|
cc = []
|
||||||
|
for c in nmc.get_connections():
|
||||||
|
if arg_type in ["id", "*"] and arg_param == c.get_id():
|
||||||
|
cc.append(c)
|
||||||
|
if arg_type in ["uuid", "*"] and arg_param == c.get_uuid():
|
||||||
|
cc.append(c)
|
||||||
|
|
||||||
|
if not cc:
|
||||||
|
raise MyError(
|
||||||
|
'Could not find a matching connection "%s" "%s"' % (arg_type, arg_param)
|
||||||
|
)
|
||||||
|
if len(cc) > 1:
|
||||||
|
raise MyError(
|
||||||
|
'Could not find a unique matching connection "%s" "%s", instead %d profiles found'
|
||||||
|
% (arg_type, arg_param, len(cc))
|
||||||
|
)
|
||||||
|
|
||||||
|
if cc[0] not in result:
|
||||||
|
# we allow duplicates, but combine them.
|
||||||
|
result.extend(cc)
|
||||||
|
|
||||||
|
for c in result:
|
||||||
|
log(
|
||||||
|
"requested connection: %s (%s) (%s)"
|
||||||
|
% (c.get_id(), c.get_uuid(), c.get_path())
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def nmc_activate_start(nmc, con):
|
||||||
|
|
||||||
|
# Call nmc.activate_connection_async() and return a user data
|
||||||
|
# with the information about the pending operation.
|
||||||
|
|
||||||
|
activation = {
|
||||||
|
"con": con,
|
||||||
|
"result": None,
|
||||||
|
"result_msg": None,
|
||||||
|
"result_ac": None,
|
||||||
|
"ac_result": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
log("activation %s (%s) start asynchronously" % (con.get_id(), con.get_uuid()))
|
||||||
|
|
||||||
|
def cb(source_object, res, activation):
|
||||||
|
# The callback does not call other code for signaling the
|
||||||
|
# completion. Instead, we remember in "activation" that
|
||||||
|
# the callback was completed.
|
||||||
|
#
|
||||||
|
# Other code will repeatedly go through the "activation_list"
|
||||||
|
# and find those that are completed (nmc_activate_find_completed()).
|
||||||
|
try:
|
||||||
|
ac = nmc.activate_connection_finish(res)
|
||||||
|
except Exception as e:
|
||||||
|
activation["result"] = False
|
||||||
|
activation["result_msg"] = str(e)
|
||||||
|
else:
|
||||||
|
activation["result"] = True
|
||||||
|
activation["result_msg"] = "success"
|
||||||
|
activation["result_ac"] = ac
|
||||||
|
|
||||||
|
nmc.activate_connection_async(con, None, None, None, cb, activation)
|
||||||
|
|
||||||
|
return activation
|
||||||
|
|
||||||
|
|
||||||
|
def nmc_activate_find_completed(activation_list):
|
||||||
|
|
||||||
|
# Iterate over list of "activation" data, find the first
|
||||||
|
# one that is completed, remove it from the list and return
|
||||||
|
# it.
|
||||||
|
|
||||||
|
for idx, activation in enumerate(activation_list):
|
||||||
|
if activation["result"] is not None:
|
||||||
|
del activation_list[idx]
|
||||||
|
return activation
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def nmc_activate_complete(
|
||||||
|
nmc, activation_list, completed_list, num_parallel_invocations
|
||||||
|
):
|
||||||
|
|
||||||
|
# We schedule activations asynchronously and in parallel. However, we
|
||||||
|
# still want to rate limit the number of parallel activations. This
|
||||||
|
# function does that: if there are more than "num_parallel_invocations" activations
|
||||||
|
# in progress, then wait until the excess number of them completed.
|
||||||
|
# The completed ones move from "activation_list" over to "completed_list".
|
||||||
|
|
||||||
|
completed = 0
|
||||||
|
while True:
|
||||||
|
|
||||||
|
need_to_wait = len(activation_list) > num_parallel_invocations
|
||||||
|
|
||||||
|
# Even if we don't need to wait (that is, the list of pending activations
|
||||||
|
# is reasonably short), we still tentatively iterate the GMainContext a bit.
|
||||||
|
if not nmc.get_main_context().iteration(may_block=need_to_wait):
|
||||||
|
if need_to_wait:
|
||||||
|
continue
|
||||||
|
# Ok, nothing ready yet.
|
||||||
|
break
|
||||||
|
|
||||||
|
# this is not efficient after each iteration(), but it's good enough.
|
||||||
|
# The activation list is supposed to be short.
|
||||||
|
activation = nmc_activate_find_completed(activation_list)
|
||||||
|
|
||||||
|
if activation is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
con = activation["con"]
|
||||||
|
log(
|
||||||
|
"activation %s (%s) start complete: %s%s"
|
||||||
|
% (
|
||||||
|
con.get_id(),
|
||||||
|
con.get_uuid(),
|
||||||
|
activation["result_msg"],
|
||||||
|
(
|
||||||
|
""
|
||||||
|
if not activation["result"]
|
||||||
|
else (" (%s)" % (activation["result_ac"].get_path()))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
completed += 1
|
||||||
|
|
||||||
|
completed_list.append(activation)
|
||||||
|
|
||||||
|
if completed > 0:
|
||||||
|
log(
|
||||||
|
"completed %d activations, %d activations still pending"
|
||||||
|
% (completed, len(activation_list))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def nmc_activate_all(nmc, cons):
|
||||||
|
|
||||||
|
# iterate of all connections ("cons") and activate them
|
||||||
|
# in parallel. nmc_activate_complete() is used to rate limits
|
||||||
|
# how many parallel invocations we allow.
|
||||||
|
|
||||||
|
num_parallel_invocations = 100
|
||||||
|
|
||||||
|
activation_list = []
|
||||||
|
completed_list = []
|
||||||
|
for c in cons:
|
||||||
|
activation = nmc_activate_start(nmc, c)
|
||||||
|
activation_list.append(activation)
|
||||||
|
nmc_activate_complete(
|
||||||
|
nmc, activation_list, completed_list, num_parallel_invocations
|
||||||
|
)
|
||||||
|
nmc_activate_complete(nmc, activation_list, completed_list, 0)
|
||||||
|
assert not activation_list
|
||||||
|
assert len(completed_list) == len(cons)
|
||||||
|
|
||||||
|
return completed_list
|
||||||
|
|
||||||
|
|
||||||
|
def nmc_activate_wait_for_pending(nmc, completed_list):
|
||||||
|
|
||||||
|
# go through the list of activations and wait that they
|
||||||
|
# all reach a final state. That is, either that they are failed
|
||||||
|
# or fully ACTIVATED state.
|
||||||
|
|
||||||
|
log("wait for all active connection to either reach ACTIVATED state or fail...")
|
||||||
|
|
||||||
|
def log_result(activation, message):
|
||||||
|
activation["ac_result"] = message
|
||||||
|
log(
|
||||||
|
"connection %s (%s) activation fully completed: %s"
|
||||||
|
% (ac.get_id(), ac.get_uuid(), message)
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
# again, it's not efficient to check the entire list for completion
|
||||||
|
# after each g_main_context_iteration(). But "completed_list" should
|
||||||
|
# be reasonably small.
|
||||||
|
|
||||||
|
activation = None
|
||||||
|
for idx, activ in enumerate(completed_list):
|
||||||
|
if activ["ac_result"] is not None:
|
||||||
|
continue
|
||||||
|
if activ["result"] is False:
|
||||||
|
log_result(activ, "failed to start activation")
|
||||||
|
continue
|
||||||
|
ac = activ["result_ac"]
|
||||||
|
if ac.get_client() is None:
|
||||||
|
log_result(activ, "active connection disappeared")
|
||||||
|
continue
|
||||||
|
if ac.get_state() == NM.ActiveConnectionState.ACTIVATED:
|
||||||
|
log_result(activ, "connection successfully activated")
|
||||||
|
continue
|
||||||
|
if ac.get_state() > NM.ActiveConnectionState.ACTIVATED:
|
||||||
|
log_result(
|
||||||
|
activ, "connection failed to activate (state %s)" % (ac.get_state())
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
activation = activ
|
||||||
|
break
|
||||||
|
|
||||||
|
if activation is None:
|
||||||
|
log("no more activation to wait for")
|
||||||
|
break
|
||||||
|
|
||||||
|
nmc.get_main_context().iteration(may_block=True)
|
||||||
|
|
||||||
|
|
||||||
|
def nmc_activate_check_good(nmc, completed_list):
|
||||||
|
|
||||||
|
# go through the list of activations and check that all of them are
|
||||||
|
# in a good state.
|
||||||
|
|
||||||
|
n_good = 0
|
||||||
|
n_bad = 0
|
||||||
|
|
||||||
|
for activ in completed_list:
|
||||||
|
if activ["result"] is False:
|
||||||
|
n_bad += 1
|
||||||
|
continue
|
||||||
|
ac = activ["result_ac"]
|
||||||
|
if ac.get_client() is None:
|
||||||
|
n_bad += 1
|
||||||
|
continue
|
||||||
|
if ac.get_state() != NM.ActiveConnectionState.ACTIVATED:
|
||||||
|
n_bad += 1
|
||||||
|
continue
|
||||||
|
n_good += 1
|
||||||
|
|
||||||
|
log(
|
||||||
|
"%d out of %d activations are now successfully activated"
|
||||||
|
% (n_good, n_good + n_bad)
|
||||||
|
)
|
||||||
|
|
||||||
|
return n_bad == 0
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
nmc = nmc_new()
|
||||||
|
|
||||||
|
cons = find_connections(nmc, sys.argv[1:])
|
||||||
|
|
||||||
|
completed_list = nmc_activate_all(nmc, cons)
|
||||||
|
|
||||||
|
nmc_activate_wait_for_pending(nmc, completed_list)
|
||||||
|
|
||||||
|
all_good = nmc_activate_check_good(nmc, completed_list)
|
||||||
|
|
||||||
|
nmc_transfer_ref = [nmc]
|
||||||
|
del nmc
|
||||||
|
nmc_destroy(nmc_transfer_ref)
|
||||||
|
|
||||||
|
sys.exit(0 if all_good else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Reference in New Issue
Block a user