Commit 69f048bf authored by Thomas Haller's avatar Thomas Haller

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.
parent 2b6f5a30
......@@ -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
......
......@@ -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
###############################################################################
......
......@@ -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
......
#!/bin/sh
case "$2" in
up|dhcp4-change)
exec systemctl --no-block restart nm-cloud-setup.service
;;
esac
// 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),