about summary refs log tree commit diff
path: root/examples/python/gi
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2021-02-11 18:11:46 +0100
committerMichael Biebl <biebl@debian.org>2021-02-11 18:11:46 +0100
commit80ec1decc49c72efec2a8b87c06245c92c0ab807 (patch)
treee3b229aa94e8dcf0590f2317664176e7b8f7607b /examples/python/gi
parent65f86e8f56267192d42f2b629fc6b0c99fb9cd0c (diff)
New upstream version 1.29.90 upstream/1.29.90
Diffstat (limited to 'examples/python/gi')
-rwxr-xr-xexamples/python/gi/add_connection.py2
-rwxr-xr-xexamples/python/gi/deactivate-all.py2
-rwxr-xr-xexamples/python/gi/device-state-ip4config.py2
-rwxr-xr-xexamples/python/gi/firewall-zone.py2
-rwxr-xr-xexamples/python/gi/get-active-connections.py2
-rwxr-xr-xexamples/python/gi/get_ips.py2
-rwxr-xr-xexamples/python/gi/list-connections.py2
-rwxr-xr-xexamples/python/gi/nm-add-connection2.py2
-rwxr-xr-xexamples/python/gi/nm-connection-update-stable-id.py2
-rwxr-xr-xexamples/python/gi/nm-keyfile.py139
-rwxr-xr-xexamples/python/gi/nm-update2.py2
-rwxr-xr-xexamples/python/gi/nm-wg-set2
-rwxr-xr-xexamples/python/gi/ovs-external-ids.py663
-rwxr-xr-xexamples/python/gi/setting-user-data.py2
-rwxr-xr-xexamples/python/gi/show-wifi-networks.py3
-rwxr-xr-xexamples/python/gi/update-ip4-method.py2
16 files changed, 816 insertions, 15 deletions
diff --git a/examples/python/gi/add_connection.py b/examples/python/gi/add_connection.py
index ac537ead..e6e1d5a6 100755
--- a/examples/python/gi/add_connection.py
+++ b/examples/python/gi/add_connection.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2014 Red Hat, Inc.
 #
diff --git a/examples/python/gi/deactivate-all.py b/examples/python/gi/deactivate-all.py
index cc181dd9..bb590b23 100755
--- a/examples/python/gi/deactivate-all.py
+++ b/examples/python/gi/deactivate-all.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2015 Red Hat, Inc.
 #
diff --git a/examples/python/gi/device-state-ip4config.py b/examples/python/gi/device-state-ip4config.py
index 038b8a72..6ad882c1 100755
--- a/examples/python/gi/device-state-ip4config.py
+++ b/examples/python/gi/device-state-ip4config.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2012 Red Hat, Inc.
 #
diff --git a/examples/python/gi/firewall-zone.py b/examples/python/gi/firewall-zone.py
index 3de06f52..e3ee2511 100755
--- a/examples/python/gi/firewall-zone.py
+++ b/examples/python/gi/firewall-zone.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2013 - 2014 Red Hat, Inc.
 #
diff --git a/examples/python/gi/get-active-connections.py b/examples/python/gi/get-active-connections.py
index c2f261ab..aedfad6f 100755
--- a/examples/python/gi/get-active-connections.py
+++ b/examples/python/gi/get-active-connections.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2014 Red Hat, Inc.
 #
diff --git a/examples/python/gi/get_ips.py b/examples/python/gi/get_ips.py
index 07d6f0cc..e4276ab9 100755
--- a/examples/python/gi/get_ips.py
+++ b/examples/python/gi/get_ips.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2014 Red Hat, Inc.
 #
diff --git a/examples/python/gi/list-connections.py b/examples/python/gi/list-connections.py
index ba4f50e2..d3a74d5a 100755
--- a/examples/python/gi/list-connections.py
+++ b/examples/python/gi/list-connections.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2012 - 2014 Red Hat, Inc.
 #
