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