From 9c2fa5cbb769c271a20e867ae238dab4822012dd Mon Sep 17 00:00:00 2001 From: Florian Forster Date: Sat, 20 Jun 2015 10:02:27 +0200 Subject: [PATCH] src/utils_gce.[ch]: Add utility for Google Compute Engine. These functions allow to autodetect some settings when running on Google Compute Engine (GCE), for example the Project ID, using the metadata service. --- Makefile.am | 12 +++ src/utils_gce.c | 286 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/utils_gce.h | 52 +++++++++++ 3 files changed, 350 insertions(+) create mode 100644 src/utils_gce.c create mode 100644 src/utils_gce.h diff --git a/Makefile.am b/Makefile.am index ce8c92be..a7e2f4d5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -599,6 +599,18 @@ endif endif endif +if BUILD_WITH_LIBCURL +noinst_LTLIBRARIES += libgce.la +libgce_la_SOURCES = \ + src/utils_gce.c \ + src/utils_gce.h +libgce_la_CPPFLAGS = \ + $(AM_CPPFLAGS) \ + $(BUILD_WITH_LIBCURL_CFLAGS) +libgce_la_LIBADD = \ + $(BUILD_WITH_LIBCURL_LIBS) +endif + if BUILD_PLUGIN_AGGREGATION pkglib_LTLIBRARIES += aggregation.la diff --git a/src/utils_gce.c b/src/utils_gce.c new file mode 100644 index 00000000..3a78478a --- /dev/null +++ b/src/utils_gce.c @@ -0,0 +1,286 @@ +/** + * collectd - src/utils_gce.c + * ISC license + * + * Copyright (C) 2017 Florian Forster + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Authors: + * Florian Forster + **/ + +#include "collectd.h" + +#include "common.h" +#include "plugin.h" +#include "utils_gce.h" +#include "utils_oauth.h" +#include "utils_time.h" + +#include + +#ifndef GCP_METADATA_PREFIX +#define GCP_METADATA_PREFIX "http://metadata.google.internal/computeMetadata/v1" +#endif +#ifndef GCE_METADATA_HEADER +#define GCE_METADATA_HEADER "Metadata-Flavor: Google" +#endif + +#ifndef GCE_INSTANCE_ID_URL +#define GCE_INSTANCE_ID_URL GCP_METADATA_PREFIX "/instance/id" +#endif +#ifndef GCE_PROJECT_NUM_URL +#define GCE_PROJECT_NUM_URL GCP_METADATA_PREFIX "/project/numeric-project-id" +#endif +#ifndef GCE_PROJECT_ID_URL +#define GCE_PROJECT_ID_URL GCP_METADATA_PREFIX "/project/project-id" +#endif +#ifndef GCE_ZONE_URL +#define GCE_ZONE_URL GCP_METADATA_PREFIX "/instance/zone" +#endif + +#ifndef GCE_DEFAULT_SERVICE_ACCOUNT +#define GCE_DEFAULT_SERVICE_ACCOUNT "default" +#endif + +#ifndef GCE_SCOPE_URL +#define GCE_SCOPE_URL_FORMAT \ + GCP_METADATA_PREFIX "/instance/service-accounts/%s/scopes" +#endif +#ifndef GCE_TOKEN_URL +#define GCE_TOKEN_URL_FORMAT \ + GCP_METADATA_PREFIX "/instance/service-accounts/%s/token" +#endif + +struct blob_s { + char *data; + size_t size; +}; +typedef struct blob_s blob_t; + +static int on_gce = -1; + +static char *token = NULL; +static char *token_email = NULL; +static cdtime_t token_valid_until = 0; +static pthread_mutex_t token_lock = PTHREAD_MUTEX_INITIALIZER; + +static size_t write_callback(void *contents, size_t size, size_t nmemb, + void *ud) /* {{{ */ +{ + size_t realsize = size * nmemb; + blob_t *blob = ud; + + if ((0x7FFFFFF0 < blob->size) || (0x7FFFFFF0 - blob->size < realsize)) { + ERROR("utils_gce: write_callback: integer overflow"); + return 0; + } + + blob->data = realloc(blob->data, blob->size + realsize + 1); + if (blob->data == NULL) { + /* out of memory! */ + ERROR( + "utils_gce: write_callback: not enough memory (realloc returned NULL)"); + return 0; + } + + memcpy(blob->data + blob->size, contents, realsize); + blob->size += realsize; + blob->data[blob->size] = 0; + + return realsize; +} /* }}} size_t write_callback */ + +/* read_url will issue a GET request for the given URL, setting the magic GCE + * metadata header in the process. On success, the response body is returned + * and it's the caller's responsibility to free it. On failure, an error is + * logged and NULL is returned. */ +static char *read_url(char const *url) /* {{{ */ +{ + CURL *curl = curl_easy_init(); + if (!curl) { + ERROR("utils_gce: curl_easy_init failed."); + return NULL; + } + + struct curl_slist *headers = curl_slist_append(NULL, GCE_METADATA_HEADER); + + char curl_errbuf[CURL_ERROR_SIZE]; + blob_t blob = {0}; + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &blob); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_URL, url); + + int status = curl_easy_perform(curl); + if (status != CURLE_OK) { + ERROR("utils_gce: fetching %s failed: %s", url, curl_errbuf); + sfree(blob.data); + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + return NULL; + } + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + if ((http_code < 200) || (http_code >= 300)) { + ERROR("write_gcm plugin: fetching %s failed: HTTP error %ld", url, + http_code); + sfree(blob.data); + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + return NULL; + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + return blob.data; +} /* }}} char *read_url */ + +_Bool gce_check(void) /* {{{ */ +{ + if (on_gce != -1) + return on_gce == 1; + + DEBUG("utils_gce: Checking whether I'm running on GCE ..."); + + CURL *curl = curl_easy_init(); + if (!curl) { + ERROR("utils_gce: curl_easy_init failed."); + return 0; + } + + struct curl_slist *headers = curl_slist_append(NULL, GCE_METADATA_HEADER); + + char curl_errbuf[CURL_ERROR_SIZE]; + blob_t blob = {NULL, 0}; + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEHEADER, &blob); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_URL, GCP_METADATA_PREFIX "/"); + + int status = curl_easy_perform(curl); + if ((status != CURLE_OK) || (blob.data == NULL) || + (strstr(blob.data, "Metadata-Flavor: Google") == NULL)) { + DEBUG("utils_gce: ... no (%s)", + (status != CURLE_OK) + ? "curl_easy_perform failed" + : (blob.data == NULL) ? "blob.data == NULL" + : "Metadata-Flavor header not found"); + sfree(blob.data); + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + on_gce = 0; + return 0; + } + sfree(blob.data); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + if ((http_code < 200) || (http_code >= 300)) { + DEBUG("utils_gce: ... no (HTTP status %ld)", http_code); + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + on_gce = 0; + return 0; + } + + DEBUG("utils_gce: ... yes"); + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + on_gce = 1; + return 1; +} /* }}} _Bool gce_check */ + +char *gce_project_id(void) /* {{{ */ +{ + return read_url(GCE_PROJECT_ID_URL); +} /* }}} char *gce_project_id */ + +char *gce_instance_id(void) /* {{{ */ +{ + return read_url(GCE_INSTANCE_ID_URL); +} /* }}} char *gce_instance_id */ + +char *gce_zone(void) /* {{{ */ +{ + return read_url(GCE_ZONE_URL); +} /* }}} char *gce_instance_id */ + +char *gce_scope(char const *email) /* {{{ */ +{ + char url[1024]; + + snprintf(url, sizeof(url), GCE_SCOPE_URL_FORMAT, + (email != NULL) ? email : GCE_DEFAULT_SERVICE_ACCOUNT); + + return read_url(url); +} /* }}} char *gce_scope */ + +int gce_access_token(char const *email, char *buffer, + size_t buffer_size) /* {{{ */ +{ + char url[1024]; + char *json; + cdtime_t now = cdtime(); + + pthread_mutex_lock(&token_lock); + + if (email == NULL) + email = GCE_DEFAULT_SERVICE_ACCOUNT; + + if ((token_email != NULL) && (strcmp(email, token_email) == 0) && + (token_valid_until > now)) { + sstrncpy(buffer, token, buffer_size); + pthread_mutex_unlock(&token_lock); + return 0; + } + + snprintf(url, sizeof(url), GCE_TOKEN_URL_FORMAT, email); + json = read_url(url); + if (json == NULL) { + pthread_mutex_unlock(&token_lock); + return -1; + } + + char tmp[256]; + cdtime_t expires_in = 0; + int status = oauth_parse_json_token(json, tmp, sizeof(tmp), &expires_in); + sfree(json); + if (status != 0) { + pthread_mutex_unlock(&token_lock); + return status; + } + + sfree(token); + token = strdup(tmp); + + sfree(token_email); + token_email = strdup(email); + + /* let tokens expire a bit early */ + expires_in = (expires_in * 95) / 100; + token_valid_until = now + expires_in; + + sstrncpy(buffer, token, buffer_size); + pthread_mutex_unlock(&token_lock); + return 0; +} /* }}} char *gce_token */ + +/* vim: set sw=2 sts=2 et fdm=marker : */ diff --git a/src/utils_gce.h b/src/utils_gce.h new file mode 100644 index 00000000..2ee3f6eb --- /dev/null +++ b/src/utils_gce.h @@ -0,0 +1,52 @@ +/** + * collectd - src/utils_gce.h + * ISC license + * + * Copyright (C) 2017 Florian Forster + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Authors: + * Florian Forster + **/ + +#ifndef UTILS_GCE_H +#define UTILS_GCE_H 1 + +/* gce_check returns 1 when running on Google Compute Engine (GCE) and 0 + * otherwise. */ +_Bool gce_check(void); + +/* gce_project_id returns the project ID of the instance, as configured when + * creating the project. + * For example "example-project-a". */ +char *gce_project_id(void); + +/* gce_instance_id returns the unique ID of the GCE instance. */ +char *gce_instance_id(void); + +/* gce_zone returns the zone in which the GCE instance runs. */ +char *gce_zone(void); + +/* gce_scope returns the list of scopes for the given service account (or the + * default service account when NULL is passed). */ +char *gce_scope(char const *email); + +/* gce_access_token acquires an OAuth access token for the given service account + * (or + * the default service account when NULL is passed) and stores it in buffer. + * Access tokens are automatically cached and renewed when they expire. Returns + * zero on success, non-zero otherwise. */ +int gce_access_token(char const *email, char *buffer, size_t buffer_size); + +#endif -- 2.11.0