about summary refs log tree commit diff
path: root/src/nmcli/settings.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
committerMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
commite74c568b07b50b97873fb4ee1d776dedefbd54d6 (patch)
tree3469f17ea9af91f7ff169b890633bda68b0cf76e /src/nmcli/settings.c
parentbfe522304da217296e2a61040f58e35ec5d6f3f2 (diff)
New upstream version 1.32.12 upstream/1.32.12
Diffstat (limited to 'src/nmcli/settings.c')
-rw-r--r--src/nmcli/settings.c762
1 files changed, 762 insertions, 0 deletions
diff --git a/src/nmcli/settings.c b/src/nmcli/settings.c
new file mode 100644
index 00000000..6c93c021
--- /dev/null
+++ b/src/nmcli/settings.c
@@ -0,0 +1,762 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2010 - 2015 Red Hat, Inc.
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "settings.h"
+
+#include <stdlib.h>
+#include <arpa/inet.h>
+
+#include "libnm-core-aux-intern/nm-common-macros.h"
+
+#include "libnmc-base/nm-client-utils.h"
+#include "libnmc-base/nm-vpn-helpers.h"
+#include "libnmc-setting/nm-meta-setting-access.h"
+
+#include "utils.h"
+#include "common.h"
+
+/*****************************************************************************/
+
+static gboolean
+get_answer(const char *prop, const char *value)
+{
+    char *   tmp_str;
+    char *   question;
+    gboolean answer = FALSE;
+
+    if (value)
+        question = g_strdup_printf(_("Do you also want to set '%s' to '%s'? [yes]: "), prop, value);
+    else
+        question = g_strdup_printf(_("Do you also want to clear '%s'? [yes]: "), prop);
+    tmp_str = nmc_get_user_input(question);
+    if (!tmp_str || matches(tmp_str, "yes"))
+        answer = TRUE;
+    g_free(tmp_str);
+    g_free(question);
+    return answer;
+}
+
+static void ipv4_method_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data);
+static void ipv6_method_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data);
+
+static void
+ipv4_addresses_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    static gboolean answered = FALSE;
+    static gboolean answer   = FALSE;
+
+    g_signal_handlers_block_by_func(object, G_CALLBACK(ipv4_method_changed_cb), NULL);
+
+    /* If we have some IP addresses set method to 'manual'.
+     * Else if the method was 'manual', change it back to 'auto'.
+     */
+    if (nm_setting_ip_config_get_num_addresses(NM_SETTING_IP_CONFIG(object))) {
+        if (g_strcmp0(nm_setting_ip_config_get_method(NM_SETTING_IP_CONFIG(object)),
+                      NM_SETTING_IP4_CONFIG_METHOD_MANUAL)) {
+            if (!answered) {
+                answered = TRUE;
+                answer   = get_answer("ipv4.method", "manual");
+            }
+            if (answer)
+                g_object_set(object,
+                             NM_SETTING_IP_CONFIG_METHOD,
+                             NM_SETTING_IP4_CONFIG_METHOD_MANUAL,
+                             NULL);
+        }
+    } else {
+        answered = FALSE;
+        if (!g_strcmp0(nm_setting_ip_config_get_method(NM_SETTING_IP_CONFIG(object)),
+                       NM_SETTING_IP4_CONFIG_METHOD_MANUAL))
+            g_object_set(object,
+                         NM_SETTING_IP_CONFIG_METHOD,
+                         NM_SETTING_IP4_CONFIG_METHOD_AUTO,
+                         NULL);
+    }
+
+    g_signal_handlers_unblock_by_func(object, G_CALLBACK(ipv4_method_changed_cb), NULL);
+}
+
+static void
+ipv4_method_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    static GPtrArray *old_value = NULL;
+    static gboolean   answered  = FALSE;
+    static gboolean   answer    = FALSE;
+
+    g_signal_handlers_block_by_func(object, G_CALLBACK(ipv4_addresses_changed_cb), NULL);
+
+    /* If method != manual, remove addresses (save them for restoring them later when method becomes 'manual' */
+    if (g_strcmp0(nm_setting_ip_config_get_method(NM_SETTING_IP_CONFIG(object)),
+                  NM_SETTING_IP4_CONFIG_METHOD_MANUAL)) {
+        if (nm_setting_ip_config_get_num_addresses(NM_SETTING_IP_CONFIG(object))) {
+            if (!answered) {
+                answered = TRUE;
+                answer   = get_answer("ipv4.addresses", NULL);
+            }
+            if (answer) {
+                nm_clear_pointer(&old_value, g_ptr_array_unref);
+                g_object_get(object, NM_SETTING_IP_CONFIG_ADDRESSES, &old_value, NULL);
+                g_object_set(object, NM_SETTING_IP_CONFIG_ADDRESSES, NULL, NULL);
+            }
+        }
+    } else {
+        answered = FALSE;
+        if (old_value) {
+            gs_unref_ptrarray GPtrArray *v = g_steal_pointer(&old_value);
+
+            g_object_set(object, NM_SETTING_IP_CONFIG_ADDRESSES, v, NULL);
+        }
+    }
+
+    g_signal_handlers_unblock_by_func(object, G_CALLBACK(ipv4_addresses_changed_cb), NULL);
+}
+
+static void
+ipv6_addresses_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    static gboolean answered = FALSE;
+    static gboolean answer   = FALSE;
+
+    g_signal_handlers_block_by_func(object, G_CALLBACK(ipv6_method_changed_cb), NULL);
+
+    /* If we have some IP addresses set method to 'manual'.
+     * Else if the method was 'manual', change it back to 'auto'.
+     */
+    if (nm_setting_ip_config_get_num_addresses(NM_SETTING_IP_CONFIG(object))) {
+        if (g_strcmp0(nm_setting_ip_config_get_method(NM_SETTING_IP_CONFIG(object)),
+                      NM_SETTING_IP6_CONFIG_METHOD_MANUAL)) {
+            if (!answered) {
+                answered = TRUE;
+                answer   = get_answer("ipv6.method", "manual");
+            }
+            if (answer)
+                g_object_set(object,
+                             NM_SETTING_IP_CONFIG_METHOD,
+                             NM_SETTING_IP6_CONFIG_METHOD_MANUAL,
+                             NULL);
+        }
+    } else {
+        answered = FALSE;
+        /* FIXME: editor_init_existing_connection() and registering handlers is not the
+         *  right approach.
+         *
+         * This only happens to work because in nmcli's edit mode
+         * tends to append addresses -- instead of setting them.
+         * If we would change that (to behavior I'd expect), we'd get:
+         *
+         *   nmcli> set ipv6.addresses fc01::1:5/68
+         *   Do you also want to set 'ipv6.method' to 'manual'? [yes]: y
+         *   nmcli> set ipv6.addresses fc01::1:6/68
+         *   Do you also want to set 'ipv6.method' to 'manual'? [yes]:
+         *
+         * That's because nmc_setting_set_property() calls set_fcn(). With modifier '\0'
+         * (set), it would first clear all addresses before adding the address. Thereby
+         * emitting multiple property changed signals.
+         *
+         * That can be avoided by freezing/thawing the signals, but this solution
+         * here is ugly in general.
+         */
+        if (!g_strcmp0(nm_setting_ip_config_get_method(NM_SETTING_IP_CONFIG(object)),
+                       NM_SETTING_IP6_CONFIG_METHOD_MANUAL))
+            g_object_set(object,
+                         NM_SETTING_IP_CONFIG_METHOD,
+                         NM_SETTING_IP6_CONFIG_METHOD_AUTO,
+                         NULL);
+    }
+
+    g_signal_handlers_unblock_by_func(object, G_CALLBACK(ipv6_method_changed_cb), NULL);
+}
+
+static void
+ipv6_method_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    static GPtrArray *old_value = NULL;
+    static gboolean   answered  = FALSE;
+    static gboolean   answer    = FALSE;
+
+    g_signal_handlers_block_by_func(object, G_CALLBACK(ipv6_addresses_changed_cb), NULL);
+
+    /* If method != manual, remove addresses (save them for restoring them later when method becomes 'manual' */
+    if (g_strcmp0(nm_setting_ip_config_get_method(NM_SETTING_IP_CONFIG(object)),
+                  NM_SETTING_IP6_CONFIG_METHOD_MANUAL)) {
+        if (nm_setting_ip_config_get_num_addresses(NM_SETTING_IP_CONFIG(object))) {
+            if (!answered) {
+                answered = TRUE;
+                answer   = get_answer("ipv6.addresses", NULL);
+            }
+            if (answer) {
+                nm_clear_pointer(&old_value, g_ptr_array_unref);
+                g_object_get(object, NM_SETTING_IP_CONFIG_ADDRESSES, &old_value, NULL);
+                g_object_set(object, NM_SETTING_IP_CONFIG_ADDRESSES, NULL, NULL);
+            }
+        }
+    } else {
+        answered = FALSE;
+        if (old_value) {
+            gs_unref_ptrarray GPtrArray *v = g_steal_pointer(&old_value);
+
+            g_object_set(object, NM_SETTING_IP_CONFIG_ADDRESSES, v, NULL);
+        }
+    }
+
+    g_signal_handlers_unblock_by_func(object, G_CALLBACK(ipv6_addresses_changed_cb), NULL);
+}
+
+static void
+proxy_method_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    NMSettingProxyMethod method;
+
+    method = nm_setting_proxy_get_method(NM_SETTING_PROXY(object));
+
+    if (method == NM_SETTING_PROXY_METHOD_NONE) {
+        g_object_set(object,
+                     NM_SETTING_PROXY_PAC_URL,
+                     NULL,
+                     NM_SETTING_PROXY_PAC_SCRIPT,
+                     NULL,
+                     NULL);
+    }
+}
+
+static void
+wireless_band_channel_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    const char *       value = NULL, *mode;
+    char               str[16];
+    NMSettingWireless *s_wireless = NM_SETTING_WIRELESS(object);
+
+    if (strcmp(g_param_spec_get_name(pspec), NM_SETTING_WIRELESS_BAND) == 0) {
+        value = nm_setting_wireless_get_band(s_wireless);
+        if (!value)
+            return;
+    } else {
+        guint32 channel = nm_setting_wireless_get_channel(s_wireless);
+
+        if (channel == 0)
+            return;
+
+        g_snprintf(str, sizeof(str), "%d", nm_setting_wireless_get_channel(s_wireless));
+        value = str;
+    }
+
+    mode = nm_setting_wireless_get_mode(NM_SETTING_WIRELESS(object));
+    if (!mode || !*mode || strcmp(mode, NM_SETTING_WIRELESS_MODE_INFRA) == 0) {
+        g_print(_("Warning: %s.%s set to '%s', but it might be ignored in infrastructure mode\n"),
+                nm_setting_get_name(NM_SETTING(s_wireless)),
+                g_param_spec_get_name(pspec),
+                value);
+    }
+}
+
+static void
+connection_master_changed_cb(GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+    NMSettingConnection *s_con      = NM_SETTING_CONNECTION(object);
+    NMConnection *       connection = NM_CONNECTION(user_data);
+    NMSetting *          s_ipv4, *s_ipv6;
+    const char *         value, *tmp_str;
+
+    value = nm_setting_connection_get_master(s_con);
+    if (value) {
+        s_ipv4 = nm_connection_get_setting_by_name(connection, NM_SETTING_IP4_CONFIG_SETTING_NAME);
+        s_ipv6 = nm_connection_get_setting_by_name(connection, NM_SETTING_IP6_CONFIG_SETTING_NAME);
+        if (s_ipv4 || s_ipv6) {
+            g_print(_("Warning: setting %s.%s requires removing ipv4 and ipv6 settings\n"),
+                    nm_setting_get_name(NM_SETTING(s_con)),
+                    g_param_spec_get_name(pspec));
+            tmp_str = nmc_get_user_input(_("Do you want to remove them? [yes] "));
+            if (!tmp_str || matches(tmp_str, "yes")) {
+                if (s_ipv4)
+                    nm_connection_remove_setting(connection, G_OBJECT_TYPE(s_ipv4));
+                if (s_ipv6)
+                    nm_connection_remove_setting(connection, G_OBJECT_TYPE(s_ipv6));
+            }
+        }
+    }
+}
+
+void
+nmc_setting_ip4_connect_handlers(NMSettingIPConfig *setting)
+{
+    g_return_if_fail(NM_IS_SETTING_IP4_CONFIG(setting));
+
+    g_signal_connect(setting,
+                     "notify::" NM_SETTING_IP_CONFIG_ADDRESSES,
+                     G_CALLBACK(ipv4_addresses_changed_cb),
+                     NULL);
+    g_signal_connect(setting,
+                     "notify::" NM_SETTING_IP_CONFIG_METHOD,
+                     G_CALLBACK(ipv4_method_changed_cb),
+                     NULL);
+}
+
+void
+nmc_setting_ip6_connect_handlers(NMSettingIPConfig *setting)
+{
+    g_return_if_fail(NM_IS_SETTING_IP6_CONFIG(setting));
+
+    g_signal_connect(setting,
+                     "notify::" NM_SETTING_IP_CONFIG_ADDRESSES,
+                     G_CALLBACK(ipv6_addresses_changed_cb),
+                     NULL);
+    g_signal_connect(setting,
+                     "notify::" NM_SETTING_IP_CONFIG_METHOD,
+                     G_CALLBACK(ipv6_method_changed_cb),
+                     NULL);
+}
+
+void
+nmc_setting_proxy_connect_handlers(NMSettingProxy *setting)
+{
+    g_return_if_fail(NM_IS_SETTING_PROXY(setting));
+
+    g_signal_connect(setting,
+                     "notify::" NM_SETTING_PROXY_METHOD,
+                     G_CALLBACK(proxy_method_changed_cb),
+                     NULL);
+}
+
+void
+nmc_setting_wireless_connect_handlers(NMSettingWireless *setting)
+{
+    g_return_if_fail(NM_IS_SETTING_WIRELESS(setting));
+
+    g_signal_connect(setting,
+                     "notify::" NM_SETTING_WIRELESS_BAND,
+                     G_CALLBACK(wireless_band_channel_changed_cb),
+                     NULL);
+    g_signal_connect(setting,
+                     "notify::" NM_SETTING_WIRELESS_CHANNEL,
+                     G_CALLBACK(wireless_band_channel_changed_cb),
+                     NULL);
+}
+
+void
+nmc_setting_connection_connect_handlers(NMSettingConnection *setting, NMConnection *connection)
+{
+    g_return_if_fail(NM_IS_SETTING_CONNECTION(setting));
+
+    g_signal_connect(setting,
+                     "notify::" NM_SETTING_CONNECTION_MASTER,
+                     G_CALLBACK(connection_master_changed_cb),
+                     connection);
+}
+
+/*****************************************************************************/
+
+static gboolean
+_set_fcn_precheck_connection_secondaries(NMClient *  client,
+                                         const char *value,
+                                         char **     value_coerced,
+                                         GError **   error)
+{
+    const GPtrArray *    connections;
+    NMConnection *       con;
+    gs_free const char **strv0 = NULL;
+    gs_strfreev char **  strv  = NULL;
+    char **              iter;
+    gboolean             modified = FALSE;
+
+    strv0 = nm_utils_strsplit_set(value, " \t,");
+    if (!strv0)
+        return TRUE;
+
+    connections = nm_client_get_connections(client);
+
+    strv = g_strdupv((char **) strv0);
+    for (iter = strv; *iter; iter++) {
+        if (nm_utils_is_uuid(*iter)) {
+            con = nmc_find_connection(connections, "uuid", *iter, NULL, FALSE);
+            if (!con) {
+                g_print(_("Warning: %s is not an UUID of any existing connection profile\n"),
+                        *iter);
+            } else {
+                /* Currently, NM only supports VPN connections as secondaries */
+                if (!nm_connection_is_type(con, NM_SETTING_VPN_SETTING_NAME)) {
+                    g_set_error(error, 1, 0, _("'%s' is not a VPN connection profile"), *iter);
+                    return FALSE;
+                }
+            }
+        } else {
+            con = nmc_find_connection(connections, "id", *iter, NULL, FALSE);
+            if (!con) {
+                g_set_error(error, 1, 0, _("'%s' is not a name of any exiting profile"), *iter);
+                return FALSE;
+            }
+
+            /* Currently, NM only supports VPN connections as secondaries */
+            if (!nm_connection_is_type(con, NM_SETTING_VPN_SETTING_NAME)) {
+                g_set_error(error, 1, 0, _("'%s' is not a VPN connection profile"), *iter);
+                return FALSE;
+            }
+
+            /* translate id to uuid */
+            g_free(*iter);
+            *iter    = g_strdup(nm_connection_get_uuid(con));
+            modified = TRUE;
+        }
+    }
+
+    if (modified)
+        *value_coerced = g_strjoinv(" ", strv);
+
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+_env_warn_fcn_handle(
+    const NMMetaEnvironment *environment,
+    gpointer                 environment_user_data,
+    NMMetaEnvWarnLevel       warn_level,
+    const char *
+        fmt_l10n, /* the untranslated format string, but it is marked for translation using N_(). */
+    va_list ap)
+{
+    NmCli *       nmc = environment_user_data;
+    gs_free char *m   = NULL;
+
+    if (nmc->complete)
+        return;
+
+    NM_PRAGMA_WARNING_DISABLE("-Wformat-nonliteral")
+    m = g_strdup_vprintf(_(fmt_l10n), ap);
+    NM_PRAGMA_WARNING_REENABLE
+
+    switch (warn_level) {
+    case NM_META_ENV_WARN_LEVEL_WARN:
+        g_print(_("Warning: %s\n"), m);
+        return;
+    case NM_META_ENV_WARN_LEVEL_INFO:
+        g_print(_("Info: %s\n"), m);
+        return;
+    }
+    g_print(_("Error: %s\n"), m);
+}
+
+static NMDevice *const *
+_env_get_nm_devices(const NMMetaEnvironment *environment,
+                    gpointer                 environment_user_data,
+                    guint *                  out_len)
+{
+    NmCli *          nmc = environment_user_data;
+    const GPtrArray *devices;
+
+    nm_assert(nmc);
+
+    /* the returned list is *not* NULL terminated. Need to
+     * provide and honor the out_len argument. */
+    nm_assert(out_len);
+
+    devices = nm_client_get_devices(nmc->client);
+    if (!devices) {
+        *out_len = 0;
+        return NULL;
+    }
+
+    *out_len = devices->len;
+    return (NMDevice *const *) devices->pdata;
+}
+
+static NMRemoteConnection *const *
+_env_get_nm_connections(const NMMetaEnvironment *environment,
+                        gpointer                 environment_user_data,
+                        guint *                  out_len)
+{
+    NmCli *          nmc = environment_user_data;
+    const GPtrArray *values;
+
+    nm_assert(nmc);
+
+    /* the returned list is *not* NULL terminated. Need to
+     * provide and honor the out_len argument. */
+    nm_assert(out_len);
+
+    values = nm_client_get_connections(nmc->client);
+    if (!values) {
+        *out_len = 0;
+        return NULL;
+    }
+
+    *out_len = values->len;
+    return (NMRemoteConnection *const *) values->pdata;
+}
+
+/*****************************************************************************/
+
+const NMMetaEnvironment *const nmc_meta_environment = &((NMMetaEnvironment){
+    .warn_fcn           = _env_warn_fcn_handle,
+    .get_nm_devices     = _env_get_nm_devices,
+    .get_nm_connections = _env_get_nm_connections,
+});
+
+static char *
+get_property_val(NMSetting *           setting,
+                 const char *          prop,
+                 NMMetaAccessorGetType get_type,
+                 gboolean              show_secrets,
+                 GError **             error)
+{
+    const NMMetaPropertyInfo *property_info;
+
+    g_return_val_if_fail(NM_IS_SETTING(setting), NULL);
+    g_return_val_if_fail(!error || !*error, NULL);
+    g_return_val_if_fail(
+        NM_IN_SET(get_type, NM_META_ACCESSOR_GET_TYPE_PARSABLE, NM_META_ACCESSOR_GET_TYPE_PRETTY),
+        NULL);
+
+    if ((property_info = nm_meta_property_info_find_by_setting(setting, prop))) {
+        if (property_info->property_type->get_fcn) {
+            NMMetaAccessorGetOutFlags out_flags = NM_META_ACCESSOR_GET_OUT_FLAGS_NONE;
+            char *                    to_free   = NULL;
+            const char *              value;
+
+            value = property_info->property_type->get_fcn(
+                property_info,
+                nmc_meta_environment,
+                (gpointer) nmc_meta_environment_arg,
+                setting,
+                get_type,
+                show_secrets ? NM_META_ACCESSOR_GET_FLAGS_SHOW_SECRETS : 0,
+                &out_flags,
+                NULL,
+                (gpointer *) &to_free);
+            nm_assert(!out_flags);
+            return to_free ?: g_strdup(value);
+        }
+    }
+
+    g_set_error_literal(error, 1, 0, _("don't know how to get the property value"));
+    return NULL;
+}
+
+/*
+ * Generic function for getting property value.
+ *
+ * Gets property value as a string by calling specialized functions.
+ *
+ * Returns: current property value. The caller must free the returned string.
+ */
+char *
+nmc_setting_get_property(NMSetting *setting, const char *prop, GError **error)
+{
+    return get_property_val(setting, prop, NM_META_ACCESSOR_GET_TYPE_PRETTY, TRUE, error);
+}
+
+/*
+ * Similar to nmc_setting_get_property(), but returns the property in a string
+ * format that can be parsed via nmc_setting_set_property().
+ */
+char *
+nmc_setting_get_property_parsable(NMSetting *setting, const char *prop, GError **error)
+{
+    return get_property_val(setting, prop, NM_META_ACCESSOR_GET_TYPE_PARSABLE, TRUE, error);
+}
+
+gboolean
+nmc_setting_set_property(NMClient *             client,
+                         NMSetting *            setting,
+                         const char *           prop,
+                         NMMetaAccessorModifier modifier,
+                         const char *           value,
+                         GError **              error)
+{
+    const NMMetaPropertyInfo *property_info;
+    gs_free char *            value_to_free = NULL;
+    gboolean                  success;
+
+    g_return_val_if_fail(NM_IS_SETTING(setting), FALSE);
+    g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
+    g_return_val_if_fail(NM_IN_SET(modifier,
+                                   NM_META_ACCESSOR_MODIFIER_SET,
+                                   NM_META_ACCESSOR_MODIFIER_DEL,
+                                   NM_META_ACCESSOR_MODIFIER_ADD),
+                         FALSE);
+
+    if (!(property_info = nm_meta_property_info_find_by_setting(setting, prop)))
+        goto out_fail_read_only;
+    if (!property_info->property_type->set_fcn)
+        goto out_fail_read_only;
+
+    if (modifier == NM_META_ACCESSOR_MODIFIER_DEL
+        && !property_info->property_type->set_supports_remove) {
+        /* The property is a plain property. It does not support '-'.
+         *
+         * Maybe we should fail, but just return silently. */
+        return TRUE;
+    }
+
+    if (value) {
+        switch (property_info->setting_info->general->meta_type) {
+        case NM_META_SETTING_TYPE_CONNECTION:
+            if (nm_streq(property_info->property_name, NM_SETTING_CONNECTION_SECONDARIES)) {
+                if (!_set_fcn_precheck_connection_secondaries(client, value, &value_to_free, error))
+                    return FALSE;
+                if (value_to_free)
+                    value = value_to_free;
+            }
+            break;
+        default:
+            break;
+        }
+    }
+
+    if (NM_IN_SET(modifier, NM_META_ACCESSOR_MODIFIER_ADD, NM_META_ACCESSOR_MODIFIER_DEL)
+        && (!value || !value[0])) {
+        /* nothing to do. */
+        return TRUE;
+    }
+
+    g_object_freeze_notify(G_OBJECT(setting));
+    success = property_info->property_type->set_fcn(property_info,
+                                                    nmc_meta_environment,
+                                                    (gpointer) nmc_meta_environment_arg,
+                                                    setting,
+                                                    modifier,
+                                                    value,
+                                                    error);
+    g_object_thaw_notify(G_OBJECT(setting));
+    return success;
+
+out_fail_read_only:
+    nm_utils_error_set(error, NM_UTILS_ERROR_UNKNOWN, _("the property can't be changed"));
+    return FALSE;
+}
+
+/*
+ * Get valid property names for a setting.
+ *
+ * Returns: string array with the properties or NULL on failure.
+ *          The returned value should be freed with g_strfreev()
+ */
+char **
+nmc_setting_get_valid_properties(NMSetting *setting)
+{
+    const NMMetaSettingInfoEditor *setting_info;
+    char **                        valid_props;
+    guint                          i, num;
+
+    setting_info = nm_meta_setting_info_editor_find_by_setting(setting);
+
+    num = setting_info ? setting_info->properties_num : 0;
+
+    valid_props = g_new(char *, num + 1);
+    for (i = 0; i < num; i++)
+        valid_props[i] = g_strdup(setting_info->properties[i]->property_name);
+
+    valid_props[num] = NULL;
+    return valid_props;
+}
+
+const char *const *
+nmc_setting_get_property_allowed_values(NMSetting *setting, const char *prop, char ***out_to_free)
+{
+    const NMMetaPropertyInfo *property_info;
+
+    g_return_val_if_fail(NM_IS_SETTING(setting), FALSE);
+    g_return_val_if_fail(out_to_free, FALSE);
+
+    *out_to_free = NULL;
+
+    if ((property_info = nm_meta_property_info_find_by_setting(setting, prop))) {
+        if (property_info->property_type->values_fcn) {
+            return property_info->property_type->values_fcn(property_info, out_to_free);
+        } else if (property_info->property_typ_data
+                   && property_info->property_typ_data->values_static)
+            return property_info->property_typ_data->values_static;
+    }
+
+    return NULL;
+}
+
+/*
+ * Create a description string for a property.
+ *
+ * It returns a description got from property documentation, concatenated with
+ * nmcli specific description (if it exists).
+ *
+ * Returns: property description or NULL on failure. The caller must free the string.
+ */
+char *
+nmc_setting_get_property_desc(NMSetting *setting, const char *prop)
+{
+    gs_free char *            desc_to_free       = NULL;
+    const char *              setting_desc       = NULL;
+    const char *              setting_desc_title = "";
+    const char *              nmcli_desc         = NULL;
+    const char *              nmcli_desc_title   = "";
+    const char *              nmcli_nl           = "";
+    const NMMetaPropertyInfo *property_info;
+    const char *              desc = NULL;
+
+    g_return_val_if_fail(NM_IS_SETTING(setting), FALSE);
+
+    property_info = nm_meta_property_info_find_by_setting(setting, prop);
+    if (!property_info)
+        return NULL;
+
+    if (property_info->describe_doc) {
+        setting_desc       = _(property_info->describe_doc);
+        setting_desc_title = _("[NM property description]");
+    }
+
+    if (property_info->property_type->describe_fcn) {
+        desc = property_info->property_type->describe_fcn(property_info, &desc_to_free);
+    } else
+        desc = _(property_info->describe_message);
+
+    if (desc) {
+        nmcli_desc       = desc;
+        nmcli_desc_title = _("[nmcli specific description]");
+        nmcli_nl         = "\n";
+    }
+
+    return g_strdup_printf("%s\n%s\n%s%s%s%s",
+                           setting_desc_title,
+                           setting_desc ?: "",
+                           nmcli_nl,
+                           nmcli_desc_title,
+                           nmcli_nl,
+                           nmcli_desc ?: "");
+}
+
+/*****************************************************************************/
+
+gboolean
+setting_details(const NmcConfig *nmc_config, NMSetting *setting, const char *one_prop)
+{
+    const NMMetaSettingInfoEditor *setting_info;
+    gs_free_error GError *error      = NULL;
+    gs_free char *        fields_str = NULL;
+
+    g_return_val_if_fail(NM_IS_SETTING(setting), FALSE);
+
+    setting_info = nm_meta_setting_info_editor_find_by_setting(setting);
+    if (!setting_info)
+        return FALSE;
+
+    if (one_prop) {
+        /* hack around setting-details being called for one setting. Must prefix the
+         * property name with the setting name. Later we should remove setting_details()
+         * and merge it into the caller. */
+        fields_str = g_strdup_printf("%s.%s", nm_setting_get_name(setting), one_prop);
+    }
+
+    if (!nmc_print(
+            nmc_config,
+            (gpointer[]){setting, NULL},
+            NULL,
+            NULL,
+            (const NMMetaAbstractInfo *const[]){(const NMMetaAbstractInfo *) setting_info, NULL},
+            fields_str,
+            &error))
+        return FALSE;
+
+    return TRUE;
+}