/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Dan Williams <dcbw@redhat.com>
 * Søren Sandmann <sandmann@daimi.au.dk>
 * Copyright (C) 2007 - 2011 Red Hat, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nms-ifcfg-rh-plugin.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include "libnm-std-aux/c-list-util.h"
#include "libnm-glib-aux/nm-c-list.h"
#include "libnm-glib-aux/nm-io-utils.h"
#include "libnm-std-aux/nm-dbus-compat.h"
#include "nm-utils.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "nm-config.h"
#include "nm-dbus-manager.h"
#include "settings/nm-settings-plugin.h"
#include "settings/nm-settings-utils.h"
#include "NetworkManagerUtils.h"

#include "nms-ifcfg-rh-storage.h"
#include "nms-ifcfg-rh-common.h"
#include "nms-ifcfg-rh-utils.h"
#include "nms-ifcfg-rh-reader.h"
#include "nms-ifcfg-rh-writer.h"

#define IFCFGRH1_BUS_NAME                        "com.redhat.ifcfgrh1"
#define IFCFGRH1_OBJECT_PATH                     "/com/redhat/ifcfgrh1"
#define IFCFGRH1_IFACE1_NAME                     "com.redhat.ifcfgrh1"
#define IFCFGRH1_IFACE1_METHOD_GET_IFCFG_DETAILS "GetIfcfgDetails"

/*****************************************************************************/

typedef struct {
    NMConfig *config;

    struct {
        GDBusConnection *connection;
        GCancellable    *cancellable;
        gulong           signal_id;
        guint            regist_id;
    } dbus;

    NMSettUtilStorages storages;

    GHashTable *unmanaged_specs;
    GHashTable *unrecognized_specs;
} NMSIfcfgRHPluginPrivate;

struct _NMSIfcfgRHPlugin {
    NMSettingsPlugin        parent;
    NMSIfcfgRHPluginPrivate _priv;
};

struct _NMSIfcfgRHPluginClass {
    NMSettingsPluginClass parent;
};

G_DEFINE_TYPE(NMSIfcfgRHPlugin, nms_ifcfg_rh_plugin, NM_TYPE_SETTINGS_PLUGIN)

#define NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMSIfcfgRHPlugin, NMS_IS_IFCFG_RH_PLUGIN, NMSettingsPlugin)

/*****************************************************************************/

