// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2016 Red Hat, Inc.
 */

#include "nm-default.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_visiblity: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_visiblity
		                                     ? 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_visiblity = 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);
}
