mirror of
https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git
synced 2026-02-28 06:20:36 +01:00
The present version of the EC2 metadata API (IMDSv2) requires a header with a token to be present in all requests. The token is essentially a cookie that's not actually a cookie that's obtained with a PUT call that doesn't put anything. Apparently it's too easy to trick someone into calling a GET method. EC2 now supports IMDSv2 everywhere with IMDSv1 being optional, so let's just use IMDSv2 unconditionally. Also, the presence of a token API can be used to detect the AWS EC2 cloud. Conflicts: - code format - missing494819bbbf("cloud-setup: move common code for get_config() to base class and improve cancellation"). From it we only needed the `get_config_data->self` part, but used g_task_get_source_object instead. - missing5fb2f7e717("cloud-setup/trivial: rename "response_data" variable") https://bugzilla.redhat.com/show_bug.cgi?id=2151986 (cherry picked from commit8b7e12c2d6) (cherry picked from commit429f36cd81) (cherry picked from commite3ac982b32) (cherry picked from commitc5a3e739b1) (cherry picked from commitee157ad48b) (cherry picked from commitae3ec36462) (cherry picked from commit865fe0732e) (cherry picked from commitd75e307ebc) (cherry picked from commitef3d4758d6)
595 lines
19 KiB
C
595 lines
19 KiB
C
// 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/"
|
|
|
|
/* Token TTL of 180 seconds is chosen abitrarily, in hope that it is
|
|
* surely more than enough to read all relevant metadata. */
|
|
#define NM_EC2_TOKEN_TTL_HEADER "X-aws-ec2-metadata-token-ttl-seconds: 180"
|
|
#define NM_EC2_TOKEN_HEADER "X-aws-ec2-metadata-token: "
|
|
|
|
static const char *
|
|
_ec2_base (void)
|
|
{
|
|
static const char *base_cached = NULL;
|
|
const char *base;
|
|
|
|
again:
|
|
base = g_atomic_pointer_get (&base_cached);
|
|
if (G_UNLIKELY (!base)) {
|
|
|
|
/* The base URI can be set via environment variable.
|
|
* This is mainly for testing, it's not usually supposed to be configured.
|
|
* Consider this private API! */
|
|
base = g_getenv (NMCS_ENV_VARIABLE ("NM_CLOUD_SETUP_EC2_HOST"));
|
|
|
|
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__)
|
|
|
|
/*****************************************************************************/
|
|
|
|
enum {
|
|
NM_EC2_HTTP_HEADER_TOKEN,
|
|
NM_EC2_HTTP_HEADER_SENTINEL,
|
|
_NM_EC2_HTTP_HEADER_NUM,
|
|
};
|
|
|
|
struct _NMCSProviderEC2 {
|
|
NMCSProvider parent;
|
|
char *token;
|
|
};
|
|
|
|
struct _NMCSProviderEC2Class {
|
|
NMCSProviderClass parent;
|
|
};
|
|
|
|
G_DEFINE_TYPE (NMCSProviderEC2, nmcs_provider_ec2, NMCS_TYPE_PROVIDER);
|
|
|
|
/*****************************************************************************/
|
|
|
|
static void
|
|
_detect_get_token_done_cb (GObject *source,
|
|
GAsyncResult *result,
|
|
gpointer user_data)
|
|
{
|
|
gs_unref_object GTask *task = user_data;
|
|
NMCSProviderEC2 *self = NMCS_PROVIDER_EC2(g_task_get_source_object(task));
|
|
gs_unref_bytes GBytes *response = NULL;
|
|
gs_free_error GError *get_error = NULL;
|
|
gs_free_error GError *error = NULL;
|
|
gboolean success;
|
|
|
|
nm_clear_g_free(&self->token);
|
|
|
|
success = nm_http_client_poll_req_finish (NM_HTTP_CLIENT (source),
|
|
result,
|
|
NULL,
|
|
&response,
|
|
&get_error);
|
|
|
|
if (nm_utils_error_is_cancelled (get_error)) {
|
|
g_task_return_error (task, g_steal_pointer (&get_error));
|
|
return;
|
|
}
|
|
|
|
if (get_error) {
|
|
nm_utils_error_set (&error,
|
|
NM_UTILS_ERROR_UNKNOWN,
|
|
"failure to get EC2 metadata: %s",
|
|
get_error->message);
|
|
g_task_return_error (task, g_steal_pointer (&error));
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* We use the token as-is. Special characters can cause confusion (e.g.
|
|
* response splitting), but we're not crossing a security boundary.
|
|
* None of the examples in AWS documentation does any sort of
|
|
* sanitization either. */
|
|
self->token = g_strconcat(NM_EC2_TOKEN_HEADER, g_bytes_get_data(response, NULL), NULL);
|
|
|
|
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 = _ec2_uri_concat ("latest/api/token")),
|
|
HTTP_TIMEOUT_MS,
|
|
256*1024,
|
|
7000,
|
|
1000,
|
|
NM_MAKE_STRV(NM_EC2_TOKEN_TTL_HEADER),
|
|
"PUT",
|
|
g_task_get_cancellable (task),
|
|
NULL,
|
|
NULL,
|
|
_detect_get_token_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))
|
|
_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_req_finish (http_client,
|
|
result,
|
|
NULL,
|
|
&response_data,
|
|
&error);
|
|
if (nm_utils_error_is_cancelled (error))
|
|
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;
|
|
NMCSProviderEC2 *self = NMCS_PROVIDER_EC2(g_task_get_source_object(get_config_data->task));
|
|
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_req_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)) {
|
|
_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_req (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,
|
|
NM_MAKE_STRV(self->token),
|
|
NULL,
|
|
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_req (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,
|
|
NM_MAKE_STRV(self->token),
|
|
NULL,
|
|
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 (GetConfigMetadataMac) + 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)
|
|
{
|
|
NMCSProviderEC2 *self = NMCS_PROVIDER_EC2(provider);
|
|
gs_free char *uri = NULL;
|
|
GetConfigMetadataData *metadata_data;
|
|
|
|
/* This can be called only if detect() succeeded, which implies
|
|
* there must be a token.
|
|
*/
|
|
nm_assert(self->token);
|
|
|
|
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_req (nmcs_provider_get_http_client (provider),
|
|
(uri = _ec2_uri_interfaces ()),
|
|
HTTP_TIMEOUT_MS,
|
|
256 * 1024,
|
|
15000,
|
|
1000,
|
|
NM_MAKE_STRV(self->token),
|
|
NULL,
|
|
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
|
|
dispose(GObject *object)
|
|
{
|
|
NMCSProviderEC2 *self = NMCS_PROVIDER_EC2(object);
|
|
|
|
nm_clear_g_free(&self->token);
|
|
|
|
G_OBJECT_CLASS(nmcs_provider_ec2_parent_class)->dispose(object);
|
|
}
|
|
|
|
static void
|
|
nmcs_provider_ec2_class_init (NMCSProviderEC2Class *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
|
NMCSProviderClass *provider_class = NMCS_PROVIDER_CLASS (klass);
|
|
|
|
object_class->dispose = dispose;
|
|
|
|
provider_class->_name = "ec2";
|
|
provider_class->_env_provider_enabled = NMCS_ENV_VARIABLE ("NM_CLOUD_SETUP_EC2");
|
|
provider_class->detect = detect;
|
|
provider_class->get_config = get_config;
|
|
}
|