diff options
| author | Michael Biebl <biebl@debian.org> | 2021-10-01 23:05:04 +0200 |
|---|---|---|
| committer | Michael Biebl <biebl@debian.org> | 2021-10-01 23:05:04 +0200 |
| commit | e74c568b07b50b97873fb4ee1d776dedefbd54d6 (patch) | |
| tree | 3469f17ea9af91f7ff169b890633bda68b0cf76e /src/nm-cloud-setup | |
| parent | bfe522304da217296e2a61040f58e35ec5d6f3f2 (diff) | |
New upstream version 1.32.12 upstream/1.32.12
Diffstat (limited to 'src/nm-cloud-setup')
21 files changed, 5497 insertions, 0 deletions
diff --git a/src/nm-cloud-setup/90-nm-cloud-setup.sh b/src/nm-cloud-setup/90-nm-cloud-setup.sh new file mode 100755 index 00000000..817e2555 --- /dev/null +++ b/src/nm-cloud-setup/90-nm-cloud-setup.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +case "$2" in + up|dhcp4-change) + if systemctl -q is-enabled nm-cloud-setup.service ; then + exec systemctl --no-block restart nm-cloud-setup.service + fi + ;; +esac diff --git a/src/nm-cloud-setup/main.c b/src/nm-cloud-setup/main.c new file mode 100644 index 00000000..916f41da --- /dev/null +++ b/src/nm-cloud-setup/main.c @@ -0,0 +1,683 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "libnm-client-aux-extern/nm-libnm-aux.h" + +#include <linux/rtnetlink.h> + +#include "nm-cloud-setup-utils.h" +#include "nmcs-provider-ec2.h" +#include "nmcs-provider-gcp.h" +#include "nmcs-provider-azure.h" +#include "nmcs-provider-aliyun.h" +#include "libnm-core-aux-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)) + 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, + NMCS_TYPE_PROVIDER_GCP, + NMCS_TYPE_PROVIDER_AZURE, + NMCS_TYPE_PROVIDER_ALIYUN, + }; + 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; +} + +/*****************************************************************************/ + +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; + NMCSProviderGetConfigResult *result; +} GetConfigData; + +static void +_get_config_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + GetConfigData * data = user_data; + nm_auto_free_nmcs_provider_get_config_result NMCSProviderGetConfigResult *result = NULL; + gs_free_error GError *error = NULL; + + result = nmcs_provider_get_config_finish(NMCS_PROVIDER(source), res, &error); + + if (!result) { + if (!nm_utils_error_is_cancelled(error)) + _LOGI("failure to get meta data: %s", error->message); + } else + _LOGD("meta data received"); + + data->result = g_steal_pointer(&result); + g_main_loop_quit(data->main_loop); +} + +static NMCSProviderGetConfigResult * +_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.result; +} + +/*****************************************************************************/ + +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, + const NMCSProviderGetConfigResult * result, + const NMCSProviderGetConfigIfaceData *config_data, + gboolean * out_skipped_single_addr, + gboolean * out_changed) +{ + NMSettingIPConfig * s_ip; + NMActiveConnection *ac; + NMConnection * remote_connection; + NMSettingIPConfig * remote_s_ip = NULL; + gsize i; + gboolean addrs_changed = FALSE; + gboolean rules_changed = FALSE; + gboolean routes_changed = FALSE; + gs_unref_ptrarray GPtrArray *addrs_new = NULL; + gs_unref_ptrarray GPtrArray *rules_new = NULL; + gs_unref_ptrarray GPtrArray *routes_new = NULL; + + NM_SET_OUT(out_skipped_single_addr, FALSE); + NM_SET_OUT(out_changed, FALSE); + + 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; + + if ((ac = nm_device_get_active_connection(device)) + && (remote_connection = NM_CONNECTION(nm_active_connection_get_connection(ac)))) + remote_s_ip = nm_connection_get_setting_ip4_config(remote_connection); + + addrs_new = g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_address_unref); + rules_new = + g_ptr_array_new_full(config_data->ipv4s_len, (GDestroyNotify) nm_ip_routing_rule_unref); + routes_new = g_ptr_array_new_full(config_data->iproutes_len + !!config_data->ipv4s_len, + (GDestroyNotify) nm_ip_route_unref); + + if (remote_s_ip) { + guint len; + guint j; + + len = nm_setting_ip_config_get_num_addresses(remote_s_ip); + for (j = 0; j < len; j++) { + g_ptr_array_add(addrs_new, + nm_ip_address_dup(nm_setting_ip_config_get_address(remote_s_ip, j))); + } + + len = nm_setting_ip_config_get_num_routes(remote_s_ip); + for (j = 0; j < len; j++) { + g_ptr_array_add(routes_new, + nm_ip_route_dup(nm_setting_ip_config_get_route(remote_s_ip, j))); + } + + len = nm_setting_ip_config_get_num_routing_rules(remote_s_ip); + for (j = 0; j < len; j++) { + g_ptr_array_add( + rules_new, + nm_ip_routing_rule_ref(nm_setting_ip_config_get_routing_rule(remote_s_ip, j))); + } + } + + if (result->num_valid_ifaces <= 1 && result->num_ipv4s <= 1) { + /* this setup only has one interface and one IPv4 address (or less). + * We don't need to configure policy routing in this case. */ + NM_SET_OUT(out_skipped_single_addr, TRUE); + } else if (config_data->has_ipv4s && config_data->has_cidr) { + gs_unref_hashtable GHashTable *unique_subnets = + g_hash_table_new(nm_direct_hash, g_direct_equal); + NMIPAddress * addr_entry; + NMIPRoute * route_entry; + NMIPRoutingRule *rule_entry; + in_addr_t gateway; + char sbuf[NM_UTILS_INET_ADDRSTRLEN]; + + for (i = 0; i < config_data->ipv4s_len; i++) { + addr_entry = nm_ip_address_new_binary(AF_INET, + &config_data->ipv4s_arr[i], + config_data->cidr_prefix, + NULL); + nm_assert(addr_entry); + g_ptr_array_add(addrs_new, addr_entry); + } + + if (config_data->has_gateway && config_data->gateway) { + gateway = config_data->gateway; + } else { + gateway = nm_utils_ip4_address_clear_host_address(config_data->cidr_addr, + config_data->cidr_prefix); + if (config_data->cidr_prefix < 32) + ((guint8 *) &gateway)[3] += 1; + } + + for (i = 0; i < config_data->ipv4s_len; i++) { + in_addr_t a = config_data->ipv4s_arr[i]; + + a = nm_utils_ip4_address_clear_host_address(a, config_data->cidr_prefix); + + G_STATIC_ASSERT_EXPR(sizeof(gsize) >= sizeof(in_addr_t)); + if (g_hash_table_add(unique_subnets, GSIZE_TO_POINTER(a))) { + route_entry = + nm_ip_route_new_binary(AF_INET, &a, config_data->cidr_prefix, NULL, 10, NULL); + nm_ip_route_set_attribute(route_entry, + NM_IP_ROUTE_ATTRIBUTE_TABLE, + g_variant_new_uint32(30200 + config_data->iface_idx)); + g_ptr_array_add(routes_new, route_entry); + } + + rule_entry = nm_ip_routing_rule_new(AF_INET); + nm_ip_routing_rule_set_priority(rule_entry, 30200 + config_data->iface_idx); + nm_ip_routing_rule_set_from(rule_entry, + _nm_utils_inet4_ntop(config_data->ipv4s_arr[i], sbuf), + 32); + nm_ip_routing_rule_set_table(rule_entry, 30200 + config_data->iface_idx); + nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL)); + g_ptr_array_add(rules_new, rule_entry); + } + + rule_entry = nm_ip_routing_rule_new(AF_INET); + nm_ip_routing_rule_set_priority(rule_entry, 30350); + nm_ip_routing_rule_set_table(rule_entry, RT_TABLE_MAIN); + nm_ip_routing_rule_set_suppress_prefixlength(rule_entry, 0); + nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL)); + g_ptr_array_add(rules_new, rule_entry); + + route_entry = nm_ip_route_new_binary(AF_INET, &nm_ip_addr_zero, 0, &gateway, 10, NULL); + nm_ip_route_set_attribute(route_entry, + NM_IP_ROUTE_ATTRIBUTE_TABLE, + g_variant_new_uint32(30400 + config_data->iface_idx)); + g_ptr_array_add(routes_new, route_entry); + + for (i = 0; i < config_data->ipv4s_len; i++) { + rule_entry = nm_ip_routing_rule_new(AF_INET); + nm_ip_routing_rule_set_priority(rule_entry, 30400 + config_data->iface_idx); + nm_ip_routing_rule_set_from(rule_entry, + _nm_utils_inet4_ntop(config_data->ipv4s_arr[i], sbuf), + 32); + nm_ip_routing_rule_set_table(rule_entry, 30400 + config_data->iface_idx); + nm_assert(nm_ip_routing_rule_validate(rule_entry, NULL)); + g_ptr_array_add(rules_new, rule_entry); + } + } + + for (i = 0; i < config_data->iproutes_len; ++i) + g_ptr_array_add(routes_new, config_data->iproutes_arr[i]); + + 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, + (NMIPRoute **) routes_new->pdata, + routes_new->len); + + 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 gboolean +_config_one(GCancellable * sigterm_cancellable, + NMClient * nmc, + const NMCSProviderGetConfigResult *result, + guint idx) +{ + const NMCSProviderGetConfigIfaceData *config_data = result->iface_datas_arr[idx]; + const char * hwaddr = config_data->hwaddr; + 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 skipped_single_addr; + 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; + } + + if (config_data->iface_idx >= 100) { + /* since we use the iface_idx to select a table number, the range is limited from + * 0 to 99. Note that the providers are required to provide increasing numbers, + * so this means we bail out after the first 100 devices. */ + _LOGD("config device %s: skip because number of supported interfaces reached", 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)) + _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, + result, + config_data, + &skipped_single_addr, + &changed)) { + _LOGD("config device %s: device has no suitable applied connection. Skip", hwaddr); + return any_changes; + } + + if (!changed) { + if (skipped_single_addr) { + _LOGD("config device %s: device needs no update to applied connection \"%s\" (%s) " + "because there are not multiple IP addresses. Skip", + hwaddr, + nm_connection_get_id(applied_connection), + nm_connection_get_uuid(applied_connection)); + } else { + _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(). Even 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)) { + _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, + const NMCSProviderGetConfigResult *result) +{ + gboolean any_changes = FALSE; + guint i; + + for (i = 0; i < result->n_iface_datas; i++) { + if (_config_one(sigterm_cancellable, nmc, result, i)) + 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; + nm_auto_free_nmcs_provider_get_config_result NMCSProviderGetConfigResult *result = NULL; + gs_free_error GError *error = NULL; + + _nm_logging_enabled_init(g_getenv(NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_LOG"))); + + _LOGD("nm-cloud-setup %s starting...", NM_DIST_VERSION); + + if (argc != 1) { + g_printerr("%s: no command line arguments supported\n", argv[0]); + return EXIT_FAILURE; + } + + sigterm_cancellable = g_cancellable_new(); + + sigterm_source = nm_g_source_attach(nm_g_unix_signal_source_new(SIGTERM, + G_PRIORITY_DEFAULT, + sigterm_handler, + sigterm_cancellable, + NULL), + NULL); + + provider = _provider_detect(sigterm_cancellable); + if (!provider) + goto done; + + nmc_client_new_waitsync(sigterm_cancellable, + &nmc, + &error, + NM_CLIENT_INSTANCE_FLAGS, + (guint) NM_CLIENT_INSTANCE_FLAGS_NO_AUTO_FETCH_PERMISSIONS, + NULL); + + nmcs_wait_for_objects_register(nmc); + nmcs_wait_for_objects_register(nm_client_get_context_busy_watcher(nmc)); + + if (error) { + if (!nm_utils_error_is_cancelled(error)) + _LOGI("failure to talk to NetworkManager: %s", error->message); + goto done; + } + + if (!nm_client_get_nm_running(nmc)) { + _LOGI("NetworkManager is not running"); + goto done; + } + + result = _get_config(sigterm_cancellable, provider, nmc); + if (!result) + goto done; + + if (_config_all(sigterm_cancellable, nmc, result)) + _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(&result, nmcs_provider_get_config_result_free); + 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/src/nm-cloud-setup/meson.build b/src/nm-cloud-setup/meson.build new file mode 100644 index 00000000..ea4ad113 --- /dev/null +++ b/src/nm-cloud-setup/meson.build @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +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 + +libnm_cloud_setup_core = static_library( + 'nm-cloud-setup-core', + sources: files( + 'nm-cloud-setup-utils.c', + 'nm-http-client.c', + 'nmcs-provider-ec2.c', + 'nmcs-provider-gcp.c', + 'nmcs-provider-azure.c', + 'nmcs-provider-aliyun.c', + 'nmcs-provider.c', + ), + dependencies: [ + libnm_dep, + glib_dep, + libcurl_dep, + ], +) + +executable( + 'nm-cloud-setup', + files( + 'main.c', + ), + dependencies: [ + libnm_dep, + glib_dep, + libcurl_dep, + ], + link_with: [ + libnm_cloud_setup_core, + libnmc_base, + libnm_client_aux_extern, + libnm_core_aux_extern, + libnm_core_aux_intern, + libnm_log_null, + libnm_glib_aux, + libnm_std_aux, + libc_siphash, + ], + link_args: ldflags_linker_script_binary, + link_depends: linker_script_binary, + install: true, + install_dir: nm_libexecdir, +) diff --git a/src/nm-cloud-setup/nm-cloud-setup-utils.c b/src/nm-cloud-setup/nm-cloud-setup-utils.c new file mode 100644 index 00000000..ed36b4dd --- /dev/null +++ b/src/nm-cloud-setup/nm-cloud-setup-utils.c @@ -0,0 +1,916 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nm-cloud-setup-utils.h" + +#include <linux/if_ether.h> +#include <linux/if_infiniband.h> + +#include "libnm-glib-aux/nm-time-utils.h" +#include "libnm-glib-aux/nm-logging-base.h" +#include "libnm-glib-aux/nm-str-buf.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_nsec(CLOCK_BOOTTIME); + + g_print("[%" G_GINT64_FORMAT ".%05" G_GINT64_FORMAT "] %s %s\n", + ts / NM_UTILS_NSEC_PER_SEC, + (ts / (NM_UTILS_NSEC_PER_SEC / 10000)) % 10000, + level_str, + msg); +} + +void +_nm_utils_monotonic_timestamp_initialized(const struct timespec *tp, + gint64 offset_sec, + gboolean is_boottime) +{} + +/*****************************************************************************/ + +G_LOCK_DEFINE_STATIC(_wait_for_objects_lock); +static GSList *_wait_for_objects_list; +static GSList *_wait_for_objects_iterate_loops; + +static void +_wait_for_objects_maybe_quit_mainloops_with_lock(void) +{ + GSList *iter; + + if (!_wait_for_objects_list) { + for (iter = _wait_for_objects_iterate_loops; iter; iter = iter->next) + g_main_loop_quit(iter->data); + } +} + +static void +_wait_for_objects_weak_cb(gpointer data, GObject *where_the_object_was) +{ + G_LOCK(_wait_for_objects_lock); + nm_assert(g_slist_find(_wait_for_objects_list, where_the_object_was)); + _wait_for_objects_list = g_slist_remove(_wait_for_objects_list, where_the_object_was); + _wait_for_objects_maybe_quit_mainloops_with_lock(); + G_UNLOCK(_wait_for_objects_lock); +} + +/** + * nmcs_wait_for_objects_register: + * @target: a #GObject to wait for. + * + * Registers @target as a pointer to wait during shutdown. Using + * nmcs_wait_for_objects_iterate_until_done() we keep waiting until + * @target gets destroyed, which means that it gets completely unreferenced. + */ +gpointer +nmcs_wait_for_objects_register(gpointer target) +{ + g_return_val_if_fail(G_IS_OBJECT(target), NULL); + + G_LOCK(_wait_for_objects_lock); + _wait_for_objects_list = g_slist_prepend(_wait_for_objects_list, target); + G_UNLOCK(_wait_for_objects_lock); + + g_object_weak_ref(target, _wait_for_objects_weak_cb, NULL); + return target; +} + +typedef struct { + GMainLoop *loop; + gboolean got_timeout; +} WaitForObjectsData; + +static gboolean +_wait_for_objects_iterate_until_done_timeout_cb(gpointer user_data) +{ + WaitForObjectsData *data = user_data; + + data->got_timeout = TRUE; + g_main_loop_quit(data->loop); + return G_SOURCE_CONTINUE; +} + +static gboolean +_wait_for_objects_iterate_until_done_idle_cb(gpointer user_data) +{ + /* This avoids a race where: + * + * - we check whether there are objects to wait for. + * - the last object to wait for gets removed (issuing g_main_loop_quit()). + * - we run the mainloop (and missed our signal). + * + * It's really a missing feature of GMainLoop where the "is-running" flag is always set to + * TRUE by g_main_loop_run(). That means, you cannot catch a g_main_loop_quit() in a race + * free way while not iterating the loop. + * + * Avoid this, by checking once again after we start running the mainloop. + */ + + G_LOCK(_wait_for_objects_lock); + _wait_for_objects_maybe_quit_mainloops_with_lock(); + G_UNLOCK(_wait_for_objects_lock); + return G_SOURCE_REMOVE; +} + +/** + * nmcs_wait_for_objects_iterate_until_done: + * @context: the #GMainContext to iterate. + * @timeout_msec: timeout or -1 for no timeout. + * + * Iterates the provided @context until all objects that we wait for + * are destroyed. + * + * The purpose of this is to cleanup all objects that we have on exit. That + * is especially because objects have asynchronous operations pending that + * should be cancelled and properly completed during exit. + * + * Returns: %FALSE on timeout or %TRUE if all objects destroyed before timeout. + */ +gboolean +nmcs_wait_for_objects_iterate_until_done(GMainContext *context, int timeout_msec) +{ + nm_auto_unref_gmainloop GMainLoop *loop = g_main_loop_new(context, FALSE); + nm_auto_destroy_and_unref_gsource GSource *timeout_source = NULL; + WaitForObjectsData data; + gboolean has_more_objects; + + G_LOCK(_wait_for_objects_lock); + if (!_wait_for_objects_list) { + G_UNLOCK(_wait_for_objects_lock); + return TRUE; + } + _wait_for_objects_iterate_loops = g_slist_prepend(_wait_for_objects_iterate_loops, loop); + G_UNLOCK(_wait_for_objects_lock); + + data = (WaitForObjectsData){ + .loop = loop, + .got_timeout = FALSE, + }; + + if (timeout_msec >= 0) { + timeout_source = nm_g_source_attach( + nm_g_timeout_source_new(timeout_msec, + G_PRIORITY_DEFAULT, + _wait_for_objects_iterate_until_done_timeout_cb, + &data, + NULL), + context); + } + + has_more_objects = TRUE; + while (has_more_objects && !data.got_timeout) { + nm_auto_destroy_and_unref_gsource GSource *idle_source = NULL; + + idle_source = + nm_g_source_attach(nm_g_idle_source_new(G_PRIORITY_DEFAULT, + _wait_for_objects_iterate_until_done_idle_cb, + &data, + NULL), + context); + + g_main_loop_run(loop); + + G_LOCK(_wait_for_objects_lock); + has_more_objects = (!!_wait_for_objects_list); + if (data.got_timeout || !has_more_objects) + _wait_for_objects_iterate_loops = g_slist_remove(_wait_for_objects_iterate_loops, loop); + G_UNLOCK(_wait_for_objects_lock); + } + + return !data.got_timeout; +} + +/*****************************************************************************/ + +typedef struct { + GTask * task; + GSource * source_timeout; + GSource * source_next_poll; + GMainContext * context; + GCancellable * internal_cancellable; + NMCSUtilsPollProbeStartFcn probe_start_fcn; + NMCSUtilsPollProbeFinishFcn probe_finish_fcn; + gpointer probe_user_data; + gulong cancellable_id; + gint64 last_poll_start_ms; + int sleep_timeout_ms; + int ratelimit_timeout_ms; + bool completed : 1; +} PollTaskData; + +static void +_poll_task_data_free(gpointer data) +{ + PollTaskData *poll_task_data = data; + + nm_assert(G_IS_TASK(poll_task_data->task)); + nm_assert(!poll_task_data->source_next_poll); + nm_assert(!poll_task_data->source_timeout); + nm_assert(poll_task_data->cancellable_id == 0); + + g_main_context_unref(poll_task_data->context); + + nm_g_slice_free(poll_task_data); +} + +static void +_poll_return(PollTaskData *poll_task_data, 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, TRUE); + + 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)) { + /* we already handle this differently. Nothing to do. */ + return; + } + + if (error || is_finished) { + _poll_return(poll_task_data, g_steal_pointer(&error)); + return; + } + + now_ms = nm_utils_get_monotonic_timestamp_msec(); + 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_msec(); + + 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, nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "timeout expired")); + return G_SOURCE_CONTINUE; +} + +static void +_poll_cancelled_cb(GObject *object, gpointer user_data) +{ + PollTaskData *poll_task_data = user_data; + GError * error = NULL; + + 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, 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 (and 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 (asynchronous) 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 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) +{ + 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 with error set. + * Error is always set if (and only if) the function returns %FALSE. + */ +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; + char * hw; + guint8 buf[ETH_ALEN]; + gsize l; + + nm_assert(len >= -1); + + if (len < 0) { + if (!hwaddr) + return NULL; + l = strlen(hwaddr); + } else { + l = len; + if (l > 0 && hwaddr[l - 1] == '\0') { + /* we accept one '\0' at the end of the string. */ + l--; + } + if (memchr(hwaddr, '\0', l)) { + /* but we don't accept other NUL characters in the middle. */ + return NULL; + } + } + + if (l == 0) + return NULL; + + nm_assert(hwaddr); + hw = nm_strndup_a(300, hwaddr, l, &hwaddr_clone); + + g_strstrip(hw); + + /* we cannot use _nm_utils_hwaddr_aton() because that requires a delimiter. + * Azure exposes MAC addresses without delimiter, so accept that too. */ + if (!nm_utils_hexstr2bin_full(hw, + FALSE, + FALSE, + FALSE, + ":-", + sizeof(buf), + buf, + sizeof(buf), + NULL)) + return NULL; + + return nm_utils_hwaddr_ntoa(buf, sizeof(buf)); +} + +/*****************************************************************************/ + +gboolean +nmcs_utils_ipaddr_normalize_bin(int addr_family, + const char *addr, + gssize len, + int * out_addr_family, + gpointer out_addr_bin) +{ + gs_free char *addr_clone = NULL; + char * ad; + gsize l; + + nm_assert(len >= -1); + + if (len < 0) { + if (!addr) + return FALSE; + l = strlen(addr); + } else { + l = len; + if (l > 0 && addr[l - 1] == '\0') { + /* we accept one '\0' at the end of the string. */ + l--; + } + if (memchr(addr, '\0', l)) { + /* but we don't accept other NUL characters in the middle. */ + return FALSE; + } + } + + if (l == 0) + return FALSE; + + nm_assert(addr); + ad = nm_strndup_a(300, addr, l, &addr_clone); + + g_strstrip(ad); + + return nm_utils_parse_inaddr_bin(addr_family, ad, out_addr_family, out_addr_bin); +} + +char * +nmcs_utils_ipaddr_normalize(int addr_family, const char *addr, gssize len) +{ + NMIPAddr ipaddr; + + if (!nmcs_utils_ipaddr_normalize_bin(addr_family, addr, len, &addr_family, &ipaddr)) + return NULL; + + return nm_utils_inet_ntop_dup(addr_family, &ipaddr); +} + +/*****************************************************************************/ + +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) +{ + NMStrBuf strbuf = NM_STR_BUF_INIT(NM_UTILS_GET_NEXT_REALLOC_SIZE_104, FALSE); + + nm_assert(base); + nm_assert(base[0]); + nm_assert(!NM_STR_HAS_SUFFIX(base, "/")); + + nm_str_buf_append(&strbuf, 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 + nm_str_buf_append_c(&strbuf, '/'); + + while (n_components > 0) { + if (!components[0]) { + /* we allow NULL, to indicate nothing to append */ + } else + nm_str_buf_append(&strbuf, components[0]); + components++; + n_components--; + } + + return nm_str_buf_finalize(&strbuf, NULL); +} + +const char * +nmcs_utils_uri_complete_interned(const char *uri) +{ + gs_free char *s = NULL; + + if (nm_str_is_empty(uri)) + return NULL; + if (NM_STR_HAS_PREFIX(uri, "http://") || NM_STR_HAS_PREFIX(uri, "https://") || strchr(uri, '/')) + return g_intern_string(uri); + + s = g_strconcat("http://", uri, NULL); + return g_intern_string(s); +} + +/*****************************************************************************/ + +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/src/nm-cloud-setup/nm-cloud-setup-utils.h b/src/nm-cloud-setup/nm-cloud-setup-utils.h new file mode 100644 index 00000000..3ca7298f --- /dev/null +++ b/src/nm-cloud-setup/nm-cloud-setup-utils.h @@ -0,0 +1,154 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#ifndef __NM_CLOUD_SETUP_UTILS_H__ +#define __NM_CLOUD_SETUP_UTILS_H__ + +#include "libnm-glib-aux/nm-logging-fwd.h" + +/*****************************************************************************/ + +/* mark names for variables that can be used as configuration. Search + * for NMCS_ENV_VARIABLE() to find all honored environment variables. */ +#define NMCS_ENV_VARIABLE(var) "" var "" + +/*****************************************************************************/ + +extern volatile NMLogLevel _nm_logging_configured_level; + +static inline gboolean +nm_logging_enabled(NMLogLevel level) +{ + return level >= _nm_logging_configured_level; +} + +void _nm_logging_enabled_init(const char *level_str); + +void _nm_log_impl_cs(NMLogLevel level, const char *fmt, ...) _nm_printf(2, 3); + +#define _nm_log(level, ...) _nm_log_impl_cs((level), __VA_ARGS__); + +#define _NMLOG(level, ...) \ + G_STMT_START \ + { \ + const NMLogLevel _level = (level); \ + \ + if (nm_logging_enabled(_level)) { \ + _nm_log(_level, __VA_ARGS__); \ + } \ + } \ + G_STMT_END + +/*****************************************************************************/ + +#ifndef NM_DIST_VERSION + #define NM_DIST_VERSION VERSION +#endif + +/*****************************************************************************/ + +gpointer nmcs_wait_for_objects_register(gpointer target); + +gboolean nmcs_wait_for_objects_iterate_until_done(GMainContext *context, int timeout_msec); + +/*****************************************************************************/ + +typedef void (*NMCSUtilsPollProbeStartFcn)(GCancellable * cancellable, + gpointer probe_user_data, + GAsyncReadyCallback callback, + gpointer user_data); + +typedef gboolean (*NMCSUtilsPollProbeFinishFcn)(GObject * source, + GAsyncResult *result, + gpointer probe_user_data, + GError ** error); + +void nmcs_utils_poll(int poll_timeout_ms, + int ratelimit_timeout_ms, + int sleep_timeout_ms, + NMCSUtilsPollProbeStartFcn probe_start_fcn, + NMCSUtilsPollProbeFinishFcn probe_finish_fcn, + gpointer probe_user_data, + GCancellable * cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean nmcs_utils_poll_finish(GAsyncResult *result, gpointer *probe_user_data, GError **error); + +/*****************************************************************************/ + +char *nmcs_utils_hwaddr_normalize(const char *hwaddr, gssize len); + +static inline char * +nmcs_utils_hwaddr_normalize_gbytes(GBytes *hwaddr) +{ + const char *str; + gsize len; + + str = g_bytes_get_data(hwaddr, &len); + return nmcs_utils_hwaddr_normalize(str, len); +} + +/*****************************************************************************/ + +gboolean nmcs_utils_ipaddr_normalize_bin(int addr_family, + const char *addr, + gssize len, + int * out_addr_family, + gpointer out_addr_bin); + +char *nmcs_utils_ipaddr_normalize(int addr_family, const char *addr, gssize len); + +static inline char * +nmcs_utils_ipaddr_normalize_gbytes(int addr_family, GBytes *addr) +{ + const char *str; + gsize len; + + str = g_bytes_get_data(addr, &len); + return nmcs_utils_ipaddr_normalize(addr_family, str, 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__)) + +const char *nmcs_utils_uri_complete_interned(const char *uri); + +/*****************************************************************************/ + +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/src/nm-cloud-setup/nm-cloud-setup.service.in b/src/nm-cloud-setup/nm-cloud-setup.service.in new file mode 100644 index 00000000..f4b0e263 --- /dev/null +++ b/src/nm-cloud-setup/nm-cloud-setup.service.in @@ -0,0 +1,43 @@ +[Unit] +Description=Automatically configure NetworkManager in cloud +Documentation=man:nm-cloud-setup(8) +After=NetworkManager.service + +[Service] +Type=oneshot +ExecStart=@libexecdir@/nm-cloud-setup + +#Environment=NM_CLOUD_SETUP_LOG=TRACE + +# Cloud providers are disabled by default. You need to +# Opt-in by setting the right environment variable for +# the provider. +# +# Create a drop-in file to overwrite these variables or +# use systemctl edit. +#Environment=NM_CLOUD_SETUP_EC2=yes +#Environment=NM_CLOUD_SETUP_GCP=yes +#Environment=NM_CLOUD_SETUP_AZURE=yes +#Environment=NM_CLOUD_SETUP_ALIYUN=yes + +CapabilityBoundingSet= +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateDevices=yes +PrivateTmp=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectSystem=strict +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallFilter=@system-service + +[Install] +WantedBy=NetworkManager.service diff --git a/src/nm-cloud-setup/nm-cloud-setup.timer b/src/nm-cloud-setup/nm-cloud-setup.timer new file mode 100644 index 00000000..fd1722a6 --- /dev/null +++ b/src/nm-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/src/nm-cloud-setup/nm-http-client.c b/src/nm-cloud-setup/nm-http-client.c new file mode 100644 index 00000000..618ab1be --- /dev/null +++ b/src/nm-cloud-setup/nm-http-client.c @@ -0,0 +1,801 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nm-http-client.h" + +#include <curl/curl.h> + +#include "nm-cloud-setup-utils.h" +#include "libnm-glib-aux/nm-str-buf.h" + +#define NM_CURL_DEBUG 0 + +/*****************************************************************************/ + +typedef struct { + GMainContext *context; + CURLM * mhandle; + GSource * mhandle_source_timeout; + GHashTable * source_sockets_hashtable; +} 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; + NMStrBuf recv_data; + struct curl_slist *headers; + 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); + + nm_str_buf_destroy(&edata->recv_data); + if (edata->headers) + curl_slist_free_all(edata->headers); + 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)) + _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(nm_str_buf_get_str(&edata->recv_data), + 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 = nm_str_buf_finalize_to_gbytes(&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; + + nm_str_buf_append_len(&edata->recv_data, ptr, nconsume); + return nconsume; +} + +static gboolean +_get_timeout_cb(gpointer user_data) +{ + _ehandle_complete( + user_data, + g_error_new_literal(NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "HTTP request timed out")); + return G_SOURCE_REMOVE; +} + +static void +_get_cancelled_cb(GObject *object, gpointer user_data) +{ + EHandleData *edata = user_data; + GError * error = NULL; + + nm_clear_g_signal_handler(g_task_get_cancellable(edata->task), &edata->cancellable_id); + nm_utils_error_set_cancelled(&error, FALSE, NULL); + _ehandle_complete(edata, error); +} + +void +nm_http_client_get(NMHttpClient * self, + const char * url, + int timeout_msec, + gssize max_data, + const char *const * http_headers, + GCancellable * cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + NMHttpClientPrivate *priv; + EHandleData * edata; + guint i; + + g_return_if_fail(NM_IS_HTTP_CLIENT(self)); + g_return_if_fail(url); + g_return_if_fail(!cancellable || G_IS_CANCELLABLE(cancellable)); + g_return_if_fail(timeout_msec >= 0); + g_return_if_fail(max_data >= -1); + + priv = NM_HTTP_CLIENT_GET_PRIVATE(self); + + edata = g_slice_new(EHandleData); + *edata = (EHandleData){ + .task = nm_g_task_new(self, cancellable, nm_http_client_get, callback, user_data), + .recv_data = NM_STR_BUF_INIT(0, FALSE), + .max_data = max_data, + .url = g_strdup(url), + .headers = NULL, + }; + + 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 (http_headers) { + for (i = 0; http_headers[i]; ++i) { + struct curl_slist *tmp; + + tmp = curl_slist_append(edata->headers, http_headers[i]); + if (!tmp) { + curl_slist_free_all(tmp); + _LOGE("curl: curl_slist_append() failed adding %s", http_headers[i]); + continue; + } + edata->headers = tmp; + } + + curl_easy_setopt(edata->ehandle, CURLOPT_HTTPHEADER, edata->headers); + } + + if (timeout_msec > 0) { + edata->timeout_source = _source_attach(self, + nm_g_timeout_source_new(timeout_msec, + G_PRIORITY_DEFAULT, + _get_timeout_cb, + edata, + NULL)); + } + + curl_multi_add_handle(priv->mhandle, edata->ehandle); + + if (cancellable) { + gulong signal_id; + + signal_id = g_cancellable_connect(cancellable, G_CALLBACK(_get_cancelled_cb), edata, NULL); + if (signal_id == 0) { + /* the request is already cancelled. Return. */ + return; + } + edata->cancellable_id = signal_id; + } +} + +/** + * nm_http_client_get_finish: + * @self: the #NMHttpClient instance + * @result: the #GAsyncResult which to complete. + * @out_response_code: (allow-none) (out): the HTTP response code or -1 on other error. + * @out_response_data: (allow-none) (transfer full): the HTTP response data, if any. + * The GBytes buffer is guaranteed to have a trailing NUL character *after* the + * returned buffer size. That means, you can always trust that the buffer is NUL terminated + * and that there is one additional hidden byte after the data. + * Also, the returned buffer is allocated just for you. While GBytes is immutable, you are + * allowed to modify the buffer as it's not used by anybody else. + * @error: the error + * + * Returns: %TRUE on success or %FALSE with an error code. + */ +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); + + nm_assert(!error || (!!get_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; + const char *const * http_headers; + 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); + g_strfreev((char **) poll_get_data->http_headers); + + 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, + poll_get_data->http_headers, + 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; + gs_unref_bytes GBytes *response_data = NULL; + long response_code = -1; + + success = nm_http_client_get_finish(g_task_get_source_object(poll_get_data->task), + result, + &response_code, + &response_data, + &local_error); + + nm_assert((!!success) == (!local_error)); + + if (local_error) { + if (nm_utils_error_is_cancelled(local_error)) { + g_propagate_error(error, g_steal_pointer(&local_error)); + return TRUE; + } + /* any other error. Continue polling. */ + 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) { + /* Not yet ready. Continue polling. */ + 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); + + nm_assert((!!success) == (!error)); + + if (error) + g_task_return_error(poll_get_data->task, g_steal_pointer(&error)); + else + g_task_return_boolean(poll_get_data->task, TRUE); + + 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, + const char *const * http_headers, + 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, + .http_headers = NM_CAST_STRV_CC(g_strdupv((char **) http_headers)), + }; + + 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); + + nm_assert((!!success) == (!local_error)); + + 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) +{ + GSource * source_socket; + NMHttpClient * self = user_data; + NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self); + + (void) _NM_ENSURE_TYPE(int, fd); + + g_hash_table_remove(priv->source_sockets_hashtable, GINT_TO_POINTER(fd)); + + 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) { + source_socket = nm_g_unix_fd_source_new(fd, + condition, + G_PRIORITY_DEFAULT, + _mhandle_socket_cb, + self, + NULL); + g_source_attach(source_socket, priv->context); + + g_hash_table_insert(priv->source_sockets_hashtable, GINT_TO_POINTER(fd), source_socket); + } + } + + return CURLM_OK; +} + +static gboolean +_mhandle_timeout_cb(gpointer user_data) +{ + _mhandle_action(user_data, CURL_SOCKET_TIMEOUT, 0); + return G_SOURCE_REMOVE; +} + +static int +_mhandle_timerfunction_cb(CURLM *multi, long timeout_msec, void *user_data) +{ + NMHttpClient * self = user_data; + NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self); + + nm_clear_pointer(&priv->mhandle_source_timeout, nm_g_source_destroy_and_unref); + if (timeout_msec >= 0) { + priv->mhandle_source_timeout = + _source_attach(self, + nm_g_timeout_source_new(NM_MIN(timeout_msec, G_MAXINT), + G_PRIORITY_DEFAULT, + _mhandle_timeout_cb, + self, + NULL)); + } + return 0; +} + +/*****************************************************************************/ + +static void +nm_http_client_init(NMHttpClient *self) +{ + NMHttpClientPrivate *priv = NM_HTTP_CLIENT_GET_PRIVATE(self); + + priv->source_sockets_hashtable = + g_hash_table_new_full(nm_direct_hash, + NULL, + NULL, + (GDestroyNotify) nm_g_source_destroy_and_unref); +} + +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); + } + + 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_pointer(&priv->source_sockets_hashtable, g_hash_table_unref); + + nm_clear_g_source_inst(&priv->mhandle_source_timeout); + + 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/src/nm-cloud-setup/nm-http-client.h b/src/nm-cloud-setup/nm-http-client.h new file mode 100644 index 00000000..862b7338 --- /dev/null +++ b/src/nm-cloud-setup/nm-http-client.h @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#ifndef __NM_HTTP_CLIENT_C__ +#define __NM_HTTP_CLIENT_C__ + +/*****************************************************************************/ + +typedef struct _NMHttpClient NMHttpClient; +typedef struct _NMHttpClientClass NMHttpClientClass; + +#define NM_TYPE_HTTP_CLIENT (nm_http_client_get_type()) +#define NM_HTTP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NM_TYPE_HTTP_CLIENT, NMHttpClient)) +#define NM_HTTP_CLIENT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NM_TYPE_HTTP_CLIENT, NMHttpClientClass)) +#define NM_IS_HTTP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NM_TYPE_HTTP_CLIENT)) +#define NM_IS_HTTP_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NM_TYPE_HTTP_CLIENT)) +#define NM_HTTP_CLIENT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NM_TYPE_HTTP_CLIENT, NMHttpClientClass)) + +GType nm_http_client_get_type(void); + +NMHttpClient *nm_http_client_new(void); + +/*****************************************************************************/ + +GMainContext *nm_http_client_get_main_context(NMHttpClient *self); + +/*****************************************************************************/ + +void nm_http_client_get(NMHttpClient * self, + const char * uri, + int timeout_msec, + gssize max_data, + const char *const * http_headers, + 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, + const char *const * http_headers, + 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/src/nm-cloud-setup/nmcs-provider-aliyun.c b/src/nm-cloud-setup/nmcs-provider-aliyun.c new file mode 100644 index 00000000..126980fd --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-aliyun.c @@ -0,0 +1,507 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nmcs-provider-aliyun.h" + +#include <arpa/inet.h> + +#include "nm-cloud-setup-utils.h" + +/*****************************************************************************/ + +#define HTTP_TIMEOUT_MS 3000 + +#define NM_ALIYUN_HOST "100.100.100.200" +#define NM_ALIYUN_BASE "http://" NM_ALIYUN_HOST +#define NM_ALIYUN_API_VERSION "2016-01-01" +#define NM_ALIYUN_METADATA_URL_BASE /* $NM_ALIYUN_BASE/$NM_ALIYUN_API_VERSION */ \ + "/meta-data/network/interfaces/macs/" + +static const char * +_aliyun_base(void) +{ + static const char *base_cached = NULL; + const char * base; + +again: + base = g_atomic_pointer_get(&base_cached); + if (G_UNLIKELY(!base)) { + /* The base URI can be set via environment variable. + * This is mainly for testing, it's not usually supposed to be configured. + * Consider this private API! */ + base = g_getenv(NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_ALIYUN_HOST")); + + if (!g_atomic_pointer_compare_and_exchange(&base_cached, NULL, base)) + goto again; + } + base = nmcs_utils_uri_complete_interned(base) ?: ("" NM_ALIYUN_BASE); + return base; +} + +#define _aliyun_uri_concat(...) nmcs_utils_uri_build_concat(_aliyun_base(), __VA_ARGS__) +#define _aliyun_uri_interfaces(...) \ + _aliyun_uri_concat(NM_ALIYUN_API_VERSION, NM_ALIYUN_METADATA_URL_BASE, ##__VA_ARGS__) + +/*****************************************************************************/ + +struct _NMCSProviderAliyun { + NMCSProvider parent; +}; + +struct _NMCSProviderAliyunClass { + NMCSProviderClass parent; +}; + +G_DEFINE_TYPE(NMCSProviderAliyun, nmcs_provider_aliyun, NMCS_TYPE_PROVIDER); + +/*****************************************************************************/ + +static void +filter_chars(char *str, const char *chars) +{ + gsize i; + gsize j; + + for (i = 0, j = 0; str[i]; i++) { + if (!strchr(chars, str[i])) + str[j++] = str[i]; + } + str[j] = '\0'; +} + +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; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error); + + if (nm_utils_error_is_cancelled(get_error)) { + 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 ALIYUN metadata: %s", + get_error->message); + 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 = _aliyun_uri_concat(NM_ALIYUN_API_VERSION "/meta-data/")), + HTTP_TIMEOUT_MS, + 256 * 1024, + 7000, + 1000, + NULL, + g_task_get_cancellable(task), + NULL, + NULL, + _detect_get_meta_data_done_cb, + task); +} + +/*****************************************************************************/ + +typedef enum { + GET_CONFIG_FETCH_DONE_TYPE_SUBNET_VPC_CIDR_BLOCK, + GET_CONFIG_FETCH_DONE_TYPE_PRIVATE_IPV4S, + GET_CONFIG_FETCH_DONE_TYPE_NETMASK, + GET_CONFIG_FETCH_DONE_TYPE_GATEWAY, +} GetConfigFetchDoneType; + +static void +_get_config_fetch_done_cb(NMHttpClient * http_client, + GAsyncResult * result, + gpointer user_data, + GetConfigFetchDoneType fetch_type) +{ + NMCSProviderGetConfigTaskData *get_config_data; + gs_unref_bytes GBytes *response = NULL; + gs_free_error GError * error = NULL; + NMCSProviderGetConfigIfaceData *config_iface_data; + in_addr_t tmp_addr; + int tmp_prefix; + in_addr_t netmask_bin; + in_addr_t gateway_bin; + gs_free const char ** s_addrs = NULL; + gsize i; + gsize len; + + nm_utils_user_data_unpack(user_data, &get_config_data, &config_iface_data); + + nm_http_client_poll_get_finish(http_client, result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + if (error) + goto out; + + switch (fetch_type) { + case GET_CONFIG_FETCH_DONE_TYPE_PRIVATE_IPV4S: + + s_addrs = nm_utils_strsplit_set_full(g_bytes_get_data(response, NULL), + ",", + 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++) { + filter_chars((char *) s_addrs[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; + } + } + } + break; + + case GET_CONFIG_FETCH_DONE_TYPE_SUBNET_VPC_CIDR_BLOCK: + + if (nm_utils_parse_inaddr_prefix_bin(AF_INET, + g_bytes_get_data(response, NULL), + NULL, + &tmp_addr, + &tmp_prefix)) { + nm_assert(!config_iface_data->has_cidr); + config_iface_data->has_cidr = TRUE; + config_iface_data->cidr_addr = tmp_addr; + } + break; + + case GET_CONFIG_FETCH_DONE_TYPE_NETMASK: + + if (nm_utils_parse_inaddr_bin(AF_INET, + g_bytes_get_data(response, NULL), + NULL, + &netmask_bin)) { + config_iface_data->cidr_prefix = nm_utils_ip4_netmask_to_prefix(netmask_bin); + }; + break; + + case GET_CONFIG_FETCH_DONE_TYPE_GATEWAY: + + if (nm_utils_parse_inaddr_bin(AF_INET, + g_bytes_get_data(response, NULL), + NULL, + &gateway_bin)) { + config_iface_data->has_gateway = TRUE; + config_iface_data->gateway = gateway_bin; + }; + break; + } + +out: + get_config_data->n_pending--; + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); +} + +static void +_get_config_fetch_done_cb_vpc_cidr_block(GObject *source, GAsyncResult *result, gpointer user_data) +{ + _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), + result, + user_data, + GET_CONFIG_FETCH_DONE_TYPE_SUBNET_VPC_CIDR_BLOCK); +} + +static void +_get_config_fetch_done_cb_private_ipv4s(GObject *source, GAsyncResult *result, gpointer user_data) +{ + _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), + result, + user_data, + GET_CONFIG_FETCH_DONE_TYPE_PRIVATE_IPV4S); +} + +static void +_get_config_fetch_done_cb_netmask(GObject *source, GAsyncResult *result, gpointer user_data) +{ + _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), + result, + user_data, + GET_CONFIG_FETCH_DONE_TYPE_NETMASK); +} + +static void +_get_config_fetch_done_cb_gateway(GObject *source, GAsyncResult *result, gpointer user_data) +{ + _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), + result, + user_data, + GET_CONFIG_FETCH_DONE_TYPE_GATEWAY); +} + +typedef struct { + gssize iface_idx; + char path[0]; +} GetConfigMetadataMac; + +static void +_get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMCSProviderGetConfigTaskData *get_config_data; + gs_unref_hashtable GHashTable *response_parsed = NULL; + gs_free_error GError *error = NULL; + GetConfigMetadataMac *v_mac_data; + const char * v_hwaddr; + GHashTableIter h_iter; + NMHttpClient * http_client; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = user_data; + + response_parsed = g_steal_pointer(&get_config_data->extra_data); + get_config_data->extra_data_destroy = NULL; + + /* 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) { + _nmcs_provider_get_config_task_maybe_return( + get_config_data, + nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "meta data for interfaces not found")); + return; + } + + 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; + gs_free char * uri3 = NULL; + gs_free char * uri4 = NULL; + + config_iface_data = g_hash_table_lookup(get_config_data->result_dict, v_hwaddr); + + if (!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_create(get_config_data->result_dict, + FALSE, + v_hwaddr); + } + + 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, + config_iface_data->hwaddr, + v_mac_data->path); + + get_config_data->n_pending++; + nm_http_client_poll_get( + http_client, + (uri1 = _aliyun_uri_interfaces(v_mac_data->path, + NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/", + "vpc-cidr-block")), + HTTP_TIMEOUT_MS, + 512 * 1024, + 10000, + 1000, + NULL, + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fetch_done_cb_vpc_cidr_block, + nm_utils_user_data_pack(get_config_data, config_iface_data)); + + get_config_data->n_pending++; + nm_http_client_poll_get( + http_client, + (uri2 = _aliyun_uri_interfaces(v_mac_data->path, + NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/", + "private-ipv4s")), + HTTP_TIMEOUT_MS, + 512 * 1024, + 10000, + 1000, + NULL, + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fetch_done_cb_private_ipv4s, + nm_utils_user_data_pack(get_config_data, config_iface_data)); + + get_config_data->n_pending++; + nm_http_client_poll_get( + http_client, + (uri3 = _aliyun_uri_interfaces(v_mac_data->path, + NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/", + "netmask")), + HTTP_TIMEOUT_MS, + 512 * 1024, + 10000, + 1000, + NULL, + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fetch_done_cb_netmask, + nm_utils_user_data_pack(get_config_data, config_iface_data)); + + get_config_data->n_pending++; + nm_http_client_poll_get( + http_client, + (uri4 = _aliyun_uri_interfaces(v_mac_data->path, + NM_STR_HAS_SUFFIX(v_mac_data->path, "/") ? "" : "/", + "gateway")), + HTTP_TIMEOUT_MS, + 512 * 1024, + 10000, + 1000, + NULL, + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fetch_done_cb_gateway, + nm_utils_user_data_pack(get_config_data, config_iface_data)); + } + + _nmcs_provider_get_config_task_maybe_return(get_config_data, NULL); +} + +static gboolean +_get_config_metadata_ready_check(long response_code, + GBytes * response, + gpointer check_user_data, + GError **error) +{ + NMCSProviderGetConfigTaskData *get_config_data = check_user_data; + gs_unref_hashtable GHashTable *response_parsed = NULL; + const guint8 * r_data; + const char * cur_line; + gsize r_len; + gsize cur_line_len; + GHashTableIter h_iter; + gboolean has_all; + const char * c_hwaddr; + gssize iface_idx_counter = 0; + + if (response_code != 200 || !response) { + /* we wait longer. */ + return FALSE; + } + + r_data = g_bytes_get_data(response, &r_len); + /* NMHttpClient guarantees that there is a trailing NUL after the data. */ + nm_assert(r_data[r_len] == 0); + + while (nm_utils_parse_next_line((const char **) &r_data, &r_len, &cur_line, &cur_line_len)) { + GetConfigMetadataMac *mac_data; + char * hwaddr; + + if (cur_line_len == 0) + continue; + + /* Truncate the string. It's safe to do, because we own @response an it has an + * extra NUL character after the buffer. */ + ((char *) cur_line)[cur_line_len] = '\0'; + + hwaddr = nmcs_utils_hwaddr_normalize( + cur_line, + cur_line[cur_line_len - 1u] == '/' ? (gssize) (cur_line_len - 1u) : -1); + 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(GetConfigMetadataMac) + 1u + cur_line_len); + mac_data->iface_idx = iface_idx_counter++; + memcpy(mac_data->path, cur_line, cur_line_len + 1u); + + /* here we will ignore duplicate responses. */ + g_hash_table_insert(response_parsed, hwaddr, mac_data); + } + + has_all = TRUE; + g_hash_table_iter_init(&h_iter, 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(&get_config_data->extra_data, g_hash_table_unref); + if (response_parsed) { + get_config_data->extra_data = g_steal_pointer(&response_parsed); + get_config_data->extra_data_destroy = (GDestroyNotify) g_hash_table_unref; + } + return has_all; +} + +static void +get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data) +{ + gs_free char *uri = NULL; + + /* 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 = _aliyun_uri_interfaces()), + HTTP_TIMEOUT_MS, + 256 * 1024, + 15000, + 1000, + NULL, + get_config_data->intern_cancellable, + _get_config_metadata_ready_check, + get_config_data, + _get_config_metadata_ready_cb, + get_config_data); +} + +/*****************************************************************************/ + +static void +nmcs_provider_aliyun_init(NMCSProviderAliyun *self) +{} + +static void +nmcs_provider_aliyun_class_init(NMCSProviderAliyunClass *klass) +{ + NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass); + + provider_class->_name = "aliyun"; + provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_ALIYUN"); + provider_class->detect = detect; + provider_class->get_config = get_config; +} diff --git a/src/nm-cloud-setup/nmcs-provider-aliyun.h b/src/nm-cloud-setup/nmcs-provider-aliyun.h new file mode 100644 index 00000000..6e733a11 --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-aliyun.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#ifndef __NMCS_PROVIDER_ALIYUN_H__ +#define __NMCS_PROVIDER_ALIYUN_H__ + +#include "nmcs-provider.h" + +/*****************************************************************************/ + +typedef struct _NMCSProviderAliyun NMCSProviderAliyun; +typedef struct _NMCSProviderAliyunClass NMCSProviderAliyunClass; + +#define NMCS_TYPE_PROVIDER_ALIYUN (nmcs_provider_aliyun_get_type()) +#define NMCS_PROVIDER_ALIYUN(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER_ALIYUN, NMCSProviderAliyun)) +#define NMCS_PROVIDER_ALIYUN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER_ALIYUN, NMCSProviderAliyunClass)) +#define NMCS_IS_PROVIDER_ALIYUN(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER_ALIYUN)) +#define NMCS_IS_PROVIDER_ALIYUN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER_ALIYUN)) +#define NMCS_PROVIDER_ALIYUN_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER_ALIYUN, NMCSProviderAliyunClass)) + +GType nmcs_provider_aliyun_get_type(void); + +/*****************************************************************************/ + +#endif /* __NMCS_PROVIDER_ALIYUN_H__ */ diff --git a/src/nm-cloud-setup/nmcs-provider-azure.c b/src/nm-cloud-setup/nmcs-provider-azure.c new file mode 100644 index 00000000..b3f0c68b --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-azure.c @@ -0,0 +1,565 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nmcs-provider-azure.h" + +#include "nm-cloud-setup-utils.h" + +/*****************************************************************************/ + +#define HTTP_TIMEOUT_MS 3000 + +#define NM_AZURE_METADATA_HEADER "Metadata:true" +#define NM_AZURE_HOST "169.254.169.254" +#define NM_AZURE_BASE "http://" NM_AZURE_HOST +#define NM_AZURE_API_VERSION "?format=text&api-version=2017-04-02" +#define NM_AZURE_METADATA_URL_BASE /* $NM_AZURE_BASE/$NM_AZURE_API_VERSION */ \ + "/metadata/instance/network/interface/" + +#define _azure_uri_concat(...) \ + nmcs_utils_uri_build_concat(NM_AZURE_BASE, __VA_ARGS__, NM_AZURE_API_VERSION) +#define _azure_uri_interfaces(...) _azure_uri_concat(NM_AZURE_METADATA_URL_BASE, ##__VA_ARGS__) + +/*****************************************************************************/ + +struct _NMCSProviderAzure { + NMCSProvider parent; +}; + +struct _NMCSProviderAzureClass { + NMCSProviderClass parent; +}; + +G_DEFINE_TYPE(NMCSProviderAzure, nmcs_provider_azure, NMCS_TYPE_PROVIDER); + +/*****************************************************************************/ + +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; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error); + + if (nm_utils_error_is_cancelled(get_error)) { + 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 Azure metadata: %s", + get_error->message); + 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 = _azure_uri_concat("/metadata/instance")), + HTTP_TIMEOUT_MS, + 256 * 1024, + 7000, + 1000, + NM_MAKE_STRV(NM_AZURE_METADATA_HEADER), + g_task_get_cancellable(task), + NULL, + NULL, + _detect_get_meta_data_done_cb, + task); +} + +/*****************************************************************************/ + +typedef enum { + GET_CONFIG_FETCH_TYPE_IPV4_IPADDRESS_X_PRIVATEIPADDRESS, + GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_ADDRESS, + GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_PREFIX, +} GetConfigFetchType; + +typedef struct { + NMCSProviderGetConfigTaskData * get_config_data; + NMCSProviderGetConfigIfaceData *iface_get_config; + gssize intern_iface_idx; + gssize extern_iface_idx; + guint n_iface_data_pending; +} AzureIfaceData; + +static void +_azure_iface_data_destroy(AzureIfaceData *iface_data) +{ + nm_g_slice_free(iface_data); +} + +static void +_get_config_fetch_done_cb(NMHttpClient * http_client, + GAsyncResult * result, + AzureIfaceData * iface_data, + GetConfigFetchType fetch_type) +{ + NMCSProviderGetConfigTaskData * get_config_data; + NMCSProviderGetConfigIfaceData *iface_get_config; + gs_unref_bytes GBytes *response = NULL; + gs_free_error GError *error = NULL; + const char * resp_str = NULL; + gsize resp_len; + char tmp_addr_str[NM_UTILS_INET_ADDRSTRLEN]; + in_addr_t tmp_addr; + int tmp_prefix = -1; + + nm_http_client_poll_get_finish(http_client, result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = iface_data->get_config_data; + iface_get_config = iface_data->iface_get_config; + + if (error) + goto out_done; + + resp_str = g_bytes_get_data(response, &resp_len); + nm_assert(resp_str[resp_len] == '\0'); + + switch (fetch_type) { + case GET_CONFIG_FETCH_TYPE_IPV4_IPADDRESS_X_PRIVATEIPADDRESS: + + if (!nmcs_utils_ipaddr_normalize_bin(AF_INET, resp_str, resp_len, NULL, &tmp_addr)) { + error = + nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "ip is not a valid private ip address"); + goto out_done; + } + _LOGD("interface[%" G_GSSIZE_FORMAT "]: received address %s", + iface_data->intern_iface_idx, + _nm_utils_inet4_ntop(tmp_addr, tmp_addr_str)); + iface_get_config->ipv4s_arr[iface_get_config->ipv4s_len] = tmp_addr; + iface_get_config->has_ipv4s = TRUE; + iface_get_config->ipv4s_len++; + break; + + case GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_ADDRESS: + + if (!nmcs_utils_ipaddr_normalize_bin(AF_INET, resp_str, resp_len, NULL, &tmp_addr)) { + error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "ip is not a subnet address"); + goto out_done; + } + _LOGD("interface[%" G_GSSIZE_FORMAT "]: received subnet address %s", + iface_data->intern_iface_idx, + _nm_utils_inet4_ntop(tmp_addr, tmp_addr_str)); + iface_get_config->cidr_addr = tmp_addr; + break; + + case GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_PREFIX: + + tmp_prefix = _nm_utils_ascii_str_to_int64_bin(resp_str, resp_len, 10, 0, 32, -1); + if (tmp_prefix == -1) { + _LOGD("interface[%" G_GSSIZE_FORMAT "]: invalid prefix", iface_data->intern_iface_idx); + error = + nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "subnet does not give a valid prefix"); + goto out_done; + } + + _LOGD("interface[%" G_GSSIZE_FORMAT "]: received subnet prefix %d", + iface_data->intern_iface_idx, + tmp_prefix); + iface_get_config->cidr_prefix = tmp_prefix; + break; + } + +out_done: + if (!error) { + --iface_data->n_iface_data_pending; + if (iface_data->n_iface_data_pending > 0) + return; + + /* we surely have cidr_addr and cidr_prefix, otherwise + * we would have errored out above. */ + iface_get_config->has_cidr = TRUE; + } + + --get_config_data->n_pending; + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); +} + +static void +_get_config_fetch_done_cb_ipv4_ipaddress_x_privateipaddress(GObject * source, + GAsyncResult *result, + gpointer user_data) +{ + _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), + result, + user_data, + GET_CONFIG_FETCH_TYPE_IPV4_IPADDRESS_X_PRIVATEIPADDRESS); +} + +static void +_get_config_fetch_done_cb_ipv4_subnet_0_address(GObject * source, + GAsyncResult *result, + gpointer user_data) +{ + _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), + result, + user_data, + GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_ADDRESS); +} + +static void +_get_config_fetch_done_cb_ipv4_subnet_0_prefix(GObject * source, + GAsyncResult *result, + gpointer user_data) +{ + _get_config_fetch_done_cb(NM_HTTP_CLIENT(source), + result, + user_data, + GET_CONFIG_FETCH_TYPE_IPV4_SUBNET_0_PREFIX); +} + +static void +_get_config_ips_prefix_list_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + gs_unref_bytes GBytes *response = NULL; + AzureIfaceData * iface_data = user_data; + gs_free_error GError * error = NULL; + const char * response_str = NULL; + gsize response_len; + NMCSProviderGetConfigTaskData *get_config_data; + const char * line; + gsize line_len; + char iface_idx_str[30]; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = iface_data->get_config_data; + + if (error) + goto out_error; + + response_str = g_bytes_get_data(response, &response_len); + /* NMHttpClient guarantees that there is a trailing NUL after the data. */ + nm_assert(response_str[response_len] == 0); + + nm_assert(!iface_data->iface_get_config->ipv4s_arr); + nm_assert(!iface_data->iface_get_config->has_ipv4s); + nm_assert(!iface_data->iface_get_config->has_cidr); + + nm_sprintf_buf(iface_idx_str, "%" G_GSSIZE_FORMAT, iface_data->intern_iface_idx); + + while (nm_utils_parse_next_line(&response_str, &response_len, &line, &line_len)) { + gint64 ips_prefix_idx; + gs_free char *uri = NULL; + char buf[100]; + + if (line_len == 0) + continue; + + /* Truncate the string. It's safe to do, because we own @response an it has an + * extra NULL character after the buffer. */ + ((char *) line)[line_len] = '\0'; + + if (line[line_len - 1] == '/') + ((char *) line)[--line_len] = '\0'; + + ips_prefix_idx = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXINT64, -1); + + if (ips_prefix_idx < 0) + continue; + + iface_data->n_iface_data_pending++; + + nm_http_client_poll_get( + NM_HTTP_CLIENT(source), + (uri = _azure_uri_interfaces(iface_idx_str, + "/ipv4/ipAddress/", + nm_sprintf_buf(buf, "%" G_GINT64_FORMAT, ips_prefix_idx), + "/privateIpAddress")), + HTTP_TIMEOUT_MS, + 512 * 1024, + 10000, + 1000, + NM_MAKE_STRV(NM_AZURE_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fetch_done_cb_ipv4_ipaddress_x_privateipaddress, + iface_data); + } + + iface_data->iface_get_config->ipv4s_len = 0; + iface_data->iface_get_config->ipv4s_arr = g_new(in_addr_t, iface_data->n_iface_data_pending); + + { + gs_free char *uri = NULL; + + iface_data->n_iface_data_pending++; + nm_http_client_poll_get( + NM_HTTP_CLIENT(source), + (uri = _azure_uri_interfaces(iface_idx_str, "/ipv4/subnet/0/address/")), + HTTP_TIMEOUT_MS, + 512 * 1024, + 10000, + 1000, + NM_MAKE_STRV(NM_AZURE_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fetch_done_cb_ipv4_subnet_0_address, + iface_data); + + nm_clear_g_free(&uri); + + iface_data->n_iface_data_pending++; + nm_http_client_poll_get( + NM_HTTP_CLIENT(source), + (uri = _azure_uri_interfaces(iface_idx_str, "/ipv4/subnet/0/prefix/")), + HTTP_TIMEOUT_MS, + 512 * 1024, + 10000, + 1000, + NM_MAKE_STRV(NM_AZURE_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fetch_done_cb_ipv4_subnet_0_prefix, + iface_data); + } + return; + +out_error: + --get_config_data->n_pending; + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); +} + +static void +_get_config_iface_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMCSProviderGetConfigTaskData *get_config_data; + gs_unref_bytes GBytes *response = NULL; + AzureIfaceData * iface_data = user_data; + gs_free char * v_hwaddr = NULL; + gs_free_error GError *error = NULL; + gs_free const char * uri = NULL; + char buf[100]; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = iface_data->get_config_data; + + if (error) + goto out_done; + + v_hwaddr = nmcs_utils_hwaddr_normalize_gbytes(response); + if (!v_hwaddr) { + _LOGI("interface[%" G_GSSIZE_FORMAT "]: invalid MAC address returned", + iface_data->intern_iface_idx); + error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, + "invalid MAC address for index %" G_GSSIZE_FORMAT, + iface_data->intern_iface_idx); + goto out_done; + } + + iface_data->iface_get_config = g_hash_table_lookup(get_config_data->result_dict, v_hwaddr); + + if (!iface_data->iface_get_config) { + if (!get_config_data->any) { + _LOGD("get-config: skip fetching meta data for %s (%" G_GSSIZE_FORMAT ")", + v_hwaddr, + iface_data->intern_iface_idx); + goto out_done; + } + iface_data->iface_get_config = + nmcs_provider_get_config_iface_data_create(get_config_data->result_dict, + FALSE, + v_hwaddr); + } else { + if (iface_data->iface_get_config->iface_idx >= 0) { + _LOGI("interface[%" G_GSSIZE_FORMAT "]: duplicate MAC address %s returned", + iface_data->intern_iface_idx, + iface_data->iface_get_config->hwaddr); + error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, + "duplicate MAC address for index %" G_GSSIZE_FORMAT, + iface_data->intern_iface_idx); + goto out_done; + } + } + + iface_data->iface_get_config->iface_idx = iface_data->extern_iface_idx; + + _LOGD("interface[%" G_GSSIZE_FORMAT "]: found a matching device with hwaddr %s", + iface_data->intern_iface_idx, + iface_data->iface_get_config->hwaddr); + + nm_sprintf_buf(buf, "%" G_GSSIZE_FORMAT "/ipv4/ipAddress/", iface_data->intern_iface_idx); + + nm_http_client_poll_get(NM_HTTP_CLIENT(source), + (uri = _azure_uri_interfaces(buf)), + HTTP_TIMEOUT_MS, + 512 * 1024, + 10000, + 1000, + NM_MAKE_STRV(NM_AZURE_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_ips_prefix_list_cb, + iface_data); + return; + +out_done: + --get_config_data->n_pending; + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); +} + +static void +_get_net_ifaces_list_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMCSProviderGetConfigTaskData *get_config_data; + gs_unref_ptrarray GPtrArray *ifaces_arr = NULL; + gs_unref_bytes GBytes *response = NULL; + gs_free_error GError *error = NULL; + const char * response_str; + gsize response_len; + const char * line; + gsize line_len; + guint i; + gssize extern_iface_idx_cnt = 0; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = user_data; + + if (error) { + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); + return; + } + + response_str = g_bytes_get_data(response, &response_len); + /* NMHttpClient guarantees that there is a trailing NUL after the data. */ + nm_assert(response_str[response_len] == 0); + + ifaces_arr = g_ptr_array_new_with_free_func((GDestroyNotify) _azure_iface_data_destroy); + + while (nm_utils_parse_next_line(&response_str, &response_len, &line, &line_len)) { + AzureIfaceData *iface_data; + gssize intern_iface_idx; + + if (line_len == 0) + continue; + + /* Truncate the string. It's safe to do, because we own @response an it has an + * extra NULL character after the buffer. */ + ((char *) line)[line_len] = '\0'; + + if (line[line_len - 1] == '/') + ((char *) line)[--line_len] = '\0'; + + intern_iface_idx = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXSSIZE, -1); + if (intern_iface_idx < 0) + continue; + + iface_data = g_slice_new(AzureIfaceData); + *iface_data = (AzureIfaceData){ + .get_config_data = get_config_data, + .iface_get_config = NULL, + .intern_iface_idx = intern_iface_idx, + .extern_iface_idx = extern_iface_idx_cnt++, + .n_iface_data_pending = 0, + }; + g_ptr_array_add(ifaces_arr, iface_data); + } + + _LOGD("found azure interfaces: %u", ifaces_arr->len); + + if (ifaces_arr->len == 0) { + _nmcs_provider_get_config_task_maybe_return( + get_config_data, + nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "no Azure interfaces found")); + return; + } + + for (i = 0; i < ifaces_arr->len; ++i) { + AzureIfaceData * iface_data = ifaces_arr->pdata[i]; + gs_free const char *uri = NULL; + char buf[100]; + + _LOGD("azure interface[%" G_GSSIZE_FORMAT "]: retrieving configuration", + iface_data->intern_iface_idx); + + nm_sprintf_buf(buf, "%" G_GSSIZE_FORMAT "/macAddress", iface_data->intern_iface_idx); + + get_config_data->n_pending++; + nm_http_client_poll_get(NM_HTTP_CLIENT(source), + (uri = _azure_uri_interfaces(buf)), + HTTP_TIMEOUT_MS, + 512 * 1024, + 10000, + 1000, + NM_MAKE_STRV(NM_AZURE_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_iface_cb, + iface_data); + } + + get_config_data->extra_data_destroy = (GDestroyNotify) g_ptr_array_unref; + get_config_data->extra_data = g_steal_pointer(&ifaces_arr); +} + +static void +get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data) +{ + gs_free const char *uri = NULL; + + nm_http_client_poll_get(nmcs_provider_get_http_client(provider), + (uri = _azure_uri_interfaces()), + HTTP_TIMEOUT_MS, + 256 * 1024, + 15000, + 1000, + NM_MAKE_STRV(NM_AZURE_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_net_ifaces_list_cb, + get_config_data); +} + +/*****************************************************************************/ + +static void +nmcs_provider_azure_init(NMCSProviderAzure *self) +{} + +static void +nmcs_provider_azure_class_init(NMCSProviderAzureClass *klass) +{ + NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass); + + provider_class->_name = "azure"; + provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_AZURE"); + provider_class->detect = detect; + provider_class->get_config = get_config; +} diff --git a/src/nm-cloud-setup/nmcs-provider-azure.h b/src/nm-cloud-setup/nmcs-provider-azure.h new file mode 100644 index 00000000..be78768f --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-azure.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#ifndef __NMCS_PROVIDER_AZURE_H__ +#define __NMCS_PROVIDER_AZURE_H__ + +#include "nmcs-provider.h" + +/*****************************************************************************/ + +typedef struct _NMCSProviderAzure NMCSProviderAzure; +typedef struct _NMCSProviderAzureClass NMCSProviderAzureClass; + +#define NMCS_TYPE_PROVIDER_AZURE (nmcs_provider_azure_get_type()) +#define NMCS_PROVIDER_AZURE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER_AZURE, NMCSProviderAzure)) +#define NMCS_PROVIDER_AZURE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER_AZURE, NMCSProviderAzureClass)) +#define NMCS_IS_PROVIDER_AZURE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER_AZURE)) +#define NMCS_IS_PROVIDER_AZURE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER_AZURE)) +#define NMCS_PROVIDER_AZURE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER_AZURE, NMCSProviderAzureClass)) + +GType nmcs_provider_azure_get_type(void); + +/*****************************************************************************/ + +#endif /* __NMCS_PROVIDER_AZURE_H__ */ diff --git a/src/nm-cloud-setup/nmcs-provider-ec2.c b/src/nm-cloud-setup/nmcs-provider-ec2.c new file mode 100644 index 00000000..9fe62518 --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-ec2.c @@ -0,0 +1,408 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nmcs-provider-ec2.h" + +#include "nm-cloud-setup-utils.h" + +/*****************************************************************************/ + +#define HTTP_TIMEOUT_MS 3000 + +#define NM_EC2_HOST "169.254.169.254" +#define NM_EC2_BASE "http://" NM_EC2_HOST +#define NM_EC2_API_VERSION "2018-09-24" +#define NM_EC2_METADATA_URL_BASE /* $NM_EC2_BASE/$NM_EC2_API_VERSION */ \ + "/meta-data/network/interfaces/macs/" + +static const char * +_ec2_base(void) +{ + static const char *base_cached = NULL; + const char * base; + +again: + base = g_atomic_pointer_get(&base_cached); + if (G_UNLIKELY(!base)) { + /* The base URI can be set via environment variable. + * This is mainly for testing, it's not usually supposed to be configured. + * Consider this private API! */ + base = g_getenv(NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_EC2_HOST")); + base = nmcs_utils_uri_complete_interned(base) ?: ("" NM_EC2_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, + gpointer check_user_data, + GError **error) +{ + return response_code == 200 && nmcs_utils_parse_get_full_line(response, "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; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error); + + if (nm_utils_error_is_cancelled(get_error)) { + 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; + } + + 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, + NULL, + g_task_get_cancellable(task), + _detect_get_meta_data_check_cb, + NULL, + _detect_get_meta_data_done_cb, + task); +} + +/*****************************************************************************/ + +static void +_get_config_fetch_done_cb(NMHttpClient *http_client, + GAsyncResult *result, + gpointer user_data, + gboolean is_local_ipv4) +{ + NMCSProviderGetConfigTaskData *get_config_data; + gs_unref_bytes GBytes *response = NULL; + gs_free_error GError * error = NULL; + NMCSProviderGetConfigIfaceData *config_iface_data; + in_addr_t tmp_addr; + int tmp_prefix; + + nm_utils_user_data_unpack(user_data, &get_config_data, &config_iface_data); + + nm_http_client_poll_get_finish(http_client, result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + if (error) + goto out; + + 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, 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, 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; + } + } + +out: + get_config_data->n_pending--; + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); +} + +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); +} + +typedef struct { + gssize iface_idx; + char path[0]; +} GetConfigMetadataMac; + +static void +_get_config_metadata_ready_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMCSProviderGetConfigTaskData *get_config_data; + gs_unref_hashtable GHashTable *response_parsed = NULL; + gs_free_error GError *error = NULL; + GetConfigMetadataMac *v_mac_data; + const char * v_hwaddr; + GHashTableIter h_iter; + NMHttpClient * http_client; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = user_data; + + response_parsed = g_steal_pointer(&get_config_data->extra_data); + get_config_data->extra_data_destroy = NULL; + + /* 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) { + _nmcs_provider_get_config_task_maybe_return( + get_config_data, + nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "meta data for interfaces not found")); + return; + } + + 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; + + config_iface_data = g_hash_table_lookup(get_config_data->result_dict, v_hwaddr); + + if (!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_create(get_config_data->result_dict, + FALSE, + v_hwaddr); + } + + 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, + config_iface_data->hwaddr, + v_mac_data->path); + + get_config_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, + NULL, + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fetch_done_cb_subnet_ipv4_cidr_block, + nm_utils_user_data_pack(get_config_data, config_iface_data)); + + get_config_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, + NULL, + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fetch_done_cb_local_ipv4s, + nm_utils_user_data_pack(get_config_data, config_iface_data)); + } + + _nmcs_provider_get_config_task_maybe_return(get_config_data, NULL); +} + +static gboolean +_get_config_metadata_ready_check(long response_code, + GBytes * response, + gpointer check_user_data, + GError **error) +{ + NMCSProviderGetConfigTaskData *get_config_data = check_user_data; + gs_unref_hashtable GHashTable *response_parsed = NULL; + const guint8 * r_data; + const char * cur_line; + gsize r_len; + gsize cur_line_len; + GHashTableIter h_iter; + gboolean has_all; + const char * c_hwaddr; + gssize iface_idx_counter = 0; + + if (response_code != 200 || !response) { + /* we wait longer. */ + return FALSE; + } + + r_data = g_bytes_get_data(response, &r_len); + /* NMHttpClient guarantees that there is a trailing NUL after the data. */ + nm_assert(r_data[r_len] == 0); + + while (nm_utils_parse_next_line((const char **) &r_data, &r_len, &cur_line, &cur_line_len)) { + GetConfigMetadataMac *mac_data; + char * hwaddr; + + if (cur_line_len == 0) + continue; + + /* Truncate the string. It's safe to do, because we own @response an it has an + * extra NUL character after the buffer. */ + ((char *) cur_line)[cur_line_len] = '\0'; + + hwaddr = nmcs_utils_hwaddr_normalize( + cur_line, + cur_line[cur_line_len - 1u] == '/' ? (gssize) (cur_line_len - 1u) : -1); + 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(GetConfigMetadataMac) + 1u + cur_line_len); + mac_data->iface_idx = iface_idx_counter++; + memcpy(mac_data->path, cur_line, cur_line_len + 1u); + + /* here we will ignore duplicate responses. */ + g_hash_table_insert(response_parsed, hwaddr, mac_data); + } + + has_all = TRUE; + g_hash_table_iter_init(&h_iter, 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(&get_config_data->extra_data, g_hash_table_unref); + if (response_parsed) { + get_config_data->extra_data = g_steal_pointer(&response_parsed); + get_config_data->extra_data_destroy = (GDestroyNotify) g_hash_table_unref; + } + return has_all; +} + +static void +get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data) +{ + gs_free char *uri = NULL; + + /* 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, + NULL, + get_config_data->intern_cancellable, + _get_config_metadata_ready_check, + get_config_data, + _get_config_metadata_ready_cb, + get_config_data); +} + +/*****************************************************************************/ + +static void +nmcs_provider_ec2_init(NMCSProviderEC2 *self) +{} + +static void +nmcs_provider_ec2_class_init(NMCSProviderEC2Class *klass) +{ + NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass); + + provider_class->_name = "ec2"; + provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_EC2"); + provider_class->detect = detect; + provider_class->get_config = get_config; +} diff --git a/src/nm-cloud-setup/nmcs-provider-ec2.h b/src/nm-cloud-setup/nmcs-provider-ec2.h new file mode 100644 index 00000000..8f8b6400 --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-ec2.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#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/src/nm-cloud-setup/nmcs-provider-gcp.c b/src/nm-cloud-setup/nmcs-provider-gcp.c new file mode 100644 index 00000000..60425ad9 --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-gcp.c @@ -0,0 +1,462 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nmcs-provider-gcp.h" + +#include "nm-cloud-setup-utils.h" + +/*****************************************************************************/ + +#define HTTP_TIMEOUT_MS 3000 +#define HTTP_REQ_MAX_DATA 512 * 1024 +#define HTTP_POLL_TIMEOUT_MS 10000 +#define HTTP_RATE_LIMIT_MS 1000 + +#define NM_GCP_HOST "metadata.google.internal" +#define NM_GCP_BASE "http://" NM_GCP_HOST +#define NM_GCP_API_VERSION "/v1" +#define NM_GCP_METADATA_URL_BASE NM_GCP_BASE "/computeMetadata" NM_GCP_API_VERSION "/instance" +#define NM_GCP_METADATA_URL_NET "/network-interfaces/" + +#define NM_GCP_METADATA_HEADER "Metadata-Flavor: Google" + +#define _gcp_uri_concat(...) nmcs_utils_uri_build_concat(NM_GCP_METADATA_URL_BASE, __VA_ARGS__) +#define _gcp_uri_interfaces(...) _gcp_uri_concat(NM_GCP_METADATA_URL_NET, ##__VA_ARGS__) + +/*****************************************************************************/ + +struct _NMCSProviderGCP { + NMCSProvider parent; +}; + +struct _NMCSProviderGCPClass { + NMCSProviderClass parent; +}; + +G_DEFINE_TYPE(NMCSProviderGCP, nmcs_provider_gcp, NMCS_TYPE_PROVIDER); + +/*****************************************************************************/ + +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; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error); + + if (nm_utils_error_is_cancelled(get_error)) { + 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 GCP metadata: %s", + get_error->message); + 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 = _gcp_uri_concat("id")), + HTTP_TIMEOUT_MS, + 256 * 1024, + 7000, + 1000, + NM_MAKE_STRV(NM_GCP_METADATA_HEADER), + g_task_get_cancellable(task), + NULL, + NULL, + _detect_get_meta_data_done_cb, + task); +} + +/*****************************************************************************/ + +typedef struct { + NMCSProviderGetConfigTaskData * get_config_data; + NMCSProviderGetConfigIfaceData *iface_get_config; + gssize intern_iface_idx; + gssize extern_iface_idx; + guint n_fips_pending; +} GCPIfaceData; + +static void +_gcp_iface_data_destroy(GCPIfaceData *iface_data) +{ + nm_g_slice_free(iface_data); +} + +static void +_get_config_fip_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMCSProviderGetConfigTaskData * get_config_data; + NMCSProviderGetConfigIfaceData *iface_get_config; + gs_unref_bytes GBytes *response = NULL; + GCPIfaceData * iface_data = user_data; + gs_free_error GError *error = NULL; + gs_free char * ipaddr = NULL; + NMIPRoute ** routes_arr; + NMIPRoute * route_new; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = iface_data->get_config_data; + + if (error) + goto out_done; + + ipaddr = nmcs_utils_ipaddr_normalize_gbytes(AF_INET, response); + if (!ipaddr) { + error = + nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "forwarded-ip is not a valid ip address"); + goto out_done; + } + + _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: adding forwarded-ip %s", + iface_data->intern_iface_idx, + ipaddr); + + iface_get_config = iface_data->iface_get_config; + routes_arr = iface_get_config->iproutes_arr; + + route_new = nm_ip_route_new(AF_INET, ipaddr, 32, NULL, 100, &error); + if (error) + goto out_done; + + nm_ip_route_set_attribute(route_new, NM_IP_ROUTE_ATTRIBUTE_TYPE, g_variant_new_string("local")); + routes_arr[iface_get_config->iproutes_len] = route_new; + ++iface_get_config->iproutes_len; + +out_done: + if (!error) { + --iface_data->n_fips_pending; + if (iface_data->n_fips_pending > 0) + return; + } + + --get_config_data->n_pending; + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); +} + +static void +_get_config_ips_list_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMCSProviderGetConfigTaskData *get_config_data; + gs_unref_ptrarray GPtrArray *uri_arr = NULL; + gs_unref_bytes GBytes *response = NULL; + GCPIfaceData * iface_data = user_data; + gs_free_error GError *error = NULL; + const char * response_str = NULL; + gsize response_len; + const char * line; + gsize line_len; + guint i; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = iface_data->get_config_data; + + if (error) + goto out_error; + + response_str = g_bytes_get_data(response, &response_len); + /* NMHttpClient guarantees that there is a trailing NUL after the data. */ + nm_assert(response_str[response_len] == 0); + + uri_arr = g_ptr_array_new_with_free_func(g_free); + while (nm_utils_parse_next_line(&response_str, &response_len, &line, &line_len)) { + gint64 fip_index; + + /* Truncate the string. It's safe to do, because we own @response an it has an + * extra NUL character after the buffer. */ + ((char *) line)[line_len] = '\0'; + + fip_index = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXINT64, -1); + if (fip_index < 0) + continue; + + g_ptr_array_add(uri_arr, + g_strdup_printf("%" G_GSSIZE_FORMAT "/forwarded-ips/%" G_GINT64_FORMAT, + iface_data->intern_iface_idx, + fip_index)); + } + + iface_data->n_fips_pending = uri_arr->len; + + _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: found %u forwarded ips", + iface_data->intern_iface_idx, + iface_data->n_fips_pending); + + if (iface_data->n_fips_pending == 0) { + error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "found no forwarded ip"); + goto out_error; + } + + iface_data->iface_get_config->iproutes_arr = g_new(NMIPRoute *, iface_data->n_fips_pending); + + for (i = 0; i < uri_arr->len; ++i) { + const char * str = uri_arr->pdata[i]; + gs_free const char *uri = NULL; + + nm_http_client_poll_get(NM_HTTP_CLIENT(source), + (uri = _gcp_uri_interfaces(str)), + HTTP_TIMEOUT_MS, + HTTP_REQ_MAX_DATA, + HTTP_POLL_TIMEOUT_MS, + HTTP_RATE_LIMIT_MS, + NM_MAKE_STRV(NM_GCP_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_fip_cb, + iface_data); + } + return; + +out_error: + --get_config_data->n_pending; + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); +} + +static void +_get_config_iface_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + gs_unref_bytes GBytes *response = NULL; + GCPIfaceData * iface_data = user_data; + gs_free_error GError * error = NULL; + gs_free char * v_hwaddr = NULL; + gs_free const char * uri = NULL; + char sbuf[100]; + NMCSProviderGetConfigTaskData *get_config_data; + gboolean is_requested; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = iface_data->get_config_data; + + if (error) + goto out_done; + + v_hwaddr = nmcs_utils_hwaddr_normalize_gbytes(response); + if (!v_hwaddr) { + _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: invalid MAC address returned", + iface_data->intern_iface_idx); + error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, + "invalid MAC address for index %" G_GSSIZE_FORMAT, + iface_data->intern_iface_idx); + goto out_done; + } + + iface_data->iface_get_config = g_hash_table_lookup(get_config_data->result_dict, v_hwaddr); + + if (!iface_data->iface_get_config) { + if (!get_config_data->any) { + _LOGD("get-config: skip fetching meta data for %s (%" G_GSSIZE_FORMAT ")", + v_hwaddr, + iface_data->intern_iface_idx); + goto out_done; + } + iface_data->iface_get_config = + nmcs_provider_get_config_iface_data_create(get_config_data->result_dict, + FALSE, + v_hwaddr); + is_requested = FALSE; + } else { + if (iface_data->iface_get_config->iface_idx >= 0) { + _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: duplicate MAC address %s returned", + iface_data->intern_iface_idx, + iface_data->iface_get_config->hwaddr); + error = nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, + "duplicate MAC address for index %" G_GSSIZE_FORMAT, + iface_data->intern_iface_idx); + goto out_done; + } + is_requested = TRUE; + } + + iface_data->iface_get_config->iface_idx = iface_data->extern_iface_idx; + + _LOGI("GCP interface[%" G_GSSIZE_FORMAT "]: found a %sdevice with hwaddr %s", + iface_data->intern_iface_idx, + is_requested ? "requested " : "", + iface_data->iface_get_config->hwaddr); + + nm_sprintf_buf(sbuf, "%" G_GSSIZE_FORMAT "/forwarded-ips/", iface_data->intern_iface_idx); + + nm_http_client_poll_get(NM_HTTP_CLIENT(source), + (uri = _gcp_uri_interfaces(sbuf)), + HTTP_TIMEOUT_MS, + HTTP_REQ_MAX_DATA, + HTTP_POLL_TIMEOUT_MS, + HTTP_RATE_LIMIT_MS, + NM_MAKE_STRV(NM_GCP_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_ips_list_cb, + iface_data); + return; + +out_done: + --get_config_data->n_pending; + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); +} + +static void +_get_net_ifaces_list_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + gs_unref_ptrarray GPtrArray *ifaces_arr = NULL; + gs_unref_bytes GBytes *response = NULL; + gs_free_error GError * error = NULL; + NMCSProviderGetConfigTaskData *get_config_data; + const char * response_str; + gsize response_len; + const char * line; + gsize line_len; + guint i; + gssize extern_iface_idx_cnt = 0; + + nm_http_client_poll_get_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = user_data; + + if (error) { + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); + return; + } + + response_str = g_bytes_get_data(response, &response_len); + /* NMHttpClient guarantees that there is a trailing NUL after the data. */ + nm_assert(response_str[response_len] == 0); + + ifaces_arr = g_ptr_array_new_with_free_func((GDestroyNotify) _gcp_iface_data_destroy); + + while (nm_utils_parse_next_line(&response_str, &response_len, &line, &line_len)) { + GCPIfaceData *iface_data; + gssize intern_iface_idx; + + if (line_len == 0) + continue; + + /* Truncate the string. It's safe to do, because we own @response an it has an + * extra NUL character after the buffer. */ + ((char *) line)[line_len] = '\0'; + if (line[line_len - 1] == '/') + ((char *) line)[--line_len] = '\0'; + + intern_iface_idx = _nm_utils_ascii_str_to_int64(line, 10, 0, G_MAXSSIZE, -1); + if (intern_iface_idx < 0) + continue; + + iface_data = g_slice_new(GCPIfaceData); + *iface_data = (GCPIfaceData){ + .get_config_data = get_config_data, + .iface_get_config = NULL, + .intern_iface_idx = intern_iface_idx, + .extern_iface_idx = extern_iface_idx_cnt++, + .n_fips_pending = 0, + }; + g_ptr_array_add(ifaces_arr, iface_data); + } + + _LOGI("found GCP interfaces: %u", ifaces_arr->len); + + if (ifaces_arr->len == 0) { + _nmcs_provider_get_config_task_maybe_return( + get_config_data, + nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "no GCP interfaces found")); + return; + } + + for (i = 0; i < ifaces_arr->len; ++i) { + GCPIfaceData * data = ifaces_arr->pdata[i]; + gs_free const char *uri = NULL; + char sbuf[100]; + + _LOGD("GCP interface[%" G_GSSIZE_FORMAT "]: retrieving configuration", + data->intern_iface_idx); + + nm_sprintf_buf(sbuf, "%" G_GSSIZE_FORMAT "/mac", data->intern_iface_idx); + + get_config_data->n_pending++; + nm_http_client_poll_get(NM_HTTP_CLIENT(source), + (uri = _gcp_uri_interfaces(sbuf)), + HTTP_TIMEOUT_MS, + HTTP_REQ_MAX_DATA, + HTTP_POLL_TIMEOUT_MS, + HTTP_RATE_LIMIT_MS, + NM_MAKE_STRV(NM_GCP_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_iface_cb, + data); + } + + get_config_data->extra_data = g_steal_pointer(&ifaces_arr); + get_config_data->extra_data_destroy = (GDestroyNotify) g_ptr_array_unref; +} + +static void +get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data) +{ + gs_free const char *uri = NULL; + + nm_http_client_poll_get(nmcs_provider_get_http_client(provider), + (uri = _gcp_uri_interfaces()), + HTTP_TIMEOUT_MS, + HTTP_REQ_MAX_DATA, + HTTP_POLL_TIMEOUT_MS, + HTTP_RATE_LIMIT_MS, + NM_MAKE_STRV(NM_GCP_METADATA_HEADER), + get_config_data->intern_cancellable, + NULL, + NULL, + _get_net_ifaces_list_cb, + get_config_data); +} + +/*****************************************************************************/ + +static void +nmcs_provider_gcp_init(NMCSProviderGCP *self) +{} + +static void +nmcs_provider_gcp_class_init(NMCSProviderGCPClass *klass) +{ + NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass); + + provider_class->_name = "GCP"; + provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE("NM_CLOUD_SETUP_GCP"); + provider_class->detect = detect; + provider_class->get_config = get_config; +} diff --git a/src/nm-cloud-setup/nmcs-provider-gcp.h b/src/nm-cloud-setup/nmcs-provider-gcp.h new file mode 100644 index 00000000..b0329935 --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-gcp.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#ifndef __NMCS_PROVIDER_GCP_H__ +#define __NMCS_PROVIDER_GCP_H__ + +#include "nmcs-provider.h" + +/*****************************************************************************/ + +typedef struct _NMCSProviderGCP NMCSProviderGCP; +typedef struct _NMCSProviderGCPClass NMCSProviderGCPClass; + +#define NMCS_TYPE_PROVIDER_GCP (nmcs_provider_gcp_get_type()) +#define NMCS_PROVIDER_GCP(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER_GCP, NMCSProviderGCP)) +#define NMCS_PROVIDER_GCP_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER_GCP, NMCSProviderGCPClass)) +#define NMCS_IS_PROVIDER_GCP(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER_GCP)) +#define NMCS_IS_PROVIDER_GCP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER_GCP)) +#define NMCS_PROVIDER_GCP_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER_GCP, NMCSProviderGCPClass)) + +GType nmcs_provider_gcp_get_type(void); + +/*****************************************************************************/ + +#endif /* __NMCS_PROVIDER_GCP_H__ */ diff --git a/src/nm-cloud-setup/nmcs-provider.c b/src/nm-cloud-setup/nmcs-provider.c new file mode 100644 index 00000000..138e78d4 --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider.c @@ -0,0 +1,380 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.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); +} +/*****************************************************************************/ + +static int +_result_new_sort_iface_data(gconstpointer pa, gconstpointer pb) +{ + const NMCSProviderGetConfigIfaceData *a = *((const NMCSProviderGetConfigIfaceData *const *) pa); + const NMCSProviderGetConfigIfaceData *b = *((const NMCSProviderGetConfigIfaceData *const *) pb); + + /* negative iface_idx are sorted to the end. */ + NM_CMP_DIRECT((a->iface_idx < 0), (b->iface_idx < 0)); + + NM_CMP_FIELD(a, b, iface_idx); + return 0; +} + +static NMCSProviderGetConfigResult * +nmcs_provider_get_config_result_new(GHashTable *iface_datas) +{ + const NMCSProviderGetConfigIfaceData *iface_data; + NMCSProviderGetConfigResult * result; + GHashTableIter h_iter; + guint num_valid_ifaces = 0; + guint num_ipv4s = 0; + GPtrArray * ptrarr; + guint n_iface_datas; + + n_iface_datas = g_hash_table_size(iface_datas); + + ptrarr = g_ptr_array_sized_new(n_iface_datas + 1u); + + g_hash_table_iter_init(&h_iter, iface_datas); + while (g_hash_table_iter_next(&h_iter, NULL, (gpointer *) &iface_data)) { + if (nmcs_provider_get_config_iface_data_is_valid(iface_data)) { + num_valid_ifaces++; + num_ipv4s += iface_data->ipv4s_len; + } + g_ptr_array_add(ptrarr, (gpointer) iface_data); + } + + g_ptr_array_sort(ptrarr, _result_new_sort_iface_data); + + nm_assert(n_iface_datas == ptrarr->len); + + g_ptr_array_add(ptrarr, NULL); + + result = g_new(NMCSProviderGetConfigResult, 1); + *result = (NMCSProviderGetConfigResult){ + .iface_datas = g_hash_table_ref(iface_datas), + .n_iface_datas = n_iface_datas, + .iface_datas_arr = + (const NMCSProviderGetConfigIfaceData **) g_ptr_array_free(ptrarr, FALSE), + .num_valid_ifaces = num_valid_ifaces, + .num_ipv4s = num_ipv4s, + }; + +#if NM_MORE_ASSERTS > 5 + { + gsize iface_idx_expected = 0; + guint i; + + for (i = 0; i < result->n_iface_datas; i++) { + if (result->iface_datas_arr[i]->iface_idx < 0) { + nm_assert(result->iface_datas_arr[i]->iface_idx == -1); + iface_idx_expected = -1; + continue; + } + nm_assert(result->iface_datas_arr[i]->iface_idx == iface_idx_expected); + iface_idx_expected++; + } + } +#endif + + return result; +} + +void +nmcs_provider_get_config_result_free(NMCSProviderGetConfigResult *result) +{ + if (result) { + nm_g_hash_table_unref(result->iface_datas); + g_free((gpointer) result->iface_datas_arr); + g_free(result); + } +} + +/*****************************************************************************/ + +void +nmcs_provider_detect(NMCSProvider * self, + GCancellable * cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + gs_unref_object GTask *task = NULL; + const char * env; + + g_return_if_fail(NMCS_IS_PROVIDER(self)); + g_return_if_fail(!cancellable || G_IS_CANCELLABLE(cancellable)); + + task = nm_g_task_new(self, cancellable, nmcs_provider_detect, callback, user_data); + + nmcs_wait_for_objects_register(task); + + env = g_getenv(NMCS_PROVIDER_GET_CLASS(self)->_env_provider_enabled); + if (!_nm_utils_ascii_str_to_bool(env, FALSE)) { + g_task_return_error(task, + nm_utils_error_new(NM_UTILS_ERROR_UNKNOWN, "provider is disabled")); + return; + } + + NMCS_PROVIDER_GET_CLASS(self)->detect(self, g_steal_pointer(&task)); +} + +gboolean +nmcs_provider_detect_finish(NMCSProvider *self, GAsyncResult *result, GError **error) +{ + g_return_val_if_fail(NMCS_IS_PROVIDER(self), FALSE); + g_return_val_if_fail(nm_g_task_is_valid(result, self, nmcs_provider_detect), FALSE); + + return g_task_propagate_boolean(G_TASK(result), error); +} + +/*****************************************************************************/ + +NMCSProviderGetConfigIfaceData * +nmcs_provider_get_config_iface_data_create(GHashTable *iface_datas, + gboolean was_requested, + const char *hwaddr) +{ + NMCSProviderGetConfigIfaceData *iface_data; + + nm_assert(hwaddr); + + iface_data = g_slice_new(NMCSProviderGetConfigIfaceData); + *iface_data = (NMCSProviderGetConfigIfaceData){ + .hwaddr = g_strdup(hwaddr), + .iface_idx = -1, + .was_requested = was_requested, + }; + + /* the has does not own the key (iface_datta->hwaddr), the lifetime of the + * key is associated with the iface_data instance. */ + g_hash_table_replace(iface_datas, (char *) iface_data->hwaddr, iface_data); + + return iface_data; +} + +static void +_iface_data_free(gpointer data) +{ + NMCSProviderGetConfigIfaceData *iface_data = data; + + g_free(iface_data->ipv4s_arr); + g_free(iface_data->iproutes_arr); + g_free((char *) iface_data->hwaddr); + + nm_g_slice_free(iface_data); +} + +static void +_get_config_task_maybe_return(NMCSProviderGetConfigTaskData *get_config_data, GError *error_take) +{ + gs_free_error GError *error = error_take; + + nm_assert(get_config_data); + nm_assert(G_IS_TASK(get_config_data->task)); + + if (!error) { + if (get_config_data->n_pending > 0) + return; + } + + g_cancellable_cancel(get_config_data->intern_cancellable); + + if (error) { + if (nm_utils_error_is_cancelled(error)) + _LOGD("get-config: cancelled"); + else + _LOGD("get-config: failed: %s", error->message); + g_task_return_error(get_config_data->task, g_steal_pointer(&error)); + } else { + _LOGD("get-config: success"); + g_task_return_pointer(get_config_data->task, + nmcs_provider_get_config_result_new(get_config_data->result_dict), + (GDestroyNotify) nmcs_provider_get_config_result_free); + } + + nm_clear_g_signal_handler(g_task_get_cancellable(get_config_data->task), + &get_config_data->extern_cancelled_id); + + if (get_config_data->extra_data_destroy) + get_config_data->extra_data_destroy(get_config_data->extra_data); + + nm_clear_pointer(&get_config_data->result_dict, g_hash_table_unref); + + nm_g_object_unref(get_config_data->intern_cancellable); + g_object_unref(get_config_data->task); + nm_g_slice_free(get_config_data); +} + +void +_nmcs_provider_get_config_task_maybe_return(NMCSProviderGetConfigTaskData *get_config_data, + GError * error_take) +{ + nm_assert(!error_take || !nm_utils_error_is_cancelled(error_take)); + _get_config_task_maybe_return(get_config_data, error_take); +} + +static void +_get_config_cancelled_cb(GObject *object, gpointer user_data) +{ + _get_config_task_maybe_return(user_data, nm_utils_error_new_cancelled(FALSE, NULL)); +} + +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)); + + _LOGD("get-config: starting"); + + 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, NULL, _iface_data_free), + }; + + nmcs_wait_for_objects_register(get_config_data->task); + + for (; hwaddrs && hwaddrs[0]; hwaddrs++) + nmcs_provider_get_config_iface_data_create(get_config_data->result_dict, TRUE, hwaddrs[0]); + + if (cancellable) { + gulong cancelled_id; + + cancelled_id = g_cancellable_connect(cancellable, + G_CALLBACK(_get_config_cancelled_cb), + get_config_data, + NULL); + if (cancelled_id == 0) { + /* the callback was already invoked synchronously and the task already returned. */ + return; + } + + get_config_data->extern_cancelled_id = cancelled_id; + get_config_data->intern_cancellable = g_cancellable_new(); + } + + NMCS_PROVIDER_GET_CLASS(self)->get_config(self, get_config_data); +} + +NMCSProviderGetConfigResult * +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/src/nm-cloud-setup/nmcs-provider.h b/src/nm-cloud-setup/nmcs-provider.h new file mode 100644 index 00000000..36632059 --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider.h @@ -0,0 +1,170 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#ifndef __NMCS_PROVIDER_H__ +#define __NMCS_PROVIDER_H__ + +/*****************************************************************************/ + +#include "nm-http-client.h" + +/*****************************************************************************/ + +typedef struct { + /* And it's exactly the same pointer that is also the key for the iface_datas + * dictionary. */ + const char *hwaddr; + + in_addr_t *ipv4s_arr; + gsize ipv4s_len; + + /* If the interface was seen, get_config() should set this to a + * unique, increasing, positive index. If the interface is requested, + * it is initialized to -1. */ + gssize iface_idx; + + in_addr_t cidr_addr; + in_addr_t gateway; + guint8 cidr_prefix; + bool has_ipv4s : 1; + bool has_cidr : 1; + bool has_gateway : 1; + + NMIPRoute **iproutes_arr; + gsize iproutes_len; + + /* 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_ipv4s && config_data->has_cidr) || config_data->iproutes_len); +} + +NMCSProviderGetConfigIfaceData *nmcs_provider_get_config_iface_data_create(GHashTable *iface_datas, + gboolean was_requested, + const char *hwaddr); + +/*****************************************************************************/ + +typedef struct { + /* A dictionary of (const char *) -> (NMCSProviderGetConfigIfaceData *). + * This is the per-interface result of get_config(). + * + * The key is the same pointer as NMCSProviderGetConfigIfaceData's hwaddr. */ + GHashTable *iface_datas; + + /* The number of iface_datas that are nmcs_provider_get_config_iface_data_is_valid(). */ + guint num_valid_ifaces; + + /* the number of IPv4 addresses over all valid iface_datas. */ + guint num_ipv4s; + + guint n_iface_datas; + + /* The sorted value of @iface_datas, sorted by iface_idx. + * + * Not found entries (iface_idx == -1) are sorted at the end. */ + const NMCSProviderGetConfigIfaceData *const *iface_datas_arr; + +} NMCSProviderGetConfigResult; + +void nmcs_provider_get_config_result_free(NMCSProviderGetConfigResult *result); + +NM_AUTO_DEFINE_FCN0(NMCSProviderGetConfigResult *, + _nm_auto_free_nmcs_provider_get_config_result, + nmcs_provider_get_config_result_free); +#define nm_auto_free_nmcs_provider_get_config_result \ + nm_auto(_nm_auto_free_nmcs_provider_get_config_result) + +/*****************************************************************************/ + +typedef struct { + GTask *task; + + GHashTable *result_dict; + + /* this cancellable should be used for the provider implementation + * to listen for cancellation. */ + GCancellable *intern_cancellable; + + /* the provider implementation may attach extra data. */ + gpointer extra_data; + GDestroyNotify extra_data_destroy; + + gulong extern_cancelled_id; + + /* the provider implementation may use this field to track the number of pending + * operations. */ + guint n_pending; + + bool any : 1; +} NMCSProviderGetConfigTaskData; + +#define NMCS_TYPE_PROVIDER (nmcs_provider_get_type()) +#define NMCS_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER, NMCSProvider)) +#define NMCS_PROVIDER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER, NMCSProviderClass)) +#define NMCS_IS_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER)) +#define NMCS_IS_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER)) +#define NMCS_PROVIDER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER, NMCSProviderClass)) + +#define NMCS_PROVIDER_HTTP_CLIENT "http-client" + +struct _NMCSProviderPrivate; + +typedef struct { + GObject parent; + struct _NMCSProviderPrivate *_priv; +} NMCSProvider; + +typedef struct { + GObjectClass parent; + const char * _name; + const char * _env_provider_enabled; + + void (*detect)(NMCSProvider *self, GTask *task); + + void (*get_config)(NMCSProvider *self, NMCSProviderGetConfigTaskData *get_config_data); + +} NMCSProviderClass; + +GType nmcs_provider_get_type(void); + +/*****************************************************************************/ + +const char *nmcs_provider_get_name(NMCSProvider *provider); + +NMHttpClient *nmcs_provider_get_http_client(NMCSProvider *provider); +GMainContext *nmcs_provider_get_main_context(NMCSProvider *provider); + +/*****************************************************************************/ + +void nmcs_provider_detect(NMCSProvider * provider, + GCancellable * cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean nmcs_provider_detect_finish(NMCSProvider *provider, GAsyncResult *result, GError **error); + +/*****************************************************************************/ + +void _nmcs_provider_get_config_task_maybe_return(NMCSProviderGetConfigTaskData *get_config_data, + GError * error_take); + +void nmcs_provider_get_config(NMCSProvider * provider, + gboolean any, + const char *const * hwaddrs, + GCancellable * cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +NMCSProviderGetConfigResult * +nmcs_provider_get_config_finish(NMCSProvider *provider, GAsyncResult *result, GError **error); + +#endif /* __NMCS_PROVIDER_H__ */ diff --git a/src/nm-cloud-setup/tests/meson.build b/src/nm-cloud-setup/tests/meson.build new file mode 100644 index 00000000..c6881a57 --- /dev/null +++ b/src/nm-cloud-setup/tests/meson.build @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +exe = executable( + 'test-cloud-setup-general', + 'test-cloud-setup-general.c', + dependencies: [ + libnm_dep, + glib_dep, + libcurl_dep, + ], + link_with: [ + libnm_cloud_setup_core, + libnmc_base, + libnm_log_null, + libnm_glib_aux, + libnm_std_aux, + libc_siphash, + ], +) + +test( + 'src/nm-cloud-setup/tests/test-cloud-setup-general', + test_script, + args: test_args + [exe.full_path()], +) diff --git a/src/nm-cloud-setup/tests/test-cloud-setup-general.c b/src/nm-cloud-setup/tests/test-cloud-setup-general.c new file mode 100644 index 00000000..2039ce34 --- /dev/null +++ b/src/nm-cloud-setup/tests/test-cloud-setup-general.c @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "nm-cloud-setup/nm-cloud-setup-utils.h" +#include "libnm-core-aux-intern/nm-libnm-core-utils.h" + +#include "libnm-glib-aux/nm-test-utils.h" + +/*****************************************************************************/ + +static NMSettingIPConfig * +_ri4a_new_s_ip(const char *const *addrs) +{ + NMSettingIPConfig *s_ip; + + s_ip = NM_SETTING_IP_CONFIG(nm_setting_ip4_config_new()); + + for (; addrs && addrs[0]; addrs++) { + nm_auto_unref_ip_address NMIPAddress *a = nmtst_ip_address_new(AF_INET, addrs[0]); + + nm_setting_ip_config_add_address(s_ip, a); + } + return s_ip; +} + +static GPtrArray * +_ri4a_new_arr(const char *const *addrs) +{ + GPtrArray *arr; + + arr = g_ptr_array_new_with_free_func((GDestroyNotify) nm_ip_address_unref); + + for (; addrs && addrs[0]; addrs++) + g_ptr_array_add(arr, nmtst_ip_address_new(AF_INET, addrs[0])); + + return arr; +} + +static void +_test_ri4a(const char *const *addrs_before, const char *const *addrs_new) +{ + gs_unref_object NMSettingIPConfig *s_ip = NULL; + gs_unref_ptrarray GPtrArray *arr = NULL; + const gsize n = NM_PTRARRAY_LEN(addrs_new); + gboolean changed; + gsize i; + + s_ip = _ri4a_new_s_ip(addrs_before); + arr = _ri4a_new_arr(addrs_new); + + g_assert_cmpint(nm_setting_ip_config_get_num_addresses(s_ip), + ==, + NM_PTRARRAY_LEN(addrs_before)); + g_assert_cmpint(arr->len, ==, n); + + changed = nmcs_setting_ip_replace_ipv4_addresses(s_ip, (NMIPAddress **) arr->pdata, arr->len); + + g_assert_cmpint(changed, !=, nm_utils_strv_equal(addrs_before, addrs_new)); + g_assert_cmpint(nm_setting_ip_config_get_num_addresses(s_ip), ==, n); + + for (i = 0; i < n; i++) { + NMIPAddress *a = arr->pdata[i]; + NMIPAddress *b = nm_setting_ip_config_get_address(s_ip, i); + + if (nmtst_get_rand_bool()) + NM_SWAP(&a, &b); + + g_assert(nm_ip_address_cmp_full(a, b, NM_IP_ADDRESS_CMP_FLAGS_WITH_ATTRS) == 0); + } +} + +static void +test_replace_ipv4_addresses(void) +{ + const char *const *const LISTS[] = { + NM_MAKE_STRV(), + NM_MAKE_STRV("192.168.5.1/24"), + NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24"), + NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24", "192.168.5.3/24"), + NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24", "192.168.5.3/24", "192.168.5.4/24"), + }; + int i_run; + + for (i_run = 0; i_run < 20; i_run++) { + gs_free const char **addrs_before = NULL; + gs_free const char **addrs_new = NULL; + + addrs_before = nmtst_rand_perm_strv(LISTS[nmtst_get_rand_uint32() % G_N_ELEMENTS(LISTS)]); + addrs_new = nmtst_rand_perm_strv(LISTS[nmtst_get_rand_uint32() % G_N_ELEMENTS(LISTS)]); + + _test_ri4a(addrs_before, addrs_new); + } + + _test_ri4a(NM_MAKE_STRV(), NM_MAKE_STRV()); + _test_ri4a(NM_MAKE_STRV(), NM_MAKE_STRV("192.168.5.1/24")); + _test_ri4a(NM_MAKE_STRV(), NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24")); + _test_ri4a(NM_MAKE_STRV("192.168.5.1/24"), NM_MAKE_STRV()); + _test_ri4a(NM_MAKE_STRV("192.168.5.1/24"), NM_MAKE_STRV("192.168.5.1/24")); + _test_ri4a(NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24"), NM_MAKE_STRV()); + _test_ri4a(NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24"), NM_MAKE_STRV("192.168.5.1/24")); + _test_ri4a(NM_MAKE_STRV("192.168.5.1/24", "192.168.5.2/24"), NM_MAKE_STRV("192.168.5.2/24")); +} + +/*****************************************************************************/ + +NMTST_DEFINE(); + +int +main(int argc, char **argv) +{ + nmtst_init(&argc, &argv, TRUE); + + g_test_add_func("/cloud-setup/general/replace-ipv4-addresses", test_replace_ipv4_addresses); + + return g_test_run(); +} |