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.
This commit is contained in:
Íñigo Huguet
2024-10-22 15:48:47 +02:00
parent e4c3d46572
commit 4024e5c612
9 changed files with 289 additions and 0 deletions

1
NEWS
View File

@@ -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

View File

@@ -184,6 +184,10 @@
<para><literal>NM_CLOUD_SETUP_ALIYUN</literal>: boolean, whether Alibaba Cloud (Aliyun) support is enabled. Defaults
to <literal>no</literal>.</para>
</listitem>
<listitem>
<para><literal>NM_CLOUD_SETUP_OCI</literal>: boolean, whether Oracle Cloud (OCI) support is enabled. Defaults
to <literal>no</literal>.</para>
</listitem>
</itemizedlist>
</refsect1>
@@ -417,6 +421,34 @@ ln -s /etc/systemd/system/timers.target.wants/nm-cloud-setup.timer /usr/lib/syst
</itemizedlist>
</refsect2>
<refsect2>
<title>Oracle Cloud (OCI)</title>
<para>For OCI, the tools tries to fetch configuration from <literal>http://169.254.169.254/</literal>. Currently, it only
configures IPv4 and does nothing about IPv6. It will do the following.</para>
<itemizedlist>
<listitem>
<para>First fetch <literal>http://169.254.169.254/opc/v2/instance</literal> 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.</para>
</listitem>
<listitem>
<para>Fetch <literal>http://169.254.169.254/opc/v2/vnics</literal> to get the configuration
for all the VNICs, getting their MAC address, private IP address, gateway and subnet block.</para>
</listitem>
<listitem>
<para>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 <literal>org.freedesktop.nm-cloud-setup.skip=yes</literal>,
it is skipped. Also, there is only one interface and one IP address, the tool does nothing.</para>
<para>Then the tool configures the system like doing for AWS environment. That is, using source based policy routing
with the tables/rules 30200/30400.</para>
</listitem>
</itemizedlist>
</refsect2>
</refsect1>
<refsect1>

View File

@@ -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')

View File

@@ -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;

View File

@@ -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,
],
)

View File

@@ -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"
/*****************************************************************************/

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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__ */