网络管理器 是默认的网络管理服务 Fedora 和其他几个 Linux 发行版。 它的主要目的是处理诸如设置接口、向它们添加地址和路由以及配置系统的其他网络相关方面(例如 DNS)之类的事情。
还有其他提供类似功能的工具。 然而,NetworkManager 的优点之一是它提供了强大的 API。 使用此 API,其他应用程序可以检查、监视和更改系统的网络状态。
本文首先介绍 NetworkManager 的 API,并介绍如何从 Python 程序中使用它。 在第二部分中,它展示了一些实际示例:如何连接到无线网络或通过 NetworkManager 以编程方式将 IP 地址添加到接口。
API
NetworkManager 提供了一个 D-Bus API。 D-总线 是一个消息总线系统,允许进程相互通信; 使用 D-Bus,想要提供某些服务的进程可以在总线上注册一个众所周知的名称(例如 example, “org.freedesktop.NetworkManager”)并公开一些对象,每个对象都由路径标识。 使用检查 D-Bus 对象的图形工具 d-feet,我们可以看到 NetworkManager 服务公开的对象树:
每个对象都有属性、方法和信号,它们被分组到不同的接口中。 为了 example,下面是第二个设备对象的接口的简化视图:
我们看到有不同的接口; org.freedesktop.NetworkManager.Device 接口包含所有设备共有的一些属性,例如状态、MTU 和 IP 配置。 由于这个设备是以太网,它还有一个 org.freedesktop.NetworkManager.Device.Wired D-Bus 接口,其中包含其他属性,例如链接速度。
的完整文档 NetworkManager 的 D-Bus API 在这里。
客户端可以使用众所周知的名称连接到 NetworkManager 服务并对暴露的对象执行操作。 为了 example,它可以调用方法、访问属性或通过信号接收通知。 这样,它几乎可以控制网络配置的各个方面。 事实上,所有与 NetworkManager 交互的工具——nmcli、nmtui、GNOME 控制中心、KDE 小程序、Cockpit——都使用这个 API。
库
在开发程序时,可以方便地从 D-Bus 上可用的对象中自动实例化对象并保持其属性同步; 或者能够对这些对象进行方法调用,自动分派到相应的 D-Bus 方法。 此类对象通常称为代理,用于向开发人员隐藏 D-Bus 通信的复杂性。
为此,NetworkManager 项目提供了一个名为 库,用 C 语言编写,基于 GNOME 的 GLib 和 GObject。 该库为 NetworkManager 提供的功能提供 C 语言绑定。 作为一个 GLib 库,它也可以通过 GObject 自省从其他语言中使用,如下所述。
该库与 NetworkManager 的 D-Bus API 非常接近。 它将远程 D-Bus 对象包装为本地 GObject,并将 D-Bus 信号和属性包装为 GObject 信号和属性。 此外,它还提供有用的访问器和实用功能。
libnm 对象概述
下图显示了 libnm 中最重要的对象及其关系:

NMClient 缓存所有从 D-Bus 实例化的对象。 该对象通常在程序开始时创建,并提供访问其他对象的方法。
NMDevice 表示网络接口,物理的(如以太网、Infiniband、Wi-Fi 等)或虚拟的(如网桥或 IP 隧道)。 NetworkManager 支持的每种设备类型都有一个专用的子类,用于实现特定于类型的属性和方法。 为了 example, 一个 NMDeviceWifi 具有与无线配置和扫描期间发现的接入点相关的属性,而 NMDeviceVlan 具有描述其 VLAN-id 和父设备的属性。
NMClient 还提供了 NMRemoteConnection 对象的列表。 NMRemoteConnection 是 NMConnection 接口的两种实现之一。 连接(或连接配置文件)包含连接到特定网络所需的所有配置。
NMRemoteConnection 和 NMSimpleConnection 之间的区别在于前者是 D-Bus 上存在的连接的代理,而后者不是。 特别是,当需要一个新的空白连接对象时,可以实例化 NMSimpleConnection。 这对, example在向 NetworkManager 添加新连接时。
图中的最后一个对象是 NMActiveConnection。 这表示使用来自 NMRemoteConnection 的设置与特定网络的活动连接。
GObject 内省
GObject 内省 是一个层,它充当使用 GObject 的 C 库和诸如 JavaScript、Python、Perl、Java、Lua、.NET、Scheme 等编程语言运行时之间的桥梁。
构建库时,会扫描源以生成自省元数据,以与语言无关的方式描述库导出的所有常量、类型、函数、信号等。 生成的元数据用于自动生成绑定以从其他语言调用 C 库。
元数据的一种形式是 GObject Introspection Repository (GIR) XML 文件。 GIR 主要由在编译时生成绑定的语言使用。 GIR 可以翻译成一种称为 Typelib 的机器可读格式,该格式针对快速访问和更低的内存占用进行了优化; 出于这个原因,它主要由在运行时生成绑定的语言使用。
这一页 列出其他语言的所有自省绑定。 对于 Python example 我们将使用 PyGObject 它包含在 python3-gobject RPM 上 Fedora.
一个基本的 example
让我们从一个打印系统信息的简单 Python 程序开始:
importgi gi.require_version("NM", "1.0") fromgi.repositoryimport GLib, NM client = NM.Client.new(None) print("version:", client.get_version())
一开始我们导入自省模块,然后导入 Glib 和 NM 模块。 由于系统中可能有多个版本的 NM 模块,我们确保加载正确的一个。 然后我们创建一个客户端对象并打印 NetworkManager 的版本。
接下来,我们要获取设备列表并打印它们的一些属性:
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)
设备状态是 NMDeviceState 类型的枚举,我们使用 value_nick 来获取它的描述。 输出类似于:
version: 1.41.0 devices: - name: lo type: loopback state: unmanaged - name: enp1s0 type: ethernet state: activated - name: wlp4s0 type: wifi state: activated
在 libnm 文档中,我们看到 网管设备 对象有一个 get_ip4_config() 方法,该方法返回一个 NMIPConfig 对象并提供对设备上当前设置的地址、路由和其他参数的访问。 我们可以打印它们:
ip4 = device.get_ip4_config() if ip4 is not None: print(" addresses:") for a in ip4.get_addresses(): print(" - {}/{}".format(a.get_address(), a.get_prefix())) print(" routes:") for r in ip4.get_routes(): print(" - {}/{} via {}".format(r.get_dest(), r.get_prefix(), r.get_next_hop()))
由此,enp1s0 的输出变为:
- name: enp1s0 type: ethernet state: activated addresses: - 192.168.122.191/24 - 172.26.1.1/16 routes: - 172.26.0.0/16 via None - 192.168.122.0/24 via None - 0.0.0.0/0 via 192.168.122.1
连接到 Wi-Fi 网络
现在我们已经掌握了基础知识,让我们尝试一些更高级的东西。 假设我们在无线网络的范围内,并且我们想要连接到它。
如前所述,连接配置文件描述了连接到特定网络所需的所有设置。 从概念上讲,我们需要执行两个不同的操作:首先将新的连接配置文件插入到 NetworkManager 的配置中,然后激活它。 幸运的是,API 提供了方法 nm_client_add_and_activate_connection_async() 一步完成所有事情。 在调用该方法时,我们至少需要传递以下参数:
- 我们要添加的 NMConnection,包含所有需要的属性;
- 激活连接的设备;
- 当方法异步完成时调用的回调函数。
我们可以通过以下方式构建连接:
defcreate_connection(): connection = NM.SimpleConnection.new() ssid = GLib.Bytes.new("Home".encode("utf-8")) 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() 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
该函数创建一个新的 NMSimpleConnection 并设置所有需要的属性。 所有属性都分组到设置中。 特别是,NMSettingConnection 设置包含常规属性,例如配置文件名称及其类型。 NMSettingWireless 表示无线网络名称(SSID),我们希望在“基础设施”模式下运行,即作为无线客户端。 无线安全设置指定身份验证机制和密码。 我们将 IPv4 和 IPv6 都设置为“自动”,以便接口通过 DHCP 和 IPv6 自动配置获取地址。
NetworkManager 支持的所有属性都在 nm-settings 手册页和“连接和设置 API 参考”中进行了描述 部分 的 libnm 文档。
为了找到合适的接口,我们循环遍历系统上的所有设备并返回第一个 Wi-Fi 设备。
deffind_wifi_device(client): for device in client.get_devices(): if device.get_device_type() == NM.DeviceType.WIFI: return device returnNone
现在缺少的是一个回调函数,但是如果我们稍后再看它会更容易。 我们可以继续调用 add_and_activate_connection_async() 方法:
importgi gi.require_version("NM", "1.0") fromgi.repositoryimport GLib, NM # other functions here... 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()
为了支持多个异步操作而不阻塞整个程序的执行,libnm 使用了一个 事件循环 机制。 有关 GLib 中事件循环的介绍,请参见 本教程. 对 main_loop.run() 的调用一直等到有事件发生(例如我们的方法调用的回调,或者来自 D-Bus 的任何更新)。 事件处理一直持续到主循环被显式终止。 这发生在回调中:
defadd_and_activate_cb(client, result, data): try: ac = client.add_and_activate_connection_finish(result) print("ActiveConnection {}".format(ac.get_path())) print("State {}".format(ac.get_state().value_nick)) exceptExceptionas e: print("Error:", e) main_loop.quit()
在这里,我们使用 client.add_and_activate_connection_finish() 来获取异步方法的结果。 结果是一个 NMActiveConnection 对象,我们打印它的 D-Bus 路径和状态。
请注意,一旦创建活动连接,就会调用回调。 它可能仍在尝试连接。 换句话说,当回调运行时,我们不能保证激活成功完成。 如果我们想确保这一点,我们将需要监视活动连接状态,直到它变为激活(或在失败的情况下变为停用)。 在这个 example,我们只是打印激活开始,或者为什么失败,然后我们退出主循环; 之后, main_loop.run() 调用将结束,我们的程序将终止。
向设备添加地址
一旦设备上的连接处于活动状态,我们可能会决定要在其上配置一个额外的 IP 地址。
有不同的方法可以做到这一点。 一种方法是修改配置文件并再次激活它,类似于我们在之前看到的 example. 另一种方法是更改设备的运行时配置而不更新磁盘上的配置文件。
为此,我们使用 重新申请() 方法。 它至少需要以下参数:
- 应用新配置的 NMDevice;
- 包含配置的 NMConnection。
由于我们只想更改 IP 地址并保持其他所有内容不变,因此我们首先需要检索设备的当前配置(也称为“应用连接”)。 然后我们用静态地址更新它并将其重新应用到设备上。
应用的连接,不出所料,可以用method查询 get_applied_connection() NMDevice 的。 请注意,该方法还返回一个版本 ID,该版本 ID 在重新应用期间可能很有用,以避免与其他客户端的竞争条件。 为简单起见,我们不打算使用它。
在这个 example 我们假设我们已经知道要更新的设备的名称:
importgiimportsocket gi.require_version("NM", "1.0") fromgi.repositoryimport GLib, NM # other functions here... main_loop = GLib.MainLoop() client = NM.Client.new(None) device = client.get_device_by_iface("enp1s0") device.get_applied_connection_async(0, None, get_applied_cb, None) main_loop.run()
回调函数从结果中检索应用的连接,更改 IPv4 配置并重新应用它:
defget_applied_cb(device, result, data): (connection, v) = device.get_applied_connection_finish(result) s_ip4 = connection.get_setting_ip4_config() s_ip4.add_address(NM.IPAddress.new(socket.AF_INET, "172.25.12.1", 24)) device.reapply_async(connection, 0, 0, None, reapply_cb, None)
为简洁起见省略异常处理,重新应用回调很简单:
defreapply_cb(device, result, data): device.reapply_finish(result) main_loop.quit()
当程序退出时,我们会在界面上看到配置的新地址。
结论
本文介绍了 NetworkManager 的 D-Bus 和 libnm API,并给出了一些实际使用示例。 希望当您需要开发下一个涉及网络的项目时它会很有用!
除了此处提供的示例之外,NetworkManager git 树还包括 好多其它的 针对不同的编程语言。 要及时了解 NetworkManager 世界的最新消息,请关注 博客.