diff --git a/examples/python/gi/nm-add-connection2.py b/examples/python/gi/nm-add-connection2.py
index f487e0bb..2cd760a2 100755
--- a/examples/python/gi/nm-add-connection2.py
+++ b/examples/python/gi/nm-add-connection2.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2019 Red Hat, Inc.
 #
diff --git a/examples/python/gi/nm-connection-update-stable-id.py b/examples/python/gi/nm-connection-update-stable-id.py
index 091a67d6..05926f40 100755
--- a/examples/python/gi/nm-connection-update-stable-id.py
+++ b/examples/python/gi/nm-connection-update-stable-id.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2017 Red Hat, Inc.
 #
diff --git a/examples/python/gi/nm-keyfile.py b/examples/python/gi/nm-keyfile.py
new file mode 100755
index 00000000..3a467d43
--- /dev/null
+++ b/examples/python/gi/nm-keyfile.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import sys
+import os
+
+import gi
+
+gi.require_version("NM", "1.0")
+from gi.repository import NM, GLib
+
+###############################################################################
+
+
+def kf_load_from_file(filename):
+    kf = GLib.KeyFile.new()
+    kf.load_from_file(filename, GLib.KeyFileFlags.NONE)
+    return kf
+
+
+def kf_to_string(kf):
+    d, l = kf.to_data()
+    return d
+
+
+def debug(message):
+    if os.getenv("DEBUG") == "1":
+        print(">>> %s" % (message))
+
+
+###############################################################################
+
+filename = sys.argv[1]
+base_dir = os.path.dirname(os.path.realpath(filename))
+
+kf = kf_load_from_file(filename)
+
+print('> keyfile "%s":' % (filename))
+print(">>\n%s\n<<" % (kf_to_string(kf)))
+
+###############################################################################
+
+
+def kf_handler_read(keyfile, connection, handler_type, handler_data, user_data):
+    kf_handler_read_cnt = globals().get("kf_handler_read_cnt", 0) + 1
+    globals()["kf_handler_read_cnt"] = kf_handler_read_cnt
+
+    [kf_group, kf_key, cur_setting, cur_property] = handler_data.get_context()
+
+    debug("kf_handler_read(%s): keyfile=%r" % (kf_handler_read_cnt, keyfile))
+    debug("kf_handler_read(%s): connection=%r" % (kf_handler_read_cnt, connection))
+    debug("kf_handler_read(%s): handler-type=%r" % (kf_handler_read_cnt, handler_type))
+    debug("kf_handler_read(%s): handler-data=%r" % (kf_handler_read_cnt, handler_data))
+    debug("kf_handler_read(%s): user-data=%r" % (kf_handler_read_cnt, user_data))
+    debug("kf_handler_read(%s): kf-group=%r" % (kf_handler_read_cnt, kf_group))
+    debug("kf_handler_read(%s): kf-key=%r" % (kf_handler_read_cnt, kf_key))
+    debug("kf_handler_read(%s): kf-setting=%r" % (kf_handler_read_cnt, cur_setting))
+    debug("kf_handler_read(%s): kf-property=%r" % (kf_handler_read_cnt, cur_property))
+
+    if handler_type == NM.KeyfileHandlerType.WARN:
+        [message, severity] = handler_data.warn_get()
+        debug('parse-warning: <%s> = "%s"' % (severity, message))
+        print("> warning: %s" % (message))
+        return False
+
+    if handler_type == NM.KeyfileHandlerType.WRITE_CERT:
+        # just to show how to abort the parsing. This event won't happen
+        # for read.
+        handler_data.fail_with_error(
+            GLib.GError.new_literal(
+                NM.ConnectionError.quark(), "hallo1", NM.ConnectionError.MISSINGPROPERTY
+            )
+        )
+
+    # don't handle unknown handler types.
+    return False
+
+
+try:
+    print("parse keyfile...")
+    c = NM.keyfile_read(kf, base_dir, NM.KeyfileHandlerFlags.NONE, kf_handler_read, 42)
+except Exception as e:
+    print("parsing failed: %r" % (e))
+    raise
+
+verify_failure = None
+try:
+    c.verify()
+except Exception as e:
+    verify_failure = e.message
+
+print(
+    'parsing succeeded: "%s" (%s)%s'
+    % (
+        c.get_id(),
+        c.get_uuid(),
+        " (invalid: " + verify_failure + ")" if verify_failure is not None else "",
+    )
+)
+
+
+###############################################################################
+
+
+def kf_handler_write(connection, keyfile, handler_type, handler_data, user_data):
+    kf_handler_write_cnt = globals().get("kf_handler_write_cnt", 0) + 1
+    globals()["kf_handler_write_cnt"] = kf_handler_write_cnt
+
+    [kf_group, kf_key, cur_setting, cur_property] = handler_data.get_context()
+
+    debug("kf_handler_write(%s): keyfile=%r" % (kf_handler_write_cnt, keyfile))
+    debug("kf_handler_write(%s): connection=%r" % (kf_handler_write_cnt, connection))
+    debug(
+        "kf_handler_write(%s): handler-type=%r" % (kf_handler_write_cnt, handler_type)
+    )
+    debug(
+        "kf_handler_write(%s): handler-data=%r" % (kf_handler_write_cnt, handler_data)
+    )
+    debug("kf_handler_write(%s): user-data=%r" % (kf_handler_write_cnt, user_data))
+    debug("kf_handler_write(%s): kf-group=%r" % (kf_handler_write_cnt, kf_group))
+    debug("kf_handler_write(%s): kf-key=%r" % (kf_handler_write_cnt, kf_key))
+    debug("kf_handler_write(%s): kf-setting=%r" % (kf_handler_write_cnt, cur_setting))
+    debug("kf_handler_write(%s): kf-property=%r" % (kf_handler_write_cnt, cur_property))
+
+    if handler_type == NM.KeyfileHandlerType.WRITE_CERT:
+        return False
+    return False
+
+
+try:
+    print("")
+    print("write keyfile...")
+    kf2 = NM.keyfile_write(c, NM.KeyfileHandlerFlags.NONE, kf_handler_write, 43)
+except Exception as e:
+    print("write failed: %r" % (e))
+    raise
+
+print("persisted again:")
+print(">>\n%s\n<<" % (kf_to_string(kf2)))
diff --git a/examples/python/gi/nm-update2.py b/examples/python/gi/nm-update2.py
index 192768ee..9d272ead 100755
--- a/examples/python/gi/nm-update2.py
+++ b/examples/python/gi/nm-update2.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2019 Red Hat, Inc.
 #
