From 4024e5c6123f756db5d1d5b126c2e90336893989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8D=C3=B1igo=20Huguet?= Date: Tue, 22 Oct 2024 15:48:47 +0200 Subject: [PATCH] cloud-setup: Add OCI (Oracle Cloud) provider Initial support for OCI. It doesn't support VLAN configuration yet as the requirements are not clear. It doesn't support secondary IP addresses because the IMDS server doesn't expose them. Instead of using plain text format, it gets a single response in JSON format and parses it. The dependency to jansson is now mandatory for that. --- NEWS | 1 + man/nm-cloud-setup.xml | 32 +++ meson.build | 1 + src/nm-cloud-setup/main.c | 2 + src/nm-cloud-setup/meson.build | 2 + src/nm-cloud-setup/nm-cloud-setup-utils.h | 2 + src/nm-cloud-setup/nm-cloud-setup.service.in | 1 + src/nm-cloud-setup/nmcs-provider-oci.c | 221 +++++++++++++++++++ src/nm-cloud-setup/nmcs-provider-oci.h | 27 +++ 9 files changed, 289 insertions(+) create mode 100644 src/nm-cloud-setup/nmcs-provider-oci.c create mode 100644 src/nm-cloud-setup/nmcs-provider-oci.h diff --git a/NEWS b/NEWS index d2f5a31d4b..427a1afe9c 100644 --- a/NEWS +++ b/NEWS @@ -22,6 +22,7 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! * Support automatically adding routes to DNS servers via the ipv4.routed-dns and ipv6.routed-dns properties; when enabled, each name server is reached only via the device that specifies it. +* Support OCI in nm-cloud-setup ============================================= NetworkManager-1.50 diff --git a/man/nm-cloud-setup.xml b/man/nm-cloud-setup.xml index 7f9a7dbc52..b24de6b982 100644 --- a/man/nm-cloud-setup.xml +++ b/man/nm-cloud-setup.xml @@ -184,6 +184,10 @@ NM_CLOUD_SETUP_ALIYUN: boolean, whether Alibaba Cloud (Aliyun) support is enabled. Defaults to no. + + NM_CLOUD_SETUP_OCI: boolean, whether Oracle Cloud (OCI) support is enabled. Defaults + to no. + @@ -417,6 +421,34 @@ ln -s /etc/systemd/system/timers.target.wants/nm-cloud-setup.timer /usr/lib/syst + + Oracle Cloud (OCI) + + For OCI, the tools tries to fetch configuration from http://169.254.169.254/. Currently, it only + configures IPv4 and does nothing about IPv6. It will do the following. + + + + First fetch http://169.254.169.254/opc/v2/instance to determine whether the + expected API is present. This determines whether OCI environment is detected and whether to proceed + to configure the host using OCI meta data. + + + Fetch http://169.254.169.254/opc/v2/vnics to get the configuration + for all the VNICs, getting their MAC address, private IP address, gateway and subnet block. + + + Then nm-cloud-setup iterates over all interfaces for which it could fetch a configuration. + If no ethernet device for the respective MAC address is found, it is skipped. + Also, if the device is currently not activated in NetworkManager or if the currently + activated profile has a user-data org.freedesktop.nm-cloud-setup.skip=yes, + it is skipped. Also, there is only one interface and one IP address, the tool does nothing. + Then the tool configures the system like doing for AWS environment. That is, using source based policy routing + with the tables/rules 30200/30400. + + + + diff --git a/meson.build b/meson.build index f535cd3725..b4449b0134 100644 --- a/meson.build +++ b/meson.build @@ -800,6 +800,7 @@ 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') + assert(jansson_dep.found(), 'nm-cloud-setup requires jansson library. Use -Dnm_cloud_setup=false to disable it') endif enable_docs = get_option('docs') diff --git a/src/nm-cloud-setup/main.c b/src/nm-cloud-setup/main.c index 6f614a607a..1de890be58 100644 --- a/src/nm-cloud-setup/main.c +++ b/src/nm-cloud-setup/main.c @@ -11,6 +11,7 @@ #include "nmcs-provider-gcp.h" #include "nmcs-provider-azure.h" #include "nmcs-provider-aliyun.h" +#include "nmcs-provider-oci.h" #include "libnm-core-aux-intern/nm-libnm-core-utils.h" /*****************************************************************************/ @@ -104,6 +105,7 @@ _provider_detect(SigTermData *sigterm_data) NMCS_TYPE_PROVIDER_GCP, NMCS_TYPE_PROVIDER_AZURE, NMCS_TYPE_PROVIDER_ALIYUN, + NMCS_TYPE_PROVIDER_OCI, }; int i; gulong cancellable_signal_id; diff --git a/src/nm-cloud-setup/meson.build b/src/nm-cloud-setup/meson.build index 872b3352b8..adb425ecb0 100644 --- a/src/nm-cloud-setup/meson.build +++ b/src/nm-cloud-setup/meson.build @@ -36,12 +36,14 @@ libnm_cloud_setup_core = static_library( 'nmcs-provider-gcp.c', 'nmcs-provider-azure.c', 'nmcs-provider-aliyun.c', + 'nmcs-provider-oci.c', 'nmcs-provider.c', ), dependencies: [ libnm_dep, glib_dep, libcurl_dep, + jansson_dep, ], ) diff --git a/src/nm-cloud-setup/nm-cloud-setup-utils.h b/src/nm-cloud-setup/nm-cloud-setup-utils.h index 434f7fcf0b..c4662842d2 100644 --- a/src/nm-cloud-setup/nm-cloud-setup-utils.h +++ b/src/nm-cloud-setup/nm-cloud-setup-utils.h @@ -12,6 +12,7 @@ #define NMCS_ENV_NM_CLOUD_SETUP_AZURE "NM_CLOUD_SETUP_AZURE" #define NMCS_ENV_NM_CLOUD_SETUP_EC2 "NM_CLOUD_SETUP_EC2" #define NMCS_ENV_NM_CLOUD_SETUP_GCP "NM_CLOUD_SETUP_GCP" +#define NMCS_ENV_NM_CLOUD_SETUP_OCI "NM_CLOUD_SETUP_OCI" #define NMCS_ENV_NM_CLOUD_SETUP_LOG "NM_CLOUD_SETUP_LOG" /* Undocumented/internal environment variables for configuring nm-cloud-setup. @@ -20,6 +21,7 @@ #define NMCS_ENV_NM_CLOUD_SETUP_AZURE_HOST "NM_CLOUD_SETUP_AZURE_HOST" #define NMCS_ENV_NM_CLOUD_SETUP_EC2_HOST "NM_CLOUD_SETUP_EC2_HOST" #define NMCS_ENV_NM_CLOUD_SETUP_GCP_HOST "NM_CLOUD_SETUP_GCP_HOST" +#define NMCS_ENV_NM_CLOUD_SETUP_OCI_HOST "NM_CLOUD_SETUP_OCI_HOST" #define NMCS_ENV_NM_CLOUD_SETUP_MAP_INTERFACES "NM_CLOUD_SETUP_MAP_INTERFACES" /*****************************************************************************/ diff --git a/src/nm-cloud-setup/nm-cloud-setup.service.in b/src/nm-cloud-setup/nm-cloud-setup.service.in index cb782b2219..455f0ee066 100644 --- a/src/nm-cloud-setup/nm-cloud-setup.service.in +++ b/src/nm-cloud-setup/nm-cloud-setup.service.in @@ -31,6 +31,7 @@ ExecStart=@libexecdir@/nm-cloud-setup #Environment=NM_CLOUD_SETUP_GCP=yes #Environment=NM_CLOUD_SETUP_AZURE=yes #Environment=NM_CLOUD_SETUP_ALIYUN=yes +#Environment=NM_CLOUD_SETUP_OCI=yes CapabilityBoundingSet= KeyringMode=private diff --git a/src/nm-cloud-setup/nmcs-provider-oci.c b/src/nm-cloud-setup/nmcs-provider-oci.c new file mode 100644 index 0000000000..324d88bd84 --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-oci.c @@ -0,0 +1,221 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "libnm-client-aux-extern/nm-default-client.h" +#include "nmcs-provider-oci.h" +#include "nm-cloud-setup-utils.h" +#include "libnm-glib-aux/nm-jansson.h" + +/*****************************************************************************/ + +#define HTTP_TIMEOUT_MS 3000 + +#define NM_OCI_HEADER "Authorization:Bearer Oracle" +#define NM_OCI_HOST "169.254.169.254" +#define NM_OCI_BASE "http://" NM_OCI_HOST + +NMCS_DEFINE_HOST_BASE(_oci_base, NMCS_ENV_NM_CLOUD_SETUP_OCI_HOST, NM_OCI_BASE); + +#define _oci_uri_concat(...) nmcs_utils_uri_build_concat(_oci_base(), "opc/v2/", __VA_ARGS__) + +/*****************************************************************************/ + +struct _NMCSProviderOCI { + NMCSProvider parent; +}; + +struct _NMCSProviderOCIClass { + NMCSProviderClass parent; +}; + +G_DEFINE_TYPE(NMCSProviderOCI, nmcs_provider_oci, NMCS_TYPE_PROVIDER); + +/*****************************************************************************/ + +static void +_detect_done_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + gs_unref_object GTask *task = user_data; + gs_free_error GError *get_error = NULL; + gs_free_error GError *error = NULL; + + nm_http_client_poll_req_finish(NM_HTTP_CLIENT(source), result, NULL, NULL, &get_error); + + if (nm_utils_error_is_cancelled(get_error)) { + g_task_return_error(task, g_steal_pointer(&get_error)); + return; + } + + if (get_error) { + nm_utils_error_set(&error, + NM_UTILS_ERROR_UNKNOWN, + "failure to get OCI instance data: %s", + get_error->message); + g_task_return_error(task, g_steal_pointer(&error)); + return; + } + + g_task_return_boolean(task, TRUE); +} + +static void +detect(NMCSProvider *provider, GTask *task) +{ + NMHttpClient *http_client; + gs_free char *uri = NULL; + + http_client = nmcs_provider_get_http_client(provider); + + nm_http_client_poll_req(http_client, + (uri = _oci_uri_concat("instance")), + HTTP_TIMEOUT_MS, + 256 * 1024, + 7000, + 1000, + NM_MAKE_STRV(NM_OCI_HEADER), + NULL, + g_task_get_cancellable(task), + NULL, + NULL, + _detect_done_cb, + task); +} + +/*****************************************************************************/ + +static void +_get_config_done_cb(GObject *source, GAsyncResult *result, gpointer user_data) +{ + NMCSProviderGetConfigTaskData *get_config_data; + NMCSProviderGetConfigIfaceData *config_iface_data; + gs_unref_bytes GBytes *response = NULL; + gs_free_error GError *error = NULL; + nm_auto_decref_json json_t *vnics = NULL; + size_t i; + + nm_http_client_poll_req_finish(NM_HTTP_CLIENT(source), result, NULL, &response, &error); + + if (nm_utils_error_is_cancelled(error)) + return; + + get_config_data = user_data; + + if (error) + goto out; + + vnics = json_loads(g_bytes_get_data(response, NULL), JSON_REJECT_DUPLICATES, NULL); + if (!vnics || !json_is_array(vnics)) { + nm_utils_error_set(&error, + NM_UTILS_ERROR_UNKNOWN, + "get-config: JSON parse failure, can't configure VNICs"); + goto out; + } + + for (i = 0; i < json_array_size(vnics); i++) { + json_t *vnic, *field; + const char *vnic_id, *val; + gs_free char *mac = NULL; + in_addr_t addr; + int prefix; + + vnic = json_array_get(vnics, i); + if (!json_is_object(vnic)) { + _LOGW("get-config: JSON parse failure for VNIC at index %zu, ignoring VNIC", i); + continue; + } + + field = json_object_get(vnic, "vnicId"); + vnic_id = field && json_is_string(field) ? json_string_value(field) : ""; + + field = json_object_get(vnic, "macAddr"); + val = field && json_is_string(field) ? json_string_value(field) : NULL; + if (!val) { + _LOGW("get-config: missing or invalid 'macAddr' (VNIC %s idx=%zu), ignoring VNIC", + vnic_id, + i); + continue; + } + + mac = nmcs_utils_hwaddr_normalize(val, json_string_length(field)); + config_iface_data = nmcs_provider_get_config_iface_data_create(get_config_data, FALSE, mac); + config_iface_data->iface_idx = i; + + field = json_object_get(vnic, "privateIp"); + val = field && json_is_string(field) ? json_string_value(field) : NULL; + if (val && nm_inet_parse_bin(AF_INET, val, NULL, &addr)) { + config_iface_data->has_ipv4s = TRUE; + config_iface_data->ipv4s_len = 1; + config_iface_data->ipv4s_arr = g_new(in_addr_t, 1); + config_iface_data->ipv4s_arr[0] = addr; + } else { + _LOGW("get-config: missing or invalid 'privateIp' (VNIC %s idx=%zu)", vnic_id, i); + } + + field = json_object_get(vnic, "virtualRouterIp"); + val = field && json_is_string(field) ? json_string_value(field) : NULL; + if (val && nm_inet_parse_bin(AF_INET, val, NULL, &addr)) { + config_iface_data->has_gateway = TRUE; + config_iface_data->gateway = addr; + } else { + _LOGW("get-config: missing or invalid 'virtualRouterIp' (VNIC %s idx=%zu)", vnic_id, i); + } + + field = json_object_get(vnic, "subnetCidrBlock"); + val = field && json_is_string(field) ? json_string_value(field) : NULL; + if (val && nm_inet_parse_with_prefix_bin(AF_INET, val, NULL, &addr, &prefix)) { + config_iface_data->has_cidr = TRUE; + config_iface_data->cidr_addr = addr; + config_iface_data->cidr_prefix = prefix; + } else { + _LOGW("get-config: missing or invalid 'subnetCidrBlock' (VNIC %s idx=%zu)", vnic_id, i); + } + } + +out: + _nmcs_provider_get_config_task_maybe_return(get_config_data, g_steal_pointer(&error)); +} + +static void +get_config(NMCSProvider *provider, NMCSProviderGetConfigTaskData *get_config_data) +{ + gs_free const char *uri = NULL; + + nm_http_client_poll_req(nmcs_provider_get_http_client(provider), + (uri = _oci_uri_concat("vnics")), + HTTP_TIMEOUT_MS, + 256 * 1024, + 15000, + 1000, + NM_MAKE_STRV(NM_OCI_HEADER), + NULL, + get_config_data->intern_cancellable, + NULL, + NULL, + _get_config_done_cb, + get_config_data); +} + +/*****************************************************************************/ + +static void +nmcs_provider_oci_init(NMCSProviderOCI *self) +{} + +static void +dispose(GObject *object) +{ + G_OBJECT_CLASS(nmcs_provider_oci_parent_class)->dispose(object); +} + +static void +nmcs_provider_oci_class_init(NMCSProviderOCIClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS(klass); + + object_class->dispose = dispose; + + provider_class->_name = "oci"; + provider_class->_env_provider_enabled = NMCS_ENV_NM_CLOUD_SETUP_OCI; + provider_class->detect = detect; + provider_class->get_config = get_config; +} diff --git a/src/nm-cloud-setup/nmcs-provider-oci.h b/src/nm-cloud-setup/nmcs-provider-oci.h new file mode 100644 index 0000000000..8447bc1c5b --- /dev/null +++ b/src/nm-cloud-setup/nmcs-provider-oci.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#ifndef __NMCS_PROVIDER_OCI_H__ +#define __NMCS_PROVIDER_OCI_H__ + +#include "nmcs-provider.h" + +/*****************************************************************************/ + +typedef struct _NMCSProviderOCI NMCSProviderOCI; +typedef struct _NMCSProviderOCIClass NMCSProviderOCIClass; + +#define NMCS_TYPE_PROVIDER_OCI (nmcs_provider_oci_get_type()) +#define NMCS_PROVIDER_OCI(obj) \ + (_NM_G_TYPE_CHECK_INSTANCE_CAST((obj), NMCS_TYPE_PROVIDER_OCI, NMCSProviderOCI)) +#define NMCS_PROVIDER_OCI_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), NMCS_TYPE_PROVIDER_OCI, NMCSProviderOCIClass)) +#define NMCS_IS_PROVIDER_OCI(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), NMCS_TYPE_PROVIDER_OCI)) +#define NMCS_IS_PROVIDER_OCI_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), NMCS_TYPE_PROVIDER_OCI)) +#define NMCS_PROVIDER_OCI_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), NMCS_TYPE_PROVIDER_OCI, NMCSProviderOCIClass)) + +GType nmcs_provider_oci_get_type(void); + +/*****************************************************************************/ + +#endif /* __NMCS_PROVIDER_OCI_H__ */