Add License to VES library
[demo.git] / vnfs / VES / code / evel_library / metadata.c
1 /**************************************************************************//**
2  * @file
3  * Wrap the OpenStack metadata service.
4  *
5  * License
6  * -------
7  *
8  * Copyright © 2017 AT&T Intellectual Property. All rights reserved.
9  *
10  * Licensed under the Apache License, Version 2.0 (the "License");
11  * you may not use this file except in compliance with the License.
12  * You may obtain a copy of the License at
13  *        http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  *****************************************************************************/
21
22 #include <string.h>
23 #include <assert.h>
24 #include <malloc.h>
25
26 #include <curl/curl.h>
27
28 #include "evel.h"
29 #include "evel_internal.h"
30 #include "jsmn.h"
31 #include "metadata.h"
32
33 /**************************************************************************//**
34  * URL on the link-local IP address where we can get the metadata in
35  * machine-friendly format.
36  *****************************************************************************/
37 static const char * OPENSTACK_METADATA_URL =
38                       "http://169.254.169.254/openstack/latest/meta_data.json";
39
40 /**************************************************************************//**
41  * How long we're prepared to wait for the metadata service to respond in
42  * seconds.
43  *****************************************************************************/
44 static const int OPENSTACK_METADATA_TIMEOUT = 2;
45
46 /**************************************************************************//**
47  * Size of fields extracted from metadata service.
48  *****************************************************************************/
49 #define MAX_METADATA_STRING  64
50
51 /**************************************************************************//**
52  * UUID of the VM extracted from the OpenStack metadata service.
53  *****************************************************************************/
54 static char vm_uuid[MAX_METADATA_STRING+1] = {0};
55
56 /**************************************************************************//**
57  * Name of the VM extracted from the OpenStack metadata service.
58  *****************************************************************************/
59 static char vm_name[MAX_METADATA_STRING+1] = {0};
60
61 /**************************************************************************//**
62  * How many metadata elements we allow for in the retrieved JSON.
63  *****************************************************************************/
64 static const int MAX_METADATA_TOKENS = 128;
65
66 /*****************************************************************************/
67 /* Local prototypes.                                                         */
68 /*****************************************************************************/
69 static EVEL_ERR_CODES json_get_top_level_string(const char * json_string,
70                                                 const jsmntok_t *tokens,
71                                                 int json_token_count,
72                                                 const char * key,
73                                                 char * value);
74 static EVEL_ERR_CODES json_get_string(const char * json_string,
75                                       const jsmntok_t *tokens,
76                                       int json_token_count,
77                                       const char * key,
78                                       char * value);
79 static int jsoneq(const char *json, const jsmntok_t *tok, const char *s);
80
81 /**************************************************************************//**
82  * Download metadata from the OpenStack metadata service.
83  *
84  * @param verbosity   Controls whether to generate debug to stdout.  Zero:
85  *                    none.  Non-zero: generate debug.
86  * @returns Status code
87  * @retval  EVEL_SUCCESS      On success
88  * @retval  ::EVEL_ERR_CODES  On failure.
89  *****************************************************************************/
90 EVEL_ERR_CODES openstack_metadata(int verbosity)
91 {
92   int rc = EVEL_SUCCESS;
93   CURLcode curl_rc = CURLE_OK;
94   CURL * curl_handle = NULL;
95   MEMORY_CHUNK rx_chunk;
96   char curl_err_string[CURL_ERROR_SIZE] = "<NULL>";
97   jsmn_parser json_parser;
98   jsmntok_t tokens[MAX_METADATA_TOKENS];
99   int json_token_count = 0;
100
101   EVEL_ENTER();
102
103   /***************************************************************************/
104   /* Initialize dummy values for the metadata - needed for test              */
105   /* environments.                                                           */
106   /***************************************************************************/
107   openstack_metadata_initialize();
108
109   /***************************************************************************/
110   /* Get a curl handle which we'll use for accessing the metadata service.   */
111   /***************************************************************************/
112   curl_handle = curl_easy_init();
113   if (curl_handle == NULL)
114   {
115     rc = EVEL_CURL_LIBRARY_FAIL;
116     EVEL_ERROR("Failed to get libcurl handle");
117     goto exit_label;
118   }
119
120   /***************************************************************************/
121   /* Prime the library to give friendly error codes.                         */
122   /***************************************************************************/
123   curl_rc = curl_easy_setopt(curl_handle,
124                              CURLOPT_ERRORBUFFER,
125                              curl_err_string);
126   if (curl_rc != CURLE_OK)
127   {
128     rc = EVEL_CURL_LIBRARY_FAIL;
129     EVEL_ERROR("Failed to initialize libcurl to provide friendly errors. "
130                "Error code=%d", curl_rc);
131     goto exit_label;
132   }
133
134   /***************************************************************************/
135   /* Set the URL for the metadata API.                                       */
136   /***************************************************************************/
137   curl_rc = curl_easy_setopt(curl_handle, CURLOPT_URL, OPENSTACK_METADATA_URL);
138   if (curl_rc != CURLE_OK)
139   {
140     rc = EVEL_CURL_LIBRARY_FAIL;
141     EVEL_ERROR("Failed to initialize libcurl with the API URL. "
142                "Error code=%d (%s)", curl_rc, curl_err_string);
143     goto exit_label;
144   }
145
146   /***************************************************************************/
147   /* send all data to this function.                                         */
148   /***************************************************************************/
149   curl_rc = curl_easy_setopt(curl_handle,
150                              CURLOPT_WRITEFUNCTION,
151                              evel_write_callback);
152   if (curl_rc != CURLE_OK)
153   {
154     rc = EVEL_CURL_LIBRARY_FAIL;
155     EVEL_ERROR("Failed to initialize libcurl with the write callback. "
156              "Error code=%d (%s)", curl_rc, curl_err_string);
157     goto exit_label;
158   }
159
160   /***************************************************************************/
161   /* some servers don't like requests that are made without a user-agent     */
162   /* field, so we provide one.                                               */
163   /***************************************************************************/
164   curl_rc = curl_easy_setopt(curl_handle,
165                              CURLOPT_USERAGENT,
166                              "libcurl-agent/1.0");
167   if (curl_rc != CURLE_OK)
168   {
169     rc = EVEL_CURL_LIBRARY_FAIL;
170     EVEL_ERROR("Failed to initialize libcurl to upload.  Error code=%d (%s)",
171                curl_rc, curl_err_string);
172     goto exit_label;
173   }
174
175   /***************************************************************************/
176   /* Set the timeout for the operation.                                      */
177   /***************************************************************************/
178   curl_rc = curl_easy_setopt(curl_handle,
179                              CURLOPT_TIMEOUT,
180                              OPENSTACK_METADATA_TIMEOUT);
181   if (curl_rc != CURLE_OK)
182   {
183     rc = EVEL_NO_METADATA;
184     EVEL_ERROR("Failed to initialize libcurl to set timeout. "
185                "Error code=%d (%s)", curl_rc, curl_err_string);
186     goto exit_label;
187   }
188
189   /***************************************************************************/
190   /* Create the memory chunk to be used for the response to the post.  The   */
191   /* will be realloced.                                                      */
192   /***************************************************************************/
193   rx_chunk.memory = malloc(1);
194   assert(rx_chunk.memory != NULL);
195   rx_chunk.size = 0;
196
197   /***************************************************************************/
198   /* Point to the data to be received.                                       */
199   /***************************************************************************/
200   curl_rc = curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&rx_chunk);
201   if (curl_rc != CURLE_OK)
202   {
203     rc = EVEL_CURL_LIBRARY_FAIL;
204     EVEL_ERROR("Failed to initialize libcurl to receive metadata. "
205                "Error code=%d (%s)", curl_rc, curl_err_string);
206     goto exit_label;
207   }
208   EVEL_DEBUG("Initialized data to receive");
209
210   /***************************************************************************/
211   /* If running in verbose mode generate more output.                        */
212   /***************************************************************************/
213   if (verbosity > 0)
214   {
215     curl_rc = curl_easy_setopt(curl_handle, CURLOPT_VERBOSE, 1L);
216     if (curl_rc != CURLE_OK)
217     {
218       rc = EVEL_CURL_LIBRARY_FAIL;
219       log_error_state("Failed to initialize libcurl to be verbose. "
220                       "Error code=%d", curl_rc);
221       goto exit_label;
222     }
223   }
224
225   /***************************************************************************/
226   /* Now run off and do what you've been told!                               */
227   /***************************************************************************/
228   curl_rc = curl_easy_perform(curl_handle);
229   if (curl_rc != CURLE_OK)
230   {
231     rc = EVEL_CURL_LIBRARY_FAIL;
232     EVEL_ERROR("Failed to transfer the data from metadata service. "
233                "Error code=%d (%s)", curl_rc, curl_err_string);
234   }
235   else
236   {
237     /*************************************************************************/
238     /* We have some metadata available, so break it out into tokens.         */
239     /*************************************************************************/
240     EVEL_DEBUG("Received metadata size = %d", rx_chunk.size);
241     EVEL_INFO("Received metadata = %s", rx_chunk.memory);
242     jsmn_init(&json_parser);
243     json_token_count = jsmn_parse(&json_parser,
244                                   rx_chunk.memory, rx_chunk.size,
245                                   tokens, MAX_METADATA_TOKENS);
246
247     /*************************************************************************/
248     /* Check that we parsed some data and that the top level is as expected. */
249     /*************************************************************************/
250     if (json_token_count < 0 || tokens[0].type != JSMN_OBJECT)
251     {
252       rc = EVEL_BAD_METADATA;
253       EVEL_ERROR("Failed to parse received JSON OpenStack metadata.  "
254                  "Error code=%d", json_token_count);
255       goto exit_label;
256     }
257     else
258     {
259       EVEL_DEBUG("Extracted %d tokens from the JSON OpenStack metadata.  ",
260                                                              json_token_count);
261     }
262
263     /*************************************************************************/
264     /* Find the keys we want from the metadata.                              */
265     /*************************************************************************/
266     if (json_get_string(rx_chunk.memory,
267                         tokens,
268                         json_token_count,
269                         "uuid",
270                         vm_uuid) != EVEL_SUCCESS)
271     {
272       rc = EVEL_BAD_METADATA;
273       EVEL_ERROR("Failed to extract UUID from OpenStack metadata");
274     }
275     else
276     {
277       EVEL_DEBUG("UUID: %s", vm_uuid);
278     }
279     if (json_get_top_level_string(rx_chunk.memory,
280                                   tokens,
281                                   json_token_count,
282                                   "name",
283                                   vm_name) != EVEL_SUCCESS)
284     {
285       rc = EVEL_BAD_METADATA;
286       EVEL_ERROR("Failed to extract VM Name from OpenStack metadata");
287     }
288     else
289     {
290       EVEL_DEBUG("VM Name: %s", vm_name);
291     }
292   }
293
294 exit_label:
295
296   /***************************************************************************/
297   /* Shut down the cURL library in a tidy manner.                            */
298   /***************************************************************************/
299   if (curl_handle != NULL)
300   {
301     curl_easy_cleanup(curl_handle);
302     curl_handle = NULL;
303   }
304   free(rx_chunk.memory);
305
306   EVEL_EXIT();
307   return rc;
308 }
309
310 /**************************************************************************//**
311  * Initialize default values for vm_name and vm_uuid - for testing purposes.
312  *****************************************************************************/
313 void openstack_metadata_initialize()
314 {
315   strncpy(vm_uuid,
316           "Dummy VM UUID - No Metadata available",
317           MAX_METADATA_STRING);
318   strncpy(vm_name,
319           "Dummy VM name - No Metadata available",
320           MAX_METADATA_STRING);
321 }
322
323 /**************************************************************************//**
324  * Get a string value from supplied JSON by matching the key.
325  *
326  * As the structure of the metadata we're looking at is pretty straightforward
327  * we don't do anything complex (a la XPath) to extract nested keys with the
328  * same leaf name, for example.  Simply walk the structure until we find a
329  * string with the correct value.
330  *
331  * @param[in] json_string   The string which contains the JSON and has already
332  *                          been parsed.
333  * @param[in] tokens        The tokens which the JSON parser found in the JSON.
334  * @param[in] json_token_count  How many tokens were found.
335  * @param[in] key           The key we're looking for.
336  * @param[out] value        The string we found at @p key.
337  *
338  * @returns Status code
339  * @retval  EVEL_SUCCESS      On success - contents of @p value updated.
340  * @retval  EVEL_JSON_KEY_NOT_FOUND  Key not found - @p value not updated.
341  * @retval  EVEL_BAD_JSON     Parser hit unexpected data - @p value not
342  *                            updated.
343  *****************************************************************************/
344 static EVEL_ERR_CODES json_get_string(const char * json_string,
345                                       const jsmntok_t * tokens,
346                                       int json_token_count,
347                                       const char * key,
348                                       char * value)
349 {
350   EVEL_ERR_CODES rc = EVEL_JSON_KEY_NOT_FOUND;
351   int token_num = 0;
352   int token_len = 0;
353
354   EVEL_ENTER();
355
356   /***************************************************************************/
357   /* Check assumptions.                                                      */
358   /***************************************************************************/
359   assert(json_string != NULL);
360   assert(tokens != NULL);
361   assert(json_token_count >= 0);
362   assert(key != NULL);
363   assert(value != NULL);
364
365   for (token_num = 0; token_num < json_token_count; token_num++)
366   {
367     switch(tokens[token_num].type)
368     {
369     case JSMN_OBJECT:
370       EVEL_DEBUG("Skipping object");
371       break;
372
373     case JSMN_ARRAY:
374       EVEL_DEBUG("Skipping array");
375       break;
376
377     case JSMN_STRING:
378       /***********************************************************************/
379       /* This is a string, so may be what we want.  Compare keys.            */
380       /***********************************************************************/
381       if (jsoneq(json_string, &tokens[token_num], key) == 0)
382       {
383         token_len = tokens[token_num + 1].end - tokens[token_num + 1].start;
384         EVEL_DEBUG("Token %d len %d matches at %d to %d", token_num,
385                                                    tokens[token_num + 1].start,
386                                                    tokens[token_num + 1].end);
387         strncpy(value, json_string + tokens[token_num + 1].start, token_len);
388         value[token_len] = '\0';
389         EVEL_DEBUG("Extracted key: \"%s\" Value: \"%s\"", key, value);
390         rc = EVEL_SUCCESS;
391         goto exit_label;
392       }
393       else
394       {
395         EVEL_DEBUG("String key did not match");
396       }
397
398       /***********************************************************************/
399       /* Step over the value, whether we used it or not.                     */
400       /***********************************************************************/
401       token_num++;
402       break;
403
404     case JSMN_PRIMITIVE:
405       EVEL_INFO("Skipping primitive");
406       break;
407
408     case JSMN_UNDEFINED:
409     default:
410       rc = EVEL_BAD_JSON_FORMAT;
411       EVEL_ERROR("Unexpected JSON format at token %d (%d)",
412                   token_num,
413                   tokens[token_num].type);
414       goto exit_label;
415     }
416   }
417
418 exit_label:
419   EVEL_EXIT();
420   return rc;
421 }
422
423 /**************************************************************************//**
424  * Get a top-level string value from supplied JSON by matching the key.
425  *
426  * Unlike json_get_string, this only returns a value that is in the top-level
427  * JSON object.
428  *
429  * @param[in] json_string   The string which contains the JSON and has already
430  *                          been parsed.
431  * @param[in] tokens        The tokens which the JSON parser found in the JSON.
432  * @param[in] json_token_count  How many tokens were found.
433  * @param[in] key           The key we're looking for.
434  * @param[out] value        The string we found at @p key.
435  *
436  * @returns Status code
437  * @retval  EVEL_SUCCESS      On success - contents of @p value updated.
438  * @retval  EVEL_JSON_KEY_NOT_FOUND  Key not found - @p value not updated.
439  * @retval  EVEL_BAD_JSON     Parser hit unexpected data - @p value not
440  *                            updated.
441  *****************************************************************************/
442 static EVEL_ERR_CODES json_get_top_level_string(const char * json_string,
443                                                 const jsmntok_t * tokens,
444                                                 int json_token_count,
445                                                 const char * key,
446                                                 char * value)
447 {
448   EVEL_ERR_CODES rc = EVEL_JSON_KEY_NOT_FOUND;
449   int token_num = 0;
450   int token_len = 0;
451   int bracket_count = 0;
452   int string_index = 0;
453   int increment = 0;
454
455   EVEL_ENTER();
456
457   /***************************************************************************/
458   /* Check assumptions.                                                      */
459   /***************************************************************************/
460   assert(json_string != NULL);
461   assert(tokens != NULL);
462   assert(json_token_count >= 0);
463   assert(key != NULL);
464   assert(value != NULL);
465
466   for (token_num = 0; token_num < json_token_count; token_num++)
467   {
468     switch(tokens[token_num].type)
469     {
470     case JSMN_OBJECT:
471       EVEL_DEBUG("Skipping object");
472       break;
473
474     case JSMN_ARRAY:
475       EVEL_DEBUG("Skipping array");
476       break;
477
478     case JSMN_STRING:
479       /***********************************************************************/
480       /* This is a string, so may be what we want.  Compare keys.            */
481       /***********************************************************************/
482       if (jsoneq(json_string, &tokens[token_num], key) == 0)
483       {
484         /*********************************************************************/
485         /* Count the difference in the number of opening and closing         */
486         /* brackets up to this token.  This needs to be 1 for a top-level    */
487         /* string.  Let's just hope we don't have any strings containing     */
488         /* brackets.                                                         */
489         /*********************************************************************/
490         increment = ((string_index < tokens[token_num].start) ? 1 : -1);
491
492         while (string_index != tokens[token_num].start)
493         {
494           if (json_string[string_index] == '{')
495           {
496             bracket_count += increment;
497           }
498           else if (json_string[string_index] == '}')
499           {
500             bracket_count -= increment;
501           }
502
503           string_index += increment;
504         }
505
506         if (bracket_count == 1)
507         {
508           token_len = tokens[token_num + 1].end - tokens[token_num + 1].start;
509           EVEL_DEBUG("Token %d len %d matches at top level at %d to %d",
510                      token_num,
511                      tokens[token_num + 1].start,
512                      tokens[token_num + 1].end);
513           strncpy(value, json_string + tokens[token_num + 1].start, token_len);
514           value[token_len] = '\0';
515           EVEL_DEBUG("Extracted key: \"%s\" Value: \"%s\"", key, value);
516           rc = EVEL_SUCCESS;
517           goto exit_label;
518         }
519         else
520         {
521           EVEL_DEBUG("String key did match, but not at top level");
522         }
523       }
524       else
525       {
526         EVEL_DEBUG("String key did not match");
527       }
528
529       /***********************************************************************/
530       /* Step over the value, whether we used it or not.                     */
531       /***********************************************************************/
532       token_num++;
533       break;
534
535     case JSMN_PRIMITIVE:
536       EVEL_INFO("Skipping primitive");
537       break;
538
539     case JSMN_UNDEFINED:
540     default:
541       rc = EVEL_BAD_JSON_FORMAT;
542       EVEL_ERROR("Unexpected JSON format at token %d (%d)",
543                   token_num,
544                   tokens[token_num].type);
545       goto exit_label;
546     }
547   }
548
549 exit_label:
550   EVEL_EXIT();
551   return rc;
552 }
553
554 /**************************************************************************//**
555  * Compare a JSON string token with a value.
556  *
557  * @param[in] json The string which contains the JSON and has already been
558  *                 parsed.
559  * @param[in] tok  The token which the JSON parser found in the JSON.
560  * @param[in] s    The string we're looking for.
561  *
562  * @returns        Whether the token matches the string or not.
563  * @retval  0      Value matches
564  * @retval  -1     Value does not match.
565  *****************************************************************************/
566 static int jsoneq(const char *json, const jsmntok_t *tok, const char *s) {
567   if (tok->type == JSMN_STRING && (int) strlen(s) == tok->end - tok->start &&
568       strncmp(json + tok->start, s, tok->end - tok->start) == 0) {
569     return 0;
570   }
571   return -1;
572 }
573
574 /**************************************************************************//**
575  * Get the VM name provided by the metadata service.
576  *
577  * @returns VM name
578  *****************************************************************************/
579 const char *openstack_vm_name()
580 {
581   return vm_name;
582 }
583
584 /**************************************************************************//**
585  * Get the VM UUID provided by the metadata service.
586  *
587  * @returns VM UUID
588  *****************************************************************************/
589 const char *openstack_vm_uuid()
590 {
591   return vm_uuid;
592 }