summary refs log tree commit diff
path: root/clients/cloud-setup
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2019-12-18 18:29:24 +0100
committerMichael Biebl <biebl@debian.org>2019-12-18 18:29:24 +0100
commit28028b26b3371756811e95d894f709f4b1207c00 (patch)
tree6fe7316fd743b51042db47601a8ef8814b3134ac /clients/cloud-setup
parente22609983008e1a669196ad64ba3a59ae8c76e0d (diff)
New upstream version 1.22.0 upstream/1.22.0
Diffstat (limited to 'clients/cloud-setup')
-rwxr-xr-xclients/cloud-setup/90-nm-cloud-setup.sh9
-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.h127
-rw-r--r--clients/cloud-setup/nm-cloud-setup.service.in36
-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.c555
-rw-r--r--clients/cloud-setup/nmcs-provider-ec2.h24
-rw-r--r--clients/cloud-setup/nmcs-provider.c245
-rw-r--r--clients/cloud-setup/nmcs-provider.h108
13 files changed, 3456 insertions, 0 deletions
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 00000000..817e2555
--- /dev/null
+++ b/clients/cloud-setup/90-nm-cloud-setup.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+case "$2" in
+    up|dhcp4-change)
+        if systemctl -q is-enabled nm-cloud-setup.service ; then
+            exec systemctl --no-block restart nm-cloud-setup.service
+        fi
+        ;;
+esac
diff --git a/clients/cloud-setup/main.c b/clients/cloud-setup/main.c
new file mode 100644
index 00000000..8250945c
--- /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 (NMCS_ENV_VARIABLE ("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 00000000..e9cd970a
--- /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 00000000..50a59892
--- /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_msec: 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_msec)
+{
+	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_msec >= 0) {
+		timeout_source = nm_g_source_attach (nm_g_timeout_source_new (timeout_msec,
+		                                                              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 00000000..7c3e267a
--- /dev/null
+++ b/clients/cloud-setup/nm-cloud-setup-utils.h
@@ -0,0 +1,127 @@
+// 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"
+
+/*****************************************************************************/
+
+/* mark names for  variables that can be used as configuration. Search
+ * for NMCS_ENV_VARIABLE() to find all honored environment variables. */
+#define NMCS_ENV_VARIABLE(var) ""var""
+
+/*****************************************************************************/
+
+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_msec);
+
+/*****************************************************************************/
+
+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 00000000..69a1a29c
--- /dev/null
+++ b/clients/cloud-setup/nm-cloud-setup.service.in
@@ -0,0 +1,36 @@
+[Unit]
+Description=Automatically configure NetworkManager in cloud
+After=NetworkManager.service
+
+[Service]
+Type=oneshot
+ExecStart=@libexecdir@/nm-cloud-setup
+
+#Environment=NM_CLOUD_SETUP_LOG=TRACE
+
+# Cloud providers are disabled by default. You need to
+# Opt-in by setting the right environment variable for
+# the provider.
+#Environment=NM_CLOUD_SETUP_EC2=yes
+
+CapabilityBoundingSet=
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+NoNewPrivileges=yes
+PrivateDevices=yes
+PrivateTmp=yes
+ProtectControlGroups=yes
+ProtectHome=yes
+ProtectHostname=yes
+ProtectKernelLogs=yes
+ProtectKernelModules=yes
+ProtectKernelTunables=yes
+ProtectSystem=strict
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
+RestrictNamespaces=yes
+RestrictRealtime=yes
+RestrictSUIDSGID=yes
+SystemCallFilter=@system-service
+
+[Install]
+WantedBy=NetworkManager.service
diff --git a/clients/cloud-setup/nm-cloud-setup.timer b/clients/cloud-setup/nm-cloud-setup.timer
new file mode 100644
index 00000000..fd1722a6
--- /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 00000000..94331095
--- /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_msec,
+                    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_msec >= 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_msec > 0) {
+		edata->timeout_source = _source_attach (self,
+		                                        nm_g_timeout_source_new (timeout_msec,
+		                                                                 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_msec, 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_msec >= 0) {
+		priv->mhandle_source_timeout = _source_attach (self,
+		                                               nm_g_timeout_source_new (NM_MIN (timeout_msec, 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 00000000..86ee938e
--- /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_msec,
+                         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 00000000..b6ac5a4c
--- /dev/null
+++ b/clients/cloud-setup/nmcs-provider-ec2.c
@@ -0,0 +1,555 @@
+// 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 mainly for testing, it's not usually supposed to be configured.
+		 * Consider this private API! */
+		base = g_getenv (NMCS_ENV_VARIABLE ("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->_env_provider_enabled = NMCS_ENV_VARIABLE ("NM_CLOUD_SETUP_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 00000000..763c3af8
--- /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 00000000..1f1b6e60
--- /dev/null
+++ b/clients/cloud-setup/nmcs-provider.c
@@ -0,0 +1,245 @@
+// 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;
+	const char *env;
+
+	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);
+
+	env = g_getenv (NMCS_PROVIDER_GET_CLASS (self)->_env_provider_enabled);
+	if (!_nm_utils_ascii_str_to_bool (env, FALSE)) {
+		g_task_return_error (task,
+		                     nm_utils_error_new (NM_UTILS_ERROR_UNKNOWN,
+		                                         "provider is disabled"));
+		return;
+	}
+
+	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 00000000..e5a44da1
--- /dev/null
+++ b/clients/cloud-setup/nmcs-provider.h
@@ -0,0 +1,108 @@
+// 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;
+	const char *_env_provider_enabled;
+
+	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__ */