Using Python and NetworkManager to control the network

NetworkManager is the default network management service on Fedora and several other Linux distributions. This article will introduce the API it offers to control the network, and how to use it from a Python program.

The API

NetworkManager provides a D-Bus API. D-Bus is a message bus system that allows processes to talk to each other. Using D-Bus, a process that wants to offer some services can register on the bus with a well-known name (for example, org.freedesktop.NetworkManager) and expose some objects, each identified by a path. Taking NetworkManager as example, we can see that it exposes some objects of type ActiveConnection, Device, IP4Config, and so on:

# busctl tree org.freedesktop.NetworkManager

└─/org
  └─/org/freedesktop
    └─/org/freedesktop/NetworkManager
      ├─/org/freedesktop/NetworkManager/ActiveConnection
      │ └─/org/freedesktop/NetworkManager/ActiveConnection/1
      ├─/org/freedesktop/NetworkManager/AgentManager
      ├─/org/freedesktop/NetworkManager/Devices
      │ ├─/org/freedesktop/NetworkManager/Devices/1
      │ └─/org/freedesktop/NetworkManager/Devices/2
      ├─/org/freedesktop/NetworkManager/DnsManager
      ├─/org/freedesktop/NetworkManager/IP4Config
      │ ├─/org/freedesktop/NetworkManager/IP4Config/1
      │ └─/org/freedesktop/NetworkManager/IP4Config/2
      ├─/org/freedesktop/NetworkManager/IP6Config
      │ ├─/org/freedesktop/NetworkManager/IP6Config/1
      │ └─/org/freedesktop/NetworkManager/IP6Config/2
      └─/org/freedesktop/NetworkManager/Settings
        ├─/org/freedesktop/NetworkManager/Settings/1
        └─/org/freedesktop/NetworkManager/Settings/2

Each of those objects has properties, methods and signals, grouped into different interfaces. For example, let’s display what is available on the first device object:

# busctl introspect org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/Devices/1

org.freedesktop.DBus.Properties                  interface -           -
.Get                                             method    ss          v
.GetAll                                          method    s           a{sv}
.Set                                             method    ssv         -
.PropertiesChanged                               signal    sa{sv}as    -

org.freedesktop.NetworkManager.Device            interface -           -
.Delete                                          method    -           -
.Disconnect                                      method    -           -
.GetAppliedConnection                            method    u           a{sa{sv}}t
.Reapply                                         method    a{sa{sv}}tu -
.ActiveConnection                                property  o           "/"
.Autoconnect                                     property  b           true
.AvailableConnections                            property  ao          0
.Capabilities                                    property  u           7
.DeviceType                                      property  u           14
.Dhcp4Config                                     property  o           "/"
.Dhcp6Config                                     property  o           "/"
.Driver                                          property  s           "unknown"
.StateChanged                                    signal    uuu         -
[...]

We see that there is a org.freedesktop.DBus.Properties interface, containing some methods and a signal. Then we find a org.freedesktop.NetworkManager.Device interface with other methods, properties and signals.

The full documentation for the D-Bus API of NetworkManager can be found here.

A client can connect to the service using the well-known name, and perform operations on those objects: it can invoke methods, access properties or receive notifications when a signal is emitted. In this way, it can control almost every aspect of a machine network configuration; indeed, all the tools that interact with NetworkManager – as nmcli, nmtui, GNOME control center, the KDE applet, etc. – use this API directly.

A D-Bus object is somehow similar to a object in the programming sense: they both have properties, methods, and can implement interfaces. Therefore, it would be nice to have a middle layer that instantiates objects from D-Bus objects and keeps their properties synchronized; these objects are usually called proxies and can be easily used from the language runtime, hiding the complexity of the D-Bus communication.

For this purpose, NetworkManager project provides a library called libnm; it’s written in C and uses GNOME’s GLib and GObject. The library provides C language bindings for functionality provided by NetworkManager, optionally useful from other language runtimes as well. The library wraps the D-Bus API in easy-to-use GObjects and is often much simpler for GLib-based applications to use than explicit D-Bus.

libnm overview

The library maps fairly closely to the D-Bus API of NetworkManager, wrapping the remote D-Bus objects as native GObjects, mapping D-Bus signals and properties to GObject signals and properties, and providing helpful accessor and utility functions. The diagram below shows the main objects available in the library and their relationship:

Overview of libnm objects

The base object of the library is NMClient; it is typically instantiated when the program is created and provides access to other objects; it also implements methods that …

A NMDevice represents a network interface, physical (as Ethernet, Infiniband, Wi-Fi, etc.) or virtual (as a bridge or a IP tunnel). Each device type supported by NetworkManager has a dedicated subclass that implements type-specific properties and methods. For example, a NMDeviceWifi has properties related to the wireless configuration and to access points found during the scan:

Wireless device D-Bus properties

NMClient implements a .get_connections() method to get a list of NMRemoteConnection objects. NMRemoteConnection is one of the two implementations of the NMConnection interface. A connection is the basic unit of network configuration information and describes all the settings required to connect to a specific network. For example, a Ethernet connection might specify the autonegotiation and speed settings for the Ethernet link, as well as IPv4 and IPv6 addressing parameters.

