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:
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:
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()