2 * collectd - src/utils_gce.c
5 * Copyright (C) 2017 Florian Forster
7 * Permission to use, copy, modify, and/or distribute this software for any
8 * purpose with or without fee is hereby granted, provided that the above
9 * copyright notice and this permission notice appear in all copies.
11 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 * Florian Forster <octo at collectd.org>
27 #include "utils_gce.h"
28 #include "utils_oauth.h"
29 #include "utils_time.h"
31 #include <curl/curl.h>
33 #ifndef GCP_METADATA_PREFIX
34 #define GCP_METADATA_PREFIX "http://metadata.google.internal/computeMetadata/v1"
36 #ifndef GCE_METADATA_HEADER
37 #define GCE_METADATA_HEADER "Metadata-Flavor: Google"
40 #ifndef GCE_INSTANCE_ID_URL
41 #define GCE_INSTANCE_ID_URL GCP_METADATA_PREFIX "/instance/id"
43 #ifndef GCE_PROJECT_NUM_URL
44 #define GCE_PROJECT_NUM_URL GCP_METADATA_PREFIX "/project/numeric-project-id"
46 #ifndef GCE_PROJECT_ID_URL
47 #define GCE_PROJECT_ID_URL GCP_METADATA_PREFIX "/project/project-id"
50 #define GCE_ZONE_URL GCP_METADATA_PREFIX "/instance/zone"
53 #ifndef GCE_DEFAULT_SERVICE_ACCOUNT
54 #define GCE_DEFAULT_SERVICE_ACCOUNT "default"
58 #define GCE_SCOPE_URL_FORMAT \
59 GCP_METADATA_PREFIX "/instance/service-accounts/%s/scopes"
62 #define GCE_TOKEN_URL_FORMAT \
63 GCP_METADATA_PREFIX "/instance/service-accounts/%s/token"
70 typedef struct blob_s blob_t;
72 static int on_gce = -1;
74 static char *token = NULL;
75 static char *token_email = NULL;
76 static cdtime_t token_valid_until = 0;
77 static pthread_mutex_t token_lock = PTHREAD_MUTEX_INITIALIZER;
79 static size_t write_callback(void *contents, size_t size, size_t nmemb,
82 size_t realsize = size * nmemb;
85 if ((0x7FFFFFF0 < blob->size) || (0x7FFFFFF0 - blob->size < realsize)) {
86 ERROR("utils_gce: write_callback: integer overflow");
90 blob->data = realloc(blob->data, blob->size + realsize + 1);
91 if (blob->data == NULL) {
94 "utils_gce: write_callback: not enough memory (realloc returned NULL)");
98 memcpy(blob->data + blob->size, contents, realsize);
99 blob->size += realsize;
100 blob->data[blob->size] = 0;
103 } /* }}} size_t write_callback */
105 /* read_url will issue a GET request for the given URL, setting the magic GCE
106 * metadata header in the process. On success, the response body is returned
107 * and it's the caller's responsibility to free it. On failure, an error is
108 * logged and NULL is returned. */
109 static char *read_url(char const *url) /* {{{ */
111 CURL *curl = curl_easy_init();
113 ERROR("utils_gce: curl_easy_init failed.");
117 struct curl_slist *headers = curl_slist_append(NULL, GCE_METADATA_HEADER);
119 char curl_errbuf[CURL_ERROR_SIZE];
121 curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
122 curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
123 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
124 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &blob);
125 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
126 curl_easy_setopt(curl, CURLOPT_URL, url);
128 int status = curl_easy_perform(curl);
129 if (status != CURLE_OK) {
130 ERROR("utils_gce: fetching %s failed: %s", url, curl_errbuf);
132 curl_easy_cleanup(curl);
133 curl_slist_free_all(headers);
138 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
139 if ((http_code < 200) || (http_code >= 300)) {
140 ERROR("write_gcm plugin: fetching %s failed: HTTP error %ld", url,
143 curl_easy_cleanup(curl);
144 curl_slist_free_all(headers);
148 curl_easy_cleanup(curl);
149 curl_slist_free_all(headers);
151 } /* }}} char *read_url */
153 _Bool gce_check(void) /* {{{ */
158 DEBUG("utils_gce: Checking whether I'm running on GCE ...");
160 CURL *curl = curl_easy_init();
162 ERROR("utils_gce: curl_easy_init failed.");
166 struct curl_slist *headers = curl_slist_append(NULL, GCE_METADATA_HEADER);
168 char curl_errbuf[CURL_ERROR_SIZE];
169 blob_t blob = {NULL, 0};
170 curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_errbuf);
171 curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
172 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
173 curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_callback);
174 curl_easy_setopt(curl, CURLOPT_WRITEHEADER, &blob);
175 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
176 curl_easy_setopt(curl, CURLOPT_URL, GCP_METADATA_PREFIX "/");
178 int status = curl_easy_perform(curl);
179 if ((status != CURLE_OK) || (blob.data == NULL) ||
180 (strstr(blob.data, "Metadata-Flavor: Google") == NULL)) {
181 DEBUG("utils_gce: ... no (%s)",
183 ? "curl_easy_perform failed"
184 : (blob.data == NULL) ? "blob.data == NULL"
185 : "Metadata-Flavor header not found");
187 curl_easy_cleanup(curl);
188 curl_slist_free_all(headers);
195 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
196 if ((http_code < 200) || (http_code >= 300)) {
197 DEBUG("utils_gce: ... no (HTTP status %ld)", http_code);
198 curl_easy_cleanup(curl);
199 curl_slist_free_all(headers);
204 DEBUG("utils_gce: ... yes");
205 curl_easy_cleanup(curl);
206 curl_slist_free_all(headers);
209 } /* }}} _Bool gce_check */
211 char *gce_project_id(void) /* {{{ */
213 return read_url(GCE_PROJECT_ID_URL);
214 } /* }}} char *gce_project_id */
216 char *gce_instance_id(void) /* {{{ */
218 return read_url(GCE_INSTANCE_ID_URL);
219 } /* }}} char *gce_instance_id */
221 char *gce_zone(void) /* {{{ */
223 return read_url(GCE_ZONE_URL);
224 } /* }}} char *gce_instance_id */
226 char *gce_scope(char const *email) /* {{{ */
230 snprintf(url, sizeof(url), GCE_SCOPE_URL_FORMAT,
231 (email != NULL) ? email : GCE_DEFAULT_SERVICE_ACCOUNT);
233 return read_url(url);
234 } /* }}} char *gce_scope */
236 int gce_access_token(char const *email, char *buffer,
237 size_t buffer_size) /* {{{ */
241 cdtime_t now = cdtime();
243 pthread_mutex_lock(&token_lock);
246 email = GCE_DEFAULT_SERVICE_ACCOUNT;
248 if ((token_email != NULL) && (strcmp(email, token_email) == 0) &&
249 (token_valid_until > now)) {
250 sstrncpy(buffer, token, buffer_size);
251 pthread_mutex_unlock(&token_lock);
255 snprintf(url, sizeof(url), GCE_TOKEN_URL_FORMAT, email);
256 json = read_url(url);
258 pthread_mutex_unlock(&token_lock);
263 cdtime_t expires_in = 0;
264 int status = oauth_parse_json_token(json, tmp, sizeof(tmp), &expires_in);
267 pthread_mutex_unlock(&token_lock);
275 token_email = strdup(email);
277 /* let tokens expire a bit early */
278 expires_in = (expires_in * 95) / 100;
279 token_valid_until = now + expires_in;
281 sstrncpy(buffer, token, buffer_size);
282 pthread_mutex_unlock(&token_lock);
284 } /* }}} char *gce_token */
286 /* vim: set sw=2 sts=2 et fdm=marker : */