#define _NMLOG_DOMAIN LOGD_SETTINGS
#define _NMLOG(level, ...)                                      \
    G_STMT_START                                                \
    {                                                           \
        nm_log((level),                                         \
               (_NMLOG_DOMAIN),                                 \
               NULL,                                            \
               NULL,                                            \
               "%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__),         \
               "ifcfg-rh: " _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
    }                                                           \
    G_STMT_END

/*****************************************************************************/

static void _unhandled_specs_reset(NMSIfcfgRHPlugin *self);

static void _unhandled_specs_merge_storages(NMSIfcfgRHPlugin *self, NMSettUtilStorages *storages);

/*****************************************************************************/

static void
nm_assert_self(NMSIfcfgRHPlugin *self, gboolean unhandled_specs_consistent)
{
    nm_assert(NMS_IS_IFCFG_RH_PLUGIN(self));

#if NM_MORE_ASSERTS > 5
    {
        NMSIfcfgRHPluginPrivate       *priv = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
        NMSIfcfgRHStorage             *storage;
        gsize                          n_uuid;
        gs_unref_hashtable GHashTable *h_unmanaged    = NULL;
        gs_unref_hashtable GHashTable *h_unrecognized = NULL;

        nm_assert(g_hash_table_size(priv->storages.idx_by_filename)
                  == c_list_length(&priv->storages._storage_lst_head));

        h_unmanaged    = g_hash_table_new(nm_str_hash, g_str_equal);
        h_unrecognized = g_hash_table_new(nm_str_hash, g_str_equal);

        n_uuid = 0;

        c_list_for_each_entry (storage, &priv->storages._storage_lst_head, parent._storage_lst) {
            const char *uuid;
            const char *filename;

            filename = nms_ifcfg_rh_storage_get_filename(storage);

            nm_assert(filename && NM_STR_HAS_PREFIX(filename, IFCFG_DIR "/"));

            uuid = nms_ifcfg_rh_storage_get_uuid_opt(storage);

            nm_assert((!!uuid) + (!!storage->unmanaged_spec) + (!!storage->unrecognized_spec) == 1);

            nm_assert(storage
                      == nm_sett_util_storages_lookup_by_filename(&priv->storages, filename));

            if (uuid) {
                NMSettUtilStorageByUuidHead *sbuh;
                NMSettUtilStorageByUuidHead *sbuh2;

                if (storage->connection)
                    nm_assert(nm_streq0(nm_connection_get_uuid(storage->connection), uuid));

                if (!g_hash_table_lookup_extended(priv->storages.idx_by_uuid,
                                                  &uuid,
                                                  (gpointer *) &sbuh,
                                                  (gpointer *) &sbuh2))
                    nm_assert_not_reached();

                nm_assert(sbuh);
                nm_assert(nm_streq(uuid, sbuh->uuid));
                nm_assert(sbuh == sbuh2);
                nm_assert(c_list_contains(&sbuh->_storage_by_uuid_lst_head,
                                          &storage->parent._storage_by_uuid_lst));

                if (c_list_first(&sbuh->_storage_by_uuid_lst_head)
                    == &storage->parent._storage_by_uuid_lst)
                    n_uuid++;
            } else if (storage->unmanaged_spec) {
                nm_assert(strlen(storage->unmanaged_spec) > 0);
                g_hash_table_add(h_unmanaged, storage->unmanaged_spec);
            } else if (storage->unrecognized_spec) {
                nm_assert(strlen(storage->unrecognized_spec) > 0);
                g_hash_table_add(h_unrecognized, storage->unrecognized_spec);
            } else
                nm_assert_not_reached();

            nm_assert(!storage->connection);
        }

        nm_assert(g_hash_table_size(priv->storages.idx_by_uuid) == n_uuid);

        if (unhandled_specs_consistent) {
            nm_assert(nm_utils_hashtable_same_keys(h_unmanaged, priv->unmanaged_specs));
            nm_assert(nm_utils_hashtable_same_keys(h_unrecognized, priv->unrecognized_specs));
        }
    }
#endif
}

/*****************************************************************************/

static NMSIfcfgRHStorage *
_load_file(NMSIfcfgRHPlugin *self, const char *filename, GError **error)
{
    NMSIfcfgRHStorage            *ret            = NULL;
    gs_unref_object NMConnection *connection     = NULL;
    gs_free_error GError         *load_error     = NULL;
    gs_free char                 *unhandled_spec = NULL;
    gboolean                      load_error_ignore;
    struct stat                   st;

    if (stat(filename, &st) != 0) {
        int errsv = errno;

        if (error) {
            nm_utils_error_set_errno(error, errsv, "failure to stat file \%s\": %s", filename);
        } else
            _LOGT("load[%s]: failure to stat file: %s", filename, nm_strerror_native(errsv));
        return NULL;
    }

    connection = connection_from_file(filename, &unhandled_spec, &load_error, &load_error_ignore);
    if (load_error) {
        if (error) {
            nm_utils_error_set(error,
                               NM_UTILS_ERROR_UNKNOWN,
                               "failure to read file \"%s\": %s",
                               filename,
                               load_error->message);
        } else {
            _NMLOG(load_error_ignore ? LOGL_TRACE : LOGL_WARN,
                   "load[%s]: failure to read file: %s",
                   filename,
                   load_error->message);
        }
        return NULL;
    }

    if (unhandled_spec) {
        const char *unmanaged_spec;
        const char *unrecognized_spec;

        if (!nms_ifcfg_rh_utils_parse_unhandled_spec(unhandled_spec,
                                                     &unmanaged_spec,
                                                     &unrecognized_spec)) {
            nm_utils_error_set(error,
                               NM_UTILS_ERROR_UNKNOWN,
                               "invalid unhandled spec \"%s\"",
                               unhandled_spec);
            nm_assert_not_reached();
            return NULL;
        }

        ret = nms_ifcfg_rh_storage_new_unhandled(self, filename, unmanaged_spec, unrecognized_spec);
    } else {
        ret = nms_ifcfg_rh_storage_new_connection(self,
                                                  filename,
                                                  g_steal_pointer(&connection),
                                                  &st.st_mtim);
    }

    return ret;
}

static void
_load_dir(NMSIfcfgRHPlugin *self, NMSettUtilStorages *storages)
{
    gs_unref_hashtable GHashTable *dupl_filenames = NULL;
    gs_free_error GError          *local          = NULL;
    const char                    *f_filename;
    GDir                          *dir;

    dir = g_dir_open(IFCFG_DIR, 0, &local);
    if (!dir) {
        _LOGT("Could not read directory '%s': %s", IFCFG_DIR, local->message);
        return;
    }

    dupl_filenames = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, g_free);

    while ((f_filename = g_dir_read_name(dir))) {
        gs_free char      *full_path = NULL;
        NMSIfcfgRHStorage *storage;
        char              *full_filename;

        full_path     = g_build_filename(IFCFG_DIR, f_filename, NULL);
        full_filename = utils_detect_ifcfg_path(full_path, TRUE);
        if (!full_filename)
            continue;

        if (!g_hash_table_add(dupl_filenames, full_filename))
            continue;

        nm_assert(!nm_sett_util_storages_lookup_by_filename(storages, full_filename));

        storage = _load_file(self, full_filename, NULL);
        if (storage)
            nm_sett_util_storages_add_take(storages, storage);
    }
    g_dir_close(dir);
}