diff --git a/examples/python/gi/nm-wg-set b/examples/python/gi/nm-wg-set
index 4f074605..4d162570 100755
--- a/examples/python/gi/nm-wg-set
+++ b/examples/python/gi/nm-wg-set
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2018 - 2019 Red Hat, Inc.
 #
diff --git a/examples/python/gi/ovs-external-ids.py b/examples/python/gi/ovs-external-ids.py
new file mode 100755
index 00000000..63f9695e
--- /dev/null
+++ b/examples/python/gi/ovs-external-ids.py
@@ -0,0 +1,663 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2017, 2020 Red Hat, Inc.
+#
+
+#
+# set and show OVS external-ids for a connection:
+#
+
+import sys
+import os
+import re
+import pprint
+import subprocess
+
+import gi
+
+gi.require_version("NM", "1.0")
+from gi.repository import GLib, NM
+
+###############################################################################
+
+MODE_GET = "get"
+MODE_SET = "set"
+MODE_APPLY = "apply"
+
+
+def memoize0(f):
+    result = []
+
+    def helper():
+        if len(result) == 0:
+            result.append(f())
+        return result[0]
+
+    return helper
+
+
+def memoize(f):
+    memo = {}
+
+    def helper(x):
+        if x not in memo:
+            memo[x] = f(x)
+        return memo[x]
+
+    return helper
+
+
+def pr(v):
+    pprint.pprint(v, indent=4, depth=5, width=60)
+
+
+@memoize0
+def is_libnm_debug():
+    return os.getenv("LIBNM_CLIENT_DEBUG") is not None
+
+
+@memoize0
+def can_sudo():
+    try:
+        return (
+            subprocess.run(
+                ["sudo", "-n", "true"],
+                stdout=subprocess.DEVNULL,
+                stderr=subprocess.DEVNULL,
+            ).returncode
+            == 0
+        )
+    except:
+        return False
+
+
+def _print(msg=""):
+    if is_libnm_debug():
+        # we want to use the same logging mechanism as libnm's debug
+        # logging with "LIBNM_CLIENT_DEBUG=trace,stdout".
+        NM.utils_print(0, msg + "\n")
+        return
+    print(msg)
+
+
+def mainloop_run(timeout_msec=0, mainloop=None):
+    if mainloop is None:
+        mainloop = GLib.MainLoop()
+
+    timeout_id = None
+    timeout_reached = []
+
+    if timeout_msec > 0:
+
+        def _timeout_cb(unused):
+            # it can happen that the caller already quit the mainloop
+            # otherwise. In that case, we don't want to signal a timeout.
+            if mainloop.is_running():
+                timeout_reached.append(1)
+                mainloop.quit()
+            return True
+
+        timeout_id = GLib.timeout_add(timeout_msec, _timeout_cb, None)
+
+    mainloop.run()
+    if timeout_id:
+        GLib.source_remove(timeout_id)
+    return not timeout_reached
+
+
+###############################################################################
+
+
+def connection_update2(remote_connection, connection):
+
+    mainloop = GLib.MainLoop()
+    result_error = []
+
+    def cb(c, result):
+        try:
+            c.update2_finish(result)
+        except Exception as e:
+            result_error.append(e)
+        mainloop.quit()
+
+    remote_connection.update2(
+        connection.to_dbus(NM.ConnectionSerializationFlags.ALL),
+        NM.SettingsUpdate2Flags.NO_REAPPLY,
+        None,
+        None,
+        cb,
+    )
+
+    mainloop_run(mainloop=mainloop)
+
+    if result_error:
+        raise result_error[0]
+
+
+def device_get_applied_connection(device):
+    mainloop = GLib.MainLoop()
+    rr = []
+
+    def cb(c, result):
+        try:
+            con, version_id = c.get_applied_connection_finish(result)
+        except Exception as e:
+            rr.append(e)
+        else:
+            rr.append(con)
+            rr.append(version_id)
+        mainloop.quit()
+
+    device.get_applied_connection_async(0, None, cb)
+
+    mainloop_run(mainloop=mainloop)
+
+    if len(rr) == 1:
+        raise rr[0]
+    return rr[0], rr[1]
+
+
+def device_reapply(device, connection, version_id):
+
+    mainloop = GLib.MainLoop()
+    result_error = []
+
+    def cb(d, result):
+        try:
+            d.reapply_finish(result)
+        except Exception as e:
+            result_error.append(e)
+        mainloop.quit()
+
+    device.reapply_async(connection, version_id, 0, None, cb)
+
+    mainloop_run(mainloop=mainloop)
+
+    if len(result_error) == 1:
+        raise result_error[0]
+
+
+def ovs_print_external_ids(prefix):
+    if not can_sudo():
+        _print(prefix + ": not running as root and cannot call ovs-vsctl")
+        return
+
+    cmds = [["ovs-vsctl", "show"]]
+    for typ in ["Bridge", "Port", "Interface"]:
+        cmds += [["ovs-vsctl", "--columns=name,external-ids", "list", typ]]
+
+    out = ""
+    for cmd in cmds:
+        p = subprocess.run(cmd, stdout=subprocess.PIPE, check=True)
+        out += p.stdout.decode("utf-8") + "\n"
+    out = "\n".join([prefix + s for s in out.split("\n")])
+    _print(out)
+
+
+###############################################################################
+
+
+def usage():
+    _print("%s g[et]   PROFILE [ GETTER ]" % (sys.argv[0]))
+    _print("%s s[et]   PROFILE   SETTER   [--test]" % (sys.argv[0]))
+    _print("%s a[pply] DEVICE    SETTER   [--test]" % (sys.argv[0]))
+    _print(
+        "   PROFILE :=  [id | uuid | type] STRING  |  [ ~id | ~type ] REGEX_STRING  |  STRING"
+    )
+    _print("   DEVICE :=  [iface] STRING")
+    _print("   GETTER  := ( KEY | ~REGEX_KEY )  [... GETTER]")
+    _print("   SETTER  := ( + | - | -KEY | [+]KEY VALUE ) [... SETTER]")
+
+
+def die(msg, show_usage=False):
+    _print("FAILED: %s" % (msg))
+    if show_usage:
+        usage()
+    sys.exit(1)
+
+
+def die_usage(msg):
+    die(msg, show_usage=True)
+
+
+def parse_args(argv):
+    had_dash_dash = False
+    args = {
+        "mode": MODE_GET,
+        "select_arg": None,
+        "ids_arg": [],
+        "do_test": False,
+    }
+    i = 1
+    while i < len(argv):
+        a = argv[i]
+
+        if i == 1:
+            if a in ["s", "set"]:
+                args["mode"] = MODE_SET
+            elif a in ["g", "get"]:
+                args["mode"] = MODE_GET
+            elif a in ["a", "apply"]:
+                args["mode"] = MODE_APPLY
+            else:
+                die_usage("unexpected mode argument '%s'" % (a))
+            i += 1
+            continue
+
+        if a == "--test":
+            args["do_test"] = True
+            i += 1
+            continue
+
+        if args["select_arg"] is None:
+            if args["mode"] == MODE_APPLY:
+                possible_selects = ["iface"]
+            else:
+                possible_selects = ["id", "~id", "uuid", "type", "~type"]
+
+            if a in possible_selects:
+                if i + 1 >= len(argv):
+                    die_usage("'%s' requires an argument'" % (a))
+                args["select_arg"] = (a, argv[i + 1])
+                i += 2
+                continue
+
+            if a == "*":
+                a = None
+            args["select_arg"] = ("*", a)
+            i += 1
+            continue
+
+        if args["mode"] == MODE_GET:
+            args["ids_arg"].append(a)
+            i += 1
+            continue
+
+        if not a:
+            die_usage("argument should specify a external-id but is empty string")
+
+        if a[0] == "-":
+            v = (a, None)
+            i += 1
+        elif a == "+":
+            v = (a, None)
+            i += 1
+        else:
+            if a[0] != "+":
+                a = "+" + a
+            if i + 1 >= len(argv):
+                die_usage("'%s' requires an argument'" % (a))
+            v = (a, argv[i + 1])
+            i += 2
+
+        args["ids_arg"].append(v)
+
+    if args["mode"] == MODE_SET:
+        if not args["ids_arg"]:
+            die_usage("Requires one or more external-ids to set or delete")
+
+    return args
+
+
+def device_to_str(device, show_type=False):
+    if show_type:
+        return "%s (%s)" % (device.get_iface(), device.get_type_desc())
+    return "%s" % (device.get_iface(),)
+
+
+def connection_to_str(connection, show_type=False):
+    if show_type:
+        return "%s (%s, %s)" % (
+            connection.get_id(),
+            connection.get_uuid(),
+            connection.get_connection_type(),
+        )
+    return "%s (%s)" % (connection.get_id(), connection.get_uuid())
+
+
+def devices_filter(devices, select_arg):
+    devices = list(sorted(devices, key=device_to_str))
+    if not select_arg:
+        return devices
+    # we preserve the order of the selected devices. And
+    # if devices are selected multiple times, we return
+    # them multiple times.
+    l = []
+    f = select_arg
+    for d in devices:
+        if f[0] == "iface":
+            if f[1] == d.get_iface():
+                l.append(d)
+        else:
+            assert f[0] == "*"
+            if f[1] is None:
+                l.append(d)
+            else:
+                if f[1] in [d.get_iface()]:
+                    l.append(d)
+    return l
+
+
+def connections_filter(connections, select_arg):
+    connections = list(sorted(connections, key=connection_to_str))
+    if not select_arg:
+        return connections
+    # we preserve the order of the selected connections. And
+    # if connections are selected multiple times, we return
+    # them multiple times.
+    l = []
+    f = select_arg
+    for c in connections:
+        if f[0] == "id":
+            if f[1] == c.get_id():
+                l.append(c)
+        elif f[0] == "~id":
+            if re.match(f[1], c.get_id()):
+                l.append(c)
+        elif f[0] == "uuid":
+            if f[1] == c.get_uuid():
+                l.append(c)
+        elif f[0] == "type":
+            if f[1] == c.get_connection_type():
+                l.append(c)
+        elif f[0] == "~type":
+            if re.match(f[1], c.get_connection_type()):
+                l.append(c)
+        else:
+            assert f[0] == "*"
+            if f[1] is None:
+                l.append(c)
+            else:
+                if f[1] in [c.get_uuid(), c.get_id()]:
+                    l.append(c)
+    return l
+
+
+def ids_select(ids, mode, ids_arg):
+    ids = list(ids)
+    if not ids_arg:
+        return (ids, [])
+
+    keys = set()
+    requested = []
+    for d in ids_arg:
+        if mode == MODE_GET:
+            if d[0] == "~":
+                r = re.compile(d[1:])
+                keys.update([k for k in ids if r.match(k)])
+            else:
+                keys.update([k for k in ids if k == d])
+                if d not in requested:
+                    requested.append(d)
+        else:
+            assert mode in [MODE_SET, MODE_APPLY]
+            d2 = d[0]
+            assert d2[0] in ["-", "+"]
+            d3 = d2[1:]
+            if d3 in ids:
+                keys.add(d3)
+    return (list([k for k in ids if k in keys]), requested)
+
+
+def connection_print(connection, mode, ids_arg, dbus_path, prefix=""):
+    sett = connection.get_setting(NM.SettingOvsExternalIDs)
+
+    if sett is not None:
+        all_ids = list(sett.get_data_keys())
+        keys, requested = ids_select(all_ids, mode, ids_arg)
+        num_str = "%s" % (len(all_ids))
+    else:
+        keys = []
+        requested = []
+        num_str = "none"
+
+    _print(
+        "%s%s [%s]" % (prefix, connection_to_str(connection, show_type=True), num_str)
+    )
+    if dbus_path:
+        _print("%s   %s" % (prefix, dbus_path))
+    if sett is not None:
+        dd = sett.get_property(NM.SETTING_OVS_EXTERNAL_IDS_DATA)
+    else:
+        dd = {}
+    for k in keys:
+        v = sett.get_data(k)
+        assert v is not None
+        assert v == dd.get(k, None)
+        _print('%s   "%s" = "%s"' % (prefix, k, v))
+    for k in requested:
+        _print('%s   "%s" = <unset>' % (prefix, k))
+
+
+def sett_update(connection, ids_arg):
+
+    sett = connection.get_setting(NM.SettingOvsExternalIDs)
+
+    for d in ids_arg:
+        op = d[0][0]
+        key = d[0][1:]
+        val = d[1]
+
+        oldval = None
+        if sett is not None:
+            oldval = sett.get_data(key)
+
+        if op == "-":
+            assert val is None
+            if key == "":
+                if sett is None:
+                    _print(" DEL: setting (ovs-external-ids group was not present)")
+                else:
+                    connection.remove_setting(NM.SettingOvsExternalIDs)
+                    sett = None
+                    _print(" DEL: setting")
+                continue
+
+            if sett is None:
+                _print(' DEL: "%s" (ovs-external-ids group was not present)' % (key))
+                continue
+            if oldval is None:
+                _print(' DEL: "%s" (id was unset)' % (key))
+                continue
+            _print(' DEL: "%s" (id was set to"%s")' % (key, oldval))
+            sett.set_data(key, None)
+            continue
+
+        if key == "":
+            assert val is None
+            if sett is None:
+                sett = NM.SettingOvsExternalIDs.new()
+                connection.add_setting(sett)
+                _print(" SET: setting (external-ids group was added)")
+                continue
+
+            _print(" SET: setting (external-ids group was present)")
+            continue
+
+        assert val is not None
+
+        if sett is None:
+            sett = NM.SettingOvsExternalIDs.new()
+            connection.add_setting(sett)
+            _print(
+                ' SET: "%s" = "%s" (external-ids group was not present)' % (key, val)
+            )
+        elif oldval is None:
+            _print(' SET: "%s" = "%s" (new)' % (key, val))
+        elif oldval != val:
+            _print(' SET: "%s" = "%s" (was "%s")' % (key, val, oldval))
+        else:
+            _print(' SET: "%s" = "%s" (unchanged)' % (key, val))
+        sett.set_data(key, val)
+
+
+def do_get(connections, ids_arg):
+    first_line = True
+    for c in connections:
+        if first_line:
+            first_line = False
+        else:
+            _print()
+        connection_print(c, MODE_GET, ids_arg, dbus_path=c.get_path())
+
+
+def do_set(nmc, connection, ids_arg, do_test):
+
+    remote_connection = connection
+    connection = NM.SimpleConnection.new_clone(remote_connection)
+
+    connection_print(
+        connection, MODE_SET, [], remote_connection.get_path(), prefix="BEFORE: "
+    )
+    _print()
+
+    sett_update(connection, ids_arg)
+
+    if do_test:
+        _print()
+        _print("Only show. Run without --test to set")
+        return
+
+    try:
+        connection_update2(remote_connection, connection)
+    except Exception as e:
+        _print()
+        _print("FAILURE to commit connection: %s" % (e))
+        return
+
+    # NMClient received the completion of Update2() call. It also received
+    # a property changed signal that the profile changed, and it is about
+    # to fetch the new value. However, that value is not yet here.
+    #
+    # libnm should provide a better API for this. For example, not signal
+    # completion of update2() until the profile was refetched. Or, indicate
+    # that the settings are dirty, so we would know how long to wait.
+    #
+    # Add an ugly workaround here and wait a bit.
+    _print()
+    _print("WORKAROUND: wait for connection to change")
+    mainloop_run(timeout_msec=500)
+
+    if remote_connection is not nmc.get_object_by_path(remote_connection.get_path()):
+        _print()
+        _print(
+            "Connection %s no longer exists after commit"
+            % (remote_connection.get_path())
+        )
+        return
+
+    _print()
+    connection_print(
+        remote_connection, MODE_SET, [], remote_connection.get_path(), prefix="AFTER:  "
+    )
+
+    _print()
+    if remote_connection.compare(connection, NM.SettingCompareFlags.EXACT):
+        _print("resulting connection is as expected")
+    else:
+        _print("WARNING: resulting connection is not as expected")
+
+
+def do_apply(nmc, device, ids_arg, do_test):
+
+    try:
+        connection_orig, version_id = device_get_applied_connection(device)
+    except Exception as e:
+        _print(
+            'failure to get applied connection for %s: %s"' % (device_to_str(device), e)
+        )
+        die("The device does not seem active? Nothing to reapply")
+
+    _print(
+        "REAPPLY device %s (%s) with connection %s (version-id = %s)"
+        % (
+            device_to_str(device),
+            NM.Object.get_path(device),
+            connection_to_str(connection_orig),
+            version_id,
+        )
+    )
+    _print()
+
+    ovs_print_external_ids("BEFORE-OVS-VSCTL: ")
+    _print()
+
+    connection = NM.SimpleConnection.new_clone(connection_orig)
+
+    connection_print(connection, MODE_APPLY, [], device.get_path(), prefix="BEFORE: ")
+    _print()
+
+    sett_update(connection, ids_arg)
+
+    if do_test:
+        _print()
+        _print("Only show. Run without --test to set")
+        return
+
+    _print()
+    _print("reapply...")
+
+    try:
+        device_reapply(device, connection, version_id)
+    except Exception as e:
+        _print()
+        _print("FAILURE to commit connection: %s" % (e))
+        return
+
+    try:
+        connection_after, version_id = device_get_applied_connection(device)
+    except Exception as e:
+        _print(
+            'failure to get applied connection after reapply for device %s: %s"'
+            % (device_to_str(device), e)
+        )
+        die("FAILURE to get applied connection after reapply")
+
+    _print()
+    connection_print(connection, MODE_APPLY, [], device.get_path(), prefix="AFTER: ")
+    _print()
+
+    ovs_print_external_ids("AFTER-OVS-VSCTL: ")
+
+
+###############################################################################
+
+if __name__ == "__main__":
+
+    args = parse_args(sys.argv)
+
+    nmc = NM.Client.new(None)
+
+    if args["mode"] == MODE_APPLY:
+
+        devices = devices_filter(nmc.get_devices(), args["select_arg"])
+
+        if len(devices) != 1:
+            _print(
+                "To apply the external-ids of a device, exactly one connection must be selected. Instead, %s devices matched ([%s])"
+                % (len(devices), ", ".join([device_to_str(c) for c in devices]))
+            )
+            die_usage("Select unique device to apply")
+        do_apply(nmc, devices[0], args["ids_arg"], do_test=args["do_test"])
+
+    else:
+
+        connections = connections_filter(nmc.get_connections(), args["select_arg"])
+
+        if args["mode"] == MODE_SET:
+            if len(connections) != 1:
+                _print(
+                    "To set the external-ids of a connection, exactly one connection must be selected via id|uuid. Instead, %s connection matched ([%s])"
+                    % (
+                        len(connections),
+                        ", ".join([connection_to_str(c) for c in connections]),
+                    )
+                )
+                die_usage("Select unique connection to set")
+            do_set(nmc, connections[0], args["ids_arg"], do_test=args["do_test"])
+        else:
+            if len(connections) < 1:
+                _print("No connection selected for printing the external ids")
+                die_usage("Select connection to get")
+            do_get(connections, args["ids_arg"])
diff --git a/examples/python/gi/setting-user-data.py b/examples/python/gi/setting-user-data.py
index e2194074..20c6a2e9 100755
--- a/examples/python/gi/setting-user-data.py
+++ b/examples/python/gi/setting-user-data.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2017 Red Hat, Inc.
 #
diff --git a/examples/python/gi/show-wifi-networks.py b/examples/python/gi/show-wifi-networks.py
index b865a85a..eb71c4c4 100755
--- a/examples/python/gi/show-wifi-networks.py
+++ b/examples/python/gi/show-wifi-networks.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
-# coding=utf-8
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2013 Red Hat, Inc.
 #
diff --git a/examples/python/gi/update-ip4-method.py b/examples/python/gi/update-ip4-method.py
index 6fae2603..811d10b4 100755
--- a/examples/python/gi/update-ip4-method.py
+++ b/examples/python/gi/update-ip4-method.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# SPDX-License-Identifier: GPL-2.0+
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Copyright (C) 2014 Red Hat, Inc.
 #