The difference between a NMRemoteConnection and a NMSimpleConnection is that the former is a proxy for a connection existing on D-Bus while the latter is not; in particular, NMSimpleConnection can be instantiated when we need a new blank connection object; this is useful for example when we need to add a new connection to NetworkManager.

The last object shown in the diagram is NMActiveConnection, which represents an active connection to a specific network, using settings from a NMRemoteConnection; the active connection is associated to one or more devices.

Using libnm via GObject introspection

GObject introspection is layer that acts as a bridge between C library using GObject and programming language runtimes. It allows to develop a library in C, and then use it from runtimes as JavaScript, Python, Perl, Java, Lua, .NET, Scheme etc.

From the library sources, some tools create a XML called GIR that contains introspection information, that is, information about functions, classes, etc. . The GIR is translated into a machine-readable format called Typelib that is suitable for fast access and has low memory footprint.

The language runtime can then use the Typelib to generate bindings for the library. For Python, this is done by the PyGObject package, included in the python3-gobject Fedora RPM.

A basic example

Let’s start with the most basic program that creates a NMClient and prints the version of NetworkManager:

import gi
gi.require_version("NM", "1.0")
from gi.repository import GLib, NM

client = NM.Client.new(None)
print("version:", client.get_version())

At the beginning we import the introspection module and then the Glib and NM bindings. Since there could be multiple versions of the NM module installed, we ensure that the right one is loaded. Then we create a client object and invoke the .get_version() method on it.

In the next part, we get a list of devices from the client object and print some of their properties:

devices = client.get_devices()
print("devices:")
for device in devices:
    print("  - name:", device.get_iface());
    print("    type:", device.get_type_description())
    print("    state:", device.get_state().value_nick)

The output is something like:

version: 1.41.0
devices:
  - name: lo
    type: loopback
    state: unmanaged
  - name: enp1s0
    type: ethernet
    state: activated
  - name: wlp4s0
    type: wifi
    state: activated

What if we wanted to also show IP addresses configured on the devices? In the libnm documentation we see that the NMDevice object has a get_ip4_config() method, which returns a NMIPConfig object and provides access to addresses, routes and other parameters:

    ip4 = device.get_ip4_config()
    if ip4 is not None:
       addrs = ip4.get_addresses()
       for addr in addrs:
           print(" - address {}/{}".format(addr.get_address(), addr.get_prefix()))

With this, the output becomes:

  ...
  - name: enp1s0
    type: ethernet
    state: activated
    addresses:
      - 192.168.122.191/24
      - 192.168.122.170/24
  ...

Connecting to a Wi-Fi network

Now that we have mastered the basics, let’s try to do something more interesting, like connecting to a Wi-Fi network. As mentioned before, a key concept in NetworkManager is a connection profile, that contains all the settings necessary to connect to a network.

To connect to our Wi-Fi network, we need to perform two steps: first, add the connection profile and then tell NM to activate it. Looking at the libnm API, the nm_client_add_and_activate_connection_async() method seems to do what we need. To call it, we need:

  • a NMConnection object that we want to add, containing all the needed properties

  • a device to activate the connection on

  • a callback function to be called when the method invocation completes asynchronously

All other arguments are optional and we are not going to pass them.

First, let’s construct a new connection:

def create_connection():
    connection = NM.SimpleConnection.new()

    s_con = NM.SettingConnection.new()
    s_con.set_property(NM.SETTING_CONNECTION_ID, "my-wifi-connection")
    s_con.set_property(NM.SETTING_CONNECTION_TYPE, "802-11-wireless")

    s_wifi = NM.SettingWireless.new()
    ssid = GLib.Bytes.new("guest".encode("utf-8"))
    s_wifi.set_property(NM.SETTING_WIRELESS_SSID, ssid)
    s_wifi.set_property(NM.SETTING_WIRELESS_MODE, "infrastructure")

    s_wsec = NM.SettingWirelessSecurity.new()
    s_wsec.set_property(NM.SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-psk")
    s_wsec.set_property(NM.SETTING_WIRELESS_SECURITY_PSK, "z!q9at#0b1")

    s_ip4 = NM.SettingIP4Config.new()
    s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")
    s_ip6 = NM.SettingIP6Config.new()
    s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")

    connection.add_setting(s_con)
    connection.add_setting(s_wifi)
    connection.add_setting(s_wsec)
    connection.add_setting(s_ip4)
    connection.add_setting(s_ip6)

    return connection

To find a suitable interface, we can loop through all interfaces and return the first Wi-fi one:

def find_wifi_device(client):
    for device in client.get_devices():
        if device.get_device_type() == NM.DeviceType.WIFI:
            return device
    return None

Now that we all required bits, put everything together:

def add_and_activate_cb(client, result, data):
    try:
        client.add_and_activate_connection_finish(result)
        print("Done")
    except Exception as e:
        print("Error:", e)
    main_loop.quit()

main_loop = GLib.MainLoop()
client = NM.Client.new(None)
connection = create_connection()
device = find_wifi_device(client)
client.add_and_activate_connection_async(
    connection, device, None, None, add_and_activate_cb, None
)
main_loop.run()

Conclusion