static void
_storages_consolidate(NMSIfcfgRHPlugin                      *self,
                      NMSettUtilStorages                    *storages_new,
                      gboolean                               replace_all,
                      GHashTable                            *storages_replaced,
                      NMSettingsPluginConnectionLoadCallback callback,
                      gpointer                               user_data)
{
    NMSIfcfgRHPluginPrivate     *priv                  = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    CList                        lst_conn_info_deleted = C_LIST_INIT(lst_conn_info_deleted);
    gs_unref_ptrarray GPtrArray *storages_modified     = NULL;
    CList                        storages_deleted;
    NMSIfcfgRHStorage           *storage_safe;
    NMSIfcfgRHStorage           *storage_new;
    NMSIfcfgRHStorage           *storage_old;
    NMSIfcfgRHStorage           *storage;
    guint                        i;

    /* when we reload all files, we must signal add/update/modify of profiles one-by-one.
     * NMSettings then goes ahead and emits further signals and a lot of things happen.
     *
     * So, first, emit an update of the unmanaged/unrecognized specs that contains *all*
     * the unmanaged/unrecognized devices from before and after. Since both unmanaged/unrecognized
     * specs have the meaning of "not doing something", it makes sense that we temporarily
     * disable that action for the sum of before and after. */
    _unhandled_specs_merge_storages(self, storages_new);

    storages_modified = g_ptr_array_new_with_free_func(g_object_unref);
    c_list_init(&storages_deleted);

    c_list_for_each_entry (storage_old, &priv->storages._storage_lst_head, parent._storage_lst)
        storage_old->dirty = TRUE;

    c_list_for_each_entry_safe (storage_new,
                                storage_safe,
                                &storages_new->_storage_lst_head,
                                parent._storage_lst) {
        storage_old = nm_sett_util_storages_lookup_by_filename(
            &priv->storages,
            nms_ifcfg_rh_storage_get_filename(storage_new));

        nm_sett_util_storages_steal(storages_new, storage_new);

        if (!storage_old || !nms_ifcfg_rh_storage_equal_type(storage_new, storage_old)) {
            if (storage_old) {
                nm_sett_util_storages_steal(&priv->storages, storage_old);
                if (nms_ifcfg_rh_storage_get_uuid_opt(storage_old))
                    c_list_link_tail(&storages_deleted, &storage_old->parent._storage_lst);
                else
                    nms_ifcfg_rh_storage_destroy(storage_old);
            }
            storage_new->dirty = FALSE;
            nm_sett_util_storages_add_take(&priv->storages, storage_new);
            g_ptr_array_add(storages_modified, g_object_ref(storage_new));
            continue;
        }

        storage_old->dirty = FALSE;
        nms_ifcfg_rh_storage_copy_content(storage_old, storage_new);
        nms_ifcfg_rh_storage_destroy(storage_new);
        g_ptr_array_add(storages_modified, g_object_ref(storage_old));
    }

    c_list_for_each_entry_safe (storage_old,
                                storage_safe,
                                &priv->storages._storage_lst_head,
                                parent._storage_lst) {
        if (!storage_old->dirty)
            continue;
        if (replace_all
            || (storages_replaced && g_hash_table_contains(storages_replaced, storage_old))) {
            nm_sett_util_storages_steal(&priv->storages, storage_old);
            if (nms_ifcfg_rh_storage_get_uuid_opt(storage_old))
                c_list_link_tail(&storages_deleted, &storage_old->parent._storage_lst);
            else
                nms_ifcfg_rh_storage_destroy(storage_old);
        }
    }

    /* raise events. */

    for (i = 0; i < storages_modified->len; i++) {
        storage        = storages_modified->pdata[i];
        storage->dirty = TRUE;
    }

    for (i = 0; i < storages_modified->len; i++) {
        gs_unref_object NMConnection *connection = NULL;
        storage                                  = storages_modified->pdata[i];

        if (!storage->dirty) {
            /* the entry is no longer dirty. In the meantime we already emitted
             * another signal for it. */
            continue;
        }
        storage->dirty = FALSE;
        if (storage
            != nm_sett_util_storages_lookup_by_filename(
                &priv->storages,
                nms_ifcfg_rh_storage_get_filename(storage))) {
            /* hm? The profile was deleted in the meantime? That is only possible
             * if the signal handler called again into the plugin. In any case, the event
             * was already emitted. Skip. */
            continue;
        }

        connection = nms_ifcfg_rh_storage_steal_connection(storage);
        if (!connection) {
            nm_assert(!nms_ifcfg_rh_storage_get_uuid_opt(storage));
            continue;
        }

        nm_assert(NM_IS_CONNECTION(connection));
        nm_assert(nms_ifcfg_rh_storage_get_uuid_opt(storage));
        callback(NM_SETTINGS_PLUGIN(self), NM_SETTINGS_STORAGE(storage), connection, user_data);
    }

    while (
        (storage = c_list_first_entry(&storages_deleted, NMSIfcfgRHStorage, parent._storage_lst))) {
        c_list_unlink(&storage->parent._storage_lst);
        callback(NM_SETTINGS_PLUGIN(self), NM_SETTINGS_STORAGE(storage), NULL, user_data);
        nms_ifcfg_rh_storage_destroy(storage);
    }
}

/*****************************************************************************/

