/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Copyright (C) 2008 Novell, Inc.
* Copyright (C) 2008 - 2018 Red Hat, Inc.
*/
#include "nm-default.h"
#include "nms-keyfile-plugin.h"
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include "nm-std-aux/c-list-util.h"
#include "nm-glib-aux/nm-c-list.h"
#include "nm-glib-aux/nm-io-utils.h"
#include "nm-connection.h"
#include "nm-setting.h"
#include "nm-setting-connection.h"
#include "nm-utils.h"
#include "nm-config.h"
#include "nm-core-internal.h"
#include "nm-keyfile/nm-keyfile-internal.h"
#include "systemd/nm-sd-utils-shared.h"
#include "settings/nm-settings-plugin.h"
#include "settings/nm-settings-storage.h"
#include "settings/nm-settings-utils.h"
#include "nms-keyfile-storage.h"
#include "nms-keyfile-writer.h"
#include "nms-keyfile-reader.h"
#include "nms-keyfile-utils.h"
/*****************************************************************************/
typedef struct {
NMConfig *config;
/* there can/could be multiple read-only directories. For example, one
* could set dirname_libs to
* - /usr/lib/NetworkManager/profiles/
* - /etc/NetworkManager/system-connections
* and leave dirname_etc unset. In this case, there would be multiple
* read-only directories.
*
* Directories that come later have higher priority and shadow profiles
* from earlier directories.
*
* Currently, this is only an array with zero or one elements. It could be
* easily extended to support multiple read-only directories.
*/
char *dirname_libs[2];
char *dirname_etc;
char *dirname_run;
NMSettUtilStorages storages;
} NMSKeyfilePluginPrivate;
struct _NMSKeyfilePlugin {
NMSettingsPlugin parent;
NMSKeyfilePluginPrivate _priv;
};
struct _NMSKeyfilePluginClass {
NMSettingsPluginClass parent;
};
G_DEFINE_TYPE(NMSKeyfilePlugin, nms_keyfile_plugin, NM_TYPE_SETTINGS_PLUGIN)
#define NMS_KEYFILE_PLUGIN_GET_PRIVATE(self) \
_NM_GET_PRIVATE(self, NMSKeyfilePlugin, NMS_IS_KEYFILE_PLUGIN, NMSettingsPlugin)
/*****************************************************************************/
#define _NMLOG_PREFIX_NAME "keyfile"
#define _NMLOG_DOMAIN LOGD_SETTINGS
#define _NMLOG(level, ...) \
nm_log((level), \
_NMLOG_DOMAIN, \
NULL, \
NULL, \
"%s" _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
_NMLOG_PREFIX_NAME ": " _NM_UTILS_MACRO_REST(__VA_ARGS__))
/*****************************************************************************/
static const char *
_extra_flags_to_string(char * str,
gsize str_len,
gboolean is_nm_generated,
gboolean is_volatile,
gboolean is_external)
{
const char *str0 = str;
if (!is_nm_generated && !is_volatile && !is_external)
nm_utils_strbuf_append_str(&str, &str_len, "");
else {
char ch = '(';
nm_utils_strbuf_append_c(&str, &str_len, ' ');
if (is_nm_generated) {
nm_utils_strbuf_append_c(&str, &str_len, ch);
nm_utils_strbuf_append_str(&str, &str_len, "nm-generated");
ch = ',';
}
if (is_volatile) {
nm_utils_strbuf_append_c(&str, &str_len, ch);
nm_utils_strbuf_append_str(&str, &str_len, "volatile");
ch = ',';
}
if (is_external) {
nm_utils_strbuf_append_c(&str, &str_len, ch);
nm_utils_strbuf_append_str(&str, &str_len, "external");
ch = ',';
}
nm_utils_strbuf_append_c(&str, &str_len, ')');
}
return str0;
}
static gboolean
_ignore_filename(NMSKeyfileStorageType storage_type, const char *filename)
{
/* for backward-compatibility, we don't require an extension for
* files under "/etc/...". */
return nm_keyfile_utils_ignore_filename(filename,
(storage_type != NMS_KEYFILE_STORAGE_TYPE_ETC));
}
static const char *
_get_plugin_dir(NMSKeyfilePluginPrivate *priv)
{
/* the plugin dir is only needed to generate connection.uuid value via
* nm_keyfile_read_ensure_uuid(). This is either the configured /etc
* directory, of the compile-time default (in case the /etc directory
* is disabled). */
return priv->dirname_etc ?: NM_KEYFILE_PATH_NAME_ETC_DEFAULT;
}
static gboolean
_path_detect_storage_type(const char * full_filename,
const char *const * dirname_libs,
const char * dirname_etc,
const char * dirname_run,
NMSKeyfileStorageType *out_storage_type,
const char ** out_dirname,
const char ** out_filename,
gboolean * out_is_nmmeta_file,
gboolean * out_failed_due_to_invalid_filename)
{
NMSKeyfileStorageType storage_type;
const char * filename = NULL;
const char * dirname = NULL;
guint i;
gboolean is_nmmeta_file = FALSE;
NM_SET_OUT(out_failed_due_to_invalid_filename, FALSE);
if (full_filename[0] != '/')
return FALSE;
if (dirname_run && (filename = nm_utils_file_is_in_path(full_filename, dirname_run))) {
storage_type = NMS_KEYFILE_STORAGE_TYPE_RUN;
dirname = dirname_run;
} else if (dirname_etc && (filename = nm_utils_file_is_in_path(full_filename, dirname_etc))) {
storage_type = NMS_KEYFILE_STORAGE_TYPE_ETC;
dirname = dirname_etc;
} else {
for (i = 0; dirname_libs && dirname_libs[i]; i++) {
if ((filename = nm_utils_file_is_in_path(full_filename, dirname_libs[i]))) {
storage_type = NMS_KEYFILE_STORAGE_TYPE_LIB(i);
dirname = dirname_libs[i];
break;
}
}
if (!dirname)
return FALSE;
}
if (_ignore_filename(storage_type, filename)) {
/* we accept nmmeta files, but only in /etc and /run directories. */
if (!NM_IN_SET(storage_type, NMS_KEYFILE_STORAGE_TYPE_RUN, NMS_KEYFILE_STORAGE_TYPE_ETC)
|| !nms_keyfile_nmmeta_check_filename(filename, NULL)) {
NM_SET_OUT(out_failed_due_to_invalid_filename, TRUE);
return FALSE;
}
is_nmmeta_file = TRUE;
}
NM_SET_OUT(out_storage_type, storage_type);
NM_SET_OUT(out_dirname, dirname);
NM_SET_OUT(out_filename, filename);
NM_SET_OUT(out_is_nmmeta_file, is_nmmeta_file);
return TRUE;
}
/*****************************************************************************/
static NMConnection *
_read_from_file(const char * full_filename,
const char * plugin_dir,
struct stat *out_stat,
NMTernary * out_is_nm_generated,
NMTernary * out_is_volatile,
NMTernary * out_is_external,
char ** out_shadowed_storage,
NMTernary * out_shadowed_owned,
GError ** error)
{
NMConnection *connection;
nm_assert(full_filename && full_filename[0] == '/');
connection = nms_keyfile_reader_from_file(full_filename,
plugin_dir,
out_stat,
out_is_nm_generated,
out_is_volatile,
out_is_external,
out_shadowed_storage,
out_shadowed_owned,
error);
nm_assert(!connection
|| (_nm_connection_verify(connection, NULL) == NM_SETTING_VERIFY_SUCCESS));
nm_assert(!connection || nm_utils_is_uuid(nm_connection_get_uuid(connection)));
return connection;
}
/*****************************************************************************/
static void
_nm_assert_storage(gpointer plugin /* NMSKeyfilePlugin */,
gpointer storage /* NMSKeyfileStorage */,
gboolean tracked)
{
#if NM_MORE_ASSERTS
NMSettUtilStorageByUuidHead *sbuh;
const char * uuid;
nm_assert(!plugin || NMS_IS_KEYFILE_PLUGIN(plugin));
nm_assert(NMS_IS_KEYFILE_STORAGE(storage));
nm_assert(!plugin || plugin == nm_settings_storage_get_plugin(storage));
nm_assert(({
const char *f = nms_keyfile_storage_get_filename(storage);
f && f[0] == '/';
}));
uuid = nms_keyfile_storage_get_uuid(storage);
nm_assert(nm_utils_is_uuid(uuid));
nm_assert(((NMSKeyfileStorage *) storage)->is_meta_data
|| !(((NMSKeyfileStorage *) storage)->u.conn_data.connection)
|| (NM_IS_CONNECTION((((NMSKeyfileStorage *) storage)->u.conn_data.connection))
&& nm_streq0(uuid,
nm_connection_get_uuid(
(((NMSKeyfileStorage *) storage)->u.conn_data.connection)))));
nm_assert(
!tracked || !plugin
|| c_list_contains(&NMS_KEYFILE_PLUGIN_GET_PRIVATE(plugin)->storages._storage_lst_head,
&NMS_KEYFILE_STORAGE(storage)->parent._storage_lst));
nm_assert(!tracked || !plugin
|| storage
== g_hash_table_lookup(
NMS_KEYFILE_PLUGIN_GET_PRIVATE(plugin)->storages.idx_by_filename,
nms_keyfile_storage_get_filename(storage)));
if (tracked && plugin) {
sbuh = g_hash_table_lookup(NMS_KEYFILE_PLUGIN_GET_PRIVATE(plugin)->storages.idx_by_uuid,
&uuid);
nm_assert(sbuh);
nm_assert(c_list_contains(&sbuh->_storage_by_uuid_lst_head,
&((NMSKeyfileStorage *) storage)->parent._storage_by_uuid_lst));
}
#endif
}
/*****************************************************************************/
static NMSKeyfileStorage *
_load_file(NMSKeyfilePlugin * self,
const char * dirname,
const char * filename,
NMSKeyfileStorageType storage_type,
GError ** error)
{
NMSKeyfilePluginPrivate *priv;
gs_unref_object NMConnection *connection = NULL;
NMTernary is_nm_generated_opt;
NMTernary is_volatile_opt;
NMTernary is_external_opt;
NMTernary shadowed_owned_opt;
gs_free char * shadowed_storage = NULL;
gs_free_error GError *local = NULL;
gs_free char * full_filename = NULL;
struct stat st;
if (_ignore_filename(storage_type, filename)) {
gs_free char *nmmeta = NULL;
gs_free char *loaded_path = NULL;
gs_free char *shadowed_storage_filename = NULL;
if (!nms_keyfile_nmmeta_check_filename(filename, NULL)) {
if (error)
nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "skip due to invalid filename");
else
_LOGT("load: \"%s/%s\": skip file due to invalid filename", dirname, filename);
return NULL;
}
if (!nms_keyfile_nmmeta_read(dirname,
filename,
&full_filename,
&nmmeta,
&loaded_path,
&shadowed_storage_filename,
NULL)) {
if (error)
nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, "skip unreadable nmmeta file");
else
_LOGT("load: \"%s/%s\": skip unreadable nmmeta file", dirname, filename);
return NULL;
}
nm_assert(loaded_path);
if (!NM_IN_SET(storage_type, NMS_KEYFILE_STORAGE_TYPE_RUN, NMS_KEYFILE_STORAGE_TYPE_ETC)) {
if (error)
nm_utils_error_set(error,
NM_UTILS_ERROR_UNKNOWN,
"skip nmmeta file from read-only directory");
else
_LOGT("load: \"%s/%s\": skip nmmeta file from read-only directory",
dirname,
filename);
return NULL;
}
if (!nm_streq(loaded_path, NM_KEYFILE_PATH_NMMETA_SYMLINK_NULL)) {
if (error)
nm_utils_error_set(error,
NM_UTILS_ERROR_UNKNOWN,
"skip nmmeta file not symlinking %s",
NM_KEYFILE_PATH_NMMETA_SYMLINK_NULL);
else
_LOGT("load: \"%s/%s\": skip nmmeta file not symlinking to %s",
dirname,
filename,
NM_KEYFILE_PATH_NMMETA_SYMLINK_NULL);
return NULL;
}
return nms_keyfile_storage_new_tombstone(self,
nmmeta,
full_filename,
storage_type,
shadowed_storage_filename);
}
full_filename = g_build_filename(dirname, filename, NULL);
priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
connection = _read_from_file(full_filename,
_get_plugin_dir(priv),
&st,
&is_nm_generated_opt,
&is_volatile_opt,
&is_external_opt,
&shadowed_storage,
&shadowed_owned_opt,
&local);
if (!connection) {
if (error)
g_propagate_error(error, g_steal_pointer(&local));
else
_LOGW("load: \"%s\": failed to load connection: %s", full_filename, local->message);
return NULL;
}
return nms_keyfile_storage_new_connection(self,
g_steal_pointer(&connection),
full_filename,
storage_type,
is_nm_generated_opt,
is_volatile_opt,
is_external_opt,
shadowed_storage,
shadowed_owned_opt,
&st.st_mtim);
}
static NMSKeyfileStorage *
_load_file_from_path(NMSKeyfilePlugin * self,
const char * full_filename,
NMSKeyfileStorageType storage_type,
GError ** error)
{
gs_free char *f_dirname_free = NULL;
const char * f_filename;
const char * f_dirname;
nm_assert(full_filename && full_filename[0] == '/');
f_filename = strrchr(full_filename, '/');
f_dirname = nm_strndup_a(300, full_filename, f_filename - full_filename, &f_dirname_free);
f_filename++;
return _load_file(self, f_dirname, f_filename, storage_type, error);
}
static void
_load_dir(NMSKeyfilePlugin * self,
NMSKeyfileStorageType storage_type,
const char * dirname,
NMSettUtilStorages * storages)
{
const char * filename;
GDir * dir;
gs_unref_hashtable GHashTable *dupl_filenames = NULL;
dir = g_dir_open(dirname, 0, NULL);
if (!dir)
return;
dupl_filenames = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, g_free);
while ((filename = g_dir_read_name(dir))) {
gs_unref_object NMSKeyfileStorage *storage = NULL;
filename = g_strdup(filename);
if (!g_hash_table_add(dupl_filenames, (char *) filename))
continue;
storage = _load_file(self, dirname, filename, storage_type, NULL);
if (!storage)
continue;
nm_sett_util_storages_add_take(storages, g_steal_pointer(&storage));
}
g_dir_close(dir);
#if NM_MORE_ASSERTS
{
NMSKeyfileStorage *storage;
c_list_for_each_entry (storage, &storages->_storage_lst_head, parent._storage_lst)
nm_assert(NMS_IS_KEYFILE_STORAGE(storage));
}
#endif
}
/*****************************************************************************/
static void
_storages_consolidate(NMSKeyfilePlugin * self,
NMSettUtilStorages * storages_new,
gboolean replace_all,
GHashTable * storages_replaced,
NMSettingsPluginConnectionLoadCallback callback,
gpointer user_data)
{
NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_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;
NMSKeyfileStorage * storage_safe;
NMSKeyfileStorage * storage_new;
NMSKeyfileStorage * storage_old;
NMSKeyfileStorage * storage;
guint i;
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->is_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_keyfile_storage_get_filename(storage_new));
nm_sett_util_storages_steal(storages_new, storage_new);
if (!storage_old
|| !nm_streq(nms_keyfile_storage_get_uuid(storage_new),
nms_keyfile_storage_get_uuid(storage_old))) {
if (storage_old) {
nm_sett_util_storages_steal(&priv->storages, storage_old);
c_list_link_tail(&storages_deleted, &storage_old->parent._storage_by_uuid_lst);
}
storage_new->is_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->is_dirty = FALSE;
nms_keyfile_storage_copy_content(storage_old, storage_new);
nms_keyfile_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->is_dirty)
continue;
if (replace_all
|| (storages_replaced && g_hash_table_contains(storages_replaced, storage_old))) {
nm_sett_util_storages_steal(&priv->storages, storage_old);
c_list_link_tail(&storages_deleted, &storage_old->parent._storage_by_uuid_lst);
}
}
/* raise events. */
for (i = 0; i < storages_modified->len; i++) {
storage = storages_modified->pdata[i];
storage->is_dirty = TRUE;
}
for (i = 0; i < storages_modified->len; i++) {
gs_unref_object NMConnection *connection = NULL;
storage = storages_modified->pdata[i];
if (!storage->is_dirty) {
/* the entry is no longer is_dirty. In the meantime we already emitted
* another signal for it. */
continue;
}
storage->is_dirty = FALSE;
if (c_list_is_empty(&storage->parent._storage_lst)) {
/* 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;
}
nm_assert(
storage
== nm_sett_util_storages_lookup_by_filename(&priv->storages,
nms_keyfile_storage_get_filename(storage)));
connection = nms_keyfile_storage_steal_connection(storage);
callback(NM_SETTINGS_PLUGIN(self), NM_SETTINGS_STORAGE(storage), connection, user_data);
}
while ((storage = c_list_first_entry(&storages_deleted,
NMSKeyfileStorage,
parent._storage_by_uuid_lst))) {
c_list_unlink(&storage->parent._storage_by_uuid_lst);
callback(NM_SETTINGS_PLUGIN(self), NM_SETTINGS_STORAGE(storage), NULL, user_data);
nms_keyfile_storage_destroy(storage);
}
}
static void
reload_connections(NMSettingsPlugin * plugin,
NMSettingsPluginConnectionLoadCallback callback,
gpointer user_data)
{
NMSKeyfilePlugin * self = NMS_KEYFILE_PLUGIN(plugin);
NMSKeyfilePluginPrivate * priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
nm_auto_clear_sett_util_storages NMSettUtilStorages storages_new =
NM_SETT_UTIL_STORAGES_INIT(storages_new, nms_keyfile_storage_destroy);
int i;
_load_dir(self, NMS_KEYFILE_STORAGE_TYPE_RUN, priv->dirname_run, &storages_new);
if (priv->dirname_etc)
_load_dir(self, NMS_KEYFILE_STORAGE_TYPE_ETC, priv->dirname_etc, &storages_new);
for (i = 0; priv->dirname_libs[i]; i++)
_load_dir(self, NMS_KEYFILE_STORAGE_TYPE_LIB(i), priv->dirname_libs[i], &storages_new);
_storages_consolidate(self, &storages_new, TRUE, NULL, callback, user_data);
}
static void
load_connections(NMSettingsPlugin * plugin,
NMSettingsPluginConnectionLoadEntry * entries,
gsize n_entries,
NMSettingsPluginConnectionLoadCallback callback,
gpointer user_data)
{
NMSKeyfilePlugin * self = NMS_KEYFILE_PLUGIN(plugin);
NMSKeyfilePluginPrivate * priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
nm_auto_clear_sett_util_storages NMSettUtilStorages storages_new =
NM_SETT_UTIL_STORAGES_INIT(storages_new, nms_keyfile_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];
NMSKeyfileStorageType storage_type;
gs_free_error GError * local = NULL;
const char * f_filename;
const char * f_dirname;
const char * full_filename;
gs_free char * full_filename_keep = NULL;
gboolean is_nmmeta_file;
NMSettingsPluginConnectionLoadEntry *dupl_content_entry;
gboolean failed_due_to_invalid_filename;
gs_unref_object NMSKeyfileStorage *storage = NULL;
if (entry->handled)
continue;
if (!_path_detect_storage_type(entry->filename,
(const char *const *) priv->dirname_libs,
priv->dirname_etc,
priv->dirname_run,
&storage_type,
&f_dirname,
&f_filename,
&is_nmmeta_file,
&failed_due_to_invalid_filename)) {
if (failed_due_to_invalid_filename) {
entry->handled = TRUE;
nm_utils_error_set(&entry->error,
NM_UTILS_ERROR_UNKNOWN,
"filename is not valid for a keyfile");
}
continue;
}
full_filename_keep = g_build_filename(f_dirname, f_filename, NULL);
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, f_dirname, f_filename, storage_type, &local);
if (!storage) {
if (nm_utils_file_stat(full_filename, NULL) == -ENOENT) {
NMSKeyfileStorage *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;
}
g_hash_table_add(loaded_uuids, (char *) nms_keyfile_storage_get_uuid(storage));
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)) {
NMSKeyfileStorage * 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_keyfile_storage_get_filename(storage);
gs_unref_object NMSKeyfileStorage *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_from_path(self, full_filename, storage->storage_type, &local);
if (storage_new && !nm_streq(loaded_uuid, nms_keyfile_storage_get_uuid(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);
}
gboolean
nms_keyfile_plugin_add_connection(NMSKeyfilePlugin * self,
NMConnection * connection,
gboolean in_memory,
gboolean is_nm_generated,
gboolean is_volatile,
gboolean is_external,
const char * shadowed_storage,
gboolean shadowed_owned,
NMSettingsStorage **out_storage,
NMConnection ** out_connection,
GError ** error)
{
NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
gs_unref_object NMConnection *reread = NULL;
gs_free char * full_filename = NULL;
NMSKeyfileStorageType storage_type;
gs_unref_object NMSKeyfileStorage *storage = NULL;
GError * local = NULL;
const char * uuid;
gboolean reread_same;
struct timespec mtime;
char strbuf[100];
nm_assert(NM_IS_CONNECTION(connection));
nm_assert(out_storage && !*out_storage);
nm_assert(out_connection && !*out_connection);
nm_assert(in_memory
|| (!is_nm_generated && !is_volatile && !is_external && !shadowed_storage
&& !shadowed_owned));
uuid = nm_connection_get_uuid(connection);
/* Note that even if the caller requests persistent storage, we may switch to in-memory, if
* no /etc directory is configured. */
storage_type = !in_memory && priv->dirname_etc ? NMS_KEYFILE_STORAGE_TYPE_ETC
: NMS_KEYFILE_STORAGE_TYPE_RUN;
if (!nms_keyfile_writer_connection(
connection,
is_nm_generated,
is_volatile,
is_external,
shadowed_storage,
shadowed_owned,
storage_type == NMS_KEYFILE_STORAGE_TYPE_ETC ? priv->dirname_etc : priv->dirname_run,
_get_plugin_dir(priv),
NULL,
FALSE,
FALSE,
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(_nm_connection_verify(reread, NULL) == NM_SETTING_VERIFY_SUCCESS);
nm_assert(nm_streq0(nm_connection_get_uuid(connection), nm_connection_get_uuid(reread)));
nm_assert(full_filename && full_filename[0] == '/');
nm_assert(!nm_sett_util_storages_lookup_by_filename(&priv->storages, full_filename));
_LOGT("commit: %s (%s) added as \"%s\"%s%s%s%s",
uuid,
nm_connection_get_id(connection),
full_filename,
_extra_flags_to_string(strbuf, sizeof(strbuf), is_nm_generated, is_volatile, is_external),
NM_PRINT_FMT_QUOTED(shadowed_storage,
" (shadows \"",
shadowed_storage,
shadowed_owned ? "\", owned)" : "\")",
""));
storage =
nms_keyfile_storage_new_connection(self,
g_steal_pointer(&reread),
full_filename,
storage_type,
is_nm_generated ? NM_TERNARY_TRUE : NM_TERNARY_FALSE,
is_volatile ? NM_TERNARY_TRUE : NM_TERNARY_FALSE,
is_external ? NM_TERNARY_TRUE : NM_TERNARY_FALSE,
shadowed_storage,
shadowed_owned ? NM_TERNARY_TRUE : NM_TERNARY_FALSE,
nm_sett_util_stat_mtime(full_filename, FALSE, &mtime));
nm_sett_util_storages_add_take(&priv->storages, g_object_ref(storage));
*out_connection = nms_keyfile_storage_steal_connection(storage);
*out_storage = NM_SETTINGS_STORAGE(g_steal_pointer(&storage));
return TRUE;
}
static gboolean
add_connection(NMSettingsPlugin * plugin,
NMConnection * connection,
NMSettingsStorage **out_storage,
NMConnection ** out_connection,
GError ** error)
{
return nms_keyfile_plugin_add_connection(NMS_KEYFILE_PLUGIN(plugin),
connection,
FALSE,
FALSE,
FALSE,
FALSE,
NULL,
FALSE,
out_storage,
out_connection,
error);
}
gboolean
nms_keyfile_plugin_update_connection(NMSKeyfilePlugin * self,
NMSettingsStorage * storage_x,
NMConnection * connection,
gboolean is_nm_generated,
gboolean is_volatile,
gboolean is_external,
const char * shadowed_storage,
gboolean shadowed_owned,
gboolean force_rename,
NMSettingsStorage **out_storage,
NMConnection ** out_connection,
GError ** error)
{
NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
NMSKeyfileStorage * storage = NMS_KEYFILE_STORAGE(storage_x);
gs_unref_object NMConnection *connection_clone = NULL;
gs_unref_object NMConnection *reread = NULL;
gs_free char * full_filename = NULL;
gs_free_error GError *local = NULL;
struct timespec mtime;
const char * previous_filename;
gboolean reread_same;
const char * uuid;
char strbuf[100];
_nm_assert_storage(self, storage, TRUE);
nm_assert(NM_IS_CONNECTION(connection));
nm_assert(_nm_connection_verify(connection, NULL) == NM_SETTING_VERIFY_SUCCESS);
nm_assert(nm_streq(nms_keyfile_storage_get_uuid(storage), nm_connection_get_uuid(connection)));
nm_assert(!error || !*error);
nm_assert(NM_IN_SET(storage->storage_type,
NMS_KEYFILE_STORAGE_TYPE_ETC,
NMS_KEYFILE_STORAGE_TYPE_RUN));
nm_assert(!storage->is_meta_data);
nm_assert(storage->storage_type == NMS_KEYFILE_STORAGE_TYPE_RUN
|| (!is_nm_generated && !is_volatile && !is_external && !shadowed_storage
&& !shadowed_owned));
nm_assert(!shadowed_owned || shadowed_storage);
nm_assert(priv->dirname_etc || storage->storage_type != NMS_KEYFILE_STORAGE_TYPE_ETC);
previous_filename = nms_keyfile_storage_get_filename(storage);
uuid = nms_keyfile_storage_get_uuid(storage);
if (!nms_keyfile_writer_connection(
connection,
is_nm_generated,
is_volatile,
is_external,
shadowed_storage,
shadowed_owned,
storage->storage_type == NMS_KEYFILE_STORAGE_TYPE_ETC ? priv->dirname_etc
: priv->dirname_run,
_get_plugin_dir(priv),
previous_filename,
FALSE,
FALSE,
nm_sett_util_allow_filename_cb,
NM_SETT_UTIL_ALLOW_FILENAME_DATA(&priv->storages, previous_filename),
&full_filename,
&reread,
&reread_same,
&local)) {
_LOGW("commit: failure to write %s (%s) to \"%s\": %s",
uuid,
nm_connection_get_id(connection_clone),
previous_filename,
local->message);
g_propagate_error(error, g_steal_pointer(&local));
return FALSE;
}
nm_assert(full_filename && nm_streq(full_filename, previous_filename));
if (!reread || reread_same)
nm_g_object_ref_set(&reread, connection);
nm_assert(_nm_connection_verify(reread, NULL) == NM_SETTING_VERIFY_SUCCESS);
nm_assert(nm_streq(nm_connection_get_uuid(reread), uuid));
_LOGT("commit: \"%s\": profile %s (%s) written%s%s%s%s",
full_filename,
uuid,
nm_connection_get_id(connection),
_extra_flags_to_string(strbuf, sizeof(strbuf), is_nm_generated, is_volatile, is_external),
NM_PRINT_FMT_QUOTED(shadowed_storage,
shadowed_owned ? " (owns \"" : " (shadows \"",
shadowed_storage,
"\")",
""));
storage->u.conn_data.is_nm_generated = is_nm_generated;
storage->u.conn_data.is_volatile = is_volatile;
storage->u.conn_data.is_external = is_external;
storage->u.conn_data.stat_mtime = *nm_sett_util_stat_mtime(full_filename, FALSE, &mtime);
storage->u.conn_data.shadowed_owned = shadowed_owned;
*out_storage = g_object_ref(NM_SETTINGS_STORAGE(storage));
*out_connection = g_steal_pointer(&reread);
return TRUE;
}
static gboolean
update_connection(NMSettingsPlugin * plugin,
NMSettingsStorage * storage,
NMConnection * connection,
NMSettingsStorage **out_storage,
NMConnection ** out_connection,
GError ** error)
{
return nms_keyfile_plugin_update_connection(NMS_KEYFILE_PLUGIN(plugin),
storage,
connection,
FALSE,
FALSE,
FALSE,
NULL,
FALSE,
FALSE,
out_storage,
out_connection,
error);
}
static gboolean
delete_connection(NMSettingsPlugin *plugin, NMSettingsStorage *storage_x, GError **error)
{
NMSKeyfilePlugin * self = NMS_KEYFILE_PLUGIN(plugin);
NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
gs_unref_object NMSKeyfileStorage *storage = g_object_ref(NMS_KEYFILE_STORAGE(storage_x));
const char * remove_from_disk_errmsg = NULL;
const char * operation_message;
const char * previous_filename;
const char * uuid;
gboolean success = TRUE;
_nm_assert_storage(self, storage, TRUE);
nm_assert(!error || !*error);
previous_filename = nms_keyfile_storage_get_filename(storage);
uuid = nms_keyfile_storage_get_uuid(storage);
if (!NM_IN_SET(storage->storage_type,
NMS_KEYFILE_STORAGE_TYPE_ETC,
NMS_KEYFILE_STORAGE_TYPE_RUN)) {
nm_utils_error_set(error,
NM_UTILS_ERROR_UNKNOWN,
"profile in read-only storage cannot be deleted");
success = FALSE;
operation_message = "dropped readonly file from memory";
} else if (unlink(previous_filename) != 0) {
int errsv;
errsv = errno;
if (errsv != ENOENT) {
remove_from_disk_errmsg = nm_strerror_native(errsv);
operation_message = "failed to delete from disk";
success = FALSE;
nm_utils_error_set_errno(error,
errsv,
"failure to delete \"%s\": %s",
previous_filename);
} else
operation_message = "does not exist on disk";
} else
operation_message = "deleted from disk";
_LOGT("commit: deleted \"%s\", %s %s (%s%s%s%s)",
previous_filename,
storage->is_meta_data ? "meta-data" : "profile",
uuid,
operation_message,
NM_PRINT_FMT_QUOTED(remove_from_disk_errmsg, ": ", remove_from_disk_errmsg, "", ""));
if (success) {
nm_sett_util_storages_steal(&priv->storages, storage);
nms_keyfile_storage_destroy(storage);
}
return success;
}
/**
* nms_keyfile_plugin_set_nmmeta_tombstone:
* @self: the #NMSKeyfilePlugin instance
* @simulate: if %TRUE, don't do anything on the filename but just pretend
* that the loaded UUID file gets tracked/untracked. In this mode, the function
* cannot fail (except on hard-failure, see below).
* The idea is that you first try without simulate to write to disk.
* If that fails, you might still want to forcefully pretend (in-memory
* only) that this uuid is marked as tombstone (or not), as desired.
* So you repeate the call with @simulate %TRUE.
* @uuid: the UUID for which to write/delete the nmmeta file
* @in_memory: the storage type, either /etc or /run. Note that if @self
* has no /etc directory configured, this results in a hard failure.
* @set: if %TRUE, write the symlink to point to /dev/null. If %FALSE,
* delete the nmmeta file (if it exists).
* @shadowed_storage: a tombstone can also shadow an existing storage.
* In combination with @set and @in_memory, this is allowed to store
* the shadowed storage filename.
* @out_storage: (transfer full) (allow-none): the storage element that changes, or
* NULL if nothing changed. Note that the file on disk is already as
* we want to write it, then this still counts as a change. No change only
* means if we try to delete a storage (@set %FALSE) that did not
* exist previously.
* @out_hard_failure: (allow-none): on failure, indicate that this is a hard failure.
*
* The function writes or deletes nmmeta files to/from filesystem. In this case,
* the nmmeta files can only be symlinks to /dev/null (to indicate tombstones).
*
* A hard failure can only happen if @self has no /etc directory configured
* and @in_memory is FALSE. In such case even @simulate call fails (which
* otherwise would always succeed).
* Also, if you get a hard-failure (with @simulate %FALSE) there is no point
* in retrying with @simulate %TRUE (contrary to all other cases!).
*
* Returns: %TRUE on success.
*/
gboolean
nms_keyfile_plugin_set_nmmeta_tombstone(NMSKeyfilePlugin * self,
gboolean simulate,
const char * uuid,
gboolean in_memory,
gboolean set,
const char * shadowed_storage,
NMSettingsStorage **out_storage,
gboolean * out_hard_failure)
{
NMSKeyfilePluginPrivate *priv;
gboolean hard_failure = FALSE;
NMSKeyfileStorage * storage;
gs_unref_object NMSKeyfileStorage *storage_result = NULL;
gboolean nmmeta_errno;
gs_free char * nmmeta_filename = NULL;
NMSKeyfileStorageType storage_type;
const char * loaded_path;
const char * dirname;
nm_assert(NMS_IS_KEYFILE_PLUGIN(self));
nm_assert(nm_utils_is_uuid(uuid));
nm_assert(!out_storage || !*out_storage);
nm_assert(!shadowed_storage || (set && in_memory));
priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
loaded_path = set ? NM_KEYFILE_PATH_NMMETA_SYMLINK_NULL : NULL;
if (in_memory) {
storage_type = NMS_KEYFILE_STORAGE_TYPE_RUN;
dirname = priv->dirname_run;
} else {
if (!priv->dirname_etc) {
_LOGT("commit: cannot %s%s nmmeta file for %s as there is no /etc directory",
simulate ? "simulate " : "",
loaded_path ? "write" : "delete",
uuid);
nmmeta_errno = 0;
hard_failure = TRUE;
goto out;
}
storage_type = NMS_KEYFILE_STORAGE_TYPE_ETC;
dirname = priv->dirname_etc;
}
if (simulate) {
nmmeta_errno = 0;
nmmeta_filename = nms_keyfile_nmmeta_filename(dirname, uuid, FALSE);
} else {
nmmeta_errno = nms_keyfile_nmmeta_write(dirname,
uuid,
loaded_path,
FALSE,
shadowed_storage,
&nmmeta_filename);
}
_LOGT("commit: %s nmmeta file \"%s\"%s%s%s%s%s%s %s%s%s%s",
loaded_path ? "writing" : "deleting",
nmmeta_filename,
NM_PRINT_FMT_QUOTED(loaded_path, " (pointing to \"", loaded_path, "\")", ""),
NM_PRINT_FMT_QUOTED(shadowed_storage, " (shadows \"", shadowed_storage, "\")", ""),
simulate ? "simulated" : (nmmeta_errno < 0 ? "failed" : "succeeded"),
NM_PRINT_FMT_QUOTED(nmmeta_errno < 0,
" (",
nm_strerror_native(nm_errno_native(nmmeta_errno)),
")",
""));
if (nmmeta_errno < 0)
goto out;
storage = nm_sett_util_storages_lookup_by_filename(&priv->storages, nmmeta_filename);
nm_assert(!storage
|| (storage->is_meta_data && storage->storage_type == storage_type
&& nm_streq(nms_keyfile_storage_get_uuid(storage), uuid)));
if (loaded_path) {
if (!storage) {
storage = nms_keyfile_storage_new_tombstone(self,
uuid,
nmmeta_filename,
storage_type,
shadowed_storage);
nm_sett_util_storages_add_take(&priv->storages, storage);
} else {
g_free(storage->u.meta_data.shadowed_storage);
storage->u.meta_data.shadowed_storage = g_strdup(shadowed_storage);
}
storage_result = g_object_ref(storage);
} else {
if (storage)
storage_result = nm_sett_util_storages_steal(&priv->storages, storage);
}
out:
nm_assert(nmmeta_errno <= 0);
nm_assert(nmmeta_errno < 0 || !hard_failure);
nm_assert(nmmeta_errno == 0 || !storage_result);
NM_SET_OUT(out_hard_failure, hard_failure);
NM_SET_OUT(out_storage, (NMSettingsStorage *) g_steal_pointer(&storage_result));
return nmmeta_errno >= 0;
}
/*****************************************************************************/
static void
config_changed_cb(NMConfig * config,
NMConfigData * config_data,
NMConfigChangeFlags changes,
NMConfigData * old_data,
NMSKeyfilePlugin * self)
{
gs_free char *old_value = NULL;
gs_free char *new_value = NULL;
old_value = nm_config_data_get_value(old_data,
NM_CONFIG_KEYFILE_GROUP_KEYFILE,
NM_CONFIG_KEYFILE_KEY_KEYFILE_UNMANAGED_DEVICES,
NM_CONFIG_GET_VALUE_TYPE_SPEC);
new_value = nm_config_data_get_value(config_data,
NM_CONFIG_KEYFILE_GROUP_KEYFILE,
NM_CONFIG_KEYFILE_KEY_KEYFILE_UNMANAGED_DEVICES,
NM_CONFIG_GET_VALUE_TYPE_SPEC);
if (!nm_streq0(old_value, new_value))
_nm_settings_plugin_emit_signal_unmanaged_specs_changed(NM_SETTINGS_PLUGIN(self));
}
static GSList *
get_unmanaged_specs(NMSettingsPlugin *config)
{
NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(config);
gs_free char * value = NULL;
value = nm_config_data_get_value(nm_config_get_data(priv->config),
NM_CONFIG_KEYFILE_GROUP_KEYFILE,
NM_CONFIG_KEYFILE_KEY_KEYFILE_UNMANAGED_DEVICES,
NM_CONFIG_GET_VALUE_TYPE_SPEC);
return nm_match_spec_split(value);
}
/*****************************************************************************/
static void
nms_keyfile_plugin_init(NMSKeyfilePlugin *plugin)
{
NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(plugin);
priv->config = g_object_ref(nm_config_get());
priv->storages = (NMSettUtilStorages) NM_SETT_UTIL_STORAGES_INIT(priv->storages,
nms_keyfile_storage_destroy);
/* dirname_libs are a set of read-only directories with lower priority than /etc or /run.
* There is nothing complicated about having multiple of such directories, so dirname_libs
* is a list (which currently only has at most one directory). */
priv->dirname_libs[0] = nm_sd_utils_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_LIB), FALSE);
priv->dirname_libs[1] = NULL;
priv->dirname_run = nm_sd_utils_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_RUN), FALSE);
priv->dirname_etc = nm_config_data_get_value(NM_CONFIG_GET_DATA_ORIG,
NM_CONFIG_KEYFILE_GROUP_KEYFILE,
NM_CONFIG_KEYFILE_KEY_KEYFILE_PATH,
NM_CONFIG_GET_VALUE_STRIP);
if (priv->dirname_etc && priv->dirname_etc[0] == '\0') {
/* special case: configure an empty keyfile path so that NM has no writable keyfile
* directory. In this case, NM will only honor dirname_libs and dirname_run, meaning
* it cannot persist profile to non-volatile memory. */
nm_clear_g_free(&priv->dirname_etc);
} else if (!priv->dirname_etc || priv->dirname_etc[0] != '/') {
/* either invalid path or unspecified. Use the default. */
g_free(priv->dirname_etc);
priv->dirname_etc =
nm_sd_utils_path_simplify(g_strdup(NM_KEYFILE_PATH_NAME_ETC_DEFAULT), FALSE);
} else
nm_sd_utils_path_simplify(priv->dirname_etc, FALSE);
/* no duplicates */
if (NM_IN_STRSET(priv->dirname_libs[0], priv->dirname_etc, priv->dirname_run))
nm_clear_g_free(&priv->dirname_libs[0]);
if (NM_IN_STRSET(priv->dirname_etc, priv->dirname_run))
nm_clear_g_free(&priv->dirname_etc);
nm_assert(!priv->dirname_libs[0] || priv->dirname_libs[0][0] == '/');
nm_assert(!priv->dirname_etc || priv->dirname_etc[0] == '/');
nm_assert(priv->dirname_run && priv->dirname_run[0] == '/');
}
static void
constructed(GObject *object)
{
NMSKeyfilePlugin * self = NMS_KEYFILE_PLUGIN(object);
NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
G_OBJECT_CLASS(nms_keyfile_plugin_parent_class)->constructed(object);
if (nm_config_data_has_value(nm_config_get_data_orig(priv->config),
NM_CONFIG_KEYFILE_GROUP_KEYFILE,
NM_CONFIG_KEYFILE_KEY_KEYFILE_HOSTNAME,
NM_CONFIG_GET_VALUE_RAW))
_LOGW("'hostname' option is deprecated and has no effect");
if (nm_config_data_has_value(nm_config_get_data_orig(priv->config),
NM_CONFIG_KEYFILE_GROUP_MAIN,
NM_CONFIG_KEYFILE_KEY_MAIN_MONITOR_CONNECTION_FILES,
NM_CONFIG_GET_VALUE_RAW))
_LOGW("'monitor-connection-files' option is deprecated and has no effect");
g_signal_connect(G_OBJECT(priv->config),
NM_CONFIG_SIGNAL_CONFIG_CHANGED,
G_CALLBACK(config_changed_cb),
self);
}
NMSKeyfilePlugin *
nms_keyfile_plugin_new(void)
{
return g_object_new(NMS_TYPE_KEYFILE_PLUGIN, NULL);
}
static void
dispose(GObject *object)
{
NMSKeyfilePlugin * self = NMS_KEYFILE_PLUGIN(object);
NMSKeyfilePluginPrivate *priv = NMS_KEYFILE_PLUGIN_GET_PRIVATE(self);
if (priv->config)
g_signal_handlers_disconnect_by_func(priv->config, config_changed_cb, object);
nm_sett_util_storages_clear(&priv->storages);
nm_clear_g_free(&priv->dirname_libs[0]);
nm_clear_g_free(&priv->dirname_etc);
nm_clear_g_free(&priv->dirname_run);
g_clear_object(&priv->config);
G_OBJECT_CLASS(nms_keyfile_plugin_parent_class)->dispose(object);
}
static void
nms_keyfile_plugin_class_init(NMSKeyfilePluginClass *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 = "keyfile";
plugin_class->get_unmanaged_specs = get_unmanaged_specs;
plugin_class->reload_connections = reload_connections;
plugin_class->load_connections = load_connections;
plugin_class->add_connection = add_connection;
plugin_class->update_connection = update_connection;
plugin_class->delete_connection = delete_connection;
}