From 9ff530c3228196fef3a477d5fd394142be04894b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Wed, 11 Feb 2026 09:11:46 +0100 Subject: [PATCH 01/12] dbus: device: add SetManaged method The 'Managed' property only sets the managed state in runtime, but it is not possible to persist it to disk. Add a SetManaged method that will be able to persist it to disk. In this commit, it just modify the runtime state, so it actually only does the same than setting the property. Storing to disk will be added in next commits. --- .../org.freedesktop.NetworkManager.Device.xml | 16 +++ src/core/devices/nm-device.c | 109 ++++++++++++++++++ src/libnm-client-impl/libnm.ver | 3 + src/libnm-client-impl/nm-device.c | 70 ++++++++++- src/libnm-client-public/nm-device.h | 9 ++ src/libnm-core-public/nm-dbus-interface.h | 15 +++ 6 files changed, 219 insertions(+), 3 deletions(-) diff --git a/introspection/org.freedesktop.NetworkManager.Device.xml b/introspection/org.freedesktop.NetworkManager.Device.xml index 085d2a1f74..684134835a 100644 --- a/introspection/org.freedesktop.NetworkManager.Device.xml +++ b/introspection/org.freedesktop.NetworkManager.Device.xml @@ -175,6 +175,9 @@ property has a similar effect to configuring the device as unmanaged via the keyfile.unmanaged-devices setting in NetworkManager.conf. Changes to this value are not persistent and lost after NetworkManager restart. + + DEPRECATED: 1.58: Use the SetManaged method instead, which supports + additional features like persisting the state to disk --> @@ -391,6 +394,19 @@ --> + + + + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index b66ca45263..605683df42 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -5,6 +5,7 @@ src/core/NetworkManagerUtils.c src/core/devices/adsl/nm-device-adsl.c src/core/devices/bluetooth/nm-bluez-manager.c src/core/devices/bluetooth/nm-device-bt.c +src/core/devices/nm-device.c src/core/devices/nm-device-6lowpan.c src/core/devices/nm-device-bond.c src/core/devices/nm-device-bridge.c diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index ee1f71eec9..2a87026ed3 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -14945,12 +14945,71 @@ typedef struct { NMDeviceManagedFlags managed_flags; } SetManagedData; +/** + * set_managed: + * @self: the device + * @managed: the new managed state to set. + * @flags: flags to select different behaviors like storing to disk. + * @error: return location for a #GError, or %NULL + * + * Sets the managed state of the device. It can affect the runtime managed state + * if the %NM_DEVICE_MANAGED_FLAGS_RUNTIME is set, and to the value stored on disk + * (persistent across reboots) state if the %NM_DEVICE_MANAGED_FLAGS_PERMANENT is set. + * + * Returns: %TRUE if the managed state was set successfully, %FALSE otherwise. + */ static gboolean set_managed(NMDevice *self, gboolean managed, NMDeviceManagedFlags flags, GError **error) { + NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); + NMTernary old = NM_TERNARY_DEFAULT; + nm_assert((flags & ~NM_DEVICE_MANAGED_FLAGS_ALL) == 0); - g_object_set(self, NM_DEVICE_MANAGED, managed, NULL); + if (!NM_FLAGS_ANY(flags, NM_DEVICE_MANAGED_FLAGS_PERMANENT | NM_DEVICE_MANAGED_FLAGS_RUNTIME)) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + _("set managed: no permanent or runtime was selected")); + return FALSE; + } + + if (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT) { + nm_config_get_device_managed(nm_manager_get_config(priv->manager), self, &old, error); + + if (!nm_config_set_device_managed(nm_manager_get_config(priv->manager), + self, + managed, + flags & NM_DEVICE_MANAGED_FLAGS_BY_MAC, + error)) + return FALSE; + + /* Update the unmanaged flags after the change on disk */ + nm_device_set_unmanaged_by_user_conf(self); + + if (!!nm_device_get_unmanaged_flags(self, NM_UNMANAGED_USER_CONF) != !managed) { + /* We failed to make the new state effective on disk. Maybe the new config + * collides with other config. Try to revert and return error. Otherwise, + * we would set the runtime state correctly, but get an unexpected state + * after a reboot. */ + nm_config_set_device_managed(nm_manager_get_config(priv->manager), + self, + old, + flags & NM_DEVICE_MANAGED_FLAGS_BY_MAC, + NULL); + g_set_error(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_FAILED, + _("failed to persist 'managed=%d' on disk, other configurations may be " + "overriding it"), + managed); + return FALSE; + } + } + + if (flags & NM_DEVICE_MANAGED_FLAGS_RUNTIME) { + g_object_set(self, NM_DEVICE_MANAGED, !!managed, NULL); + } return TRUE; } diff --git a/src/libnm-core-public/nm-dbus-interface.h b/src/libnm-core-public/nm-dbus-interface.h index 1ead6f4adc..d2ee1d86c6 100644 --- a/src/libnm-core-public/nm-dbus-interface.h +++ b/src/libnm-core-public/nm-dbus-interface.h @@ -1251,6 +1251,10 @@ typedef enum /*< flags >*/ { /** * NMDeviceManagedFlags: * @NM_DEVICE_MANAGED_FLAGS_NONE: no flag set. + * @NM_DEVICE_MANAGED_FLAGS_RUNTIME: to set the device managed state to the runtime value. + * @NM_DEVICE_MANAGED_FLAGS_PERMANENT: to set the device managed state to the permanent (on disk) value. + * @NM_DEVICE_MANAGED_FLAGS_BY_MAC: to match the device by MAC address, not by name. + * This option only makes sense together with %NM_DEVICE_MANAGED_FLAGS_PERMANENT. * @NM_DEVICE_MANAGED_FLAGS_ALL: all flags. * * Flags for the SetManaged() D-Bus call of a device and nm_device_set_managed_async(). @@ -1258,9 +1262,12 @@ typedef enum /*< flags >*/ { * Since: 1.58 */ typedef enum /*< flags >*/ { - NM_DEVICE_MANAGED_FLAGS_NONE = 0, + NM_DEVICE_MANAGED_FLAGS_NONE = 0, + NM_DEVICE_MANAGED_FLAGS_RUNTIME = 0x1, + NM_DEVICE_MANAGED_FLAGS_PERMANENT = 0x2, + NM_DEVICE_MANAGED_FLAGS_BY_MAC = 0x4, - NM_DEVICE_MANAGED_FLAGS_ALL = 0, /* */ + NM_DEVICE_MANAGED_FLAGS_ALL = 0x7, /* */ } NMDeviceManagedFlags; /** From f346fcf977c22f12b509fff3c674dbfb67490a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Mon, 23 Feb 2026 16:02:49 +0100 Subject: [PATCH 05/12] core: device: allow to reset the managed property Previous commits added the capability to persist to disk the value of 'managed' received via the D-Bus API. Users might need to clear the previous content, thus reseting it to its default. Although this is specially useful for the PERMANENT flag, we need to be consistent and reset the runtime state too. --- .../org.freedesktop.NetworkManager.Device.xml | 4 +- src/core/devices/nm-device.c | 56 +++++++++++++------ src/libnm-client-impl/libnm.ver | 1 + src/libnm-client-impl/nm-device.c | 4 +- src/libnm-client-public/nm-device.h | 2 +- src/libnm-core-public/nm-dbus-interface.h | 16 ++++++ 6 files changed, 61 insertions(+), 22 deletions(-) diff --git a/introspection/org.freedesktop.NetworkManager.Device.xml b/introspection/org.freedesktop.NetworkManager.Device.xml index 802ee2fd68..3b0c2c46b8 100644 --- a/introspection/org.freedesktop.NetworkManager.Device.xml +++ b/introspection/org.freedesktop.NetworkManager.Device.xml @@ -396,7 +396,7 @@ - + diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 2a87026ed3..1dbc82a4aa 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -14941,7 +14941,7 @@ impl_device_get_applied_connection(NMDBusObject *obj, /*****************************************************************************/ typedef struct { - gboolean managed_state; + NMDeviceManaged managed_state; NMDeviceManagedFlags managed_flags; } SetManagedData; @@ -14959,11 +14959,13 @@ typedef struct { * Returns: %TRUE if the managed state was set successfully, %FALSE otherwise. */ static gboolean -set_managed(NMDevice *self, gboolean managed, NMDeviceManagedFlags flags, GError **error) +set_managed(NMDevice *self, NMDeviceManaged managed, NMDeviceManagedFlags flags, GError **error) { NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); - NMTernary old = NM_TERNARY_DEFAULT; + NMTernary managed_to_disk, old = NM_TERNARY_DEFAULT; + nm_assert( + NM_IN_SET(managed, NM_DEVICE_MANAGED_NO, NM_DEVICE_MANAGED_YES, NM_DEVICE_MANAGED_RESET)); nm_assert((flags & ~NM_DEVICE_MANAGED_FLAGS_ALL) == 0); if (!NM_FLAGS_ANY(flags, NM_DEVICE_MANAGED_FLAGS_PERMANENT | NM_DEVICE_MANAGED_FLAGS_RUNTIME)) { @@ -14975,11 +14977,12 @@ set_managed(NMDevice *self, gboolean managed, NMDeviceManagedFlags flags, GError } if (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT) { + managed_to_disk = managed == NM_DEVICE_MANAGED_RESET ? NM_TERNARY_DEFAULT : !!managed; nm_config_get_device_managed(nm_manager_get_config(priv->manager), self, &old, error); if (!nm_config_set_device_managed(nm_manager_get_config(priv->manager), self, - managed, + managed_to_disk, flags & NM_DEVICE_MANAGED_FLAGS_BY_MAC, error)) return FALSE; @@ -14987,7 +14990,8 @@ set_managed(NMDevice *self, gboolean managed, NMDeviceManagedFlags flags, GError /* Update the unmanaged flags after the change on disk */ nm_device_set_unmanaged_by_user_conf(self); - if (!!nm_device_get_unmanaged_flags(self, NM_UNMANAGED_USER_CONF) != !managed) { + if (managed_to_disk != NM_TERNARY_DEFAULT + && managed_to_disk != !nm_device_get_unmanaged_flags(self, NM_UNMANAGED_USER_CONF)) { /* We failed to make the new state effective on disk. Maybe the new config * collides with other config. Try to revert and return error. Otherwise, * we would set the runtime state correctly, but get an unexpected state @@ -15008,7 +15012,14 @@ set_managed(NMDevice *self, gboolean managed, NMDeviceManagedFlags flags, GError } if (flags & NM_DEVICE_MANAGED_FLAGS_RUNTIME) { - g_object_set(self, NM_DEVICE_MANAGED, !!managed, NULL); + if (managed == NM_DEVICE_MANAGED_RESET) { + nm_device_set_unmanaged_by_flags(self, + NM_UNMANAGED_USER_EXPLICIT, + NM_UNMAN_FLAG_OP_FORGET, + NM_DEVICE_STATE_REASON_UNMANAGED_USER_EXPLICIT); + } else { + g_object_set(self, NM_DEVICE_MANAGED, !!managed, NULL); + } } return TRUE; @@ -15022,7 +15033,7 @@ set_managed_cb(NMDevice *self, gpointer user_data) { SetManagedData *set_managed_data = user_data; - gboolean managed; + NMDeviceManaged managed; NMDeviceManagedFlags flags; GError *local = NULL; @@ -15030,11 +15041,20 @@ set_managed_cb(NMDevice *self, flags = set_managed_data->managed_flags; nm_g_slice_free(set_managed_data); - if (!error && (flags & ~NM_DEVICE_MANAGED_FLAGS_ALL) != 0) { - g_set_error_literal(&error, - NM_DEVICE_ERROR, - NM_DEVICE_ERROR_INVALID_ARGUMENT, - "Invalid flags"); + if (!error) { + if (!NM_IN_SET(managed, + NM_DEVICE_MANAGED_NO, + NM_DEVICE_MANAGED_YES, + NM_DEVICE_MANAGED_RESET)) + g_set_error_literal(&error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + "Invalid managed value"); + else if ((flags & ~NM_DEVICE_MANAGED_FLAGS_ALL) != 0) + g_set_error_literal(&error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + "Invalid flags"); } if (error) { @@ -15074,15 +15094,17 @@ impl_device_set_managed(NMDBusObject *obj, { NMDevice *self = NM_DEVICE(obj); gs_free_error GError *error = NULL; - gboolean managed; + guint32 managed_u; + NMDeviceManaged managed; guint32 flags_u; NMDeviceManagedFlags flags; SetManagedData *set_managed_data; - g_variant_get(parameters, "(bu)", &managed, &flags_u); + g_variant_get(parameters, "(uu)", &managed_u, &flags_u); - flags = flags_u; - nm_assert(flags == flags_u); + managed = managed_u; + flags = flags_u; + nm_assert(managed == managed_u && flags == flags_u); set_managed_data = g_slice_new(SetManagedData); *set_managed_data = (SetManagedData) { @@ -20066,7 +20088,7 @@ static const NMDBusInterfaceInfoExtended interface_info_device = { NM_DEFINE_DBUS_METHOD_INFO_EXTENDED( NM_DEFINE_GDBUS_METHOD_INFO_INIT("SetManaged", .in_args = NM_DEFINE_GDBUS_ARG_INFOS( - NM_DEFINE_GDBUS_ARG_INFO("managed", "b"), + NM_DEFINE_GDBUS_ARG_INFO("managed", "u"), NM_DEFINE_GDBUS_ARG_INFO("flags", "u"), ), ), .handle = impl_device_set_managed, ), NM_DEFINE_DBUS_METHOD_INFO_EXTENDED(NM_DEFINE_GDBUS_METHOD_INFO_INIT("Disconnect", ), diff --git a/src/libnm-client-impl/libnm.ver b/src/libnm-client-impl/libnm.ver index 67db5394a6..62d486ffa9 100644 --- a/src/libnm-client-impl/libnm.ver +++ b/src/libnm-client-impl/libnm.ver @@ -2115,6 +2115,7 @@ global: nm_device_geneve_get_ttl; nm_device_geneve_get_type; nm_device_managed_flags_get_type; + nm_device_managed_get_type; nm_device_set_managed_async; nm_device_set_managed_finish; nm_ip_config_get_clat_address; diff --git a/src/libnm-client-impl/nm-device.c b/src/libnm-client-impl/nm-device.c index 605aae5fc7..80c7de0612 100644 --- a/src/libnm-client-impl/nm-device.c +++ b/src/libnm-client-impl/nm-device.c @@ -1485,7 +1485,7 @@ nm_device_set_managed(NMDevice *device, gboolean managed) **/ void nm_device_set_managed_async(NMDevice *device, - gboolean managed, + NMDeviceManaged managed, NMDeviceManagedFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, @@ -1503,7 +1503,7 @@ nm_device_set_managed_async(NMDevice *device, _nm_object_get_path(device), NM_DBUS_INTERFACE_DEVICE, "SetManaged", - g_variant_new("(bu)", managed, flags), + g_variant_new("(uu)", managed, flags), G_VARIANT_TYPE("()"), G_DBUS_CALL_FLAGS_NONE, NM_DBUS_DEFAULT_TIMEOUT_MSEC, diff --git a/src/libnm-client-public/nm-device.h b/src/libnm-client-public/nm-device.h index 958367a70d..d0d004f86d 100644 --- a/src/libnm-client-public/nm-device.h +++ b/src/libnm-client-public/nm-device.h @@ -152,7 +152,7 @@ _NM_DEPRECATED_SYNC_METHOD void nm_device_set_managed(NMDevice *device, gboolean managed); NM_AVAILABLE_IN_1_58 void nm_device_set_managed_async(NMDevice *device, - gboolean managed, + NMDeviceManaged managed, NMDeviceManagedFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, diff --git a/src/libnm-core-public/nm-dbus-interface.h b/src/libnm-core-public/nm-dbus-interface.h index d2ee1d86c6..1bcbc00410 100644 --- a/src/libnm-core-public/nm-dbus-interface.h +++ b/src/libnm-core-public/nm-dbus-interface.h @@ -1248,6 +1248,22 @@ typedef enum /*< flags >*/ { NM_DEVICE_REAPPLY_FLAGS_PRESERVE_EXTERNAL_IP = 0x1, } NMDeviceReapplyFlags; +/** + * NMDeviceManaged: + * @NM_DEVICE_MANAGED_NO: the device is not managed. + * @NM_DEVICE_MANAGED_YES: the device is managed. + * @NM_DEVICE_MANAGED_RESET: reset the device managed state to the default value. + * + * Values for the SetManaged() D-Bus call of a device and nm_device_set_managed_async(). + * + * Since: 1.58 + */ +typedef enum { + NM_DEVICE_MANAGED_NO = 0, + NM_DEVICE_MANAGED_YES = 1, + NM_DEVICE_MANAGED_RESET = 2, +} NMDeviceManaged; + /** * NMDeviceManagedFlags: * @NM_DEVICE_MANAGED_FLAGS_NONE: no flag set. From b9725dab739f35e941166e2a81269885c5a6672f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Fri, 13 Feb 2026 15:41:27 +0100 Subject: [PATCH 06/12] core: device: allow to change the admin state of the device in SetManaged() Control it with a new NM_DEVICE_MANAGED_SET_ADMIN_STATE flag. This flag will make that, at the same time that the device is moved to managed/unmanaged, it's admin state is set to up/down. Many users want to have a way to have their devices in a DOWN admin state when they are not using them. Because of the complex activation process, NM wants to have its devices in UP state all the time. However, it is not a problem to have it DOWN if we are not managing it. --- src/core/devices/nm-device.c | 10 ++++++++++ src/libnm-core-public/nm-dbus-interface.h | 15 ++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 1dbc82a4aa..a8c2248940 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -15019,6 +15019,16 @@ set_managed(NMDevice *self, NMDeviceManaged managed, NMDeviceManagedFlags flags, NM_DEVICE_STATE_REASON_UNMANAGED_USER_EXPLICIT); } else { g_object_set(self, NM_DEVICE_MANAGED, !!managed, NULL); + + /* If requested, set the administrative state of the device to UP if the + * new managed state is YES, and to DOWN if it's NO. */ + if (flags & NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE) { + if (nm_device_get_ifindex(self)) + nm_platform_link_change_flags(nm_device_get_platform(self), + nm_device_get_ifindex(self), + IFF_UP, + !!managed); + } } } diff --git a/src/libnm-core-public/nm-dbus-interface.h b/src/libnm-core-public/nm-dbus-interface.h index 1bcbc00410..325060a0f3 100644 --- a/src/libnm-core-public/nm-dbus-interface.h +++ b/src/libnm-core-public/nm-dbus-interface.h @@ -1271,6 +1271,10 @@ typedef enum { * @NM_DEVICE_MANAGED_FLAGS_PERMANENT: to set the device managed state to the permanent (on disk) value. * @NM_DEVICE_MANAGED_FLAGS_BY_MAC: to match the device by MAC address, not by name. * This option only makes sense together with %NM_DEVICE_MANAGED_FLAGS_PERMANENT. + * @NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE: to set the administrative state of the + * device to up if the managed state is %NM_DEVICE_MANAGED_YES, and down if the managed state + * is %NM_DEVICE_MANAGED_NO. If the flag is not set, the administrative state is not changed. + * The flag is ignored for %NM_DEVICE_MANAGED_RESET. * @NM_DEVICE_MANAGED_FLAGS_ALL: all flags. * * Flags for the SetManaged() D-Bus call of a device and nm_device_set_managed_async(). @@ -1278,12 +1282,13 @@ typedef enum { * Since: 1.58 */ typedef enum /*< flags >*/ { - NM_DEVICE_MANAGED_FLAGS_NONE = 0, - NM_DEVICE_MANAGED_FLAGS_RUNTIME = 0x1, - NM_DEVICE_MANAGED_FLAGS_PERMANENT = 0x2, - NM_DEVICE_MANAGED_FLAGS_BY_MAC = 0x4, + NM_DEVICE_MANAGED_FLAGS_NONE = 0, + NM_DEVICE_MANAGED_FLAGS_RUNTIME = 0x1, + NM_DEVICE_MANAGED_FLAGS_PERMANENT = 0x2, + NM_DEVICE_MANAGED_FLAGS_BY_MAC = 0x4, + NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE = 0x8, - NM_DEVICE_MANAGED_FLAGS_ALL = 0x7, /* */ + NM_DEVICE_MANAGED_FLAGS_ALL = 0xF, /* */ } NMDeviceManagedFlags; /** From 7c8f343f2c4e0937f30231426c00bd941916e7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Wed, 25 Feb 2026 12:30:50 +0100 Subject: [PATCH 07/12] core: device: autoselect device match criteria in SetManaged() Devices like veth without a permanent MAC address cannot be matched by MAC. If using the BY_MAC flag in SetManaged(), the changes are not effective for such kind of devices. Add a BY_NAME flag, in addition to the BY_MAC one. If the client sets one of them, it means to force this mode of matching. If none is selected, the daemon will choose how to match, preferring matching by MAC when possible, and by ifname when not possible. --- src/core/devices/nm-device.c | 47 +++++++++++++++++++++-- src/libnm-core-public/nm-dbus-interface.h | 21 ++++++---- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index a8c2248940..4b05ad1ee8 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -14945,6 +14945,43 @@ typedef struct { NMDeviceManagedFlags managed_flags; } SetManagedData; +static gboolean +get_managed_match_by_mac(NMDevice *self, NMDeviceManagedFlags flags, gboolean *out, GError **error) +{ + gboolean is_fake_hwaddr; + + nm_assert(out); + + if ((flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC) + && (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME)) { + g_set_error_literal(error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + "cannot match both by 'mac' and by 'interface-name'"); + return FALSE; + } + + nm_device_get_permanent_hw_address_full(self, TRUE, &is_fake_hwaddr); + + if ((flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC) && is_fake_hwaddr) { + g_set_error_literal( + error, + NM_DEVICE_ERROR, + NM_DEVICE_ERROR_INVALID_ARGUMENT, + "cannot match by 'mac': the device doesn't have a permanent MAC address"); + return FALSE; + } + + if (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC) + *out = TRUE; + else if (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME) + *out = FALSE; + else + *out = !is_fake_hwaddr; + + return TRUE; +} + /** * set_managed: * @self: the device @@ -14962,7 +14999,6 @@ static gboolean set_managed(NMDevice *self, NMDeviceManaged managed, NMDeviceManagedFlags flags, GError **error) { NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self); - NMTernary managed_to_disk, old = NM_TERNARY_DEFAULT; nm_assert( NM_IN_SET(managed, NM_DEVICE_MANAGED_NO, NM_DEVICE_MANAGED_YES, NM_DEVICE_MANAGED_RESET)); @@ -14977,13 +15013,18 @@ set_managed(NMDevice *self, NMDeviceManaged managed, NMDeviceManagedFlags flags, } if (flags & NM_DEVICE_MANAGED_FLAGS_PERMANENT) { + NMTernary managed_to_disk, old = NM_TERNARY_DEFAULT; + gboolean by_mac; + managed_to_disk = managed == NM_DEVICE_MANAGED_RESET ? NM_TERNARY_DEFAULT : !!managed; nm_config_get_device_managed(nm_manager_get_config(priv->manager), self, &old, error); + if (!get_managed_match_by_mac(self, flags, &by_mac, error)) + return FALSE; if (!nm_config_set_device_managed(nm_manager_get_config(priv->manager), self, managed_to_disk, - flags & NM_DEVICE_MANAGED_FLAGS_BY_MAC, + by_mac, error)) return FALSE; @@ -14999,7 +15040,7 @@ set_managed(NMDevice *self, NMDeviceManaged managed, NMDeviceManagedFlags flags, nm_config_set_device_managed(nm_manager_get_config(priv->manager), self, old, - flags & NM_DEVICE_MANAGED_FLAGS_BY_MAC, + by_mac, NULL); g_set_error(error, NM_DEVICE_ERROR, diff --git a/src/libnm-core-public/nm-dbus-interface.h b/src/libnm-core-public/nm-dbus-interface.h index 325060a0f3..c16a115a08 100644 --- a/src/libnm-core-public/nm-dbus-interface.h +++ b/src/libnm-core-public/nm-dbus-interface.h @@ -1269,8 +1269,8 @@ typedef enum { * @NM_DEVICE_MANAGED_FLAGS_NONE: no flag set. * @NM_DEVICE_MANAGED_FLAGS_RUNTIME: to set the device managed state to the runtime value. * @NM_DEVICE_MANAGED_FLAGS_PERMANENT: to set the device managed state to the permanent (on disk) value. - * @NM_DEVICE_MANAGED_FLAGS_BY_MAC: to match the device by MAC address, not by name. - * This option only makes sense together with %NM_DEVICE_MANAGED_FLAGS_PERMANENT. + * @NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME: to match the device by name, not by MAC address. + * @NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC: to match the device by MAC address, not by name. * @NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE: to set the administrative state of the * device to up if the managed state is %NM_DEVICE_MANAGED_YES, and down if the managed state * is %NM_DEVICE_MANAGED_NO. If the flag is not set, the administrative state is not changed. @@ -1279,16 +1279,21 @@ typedef enum { * * Flags for the SetManaged() D-Bus call of a device and nm_device_set_managed_async(). * + * %NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME and %NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC + * are mutually exclusive, and they only make sense together with %NM_DEVICE_MANAGED_FLAGS_PERMANENT. + * If none is set, the matching criteria is selected automatically. + * * Since: 1.58 */ typedef enum /*< flags >*/ { - NM_DEVICE_MANAGED_FLAGS_NONE = 0, - NM_DEVICE_MANAGED_FLAGS_RUNTIME = 0x1, - NM_DEVICE_MANAGED_FLAGS_PERMANENT = 0x2, - NM_DEVICE_MANAGED_FLAGS_BY_MAC = 0x4, - NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE = 0x8, + NM_DEVICE_MANAGED_FLAGS_NONE = 0, + NM_DEVICE_MANAGED_FLAGS_RUNTIME = 0x1, + NM_DEVICE_MANAGED_FLAGS_PERMANENT = 0x2, + NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_NAME = 0x4, + NM_DEVICE_MANAGED_FLAGS_PERMANENT_BY_MAC = 0x8, + NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE = 0x10, - NM_DEVICE_MANAGED_FLAGS_ALL = 0xF, /* */ + NM_DEVICE_MANAGED_FLAGS_ALL = 0x1F, /* */ } NMDeviceManagedFlags; /** From d2f98a1669df1eb447785bdf2583d30737bfa4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Thu, 12 Feb 2026 15:39:47 +0100 Subject: [PATCH 08/12] nmcli: add `managed --permanent yes/no/up/down/reset` Allow to manage or unmanage a device persisting across reboots. If --permanent is not specified, only the runtime managed state is changed, preserving the previous behavior. The --permanent-only option allows to edit only the persistent value, without touching the runtime value. Also add the values up/down. Up means managed=yes and set device's administrative state UP. Down means managed=no and admin state DOWN. Add the value 'reset' too. It reverts managed runtime status to default behaviour. When used with `--permanent` flag, the persisted managed settings is cleared. Co-authored-by: Rahul Rajesh --- man/nmcli.xml | 16 +++++ src/nmcli/devices.c | 158 +++++++++++++++++++++++++++++++++----------- 2 files changed, 134 insertions(+), 40 deletions(-) diff --git a/man/nmcli.xml b/man/nmcli.xml index 764e5faca9..3df25a0cf1 100644 --- a/man/nmcli.xml +++ b/man/nmcli.xml @@ -1436,15 +1436,31 @@ + + --permanent + --permanent-only + yes no + up + down + reset Set device properties. + + The property accepts a + option to persist the managed state to disk, and not only in runtime. With + only the permanent managed state is set, and not the + runtime managed state. The special values and + can be used to set the administrative state of the device at the same time as the runtime + managed state. The value clears the explicit managed setting, and + with or it also removes + the persisted managed setting. diff --git a/src/nmcli/devices.c b/src/nmcli/devices.c index 64b4ed366f..0ac57feb5b 100644 --- a/src/nmcli/devices.c +++ b/src/nmcli/devices.c @@ -857,7 +857,8 @@ usage(void) "delete | monitor | wifi | lldp }\n\n" " status\n\n" " show []\n\n" - " set [ifname] [autoconnect yes|no] [managed yes|no]\n\n" + " set [ifname] [autoconnect yes|no] [managed [--permanent|--permanent-only] " + "yes|no|up|down|reset]\n\n" " connect \n\n" " reapply \n\n" " modify ([+|-]. )+\n\n" @@ -975,14 +976,15 @@ usage_device_delete(void) static void usage_device_set(void) { - nmc_printerr(_("Usage: nmcli device set { ARGUMENTS | help }\n" - "\n" - "ARGUMENTS := DEVICE { PROPERTY [ PROPERTY ... ] }\n" - "DEVICE := [ifname] \n" - "PROPERTY := { autoconnect { yes | no } |\n" - " { managed { yes | no }\n" - "\n" - "Modify device properties.\n\n")); + nmc_printerr(_( + "Usage: nmcli device set { ARGUMENTS | help }\n" + "\n" + "ARGUMENTS := DEVICE { PROPERTY [ PROPERTY ... ] }\n" + "DEVICE := [ifname] \n" + "PROPERTY := { autoconnect { yes | no } |\n" + " { managed [--permanent | --permanent-only] { yes | no | up | down | reset }\n" + "\n" + "Modify device properties.\n\n")); } static void @@ -2819,17 +2821,18 @@ do_devices_delete(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const static void do_device_set(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv) { -#define DEV_SET_AUTOCONNECT 0 -#define DEV_SET_MANAGED 1 NMDevice *device = NULL; int i; struct { int idx; gboolean value; - } values[2] = { - [DEV_SET_AUTOCONNECT] = {-1}, - [DEV_SET_MANAGED] = {-1}, - }; + } autoconnect_data = {-1}; + struct { + int idx; + NMDeviceManaged value; + guint32 flags; + } managed_data = {-1}; + gs_free_error GError *error = NULL; next_arg(nmc, &argc, &argv, NULL); @@ -2851,49 +2854,120 @@ do_device_set(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *ar i = 0; do { - gboolean flag; - if (argc == 1 && nmc->complete) nmc_complete_strings(*argv, "managed", "autoconnect"); if (matches(*argv, "managed")) { + NMDeviceManaged val; + guint32 flags = 0; + gboolean val_bool; + gboolean perm = FALSE, perm_only = FALSE; + argc--; argv++; + + if (argc == 1 && nmc->complete) { + nmc_complete_strings(*argv, + "true", + "yes", + "on", + "false", + "no", + "off", + "up", + "down", + "reset", + "--permanent", + "--permanent-only"); + } + + if (managed_data.idx != -1) { + g_string_printf(nmc->return_text, _("Error: 'managed' can only be set once.")); + nmc->return_value = NMC_RESULT_ERROR_USER_INPUT; + return; + } + + while (argc > 0) { + /* --perm matches with --permanent, the most common, + * but --permanent-only requires a exact match */ + if (nm_streq0(*argv, "--permanent-only")) + perm_only = TRUE; + else if (matches(*argv, "--permanent")) + perm = TRUE; + else + break; + argc--; + argv++; + } + + if (perm_only && perm) { + g_string_printf( + nmc->return_text, + _("Error: '--permanent-only' and '--permanent' cannot be used together.")); + nmc->return_value = NMC_RESULT_ERROR_USER_INPUT; + return; + } else if (perm) { + flags |= NM_DEVICE_MANAGED_FLAGS_RUNTIME | NM_DEVICE_MANAGED_FLAGS_PERMANENT; + } else if (perm_only) { + flags |= NM_DEVICE_MANAGED_FLAGS_PERMANENT; + } else { + /* If --permanent/--permanent-only are missing, set only the runtime flag, as this + * is how it used to work when these options didn't exist. */ + flags |= NM_DEVICE_MANAGED_FLAGS_RUNTIME; + } + if (!argc) { - g_string_printf(nmc->return_text, - _("Error: '%s' argument is missing."), - *(argv - 1)); + g_string_printf(nmc->return_text, _("Error: 'managed' argument is missing.")); nmc->return_value = NMC_RESULT_ERROR_USER_INPUT; return; } - if (argc == 1 && nmc->complete) - nmc_complete_bool(*argv); - if (!nmc_string_to_bool(*argv, &flag, &error)) { - g_string_printf(nmc->return_text, _("Error: 'managed': %s."), error->message); + + if (matches(*argv, "up") || matches(*argv, "down")) { + flags |= NM_DEVICE_MANAGED_FLAGS_SET_ADMIN_STATE; + val = matches(*argv, "up") ? NM_DEVICE_MANAGED_YES : NM_DEVICE_MANAGED_NO; + } else if (matches(*argv, "reset")) { + val = NM_DEVICE_MANAGED_RESET; + } else if (nmc_string_to_bool(*argv, &val_bool, NULL)) { + val = val_bool ? NM_DEVICE_MANAGED_YES : NM_DEVICE_MANAGED_NO; + } else { + g_string_printf( + nmc->return_text, + _("Error: 'managed': '%s' is not valid, use 'yes/no/up/down/reset'."), + *argv); nmc->return_value = NMC_RESULT_ERROR_USER_INPUT; return; } - values[DEV_SET_MANAGED].idx = ++i; - values[DEV_SET_MANAGED].value = flag; + + managed_data.idx = i++; + managed_data.value = val; + managed_data.flags = flags; } else if (matches(*argv, "autoconnect")) { + gboolean val; + argc--; argv++; + if (!argc) { - g_string_printf(nmc->return_text, - _("Error: '%s' argument is missing."), - *(argv - 1)); + g_string_printf(nmc->return_text, _("Error: 'autoconnect' argument is missing.")); nmc->return_value = NMC_RESULT_ERROR_USER_INPUT; return; } + if (autoconnect_data.idx != -1) { + g_string_printf(nmc->return_text, _("Error: 'autoconnect' can only be set once.")); + nmc->return_value = NMC_RESULT_ERROR_USER_INPUT; + return; + } + if (argc == 1 && nmc->complete) nmc_complete_bool(*argv); - if (!nmc_string_to_bool(*argv, &flag, &error)) { + + if (!nmc_string_to_bool(*argv, &val, &error)) { g_string_printf(nmc->return_text, _("Error: 'autoconnect': %s."), error->message); nmc->return_value = NMC_RESULT_ERROR_USER_INPUT; return; } - values[DEV_SET_AUTOCONNECT].idx = ++i; - values[DEV_SET_AUTOCONNECT].value = flag; + autoconnect_data.idx = i++; + autoconnect_data.value = val; } else { g_string_printf(nmc->return_text, _("Error: property '%s' is not known."), *argv); nmc->return_value = NMC_RESULT_ERROR_USER_INPUT; @@ -2906,15 +2980,19 @@ do_device_set(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *ar /* when multiple properties are specified, set them in the order as they * are specified on the command line. */ - if (values[DEV_SET_AUTOCONNECT].idx >= 0 && values[DEV_SET_MANAGED].idx >= 0 - && values[DEV_SET_MANAGED].idx < values[DEV_SET_AUTOCONNECT].idx) { - nm_device_set_managed(device, values[DEV_SET_MANAGED].value); - values[DEV_SET_MANAGED].idx = -1; + for (i = 0; i < 2; i++) { + if (autoconnect_data.idx == i) { + nm_device_set_autoconnect(device, autoconnect_data.value); + } + if (managed_data.idx == i) { + nm_device_set_managed_async(device, + managed_data.value, + managed_data.flags, + NULL, + NULL, + NULL); + } } - if (values[DEV_SET_AUTOCONNECT].idx >= 0) - nm_device_set_autoconnect(device, values[DEV_SET_AUTOCONNECT].value); - if (values[DEV_SET_MANAGED].idx >= 0) - nm_device_set_managed(device, values[DEV_SET_MANAGED].value); } static void From 7ee50b687a86c6cc6ecb2dd912ff58f873db5e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Mon, 2 Mar 2026 12:49:18 +0100 Subject: [PATCH 09/12] nmcli: wait for `device set` async operation to finish We need to wait for it to finish so we can show error messages, if any. Also, if we don't do it, sometimes the `d set eth0 managed ...` operation fails with the following message in the daemon's log: "Unable to determine UID of the request". This is because the client's process is terminated before the daemon can check the permissions, as it needs to check the uid and gid from the client's process. --- src/nmcli/devices.c | 58 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/nmcli/devices.c b/src/nmcli/devices.c index 0ac57feb5b..524e08b4a9 100644 --- a/src/nmcli/devices.c +++ b/src/nmcli/devices.c @@ -2818,11 +2818,52 @@ do_devices_delete(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const } } +typedef struct { + NmCli *nmc; + GSource *timeout_source; +} DeviceSetCbInfo; + +static void +device_set_cb_info_finish(DeviceSetCbInfo *info) +{ + nm_clear_g_source_inst(&info->timeout_source); + g_slice_free(DeviceSetCbInfo, info); + quit(); +} + +static gboolean +device_set_timeout_cb(gpointer user_data) +{ + DeviceSetCbInfo *cb_info = user_data; + + timeout_cb(cb_info->nmc); + device_set_cb_info_finish(cb_info); + return G_SOURCE_REMOVE; +} + +static void +device_set_cb(GObject *object, GAsyncResult *result, gpointer user_data) +{ + NMDevice *device = NM_DEVICE(object); + DeviceSetCbInfo *info = (DeviceSetCbInfo *) user_data; + NmCli *nmc = info->nmc; + gs_free_error GError *error = NULL; + + /* Only 'managed' is treated asynchronously, 'autoconnect' is treated synchronously */ + if (!nm_device_set_managed_finish(device, result, &error)) { + g_string_printf(nmc->return_text, _("Error: set managed failed: %s."), error->message); + nmc->return_value = NMC_RESULT_ERROR_UNKNOWN; + } + + device_set_cb_info_finish(info); +} + static void do_device_set(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv) { - NMDevice *device = NULL; - int i; + NMDevice *device = NULL; + DeviceSetCbInfo *cb_info = NULL; + int i; struct { int idx; gboolean value; @@ -2985,12 +3026,21 @@ do_device_set(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *ar nm_device_set_autoconnect(device, autoconnect_data.value); } if (managed_data.idx == i) { + cb_info = g_slice_new0(DeviceSetCbInfo); + cb_info->nmc = nmc; + if (nmc->timeout > 0) + cb_info->timeout_source = + nm_g_timeout_add_seconds_source(nmc->timeout, device_set_timeout_cb, cb_info); + + nmc->nowait_flag = (nmc->timeout == 0); + nmc->should_wait++; + nm_device_set_managed_async(device, managed_data.value, managed_data.flags, NULL, - NULL, - NULL); + device_set_cb, + cb_info); } } } From 1252f8dc7e27a56ea776da9ac8dcdcbb65aa5109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 3 Mar 2026 14:29:14 +0100 Subject: [PATCH 10/12] core: config: add unit tests for the new get/set_device_managed --- src/core/tests/config/test-config.c | 98 +++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/core/tests/config/test-config.c b/src/core/tests/config/test-config.c index 78fd10571d..26588d1a1d 100644 --- a/src/core/tests/config/test-config.c +++ b/src/core/tests/config/test-config.c @@ -311,6 +311,103 @@ test_config_override(void) g_assert_cmpstr(plugins[3], ==, "delta"); } +static void +test_config_managed(void) +{ + NMConfig *config; + const char *CONFIG_USER = BUILD_DIR "/test-config-managed.conf"; + const char *CONFIG_INTERN = BUILD_DIR "/test-config-managed-intern.conf"; + NMDevice *dev; + gs_free char *group_by_name = NULL; + const char *ifname, *group_by_mac; + NMTernary managed; + GKeyFile *kf = nm_config_create_keyfile(); + + dev = nm_test_device_new("11:11:11:11:11:11"); + ifname = nm_device_get_iface(dev); + group_by_name = + g_strdup_printf(NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_DEVICE "-manage-%s", ifname); + group_by_mac = NM_CONFIG_KEYFILE_GROUPPREFIX_INTERN_DEVICE "-manage-11-11-11-11-11-11"; + + g_assert(g_file_set_contents(CONFIG_USER, "", 0, NULL)); + g_assert(g_file_set_contents(CONFIG_INTERN, "", 0, NULL)); + + config = setup_config(NULL, CONFIG_USER, CONFIG_INTERN, NULL, "/no/such/dir", "", NULL); + + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert_cmpint(managed, ==, NM_TERNARY_DEFAULT); + + /* Matching by name */ + NMTST_EXPECT_NM_INFO("config: signal: *"); + g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_TRUE, FALSE, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert_cmpint(managed, ==, NM_TERNARY_TRUE); + g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); + g_assert_false(g_key_file_has_key(kf, group_by_mac, "managed", NULL)); + g_assert_true(g_key_file_has_key(kf, group_by_name, "managed", NULL)); + g_assert_cmpint(g_key_file_get_integer(kf, group_by_name, "managed", NULL), ==, 1); + + NMTST_EXPECT_NM_INFO("config: signal: *"); + g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_FALSE, FALSE, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert_cmpint(managed, ==, NM_TERNARY_FALSE); + g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); + g_assert_false(g_key_file_has_key(kf, group_by_mac, "managed", NULL)); + g_assert_true(g_key_file_has_key(kf, group_by_name, "managed", NULL)); + g_assert_cmpint(g_key_file_get_integer(kf, group_by_name, "managed", NULL), ==, 0); + + /* Matching by MAC address */ + NMTST_EXPECT_NM_INFO("config: signal: *"); + g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_TRUE, TRUE, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert_cmpint(managed, ==, NM_TERNARY_TRUE); + g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); + g_assert_false(g_key_file_has_key(kf, group_by_name, "managed", NULL)); + g_assert_true(g_key_file_has_key(kf, group_by_mac, "managed", NULL)); + g_assert_cmpint(g_key_file_get_integer(kf, group_by_mac, "managed", NULL), ==, 1); + + NMTST_EXPECT_NM_INFO("config: signal: *"); + g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_FALSE, TRUE, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert_cmpint(managed, ==, NM_TERNARY_FALSE); + g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); + g_assert_false(g_key_file_has_key(kf, group_by_name, "managed", NULL)); + g_assert_true(g_key_file_has_key(kf, group_by_mac, "managed", NULL)); + g_assert_cmpint(g_key_file_get_integer(kf, group_by_mac, "managed", NULL), ==, 0); + + /* Resetting the managed state */ + NMTST_EXPECT_NM_INFO("config: signal: *"); + g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_DEFAULT, FALSE, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert_cmpint(managed, ==, NM_TERNARY_DEFAULT); + g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); + g_assert_false(g_key_file_has_key(kf, group_by_name, "managed", NULL)); + g_assert_false(g_key_file_has_key(kf, group_by_mac, "managed", NULL)); + + g_object_unref(config); + + /* Both values set in the intern config file, different values */ + g_key_file_set_string(kf, group_by_name, "managed", "1"); + g_key_file_set_string(kf, group_by_mac, "managed", "0"); + g_assert(g_key_file_save_to_file(kf, CONFIG_INTERN, NULL)); + config = setup_config(NULL, CONFIG_USER, CONFIG_INTERN, NULL, "/no/such/dir", "", NULL); + g_assert(!nm_config_get_device_managed(config, dev, &managed, NULL)); + + g_object_unref(config); + + /* Both values set in the intern config file, same values */ + g_key_file_set_string(kf, group_by_name, "managed", "1"); + g_key_file_set_string(kf, group_by_mac, "managed", "1"); + g_assert(g_key_file_save_to_file(kf, CONFIG_INTERN, NULL)); + config = setup_config(NULL, CONFIG_USER, CONFIG_INTERN, NULL, "/no/such/dir", "", NULL); + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert_cmpint(managed, ==, NM_TERNARY_TRUE); + + g_key_file_unref(kf); + g_object_unref(dev); + g_object_unref(config); +} + static void test_config_global_dns(void) { @@ -1412,6 +1509,7 @@ main(int argc, char **argv) g_test_add_func("/config/set-values", test_config_set_values); g_test_add_func("/config/global-dns", test_config_global_dns); + g_test_add_func("/config/managed", test_config_managed); g_test_add_func("/config/connectivity-check", test_config_connectivity_check); g_test_add_func("/config/signal", test_config_signal); From 2fbaca1cbc7237175f91445e9d58cab775218f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Wed, 4 Mar 2026 10:15:32 +0100 Subject: [PATCH 11/12] checkpoint: rollback devices' "permanently managed" configuration If a device's "managed" configuration is changed persistently (stored to NM-intern), it needs to be undone in a rollback. --- src/core/devices/nm-device.c | 2 +- src/core/nm-checkpoint.c | 73 ++++++++++++++++++++++++++--- src/core/nm-config.c | 23 ++++++--- src/core/nm-config.h | 1 + src/core/tests/config/test-config.c | 21 +++++---- 5 files changed, 98 insertions(+), 22 deletions(-) diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 4b05ad1ee8..81a403f201 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -15017,7 +15017,7 @@ set_managed(NMDevice *self, NMDeviceManaged managed, NMDeviceManagedFlags flags, gboolean by_mac; managed_to_disk = managed == NM_DEVICE_MANAGED_RESET ? NM_TERNARY_DEFAULT : !!managed; - nm_config_get_device_managed(nm_manager_get_config(priv->manager), self, &old, error); + nm_config_get_device_managed(nm_manager_get_config(priv->manager), self, &old, NULL, error); if (!get_managed_match_by_mac(self, flags, &by_mac, error)) return FALSE; diff --git a/src/core/nm-checkpoint.c b/src/core/nm-checkpoint.c index 45ea3a73b6..10db0dcbab 100644 --- a/src/core/nm-checkpoint.c +++ b/src/core/nm-checkpoint.c @@ -39,7 +39,9 @@ typedef struct { bool activation_lifetime_bound_to_profile_visibility : 1; bool settings_connection_is_unsaved : 1; bool settings_connection_is_shadowed_owned : 1; + bool permanent_managed_by_mac : 1; NMUnmanFlagOp unmanaged_explicit; + NMTernary permanent_managed; NMActivationReason activation_reason; gulong dev_exported_change_id; } DeviceCheckpoint; @@ -496,14 +498,19 @@ nm_checkpoint_rollback(NMCheckpoint *self) /* Start rolling-back each device */ g_hash_table_iter_init(&iter, priv->devices); while (g_hash_table_iter_next(&iter, (gpointer *) &device, (gpointer *) &dev_checkpoint)) { - guint32 result = NM_ROLLBACK_RESULT_OK; + guint32 result = NM_ROLLBACK_RESULT_OK; + NMTernary perm_managed = NM_TERNARY_DEFAULT; + gboolean perm_managed_by_mac = FALSE; + gboolean force_perm_managed; _LOGD("rollback: restoring device %s (state %d, realized %d, explicitly unmanaged %d, " - "connection-unsaved %d, connection-shadowed %d, connection-shadowed-owned %d)", + "permanently managed %d, connection-unsaved %d, connection-shadowed %d, " + "connection-shadowed-owned %d)", dev_checkpoint->original_dev_name, (int) dev_checkpoint->state, dev_checkpoint->realized, dev_checkpoint->unmanaged_explicit, + dev_checkpoint->permanent_managed, dev_checkpoint->settings_connection_is_unsaved, !!dev_checkpoint->settings_connection_shadowed, dev_checkpoint->settings_connection_is_shadowed_owned); @@ -541,6 +548,43 @@ nm_checkpoint_rollback(NMCheckpoint *self) NM_DEVICE_STATE_REASON_NOW_MANAGED); } + force_perm_managed = !nm_config_get_device_managed(nm_config_get(), + device, + &perm_managed, + &perm_managed_by_mac, + NULL); + + if (force_perm_managed || (perm_managed != dev_checkpoint->permanent_managed) + || (dev_checkpoint->permanent_managed != NM_TERNARY_DEFAULT + && perm_managed_by_mac != dev_checkpoint->permanent_managed_by_mac)) { + gs_free_error GError *error = NULL; + NMUnmanFlagOp set_op; + + _LOGD("rollback: restore permanent managed state"); + + if (!nm_config_set_device_managed(nm_config_get(), + device, + dev_checkpoint->permanent_managed, + dev_checkpoint->permanent_managed_by_mac, + &error)) { + _LOGE("rollback: failed to restore permanent managed state: %s", error->message); + result = NM_ROLLBACK_RESULT_ERR_FAILED; + /* even if this failed, we try to continue the rollback */ + } + + if (dev_checkpoint->permanent_managed == NM_TERNARY_TRUE) + set_op = NM_UNMAN_FLAG_OP_SET_MANAGED; + else if (dev_checkpoint->permanent_managed == NM_TERNARY_FALSE) + set_op = NM_UNMAN_FLAG_OP_SET_UNMANAGED; + else + set_op = NM_UNMAN_FLAG_OP_FORGET; + + nm_device_set_unmanaged_by_flags_queue(device, + NM_UNMANAGED_USER_CONF, + set_op, + NM_DEVICE_STATE_REASON_NOW_MANAGED); + } + if (dev_checkpoint->state == NM_DEVICE_STATE_UNMANAGED) { if (nm_device_get_state(device) != NM_DEVICE_STATE_UNMANAGED || dev_checkpoint->unmanaged_explicit == NM_UNMAN_FLAG_OP_SET_UNMANAGED) { @@ -703,6 +747,8 @@ device_checkpoint_create(NMCheckpoint *self, NMDevice *device) NMSettingsConnection *settings_connection; const char *path; NMActRequest *act_request; + gboolean perm_managed_by_mac; + gs_free_error GError *error = NULL; nm_assert(NM_IS_DEVICE(device)); nm_assert(nm_device_is_real(device)); @@ -728,12 +774,26 @@ device_checkpoint_create(NMCheckpoint *self, NMDevice *device) } else dev_checkpoint->unmanaged_explicit = NM_UNMAN_FLAG_OP_FORGET; + if (nm_config_get_device_managed(nm_config_get(), + device, + &dev_checkpoint->permanent_managed, + &perm_managed_by_mac, + NULL)) { + dev_checkpoint->permanent_managed_by_mac = perm_managed_by_mac; + } else { + dev_checkpoint->permanent_managed = NM_TERNARY_DEFAULT; + dev_checkpoint->permanent_managed_by_mac = FALSE; + _LOGW("error getting permanent managed state for %s: %s", + nm_device_get_iface(device), + error->message); + g_clear_error(&error); + } + act_request = nm_device_get_act_request(device); if (act_request) { - NMSettingsStorage *storage; - gboolean shadowed_owned = FALSE; - const char *shadowed_file; - gs_free_error GError *error = NULL; + NMSettingsStorage *storage; + gboolean shadowed_owned = FALSE; + const char *shadowed_file; settings_connection = nm_act_request_get_settings_connection(act_request); applied_connection = nm_act_request_get_applied_connection(act_request); @@ -764,6 +824,7 @@ device_checkpoint_create(NMCheckpoint *self, NMDevice *device) _LOGW("error reading shadowed connection file for %s: %s", nm_device_get_iface(device), error->message); + g_clear_error(&error); } } } diff --git a/src/core/nm-config.c b/src/core/nm-config.c index e9a2681320..83816e1e10 100644 --- a/src/core/nm-config.c +++ b/src/core/nm-config.c @@ -2106,7 +2106,11 @@ normalize_hwaddr_for_group_name(const char *hwaddr, char *out, GError **error) * Returns: TRUE if there were no errors, FALSE otherwise. */ gboolean -nm_config_get_device_managed(NMConfig *self, NMDevice *device, NMTernary *out, GError **error) +nm_config_get_device_managed(NMConfig *self, + NMDevice *device, + NMTernary *out_managed, + gboolean *out_by_mac, + GError **error) { NMConfigPrivate *priv; const GKeyFile *keyfile = NULL; @@ -2119,14 +2123,15 @@ nm_config_get_device_managed(NMConfig *self, NMDevice *device, NMTernary *out, G g_return_val_if_fail(NM_IS_CONFIG(self), FALSE); g_return_val_if_fail(NM_CONFIG_GET_PRIVATE(self)->config_data, FALSE); - g_return_val_if_fail(out, FALSE); + g_return_val_if_fail(out_managed, FALSE); g_return_val_if_fail(ifname, FALSE); priv = NM_CONFIG_GET_PRIVATE(self); keyfile = _nm_config_data_get_keyfile_intern(priv->config_data); if (!keyfile) { - *out = NM_TERNARY_DEFAULT; + NM_SET_OUT(out_managed, NM_TERNARY_DEFAULT); + NM_SET_OUT(out_by_mac, FALSE); return TRUE; } @@ -2153,16 +2158,20 @@ nm_config_get_device_managed(NMConfig *self, NMDevice *device, NMTernary *out, G } if (val_by_name != NM_TERNARY_DEFAULT && val_by_mac == NM_TERNARY_DEFAULT) { - *out = val_by_name; + NM_SET_OUT(out_managed, val_by_name); + NM_SET_OUT(out_by_mac, FALSE); return TRUE; } else if (val_by_mac != NM_TERNARY_DEFAULT && val_by_name == NM_TERNARY_DEFAULT) { - *out = val_by_mac; + NM_SET_OUT(out_managed, val_by_mac); + NM_SET_OUT(out_by_mac, TRUE); return TRUE; } else if (val_by_name == NM_TERNARY_DEFAULT && val_by_mac == NM_TERNARY_DEFAULT) { - *out = NM_TERNARY_DEFAULT; + NM_SET_OUT(out_managed, NM_TERNARY_DEFAULT); + NM_SET_OUT(out_by_mac, FALSE); return TRUE; } else if (val_by_name == val_by_mac) { - *out = val_by_name; + NM_SET_OUT(out_managed, val_by_name); + NM_SET_OUT(out_by_mac, FALSE); return TRUE; } diff --git a/src/core/nm-config.h b/src/core/nm-config.h index a1b1463418..0cfbdfe40e 100644 --- a/src/core/nm-config.h +++ b/src/core/nm-config.h @@ -145,6 +145,7 @@ void nm_config_set_connectivity_check_enabled(NMConfig *self, gboolean enabled); gboolean nm_config_get_device_managed(NMConfig *self, NMDevice *device, NMTernary *out_managed, + gboolean *out_by_mac, GError **error); gboolean nm_config_set_device_managed(NMConfig *self, NMDevice *device, diff --git a/src/core/tests/config/test-config.c b/src/core/tests/config/test-config.c index 26588d1a1d..b2f29821e8 100644 --- a/src/core/tests/config/test-config.c +++ b/src/core/tests/config/test-config.c @@ -321,6 +321,7 @@ test_config_managed(void) gs_free char *group_by_name = NULL; const char *ifname, *group_by_mac; NMTernary managed; + gboolean by_mac; GKeyFile *kf = nm_config_create_keyfile(); dev = nm_test_device_new("11:11:11:11:11:11"); @@ -334,14 +335,15 @@ test_config_managed(void) config = setup_config(NULL, CONFIG_USER, CONFIG_INTERN, NULL, "/no/such/dir", "", NULL); - g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL, NULL)); g_assert_cmpint(managed, ==, NM_TERNARY_DEFAULT); /* Matching by name */ NMTST_EXPECT_NM_INFO("config: signal: *"); g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_TRUE, FALSE, NULL)); - g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, &by_mac, NULL)); g_assert_cmpint(managed, ==, NM_TERNARY_TRUE); + g_assert_false(by_mac); g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); g_assert_false(g_key_file_has_key(kf, group_by_mac, "managed", NULL)); g_assert_true(g_key_file_has_key(kf, group_by_name, "managed", NULL)); @@ -349,8 +351,9 @@ test_config_managed(void) NMTST_EXPECT_NM_INFO("config: signal: *"); g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_FALSE, FALSE, NULL)); - g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, &by_mac, NULL)); g_assert_cmpint(managed, ==, NM_TERNARY_FALSE); + g_assert_false(by_mac); g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); g_assert_false(g_key_file_has_key(kf, group_by_mac, "managed", NULL)); g_assert_true(g_key_file_has_key(kf, group_by_name, "managed", NULL)); @@ -359,8 +362,9 @@ test_config_managed(void) /* Matching by MAC address */ NMTST_EXPECT_NM_INFO("config: signal: *"); g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_TRUE, TRUE, NULL)); - g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, &by_mac, NULL)); g_assert_cmpint(managed, ==, NM_TERNARY_TRUE); + g_assert_true(by_mac); g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); g_assert_false(g_key_file_has_key(kf, group_by_name, "managed", NULL)); g_assert_true(g_key_file_has_key(kf, group_by_mac, "managed", NULL)); @@ -368,8 +372,9 @@ test_config_managed(void) NMTST_EXPECT_NM_INFO("config: signal: *"); g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_FALSE, TRUE, NULL)); - g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, &by_mac, NULL)); g_assert_cmpint(managed, ==, NM_TERNARY_FALSE); + g_assert_true(by_mac); g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); g_assert_false(g_key_file_has_key(kf, group_by_name, "managed", NULL)); g_assert_true(g_key_file_has_key(kf, group_by_mac, "managed", NULL)); @@ -378,7 +383,7 @@ test_config_managed(void) /* Resetting the managed state */ NMTST_EXPECT_NM_INFO("config: signal: *"); g_assert(nm_config_set_device_managed(config, dev, NM_TERNARY_DEFAULT, FALSE, NULL)); - g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL, NULL)); g_assert_cmpint(managed, ==, NM_TERNARY_DEFAULT); g_key_file_load_from_file(kf, CONFIG_INTERN, G_KEY_FILE_NONE, NULL); g_assert_false(g_key_file_has_key(kf, group_by_name, "managed", NULL)); @@ -391,7 +396,7 @@ test_config_managed(void) g_key_file_set_string(kf, group_by_mac, "managed", "0"); g_assert(g_key_file_save_to_file(kf, CONFIG_INTERN, NULL)); config = setup_config(NULL, CONFIG_USER, CONFIG_INTERN, NULL, "/no/such/dir", "", NULL); - g_assert(!nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert(!nm_config_get_device_managed(config, dev, &managed, NULL, NULL)); g_object_unref(config); @@ -400,7 +405,7 @@ test_config_managed(void) g_key_file_set_string(kf, group_by_mac, "managed", "1"); g_assert(g_key_file_save_to_file(kf, CONFIG_INTERN, NULL)); config = setup_config(NULL, CONFIG_USER, CONFIG_INTERN, NULL, "/no/such/dir", "", NULL); - g_assert(nm_config_get_device_managed(config, dev, &managed, NULL)); + g_assert(nm_config_get_device_managed(config, dev, &managed, NULL, NULL)); g_assert_cmpint(managed, ==, NM_TERNARY_TRUE); g_key_file_unref(kf); From b6bd9cee872ac80883ef938ecdee54c71ce43ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 3 Mar 2026 14:31:36 +0100 Subject: [PATCH 12/12] NEWS: update --- NEWS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS b/NEWS index 7e1f21462d..3855bda0f2 100644 --- a/NEWS +++ b/NEWS @@ -50,6 +50,9 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! * Add support for GENEVE interface. * The DHCPv4 internal client now ignores option 3 (Router) if the lease contains option 121 (Classless Static Route), as recommended by RFC 3442. +* Allow persisting the managed state across reboots from nmcli and the D-Bus API. +* Allow changing the device's administrative state in the kernel at the same + time as a change to the managed state from nmcli and the D-Bus API. ============================================= NetworkManager-1.56