static void
load_connections(NMSettingsPlugin                      *plugin,
                 NMSettingsPluginConnectionLoadEntry   *entries,
                 gsize                                  n_entries,
                 NMSettingsPluginConnectionLoadCallback callback,
                 gpointer                               user_data)
{
    NMSIfcfgRHPlugin        *self = NMS_IFCFG_RH_PLUGIN(plugin);
    NMSIfcfgRHPluginPrivate *priv = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    nm_auto_clear_sett_util_storages NMSettUtilStorages storages_new =
        NM_SETT_UTIL_STORAGES_INIT(storages_new, nms_ifcfg_rh_storage_destroy);
    gs_unref_hashtable GHashTable *dupl_filenames    = NULL;
    gs_unref_hashtable GHashTable *storages_replaced = NULL;
    gs_unref_hashtable GHashTable *loaded_uuids      = NULL;
    const char                    *loaded_uuid;
    GHashTableIter                 h_iter;
    gsize                          i;

    if (n_entries == 0)
        return;

    dupl_filenames = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL);

    loaded_uuids = g_hash_table_new(nm_str_hash, g_str_equal);

    storages_replaced = g_hash_table_new_full(nm_direct_hash, NULL, g_object_unref, NULL);

    for (i = 0; i < n_entries; i++) {
        NMSettingsPluginConnectionLoadEntry *const entry = &entries[i];
        gs_free_error GError                      *local = NULL;
        const char                                *full_filename;
        const char                                *uuid;
        gs_free char                              *full_filename_keep = NULL;
        NMSettingsPluginConnectionLoadEntry       *dupl_content_entry;
        gs_unref_object NMSIfcfgRHStorage         *storage = NULL;

        if (entry->handled)
            continue;

        if (entry->filename[0] != '/')
            continue;

        full_filename_keep = utils_detect_ifcfg_path(entry->filename, FALSE);

        if (!full_filename_keep) {
            if (nm_utils_file_is_in_path(entry->filename, IFCFG_DIR)) {
                nm_utils_error_set(&entry->error,
                                   NM_UTILS_ERROR_UNKNOWN,
                                   ("path is not a valid name for an ifcfg-rh file"));
                entry->handled = TRUE;
            }
            continue;
        }

        if ((dupl_content_entry = g_hash_table_lookup(dupl_filenames, full_filename_keep))) {
            /* we already visited this file. */
            entry->handled = dupl_content_entry->handled;
            if (dupl_content_entry->error) {
                g_set_error_literal(&entry->error,
                                    dupl_content_entry->error->domain,
                                    dupl_content_entry->error->code,
                                    dupl_content_entry->error->message);
            }
            continue;
        }

        entry->handled = TRUE;

        full_filename = full_filename_keep;
        if (!g_hash_table_insert(dupl_filenames, g_steal_pointer(&full_filename_keep), entry))
            nm_assert_not_reached();

        storage = _load_file(self, full_filename, &local);
        if (!storage) {
            if (nm_utils_file_stat(full_filename, NULL) == -ENOENT) {
                NMSIfcfgRHStorage *storage2;

                /* the file does not exist. We take that as indication to unload the file
                 * that was previously loaded... */
                storage2 = nm_sett_util_storages_lookup_by_filename(&priv->storages, full_filename);
                if (storage2)
                    g_hash_table_add(storages_replaced, g_object_ref(storage2));
                continue;
            }
            g_propagate_error(&entry->error, g_steal_pointer(&local));
            continue;
        }

        uuid = nms_ifcfg_rh_storage_get_uuid_opt(storage);
        if (uuid)
            g_hash_table_add(loaded_uuids, (char *) uuid);

        nm_sett_util_storages_add_take(&storages_new, g_steal_pointer(&storage));
    }

    /* now we visit all UUIDs that are about to change... */
    g_hash_table_iter_init(&h_iter, loaded_uuids);
    while (g_hash_table_iter_next(&h_iter, (gpointer *) &loaded_uuid, NULL)) {
        NMSIfcfgRHStorage           *storage;
        NMSettUtilStorageByUuidHead *sbuh;

        sbuh = nm_sett_util_storages_lookup_by_uuid(&priv->storages, loaded_uuid);
        if (!sbuh)
            continue;

        c_list_for_each_entry (storage,
                               &sbuh->_storage_by_uuid_lst_head,
                               parent._storage_by_uuid_lst) {
            const char *full_filename = nms_ifcfg_rh_storage_get_filename(storage);
            gs_unref_object NMSIfcfgRHStorage *storage_new = NULL;
            gs_free_error GError              *local       = NULL;

            if (g_hash_table_contains(dupl_filenames, full_filename)) {
                /* already re-loaded. */
                continue;
            }

            /* @storage has a UUID that was just loaded from disk, but we have an entry in cache.
             * Reload that file too despite not being told to do so. The reason is to get
             * the latest file timestamp so that we get the priorities right. */

            storage_new = _load_file(self, full_filename, &local);
            if (storage_new
                && !nm_streq0(loaded_uuid, nms_ifcfg_rh_storage_get_uuid_opt(storage_new))) {
                /* the file now references a different UUID. We are not told to reload
                 * that file, so this means the existing storage (with the previous
                 * filename and UUID tuple) is no longer valid. */
                g_clear_object(&storage_new);
            }

            g_hash_table_add(storages_replaced, g_object_ref(storage));
            if (storage_new)
                nm_sett_util_storages_add_take(&storages_new, g_steal_pointer(&storage_new));
        }
    }

    nm_clear_pointer(&loaded_uuids, g_hash_table_destroy);
    nm_clear_pointer(&dupl_filenames, g_hash_table_destroy);

    _storages_consolidate(self, &storages_new, FALSE, storages_replaced, callback, user_data);
}

static void
reload_connections(NMSettingsPlugin                      *plugin,
                   NMSettingsPluginConnectionLoadCallback callback,
                   gpointer                               user_data)
{
    NMSIfcfgRHPlugin                                   *self = NMS_IFCFG_RH_PLUGIN(plugin);
    nm_auto_clear_sett_util_storages NMSettUtilStorages storages_new =
        NM_SETT_UTIL_STORAGES_INIT(storages_new, nms_ifcfg_rh_storage_destroy);

    nm_assert_self(self, TRUE);

    _load_dir(self, &storages_new);

    _storages_consolidate(self, &storages_new, TRUE, NULL, callback, user_data);

    nm_assert_self(self, FALSE);
}

static void
load_connections_done(NMSettingsPlugin *plugin)
{
    NMSIfcfgRHPlugin *self = NMS_IFCFG_RH_PLUGIN(plugin);

    /* at the beginning of a load, we emit a change signal for unmanaged/unrecognized
     * specs that contain the sum of before and after (_unhandled_specs_merge_storages()).
     *
     * The idea is that while we emit signals about changes to connection, we have
     * the sum of all unmanaged/unrecognized devices from before and after.
     *
     * This if triggered at the end, to reset the specs. */
    _unhandled_specs_reset(self);

    nm_assert_self(self, TRUE);
}

/*****************************************************************************/

