summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile.am81
-rw-r--r--NEWS2
-rwxr-xr-xclients/cloud-setup/90-nm-cloud-setup.sh7
-rw-r--r--clients/cloud-setup/main.c646
-rw-r--r--clients/cloud-setup/meson.build49
-rw-r--r--clients/cloud-setup/nm-cloud-setup-utils.c835
-rw-r--r--clients/cloud-setup/nm-cloud-setup-utils.h121
-rw-r--r--clients/cloud-setup/nm-cloud-setup.service.in8
-rw-r--r--clients/cloud-setup/nm-cloud-setup.timer9
-rw-r--r--clients/cloud-setup/nm-http-client.c746
-rw-r--r--clients/cloud-setup/nm-http-client.h67
-rw-r--r--clients/cloud-setup/nmcs-provider-ec2.c551
-rw-r--r--clients/cloud-setup/nmcs-provider-ec2.h24
-rw-r--r--clients/cloud-setup/nmcs-provider.c236
-rw-r--r--clients/cloud-setup/nmcs-provider.h107
-rw-r--r--clients/meson.build4
-rw-r--r--configure.ac12
-rw-r--r--contrib/fedora/rpm/NetworkManager.spec54
-rwxr-xr-xcontrib/fedora/rpm/build_clean.sh1
-rw-r--r--meson.build11
-rw-r--r--meson_options.txt1
-rw-r--r--po/POTFILES.skip10
-rwxr-xr-xtools/meson-post-install.sh7
24 files changed, 3586 insertions, 5 deletions
diff --git a/.gitignore b/.gitignore
index afa51c3ec2..7ef42f5c4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,6 +65,8 @@ test-*.trs
/dispatcher/tests/test-dispatcher-envp
/clients/cli/nmcli
+/clients/cloud-setup/nm-cloud-setup
+/clients/cloud-setup/nm-cloud-setup.service
/clients/common/settings-docs.h
/clients/common/tests/test-clients-common
/clients/common/tests/test-libnm-core-aux
diff --git a/Makefile.am b/Makefile.am
index d1e00ae057..089af41726 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -4637,6 +4637,87 @@ EXTRA_DIST += \
clients/tui/newt/meson.build
###############################################################################
+# clients/nm-cloud-setup
+###############################################################################
+
+if BUILD_NM_CLOUD_SETUP
+
+libexec_PROGRAMS += clients/cloud-setup/nm-cloud-setup
+
+clients_cloud_setup_nm_cloud_setup_SOURCES = \
+ clients/cloud-setup/main.c \
+ clients/cloud-setup/nm-cloud-setup-utils.c \
+ clients/cloud-setup/nm-cloud-setup-utils.h \
+ clients/cloud-setup/nm-http-client.c \
+ clients/cloud-setup/nm-http-client.h \
+ clients/cloud-setup/nmcs-provider.c \
+ clients/cloud-setup/nmcs-provider.h \
+ clients/cloud-setup/nmcs-provider-ec2.c \
+ clients/cloud-setup/nmcs-provider-ec2.h \
+ $(NULL)
+
+clients_cloud_setup_nm_cloud_setup_CPPFLAGS = \
+ $(clients_cppflags) \
+ -DG_LOG_DOMAIN=\""nm-cloud-setup"\" \
+ $(LIBCURL_CFLAGS) \
+ $(NULL)
+
+clients_cloud_setup_nm_cloud_setup_LDFLAGS = \
+ -Wl,--version-script="$(srcdir)/linker-script-binary.ver" \
+ $(SANITIZER_EXEC_LDFLAGS) \
+ $(NULL)
+
+clients_cloud_setup_nm_cloud_setup_LDADD = \
+ shared/nm-libnm-core-aux/libnm-libnm-core-aux.la \
+ shared/nm-libnm-core-intern/libnm-libnm-core-intern.la \
+ shared/nm-glib-aux/libnm-glib-aux.la \
+ shared/nm-std-aux/libnm-std-aux.la \
+ shared/libcsiphash.la \
+ libnm/libnm.la \
+ $(GLIB_LIBS) \
+ $(LIBCURL_LIBS) \
+ $(NULL)
+
+$(clients_cloud_setup_nm_cloud_setup_OBJECTS): $(libnm_core_lib_h_pub_mkenums)
+$(clients_cloud_setup_nm_cloud_setup_OBJECTS): $(libnm_lib_h_pub_mkenums)
+
+if HAVE_SYSTEMD
+
+systemdsystemunit_DATA += \
+ clients/cloud-setup/nm-cloud-setup.service \
+ clients/cloud-setup/nm-cloud-setup.timer \
+ $(NULL)
+
+clients/cloud-setup/nm-cloud-setup.service: $(srcdir)/clients/cloud-setup/nm-cloud-setup.service.in
+ $(AM_V_GEN) $(data_edit) $< >$@
+
+install-data-hook-cloud-setup: install-data-hook-dispatcher
+ $(INSTALL_SCRIPT) "$(srcdir)/clients/cloud-setup/90-nm-cloud-setup.sh" "$(DESTDIR)$(nmlibdir)/dispatcher.d/no-wait.d/"
+ ln -fs no-wait.d/90-nm-cloud-setup.sh "$(DESTDIR)$(nmlibdir)/dispatcher.d/90-nm-cloud-setup.sh"
+
+install_data_hook += install-data-hook-cloud-setup
+
+uninstall-hook-cloud-setup:
+ rm -f "$(DESTDIR)$(nmlibdir)/dispatcher.d/no-wait.d/90-nm-cloud-setup.sh"
+ rm -f "$(DESTDIR)$(nmlibdir)/dispatcher.d/90-nm-cloud-setup.sh"
+
+uninstall_hook += uninstall-hook-cloud-setup
+
+endif
+
+EXTRA_DIST += \
+ clients/cloud-setup/90-nm-cloud-setup.sh \
+ clients/cloud-setup/meson.build \
+ clients/cloud-setup/nm-cloud-setup.service.in \
+ clients/cloud-setup/nm-cloud-setup.timer \
+ $(NULL)
+
+CLEANFILES += \
+ clients/cloud-setup/nm-cloud-setup.service
+
+endif
+
+###############################################################################
# clients/tests
###############################################################################
diff --git a/NEWS b/NEWS
index daead33e0b..d771fd6501 100644
--- a/NEWS
+++ b/NEWS
@@ -29,6 +29,8 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE!
* libnm: heavily internal rework NMClient. This slims down libnm and makes the
implementation more efficient. NMClient should work now well with a separate
GMainContext.
+* nm-cloud-setup: add new tool for automatically configuring NetworkManager
+ in cloud. Currently only EC2 and IPv4 is supported.
=============================================
NetworkManager-1.20
diff --git a/clients/cloud-setup/90-nm-cloud-setup.sh b/clients/cloud-setup/90-nm-cloud-setup.sh
new file mode 100755
index 0000000000..9fb2a31da6
--- /dev/null
+++ b/clients/cloud-setup/90-nm-cloud-setup.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+case "$2" in
+ up|dhcp4-change)
+ exec systemctl --no-block restart nm-cloud-setup.service
+ ;;
+esac
diff --git a/clients/cloud-setup/main.c b/clients/cloud-setup/main.c
new file mode 100644
index 0000000000..994a9fe0e6
--- /dev/null
+++ b/clients/cloud-setup/main.c
@@ -0,0 +1,646 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#include "nm-default.h"
+
+#include "nm-cloud-setup-utils.h"
+
+#include "nmcs-provider-ec2.h"
+#include "nm-libnm-core-intern/nm-libnm-core-utils.h"
+
+/*****************************************************************************/
+
+typedef struct {
+ GMainLoop *main_loop;
+ GCancellable *cancellable;
+ NMCSProvider *provider_result;
+ guint detect_count;
+} ProviderDetectData;
+
+static void
+_provider_detect_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ gs_unref_object NMCSProvider *provider = NMCS_PROVIDER (source);
+ gs_free_error GError *error = NULL;
+ ProviderDetectData *dd;
+ gboolean success;
+
+ success = nmcs_provider_detect_finish (provider, result, &error);
+
+ nm_assert (success != (!!error));
+
+ if (nm_utils_error_is_cancelled (error, FALSE))
+ return;
+
+ dd = user_data;
+
+ nm_assert (dd->detect_count > 0);
+ dd->detect_count--;
+
+ if (error) {
+ _LOGI ("provider %s not detected: %s", nmcs_provider_get_name (provider), error->message);
+ if (dd->detect_count > 0) {
+ /* wait longer. */
+ return;
+ }
+
+ _LOGI ("no provider detected");
+ goto done;
+ }
+
+ _LOGI ("provider %s detected", nmcs_provider_get_name (provider));
+ dd->provider_result = g_steal_pointer (&provider);
+
+done:
+ g_cancellable_cancel (dd->cancellable);
+ g_main_loop_quit (dd->main_loop);
+}
+
+static void
+_provider_detect_sigterm_cb (GCancellable *source,
+ gpointer user_data)
+{
+ ProviderDetectData *dd = user_data;
+
+ g_cancellable_cancel (dd->cancellable);
+ g_clear_object (&dd->provider_result);
+ dd->detect_count = 0;
+ g_main_loop_quit (dd->main_loop);
+}
+
+static NMCSProvider *
+_provider_detect (GCancellable *sigterm_cancellable)
+{
+ nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new (NULL, FALSE);
+ gs_unref_object GCancellable *cancellable = g_cancellable_new ();
+ gs_unref_object NMHttpClient *http_client = NULL;
+ ProviderDetectData dd = {
+ .cancellable = cancellable,
+ .main_loop = main_loop,
+ .detect_count = 0,
+ .provider_result = NULL,
+ };
+ const GType gtypes[] = {
+ NMCS_TYPE_PROVIDER_EC2,
+ };
+ int i;
+ gulong cancellable_signal_id;
+
+ cancellable_signal_id = g_cancellable_connect (sigterm_cancellable,
+ G_CALLBACK (_provider_detect_sigterm_cb),
+ &dd,
+ NULL);
+ if (!cancellable_signal_id)
+ goto out;
+
+ http_client = nmcs_wait_for_objects_register (nm_http_client_new ());
+
+ for (i = 0; i < G_N_ELEMENTS (gtypes); i++) {
+ NMCSProvider *provider;
+
+ provider = g_object_new (gtypes[i],
+ NMCS_PROVIDER_HTTP_CLIENT, http_client,
+ NULL);
+ nmcs_wait_for_objects_register (provider);
+
+ _LOGD ("start detecting %s provider...", nmcs_provider_get_name (provider));
+ dd.detect_count++;
+ nmcs_provider_detect (provider,
+ cancellable,
+ _provider_detect_cb,
+ &dd);
+ }
+
+ if (dd.detect_count > 0)
+ g_main_loop_run (main_loop);
+
+out:
+ nm_clear_g_signal_handler (sigterm_cancellable, &cancellable_signal_id);
+ return dd.provider_result;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+ GMainLoop *main_loop;
+ NMClient *nmc;
+} ClientCreateData;
+
+static void
+_nmc_create_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ gs_unref_object NMClient *nmc = NULL;
+ ClientCreateData *data = user_data;
+ gs_free_error GError *error = NULL;
+
+ nmc = nm_client_new_finish (result, &error);
+ if (!nmc) {
+ if (!nm_utils_error_is_cancelled (error, FALSE))
+ _LOGI ("failure to talk to NetworkManager: %s", error->message);
+ goto out;
+ }
+
+ if (!nm_client_get_nm_running (nmc)) {
+ _LOGI ("NetworkManager is not running");
+ goto out;
+ }
+
+ _LOGD ("NetworkManager is running");
+ nmcs_wait_for_objects_register (nmc);
+ nmcs_wait_for_objects_register (nm_client_get_context_busy_watcher (nmc));
+
+ data->nmc = g_steal_pointer (&nmc);
+out:
+ g_main_loop_quit (data->main_loop);
+}
+
+static NMClient *
+_nmc_create (GCancellable *sigterm_cancellable)
+{
+ nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new (NULL, FALSE);
+ ClientCreateData data = {
+ .main_loop = main_loop,
+ };
+
+ nm_client_new_async (sigterm_cancellable, _nmc_create_cb, &data);
+
+ g_main_loop_run (main_loop);
+
+ return data.nmc;
+}
+
+/*****************************************************************************/
+
+static char **
+_nmc_get_hwaddrs (NMClient *nmc)
+{
+ gs_unref_ptrarray GPtrArray *hwaddrs = NULL;
+ const GPtrArray *devices;
+ char **hwaddrs_v;
+ gs_free char *str = NULL;
+ guint i;
+
+ devices = nm_client_get_devices (nmc);
+
+ for (i = 0; i < devices->len; i++) {
+ NMDevice *device = devices->pdata[i];
+ const char *hwaddr;
+ char *s;
+
+ if (!NM_IS_DEVICE_ETHERNET (device))
+ continue;
+
+ if (nm_device_get_state (device) < NM_DEVICE_STATE_UNAVAILABLE)
+ continue;
+
+ hwaddr = nm_device_ethernet_get_permanent_hw_address (NM_DEVICE_ETHERNET (device));
+ if (!hwaddr)
+ continue;
+
+ s = nmcs_utils_hwaddr_normalize (hwaddr, -1);
+ if (!s)
+ continue;
+
+ if (!hwaddrs)
+ hwaddrs = g_ptr_array_new_with_free_func (g_free);
+ g_ptr_array_add (hwaddrs, s);
+ }
+
+ if (!hwaddrs) {
+ _LOGD ("found interfaces: none");
+ return NULL;
+ }
+
+ g_ptr_array_add (hwaddrs, NULL);
+ hwaddrs_v = (char **) g_ptr_array_free (g_steal_pointer (&hwaddrs), FALSE);
+
+ _LOGD ("found interfaces: %s", (str = g_strjoinv (", ", hwaddrs_v)));
+
+ return hwaddrs_v;
+}
+
+static NMDevice *
+_nmc_get_device_by_hwaddr (NMClient *nmc,
+ const char *hwaddr)
+{
+ const GPtrArray *devices;
+ guint i;
+
+ devices = nm_client_get_devices (nmc);
+
+ for (i = 0; i < devices->len; i++) {
+ NMDevice *device = devices->pdata[i];
+ const char *hwaddr_dev;
+ gs_free char *s = NULL;
+
+ if (!NM_IS_DEVICE_ETHERNET (device))
+ continue;
+
+ hwaddr_dev = nm_device_ethernet_get_permanent_hw_address (NM_DEVICE_ETHERNET (device));
+ if (!hwaddr_dev)
+ continue;
+
+ s = nmcs_utils_hwaddr_normalize (hwaddr_dev, -1);
+ if (s && nm_streq (s, hwaddr))
+ return device;
+ }
+
+ return NULL;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+ GMainLoop *main_loop;
+ GHashTable *config_dict;
+} GetConfigData;
+
+static void
+_get_config_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GetConfigData *data = user_data;
+ gs_unref_hashtable GHashTable *config_dict = NULL;
+ gs_free_error GError *error = NULL;
+
+ config_dict = nmcs_provider_get_config_finish (NMCS_PROVIDER (source), result, &error);
+
+ if (!config_dict) {
+ if (!nm_utils_error_is_cancelled (error, FALSE))
+ _LOGI ("failure to get meta data: %s", error->message);
+ } else
+ _LOGD ("meta data received");
+
+ data->config_dict = g_steal_pointer (&config_dict);
+ g_main_loop_quit (data->main_loop);
+}
+
+static GHashTable *
+_get_config (GCancellable *sigterm_cancellable,
+ NMCSProvider *provider,
+ NMClient *nmc)
+{
+ nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new (NULL, FALSE);
+ GetConfigData data = {
+ .main_loop = main_loop,
+ };
+ gs_strfreev char **hwaddrs = NULL;
+
+ hwaddrs = _nmc_get_hwaddrs (nmc);
+
+ nmcs_provider_get_config (provider,
+ TRUE,
+ (const char *const*) hwaddrs,
+ sigterm_cancellable,
+ _get_config_cb,
+ &data);
+
+ g_main_loop_run (main_loop);
+
+ return data.config_dict;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_nmc_skip_connection (NMConnection *connection)
+{
+ NMSettingUser *s_user;
+ const char *v;
+
+ s_user = NM_SETTING_USER (nm_connection_get_setting (connection, NM_TYPE_SETTING_USER));
+ if (!s_user)
+ return FALSE;
+
+#define USER_TAG_SKIP "org.freedesktop.nm-cloud-setup.skip"
+
+ nm_assert (nm_setting_user_check_key (USER_TAG_SKIP, NULL));
+
+ v = nm_setting_user_get_data (s_user, USER_TAG_SKIP);
+ return _nm_utils_ascii_str_to_bool (v, FALSE);
+}
+
+static gboolean
+_nmc_mangle_connection (NMDevice *device,
+ NMConnection *connection,
+ gboolean is_single_nic,
+ const NMCSProviderGetConfigIfaceData *config_data,
+ gboolean *out_changed)
+{
+ NMSettingIPConfig *s_ip;
+ gboolean addrs_changed;
+ gboolean routes_changed;
+ gboolean rules_changed;
+ gsize i;
+ in_addr_t gateway;
+ gint64 rt_metric;
+ guint32 rt_table;
+ gs_unref_ptrarray GPtrArray *addrs_new = NULL;
+ gs_unref_ptrarray GPtrArray *rules_new = NULL;
+ nm_auto_unref_ip_route NMIPRoute *route_new = NULL;
+
+ if (!nm_streq0 (nm_connection_get_connection_type (connection), NM_SETTING_WIRED_SETTING_NAME))
+ return FALSE;
+
+ s_ip = nm_connection_get_setting_ip4_config (connection);
+ if (!s_ip)
+ return FALSE;
+
+ addrs_new = g_ptr_array_new_full (config_data->ipv4s_len, (GDestroyNotify) nm_ip_address_unref);
+ for (i = 0; i < config_data->ipv4s_len; i++) {
+ NMIPAddress *entry;
+
+ entry = nm_ip_address_new_binary (AF_INET,
+ &config_data->ipv4s_arr[i],
+ config_data->cidr_prefix,
+ NULL);
+ if (entry)
+ g_ptr_array_add (addrs_new, entry);
+ }
+
+ gateway = nm_utils_ip4_address_clear_host_address (config_data->cidr_addr, config_data->cidr_prefix);
+ ((guint8 *) &gateway)[3] += 1;
+
+ rt_metric = 10;
+ rt_table = 30400 + config_data->iface_idx;
+
+ route_new = nm_ip_route_new_binary (AF_INET,
+ &nm_ip_addr_zero,
+ 0,
+ &gateway,
+ rt_metric,
+ NULL);
+ nm_ip_route_set_attribute (route_new,
+ NM_IP_ROUTE_ATTRIBUTE_TABLE,
+ g_variant_new_uint32 (rt_table));
+
+ rules_new = g_ptr_array_new_full (config_data->ipv4s_len, (GDestroyNotify) nm_ip_routing_rule_unref);
+ for (i = 0; i < config_data->ipv4s_len; i++) {
+ NMIPRoutingRule *entry;
+ char sbuf[NM_UTILS_INET_ADDRSTRLEN];
+
+ entry = nm_ip_routing_rule_new (AF_INET);
+ nm_ip_routing_rule_set_priority (entry, rt_table);
+ nm_ip_routing_rule_set_from (entry,
+ nm_utils_inet4_ntop (config_data->ipv4s_arr[i], sbuf),
+ 32);
+ nm_ip_routing_rule_set_table (entry, rt_table);
+
+ nm_assert (nm_ip_routing_rule_validate (entry, NULL));
+
+ g_ptr_array_add (rules_new, entry);
+ }
+
+ addrs_changed = nmcs_setting_ip_replace_ipv4_addresses (s_ip,
+ (NMIPAddress **) addrs_new->pdata,
+ addrs_new->len);
+
+ routes_changed = nmcs_setting_ip_replace_ipv4_routes (s_ip,
+ &route_new,
+ 1);
+
+ rules_changed = nmcs_setting_ip_replace_ipv4_rules (s_ip,
+ (NMIPRoutingRule **) rules_new->pdata,
+ rules_new->len);
+
+ NM_SET_OUT (out_changed, addrs_changed
+ || routes_changed
+ || rules_changed);
+ return TRUE;
+}
+
+/*****************************************************************************/
+
+static guint
+_config_data_get_num_valid (GHashTable *config_dict)
+{
+ const NMCSProviderGetConfigIfaceData *config_data;
+ GHashTableIter h_iter;
+ guint n = 0;
+
+ g_hash_table_iter_init (&h_iter, config_dict);
+ while (g_hash_table_iter_next (&h_iter, NULL, (gpointer *) &config_data)) {
+ if (nmcs_provider_get_config_iface_data_is_valid (config_data))
+ n++;
+ }
+
+ return n;
+}
+
+static gboolean
+_config_one (GCancellable *sigterm_cancellable,
+ NMClient *nmc,
+ gboolean is_single_nic,
+ const char *hwaddr,
+ const NMCSProviderGetConfigIfaceData *config_data)
+{
+ gs_unref_object NMDevice *device = NULL;
+ gs_unref_object NMConnection *applied_connection = NULL;
+ guint64 applied_version_id;
+ gs_free_error GError *error = NULL;
+ gboolean changed;
+ gboolean version_id_changed;
+ guint try_count;
+ gboolean any_changes = FALSE;
+
+ g_main_context_iteration (NULL, FALSE);
+
+ if (g_cancellable_is_cancelled (sigterm_cancellable))
+ return FALSE;
+
+ device = nm_g_object_ref (_nmc_get_device_by_hwaddr (nmc, hwaddr));
+ if (!device) {
+ _LOGD ("config device %s: skip because device not found", hwaddr);
+ return FALSE;
+ }
+
+ if (!nmcs_provider_get_config_iface_data_is_valid (config_data)) {
+ _LOGD ("config device %s: skip because meta data not successfully fetched", hwaddr);
+ return FALSE;
+ }
+
+ _LOGD ("config device %s: configuring \"%s\" (%s)...",
+ hwaddr,
+ nm_device_get_iface (device) ?: "/unknown/",
+ nm_object_get_path (NM_OBJECT (device)));
+
+ try_count = 0;
+
+try_again:
+
+ applied_connection = nmcs_device_get_applied_connection (device,
+ sigterm_cancellable,
+ &applied_version_id,
+ &error);
+ if (!applied_connection) {
+ if (!nm_utils_error_is_cancelled (error, FALSE))
+ _LOGD ("config device %s: device has no applied connection (%s). Skip", hwaddr, error->message);
+ return any_changes;
+ }
+
+ if (_nmc_skip_connection (applied_connection)) {
+ _LOGD ("config device %s: skip applied connection due to user data %s", hwaddr, USER_TAG_SKIP);
+ return any_changes;
+ }
+
+ if (!_nmc_mangle_connection (device,
+ applied_connection,
+ is_single_nic,
+ config_data,
+ &changed)) {
+ _LOGD ("config device %s: device has no suitable applied connection. Skip", hwaddr);
+ return any_changes;
+ }
+
+ if (!changed) {
+ _LOGD ("config device %s: device needs no update to applied connection \"%s\" (%s). Skip",
+ hwaddr,
+ nm_connection_get_id (applied_connection),
+ nm_connection_get_uuid (applied_connection));
+ return any_changes;
+ }
+
+ _LOGD ("config device %s: reapply connection \"%s\" (%s)",
+ hwaddr,
+ nm_connection_get_id (applied_connection),
+ nm_connection_get_uuid (applied_connection));
+
+ /* we are about to call Reapply(). If if that fails, it counts as if we changed something. */
+ any_changes = TRUE;
+
+ if (!nmcs_device_reapply (device,
+ sigterm_cancellable,
+ applied_connection,
+ applied_version_id,
+ &version_id_changed,
+ &error)) {
+ if ( version_id_changed
+ && try_count < 5) {
+ _LOGD ("config device %s: applied connection changed in the meantime. Retry...",
+ hwaddr);
+ g_clear_object (&applied_connection);
+ g_clear_error (&error);
+ try_count++;
+ goto try_again;
+ }
+
+ if (!nm_utils_error_is_cancelled (error, FALSE)) {
+ _LOGD ("config device %s: failure to reapply connection \"%s\" (%s): %s",
+ hwaddr,
+ nm_connection_get_id (applied_connection),
+ nm_connection_get_uuid (applied_connection),
+ error->message);
+ }
+ return any_changes;
+ }
+
+ _LOGD ("config device %s: connection \"%s\" (%s) reapplied",
+ hwaddr,
+ nm_connection_get_id (applied_connection),
+ nm_connection_get_uuid (applied_connection));
+
+ return any_changes;
+}
+
+static gboolean
+_config_all (GCancellable *sigterm_cancellable,
+ NMClient *nmc,
+ GHashTable *config_dict)
+{
+ GHashTableIter h_iter;
+ const NMCSProviderGetConfigIfaceData *c_config_data;
+ const char *c_hwaddr;
+ gboolean is_single_nic;
+ gboolean any_changes = FALSE;
+
+ is_single_nic = (_config_data_get_num_valid (config_dict) <= 1);
+
+ g_hash_table_iter_init (&h_iter, config_dict);
+ while (g_hash_table_iter_next (&h_iter, (gpointer *) &c_hwaddr, (gpointer *) &c_config_data)) {
+ if (_config_one (sigterm_cancellable, nmc, is_single_nic, c_hwaddr, c_config_data))
+ any_changes = TRUE;
+ }
+
+ return any_changes;
+}
+
+/*****************************************************************************/
+
+static gboolean
+sigterm_handler (gpointer user_data)
+{
+ GCancellable *sigterm_cancellable = user_data;
+
+ if (!g_cancellable_is_cancelled (sigterm_cancellable)) {
+ _LOGD ("SIGTERM received");
+ g_cancellable_cancel (user_data);
+ } else
+ _LOGD ("SIGTERM received (again)");
+ return G_SOURCE_CONTINUE;
+}
+
+/*****************************************************************************/
+
+int
+main (int argc, const char *const*argv)
+{
+ gs_unref_object GCancellable *sigterm_cancellable = NULL;
+ nm_auto_destroy_and_unref_gsource GSource *sigterm_source = NULL;
+ gs_unref_object NMCSProvider *provider = NULL;
+ gs_unref_object NMClient *nmc = NULL;
+ gs_unref_hashtable GHashTable *config_dict = NULL;
+
+ _nm_logging_enabled_init (g_getenv ("NM_CLOUD_SETUP_LOG"));
+
+ _LOGD ("nm-cloud-setup %s starting...", NM_DIST_VERSION);
+
+ if (argc != 1) {
+ g_printerr ("%s: no command line arguments supported\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ sigterm_cancellable = g_cancellable_new ();
+
+ sigterm_source = nm_g_source_attach (nm_g_unix_signal_source_new (SIGTERM,
+ G_PRIORITY_DEFAULT,
+ sigterm_handler,
+ sigterm_cancellable,
+ NULL),
+ NULL);
+
+ provider = _provider_detect (sigterm_cancellable);
+ if (!provider)
+ goto done;
+
+ nmc = _nmc_create (sigterm_cancellable);
+ if (!nmc)
+ goto done;
+
+ config_dict = _get_config (sigterm_cancellable, provider, nmc);
+ if (!config_dict)
+ goto done;
+
+ if (_config_all (sigterm_cancellable, nmc, config_dict))
+ _LOGI ("some changes were applied for provider %s", nmcs_provider_get_name (provider));
+ else
+ _LOGD ("no changes were applied for provider %s", nmcs_provider_get_name (provider));
+
+done:
+ nm_clear_pointer (&config_dict, g_hash_table_unref);
+ g_clear_object (&nmc);
+ g_clear_object (&provider);
+
+ if (!nmcs_wait_for_objects_iterate_until_done (NULL, 2000)) {
+ _LOGE ("shutdown: timeout waiting to application to quit. This is a bug");
+ nm_assert_not_reached ();
+ }
+
+ nm_clear_g_source_inst (&sigterm_source);
+ g_clear_object (&sigterm_cancellable);
+
+ return 0;
+}
diff --git a/clients/cloud-setup/meson.build b/clients/cloud-setup/meson.build
new file mode 100644
index 0000000000..e9cd970a36
--- /dev/null
+++ b/clients/cloud-setup/meson.build
@@ -0,0 +1,49 @@
+name = 'nm-cloud-setup'
+
+if install_systemdunitdir
+
+ nm_cloud_setup_service = configure_file(
+ input: 'nm-cloud-setup.service.in',
+ output: '@BASENAME@',
+ install_dir: systemd_systemdsystemunitdir,
+ configuration: data_conf,
+ )
+
+ install_data(
+ 'nm-cloud-setup.timer',
+ install_dir: systemd_systemdsystemunitdir,
+ )
+
+ install_data(
+ '90-nm-cloud-setup.sh',
+ install_dir: join_paths(nm_pkglibdir, 'dispatcher.d', 'no-wait.d'),
+ )
+
+endif
+
+sources = files(
+ 'main.c',
+ 'nm-cloud-setup-utils.c',
+ 'nm-http-client.c',
+ 'nmcs-provider-ec2.c',
+ 'nmcs-provider.c',
+)
+
+deps = [
+ libnmc_base_dep,
+ libnmc_dep,
+ libcurl_dep,
+]
+
+executable(
+ name,
+ sources,
+ dependencies: deps,
+ c_args: clients_c_flags +
+ ['-DG_LOG_DOMAIN="@0@"'.format(name)],
+ link_with: libnm_systemd_logging_stub,
+ link_args: ldflags_linker_script_binary,
+ link_depends: linker_script_binary,
+ install: true,
+ install_dir: nm_libexecdir,
+)
diff --git a/clients/cloud-setup/nm-cloud-setup-utils.c b/clients/cloud-setup/nm-cloud-setup-utils.c
new file mode 100644
index 0000000000..9051e6f20e
--- /dev/null
+++ b/clients/cloud-setup/nm-cloud-setup-utils.c
@@ -0,0 +1,835 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#include "nm-default.h"
+
+#include "nm-cloud-setup-utils.h"
+
+#include "nm-glib-aux/nm-time-utils.h"
+#include "nm-glib-aux/nm-logging-base.h"
+
+/*****************************************************************************/
+
+volatile NMLogLevel _nm_logging_configured_level = LOGL_TRACE;
+
+void
+_nm_logging_enabled_init (const char *level_str)
+{
+ NMLogLevel level;
+
+ if (!_nm_log_parse_level (level_str, &level))
+ level = LOGL_WARN;
+ else if (level == _LOGL_KEEP)
+ level = LOGL_WARN;
+
+ _nm_logging_configured_level = level;
+}
+
+void
+_nm_log_impl_cs (NMLogLevel level,
+ const char *fmt,
+ ...)
+{
+ gs_free char *msg = NULL;
+ va_list ap;
+ const char *level_str;
+ gint64 ts;
+
+ va_start (ap, fmt);
+ msg = g_strdup_vprintf (fmt, ap);
+ va_end (ap);
+
+ switch (level) {
+ case LOGL_TRACE: level_str = "<trace>"; break;
+ case LOGL_DEBUG: level_str = "<debug>"; break;
+ case LOGL_INFO: level_str = "<info> "; break;
+ case LOGL_WARN: level_str = "<warn> "; break;
+ default:
+ nm_assert (level == LOGL_ERR);
+ level_str = "<error>";
+ break;
+ }
+
+ ts = nm_utils_clock_gettime_ns (CLOCK_BOOTTIME);
+
+ g_print ("[%"G_GINT64_FORMAT".%05"G_GINT64_FORMAT"] %s %s\n",
+ ts / NM_UTILS_NS_PER_SECOND,
+ (ts / (NM_UTILS_NS_PER_SECOND / 10000)) % 10000,
+ level_str,
+ msg);
+}
+
+void
+_nm_utils_monotonic_timestamp_initialized (const struct timespec *tp,
+ gint64 offset_sec,
+ gboolean is_boottime)
+{
+}
+
+/*****************************************************************************/
+
+G_LOCK_DEFINE_STATIC (_wait_for_objects_lock);
+static GSList *_wait_for_objects_list;
+static GSList *_wait_for_objects_iterate_loops;
+
+static void
+_wait_for_objects_maybe_quit_mainloops_with_lock (void)
+{
+ GSList *iter;
+
+ if (!_wait_for_objects_list) {
+ for (iter = _wait_for_objects_iterate_loops; iter; iter = iter->next)
+ g_main_loop_quit (iter->data);
+ }
+}
+
+static void
+_wait_for_objects_weak_cb (gpointer data,
+ GObject *where_the_object_was)
+{
+ G_LOCK (_wait_for_objects_lock);
+ nm_assert (g_slist_find (_wait_for_objects_list, where_the_object_was));
+ _wait_for_objects_list = g_slist_remove (_wait_for_objects_list, where_the_object_was);
+ _wait_for_objects_maybe_quit_mainloops_with_lock ();
+ G_UNLOCK (_wait_for_objects_lock);
+}
+
+/**
+ * nmcs_wait_for_objects_register:
+ * @target: a #GObject to wait for.
+ *
+ * Registers @target as a pointer to wait during shutdown. Using
+ * nmcs_wait_for_objects_iterate_until_done() we keep waiting until
+ * @target gets destroyed, which means that it gets completely unreferenced.
+ */
+gpointer
+nmcs_wait_for_objects_register (gpointer target)
+{
+ g_return_val_if_fail (G_IS_OBJECT (target), NULL);
+
+ G_LOCK (_wait_for_objects_lock);
+ _wait_for_objects_list = g_slist_prepend (_wait_for_objects_list, target);
+ G_UNLOCK (_wait_for_objects_lock);
+
+ g_object_weak_ref (target,
+ _wait_for_objects_weak_cb,
+ NULL);
+ return target;
+}
+
+typedef struct {
+ GMainLoop *loop;
+ gboolean got_timeout;
+} WaitForObjectsData;
+
+static gboolean
+_wait_for_objects_iterate_until_done_timeout_cb (gpointer user_data)
+{
+ WaitForObjectsData *data = user_data;
+
+ data->got_timeout = TRUE;
+ g_main_loop_quit (data->loop);
+ return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+_wait_for_objects_iterate_until_done_idle_cb (gpointer user_data)
+{
+ /* This avoids a race where:
+ *
+ * - we check whether there are objects to wait for.
+ * - the last object to wait for gets removed (issuing g_main_loop_quit()).
+ * - we run the mainloop (and missed our signal).
+ *
+ * It's really a missing feature of GMainLoop where the "is-running" flag is always set to
+ * TRUE by g_main_loop_run(). That means, you cannot catch a g_main_loop_quit() in a race
+ * free way while not iterating the loop.
+ *
+ * Avoid this, by checking once again after we start running the mainloop.
+ */
+
+ G_LOCK (_wait_for_objects_lock);
+ _wait_for_objects_maybe_quit_mainloops_with_lock ();
+ G_UNLOCK (_wait_for_objects_lock);
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * nmcs_wait_for_objects_iterate_until_done:
+ * @context: the #GMainContext to iterate.
+ * @timeout_ms: timeout or -1 for no timeout.
+ *
+ * Iterates the provided @context until all objects that we wait for
+ * are destroyed.
+ *
+ * The purpose of this is to cleanup all objects that we have on exit. That
+ * is especially because objects have asynchronous operations pending that
+ * should be cancelled and properly completed during exit.
+ *
+ * Returns: %FALSE on timeout or %TRUE if all objects destroyed before timeout.
+ */
+gboolean
+nmcs_wait_for_objects_iterate_until_done (GMainContext *context,
+ int timeout_ms)
+{
+ nm_auto_unref_gmainloop GMainLoop *loop = g_main_loop_new (context, FALSE);
+ nm_auto_destroy_and_unref_gsource GSource *timeout_source = NULL;
+ WaitForObjectsData data;
+ gboolean has_more_objects;
+
+ G_LOCK (_wait_for_objects_lock);
+ if (!_wait_for_objects_list) {
+ G_UNLOCK (_wait_for_objects_lock);
+ return TRUE;
+ }
+ _wait_for_objects_iterate_loops = g_slist_prepend (_wait_for_objects_iterate_loops, loop);
+ G_UNLOCK (_wait_for_objects_lock);
+
+ data = (WaitForObjectsData) {
+ .loop = loop,
+ .got_timeout = FALSE,
+ };
+
+ if (timeout_ms >= 0) {
+ timeout_source = nm_g_source_attach (nm_g_timeout_source_new (timeout_ms,
+ G_PRIORITY_DEFAULT,
+ _wait_for_objects_iterate_until_done_timeout_cb,
+ &data,
+ NULL),
+ context);
+ }
+
+ has_more_objects = TRUE;
+ while ( has_more_objects
+ && !data.got_timeout) {
+ nm_auto_destroy_and_unref_gsource GSource *idle_source = NULL;
+
+ idle_source = nm_g_source_attach (nm_g_idle_source_new (G_PRIORITY_DEFAULT,
+ _wait_for_objects_iterate_until_done_idle_cb,
+ &data,
+ NULL),
+ context);
+
+ g_main_loop_run (loop);
+
+ G_LOCK (_wait_for_objects_lock);
+ has_more_objects = (!!_wait_for_objects_list);
+ if ( data.got_timeout
+ || !has_more_objects)
+ _wait_for_objects_iterate_loops = g_slist_remove (_wait_for_objects_iterate_loops, loop);
+ G_UNLOCK (_wait_for_objects_lock);
+ }
+
+ return !data.got_timeout;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+ GTask *task;
+ GSource *source_timeout;
+ GSource *source_next_poll;
+ GMainContext *context;
+ GCancellable *internal_cancellable;
+ NMCSUtilsPollProbeStartFcn probe_start_fcn;
+ NMCSUtilsPollProbeFinishFcn probe_finish_fcn;
+ gpointer probe_user_data;
+ gulong cancellable_id;
+ gint64 last_poll_start_ms;
+ int sleep_timeout_ms;
+ int ratelimit_timeout_ms;
+ bool completed:1;
+} PollTaskData;
+
+static void
+_poll_task_data_free (gpointer data)
+{
+ PollTaskData *poll_task_data = data;
+
+ nm_assert (G_IS_TASK (poll_task_data->task));
+ nm_assert (!poll_task_data->source_next_poll);
+ nm_assert (!poll_task_data->source_timeout);
+ nm_assert (poll_task_data->cancellable_id == 0);
+
+ g_main_context_unref (poll_task_data->context);
+
+ nm_g_slice_free (poll_task_data);
+}
+
+static void
+_poll_return (PollTaskData *poll_task_data,
+ gboolean success,
+ GError *error_take)
+{
+ nm_clear_g_source_inst (&poll_task_data->source_next_poll);
+ nm_clear_g_source_inst (&poll_task_data->source_timeout);
+ nm_clear_g_cancellable_disconnect (g_task_get_cancellable (poll_task_data->task),
+ &poll_task_data->cancellable_id);
+
+ nm_clear_g_cancellable (&poll_task_data->internal_cancellable);
+
+ if (error_take)
+ g_task_return_error (poll_task_data->task, g_steal_pointer (&error_take));
+ else
+ g_task_return_boolean (poll_task_data->task, success);
+
+ g_object_unref (poll_task_data->task);
+}
+
+static gboolean _poll_start_cb (gpointer user_data);
+
+static void
+_poll_done_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ PollTaskData *poll_task_data = user_data;
+ _nm_unused gs_unref_object GTask *task = poll_task_data->task; /* balance ref from _poll_start_cb() */
+ gs_free_error GError *error = NULL;
+ gint64 now_ms;
+ gint64 wait_ms;
+ gboolean is_finished;
+
+ is_finished = poll_task_data->probe_finish_fcn (source,
+ result,
+ poll_task_data->probe_user_data,
+ &error);
+
+ if (nm_utils_error_is_cancelled (error, FALSE)) {
+ /* we already handle this differently. Nothing to do. */
+ return;
+ }
+
+ if ( error
+ || is_finished) {
+ _poll_return (poll_task_data, TRUE, g_steal_pointer (&error));
+ return;
+ }
+
+ now_ms = nm_utils_get_monotonic_timestamp_ms ();
+ if (poll_task_data->ratelimit_timeout_ms > 0)
+ wait_ms = (poll_task_data->last_poll_start_ms + poll_task_data->ratelimit_timeout_ms) - now_ms;
+ else
+ wait_ms = 0;
+ if (poll_task_data->sleep_timeout_ms > 0)
+ wait_ms = MAX (wait_ms, poll_task_data->sleep_timeout_ms);
+
+ poll_task_data->source_next_poll = nm_g_source_attach (nm_g_timeout_source_new (MAX (1, wait_ms),
+ G_PRIORITY_DEFAULT,
+ _poll_start_cb,
+ poll_task_data,
+ NULL),
+ poll_task_data->context);
+}
+
+static gboolean
+_poll_start_cb (gpointer user_data)
+{
+ PollTaskData *poll_task_data = user_data;
+
+ nm_clear_g_source_inst (&poll_task_data->source_next_poll);
+
+ poll_task_data->last_poll_start_ms = nm_utils_get_monotonic_timestamp_ms ();
+
+ g_object_ref (poll_task_data->task); /* balanced by _poll_done_cb() */
+
+ poll_task_data->probe_start_fcn (poll_task_data->internal_cancellable,
+ poll_task_data->probe_user_data,
+ _poll_done_cb,
+ poll_task_data);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+_poll_timeout_cb (gpointer user_data)
+{
+ PollTaskData *poll_task_data = user_data;
+
+ _poll_return (poll_task_data, FALSE, NULL);
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+_poll_cancelled_cb (GObject *object, gpointer user_data)
+{
+ PollTaskData *poll_task_data = user_data;
+ GError *error = NULL;
+
+ _LOGD (">> poll cancelled");
+ nm_clear_g_signal_handler (g_task_get_cancellable (poll_task_data->task),
+ &poll_task_data->cancellable_id);
+ nm_utils_error_set_cancelled (&error, FALSE, NULL);
+ _poll_return (poll_task_data, FALSE, error);
+}
+
+/**
+ * nmcs_utils_poll:
+ * @poll_timeout_ms: if >= 0, then this is the overall timeout for how long we poll.
+ * When this timeout expires, the request completes with failure (but no error set).
+ * @ratelimit_timeout_ms: if > 0, we ratelimit the starts from one prope_start_fcn
+ * call to the next.
+ * @sleep_timeout_ms: if > 0, then we wait after a probe finished this timeout
+ * before the next. Together with @ratelimit_timeout_ms this determines how
+ * frequently we probe.
+ * @probe_start_fcn: used to start a (asynchrnous) probe. A probe must be completed
+ * by calling the provided callback. While a probe is in progress, we will not
+ * start another. This function is already invoked the first time synchronously,
+ * during nmcs_utils_poll().
+ * @probe_finish_fcn: will be called from the callback of @probe_start_fcn. If the
+ * function returns %TRUE (polling done) or an error, polling stops. Otherwise,
+ * another poll will be started.
+ * @probe_user_data: user_data for the probe functions.
+ * @cancellable: cancellable for polling.
+ * @callback: when polling completes.
+ * @user_data: for @callback.
+ *
+ * This uses the current g_main_context_get_thread_default() for scheduling
+ * actions.
+ */
+void
+nmcs_utils_poll (int poll_timeout_ms,
+ int sleep_timeout_ms,
+ int ratelimit_timeout_ms,
+ NMCSUtilsPollProbeStartFcn probe_start_fcn,
+ NMCSUtilsPollProbeFinishFcn probe_finish_fcn,
+ gpointer probe_user_data,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ PollTaskData *poll_task_data;
+
+ poll_task_data = g_slice_new (PollTaskData);
+ *poll_task_data = (PollTaskData) {
+ .task = nm_g_task_new (NULL, cancellable, nmcs_utils_poll, callback, user_data),
+ .probe_start_fcn = probe_start_fcn,
+ .probe_finish_fcn = probe_finish_fcn,
+ .probe_user_data = probe_user_data,
+ .completed = FALSE,
+ .context = g_main_context_ref_thread_default (),
+ .sleep_timeout_ms = sleep_timeout_ms,
+ .ratelimit_timeout_ms = ratelimit_timeout_ms,
+ .internal_cancellable = g_cancellable_new (),
+ };
+
+ nmcs_wait_for_objects_register (poll_task_data->task);
+
+ g_task_set_task_data (poll_task_data->task, poll_task_data, _poll_task_data_free);
+
+ if (poll_timeout_ms >= 0) {
+ poll_task_data->source_timeout = nm_g_source_attach (nm_g_timeout_source_new (poll_timeout_ms,
+ G_PRIORITY_DEFAULT,
+ _poll_timeout_cb,
+ poll_task_data,
+ NULL),
+ poll_task_data->context);
+ }
+
+ poll_task_data->source_next_poll = nm_g_source_attach (nm_g_idle_source_new (G_PRIORITY_DEFAULT,
+ _poll_start_cb,
+ poll_task_data,
+ NULL),
+ poll_task_data->context);
+
+ if (cancellable) {
+ gulong signal_id;
+
+ signal_id = g_cancellable_connect (cancellable,
+ G_CALLBACK (_poll_cancelled_cb),
+ poll_task_data,
+ NULL);
+ if (signal_id == 0) {
+ /* the request is already cancelled. Return. */
+ return;
+ }
+ poll_task_data->cancellable_id = signal_id;
+ }
+}
+
+/**
+ * nmcs_utils_poll_finish:
+ * @result: the GAsyncResult from the GAsyncReadyCallback callback.
+ * @probe_user_data: the user data provided to nmcs_utils_poll().
+ * @error: the failure code.
+ *
+ * Returns: %TRUE if the polling completed with success. In that case,
+ * the error won't be set.
+ * If the request was cancelled, this is indicated by @error and
+ * %FALSE will be returned.
+ * If the probe returned a failure, this returns %FALSE and the error
+ * provided by @probe_finish_fcn.
+ * If the request times out, this returns %FALSE without error set.
+ */
+gboolean
+nmcs_utils_poll_finish (GAsyncResult *result,
+ gpointer *probe_user_data,
+ GError **error)
+{
+ GTask *task;
+ PollTaskData *poll_task_data;
+
+ g_return_val_if_fail (nm_g_task_is_valid (result, NULL, nmcs_utils_poll), FALSE);
+ g_return_val_if_fail (!error || !*error, FALSE);
+
+ task = G_TASK (result);
+
+ if (probe_user_data) {
+ poll_task_data = g_task_get_task_data (task);
+ NM_SET_OUT (probe_user_data, poll_task_data->probe_user_data);
+ }
+
+ return g_task_propagate_boolean (task, error);
+}
+
+/*****************************************************************************/
+
+char *
+nmcs_utils_hwaddr_normalize (const char *hwaddr, gssize len)
+{
+ gs_free char *hwaddr_clone = NULL;
+ guint8 buf[ETH_ALEN];
+
+ nm_assert (len >= -1);
+
+ if (len < 0) {
+ if (!hwaddr)
+ return NULL;
+ } else {
+ if (len == 0)
+ return NULL;
+ nm_assert (hwaddr);
+ hwaddr = nm_strndup_a (300, hwaddr, len, &hwaddr_clone);
+ }
+
+ if (!nm_utils_hwaddr_aton (hwaddr, buf, sizeof (buf)))
+ return NULL;
+
+ return nm_utils_hwaddr_ntoa (buf, sizeof (buf));
+}
+
+/*****************************************************************************/
+
+const char *
+nmcs_utils_parse_memmem (GBytes *mem, const char *needle)
+{
+ const char *mem_data;
+ gsize mem_size;
+
+ g_return_val_if_fail (mem, NULL);
+ g_return_val_if_fail (needle, NULL);
+
+ mem_data = g_bytes_get_data (mem, &mem_size);
+ return memmem (mem_data, mem_size, needle, strlen (needle));
+}
+
+const char *
+nmcs_utils_parse_get_full_line (GBytes *mem, const char *needle)
+{
+ const char *mem_data;
+ gsize mem_size;
+ gsize c;
+ gsize l;
+
+ const char *line;
+
+ line = nmcs_utils_parse_memmem (mem, needle);
+ if (!line)
+ return NULL;
+
+ mem_data = g_bytes_get_data (mem, &mem_size);
+
+ if ( line != mem_data
+ && line[-1] != '\n') {
+ /* the line must be preceeded either by the begin of the data or
+ * by a newline. */
+ return NULL;
+ }
+
+ c = mem_size - (line - mem_data);
+ l = strlen (needle);
+
+ if ( c != l
+ && line[l] != '\n') {
+ /* the end of the needle must be either a newline or the end of the buffer. */
+ return NULL;
+ }
+
+ return line;
+}
+
+/*****************************************************************************/
+
+char *
+nmcs_utils_uri_build_concat_v (const char *base,
+ const char **components,
+ gsize n_components)
+{
+ GString *uri;
+
+ nm_assert (base);
+ nm_assert (base[0]);
+ nm_assert (!NM_STR_HAS_SUFFIX (base, "/"));
+
+ uri = g_string_sized_new (100);
+
+ g_string_append (uri, base);
+
+ if ( n_components > 0
+ && components[0]
+ && components[0][0] == '/') {
+ /* the first component starts with a slash. We allow that, and don't add a duplicate
+ * slash. Otherwise, we add a separator after base.
+ *
+ * We only do that for the first component. */
+ } else
+ g_string_append_c (uri, '/');
+
+ while (n_components > 0) {
+ if (!components[0]) {
+ /* we allow NULL, to indicate nothing to append*/
+ } else
+ g_string_append (uri, components[0]);
+ components++;
+ n_components--;
+ }
+
+ return g_string_free (uri, FALSE);
+}
+
+/*****************************************************************************/
+
+gboolean
+nmcs_setting_ip_replace_ipv4_addresses (NMSettingIPConfig *s_ip,
+ NMIPAddress **entries_arr,
+ guint entries_len)
+{
+ gboolean any_changes = FALSE;
+ guint i_next;
+ guint num;
+ guint i;
+
+ num = nm_setting_ip_config_get_num_addresses (s_ip);
+
+ i_next = 0;
+
+ for (i = 0; i < entries_len; i++) {
+ NMIPAddress *entry = entries_arr[i];
+
+ if (!any_changes) {
+ if (i_next < num) {
+ if (nm_ip_address_cmp_full (entry,
+ nm_setting_ip_config_get_address (s_ip, i_next),
+ NM_IP_ADDRESS_CMP_FLAGS_WITH_ATTRS) == 0) {
+ i_next++;
+ continue;
+ }
+ }
+ while (i_next < num)
+ nm_setting_ip_config_remove_address (s_ip, --num);
+ any_changes = TRUE;
+ }
+
+ if (!nm_setting_ip_config_add_address (s_ip, entry))
+ continue;
+
+ i_next++;
+ }
+ if (any_changes) {
+ while (i_next < num) {
+ nm_setting_ip_config_remove_address (s_ip, --num);
+ any_changes = TRUE;
+ }
+ }
+
+ return any_changes;
+}
+
+gboolean
+nmcs_setting_ip_replace_ipv4_routes (NMSettingIPConfig *s_ip,
+ NMIPRoute **entries_arr,
+ guint entries_len)
+{
+ gboolean any_changes = FALSE;
+ guint i_next;
+ guint num;
+ guint i;
+
+ num = nm_setting_ip_config_get_num_routes (s_ip);
+
+ i_next = 0;
+
+ for (i = 0; i < entries_len; i++) {
+ NMIPRoute *entry = entries_arr[i];
+
+ if (!any_changes) {
+ if (i_next < num) {
+ if (nm_ip_route_equal_full (entry,
+ nm_setting_ip_config_get_route (s_ip, i_next),
+ NM_IP_ROUTE_EQUAL_CMP_FLAGS_WITH_ATTRS)) {
+ i_next++;
+ continue;
+ }
+ }
+ while (i_next < num)
+ nm_setting_ip_config_remove_route (s_ip, --num);
+ any_changes = TRUE;
+ }
+
+ if (!nm_setting_ip_config_add_route (s_ip, entry))
+ continue;
+
+ i_next++;
+ }
+ if (!any_changes) {
+ while (i_next < num) {
+ nm_setting_ip_config_remove_route (s_ip, --num);
+ any_changes = TRUE;
+ }
+ }
+
+ return any_changes;
+}
+
+gboolean
+nmcs_setting_ip_replace_ipv4_rules (NMSettingIPConfig *s_ip,
+ NMIPRoutingRule **entries_arr,
+ guint entries_len)
+{
+ gboolean any_changes = FALSE;
+ guint i_next;
+ guint num;
+ guint i;
+
+ num = nm_setting_ip_config_get_num_routing_rules (s_ip);
+
+ i_next = 0;
+
+ for (i = 0; i < entries_len; i++) {
+ NMIPRoutingRule *entry = entries_arr[i];
+
+ if (!any_changes) {
+ if (i_next < num) {
+ if (nm_ip_routing_rule_cmp (entry,
+ nm_setting_ip_config_get_routing_rule (s_ip, i_next)) == 0) {
+ i_next++;
+ continue;
+ }
+ }
+ while (i_next < num)
+ nm_setting_ip_config_remove_routing_rule (s_ip, --num);
+ any_changes = TRUE;
+ }
+
+ nm_setting_ip_config_add_routing_rule (s_ip, entry);
+ i_next++;
+ }
+ if (!any_changes) {
+ while (i_next < num) {
+ nm_setting_ip_config_remove_routing_rule (s_ip, --num);
+ any_changes = TRUE;
+ }
+ }
+
+ return any_changes;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+ GMainLoop *main_loop;
+ NMConnection *connection;
+ GError *error;
+ guint64 version_id;
+} DeviceGetAppliedConnectionData;
+
+static void
+_nmcs_device_get_applied_connection_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ DeviceGetAppliedConnectionData *data = user_data;
+
+ data->connection = nm_device_get_applied_connection_finish (NM_DEVICE (source),
+ result,
+ &data->version_id,
+ &data->error);
+ g_main_loop_quit (data->main_loop);
+}
+
+NMConnection *
+nmcs_device_get_applied_connection (NMDevice *device,
+ GCancellable *cancellable,
+ guint64 *version_id,
+ GError **error)
+{
+ nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new (NULL, FALSE);
+ DeviceGetAppliedConnectionData data = {
+ .main_loop = main_loop,
+ };
+
+ nm_device_get_applied_connection_async (device,
+ 0,
+ cancellable,
+ _nmcs_device_get_applied_connection_cb,
+ &data);
+
+ g_main_loop_run (main_loop);
+
+ if (data.error)
+ g_propagate_error (error, data.error);
+ NM_SET_OUT (version_id, data.version_id);
+ return data.connection;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+ GMainLoop *main_loop;
+ GError *error;
+} DeviceReapplyData;
+
+static void
+_nmcs_device_reapply_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ DeviceReapplyData *data = user_data;
+
+ nm_device_reapply_finish (NM_DEVICE (source),
+ result,
+ &data->error);
+ g_main_loop_quit (data->main_loop);
+}
+
+gboolean
+nmcs_device_reapply (NMDevice *device,
+ GCancellable *sigterm_cancellable,
+ NMConnection *connection,
+ guint64 version_id,
+ gboolean *out_version_id_changed,
+ GError **error)
+{
+ nm_auto_unref_gmainloop GMainLoop *main_loop = g_main_loop_new (NULL, FALSE);
+ DeviceReapplyData data = {
+ .main_loop = main_loop,
+ };
+
+ nm_device_reapply_async (device,
+ connection,
+ version_id,
+ 0,
+ sigterm_cancellable,
+ _nmcs_device_reapply_cb,
+ &data);
+
+ g_main_loop_run (main_loop);
+
+ if (data.error) {
+ NM_SET_OUT (out_version_id_changed, g_error_matches (data.error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_VERSION_ID_MISMATCH));
+ g_propagate_error (error, data.error);
+ return FALSE;
+ }
+
+ NM_SET_OUT (out_version_id_changed, FALSE);
+ return TRUE;
+}
diff --git a/clients/cloud-setup/nm-cloud-setup-utils.h b/clients/cloud-setup/nm-cloud-setup-utils.h
new file mode 100644
index 0000000000..fa36d38985
--- /dev/null
+++ b/clients/cloud-setup/nm-cloud-setup-utils.h
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#ifndef __NM_CLOUD_SETUP_UTILS_H__
+#define __NM_CLOUD_SETUP_UTILS_H__
+
+#include "nm-glib-aux/nm-logging-fwd.h"
+
+/*****************************************************************************/
+
+extern volatile NMLogLevel _nm_logging_configured_level;
+
+static inline gboolean
+nm_logging_enabled (NMLogLevel level)
+{
+ return level >= _nm_logging_configured_level;
+}
+
+void _nm_logging_enabled_init (const char *level_str);
+
+void _nm_log_impl_cs (NMLogLevel level,
+ const char *fmt,
+ ...) _nm_printf (2, 3);
+
+#define _nm_log(level, ...) \
+ _nm_log_impl_cs ((level), __VA_ARGS__);
+
+#define _NMLOG(level, ...) \
+ G_STMT_START { \
+ const NMLogLevel _level = (level); \
+ \
+ if (nm_logging_enabled (_level)) { \
+ _nm_log (_level, __VA_ARGS__); \
+ } \
+ } G_STMT_END
+
+/*****************************************************************************/
+
+#ifndef NM_DIST_VERSION
+#define NM_DIST_VERSION VERSION
+#endif
+
+/*****************************************************************************/
+
+gpointer nmcs_wait_for_objects_register (gpointer target);
+
+gboolean nmcs_wait_for_objects_iterate_until_done (GMainContext *context,
+ int timeout_ms);
+
+/*****************************************************************************/
+
+typedef void (*NMCSUtilsPollProbeStartFcn) (GCancellable *cancellable,
+ gpointer probe_user_data,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+typedef gboolean (*NMCSUtilsPollProbeFinishFcn) (GObject *source,
+ GAsyncResult *result,
+ gpointer probe_user_data,
+ GError **error);
+
+void nmcs_utils_poll (int poll_timeout_ms,
+ int ratelimit_timeout_ms,
+ int sleep_timeout_ms,
+ NMCSUtilsPollProbeStartFcn probe_start_fcn,
+ NMCSUtilsPollProbeFinishFcn probe_finish_fcn,
+ gpointer probe_user_data,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean nmcs_utils_poll_finish (GAsyncResult *result,
+ gpointer *probe_user_data,
+ GError **error);
+
+/*****************************************************************************/
+
+char *nmcs_utils_hwaddr_normalize (const char *hwaddr, gssize len);
+
+/*****************************************************************************/
+
+const char *nmcs_utils_parse_memmem (GBytes *mem, const char *needle);
+
+const char *nmcs_utils_parse_get_full_line (GBytes *mem, const char *needle);
+
+/*****************************************************************************/
+
+char *nmcs_utils_uri_build_concat_v (const char *base,
+ const char **components,
+ gsize n_components);
+
+#define nmcs_utils_uri_build_concat(base, ...) nmcs_utils_uri_build_concat_v (base, ((const char *[]) { __VA_ARGS__ }), NM_NARG (__VA_ARGS__))
+
+/*****************************************************************************/
+
+gboolean nmcs_setting_ip_replace_ipv4_addresses (NMSettingIPConfig *s_ip,
+ NMIPAddress **entries_arr,
+ guint entries_len);
+
+gboolean nmcs_setting_ip_replace_ipv4_routes (NMSettingIPConfig *s_ip,
+ NMIPRoute **entries_arr,
+ guint entries_len);
+
+gboolean nmcs_setting_ip_replace_ipv4_rules (NMSettingIPConfig *s_ip,
+ NMIPRoutingRule **entries_arr,
+ guint entries_len);
+
+/*****************************************************************************/
+
+NMConnection *nmcs_device_get_applied_connection (NMDevice *device,
+ GCancellable *cancellable,
+ guint64 *version_id,
+ GError **error);
+
+gboolean nmcs_device_reapply (NMDevice *device,
+ GCancellable *sigterm_cancellable,
+ NMConnection *connection,
+ guint64 version_id,
+ gboolean *out_version_id_changed,
+ GError **error);
+
+#endif /* __NM_CLOUD_SETUP_UTILS_H__ */
diff --git a/clients/cloud-setup/nm-cloud-setup.service.in b/clients/cloud-setup/nm-cloud-setup.service.in
new file mode 100644
index 0000000000..7d09062760
--- /dev/null
+++ b/clients/cloud-setup/nm-cloud-setup.service.in
@@ -0,0 +1,8 @@
+[Unit]
+Description=Automatically configure NetworkManager in cloud
+
+[Service]
+Type=oneshot
+ExecStart=@libexecdir@/nm-cloud-setup
+
+#Environment=NM_CLOUD_SETUP_LOG=TRACE
diff --git a/clients/cloud-setup/nm-cloud-setup.timer b/clients/cloud-setup/nm-cloud-setup.timer
new file mode 100644
index 0000000000..fd1722a6ed
--- /dev/null
+++ b/clients/cloud-setup/nm-cloud-setup.timer
@@ -0,0 +1,9 @@
+[Unit]
+Description=Periodically run nm-cloud-setup
+
+[Timer]
+OnBootSec=5min
+OnUnitActiveSec=5min
+
+[Install]
+WantedBy=timers.target
diff --git a/clients/cloud-setup/nm-http-client.c b/clients/cloud-setup/nm-http-client.c
new file mode 100644
index 0000000000..7a219a3d3a
--- /dev/null
+++ b/clients/cloud-setup/nm-http-client.c
@@ -0,0 +1,746 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#include "nm-default.h"
+
+#include "nm-http-client.h"
+
+#include <curl/curl.h>
+#include <glib-unix.h>
+
+#include "nm-cloud-setup-utils.h"
+
+#define NM_CURL_DEBUG 0
+
+/*****************************************************************************/
+
+typedef struct {
+ GMainContext *context;
+ CURLM *mhandle;
+ GSource *mhandle_source_timeout;
+ GSource *mhandle_source_socket;
+} NMHttpClientPrivate;
+
+struct _NMHttpClient {
+ GObject parent;
+ NMHttpClientPrivate _priv;
+};
+
+struct _NMHttpClientClass {
+ GObjectClass parent;
+};
+
+G_DEFINE_TYPE (NMHttpClient, nm_http_client, G_TYPE_OBJECT);
+
+#define NM_HTTP_CLIENT_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMHttpClient, NM_IS_HTTP_CLIENT)
+
+/*****************************************************************************/
+
+#define _NMLOG2(level, edata, ...) \
+ G_STMT_START { \
+ EHandleData *_edata = (edata); \
+ \
+ _NMLOG (level, \
+ "http-request["NM_HASH_OBFUSCATE_PTR_FMT", \"%s\"]: " \
+ _NM_UTILS_MACRO_FIRST (__VA_ARGS__), \
+ NM_HASH_OBFUSCATE_PTR (_edata), \
+ (_edata)->url \
+ _NM_UTILS_MACRO_REST (__VA_ARGS__)); \
+ } G_STMT_END
+
+/*****************************************************************************/
+
+G_LOCK_DEFINE_STATIC (_my_curl_initalized_lock);
+static bool _my_curl_initialized = FALSE;
+
+__attribute__((destructor))
+static void
+_my_curl_global_cleanup (void)
+{
+ G_LOCK (_my_curl_initalized_lock);
+ if (_my_curl_initialized) {
+ _my_curl_initialized = FALSE;
+ curl_global_cleanup ();
+ }
+ G_UNLOCK (_my_curl_initalized_lock);
+}
+
+static void
+nm_http_client_curl_global_init (void)
+{
+ G_LOCK (_my_curl_initalized_lock);
+ if (!_my_curl_initialized) {
+ _my_curl_initialized = TRUE;
+ if (curl_global_init (CURL_GLOBAL_ALL) != CURLE_OK) {
+ /* Even if this fails, we are partly initialized. WTF. */
+ _LOGE ("curl: curl_global_init() failed!");
+ }
+ }
+ G_UNLOCK (_my_curl_initalized_lock);
+}
+
+/*****************************************************************************/
+
+GMainContext *
+nm_http_client_get_main_context (NMHttpClient *self)
+{
+ g_return_val_if_fail (NM_IS_HTTP_CLIENT (self), NULL);
+
+ return NM_HTTP_CLIENT_GET_PRIVATE (self)->context;
+}
+
+/*****************************************************************************/
+
+static GSource *
+_source_attach (NMHttpClient *self,
+ GSource *source)
+{
+ return nm_g_source_attach (source, NM_HTTP_CLIENT_GET_PRIVATE (self)->context);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+ long response_code;
+ GBytes *response_data;
+} GetResult;
+
+static void
+_get_result_free (gpointer data)
+{
+ GetResult *get_result = data;
+
+ g_bytes_unref (get_result->response_data);
+ nm_g_slice_free (get_result);
+}
+
+typedef struct {
+ GTask *task;
+ GSource *timeout_source;
+ CURLcode ehandle_result;
+ CURL *ehandle;
+ char *url;
+ GString *recv_data;
+ gssize max_data;
+ gulong cancellable_id;
+} EHandleData;
+
+static void
+_ehandle_free_ehandle (EHandleData *edata)
+{
+ if (edata->ehandle) {
+ NMHttpClient *self = g_task_get_source_object (edata->task);
+ NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
+
+ curl_multi_remove_handle (priv->mhandle, edata->ehandle);
+ curl_easy_cleanup (g_steal_pointer (&edata->ehandle));
+ }
+}
+
+static void
+_ehandle_free (EHandleData *edata)
+{
+ nm_assert (!edata->ehandle);
+ nm_assert (!edata->timeout_source);
+
+ g_object_unref (edata->task);
+
+ if (edata->recv_data)
+ g_string_free (edata->recv_data, TRUE);
+ g_free (edata->url);
+ nm_g_slice_free (edata);
+}
+
+static void
+_ehandle_complete (EHandleData *edata,
+ GError *error_take)
+{
+ GetResult *get_result;
+ gs_free char *str_tmp_1 = NULL;
+ long response_code = -1;
+
+ nm_clear_pointer (&edata->timeout_source, nm_g_source_destroy_and_unref);
+
+ nm_clear_g_cancellable_disconnect (g_task_get_cancellable (edata->task),
+ &edata->cancellable_id);
+
+ if (error_take) {
+ if (nm_utils_error_is_cancelled (error_take, FALSE))
+ _LOG2T (edata, "cancelled");
+ else
+ _LOG2D (edata, "failed with %s", error_take->message);
+ } else if (edata->ehandle_result != CURLE_OK) {
+ _LOG2D (edata, "failed with curl error \"%s\"", curl_easy_strerror (edata->ehandle_result));
+ nm_utils_error_set (&error_take,
+ NM_UTILS_ERROR_UNKNOWN,
+ "failed with curl error \"%s\"",
+ curl_easy_strerror (edata->ehandle_result));
+ }
+
+ if (error_take) {
+ _ehandle_free_ehandle (edata);
+ g_task_return_error (edata->task, error_take);
+ _ehandle_free (edata);
+ return;
+ }
+
+ if (curl_easy_getinfo (edata->ehandle,
+ CURLINFO_RESPONSE_CODE,
+ &response_code) != CURLE_OK)
+ _LOG2E (edata, "failed to get response code from curl easy handle");
+
+ _LOG2D (edata, "success getting %"G_GSIZE_FORMAT" bytes (response code %ld)",
+ edata->recv_data->len,
+ response_code);
+
+ _LOG2T (edata, "received %"G_GSIZE_FORMAT" bytes: [[%s]]",
+ edata->recv_data->len,
+ nm_utils_buf_utf8safe_escape (edata->recv_data->str, edata->recv_data->len, NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL, &str_tmp_1));
+
+ _ehandle_free_ehandle (edata);
+
+ get_result = g_slice_new (GetResult);
+ *get_result = (GetResult) {
+ .response_code = response_code,
+ /* This ensures that response_data is always NUL terminated. This is an important guarantee
+ * that NMHttpClient makes. */
+ .response_data = g_string_free_to_bytes (g_steal_pointer (&edata->recv_data)),
+ };
+
+ g_task_return_pointer (edata->task, get_result, _get_result_free);
+
+ _ehandle_free (edata);
+}
+
+/*****************************************************************************/
+
+static size_t
+_get_writefunction_cb (char *ptr, size_t size, size_t nmemb, void *user_data)
+{
+ EHandleData *edata = user_data;
+ gsize nconsume;
+
+ /* size should always be 1, but still. Multiply them to be sure. */
+ nmemb *= size;
+
+ if (edata->max_data >= 0) {
+ nm_assert (edata->recv_data->len <= edata->max_data);
+ nconsume = (((gsize) edata->max_data) - edata->recv_data->len);
+ if (nconsume > nmemb)
+ nconsume = nmemb;
+ } else
+ nconsume = nmemb;
+
+ g_string_append_len (edata->recv_data, ptr, nconsume);
+ return nconsume;
+}
+
+static gboolean
+_get_timeout_cb (gpointer user_data)
+{
+ _ehandle_complete (user_data,
+ g_error_new_literal (NM_UTILS_ERROR,
+ NM_UTILS_ERROR_UNKNOWN,
+ "HTTP request timed out"));
+ return G_SOURCE_REMOVE;
+}
+
+static void
+_get_cancelled_cb (GObject *object, gpointer user_data)
+{
+ EHandleData *edata = user_data;
+ GError *error = NULL;
+
+ nm_clear_g_signal_handler (g_task_get_cancellable (edata->task),
+ &edata->cancellable_id);
+ nm_utils_error_set_cancelled (&error, FALSE, NULL);
+ _ehandle_complete (edata, error);
+}
+
+void
+nm_http_client_get (NMHttpClient *self,
+ const char *url,
+ int timeout_ms,
+ gssize max_data,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ NMHttpClientPrivate *priv;
+ EHandleData *edata;
+
+ g_return_if_fail (NM_IS_HTTP_CLIENT (self));
+ g_return_if_fail (url);
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+ g_return_if_fail (timeout_ms >= 0);
+ g_return_if_fail (max_data >= -1);
+
+ priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
+
+ edata = g_slice_new (EHandleData);
+ *edata = (EHandleData) {
+ .task = nm_g_task_new (self, cancellable, nm_http_client_get, callback, user_data),
+ .recv_data = g_string_sized_new (NM_MIN (max_data, 245)),
+ .max_data = max_data,
+ .url = g_strdup (url),
+ };
+
+ nmcs_wait_for_objects_register (edata->task);
+
+ _LOG2D (edata, "start get ...");
+
+ edata->ehandle = curl_easy_init ();
+ if (!edata->ehandle) {
+ _ehandle_complete (edata,
+ g_error_new_literal (NM_UTILS_ERROR,
+ NM_UTILS_ERROR_UNKNOWN,
+ "HTTP request failed to create curl handle"));
+ return;
+ }
+
+ curl_easy_setopt (edata->ehandle, CURLOPT_URL, url);
+
+ curl_easy_setopt (edata->ehandle, CURLOPT_WRITEFUNCTION, _get_writefunction_cb);
+ curl_easy_setopt (edata->ehandle, CURLOPT_WRITEDATA, edata);
+ curl_easy_setopt (edata->ehandle, CURLOPT_PRIVATE, edata);
+
+ if (timeout_ms > 0) {
+ edata->timeout_source = _source_attach (self,
+ nm_g_timeout_source_new (timeout_ms,
+ G_PRIORITY_DEFAULT,
+ _get_timeout_cb,
+ edata,
+ NULL));
+ }
+
+ curl_multi_add_handle (priv->mhandle, edata->ehandle);
+
+ if (cancellable) {
+ gulong signal_id;
+
+ signal_id = g_cancellable_connect (cancellable,
+ G_CALLBACK (_get_cancelled_cb),
+ edata,
+ NULL);
+ if (signal_id == 0) {
+ /* the request is already cancelled. Return. */
+ return;
+ }
+ edata->cancellable_id = signal_id;
+ }
+}
+
+gboolean
+nm_http_client_get_finish (NMHttpClient *self,
+ GAsyncResult *result,
+ long *out_response_code,
+ GBytes **out_response_data,
+ GError **error)
+{
+ GetResult *get_result;
+
+ g_return_val_if_fail (NM_IS_HTTP_CLIENT (self), FALSE);
+ g_return_val_if_fail (nm_g_task_is_valid (result, self, nm_http_client_get), FALSE);
+
+ get_result = g_task_propagate_pointer (G_TASK (result), error);
+ if (!get_result) {
+ NM_SET_OUT (out_response_code, -1);
+ NM_SET_OUT (out_response_data, NULL);
+ return FALSE;
+ }
+
+ NM_SET_OUT (out_response_code, get_result->response_code);
+
+ /* response_data is binary, but is also guaranteed to be NUL terminated! */
+ NM_SET_OUT (out_response_data, g_steal_pointer (&get_result->response_data));
+
+ _get_result_free (get_result);
+
+ return TRUE;
+}
+
+/*****************************************************************************/
+
+typedef struct {
+ GTask *task;
+ char *uri;
+ NMHttpClientPollGetCheckFcn check_fcn;
+ gpointer check_user_data;
+ GBytes *response_data;
+ gsize request_max_data;
+ long response_code;
+ int request_timeout_ms;
+} PollGetData;
+
+static void
+_poll_get_data_free (gpointer data)
+{
+ PollGetData *poll_get_data = data;
+
+ g_free (poll_get_data->uri);
+
+ nm_clear_pointer (&poll_get_data->response_data, g_bytes_unref);
+
+ nm_g_slice_free (poll_get_data);
+}
+
+static void
+_poll_get_probe_start_fcn (GCancellable *cancellable,
+ gpointer probe_user_data,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ PollGetData *poll_get_data = probe_user_data;
+
+ /* balanced by _poll_get_probe_finish_fcn() */
+ g_object_ref (poll_get_data->task);
+
+ nm_http_client_get (g_task_get_source_object (poll_get_data->task),
+ poll_get_data->uri,
+ poll_get_data->request_timeout_ms,
+ poll_get_data->request_max_data,
+ cancellable,
+ callback,
+ user_data);
+}
+
+static gboolean
+_poll_get_probe_finish_fcn (GObject *source,
+ GAsyncResult *result,
+ gpointer probe_user_data,
+ GError **error)
+{
+ PollGetData *poll_get_data = probe_user_data;
+ _nm_unused gs_unref_object GTask *task = poll_get_data->task; /* balance ref from _poll_get_probe_start_fcn() */
+ gboolean success;
+ gs_free_error GError *local_error = NULL;
+ long response_code;
+ gs_unref_bytes GBytes *response_data = NULL;
+
+ success = nm_http_client_get_finish (g_task_get_source_object (poll_get_data->task),
+ result,
+ &response_code,
+ &response_data,
+ &local_error);
+
+ if (!success) {
+ if (nm_utils_error_is_cancelled (local_error, FALSE)) {
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ if (poll_get_data->check_fcn) {
+ success = poll_get_data->check_fcn (response_code,
+ response_data,
+ poll_get_data->check_user_data,
+ &local_error);
+ } else
+ success = (response_code == 200);
+
+ if (local_error) {
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ return TRUE;
+ }
+
+ if (!success)
+ return FALSE;
+
+ poll_get_data->response_code = response_code;
+ poll_get_data->response_data = g_steal_pointer (&response_data);
+ return TRUE;
+}
+
+static void
+_poll_get_done_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ PollGetData *poll_get_data = user_data;
+ gs_free_error GError *error = NULL;
+ gboolean success;
+
+ success = nmcs_utils_poll_finish (result, NULL, &error);
+
+ if (error)
+ g_task_return_error (poll_get_data->task, g_steal_pointer (&error));
+ else
+ g_task_return_boolean (poll_get_data->task, success);
+
+ g_object_unref (poll_get_data->task);
+}
+
+void
+nm_http_client_poll_get (NMHttpClient *self,
+ const char *uri,
+ int request_timeout_ms,
+ gssize request_max_data,
+ int poll_timeout_ms,
+ int ratelimit_timeout_ms,
+ GCancellable *cancellable,
+ NMHttpClientPollGetCheckFcn check_fcn,
+ gpointer check_user_data,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ nm_auto_pop_gmaincontext GMainContext *context = NULL;
+ PollGetData *poll_get_data;
+
+ g_return_if_fail (NM_IS_HTTP_CLIENT (self));
+ g_return_if_fail (uri && uri[0]);
+ g_return_if_fail (request_timeout_ms >= -1);
+ g_return_if_fail (request_max_data >= -1);
+ g_return_if_fail (poll_timeout_ms >= -1);
+ g_return_if_fail (ratelimit_timeout_ms >= -1);
+ g_return_if_fail (!cancellable || G_CANCELLABLE (cancellable));
+
+ poll_get_data = g_slice_new (PollGetData);
+ *poll_get_data = (PollGetData) {
+ .task = nm_g_task_new (self, cancellable, nm_http_client_poll_get, callback, user_data),
+ .uri = g_strdup (uri),
+ .request_timeout_ms = request_timeout_ms,
+ .request_max_data = request_max_data,
+ .check_fcn = check_fcn,
+ .check_user_data = check_user_data,
+ .response_code = -1,
+ };
+
+ nmcs_wait_for_objects_register (poll_get_data->task);
+
+ g_task_set_task_data (poll_get_data->task,
+ poll_get_data,
+ _poll_get_data_free);
+
+ context = nm_g_main_context_push_thread_default_if_necessary (nm_http_client_get_main_context (self));
+
+ nmcs_utils_poll (poll_timeout_ms,
+ ratelimit_timeout_ms,
+ 0,
+ _poll_get_probe_start_fcn,
+ _poll_get_probe_finish_fcn,
+ poll_get_data,
+ cancellable,
+ _poll_get_done_cb,
+ poll_get_data);
+}
+
+gboolean
+nm_http_client_poll_get_finish (NMHttpClient *self,
+ GAsyncResult *result,
+ long *out_response_code,
+ GBytes **out_response_data,
+ GError **error)
+{
+ PollGetData *poll_get_data;
+ GTask *task;
+ gboolean success;
+ gs_free_error GError *local_error = NULL;
+
+ g_return_val_if_fail (NM_HTTP_CLIENT (self), FALSE);
+ g_return_val_if_fail (nm_g_task_is_valid (result, self, nm_http_client_poll_get), FALSE);
+
+ task = G_TASK (result);
+
+ success = g_task_propagate_boolean (task, &local_error);
+ if ( local_error
+ || !success) {
+ if (local_error)
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ NM_SET_OUT (out_response_code, -1);
+ NM_SET_OUT (out_response_data, NULL);
+ return FALSE;
+ }
+
+ poll_get_data = g_task_get_task_data (task);
+
+ NM_SET_OUT (out_response_code, poll_get_data->response_code);
+ NM_SET_OUT (out_response_data, g_steal_pointer (&poll_get_data->response_data));
+
+ return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+_mhandle_action (NMHttpClient *self,
+ int sockfd,
+ int ev_bitmask)
+{
+ NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
+ EHandleData *edata;
+ CURLMsg *msg;
+ CURLcode eret;
+ int m_left;
+ CURLMcode ret;
+ int running_handles;
+
+ ret = curl_multi_socket_action (priv->mhandle, sockfd, ev_bitmask, &running_handles);
+ if (ret != CURLM_OK) {
+ _LOGE ("curl: curl_multi_socket_action() failed: (%d) %s", ret, curl_multi_strerror (ret));
+ /* really unexpected. Not clear how to handle this. */
+ }
+
+ while ((msg = curl_multi_info_read (priv->mhandle, &m_left))) {
+
+ if (msg->msg != CURLMSG_DONE)
+ continue;
+
+ eret = curl_easy_getinfo (msg->easy_handle, CURLINFO_PRIVATE, (char **) &edata);
+
+ nm_assert (eret == CURLE_OK);
+ nm_assert (edata);
+
+ edata->ehandle_result = msg->data.result;
+ _ehandle_complete (edata, NULL);
+ }
+}
+
+static gboolean
+_mhandle_socket_cb (int fd,
+ GIOCondition condition,
+ gpointer user_data)
+{
+ int ev_bitmask = 0;
+
+ if (condition & G_IO_IN)
+ ev_bitmask |= CURL_CSELECT_IN;
+ if (condition & G_IO_OUT)
+ ev_bitmask |= CURL_CSELECT_OUT;
+ if (condition & G_IO_ERR)
+ ev_bitmask |= CURL_CSELECT_ERR;
+
+ _mhandle_action (user_data, fd, ev_bitmask);
+ return G_SOURCE_CONTINUE;
+}
+
+static int
+_mhandle_socketfunction_cb (CURL *e_handle, curl_socket_t fd, int what, void *user_data, void *socketp)
+{
+ NMHttpClient *self = user_data;
+ NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
+
+ (void) _NM_ENSURE_TYPE (int, fd);
+
+ nm_clear_g_source_inst (&priv->mhandle_source_socket);
+
+ if (what != CURL_POLL_REMOVE) {
+ GIOCondition condition = 0;
+
+ if (what == CURL_POLL_IN)
+ condition = G_IO_IN;
+ else if (what == CURL_POLL_OUT)
+ condition = G_IO_OUT;
+ else if (what == CURL_POLL_INOUT)
+ condition = G_IO_IN | G_IO_OUT;
+ else
+ condition = 0;
+
+ if (condition) {
+ priv->mhandle_source_socket = g_unix_fd_source_new (fd, condition);
+ g_source_set_callback (priv->mhandle_source_socket, G_SOURCE_FUNC (_mhandle_socket_cb), self, NULL);
+ g_source_attach (priv->mhandle_source_socket, priv->context);
+ }
+ }
+
+ return CURLM_OK;
+}
+
+static gboolean
+_mhandle_timeout_cb (gpointer user_data)
+{
+ _mhandle_action (user_data, CURL_SOCKET_TIMEOUT, 0);
+ return G_SOURCE_CONTINUE;
+}
+
+static int
+_mhandle_timerfunction_cb (CURLM *multi, long timeout_ms, void *user_data)
+{
+ NMHttpClient *self = user_data;
+ NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
+
+ nm_clear_pointer (&priv->mhandle_source_timeout, nm_g_source_destroy_and_unref);
+ if (timeout_ms >= 0) {
+ priv->mhandle_source_timeout = _source_attach (self,
+ nm_g_timeout_source_new (NM_MIN (timeout_ms, G_MAXINT),
+ G_PRIORITY_DEFAULT,
+ _mhandle_timeout_cb,
+ self,
+ NULL));
+ }
+ return 0;
+}
+
+/*****************************************************************************/
+
+static void
+nm_http_client_init (NMHttpClient *self)
+{
+}
+
+static void
+constructed (GObject *object)
+{
+ NMHttpClient *self = NM_HTTP_CLIENT (object);
+ NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
+
+ priv->context = g_main_context_ref_thread_default ();
+
+ priv->mhandle = curl_multi_init ();
+ if (!priv->mhandle)
+ _LOGE ("curl: failed to create multi-handle");
+ else {
+ curl_multi_setopt (priv->mhandle, CURLMOPT_SOCKETFUNCTION, _mhandle_socketfunction_cb);
+ curl_multi_setopt (priv->mhandle, CURLMOPT_SOCKETDATA, self);
+ curl_multi_setopt (priv->mhandle, CURLMOPT_TIMERFUNCTION, _mhandle_timerfunction_cb);
+ curl_multi_setopt (priv->mhandle, CURLMOPT_TIMERDATA, self);
+ if (NM_CURL_DEBUG)
+ curl_multi_setopt (priv->mhandle, CURLOPT_VERBOSE, 1);
+ }
+
+ G_OBJECT_CLASS (nm_http_client_parent_class)->constructed (object);
+}
+
+NMHttpClient *
+nm_http_client_new (void)
+{
+ return g_object_new (NM_TYPE_HTTP_CLIENT, NULL);
+}
+
+static void
+dispose (GObject *object)
+{
+ NMHttpClient *self = NM_HTTP_CLIENT (object);
+ NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
+
+ nm_clear_pointer (&priv->mhandle, curl_multi_cleanup);
+
+ nm_clear_g_source_inst (&priv->mhandle_source_timeout);
+ nm_clear_g_source_inst (&priv->mhandle_source_socket);
+
+ G_OBJECT_CLASS (nm_http_client_parent_class)->dispose (object);
+}
+
+static void
+finalize (GObject *object)
+{
+ NMHttpClient *self = NM_HTTP_CLIENT (object);
+ NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE (self);
+
+ G_OBJECT_CLASS (nm_http_client_parent_class)->finalize (object);
+
+ g_main_context_unref (priv->context);
+
+ curl_global_cleanup ();
+}
+
+static void
+nm_http_client_class_init (NMHttpClientClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = constructed;
+ object_class->dispose = dispose;
+ object_class->finalize = finalize;
+
+ nm_http_client_curl_global_init ();
+}
diff --git a/clients/cloud-setup/nm-http-client.h b/clients/cloud-setup/nm-http-client.h
new file mode 100644
index 0000000000..5972c55686
--- /dev/null
+++ b/clients/cloud-setup/nm-http-client.h
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#ifndef __NM_HTTP_CLIENT_C__
+#define __NM_HTTP_CLIENT_C__
+
+/*****************************************************************************/
+
+typedef struct _NMHttpClient NMHttpClient;
+typedef struct _NMHttpClientClass NMHttpClientClass;
+
+#define NM_TYPE_HTTP_CLIENT (nm_http_client_get_type ())
+#define NM_HTTP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NM_TYPE_HTTP_CLIENT, NMHttpClient))
+#define NM_HTTP_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), NM_TYPE_HTTP_CLIENT, NMHttpClientClass))
+#define NM_IS_HTTP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NM_TYPE_HTTP_CLIENT))
+#define NM_IS_HTTP_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NM_TYPE_HTTP_CLIENT))
+#define NM_HTTP_CLIENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), NM_TYPE_HTTP_CLIENT, NMHttpClientClass))
+
+GType nm_http_client_get_type (void);
+
+NMHttpClient *nm_http_client_new (void);
+
+/*****************************************************************************/
+
+GMainContext *nm_http_client_get_main_context (NMHttpClient *self);
+
+/*****************************************************************************/
+
+void nm_http_client_get (NMHttpClient *self,
+ const char *uri,
+ int timeout_ms,
+ gssize max_data,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean nm_http_client_get_finish (NMHttpClient *self,
+ GAsyncResult *result,
+ long *out_response_code,
+ GBytes **out_response_data,
+ GError **error);
+
+typedef gboolean (*NMHttpClientPollGetCheckFcn) (long response_code,
+ GBytes *response_data,
+ gpointer check_user_data,
+ GError **error);
+
+void nm_http_client_poll_get (NMHttpClient *self,
+ const char *uri,
+ int request_timeout_ms,
+ gssize request_max_data,
+ int poll_timeout_ms,
+ int ratelimit_timeout_ms,
+ GCancellable *cancellable,
+ NMHttpClientPollGetCheckFcn check_fcn,
+ gpointer check_user_data,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean nm_http_client_poll_get_finish (NMHttpClient *self,
+ GAsyncResult *result,
+ long *out_response_code,
+ GBytes **out_response_data,
+ GError **error);
+
+/*****************************************************************************/
+
+#endif /* __NM_HTTP_CLIENT_C__ */
diff --git a/clients/cloud-setup/nmcs-provider-ec2.c b/clients/cloud-setup/nmcs-provider-ec2.c
new file mode 100644
index 0000000000..0bdab8106f
--- /dev/null
+++ b/clients/cloud-setup/nmcs-provider-ec2.c
@@ -0,0 +1,551 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#include "nm-default.h"
+
+#include "nmcs-provider-ec2.h"
+
+#include "nm-cloud-setup-utils.h"
+
+/*****************************************************************************/
+
+#define HTTP_TIMEOUT_MS 3000
+
+#define NM_EC2_HOST "169.254.169.254"
+#define NM_EC2_BASE "http://" NM_EC2_HOST
+#define NM_EC2_API_VERSION "2018-09-24"
+#define NM_EC2_METADATA_URL_BASE /* $NM_EC2_BASE/$NM_EC2_API_VERSION */ "/meta-data/network/interfaces/macs/"
+
+static const char *
+_ec2_base (void)
+{
+ static const char *base_cached = NULL;
+ const char *base;
+
+again:
+ base = g_atomic_pointer_get (&base_cached);
+ if (G_UNLIKELY (!base)) {
+ /* The base URI can be set via environment variable.
+ * This is only for testing, not really to be configurable! */
+ base = g_getenv ("NM_CLOUD_SETUP_EC2_HOST");
+ if ( base
+ && base[0]
+ && !strchr (base, '/')) {
+ if ( NM_STR_HAS_PREFIX (base, "http://")
+ || NM_STR_HAS_PREFIX (base, "https://"))
+ base = g_intern_string (base);
+ else {
+ gs_free char *s = NULL;
+
+ s = g_strconcat ("http://", base, NULL);
+ base = g_intern_string (s);
+ }
+ }
+ if (!base)
+ base = NM_EC2_BASE;
+
+ nm_assert (!NM_STR_HAS_SUFFIX (base, "/"));
+
+ if (!g_atomic_pointer_compare_and_exchange (&base_cached, NULL, base))
+ goto again;
+ }
+
+ return base;
+}
+
+#define _ec2_uri_concat(...) nmcs_utils_uri_build_concat (_ec2_base (), __VA_ARGS__)
+#define _ec2_uri_interfaces(...) _ec2_uri_concat (NM_EC2_API_VERSION, NM_EC2_METADATA_URL_BASE, ##__VA_ARGS__)
+
+/*****************************************************************************/
+
+struct _NMCSProviderEC2 {
+ NMCSProvider parent;
+};
+
+struct _NMCSProviderEC2Class {
+ NMCSProviderClass parent;
+};
+
+G_DEFINE_TYPE (NMCSProviderEC2, nmcs_provider_ec2, NMCS_TYPE_PROVIDER);
+
+/*****************************************************************************/
+
+static gboolean
+_detect_get_meta_data_check_cb (long response_code,
+ GBytes *response_data,
+ gpointer check_user_data,
+ GError **error)
+{
+ return response_code == 200
+ && nmcs_utils_parse_get_full_line (response_data, "ami-id");
+}
+
+static void
+_detect_get_meta_data_done_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ gs_unref_object GTask *task = user_data;
+ gs_free_error GError *get_error = NULL;
+ gs_free_error GError *error = NULL;
+ gboolean success;
+
+ success = nm_http_client_poll_get_finish (NM_HTTP_CLIENT (source),
+ result,
+ NULL,
+ NULL,
+ &get_error);
+
+ if (nm_utils_error_is_cancelled (get_error, FALSE)) {
+ g_task_return_error (task, g_steal_pointer (&get_error));
+ return;
+ }
+
+ if (get_error) {
+ nm_utils_error_set (&error,
+ NM_UTILS_ERROR_UNKNOWN,
+ "failure to get EC2 metadata: %s",
+ get_error->message);
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ if (!success) {
+ nm_utils_error_set (&error,
+ NM_UTILS_ERROR_UNKNOWN,
+ "failure to detect EC2 metadata");
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static void
+detect (NMCSProvider *provider,
+ GTask *task)
+{
+ NMHttpClient *http_client;
+ gs_free char *uri = NULL;
+
+ http_client = nmcs_provider_get_http_client (provider);
+
+ nm_http_client_poll_get (http_client,
+ (uri = _ec2_uri_concat ("latest/meta-data/")),
+ HTTP_TIMEOUT_MS,
+ 256*1024,
+ 7000,
+ 1000,
+ g_task_get_cancellable (task),
+ _detect_get_meta_data_check_cb,
+ NULL,
+ _detect_get_meta_data_done_cb,
+ task);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+ NMCSProviderGetConfigTaskData *get_config_data;
+ GCancellable *cancellable;
+ gulong cancelled_id;
+ guint n_pending;
+} GetConfigIfaceData;
+
+static void
+_get_config_task_return (GetConfigIfaceData *iface_data,
+ GError *error_take)
+{
+ NMCSProviderGetConfigTaskData *get_config_data = iface_data->get_config_data;
+
+ nm_clear_g_cancellable_disconnect (g_task_get_cancellable (get_config_data->task),
+ &iface_data->cancelled_id);
+
+ nm_clear_g_cancellable (&iface_data->cancellable);
+
+ nm_g_slice_free (iface_data);
+
+ if (error_take) {
+ if (nm_utils_error_is_cancelled (error_take, FALSE))
+ _LOGD ("get-config: cancelled");
+ else
+ _LOGD ("get-config: failed: %s", error_take->message);
+ g_task_return_error (get_config_data->task, error_take);
+ } else {
+ _LOGD ("get-config: success");
+ g_task_return_pointer (get_config_data->task,
+ g_hash_table_ref (get_config_data->result_dict),
+ (GDestroyNotify) g_hash_table_unref);
+ }
+
+ g_object_unref (get_config_data->task);
+}
+
+static void
+_get_config_fetch_done_cb (NMHttpClient *http_client,
+ GAsyncResult *result,
+ gpointer user_data,
+ gboolean is_local_ipv4)
+{
+ GetConfigIfaceData *iface_data;
+ NMCSProviderGetConfigTaskData *get_config_data;
+ const char *hwaddr = NULL;
+ gs_unref_bytes GBytes *response_data = NULL;
+ gs_free_error GError *error = NULL;
+ gboolean success;
+ NMCSProviderGetConfigIfaceData *config_iface_data;
+
+ nm_utils_user_data_unpack (user_data, &iface_data, &hwaddr);
+
+ success = nm_http_client_poll_get_finish (http_client,
+ result,
+ NULL,
+ &response_data,
+ &error);
+ if (nm_utils_error_is_cancelled (error, FALSE))
+ return;
+
+ get_config_data = iface_data->get_config_data;
+
+ config_iface_data = g_hash_table_lookup (get_config_data->result_dict, hwaddr);
+
+ if (success) {
+ in_addr_t tmp_addr;
+ int tmp_prefix;
+
+ if (is_local_ipv4) {
+ gs_free const char **s_addrs = NULL;
+ gsize i, len;
+
+ s_addrs = nm_utils_strsplit_set_full (g_bytes_get_data (response_data, NULL), "\n", NM_UTILS_STRSPLIT_SET_FLAGS_STRSTRIP);
+ len = NM_PTRARRAY_LEN (s_addrs);
+
+ nm_assert (!config_iface_data->has_ipv4s);
+ nm_assert (!config_iface_data->ipv4s_arr);
+ config_iface_data->has_ipv4s = TRUE;
+ config_iface_data->ipv4s_len = 0;
+ if (len > 0) {
+ config_iface_data->ipv4s_arr = g_new (in_addr_t, len);
+
+ for (i = 0; i < len; i++) {
+ if (nm_utils_parse_inaddr_bin (AF_INET,
+ s_addrs[i],
+ NULL,
+ &tmp_addr))
+ config_iface_data->ipv4s_arr[config_iface_data->ipv4s_len++] = tmp_addr;
+ }
+ }
+ } else {
+ if (nm_utils_parse_inaddr_prefix_bin (AF_INET,
+ g_bytes_get_data (response_data, NULL),
+ NULL,
+ &tmp_addr,
+ &tmp_prefix)) {
+ nm_assert (!config_iface_data->has_cidr);
+ config_iface_data->has_cidr = TRUE;
+ config_iface_data->cidr_prefix = tmp_prefix;
+ config_iface_data->cidr_addr = tmp_addr;
+ }
+ }
+ }
+
+ if (--iface_data->n_pending > 0)
+ return;
+
+ _get_config_task_return (iface_data, NULL);
+}
+
+static void
+_get_config_fetch_done_cb_subnet_ipv4_cidr_block (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ _get_config_fetch_done_cb (NM_HTTP_CLIENT (source), result, user_data, FALSE);
+}
+
+static void
+_get_config_fetch_done_cb_local_ipv4s (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ _get_config_fetch_done_cb (NM_HTTP_CLIENT (source), result, user_data, TRUE);
+}
+
+static void
+_get_config_fetch_cancelled_cb (GObject *object, gpointer user_data)
+{
+ GetConfigIfaceData *iface_data = user_data;
+
+ if (iface_data->cancelled_id == 0)
+ return;
+
+ nm_clear_g_signal_handler (g_task_get_cancellable (iface_data->get_config_data->task),
+ &iface_data->cancelled_id);
+ _get_config_task_return (iface_data,
+ nm_utils_error_new_cancelled (FALSE, NULL));
+}
+
+typedef struct {
+ NMCSProviderGetConfigTaskData *get_config_data;
+ GHashTable *response_parsed;
+} GetConfigMetadataData;
+
+typedef struct {
+ gssize iface_idx;
+ char path[0];
+} GetConfigMetadataMac;
+
+static void
+_get_config_metadata_ready_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GetConfigMetadataData *metadata_data = user_data;
+ GetConfigIfaceData *iface_data;
+ NMCSProviderGetConfigTaskData *get_config_data = metadata_data->get_config_data;
+ gs_unref_hashtable GHashTable *response_parsed = g_steal_pointer (&metadata_data->response_parsed);
+ gs_free_error GError *error = NULL;
+ GCancellable *cancellable;
+ GetConfigMetadataMac *v_mac_data;
+ const char *v_hwaddr;
+ GHashTableIter h_iter;
+ NMHttpClient *http_client;
+
+ nm_g_slice_free (metadata_data);
+
+ nm_http_client_poll_get_finish (NM_HTTP_CLIENT (source),
+ result,
+ NULL,
+ NULL,
+ &error);
+
+ iface_data = g_slice_new (GetConfigIfaceData);
+ *iface_data = (GetConfigIfaceData) {
+ .get_config_data = get_config_data,
+ .n_pending = 0,
+ };
+
+ if (nm_utils_error_is_cancelled (error, FALSE)) {
+ _get_config_task_return (iface_data, g_steal_pointer (&error));
+ return;
+ }
+
+ /* We ignore errors. Only if we got no response at all, it's a problem.
+ * Otherwise, we proceed with whatever we could fetch. */
+ if (!response_parsed) {
+ _get_config_task_return (iface_data,
+ nm_utils_error_new (NM_UTILS_ERROR_UNKNOWN,
+ "meta data for interfaces not found"));
+ return;
+ }
+
+ cancellable = g_task_get_cancellable (get_config_data->task);
+ if (cancellable) {
+ gulong cancelled_id;
+
+ cancelled_id = g_cancellable_connect (cancellable,
+ G_CALLBACK (_get_config_fetch_cancelled_cb),
+ iface_data,
+ NULL);
+ if (cancelled_id == 0) {
+ _get_config_task_return (iface_data,
+ nm_utils_error_new_cancelled (FALSE, NULL));
+ return;
+ }
+
+ iface_data->cancelled_id = cancelled_id;
+ }
+
+ iface_data->cancellable = g_cancellable_new ();
+
+ http_client = nmcs_provider_get_http_client (g_task_get_source_object (get_config_data->task));
+
+ g_hash_table_iter_init (&h_iter, response_parsed);
+ while (g_hash_table_iter_next (&h_iter, (gpointer *) &v_hwaddr, (gpointer *) &v_mac_data)) {
+ NMCSProviderGetConfigIfaceData *config_iface_data;
+ gs_free char *uri1 = NULL;
+ gs_free char *uri2 = NULL;
+ const char *hwaddr;
+
+ if (!g_hash_table_lookup_extended (get_config_data->result_dict, v_hwaddr, (gpointer *) &hwaddr, (gpointer *) &config_iface_data)) {
+ if (!get_config_data->any) {
+ _LOGD ("get-config: skip fetching meta data for %s (%s)", v_hwaddr, v_mac_data->path);
+ continue;
+ }
+ config_iface_data = nmcs_provider_get_config_iface_data_new (FALSE);
+ g_hash_table_insert (get_config_data->result_dict,
+ (char *) (hwaddr = g_strdup (v_hwaddr)),
+ config_iface_data);
+ }
+
+ nm_assert (config_iface_data->iface_idx == -1);
+ config_iface_data->iface_idx = v_mac_data->iface_idx;
+
+ _LOGD ("get-config: start fetching meta data for #%"G_GSSIZE_FORMAT", %s (%s)", config_iface_data->iface_idx, hwaddr, v_mac_data->path);
+
+ iface_data->n_pending++;
+ nm_http_client_poll_get (http_client,
+ (uri1 = _ec2_uri_interfaces (v_mac_data->path,
+ NM_STR_HAS_SUFFIX (v_mac_data->path, "/")
+ ? ""
+ : "/",
+ "subnet-ipv4-cidr-block")),
+ HTTP_TIMEOUT_MS,
+ 512*1024,
+ 10000,
+ 1000,
+ iface_data->cancellable,
+ NULL,
+ NULL,
+ _get_config_fetch_done_cb_subnet_ipv4_cidr_block,
+ nm_utils_user_data_pack (iface_data, hwaddr));
+
+ iface_data->n_pending++;
+ nm_http_client_poll_get (http_client,
+ (uri2 = _ec2_uri_interfaces (v_mac_data->path,
+ NM_STR_HAS_SUFFIX (v_mac_data->path, "/")
+ ? ""
+ : "/",
+ "local-ipv4s")),
+ HTTP_TIMEOUT_MS,
+ 512*1024,
+ 10000,
+ 1000,
+ iface_data->cancellable,
+ NULL,
+ NULL,
+ _get_config_fetch_done_cb_local_ipv4s,
+ nm_utils_user_data_pack (iface_data, hwaddr));
+ }
+
+ if (iface_data->n_pending == 0)
+ _get_config_task_return (iface_data, NULL);
+}
+
+static gboolean
+_get_config_metadata_ready_check (long response_code,
+ GBytes *response_data,
+ gpointer check_user_data,
+ GError **error)
+{
+ GetConfigMetadataData *metadata_data = check_user_data;
+ gs_unref_hashtable GHashTable *response_parsed = NULL;
+ const guint8 *r_data;
+ gsize r_len;
+ GHashTableIter h_iter;
+ gboolean has_all;
+ const char *c_hwaddr;
+ gssize iface_idx_counter = 0;
+
+ if ( response_code != 200
+ || !response_data) {
+ /* we wait longer. */
+ return FALSE;
+ }
+
+ r_data = g_bytes_get_data (response_data, &r_len);
+
+ while (r_len > 0) {
+ const guint8 *p_eol;
+ const char *p_start;
+ gsize p_start_l;
+ gsize p_start_l_2;
+ char *hwaddr;
+ GetConfigMetadataMac *mac_data;
+
+ p_start = (const char *) r_data;
+
+ p_eol = memchr (r_data, '\n', r_len);
+ if (p_eol) {
+ p_start_l = (p_eol - r_data);
+ r_len -= p_start_l + 1;
+ r_data = &p_eol[1];
+ } else {
+ p_start_l = r_len;
+ r_data = &r_data[r_len];
+ r_len = 0;
+ }
+
+ if (p_start_l == 0)
+ continue;
+
+ p_start_l_2 = p_start_l;
+ if (p_start[p_start_l_2 - 1] == '/') {
+ /* trim the trailing "/". */
+ p_start_l_2--;
+ }
+
+ hwaddr = nmcs_utils_hwaddr_normalize (p_start, p_start_l_2);
+ if (!hwaddr)
+ continue;
+
+ if (!response_parsed)
+ response_parsed = g_hash_table_new_full (nm_str_hash, g_str_equal, g_free, g_free);
+
+ mac_data = g_malloc (sizeof (GetConfigMetadataData) + 1 + p_start_l);
+ mac_data->iface_idx = iface_idx_counter++;
+ memcpy (mac_data->path, p_start, p_start_l);
+ mac_data->path[p_start_l] = '\0';
+
+ g_hash_table_insert (response_parsed, hwaddr, mac_data);
+ }
+
+ has_all = TRUE;
+ g_hash_table_iter_init (&h_iter, metadata_data->get_config_data->result_dict);
+ while (g_hash_table_iter_next (&h_iter, (gpointer *) &c_hwaddr, NULL)) {
+ if ( !response_parsed
+ || !g_hash_table_contains (response_parsed, c_hwaddr)) {
+ has_all = FALSE;
+ break;
+ }
+ }
+
+ nm_clear_pointer (&metadata_data->response_parsed, g_hash_table_unref);
+ metadata_data->response_parsed = g_steal_pointer (&response_parsed);
+ return has_all;
+}
+
+static void
+get_config (NMCSProvider *provider,
+ NMCSProviderGetConfigTaskData *get_config_data)
+{
+ gs_free char *uri = NULL;
+ GetConfigMetadataData *metadata_data;
+
+ metadata_data = g_slice_new (GetConfigMetadataData);
+ *metadata_data = (GetConfigMetadataData) {
+ .get_config_data = get_config_data,
+ };
+
+ /* First we fetch the "macs/". If the caller requested some particular
+ * MAC addresses, then we poll until we see them. They might not yet be
+ * around from the start...
+ */
+ nm_http_client_poll_get (nmcs_provider_get_http_client (provider),
+ (uri = _ec2_uri_interfaces ()),
+ HTTP_TIMEOUT_MS,
+ 256 * 1024,
+ 15000,
+ 1000,
+ g_task_get_cancellable (get_config_data->task),
+ _get_config_metadata_ready_check,
+ metadata_data,
+ _get_config_metadata_ready_cb,
+ metadata_data);
+}
+
+/*****************************************************************************/
+
+static void
+nmcs_provider_ec2_init (NMCSProviderEC2 *self)
+{
+}
+
+static void
+nmcs_provider_ec2_class_init (NMCSProviderEC2Class *klass)
+{
+ NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS (klass);
+
+ provider_class->_name = "ec2";
+ provider_class->detect = detect;
+ provider_class->get_config = get_config;
+}
diff --git a/clients/cloud-setup/nmcs-provider-ec2.h b/clients/cloud-setup/nmcs-provider-ec2.h
new file mode 100644
index 0000000000..763c3af8fc
--- /dev/null
+++ b/clients/cloud-setup/nmcs-provider-ec2.h
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#ifndef __NMCS_PROVIDER_EC2_H__
+#define __NMCS_PROVIDER_EC2_H__
+
+#include "nmcs-provider.h"
+
+/*****************************************************************************/
+
+typedef struct _NMCSProviderEC2 NMCSProviderEC2;
+typedef struct _NMCSProviderEC2Class NMCSProviderEC2Class;
+
+#define NMCS_TYPE_PROVIDER_EC2 (nmcs_provider_ec2_get_type ())
+#define NMCS_PROVIDER_EC2(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NMCS_TYPE_PROVIDER_EC2, NMCSProviderEC2))
+#define NMCS_PROVIDER_EC2_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), NMCS_TYPE_PROVIDER_EC2, NMCSProviderEC2Class))
+#define NMCS_IS_PROVIDER_EC2(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NMCS_TYPE_PROVIDER_EC2))
+#define NMCS_IS_PROVIDER_EC2_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NMCS_TYPE_PROVIDER_EC2))
+#define NMCS_PROVIDER_EC2_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), NMCS_TYPE_PROVIDER_EC2, NMCSProviderEC2Class))
+
+GType nmcs_provider_ec2_get_type (void);
+
+/*****************************************************************************/
+
+#endif /* __NMCS_PROVIDER_EC2_H__ */
diff --git a/clients/cloud-setup/nmcs-provider.c b/clients/cloud-setup/nmcs-provider.c
new file mode 100644
index 0000000000..ab1f12a4c6
--- /dev/null
+++ b/clients/cloud-setup/nmcs-provider.c
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#include "nm-default.h"
+
+#include "nmcs-provider.h"
+
+#include "nm-cloud-setup-utils.h"
+
+/*****************************************************************************/
+
+NM_GOBJECT_PROPERTIES_DEFINE_BASE (
+ PROP_HTTP_CLIENT,
+);
+
+typedef struct _NMCSProviderPrivate {
+ NMHttpClient *http_client;
+} NMCSProviderPrivate;
+
+G_DEFINE_TYPE (NMCSProvider, nmcs_provider, G_TYPE_OBJECT);
+
+#define NMCS_PROVIDER_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMCSProvider, NMCS_IS_PROVIDER)
+
+/*****************************************************************************/
+
+const char *
+nmcs_provider_get_name (NMCSProvider *self)
+{
+ NMCSProviderClass *klass;
+
+ g_return_val_if_fail (NMCS_IS_PROVIDER (self), NULL);
+
+ klass = NMCS_PROVIDER_GET_CLASS (self);
+ nm_assert (klass->_name);
+ return klass->_name;
+}
+
+/*****************************************************************************/
+
+NMHttpClient *
+nmcs_provider_get_http_client (NMCSProvider *self)
+{
+ g_return_val_if_fail (NMCS_IS_PROVIDER (self), NULL);
+
+ return NMCS_PROVIDER_GET_PRIVATE (self)->http_client;
+}
+
+GMainContext *
+nmcs_provider_get_main_context (NMCSProvider *self)
+{
+ g_return_val_if_fail (NMCS_IS_PROVIDER (self), NULL);
+
+ return nm_http_client_get_main_context (NMCS_PROVIDER_GET_PRIVATE (self)->http_client);
+}
+
+/*****************************************************************************/
+
+void
+nmcs_provider_detect (NMCSProvider *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ gs_unref_object GTask *task = NULL;
+
+ g_return_if_fail (NMCS_IS_PROVIDER (self));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = nm_g_task_new (self, cancellable, nmcs_provider_detect, callback, user_data);
+
+ nmcs_wait_for_objects_register (task);
+
+ NMCS_PROVIDER_GET_CLASS (self)->detect (self,
+ g_steal_pointer (&task));
+}
+
+gboolean
+nmcs_provider_detect_finish (NMCSProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (NMCS_IS_PROVIDER (self), FALSE);
+ g_return_val_if_fail (nm_g_task_is_valid (result, self, nmcs_provider_detect), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+/*****************************************************************************/
+
+NMCSProviderGetConfigIfaceData *
+nmcs_provider_get_config_iface_data_new (gboolean was_requested)
+{
+ NMCSProviderGetConfigIfaceData *iface_data;
+
+ iface_data = g_slice_new (NMCSProviderGetConfigIfaceData);
+ *iface_data = (NMCSProviderGetConfigIfaceData) {
+ .iface_idx = -1,
+ .was_requested = was_requested,
+ };
+ return iface_data;
+}
+
+static void
+_iface_data_free (gpointer data)
+{
+ NMCSProviderGetConfigIfaceData *iface_data = data;
+
+ g_free (iface_data->ipv4s_arr);
+
+ nm_g_slice_free (iface_data);
+}
+
+static void
+_get_config_data_free (gpointer data)
+{
+ NMCSProviderGetConfigTaskData *get_config_data = data;
+
+ if (get_config_data->extra_destroy)
+ get_config_data->extra_destroy (get_config_data->extra_data);
+
+ nm_clear_pointer (&get_config_data->result_dict, g_hash_table_unref);
+
+ nm_g_slice_free (get_config_data);
+}
+
+void
+nmcs_provider_get_config (NMCSProvider *self,
+ gboolean any,
+ const char *const*hwaddrs,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ NMCSProviderGetConfigTaskData *get_config_data;
+
+ g_return_if_fail (NMCS_IS_PROVIDER (self));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ get_config_data = g_slice_new (NMCSProviderGetConfigTaskData);
+ *get_config_data = (NMCSProviderGetConfigTaskData) {
+ .task = nm_g_task_new (self, cancellable, nmcs_provider_get_config, callback, user_data),
+ .any = any,
+ .result_dict = g_hash_table_new_full (nm_str_hash,
+ g_str_equal,
+ g_free,
+ _iface_data_free),
+ };
+
+ g_task_set_task_data (get_config_data->task, get_config_data, _get_config_data_free);
+
+ nmcs_wait_for_objects_register (get_config_data->task);
+
+ for (; hwaddrs && hwaddrs[0]; hwaddrs++) {
+ g_hash_table_insert (get_config_data->result_dict,
+ g_strdup (hwaddrs[0]),
+ nmcs_provider_get_config_iface_data_new (TRUE));
+ }
+
+ _LOGD ("get-config: starting");
+
+ NMCS_PROVIDER_GET_CLASS (self)->get_config (self, get_config_data);
+}
+
+GHashTable *
+nmcs_provider_get_config_finish (NMCSProvider *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (NMCS_IS_PROVIDER (self), FALSE);
+ g_return_val_if_fail (nm_g_task_is_valid (result, self, nmcs_provider_get_config), FALSE);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+/*****************************************************************************/
+
+static void
+set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ NMCSProviderPrivate *priv = NMCS_PROVIDER_GET_PRIVATE (object);
+
+ switch (prop_id) {
+ case PROP_HTTP_CLIENT:
+ priv->http_client = g_value_dup_object (value);
+ g_return_if_fail (NM_IS_HTTP_CLIENT (priv->http_client));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+/*****************************************************************************/
+
+static void
+nmcs_provider_init (NMCSProvider *self)
+{
+ NMCSProviderPrivate *priv;
+
+ priv = G_TYPE_INSTANCE_GET_PRIVATE (self, NMCS_TYPE_PROVIDER, NMCSProviderPrivate);
+
+ self->_priv = priv;
+}
+
+static void
+dispose (GObject *object)
+{
+ NMCSProvider *self = NMCS_PROVIDER (object);
+ NMCSProviderPrivate *priv = NMCS_PROVIDER_GET_PRIVATE (self);
+
+ g_clear_object (&priv->http_client);
+
+ G_OBJECT_CLASS (nmcs_provider_parent_class)->dispose (object);
+}
+
+static void
+nmcs_provider_class_init (NMCSProviderClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ g_type_class_add_private (object_class, sizeof (NMCSProviderPrivate));
+
+ object_class->set_property = set_property;
+ object_class->dispose = dispose;
+
+ obj_properties[PROP_HTTP_CLIENT] =
+ g_param_spec_object (NMCS_PROVIDER_HTTP_CLIENT, "", "",
+ NM_TYPE_HTTP_CLIENT,
+ G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, _PROPERTY_ENUMS_LAST, obj_properties);
+}
diff --git a/clients/cloud-setup/nmcs-provider.h b/clients/cloud-setup/nmcs-provider.h
new file mode 100644
index 0000000000..930b6bd80f
--- /dev/null
+++ b/clients/cloud-setup/nmcs-provider.h
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: LGPL-2.1+
+
+#ifndef __NMCS_PROVIDER_H__
+#define __NMCS_PROVIDER_H__
+
+/*****************************************************************************/
+
+#include "nm-http-client.h"
+
+/*****************************************************************************/
+
+typedef struct {
+ in_addr_t *ipv4s_arr;
+ gsize ipv4s_len;
+ gssize iface_idx;
+ in_addr_t cidr_addr;
+ guint8 cidr_prefix;
+ bool has_ipv4s:1;
+ bool has_cidr:1;
+
+ /* TRUE, if the configuration was requested via hwaddrs argument to
+ * nmcs_provider_get_config(). */
+ bool was_requested:1;
+
+} NMCSProviderGetConfigIfaceData;
+
+static inline gboolean
+nmcs_provider_get_config_iface_data_is_valid (const NMCSProviderGetConfigIfaceData *config_data)
+{
+ return config_data
+ && config_data->iface_idx >= 0
+ && config_data->has_cidr
+ && config_data->has_ipv4s;
+}
+
+NMCSProviderGetConfigIfaceData *nmcs_provider_get_config_iface_data_new (gboolean was_requested);
+
+typedef struct {
+ GTask *task;
+ GHashTable *result_dict;
+ gpointer extra_data;
+ GDestroyNotify extra_destroy;
+ bool any:1;
+} NMCSProviderGetConfigTaskData;
+
+#define NMCS_TYPE_PROVIDER (nmcs_provider_get_type ())
+#define NMCS_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NMCS_TYPE_PROVIDER, NMCSProvider))
+#define NMCS_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), NMCS_TYPE_PROVIDER, NMCSProviderClass))
+#define NMCS_IS_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NMCS_TYPE_PROVIDER))
+#define NMCS_IS_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NMCS_TYPE_PROVIDER))
+#define NMCS_PROVIDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), NMCS_TYPE_PROVIDER, NMCSProviderClass))
+
+#define NMCS_PROVIDER_HTTP_CLIENT "http-client"
+
+struct _NMCSProviderPrivate;
+
+typedef struct {
+ GObject parent;
+ struct _NMCSProviderPrivate *_priv;
+} NMCSProvider;
+
+typedef struct {
+ GObjectClass parent;
+ const char *_name;
+
+ void (*detect) (NMCSProvider *self,
+ GTask *task);
+
+ void (*get_config) (NMCSProvider *self,
+ NMCSProviderGetConfigTaskData *get_config_data);
+
+} NMCSProviderClass;
+
+GType nmcs_provider_get_type (void);
+
+/*****************************************************************************/
+
+const char *nmcs_provider_get_name (NMCSProvider *provider);
+
+NMHttpClient *nmcs_provider_get_http_client (NMCSProvider *provider);
+GMainContext *nmcs_provider_get_main_context (NMCSProvider *provider);
+
+/*****************************************************************************/
+
+void nmcs_provider_detect (NMCSProvider *provider,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean nmcs_provider_detect_finish (NMCSProvider *provider,
+ GAsyncResult *result,
+ GError **error);
+
+/*****************************************************************************/
+
+void nmcs_provider_get_config (NMCSProvider *provider,
+ gboolean any,
+ const char *const*hwaddrs,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+GHashTable *nmcs_provider_get_config_finish (NMCSProvider *provider,
+ GAsyncResult *result,
+ GError **error);
+
+#endif /* __NMCS_PROVIDER_H__ */
diff --git a/clients/meson.build b/clients/meson.build
index 71041f7e39..69e0bfef2b 100644
--- a/clients/meson.build
+++ b/clients/meson.build
@@ -26,3 +26,7 @@ endif
if enable_nmtui
subdir('tui')
endif
+
+if enable_nm_cloud_setup
+ subdir('cloud-setup')
+endif
diff --git a/configure.ac b/configure.ac
index 4961e91ce7..7bd70ef7ed 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1014,6 +1014,17 @@ else
fi
AM_CONDITIONAL(BUILD_NMCLI, test "$build_nmcli" = yes)
+AC_ARG_WITH(nm-cloud-setup,
+ AS_HELP_STRING([--with-nm-cloud-setup=yes|no], [Build nm-cloud-setup]))
+if test "$with_nm_cloud_setup" != no; then
+ PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.24.0], [have_libcurl=yes], [have_libcurl=no])
+ if test "$have_libcurl" != "yes"; then
+ AC_MSG_ERROR(--with-nm-cloud-setup requires libcurl library.)
+ fi
+ with_nm_cloud_setup=yes
+fi
+AM_CONDITIONAL(BUILD_NM_CLOUD_SETUP, test "$with_nm_cloud_setup" == yes)
+
AC_ARG_WITH(nmtui,
AS_HELP_STRING([--with-nmtui=yes|no], [Build nmtui]))
if test "$with_nmtui" != no; then
@@ -1303,6 +1314,7 @@ echo " libteamdctl: $enable_teamdctl"
echo " ovs: $enable_ovs"
echo " nmcli: $build_nmcli"
echo " nmtui: $build_nmtui"
+echo " nm-cloud-setup: $with_nm_cloud_setup"
echo " iwd: $ac_with_iwd"
echo
diff --git a/contrib/fedora/rpm/NetworkManager.spec b/contrib/fedora/rpm/NetworkManager.spec
index 531334aba6..373cf0f32e 100644
--- a/contrib/fedora/rpm/NetworkManager.spec
+++ b/contrib/fedora/rpm/NetworkManager.spec
@@ -42,6 +42,8 @@
%global systemd_units NetworkManager.service NetworkManager-wait-online.service NetworkManager-dispatcher.service
+%global systemd_units_cloud_setup nm-cloud-setup.service nm-cloud-setup.timer
+
###############################################################################
%bcond_with meson
@@ -53,6 +55,7 @@
%bcond_without ovs
%bcond_without ppp
%bcond_without nmtui
+%bcond_without nm_cloud_setup
%bcond_without regen_docs
%bcond_with debug
%bcond_with test
@@ -480,6 +483,19 @@ by nm-connection-editor and nm-applet in a non-graphical environment.
%endif
+%if %{with nm_cloud_setup}
+%package cloud-setup
+Summary: Automatically configure NetworkManager in cloud
+Group: System Environment/Base
+Requires: %{name} = %{epoch}:%{version}-%{release}
+Requires: %{name}-libnm%{?_isa} = %{epoch}:%{version}-%{release}
+
+%description cloud-setup
+Installs a nm-cloud-setup tool that can automatically configure
+NetworkManager in cloud setups. Currently only EC2 is supported.
+%endif
+
+
%prep
%autosetup -p1 -n NetworkManager-%{real_version}
@@ -539,6 +555,11 @@ by nm-connection-editor and nm-applet in a non-graphical environment.
%else
-Dnmtui=false \
%endif
+%if %{with nm_cloud_setup}
+ -Dnm_cloud_setup=true \
+%else
+ -Dnm_cloud_setup=false \
+%endif
-Dvapi=true \
-Dintrospection=true \
%if %{with regen_docs}
@@ -660,6 +681,11 @@ intltoolize --automake --copy --force
%else
--with-nmtui=no \
%endif
+%if %{with nm_cloud_setup}
+ --with-nm-cloud-setup=yes \
+%else
+ --with-nm-cloud-setup=no \
+%endif
--enable-vala=yes \
--enable-introspection \
%if %{with regen_docs}
@@ -801,6 +827,12 @@ else
fi
+%if %{with nm_cloud_setup}
+%post cloud-setup
+%systemd_post %{systemd_units_cloud_setup}
+%endif
+
+
%preun
if [ $1 -eq 0 ]; then
# Package removal, not upgrade
@@ -814,6 +846,12 @@ fi
%systemd_preun NetworkManager-wait-online.service NetworkManager-dispatcher.service
+%if %{with nm_cloud_setup}
+%preun cloud-setup
+%systemd_preun %{systemd_units_cloud_setup}
+%endif
+
+
%postun
/usr/bin/udevadm control --reload-rules || :
/usr/bin/udevadm trigger --subsystem-match=net || :
@@ -827,6 +865,12 @@ fi
%endif
+%if %{with nm_cloud_setup}
+%postun cloud-setup
+%systemd_postun %{systemd_units_cloud_setup}
+%endif
+
+
%files
%{dbus_sys_dir}/org.freedesktop.NetworkManager.conf
%{dbus_sys_dir}/nm-dispatcher.conf
@@ -995,5 +1039,15 @@ fi
%endif
+%if %{with nm_cloud_setup}
+%files cloud-setup
+%{_libexecdir}/nm-cloud-setup
+%{systemd_dir}/nm-cloud-setup.service
+%{systemd_dir}/nm-cloud-setup.timer
+%{nmlibdir}/dispatcher.d/90-nm-cloud-setup.sh
+%{nmlibdir}/dispatcher.d/no-wait.d/90-nm-cloud-setup.sh
+%endif
+
+
%changelog
__CHANGELOG__
diff --git a/contrib/fedora/rpm/build_clean.sh b/contrib/fedora/rpm/build_clean.sh
index 2b0eb8abdb..e5eb3ea15e 100755
--- a/contrib/fedora/rpm/build_clean.sh
+++ b/contrib/fedora/rpm/build_clean.sh
@@ -154,6 +154,7 @@ if [[ $NO_DIST != 1 ]]; then
--with-config-logging-backend-default=syslog \
--with-libaudit=yes-disabled-by-default \
--enable-polkit=yes \
+ --with-nm-cloud-setup=yes \
--with-config-dhcp-default=internal \
--with-config-dns-rc-manager-default=symlink \
|| die "Error autogen.sh"
diff --git a/meson.build b/meson.build
index ec2f5a94a8..e17c047f2c 100644
--- a/meson.build
+++ b/meson.build
@@ -661,9 +661,10 @@ if enable_libpsl
endif
config_h.set10('WITH_LIBPSL', enable_libpsl)
+libcurl_dep = dependency('libcurl', version: '>= 7.24.0', required: false)
+
enable_concheck = get_option('concheck')
if enable_concheck
- libcurl_dep = dependency('libcurl', version: '>= 7.24.0', required: false)
assert(libcurl_dep.found(), 'concheck requires libcurl library. Use -Dconcheck=false to disable it')
endif
config_h.set10('WITH_CONCHECK', enable_concheck)
@@ -694,6 +695,11 @@ if enable_nmtui
assert(newt_dep.found(), 'You must have libnewt installed to build nmtui. Use -Dnmtui=false to disable it')
endif
+enable_nm_cloud_setup = get_option('nm_cloud_setup')
+if enable_nm_cloud_setup
+ assert(libcurl_dep.found(), 'nm-cloud-setup requires libcurl library. Use -Dnm_cloud_setup=false to disable it')
+endif
+
more_asserts = get_option('more_asserts')
if more_asserts == 'no'
more_asserts = 0
@@ -910,6 +916,8 @@ meson.add_install_script(
nm_sysconfdir,
enable_docs ? '1' : '0',
enable_ifcfg_rh ? '1' : '0',
+ enable_nm_cloud_setup ? '1' : '0',
+ install_systemdunitdir ? '1' : '0',
)
output = '\nSystem paths:\n'
@@ -954,6 +962,7 @@ output += ' libteamdctl: ' + enable_teamdctl.to_string() + '\n'
output += ' ovs: ' + enable_ovs.to_string() + '\n'
output += ' nmcli: ' + enable_nmcli.to_string() + '\n'
output += ' nmtui: ' + enable_nmtui.to_string() + '\n'
+output += ' nm-cloud-setup: ' + enable_nm_cloud_setup.to_string() + '\n'
output += '\nConfiguration_plugins (main.plugins=' + config_plugins_default + ')\n'
output += ' ifcfg-rh: ' + enable_ifcfg_rh.to_string() + '\n'
output += ' ifupdown: ' + enable_ifupdown.to_string() + '\n'
diff --git a/meson_options.txt b/meson_options.txt
index 3ffd9a54be..a01a010c0e 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -36,6 +36,7 @@ option('teamdctl', type: 'boolean', value: false, description: 'enable Teamd con
option('ovs', type: 'boolean', value: true, description: 'enable Open vSwitch support')
option('nmcli', type: 'boolean', value: true, description: 'Build nmcli')
option('nmtui', type: 'boolean', value: true, description: 'Build nmtui')
+option('nm_cloud_setup', type: 'boolean', value: false, description: 'Build nm_cloud_setup')
option('bluez5_dun', type: 'boolean', value: false, description: 'enable Bluez5 DUN support')
option('ebpf', type: 'combo', choices : ['auto', 'true', 'false'], description: 'Enable eBPF support')
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 2b14042e10..da2247a0a0 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -1,13 +1,15 @@
+clients/cloud-setup/nm-cloud-setup.service.in
+contrib/fedora/rpm/
data/NetworkManager.service.in
+data/org.freedesktop.NetworkManager.policy.in
examples/python/NetworkManager.py
examples/python/systray/eggtrayicon.c
-data/org.freedesktop.NetworkManager.policy.in
+shared/nm-utils/nm-vpn-editor-plugin-call.h
+shared/nm-utils/nm-vpn-plugin-utils.c
vpn-daemons/openvpn
vpn-daemons/pptp
vpn-daemons/vpnc
-contrib/fedora/rpm/
-shared/nm-utils/nm-vpn-editor-plugin-call.h
-shared/nm-utils/nm-vpn-plugin-utils.c
+
# https://bugs.launchpad.net/intltool/+bug/1117944
sub/data/org.freedesktop.NetworkManager.policy.in
diff --git a/tools/meson-post-install.sh b/tools/meson-post-install.sh
index 4e8549a95e..a6fd0961ef 100755
--- a/tools/meson-post-install.sh
+++ b/tools/meson-post-install.sh
@@ -9,6 +9,8 @@ nm_mandir="$6"
nm_sysconfdir="$7"
enable_docs="$8"
enable_ifcfg_rh="$9"
+enable_nm_cloud_setup="${10}"
+install_systemdunitdir="${11}"
[ -n "$DESTDIR" ] && DESTDIR="${DESTDIR%%/}/"
@@ -55,3 +57,8 @@ fi
if [ "$enable_ifcfg_rh" = 1 ]; then
mkdir -p "${DESTDIR}${nm_sysconfdir}/sysconfig/network-scripts"
fi
+
+if [ "$enable_nm_cloud_setup" = 1 -a "$install_systemdunitdir" = 1 ]; then
+ ln -s 'no-wait.d/90-nm-cloud-setup.sh' "${DESTDIR}${nm_pkglibdir}/dispatcher.d/90-nm-cloud-setup.sh"
+fi
+