X-Git-Url: https://git.octo.it/?p=rrdtool.git;a=blobdiff_plain;f=src%2Frrd_client.c;h=21139f0ba3be7b772fc00001239d841fb136fa0c;hp=287f642db4754253eba0134679bf1e650f60ad45;hb=96b0f4aace0deef034a792a08dc2d426cd2b61a4;hpb=6f5e93c0c08c5ed796d8d079f0eba2d16c6a4bdb diff --git a/src/rrd_client.c b/src/rrd_client.c index 287f642..21139f0 100644 --- a/src/rrd_client.c +++ b/src/rrd_client.c @@ -1,19 +1,24 @@ /** * RRDTool - src/rrd_client.c - * Copyright (C) 2008 Florian octo Forster + * Copyright (C) 2008-2010 Florian octo Forster * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the - * Free Software Foundation; only version 2 of the License is applicable. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. * * Authors: * Florian octo Forster @@ -21,11 +26,13 @@ **/ #include "rrd.h" -#include "rrd_client.h" #include "rrd_tool.h" +#include "rrd_client.h" +#include #include #include +#include #include #include #include @@ -33,6 +40,7 @@ #include #include #include +#include #ifndef ENODATA #define ENODATA ENOENT @@ -65,31 +73,163 @@ static const char *get_path (const char *path, char *resolved_path) /* {{{ */ const char *ret = path; int is_unix = 0; + if ((path == NULL) || (resolved_path == NULL) || (sd_path == NULL)) + return (NULL); + if ((*sd_path == '/') || (strncmp ("unix:", sd_path, strlen ("unix:")) == 0)) is_unix = 1; - if (*path == '/') /* absolute path */ + if (is_unix) + { + ret = realpath(path, resolved_path); + if (ret == NULL) + rrd_set_error("realpath(%s): %s", path, rrd_strerror(errno)); + return ret; + } + else { - if (! is_unix) + if (*path == '/') /* not absolute path */ { rrd_set_error ("absolute path names not allowed when talking " "to a remote daemon"); - return (NULL); + return NULL; } - /* else: nothing to do */ } - else /* relative path */ + + return path; +} /* }}} char *get_path */ + +static size_t strsplit (char *string, char **fields, size_t size) /* {{{ */ +{ + size_t i; + char *ptr; + char *saveptr; + + i = 0; + ptr = string; + saveptr = NULL; + while ((fields[i] = strtok_r (ptr, " \t\r\n", &saveptr)) != NULL) { - if (is_unix) - { - realpath (path, resolved_path); - ret = resolved_path; - } - /* else: nothing to do */ + ptr = NULL; + i++; + + if (i >= size) + break; } - return (ret); -} /* }}} char *get_path */ + + return (i); +} /* }}} size_t strsplit */ + +static int parse_header (char *line, /* {{{ */ + char **ret_key, char **ret_value) +{ + char *tmp; + + *ret_key = line; + + tmp = strchr (line, ':'); + if (tmp == NULL) + return (-1); + + do + { + *tmp = 0; + tmp++; + } + while ((tmp[0] == ' ') || (tmp[0] == '\t')); + + if (*tmp == 0) + return (-1); + + *ret_value = tmp; + return (0); +} /* }}} int parse_header */ + +static int parse_ulong_header (char *line, /* {{{ */ + char **ret_key, unsigned long *ret_value) +{ + char *str_value; + char *endptr; + int status; + + str_value = NULL; + status = parse_header (line, ret_key, &str_value); + if (status != 0) + return (status); + + endptr = NULL; + errno = 0; + *ret_value = (unsigned long) strtol (str_value, &endptr, /* base = */ 0); + if ((endptr == str_value) || (errno != 0)) + return (-1); + + return (0); +} /* }}} int parse_ulong_header */ + +static int parse_char_array_header (char *line, /* {{{ */ + char **ret_key, char **array, size_t array_len, int alloc) +{ + char *tmp_array[array_len]; + char *value; + size_t num; + int status; + + value = NULL; + status = parse_header (line, ret_key, &value); + if (status != 0) + return (-1); + + num = strsplit (value, tmp_array, array_len); + if (num != array_len) + return (-1); + + if (alloc == 0) + { + memcpy (array, tmp_array, sizeof (tmp_array)); + } + else + { + size_t i; + + for (i = 0; i < array_len; i++) + array[i] = strdup (tmp_array[i]); + } + + return (0); +} /* }}} int parse_char_array_header */ + +static int parse_value_array_header (char *line, /* {{{ */ + time_t *ret_time, rrd_value_t *array, size_t array_len) +{ + char *str_key; + char *str_array[array_len]; + char *endptr; + int status; + size_t i; + + str_key = NULL; + status = parse_char_array_header (line, &str_key, + str_array, array_len, /* alloc = */ 0); + if (status != 0) + return (-1); + + errno = 0; + endptr = NULL; + *ret_time = (time_t) strtol (str_key, &endptr, /* base = */ 10); + if ((endptr == str_key) || (errno != 0)) + return (-1); + + for (i = 0; i < array_len; i++) + { + endptr = NULL; + array[i] = (rrd_value_t) strtod (str_array[i], &endptr); + if ((endptr == str_array[i]) || (errno != 0)) + return (-1); + } + + return (0); +} /* }}} int parse_value_array_header */ /* One must hold `lock' when calling `close_connection'. */ static void close_connection (void) /* {{{ */ @@ -176,6 +316,16 @@ static int buffer_add_value (const char *value, /* {{{ */ return (buffer_add_string (temp, buffer_ret, buffer_size_ret)); } /* }}} int buffer_add_value */ +static int buffer_add_ulong (const unsigned long value, /* {{{ */ + char **buffer_ret, size_t *buffer_size_ret) +{ + char temp[4096]; + + snprintf (temp, sizeof (temp), "%lu", value); + temp[sizeof (temp) - 1] = 0; + return (buffer_add_string (temp, buffer_ret, buffer_size_ret)); +} /* }}} int buffer_add_ulong */ + /* Remove trailing newline (NL) and carriage return (CR) characters. Similar to * the Perl function `chomp'. Returns the number of characters that have been * removed. */ @@ -219,34 +369,36 @@ static void response_free (rrdc_response_t *res) /* {{{ */ static int response_read (rrdc_response_t **ret_response) /* {{{ */ { - rrdc_response_t *ret; + rrdc_response_t *ret = NULL; + int status = 0; char buffer[4096]; char *buffer_ptr; size_t i; +#define DIE(code) do { status = code; goto err_out; } while(0) + if (sh == NULL) - return (-1); + DIE(-1); ret = (rrdc_response_t *) malloc (sizeof (rrdc_response_t)); if (ret == NULL) - return (-2); + DIE(-2); memset (ret, 0, sizeof (*ret)); ret->lines = NULL; ret->lines_num = 0; buffer_ptr = fgets (buffer, sizeof (buffer), sh); if (buffer_ptr == NULL) - return (-3); + DIE(-3); + chomp (buffer); ret->status = strtol (buffer, &ret->message, 0); if (buffer == ret->message) - { - response_free (ret); - return (-4); - } + DIE(-4); + /* Skip leading whitespace of the status message */ ret->message += strspn (ret->message, " \t"); @@ -254,16 +406,13 @@ static int response_read (rrdc_response_t **ret_response) /* {{{ */ { if (ret->status < 0) rrd_set_error("rrdcached: %s", ret->message); - *ret_response = ret; - return (0); + goto out; } ret->lines = (char **) malloc (sizeof (char *) * ret->status); if (ret->lines == NULL) - { - response_free (ret); - return (-5); - } + DIE(-5); + memset (ret->lines, 0, sizeof (char *) * ret->status); ret->lines_num = (size_t) ret->status; @@ -271,22 +420,27 @@ static int response_read (rrdc_response_t **ret_response) /* {{{ */ { buffer_ptr = fgets (buffer, sizeof (buffer), sh); if (buffer_ptr == NULL) - { - response_free (ret); - return (-6); - } + DIE(-6); + chomp (buffer); ret->lines[i] = strdup (buffer); if (ret->lines[i] == NULL) - { - response_free (ret); - return (-7); - } + DIE(-7); } +out: *ret_response = ret; - return (0); + fflush(sh); + return (status); + +err_out: + response_free(ret); + close_connection(); + return (status); + +#undef DIE + } /* }}} rrdc_response_t *response_read */ static int request (const char *buffer, size_t buffer_size, /* {{{ */ @@ -436,8 +590,8 @@ static int rrdc_connect_network (const char *addr_orig) /* {{{ */ rrd_set_error("garbage after address: %s", port); return (-1); } - } /* if (*addr = ']') */ - else if (strchr (addr, '.') != NULL) /* Hostname or IPv4 */ + } /* if (*addr == '[') */ + else { port = rindex(addr, ':'); if (port != NULL) @@ -671,6 +825,529 @@ int rrdc_flush (const char *filename) /* {{{ */ return (status); } /* }}} int rrdc_flush */ +rrd_info_t * rrdc_info (const char *filename) /* {{{ */ +{ + char buffer[4096]; + char *buffer_ptr; + size_t buffer_free; + size_t buffer_size; + rrdc_response_t *res; + int status; + char file_path[PATH_MAX]; + rrd_info_t *data = NULL, *cd; + rrd_infoval_t info; + unsigned int l; + rrd_info_type_t itype; + char *k, *s; + + if (filename == NULL) { + rrd_set_error ("rrdc_info: no filename"); + return (NULL); + } + + memset (buffer, 0, sizeof (buffer)); + buffer_ptr = &buffer[0]; + buffer_free = sizeof (buffer); + + status = buffer_add_string ("info", &buffer_ptr, &buffer_free); + if (status != 0) { + rrd_set_error ("rrdc_info: out of memory"); + return (NULL); + } + + pthread_mutex_lock (&lock); + filename = get_path (filename, file_path); + if (filename == NULL) + { + pthread_mutex_unlock (&lock); + return (NULL); + } + + status = buffer_add_string (filename, &buffer_ptr, &buffer_free); + if (status != 0) + { + pthread_mutex_unlock (&lock); + rrd_set_error ("rrdc_info: out of memory"); + return (NULL); + } + + assert (buffer_free < sizeof (buffer)); + buffer_size = sizeof (buffer) - buffer_free; + assert (buffer[buffer_size - 1] == ' '); + buffer[buffer_size - 1] = '\n'; + + res = NULL; + status = request (buffer, buffer_size, &res); + pthread_mutex_unlock (&lock); + + if (status != 0) { + rrd_set_error ("rrdcached: %s", res->message); + return (NULL); + } + data = cd = NULL; + for( l=0 ; l < res->lines_num ; l++ ) { + /* first extract the keyword */ + for(k = s = res->lines[l];s && *s;s++) { + if(*s == ' ') { *s = 0; s++; break; } + } + if(!s || !*s) break; + itype = atoi(s); /* extract type code */ + for(;*s;s++) { if(*s == ' ') { *s = 0; s++; break; } } + if(!*s) break; + /* finally, we're pointing to the value */ + switch(itype) { + case RD_I_VAL: + if(*s == 'N') { info.u_val = DNAN; } else { info.u_val = atof(s); } + break; + case RD_I_CNT: + info.u_cnt = atol(s); + break; + case RD_I_INT: + info.u_int = atoi(s); + break; + case RD_I_STR: + chomp(s); + info.u_str = (char*)malloc(sizeof(char) * (strlen(s) + 1)); + strcpy(info.u_str,s); + break; + case RD_I_BLO: + rrd_set_error ("rrdc_info: BLOB objects are not supported"); + return (NULL); + default: + rrd_set_error ("rrdc_info: Unsupported info type %d",itype); + return (NULL); + } + + cd = rrd_info_push(cd, sprintf_alloc("%s",k), itype, info); + if(!data) data = cd; + } + response_free (res); + + return (data); +} /* }}} int rrdc_info */ + +time_t rrdc_last (const char *filename) /* {{{ */ +{ + char buffer[4096]; + char *buffer_ptr; + size_t buffer_free; + size_t buffer_size; + rrdc_response_t *res; + int status; + char file_path[PATH_MAX]; + time_t lastup; + + if (filename == NULL) { + rrd_set_error ("rrdc_last: no filename"); + return (-1); + } + + memset (buffer, 0, sizeof (buffer)); + buffer_ptr = &buffer[0]; + buffer_free = sizeof (buffer); + + status = buffer_add_string ("last", &buffer_ptr, &buffer_free); + if (status != 0) { + rrd_set_error ("rrdc_last: out of memory"); + return (-1); + } + + pthread_mutex_lock (&lock); + filename = get_path (filename, file_path); + if (filename == NULL) + { + pthread_mutex_unlock (&lock); + return (-1); + } + + status = buffer_add_string (filename, &buffer_ptr, &buffer_free); + if (status != 0) + { + pthread_mutex_unlock (&lock); + rrd_set_error ("rrdc_last: out of memory"); + return (-1); + } + + assert (buffer_free < sizeof (buffer)); + buffer_size = sizeof (buffer) - buffer_free; + assert (buffer[buffer_size - 1] == ' '); + buffer[buffer_size - 1] = '\n'; + + res = NULL; + status = request (buffer, buffer_size, &res); + pthread_mutex_unlock (&lock); + + if (status != 0) { + rrd_set_error ("rrdcached: %s", res->message); + return (-1); + } + lastup = atol(res->message); + response_free (res); + + return (lastup); +} /* }}} int rrdc_last */ + +time_t rrdc_first (const char *filename, int rraindex) /* {{{ */ +{ + char buffer[4096]; + char *buffer_ptr; + size_t buffer_free; + size_t buffer_size; + rrdc_response_t *res; + int status; + char file_path[PATH_MAX]; + time_t firstup; + + if (filename == NULL) { + rrd_set_error ("rrdc_first: no filename specified"); + return (-1); + } + + memset (buffer, 0, sizeof (buffer)); + buffer_ptr = &buffer[0]; + buffer_free = sizeof (buffer); + + status = buffer_add_string ("first", &buffer_ptr, &buffer_free); + if (status != 0) { + rrd_set_error ("rrdc_first: out of memory"); + return (-1); + } + + pthread_mutex_lock (&lock); + filename = get_path (filename, file_path); + if (filename == NULL) + { + pthread_mutex_unlock (&lock); + return (-1); + } + + status = buffer_add_string (filename, &buffer_ptr, &buffer_free); + if (status != 0) + { + pthread_mutex_unlock (&lock); + rrd_set_error ("rrdc_first: out of memory"); + return (-1); + } + status = buffer_add_ulong (rraindex, &buffer_ptr, &buffer_free); + if (status != 0) + { + pthread_mutex_unlock (&lock); + rrd_set_error ("rrdc_first: out of memory"); + return (-1); + } + + assert (buffer_free < sizeof (buffer)); + buffer_size = sizeof (buffer) - buffer_free; + assert (buffer[buffer_size - 1] == ' '); + buffer[buffer_size - 1] = '\n'; + + res = NULL; + status = request (buffer, buffer_size, &res); + pthread_mutex_unlock (&lock); + + if (status != 0) { + rrd_set_error ("rrdcached: %s", res->message); + return (-1); + } + firstup = atol(res->message); + response_free (res); + + return (firstup); +} /* }}} int rrdc_first */ + +int rrdc_create (const char *filename, /* {{{ */ + unsigned long pdp_step, + time_t last_up, + int no_overwrite, + int argc, + const char **argv) +{ + char buffer[4096]; + char *buffer_ptr; + size_t buffer_free; + size_t buffer_size; + rrdc_response_t *res; + int status; + char file_path[PATH_MAX]; + int i; + + if (filename == NULL) { + rrd_set_error ("rrdc_create: no filename specified"); + return (-1); + } + + memset (buffer, 0, sizeof (buffer)); + buffer_ptr = &buffer[0]; + buffer_free = sizeof (buffer); + + status = buffer_add_string ("create", &buffer_ptr, &buffer_free); + if (status != 0) { + rrd_set_error ("rrdc_create: out of memory"); + return (-1); + } + + pthread_mutex_lock (&lock); + filename = get_path (filename, file_path); + if (filename == NULL) + { + pthread_mutex_unlock (&lock); + return (-1); + } + + status = buffer_add_string (filename, &buffer_ptr, &buffer_free); + status = buffer_add_string ("-b", &buffer_ptr, &buffer_free); + status = buffer_add_ulong (last_up, &buffer_ptr, &buffer_free); + status = buffer_add_string ("-s", &buffer_ptr, &buffer_free); + status = buffer_add_ulong (pdp_step, &buffer_ptr, &buffer_free); + if(no_overwrite) { + status = buffer_add_string ("-O", &buffer_ptr, &buffer_free); + } + if (status != 0) + { + pthread_mutex_unlock (&lock); + rrd_set_error ("rrdc_create: out of memory"); + return (-1); + } + + for( i=0; imessage); + return (-1); + } + response_free (res); + return(0); +} /* }}} int rrdc_create */ + +int rrdc_fetch (const char *filename, /* {{{ */ + const char *cf, + time_t *ret_start, time_t *ret_end, + unsigned long *ret_step, + unsigned long *ret_ds_num, + char ***ret_ds_names, + rrd_value_t **ret_data) +{ + char buffer[4096]; + char *buffer_ptr; + size_t buffer_free; + size_t buffer_size; + rrdc_response_t *res; + char path_buffer[PATH_MAX]; + const char *path_ptr; + + char *str_tmp; + unsigned long flush_version; + + time_t start; + time_t end; + unsigned long step; + unsigned long ds_num; + char **ds_names; + + rrd_value_t *data; + size_t data_size; + size_t data_fill; + + int status; + size_t current_line; + time_t t; + + if ((filename == NULL) || (cf == NULL)) + return (-1); + + /* Send request {{{ */ + memset (buffer, 0, sizeof (buffer)); + buffer_ptr = &buffer[0]; + buffer_free = sizeof (buffer); + + status = buffer_add_string ("FETCH", &buffer_ptr, &buffer_free); + if (status != 0) + return (ENOBUFS); + + /* change to path for rrdcached */ + path_ptr = get_path (filename, path_buffer); + if (path_ptr == NULL) + return (EINVAL); + + status = buffer_add_string (path_ptr, &buffer_ptr, &buffer_free); + if (status != 0) + return (ENOBUFS); + + status = buffer_add_string (cf, &buffer_ptr, &buffer_free); + if (status != 0) + return (ENOBUFS); + + if ((ret_start != NULL) && (*ret_start > 0)) + { + char tmp[64]; + snprintf (tmp, sizeof (tmp), "%lu", (unsigned long) *ret_start); + tmp[sizeof (tmp) - 1] = 0; + status = buffer_add_string (tmp, &buffer_ptr, &buffer_free); + if (status != 0) + return (ENOBUFS); + + if ((ret_end != NULL) && (*ret_end > 0)) + { + snprintf (tmp, sizeof (tmp), "%lu", (unsigned long) *ret_end); + tmp[sizeof (tmp) - 1] = 0; + status = buffer_add_string (tmp, &buffer_ptr, &buffer_free); + if (status != 0) + return (ENOBUFS); + } + } + + assert (buffer_free < sizeof (buffer)); + buffer_size = sizeof (buffer) - buffer_free; + assert (buffer[buffer_size - 1] == ' '); + buffer[buffer_size - 1] = '\n'; + + res = NULL; + status = request (buffer, buffer_size, &res); + if (status != 0) + return (status); + + status = res->status; + if (status < 0) + { + rrd_set_error ("rrdcached: %s", res->message); + response_free (res); + return (status); + } + /* }}} Send request */ + + ds_names = NULL; + ds_num = 0; + data = NULL; + current_line = 0; + + /* Macros to make error handling a little easier (i. e. less to type and + * read. `BAIL_OUT' sets the error message, frees all dynamically allocated + * variables and returns the provided status code. */ +#define BAIL_OUT(status, ...) do { \ + rrd_set_error ("rrdc_fetch: " __VA_ARGS__); \ + free (data); \ + if (ds_names != 0) { size_t k; for (k = 0; k < ds_num; k++) free (ds_names[k]); } \ + free (ds_names); \ + response_free (res); \ + return (status); \ + } while (0) + +#define READ_NUMERIC_FIELD(name,type,var) do { \ + char *key; \ + unsigned long value; \ + assert (current_line < res->lines_num); \ + status = parse_ulong_header (res->lines[current_line], &key, &value); \ + if (status != 0) \ + BAIL_OUT (-1, "Unable to parse header `%s'", name); \ + if (strcasecmp (key, name) != 0) \ + BAIL_OUT (-1, "Unexpected header line: Expected `%s', got `%s'", name, key); \ + var = (type) value; \ + current_line++; \ + } while (0) + + if (res->lines_num < 1) + BAIL_OUT (-1, "Premature end of response packet"); + + /* We're making some very strong assumptions about the fields below. We + * therefore check the version of the `flush' command first, so that later + * versions can change the order of fields and it's easier to implement + * backwards compatibility. */ + READ_NUMERIC_FIELD ("FlushVersion", unsigned long, flush_version); + if (flush_version != 1) + BAIL_OUT (-1, "Don't know how to handle flush format version %lu.", + flush_version); + + if (res->lines_num < 5) + BAIL_OUT (-1, "Premature end of response packet"); + + READ_NUMERIC_FIELD ("Start", time_t, start); + READ_NUMERIC_FIELD ("End", time_t, end); + if (start >= end) + BAIL_OUT (-1, "Malformed start and end times: start = %lu; end = %lu;", + (unsigned long) start, + (unsigned long) end); + + READ_NUMERIC_FIELD ("Step", unsigned long, step); + if (step < 1) + BAIL_OUT (-1, "Invalid number for Step: %lu", step); + + READ_NUMERIC_FIELD ("DSCount", unsigned long, ds_num); + if (ds_num < 1) + BAIL_OUT (-1, "Invalid number for DSCount: %lu", ds_num); + + /* It's time to allocate some memory */ + ds_names = calloc ((size_t) ds_num, sizeof (*ds_names)); + if (ds_names == NULL) + BAIL_OUT (-1, "Out of memory"); + + status = parse_char_array_header (res->lines[current_line], + &str_tmp, ds_names, (size_t) ds_num, /* alloc = */ 1); + if (status != 0) + BAIL_OUT (-1, "Unable to parse header `DSName'"); + if (strcasecmp ("DSName", str_tmp) != 0) + BAIL_OUT (-1, "Unexpected header line: Expected `DSName', got `%s'", str_tmp); + current_line++; + + data_size = ds_num * (end - start) / step; + if (data_size < 1) + BAIL_OUT (-1, "No data returned or headers invalid."); + + if (res->lines_num != (6 + (data_size / ds_num))) + BAIL_OUT (-1, "Got %zu lines, expected %zu", + res->lines_num, (6 + (data_size / ds_num))); + + data = calloc (data_size, sizeof (*data)); + if (data == NULL) + BAIL_OUT (-1, "Out of memory"); + + + data_fill = 0; + for (t = start + step; t <= end; t += step, current_line++) + { + time_t tmp; + + assert (current_line < res->lines_num); + + status = parse_value_array_header (res->lines[current_line], + &tmp, data + data_fill, (size_t) ds_num); + if (status != 0) + BAIL_OUT (-1, "Cannot parse value line"); + + data_fill += (size_t) ds_num; + } + + *ret_start = start; + *ret_end = end; + *ret_step = step; + *ret_ds_num = ds_num; + *ret_ds_names = ds_names; + *ret_data = data; + + response_free (res); + return (0); +#undef READ_NUMERIC_FIELD +#undef BAIL_OUT +} /* }}} int rrdc_flush */ /* convenience function; if there is a daemon specified, or if we can * detect one from the environment, then flush the file. Otherwise, no-op