diff options
Diffstat (limited to 'src/core/nm-checkpoint.c')
| -rw-r--r-- | src/core/nm-checkpoint.c | 790 |
1 files changed, 790 insertions, 0 deletions
diff --git a/src/core/nm-checkpoint.c b/src/core/nm-checkpoint.c new file mode 100644 index 00000000..2088c247 --- /dev/null +++ b/src/core/nm-checkpoint.c @@ -0,0 +1,790 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2016 Red Hat, Inc. + */ + +#include "src/core/nm-default-daemon.h" + +#include "nm-checkpoint.h" + +#include "nm-active-connection.h" +#include "nm-act-request.h" +#include "nm-libnm-core-intern/nm-auth-subject.h" +#include "nm-core-utils.h" +#include "nm-dbus-interface.h" +#include "devices/nm-device.h" +#include "nm-manager.h" +#include "settings/nm-settings.h" +#include "settings/nm-settings-connection.h" +#include "nm-simple-connection.h" +#include "nm-utils.h" + +/*****************************************************************************/ + +typedef struct { + char * original_dev_path; + char * original_dev_name; + NMDeviceType dev_type; + NMDevice * device; + NMConnection * applied_connection; + NMConnection * settings_connection; + guint64 ac_version_id; + NMDeviceState state; + bool is_software : 1; + bool realized : 1; + bool activation_lifetime_bound_to_profile_visibility : 1; + NMUnmanFlagOp unmanaged_explicit; + NMActivationReason activation_reason; + gulong dev_exported_change_id; +} DeviceCheckpoint; + +NM_GOBJECT_PROPERTIES_DEFINE(NMCheckpoint, PROP_DEVICES, PROP_CREATED, PROP_ROLLBACK_TIMEOUT, ); + +struct _NMCheckpointPrivate { + /* properties */ + GHashTable *devices; + GPtrArray * removed_devices; + gint64 created_at_ms; + guint32 rollback_timeout_s; + guint timeout_id; + /* private members */ + NMManager * manager; + NMCheckpointCreateFlags flags; + GHashTable * connection_uuids; + gulong dev_removed_id; + + NMCheckpointTimeoutCallback timeout_cb; + gpointer timeout_data; +}; + +struct _NMCheckpointClass { + NMDBusObjectClass parent; +}; + +G_DEFINE_TYPE(NMCheckpoint, nm_checkpoint, NM_TYPE_DBUS_OBJECT) + +#define NM_CHECKPOINT_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMCheckpoint, NM_IS_CHECKPOINT) + +/*****************************************************************************/ + +#define _NMLOG_PREFIX_NAME "checkpoint" +#define _NMLOG_DOMAIN LOGD_CORE + +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + if (nm_logging_enabled(level, _NMLOG_DOMAIN)) { \ + char __prefix[32]; \ + \ + if (self) \ + g_snprintf(__prefix, \ + sizeof(__prefix), \ + "%s[%p]", \ + ""_NMLOG_PREFIX_NAME \ + "", \ + (self)); \ + else \ + g_strlcpy(__prefix, _NMLOG_PREFIX_NAME, sizeof(__prefix)); \ + _nm_log((level), \ + (_NMLOG_DOMAIN), \ + 0, \ + NULL, \ + NULL, \ + "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \ + __prefix _NM_UTILS_MACRO_REST(__VA_ARGS__)); \ + } \ + } \ + G_STMT_END + +/*****************************************************************************/ + +void +nm_checkpoint_log_destroy(NMCheckpoint *self) +{ + _LOGI("destroy %s", nm_dbus_object_get_path(NM_DBUS_OBJECT(self))); +} + +void +nm_checkpoint_set_timeout_callback(NMCheckpoint * self, + NMCheckpointTimeoutCallback callback, + gpointer user_data) +{ + NMCheckpointPrivate *priv = NM_CHECKPOINT_GET_PRIVATE(self); + + /* in glib world, we would have a GSignal for this. But as there + * is only one subscriber, it's simpler to just set and unset(!) + * the callback this way. */ + priv->timeout_cb = callback; + priv->timeout_data = user_data; +} + +NMDevice * +nm_checkpoint_includes_devices(NMCheckpoint *self, NMDevice *const *devices, guint n_devices) +{ + NMCheckpointPrivate *priv = NM_CHECKPOINT_GET_PRIVATE(self); + guint i; + + for (i = 0; i < n_devices; i++) { + if (g_hash_table_contains(priv->devices, devices[i])) + return devices[i]; + } + return NULL; +} + +NMDevice * +nm_checkpoint_includes_devices_of(NMCheckpoint *self, NMCheckpoint *cp_for_devices) +{ + NMCheckpointPrivate *priv = NM_CHECKPOINT_GET_PRIVATE(self); + NMCheckpointPrivate *priv2 = NM_CHECKPOINT_GET_PRIVATE(cp_for_devices); + GHashTableIter iter; + NMDevice * device; + + g_hash_table_iter_init(&iter, priv2->devices); + while (g_hash_table_iter_next(&iter, (gpointer *) &device, NULL)) { + if (g_hash_table_contains(priv->devices, device)) + return device; + } + return NULL; +} + +static NMSettingsConnection * +find_settings_connection(NMCheckpoint * self, + DeviceCheckpoint *dev_checkpoint, + gboolean * need_update, + gboolean * need_activation) +{ + NMCheckpointPrivate * priv = NM_CHECKPOINT_GET_PRIVATE(self); + NMActiveConnection * active; + NMSettingsConnection *sett_conn; + const char * uuid, *ac_uuid; + const CList * tmp_clist; + + *need_activation = FALSE; + *need_update = FALSE; + + uuid = nm_connection_get_uuid(dev_checkpoint->settings_connection); + sett_conn = nm_settings_get_connection_by_uuid(NM_SETTINGS_GET, uuid); + + if (!sett_conn) + return NULL; + + /* Now check if the connection changed, ... */ + if (!nm_connection_compare(dev_checkpoint->settings_connection, + nm_settings_connection_get_connection(sett_conn), + NM_SETTING_COMPARE_FLAG_EXACT)) { + _LOGT("rollback: settings connection %s changed", uuid); + *need_update = TRUE; + *need_activation = TRUE; + } + + /* ... is active, ... */ + nm_manager_for_each_active_connection (priv->manager, active, tmp_clist) { + ac_uuid = + nm_settings_connection_get_uuid(nm_active_connection_get_settings_connection(active)); + if (nm_streq(uuid, ac_uuid)) { + _LOGT("rollback: connection %s is active", uuid); + break; + } + } + + if (!active) { + _LOGT("rollback: connection %s is not active", uuid); + *need_activation = TRUE; + return sett_conn; + } + + /* ... or if the connection was reactivated/reapplied */ + if (nm_active_connection_version_id_get(active) != dev_checkpoint->ac_version_id) { + _LOGT("rollback: active connection version id of %s changed", uuid); + *need_activation = TRUE; + } + + return sett_conn; +} + +static gboolean +restore_and_activate_connection(NMCheckpoint *self, DeviceCheckpoint *dev_checkpoint) +{ + NMCheckpointPrivate * priv = NM_CHECKPOINT_GET_PRIVATE(self); + NMSettingsConnection *connection; + gs_unref_object NMAuthSubject * subject = NULL; + GError * local_error = NULL; + gboolean need_update, need_activation; + NMSettingsConnectionPersistMode persist_mode; + NMSettingsConnectionIntFlags sett_flags; + NMSettingsConnectionIntFlags sett_mask; + + connection = find_settings_connection(self, dev_checkpoint, &need_update, &need_activation); + + /* FIXME: we need to ensure to re-create/update the profile for the + * same settings plugin. E.g. if it was a keyfile in /run or /etc, + * it must be again. If it was previously handled by a certain settings plugin, + * so it must again. + * + * FIXME: preserve and restore the right settings flags (volatile, nm-generated). */ + sett_flags = NM_SETTINGS_CONNECTION_INT_FLAGS_NONE; + sett_mask = NM_SETTINGS_CONNECTION_INT_FLAGS_NONE; + + if (connection) { + if (need_update) { + _LOGD("rollback: updating connection %s", nm_settings_connection_get_uuid(connection)); + persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_KEEP; + nm_settings_connection_update(connection, + dev_checkpoint->settings_connection, + persist_mode, + sett_flags, + sett_mask, + NM_SETTINGS_CONNECTION_UPDATE_REASON_NONE, + "checkpoint-rollback", + NULL); + } + } else { + /* The connection was deleted, recreate it */ + _LOGD("rollback: adding connection %s again", + nm_connection_get_uuid(dev_checkpoint->settings_connection)); + + persist_mode = NM_SETTINGS_CONNECTION_PERSIST_MODE_TO_DISK; + if (!nm_settings_add_connection(NM_SETTINGS_GET, + dev_checkpoint->settings_connection, + persist_mode, + NM_SETTINGS_CONNECTION_ADD_REASON_NONE, + sett_flags, + &connection, + &local_error)) { + _LOGD("rollback: connection add failure: %s", local_error->message); + g_clear_error(&local_error); + return FALSE; + } + + /* If the device is software, a brand new NMDevice may have been created */ + if (dev_checkpoint->is_software && !dev_checkpoint->device) { + dev_checkpoint->device = nm_manager_get_device(priv->manager, + dev_checkpoint->original_dev_name, + dev_checkpoint->dev_type); + nm_g_object_ref(dev_checkpoint->device); + } + need_activation = TRUE; + } + + if (!dev_checkpoint->device) { + _LOGD("rollback: device cannot be restored"); + return FALSE; + } + + if (need_activation) { + _LOGD("rollback: reactivating connection %s", nm_settings_connection_get_uuid(connection)); + subject = nm_auth_subject_new_internal(); + + /* Disconnect the device if needed. This necessary because now + * the manager prevents the reactivation of the same connection by + * an internal subject. */ + if (nm_device_get_state(dev_checkpoint->device) > NM_DEVICE_STATE_DISCONNECTED + && nm_device_get_state(dev_checkpoint->device) < NM_DEVICE_STATE_DEACTIVATING) { + nm_device_state_changed(dev_checkpoint->device, + NM_DEVICE_STATE_DEACTIVATING, + NM_DEVICE_STATE_REASON_NEW_ACTIVATION); + } + + if (!nm_manager_activate_connection( + priv->manager, + connection, + dev_checkpoint->applied_connection, + NULL, + dev_checkpoint->device, + subject, + NM_ACTIVATION_TYPE_MANAGED, + dev_checkpoint->activation_reason, + dev_checkpoint->activation_lifetime_bound_to_profile_visibility + ? NM_ACTIVATION_STATE_FLAG_LIFETIME_BOUND_TO_PROFILE_VISIBILITY + : NM_ACTIVATION_STATE_FLAG_NONE, + &local_error)) { + _LOGW("rollback: reactivation of connection %s/%s failed: %s", + nm_settings_connection_get_id(connection), + nm_settings_connection_get_uuid(connection), + local_error->message); + g_clear_error(&local_error); + return FALSE; + } + } + return TRUE; +} + +GVariant * +nm_checkpoint_rollback(NMCheckpoint *self) +{ + NMCheckpointPrivate *priv = NM_CHECKPOINT_GET_PRIVATE(self); + DeviceCheckpoint * dev_checkpoint; + GHashTableIter iter; + NMDevice * device; + GVariantBuilder builder; + uint i; + + _LOGI("rollback of %s", nm_dbus_object_get_path(NM_DBUS_OBJECT(self))); + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{su}")); + + /* Start creating removed devices (if any and if possible) */ + if (priv->removed_devices) { + for (i = 0; i < priv->removed_devices->len; i++) { + guint32 result = NM_ROLLBACK_RESULT_OK; + + dev_checkpoint = priv->removed_devices->pdata[i]; + _LOGD("rollback: restoring removed device %s (state %d, realized %d, explicitly " + "unmanaged %d)", + dev_checkpoint->original_dev_name, + (int) dev_checkpoint->state, + dev_checkpoint->realized, + dev_checkpoint->unmanaged_explicit); + + if (dev_checkpoint->applied_connection) { + if (!restore_and_activate_connection(self, dev_checkpoint)) + result = NM_ROLLBACK_RESULT_ERR_FAILED; + } + g_variant_builder_add(&builder, "{su}", dev_checkpoint->original_dev_path, result); + } + } + + /* 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; + + _LOGD("rollback: restoring device %s (state %d, realized %d, explicitly unmanaged %d)", + dev_checkpoint->original_dev_name, + (int) dev_checkpoint->state, + dev_checkpoint->realized, + dev_checkpoint->unmanaged_explicit); + + if (nm_device_is_real(device)) { + if (!dev_checkpoint->realized) { + _LOGD("rollback: device was not realized, unmanage it"); + nm_device_set_unmanaged_by_flags_queue(device, + NM_UNMANAGED_USER_EXPLICIT, + TRUE, + NM_DEVICE_STATE_REASON_NOW_UNMANAGED); + goto next_dev; + } + } else { + if (dev_checkpoint->realized) { + if (dev_checkpoint->is_software) { + /* try to recreate software device */ + _LOGD("rollback: software device not realized, will re-activate"); + goto activate; + } else { + _LOGD("rollback: device is not realized"); + result = NM_ROLLBACK_RESULT_ERR_FAILED; + } + } + goto next_dev; + } + + /* Manage the device again if needed */ + if (nm_device_get_unmanaged_flags(device, NM_UNMANAGED_USER_EXPLICIT) + && dev_checkpoint->unmanaged_explicit != NM_UNMAN_FLAG_OP_SET_UNMANAGED) { + _LOGD("rollback: restore unmanaged user-explicit"); + nm_device_set_unmanaged_by_flags_queue(device, + NM_UNMANAGED_USER_EXPLICIT, + dev_checkpoint->unmanaged_explicit, + 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) { + _LOGD("rollback: explicitly unmanage device"); + nm_device_set_unmanaged_by_flags_queue(device, + NM_UNMANAGED_USER_EXPLICIT, + TRUE, + NM_DEVICE_STATE_REASON_NOW_UNMANAGED); + } + goto next_dev; + } + +activate: + if (dev_checkpoint->applied_connection) { + if (!restore_and_activate_connection(self, dev_checkpoint)) { + result = NM_ROLLBACK_RESULT_ERR_FAILED; + goto next_dev; + } + } else { + /* The device was initially disconnected, deactivate any existing connection */ + _LOGD("rollback: disconnecting device"); + + if (nm_device_get_state(device) > NM_DEVICE_STATE_DISCONNECTED + && nm_device_get_state(device) < NM_DEVICE_STATE_DEACTIVATING) { + nm_device_state_changed(device, + NM_DEVICE_STATE_DEACTIVATING, + NM_DEVICE_STATE_REASON_USER_REQUESTED); + } + } + +next_dev: + g_variant_builder_add(&builder, "{su}", dev_checkpoint->original_dev_path, result); + } + + if (NM_FLAGS_HAS(priv->flags, NM_CHECKPOINT_CREATE_FLAG_DELETE_NEW_CONNECTIONS)) { + NMSettingsConnection *con; + gs_free NMSettingsConnection **list = NULL; + + g_return_val_if_fail(priv->connection_uuids, NULL); + list = nm_settings_get_connections_clone( + NM_SETTINGS_GET, + NULL, + NULL, + NULL, + nm_settings_connection_cmp_autoconnect_priority_p_with_data, + NULL); + + for (i = 0; list[i]; i++) { + con = list[i]; + if (!g_hash_table_contains(priv->connection_uuids, + nm_settings_connection_get_uuid(con))) { + _LOGD("rollback: deleting new connection %s", nm_settings_connection_get_uuid(con)); + nm_settings_connection_delete(con, FALSE); + } + } + } + + if (NM_FLAGS_HAS(priv->flags, NM_CHECKPOINT_CREATE_FLAG_DISCONNECT_NEW_DEVICES)) { + const CList * tmp_lst; + NMDeviceState state; + + nm_manager_for_each_device (priv->manager, device, tmp_lst) { + if (g_hash_table_contains(priv->devices, device)) + continue; + state = nm_device_get_state(device); + if (state > NM_DEVICE_STATE_DISCONNECTED && state < NM_DEVICE_STATE_DEACTIVATING) { + _LOGD("rollback: disconnecting new device %s", nm_device_get_iface(device)); + nm_device_state_changed(device, + NM_DEVICE_STATE_DEACTIVATING, + NM_DEVICE_STATE_REASON_USER_REQUESTED); + } + } + } + + return g_variant_new("(a{su})", &builder); +} + +static void +device_checkpoint_destroy(gpointer data) +{ + DeviceCheckpoint *dev_checkpoint = data; + + nm_clear_g_signal_handler(dev_checkpoint->device, &dev_checkpoint->dev_exported_change_id); + g_clear_object(&dev_checkpoint->applied_connection); + g_clear_object(&dev_checkpoint->settings_connection); + g_clear_object(&dev_checkpoint->device); + g_free(dev_checkpoint->original_dev_path); + g_free(dev_checkpoint->original_dev_name); + + g_slice_free(DeviceCheckpoint, dev_checkpoint); +} + +static void +_move_dev_to_removed_devices(NMDevice *device, NMCheckpoint *checkpoint) +{ + NMCheckpointPrivate *priv = NM_CHECKPOINT_GET_PRIVATE(checkpoint); + DeviceCheckpoint * dev_checkpoint; + + g_return_if_fail(device); + + dev_checkpoint = g_hash_table_lookup(priv->devices, device); + if (!dev_checkpoint) + return; + + g_hash_table_steal(priv->devices, dev_checkpoint->device); + nm_clear_g_signal_handler(dev_checkpoint->device, &dev_checkpoint->dev_exported_change_id); + g_clear_object(&dev_checkpoint->device); + + if (!priv->removed_devices) + priv->removed_devices = + g_ptr_array_new_with_free_func((GDestroyNotify) device_checkpoint_destroy); + g_ptr_array_add(priv->removed_devices, dev_checkpoint); + + _notify(checkpoint, PROP_DEVICES); +} + +static void +_dev_exported_changed(NMDBusObject *obj, NMCheckpoint *checkpoint) +{ + _move_dev_to_removed_devices(NM_DEVICE(obj), checkpoint); +} + +static DeviceCheckpoint * +device_checkpoint_create(NMCheckpoint *checkpoint, NMDevice *device) +{ + DeviceCheckpoint * dev_checkpoint; + NMConnection * applied_connection; + NMSettingsConnection *settings_connection; + const char * path; + NMActRequest * act_request; + + nm_assert(NM_IS_DEVICE(device)); + nm_assert(nm_device_is_real(device)); + + path = nm_dbus_object_get_path(NM_DBUS_OBJECT(device)); + + dev_checkpoint = g_slice_new0(DeviceCheckpoint); + dev_checkpoint->device = g_object_ref(device); + dev_checkpoint->original_dev_path = g_strdup(path); + dev_checkpoint->original_dev_name = g_strdup(nm_device_get_iface(device)); + dev_checkpoint->dev_type = nm_device_get_device_type(device); + dev_checkpoint->state = nm_device_get_state(device); + dev_checkpoint->is_software = nm_device_is_software(device); + dev_checkpoint->realized = nm_device_is_real(device); + dev_checkpoint->dev_exported_change_id = g_signal_connect(device, + NM_DBUS_OBJECT_EXPORTED_CHANGED, + G_CALLBACK(_dev_exported_changed), + checkpoint); + + if (nm_device_get_unmanaged_mask(device, NM_UNMANAGED_USER_EXPLICIT)) { + dev_checkpoint->unmanaged_explicit = + !!nm_device_get_unmanaged_flags(device, NM_UNMANAGED_USER_EXPLICIT); + } else + dev_checkpoint->unmanaged_explicit = NM_UNMAN_FLAG_OP_FORGET; + + act_request = nm_device_get_act_request(device); + if (act_request) { + settings_connection = nm_act_request_get_settings_connection(act_request); + applied_connection = nm_act_request_get_applied_connection(act_request); + + dev_checkpoint->applied_connection = nm_simple_connection_new_clone(applied_connection); + dev_checkpoint->settings_connection = nm_simple_connection_new_clone( + nm_settings_connection_get_connection(settings_connection)); + dev_checkpoint->ac_version_id = + nm_active_connection_version_id_get(NM_ACTIVE_CONNECTION(act_request)); + dev_checkpoint->activation_reason = + nm_active_connection_get_activation_reason(NM_ACTIVE_CONNECTION(act_request)); + dev_checkpoint->activation_lifetime_bound_to_profile_visibility = + NM_FLAGS_HAS(nm_active_connection_get_state_flags(NM_ACTIVE_CONNECTION(act_request)), + NM_ACTIVATION_STATE_FLAG_LIFETIME_BOUND_TO_PROFILE_VISIBILITY); + } + + return dev_checkpoint; +} + +static gboolean +_timeout_cb(gpointer user_data) +{ + NMCheckpoint * self = user_data; + NMCheckpointPrivate *priv = NM_CHECKPOINT_GET_PRIVATE(self); + + priv->timeout_id = 0; + + if (priv->timeout_cb) + priv->timeout_cb(self, priv->timeout_data); + + /* beware, @self likely got destroyed! */ + return G_SOURCE_REMOVE; +} + +void +nm_checkpoint_adjust_rollback_timeout(NMCheckpoint *self, guint32 add_timeout) +{ + guint32 rollback_timeout_s; + gint64 now_ms, add_timeout_ms, rollback_timeout_ms; + + NMCheckpointPrivate *priv = NM_CHECKPOINT_GET_PRIVATE(self); + + nm_clear_g_source(&priv->timeout_id); + + if (add_timeout == 0) + rollback_timeout_s = 0; + else { + now_ms = nm_utils_get_monotonic_timestamp_msec(); + add_timeout_ms = ((gint64) add_timeout) * 1000; + rollback_timeout_ms = (now_ms - priv->created_at_ms) + add_timeout_ms; + + /* round to nearest integer second. Since NM_CHECKPOINT_ROLLBACK_TIMEOUT is + * in units seconds, it will be able to exactly express the timeout. */ + rollback_timeout_s = NM_MIN((rollback_timeout_ms + 500) / 1000, (gint64) G_MAXUINT32); + + /* we expect the timeout to be positive, because add_timeout_ms is positive. + * We cannot accept a zero, because it means "infinity". */ + nm_assert(rollback_timeout_s > 0); + + priv->timeout_id = + g_timeout_add(NM_MIN(add_timeout_ms, (gint64) G_MAXUINT32), _timeout_cb, self); + } + + if (rollback_timeout_s != priv->rollback_timeout_s) { + priv->rollback_timeout_s = rollback_timeout_s; + _notify(self, PROP_ROLLBACK_TIMEOUT); + } +} + +/*****************************************************************************/ + +static void +get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + NMCheckpoint * self = NM_CHECKPOINT(object); + NMCheckpointPrivate *priv = NM_CHECKPOINT_GET_PRIVATE(self); + + switch (prop_id) { + case PROP_DEVICES: + nm_dbus_utils_g_value_set_object_path_from_hash(value, priv->devices, FALSE); + break; + case PROP_CREATED: + g_value_set_int64( + value, + nm_utils_monotonic_timestamp_as_boottime(priv->created_at_ms, NM_UTILS_NSEC_PER_MSEC)); + break; + case PROP_ROLLBACK_TIMEOUT: + g_value_set_uint(value, priv->rollback_timeout_s); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +/*****************************************************************************/ + +static void +nm_checkpoint_init(NMCheckpoint *self) +{ + NMCheckpointPrivate *priv; + + priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_CHECKPOINT, NMCheckpointPrivate); + + self->_priv = priv; + + c_list_init(&self->checkpoints_lst); + + priv->devices = g_hash_table_new_full(nm_direct_hash, NULL, NULL, device_checkpoint_destroy); +} + +static void +_device_removed(NMManager *manager, NMDevice *device, gpointer user_data) +{ + _move_dev_to_removed_devices(device, NM_CHECKPOINT(user_data)); +} + +NMCheckpoint * +nm_checkpoint_new(NMManager * manager, + GPtrArray * devices, + guint32 rollback_timeout_s, + NMCheckpointCreateFlags flags) +{ + NMCheckpoint * self; + NMCheckpointPrivate * priv; + NMSettingsConnection *const *con; + gint64 rollback_timeout_ms; + guint i; + + g_return_val_if_fail(manager, NULL); + g_return_val_if_fail(devices, NULL); + g_return_val_if_fail(devices->len > 0, NULL); + + self = g_object_new(NM_TYPE_CHECKPOINT, NULL); + + priv = NM_CHECKPOINT_GET_PRIVATE(self); + priv->manager = g_object_ref(manager); + priv->rollback_timeout_s = rollback_timeout_s; + priv->created_at_ms = nm_utils_get_monotonic_timestamp_msec(); + priv->flags = flags; + + if (rollback_timeout_s != 0) { + rollback_timeout_ms = ((gint64) rollback_timeout_s) * 1000; + priv->timeout_id = + g_timeout_add(NM_MIN(rollback_timeout_ms, (gint64) G_MAXUINT32), _timeout_cb, self); + } + + if (NM_FLAGS_HAS(flags, NM_CHECKPOINT_CREATE_FLAG_DELETE_NEW_CONNECTIONS)) { + priv->connection_uuids = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL); + for (con = nm_settings_get_connections(NM_SETTINGS_GET, NULL); *con; con++) { + g_hash_table_add(priv->connection_uuids, + g_strdup(nm_settings_connection_get_uuid(*con))); + } + } + + for (i = 0; i < devices->len; i++) { + NMDevice *device = devices->pdata[i]; + + /* As long as the check point instance exists, it will keep a reference + * to the device also if the device gets removed (by rmmod or by deleting + * a connection profile for a software device). */ + g_hash_table_insert(priv->devices, device, device_checkpoint_create(self, device)); + } + + priv->dev_removed_id = g_signal_connect(priv->manager, + NM_MANAGER_DEVICE_REMOVED, + G_CALLBACK(_device_removed), + self); + return self; +} + +static void +dispose(GObject *object) +{ + NMCheckpoint * self = NM_CHECKPOINT(object); + NMCheckpointPrivate *priv = NM_CHECKPOINT_GET_PRIVATE(self); + + nm_assert(c_list_is_empty(&self->checkpoints_lst)); + + nm_clear_pointer(&priv->devices, g_hash_table_unref); + nm_clear_pointer(&priv->connection_uuids, g_hash_table_unref); + nm_clear_pointer(&priv->removed_devices, g_ptr_array_unref); + + nm_clear_g_signal_handler(priv->manager, &priv->dev_removed_id); + g_clear_object(&priv->manager); + + nm_clear_g_source(&priv->timeout_id); + + G_OBJECT_CLASS(nm_checkpoint_parent_class)->dispose(object); +} + +static const NMDBusInterfaceInfoExtended interface_info_checkpoint = { + .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT( + NM_DBUS_INTERFACE_CHECKPOINT, + .signals = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy, ), + .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS( + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Devices", + "ao", + NM_CHECKPOINT_DEVICES), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Created", "x", NM_CHECKPOINT_CREATED), + NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("RollbackTimeout", + "u", + NM_CHECKPOINT_ROLLBACK_TIMEOUT), ), ), + .legacy_property_changed = TRUE, +}; + +static void +nm_checkpoint_class_init(NMCheckpointClass *checkpoint_class) +{ + GObjectClass * object_class = G_OBJECT_CLASS(checkpoint_class); + NMDBusObjectClass *dbus_object_class = NM_DBUS_OBJECT_CLASS(checkpoint_class); + + g_type_class_add_private(object_class, sizeof(NMCheckpointPrivate)); + + dbus_object_class->export_path = NM_DBUS_EXPORT_PATH_NUMBERED(NM_DBUS_PATH "/Checkpoint"); + dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_checkpoint); + + object_class->dispose = dispose; + object_class->get_property = get_property; + + obj_properties[PROP_DEVICES] = g_param_spec_boxed(NM_CHECKPOINT_DEVICES, + "", + "", + G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_CREATED] = g_param_spec_int64(NM_CHECKPOINT_CREATED, + "", + "", + G_MININT64, + G_MAXINT64, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + obj_properties[PROP_ROLLBACK_TIMEOUT] = + g_param_spec_uint(NM_CHECKPOINT_ROLLBACK_TIMEOUT, + "", + "", + 0, + G_MAXUINT32, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties); +} |