static gboolean
add_connection(NMSettingsPlugin   *plugin,
               NMConnection       *connection,
               NMSettingsStorage **out_storage,
               NMConnection      **out_connection,
               GError            **error)
{
    NMSIfcfgRHPlugin                  *self          = NMS_IFCFG_RH_PLUGIN(plugin);
    NMSIfcfgRHPluginPrivate           *priv          = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    gs_unref_object NMSIfcfgRHStorage *storage       = NULL;
    gs_unref_object NMConnection      *reread        = NULL;
    gs_free char                      *full_filename = NULL;
    GError                            *local         = NULL;
    gboolean                           reread_same;
    struct timespec                    mtime;

    nm_assert_self(self, TRUE);
    nm_assert(NM_IS_CONNECTION(connection));
    nm_assert(out_storage && !*out_storage);
    nm_assert(out_connection && !*out_connection);

    if (!nms_ifcfg_rh_writer_write_connection(
            connection,
            IFCFG_DIR,
            NULL,
            nm_sett_util_allow_filename_cb,
            NM_SETT_UTIL_ALLOW_FILENAME_DATA(&priv->storages, NULL),
            &full_filename,
            &reread,
            &reread_same,
            &local)) {
        _LOGT("commit: %s (%s): failed to add: %s",
              nm_connection_get_uuid(connection),
              nm_connection_get_id(connection),
              local->message);
        g_propagate_error(error, local);
        return FALSE;
    }

    if (!reread || reread_same)
        nm_g_object_ref_set(&reread, connection);

    nm_assert(full_filename && full_filename[0] == '/');

    _LOGT("commit: %s (%s) added as \"%s\"",
          nm_connection_get_uuid(reread),
          nm_connection_get_id(reread),
          full_filename);

    storage =
        nms_ifcfg_rh_storage_new_connection(self,
                                            full_filename,
                                            g_steal_pointer(&reread),
                                            nm_sett_util_stat_mtime(full_filename, FALSE, &mtime));

    nm_sett_util_storages_add_take(&priv->storages, g_object_ref(storage));

    *out_connection = nms_ifcfg_rh_storage_steal_connection(storage);
    *out_storage    = NM_SETTINGS_STORAGE(g_steal_pointer(&storage));

    nm_assert_self(self, TRUE);

    return TRUE;
}

static gboolean
update_connection(NMSettingsPlugin   *plugin,
                  NMSettingsStorage  *storage_x,
                  NMConnection       *connection,
                  NMSettingsStorage **out_storage,
                  NMConnection      **out_connection,
                  GError            **error)
{
    NMSIfcfgRHPlugin             *self    = NMS_IFCFG_RH_PLUGIN(plugin);
    NMSIfcfgRHPluginPrivate      *priv    = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    NMSIfcfgRHStorage            *storage = NMS_IFCFG_RH_STORAGE(storage_x);
    const char                   *full_filename;
    const char                   *uuid;
    GError                       *local  = NULL;
    gs_unref_object NMConnection *reread = NULL;
    gboolean                      reread_same;
    struct timespec               mtime;

    nm_assert_self(self, TRUE);
    nm_assert(NM_IS_CONNECTION(connection));
    nm_assert(NMS_IS_IFCFG_RH_STORAGE(storage));
    nm_assert(_nm_connection_verify(connection, NULL) == NM_SETTING_VERIFY_SUCCESS);
    nm_assert(!error || !*error);

    uuid = nms_ifcfg_rh_storage_get_uuid_opt(storage);

    nm_assert(uuid && nm_streq0(uuid, nm_connection_get_uuid(connection)));

    full_filename = nms_ifcfg_rh_storage_get_filename(storage);

    nm_assert(full_filename);
    nm_assert(storage == nm_sett_util_storages_lookup_by_filename(&priv->storages, full_filename));

    if (!nms_ifcfg_rh_writer_write_connection(
            connection,
            IFCFG_DIR,
            full_filename,
            nm_sett_util_allow_filename_cb,
            NM_SETT_UTIL_ALLOW_FILENAME_DATA(&priv->storages, full_filename),
            NULL,
            &reread,
            &reread_same,
            &local)) {
        _LOGT("commit: failure to write %s (%s) to \"%s\": %s",
              nm_connection_get_uuid(connection),
              nm_connection_get_id(connection),
              full_filename,
              local->message);
        g_propagate_error(error, local);
        return FALSE;
    }

    if (!reread || reread_same)
        nm_g_object_ref_set(&reread, connection);

    _LOGT("commit: \"%s\": profile %s (%s) written",
          full_filename,
          uuid,
          nm_connection_get_id(connection));

    storage->stat_mtime = *nm_sett_util_stat_mtime(full_filename, FALSE, &mtime);

    *out_storage    = NM_SETTINGS_STORAGE(g_object_ref(storage));
    *out_connection = g_steal_pointer(&reread);

    nm_assert_self(self, TRUE);

    return TRUE;
}

static gboolean
delete_connection(NMSettingsPlugin *plugin, NMSettingsStorage *storage_x, GError **error)
{
    NMSIfcfgRHPlugin        *self    = NMS_IFCFG_RH_PLUGIN(plugin);
    NMSIfcfgRHPluginPrivate *priv    = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    NMSIfcfgRHStorage       *storage = NMS_IFCFG_RH_STORAGE(storage_x);
    const char              *operation_message;
    const char              *full_filename;

    nm_assert_self(self, TRUE);
    nm_assert(!error || !*error);
    nm_assert(NMS_IS_IFCFG_RH_STORAGE(storage));

    full_filename = nms_ifcfg_rh_storage_get_filename(storage);
    nm_assert(full_filename);

    nm_assert(nms_ifcfg_rh_storage_get_uuid_opt(storage));

    nm_assert(storage == nm_sett_util_storages_lookup_by_filename(&priv->storages, full_filename));

    {
        gs_free char     *keyfile     = utils_get_keys_path(full_filename);
        gs_free char     *routefile   = utils_get_route_path(full_filename);
        gs_free char     *route6file  = utils_get_route6_path(full_filename);
        const char *const files[]     = {full_filename, keyfile, routefile, route6file};
        gboolean          any_deleted = FALSE;
        gboolean          any_failure = FALSE;
        int               i;

        for (i = 0; i < G_N_ELEMENTS(files); i++) {
            int errsv;

            if (unlink(files[i]) == 0) {
                any_deleted = TRUE;
                continue;
            }
            errsv = errno;
            if (errsv == ENOENT)
                continue;

            _LOGW("commit: failure to delete file \"%s\": %s", files[i], nm_strerror_native(errsv));
            any_failure = TRUE;
        }
        if (any_failure)
            operation_message = "failed to delete files from disk";
        else if (any_deleted)
            operation_message = "deleted from disk";
        else
            operation_message = "does not exist on disk";
    }

    _LOGT("commit: deleted \"%s\", profile %s (%s)",
          full_filename,
          nms_ifcfg_rh_storage_get_uuid_opt(storage),
          operation_message);

    nm_sett_util_storages_steal(&priv->storages, storage);
    nms_ifcfg_rh_storage_destroy(storage);

    nm_assert_self(self, TRUE);

    return TRUE;
}

