/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Copyright (C) 2016 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nm-checkpoint-manager.h"
#include "nm-checkpoint.h"
#include "nm-connection.h"
#include "nm-core-utils.h"
#include "devices/nm-device.h"
#include "nm-manager.h"
#include "nm-utils.h"
#include "c-list/src/c-list.h"
/*****************************************************************************/
struct _NMCheckpointManager {
NMManager * _manager;
GParamSpec *property_spec;
CList checkpoints_lst_head;
};
#define GET_MANAGER(self) \
({ \
typeof(self) _self = (self); \
\
_nm_unused NMCheckpointManager *_self2 = _self; \
\
nm_assert(_self); \
nm_assert(NM_IS_MANAGER(_self->_manager)); \
_self->_manager; \
})
/*****************************************************************************/
#define _NMLOG_DOMAIN LOGD_CORE
#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "checkpoint", __VA_ARGS__)
/*****************************************************************************/
static void
notify_checkpoints(NMCheckpointManager *self)
{
g_object_notify_by_pspec((GObject *) GET_MANAGER(self), self->property_spec);
}
static void
destroy_checkpoint(NMCheckpointManager *self, NMCheckpoint *checkpoint, gboolean log_destroy)
{
nm_assert(NM_IS_CHECKPOINT(checkpoint));
nm_assert(nm_dbus_object_is_exported(NM_DBUS_OBJECT(checkpoint)));
nm_assert(c_list_contains(&self->checkpoints_lst_head, &checkpoint->checkpoints_lst));
nm_checkpoint_set_timeout_callback(checkpoint, NULL, NULL);
c_list_unlink(&checkpoint->checkpoints_lst);
if (log_destroy)
nm_checkpoint_log_destroy(checkpoint);
notify_checkpoints(self);
nm_dbus_object_unexport(NM_DBUS_OBJECT(checkpoint));
g_object_unref(checkpoint);
}
static GVariant *
rollback_checkpoint(NMCheckpointManager *self, NMCheckpoint *checkpoint)
{
GVariant * result;
const CList *iter;
nm_assert(c_list_contains(&self->checkpoints_lst_head, &checkpoint->checkpoints_lst));
/* we destroy first all overlapping checkpoints that are younger/newer. */
for (iter = checkpoint->checkpoints_lst.next; iter != &self->checkpoints_lst_head;) {
NMCheckpoint *cp = c_list_entry(iter, NMCheckpoint, checkpoints_lst);
iter = iter->next;
if (nm_checkpoint_includes_devices_of(cp, checkpoint)) {
/* the younger checkpoint has overlapping devices and gets obsoleted.
* Destroy it. */
destroy_checkpoint(self, cp, TRUE);
}
}
result = nm_checkpoint_rollback(checkpoint);
destroy_checkpoint(self, checkpoint, FALSE);
return result;
}
static void
rollback_timeout_cb(NMCheckpoint *checkpoint, gpointer user_data)
{
NMCheckpointManager *self = user_data;
gs_unref_variant GVariant *result = NULL;
result = rollback_checkpoint(self, checkpoint);
}
NMCheckpoint *
nm_checkpoint_manager_create(NMCheckpointManager * self,
const char *const * device_paths,
guint32 rollback_timeout,
NMCheckpointCreateFlags flags,
GError ** error)
{
NMManager * manager;
NMCheckpoint * checkpoint;
gs_unref_ptrarray GPtrArray *devices = NULL;
NMDevice * device;
g_return_val_if_fail(self, FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
manager = GET_MANAGER(self);
devices = g_ptr_array_new();
if (!device_paths || !device_paths[0]) {
const CList *tmp_lst;
nm_manager_for_each_device (manager, device, tmp_lst) {
/* FIXME: there is no strong reason to skip over unrealized devices.
* Also, NMCheckpoint anticipates to handle them (in parts). */
if (!nm_device_is_real(device))
continue;
nm_assert(nm_dbus_object_get_path(NM_DBUS_OBJECT(device)));
g_ptr_array_add(devices, device);
}
} else if (NM_FLAGS_HAS(flags, NM_CHECKPOINT_CREATE_FLAG_DISCONNECT_NEW_DEVICES)) {
g_set_error_literal(
error,
NM_MANAGER_ERROR,
NM_MANAGER_ERROR_INVALID_ARGUMENTS,
"the DISCONNECT_NEW_DEVICES flag can only be used with an empty device list");
return NULL;
} else {
for (; *device_paths; device_paths++) {
device = nm_manager_get_device_by_path(manager, *device_paths);
if (!device) {
g_set_error(error,
NM_MANAGER_ERROR,
NM_MANAGER_ERROR_UNKNOWN_DEVICE,
"device %s does not exist",
*device_paths);
return NULL;
}
if (!nm_device_is_real(device)) {
g_set_error(error,
NM_MANAGER_ERROR,
NM_MANAGER_ERROR_UNKNOWN_DEVICE,
"device %s is not realized",
*device_paths);
return NULL;
}
g_ptr_array_add(devices, device);
}
}
if (!devices->len) {
g_set_error_literal(error,
NM_MANAGER_ERROR,
NM_MANAGER_ERROR_INVALID_ARGUMENTS,
"no device available");
return NULL;
}
if (NM_FLAGS_HAS(flags, NM_CHECKPOINT_CREATE_FLAG_DESTROY_ALL))
nm_checkpoint_manager_destroy_all(self);
else if (!NM_FLAGS_HAS(flags, NM_CHECKPOINT_CREATE_FLAG_ALLOW_OVERLAPPING)) {
c_list_for_each_entry (checkpoint, &self->checkpoints_lst_head, checkpoints_lst) {
device = nm_checkpoint_includes_devices(checkpoint,
(NMDevice *const *) devices->pdata,
devices->len);
if (device) {
g_set_error(error,
NM_MANAGER_ERROR,
NM_MANAGER_ERROR_INVALID_ARGUMENTS,
"device '%s' is already included in checkpoint %s",
nm_device_get_iface(device),
nm_dbus_object_get_path(NM_DBUS_OBJECT(checkpoint)));
return NULL;
}
}
}
checkpoint = nm_checkpoint_new(manager, devices, rollback_timeout, flags);
nm_dbus_object_export(NM_DBUS_OBJECT(checkpoint));
nm_checkpoint_set_timeout_callback(checkpoint, rollback_timeout_cb, self);
c_list_link_tail(&self->checkpoints_lst_head, &checkpoint->checkpoints_lst);
notify_checkpoints(self);
return checkpoint;
}
void
nm_checkpoint_manager_destroy_all(NMCheckpointManager *self)
{
NMCheckpoint *checkpoint;
g_return_if_fail(self);
while ((checkpoint =
c_list_first_entry(&self->checkpoints_lst_head, NMCheckpoint, checkpoints_lst)))
destroy_checkpoint(self, checkpoint, TRUE);
}
gboolean
nm_checkpoint_manager_destroy(NMCheckpointManager *self, const char *path, GError **error)
{
NMCheckpoint *checkpoint;
g_return_val_if_fail(self, FALSE);
g_return_val_if_fail(path && path[0] == '/', FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
if (!nm_dbus_path_not_empty(path)) {
nm_checkpoint_manager_destroy_all(self);
return TRUE;
}
checkpoint = nm_checkpoint_manager_lookup_by_path(self, path, error);
if (!checkpoint)
return FALSE;
destroy_checkpoint(self, checkpoint, TRUE);
return TRUE;
}
gboolean
nm_checkpoint_manager_rollback(NMCheckpointManager *self,
const char * path,
GVariant ** results,
GError ** error)
{
NMCheckpoint *checkpoint;
g_return_val_if_fail(self, FALSE);
g_return_val_if_fail(path && path[0] == '/', FALSE);
g_return_val_if_fail(results, FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
checkpoint = nm_checkpoint_manager_lookup_by_path(self, path, error);
if (!checkpoint)
return FALSE;
*results = rollback_checkpoint(self, checkpoint);
return TRUE;
}
NMCheckpoint *
nm_checkpoint_manager_lookup_by_path(NMCheckpointManager *self, const char *path, GError **error)
{
NMCheckpoint *checkpoint;
g_return_val_if_fail(self, NULL);
checkpoint =
nm_dbus_manager_lookup_object(nm_dbus_object_get_manager(NM_DBUS_OBJECT(GET_MANAGER(self))),
path);
if (!checkpoint || !NM_IS_CHECKPOINT(checkpoint)) {
g_set_error(error,
NM_MANAGER_ERROR,
NM_MANAGER_ERROR_INVALID_ARGUMENTS,
"checkpoint %s does not exist",
path);
return NULL;
}
nm_assert(c_list_contains(&self->checkpoints_lst_head, &checkpoint->checkpoints_lst));
return checkpoint;
}
const char **
nm_checkpoint_manager_get_checkpoint_paths(NMCheckpointManager *self, guint *out_length)
{
NMCheckpoint *checkpoint;
const char ** strv;
guint num, i = 0;
num = c_list_length(&self->checkpoints_lst_head);
NM_SET_OUT(out_length, num);
if (!num)
return NULL;
strv = g_new(const char *, num + 1);
c_list_for_each_entry (checkpoint, &self->checkpoints_lst_head, checkpoints_lst)
strv[i++] = nm_dbus_object_get_path(NM_DBUS_OBJECT(checkpoint));
nm_assert(i == num);
strv[i] = NULL;
return strv;
}
gboolean
nm_checkpoint_manager_adjust_rollback_timeout(NMCheckpointManager *self,
const char * path,
guint32 add_timeout,
GError ** error)
{
NMCheckpoint *checkpoint;
g_return_val_if_fail(self, FALSE);
g_return_val_if_fail(path && path[0] == '/', FALSE);
g_return_val_if_fail(!error || !*error, FALSE);
checkpoint = nm_checkpoint_manager_lookup_by_path(self, path, error);
if (!checkpoint)
return FALSE;
nm_checkpoint_adjust_rollback_timeout(checkpoint, add_timeout);
return TRUE;
}
/*****************************************************************************/
NMCheckpointManager *
nm_checkpoint_manager_new(NMManager *manager, GParamSpec *spec)
{
NMCheckpointManager *self;
g_return_val_if_fail(NM_IS_MANAGER(manager), FALSE);
self = g_slice_new0(NMCheckpointManager);
/* the NMCheckpointManager instance is actually owned by NMManager.
* Thus, we cannot take a reference to it, and we also don't bother
* taking a weak-reference. Instead let GET_MANAGER() assert that
* self->_manager is alive -- which we always expect as the lifetime
* of NMManager shall surpass the lifetime of the NMCheckpointManager
* instance. */
self->_manager = manager;
self->property_spec = spec;
c_list_init(&self->checkpoints_lst_head);
return self;
}
void
nm_checkpoint_manager_free(NMCheckpointManager *self)
{
if (!self)
return;
nm_checkpoint_manager_destroy_all(self);
g_slice_free(NMCheckpointManager, self);
}