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