/*****************************************************************************/

static void
_unhandled_specs_reset(NMSIfcfgRHPlugin *self)
{
    NMSIfcfgRHPluginPrivate       *priv               = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    gs_unref_hashtable GHashTable *unmanaged_specs    = NULL;
    gs_unref_hashtable GHashTable *unrecognized_specs = NULL;
    NMSIfcfgRHStorage             *storage;

    unmanaged_specs    = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL);
    unrecognized_specs = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL);

    c_list_for_each_entry (storage, &priv->storages._storage_lst_head, parent._storage_lst) {
        if (storage->unmanaged_spec)
            g_hash_table_add(unmanaged_specs, g_strdup(storage->unmanaged_spec));
        if (storage->unrecognized_spec)
            g_hash_table_add(unrecognized_specs, g_strdup(storage->unrecognized_spec));
    }

    if (!nm_utils_hashtable_same_keys(unmanaged_specs, priv->unmanaged_specs)) {
        g_hash_table_unref(priv->unmanaged_specs);
        priv->unmanaged_specs = g_steal_pointer(&unmanaged_specs);
    }
    if (!nm_utils_hashtable_same_keys(unrecognized_specs, priv->unrecognized_specs)) {
        g_hash_table_unref(priv->unrecognized_specs);
        priv->unrecognized_specs = g_steal_pointer(&unrecognized_specs);
    }

    if (!unmanaged_specs)
        _nm_settings_plugin_emit_signal_unmanaged_specs_changed(NM_SETTINGS_PLUGIN(self));
    if (!unrecognized_specs)
        _nm_settings_plugin_emit_signal_unrecognized_specs_changed(NM_SETTINGS_PLUGIN(self));
}

static void
_unhandled_specs_merge_storages(NMSIfcfgRHPlugin *self, NMSettUtilStorages *storages)
{
    NMSIfcfgRHPluginPrivate *priv                 = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    gboolean                 unmanaged_changed    = FALSE;
    gboolean                 unrecognized_changed = FALSE;
    NMSIfcfgRHStorage       *storage;

    c_list_for_each_entry (storage, &storages->_storage_lst_head, parent._storage_lst) {
        if (storage->unmanaged_spec
            && !g_hash_table_contains(priv->unmanaged_specs, storage->unmanaged_spec)) {
            unmanaged_changed = TRUE;
            g_hash_table_add(priv->unmanaged_specs, g_strdup(storage->unmanaged_spec));
        }
        if (storage->unrecognized_spec
            && !g_hash_table_contains(priv->unrecognized_specs, storage->unrecognized_spec)) {
            unrecognized_changed = TRUE;
            g_hash_table_add(priv->unrecognized_specs, g_strdup(storage->unrecognized_spec));
        }
    }

    if (unmanaged_changed)
        _nm_settings_plugin_emit_signal_unmanaged_specs_changed(NM_SETTINGS_PLUGIN(self));
    if (unrecognized_changed)
        _nm_settings_plugin_emit_signal_unrecognized_specs_changed(NM_SETTINGS_PLUGIN(self));
}

static GSList *
_unhandled_specs_from_hashtable(GHashTable *hash)
{
    gs_free const char **keys = NULL;
    GSList              *list = NULL;
    guint                i, l;

    keys = nm_strdict_get_keys(hash, TRUE, &l);
    for (i = l; i > 0;) {
        i--;
        list = g_slist_prepend(list, g_strdup(keys[i]));
    }
    return list;
}

static GSList *
get_unmanaged_specs(NMSettingsPlugin *plugin)
{
    return _unhandled_specs_from_hashtable(
        NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(plugin)->unmanaged_specs);
}

static GSList *
get_unrecognized_specs(NMSettingsPlugin *plugin)
{
    return _unhandled_specs_from_hashtable(
        NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(plugin)->unrecognized_specs);
}

/*****************************************************************************/

static void
impl_ifcfgrh_get_ifcfg_details(NMSIfcfgRHPlugin      *self,
                               GDBusMethodInvocation *context,
                               const char            *in_ifcfg)
{
    NMSIfcfgRHPluginPrivate *priv       = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    gs_free char            *ifcfg_path = NULL;
    NMSIfcfgRHStorage       *storage;
    const char              *uuid;
    const char              *path;

    if (in_ifcfg[0] != '/') {
        g_dbus_method_invocation_return_error(context,
                                              NM_SETTINGS_ERROR,
                                              NM_SETTINGS_ERROR_INVALID_CONNECTION,
                                              "ifcfg path '%s' is not absolute",
                                              in_ifcfg);
        return;
    }

    ifcfg_path = utils_detect_ifcfg_path(in_ifcfg, TRUE);
    if (!ifcfg_path) {
        g_dbus_method_invocation_return_error(context,
                                              NM_SETTINGS_ERROR,
                                              NM_SETTINGS_ERROR_INVALID_CONNECTION,
                                              "ifcfg path '%s' is not an ifcfg base file",
                                              in_ifcfg);
        return;
    }

    storage = nm_sett_util_storages_lookup_by_filename(&priv->storages, ifcfg_path);
    if (!storage) {
        g_dbus_method_invocation_return_error(context,
                                              NM_SETTINGS_ERROR,
                                              NM_SETTINGS_ERROR_INVALID_CONNECTION,
                                              "ifcfg file '%s' unknown",
                                              in_ifcfg);
        return;
    }

    uuid = nms_ifcfg_rh_storage_get_uuid_opt(storage);
    if (!uuid) {
        g_dbus_method_invocation_return_error(context,
                                              NM_SETTINGS_ERROR,
                                              NM_SETTINGS_ERROR_INVALID_CONNECTION,
                                              "ifcfg file '%s' not managed by NetworkManager",
                                              in_ifcfg);
        return;
    }

    /* It is ugly that the ifcfg-rh plugin needs to call back into NMSettings this
     * way.
     * There are alternatives (like invoking a signal), but they are all significant
     * extra code (and performance overhead). So the quick and dirty solution here
     * is likely to be simpler than getting this right (also from point of readability!).
     */
    path = nm_settings_get_dbus_path_for_uuid(nm_settings_get(), uuid);

    if (!path) {
        g_dbus_method_invocation_return_error(context,
                                              NM_SETTINGS_ERROR,
                                              NM_SETTINGS_ERROR_FAILED,
                                              "unable to get the connection D-Bus path");
        return;
    }

    g_dbus_method_invocation_return_value(context, g_variant_new("(so)", uuid, path));
}

/*****************************************************************************/

static void
_dbus_clear(NMSIfcfgRHPlugin *self)
{
    NMSIfcfgRHPluginPrivate *priv = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    guint                    id;

    nm_clear_g_signal_handler(priv->dbus.connection, &priv->dbus.signal_id);

    nm_clear_g_cancellable(&priv->dbus.cancellable);

    if ((id = nm_steal_int(&priv->dbus.regist_id))) {
        if (!g_dbus_connection_unregister_object(priv->dbus.connection, id))
            _LOGW("dbus: unexpected failure to unregister object");
    }

    g_clear_object(&priv->dbus.connection);
}

static void
_dbus_connection_closed(GDBusConnection *connection,
                        gboolean         remote_peer_vanished,
                        GError          *error,
                        gpointer         user_data)
{
    _LOGW("dbus: %s bus closed", IFCFGRH1_BUS_NAME);
    _dbus_clear(NMS_IFCFG_RH_PLUGIN(user_data));

    /* Retry or recover? */
}

static void
_method_call(GDBusConnection       *connection,
             const char            *sender,
             const char            *object_path,
             const char            *interface_name,
             const char            *method_name,
             GVariant              *parameters,
             GDBusMethodInvocation *invocation,
             gpointer               user_data)
{
    NMSIfcfgRHPlugin *self = NMS_IFCFG_RH_PLUGIN(user_data);

    if (nm_streq(interface_name, IFCFGRH1_IFACE1_NAME)) {
        if (nm_streq(method_name, IFCFGRH1_IFACE1_METHOD_GET_IFCFG_DETAILS)) {
            const char *ifcfg;

            g_variant_get(parameters, "(&s)", &ifcfg);
            impl_ifcfgrh_get_ifcfg_details(self, invocation, ifcfg);
            return;
        }
    }

    g_dbus_method_invocation_return_error(invocation,
                                          G_DBUS_ERROR,
                                          G_DBUS_ERROR_UNKNOWN_METHOD,
                                          "Unknown method %s",
                                          method_name);
}

static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO(
    IFCFGRH1_IFACE1_NAME,
    .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
        NM_DEFINE_GDBUS_METHOD_INFO(
            IFCFGRH1_IFACE1_METHOD_GET_IFCFG_DETAILS,
            .in_args  = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("ifcfg", "s"), ),
            .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("uuid", "s"),
                                                  NM_DEFINE_GDBUS_ARG_INFO("path", "o"), ), ), ), );

static void
_dbus_request_name_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    GDBusConnection           *connection = G_DBUS_CONNECTION(source_object);
    NMSIfcfgRHPlugin          *self;
    NMSIfcfgRHPluginPrivate   *priv;
    gs_free_error GError      *error = NULL;
    gs_unref_variant GVariant *ret   = NULL;
    guint32                    result;

    ret = g_dbus_connection_call_finish(connection, res, &error);
    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    self = NMS_IFCFG_RH_PLUGIN(user_data);
    priv = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);

    g_clear_object(&priv->dbus.cancellable);

    if (!ret) {
        _LOGW("dbus: couldn't acquire D-Bus service: %s", error->message);
        _dbus_clear(self);
        return;
    }

    g_variant_get(ret, "(u)", &result);

    if (result != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
        _LOGW("dbus: couldn't acquire ifcfgrh1 D-Bus service (already taken)");
        _dbus_clear(self);
        return;
    }

    {
        static const GDBusInterfaceVTable interface_vtable = {
            .method_call = _method_call,
        };

        priv->dbus.regist_id = g_dbus_connection_register_object(
            connection,
            IFCFGRH1_OBJECT_PATH,
            interface_info,
            NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable),
            self,
            NULL,
            &error);
        if (!priv->dbus.regist_id) {
            _LOGW("dbus: couldn't register D-Bus service: %s", error->message);
            _dbus_clear(self);
            return;
        }
    }

    _LOGD("dbus: acquired D-Bus service %s and exported %s object",
          IFCFGRH1_BUS_NAME,
          IFCFGRH1_OBJECT_PATH);
}

static void
_dbus_create_done(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
    NMSIfcfgRHPlugin        *self;
    NMSIfcfgRHPluginPrivate *priv;
    gs_free_error GError    *error = NULL;
    GDBusConnection         *connection;

    connection = g_dbus_connection_new_for_address_finish(res, &error);
    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    self = NMS_IFCFG_RH_PLUGIN(user_data);
    priv = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);

    g_clear_object(&priv->dbus.cancellable);

    if (!connection) {
        _LOGW("dbus: couldn't initialize system bus: %s", error->message);
        return;
    }

    priv->dbus.connection  = connection;
    priv->dbus.cancellable = g_cancellable_new();

    priv->dbus.signal_id = g_signal_connect(priv->dbus.connection,
                                            "closed",
                                            G_CALLBACK(_dbus_connection_closed),
                                            self);

    g_dbus_connection_call(priv->dbus.connection,
                           DBUS_SERVICE_DBUS,
                           DBUS_PATH_DBUS,
                           DBUS_INTERFACE_DBUS,
                           "RequestName",
                           g_variant_new("(su)", IFCFGRH1_BUS_NAME, DBUS_NAME_FLAG_DO_NOT_QUEUE),
                           G_VARIANT_TYPE("(u)"),
                           G_DBUS_CALL_FLAGS_NONE,
                           -1,
                           priv->dbus.cancellable,
                           _dbus_request_name_done,
                           self);
}

static void
_dbus_setup(NMSIfcfgRHPlugin *self)
{
    NMSIfcfgRHPluginPrivate *priv    = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    gs_free char            *address = NULL;
    gs_free_error GError    *error   = NULL;

    _dbus_clear(self);

    if (!NM_MAIN_DBUS_CONNECTION_GET) {
        _LOGW("dbus: don't use D-Bus for %s service", IFCFGRH1_BUS_NAME);
        return;
    }

    /* We use a separate D-Bus connection so that org.freedesktop.NetworkManager and com.redhat.ifcfgrh1
     * are exported by different connections. */
    address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SYSTEM, NULL, &error);
    if (address == NULL) {
        _LOGW("dbus: failed getting address for system bus: %s", error->message);
        return;
    }

    priv->dbus.cancellable = g_cancellable_new();

    g_dbus_connection_new_for_address(address,
                                      G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT
                                          | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
                                      NULL,
                                      priv->dbus.cancellable,
                                      _dbus_create_done,
                                      self);
}

static void
config_changed_cb(NMConfig           *config,
                  NMConfigData       *config_data,
                  NMConfigChangeFlags changes,
                  NMConfigData       *old_data,
                  NMSIfcfgRHPlugin   *self)
{
    NMSIfcfgRHPluginPrivate *priv;

    /* If the dbus connection for some reason is borked the D-Bus service
     * won't be offered.
     *
     * On SIGHUP and SIGUSR1 try to re-connect to D-Bus. So in the unlikely
     * event that the D-Bus connection is broken, that allows for recovery
     * without need for restarting NetworkManager. */
    if (!NM_FLAGS_ANY(changes, NM_CONFIG_CHANGE_CAUSE_SIGHUP | NM_CONFIG_CHANGE_CAUSE_SIGUSR1))
        return;

    priv = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);
    if (!priv->dbus.connection && !priv->dbus.cancellable)
        _dbus_setup(self);
}

/*****************************************************************************/

static void
nms_ifcfg_rh_plugin_init(NMSIfcfgRHPlugin *self)
{
    NMSIfcfgRHPluginPrivate *priv = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);

    priv->config = g_object_ref(nm_config_get());

    priv->unmanaged_specs    = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL);
    priv->unrecognized_specs = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL);

    priv->storages = (NMSettUtilStorages) NM_SETT_UTIL_STORAGES_INIT(priv->storages,
                                                                     nms_ifcfg_rh_storage_destroy);
}

static void
constructed(GObject *object)
{
    NMSIfcfgRHPlugin        *self = NMS_IFCFG_RH_PLUGIN(object);
    NMSIfcfgRHPluginPrivate *priv = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);

    G_OBJECT_CLASS(nms_ifcfg_rh_plugin_parent_class)->constructed(object);

    g_signal_connect(priv->config,
                     NM_CONFIG_SIGNAL_CONFIG_CHANGED,
                     G_CALLBACK(config_changed_cb),
                     self);

    _dbus_setup(self);
}

static void
dispose(GObject *object)
{
    NMSIfcfgRHPlugin        *self = NMS_IFCFG_RH_PLUGIN(object);
    NMSIfcfgRHPluginPrivate *priv = NMS_IFCFG_RH_PLUGIN_GET_PRIVATE(self);

    if (priv->config)
        g_signal_handlers_disconnect_by_func(priv->config, config_changed_cb, self);

    /* FIXME(shutdown) we need a stop method so that we can unregistering the D-Bus service
     * when NMSettings is shutting down, and not when the instance gets destroyed. */
    _dbus_clear(self);

    nm_sett_util_storages_clear(&priv->storages);

    g_clear_object(&priv->config);

    G_OBJECT_CLASS(nms_ifcfg_rh_plugin_parent_class)->dispose(object);

    nm_clear_pointer(&priv->unmanaged_specs, g_hash_table_destroy);
    nm_clear_pointer(&priv->unrecognized_specs, g_hash_table_destroy);
}

static void
nms_ifcfg_rh_plugin_class_init(NMSIfcfgRHPluginClass *klass)
{
    GObjectClass          *object_class = G_OBJECT_CLASS(klass);
    NMSettingsPluginClass *plugin_class = NM_SETTINGS_PLUGIN_CLASS(klass);

    object_class->constructed = constructed;
    object_class->dispose     = dispose;

    plugin_class->plugin_name            = "ifcfg-rh";
    plugin_class->get_unmanaged_specs    = get_unmanaged_specs;
    plugin_class->get_unrecognized_specs = get_unrecognized_specs;
    plugin_class->reload_connections     = reload_connections;
    plugin_class->load_connections       = load_connections;
    plugin_class->load_connections_done  = load_connections_done;
    plugin_class->add_connection         = add_connection;
    plugin_class->update_connection      = update_connection;
    plugin_class->delete_connection      = delete_connection;
}

/*****************************************************************************/

G_MODULE_EXPORT NMSettingsPlugin *
nm_settings_plugin_factory(void)
{
    return g_object_new(NMS_TYPE_IFCFG_RH_PLUGIN, NULL);
}
