1 /* chrony plugin for collectd
2 (c) 2015 by Claudius M Zingerli, ZSeng
3 Internas roughly based on the ntpd plugin
9 #include <sys/socket.h>
13 #include "common.h" /* auxiliary functions */
14 #include "plugin.h" /* plugin_register_*, plugin_dispatch_values */
16 static const char *g_config_keys[] =
23 static int g_config_keys_num = STATIC_ARRAY_SIZE (g_config_keys);
25 # define CHRONY_DEFAULT_HOST "localhost"
26 # define CHRONY_DEFAULT_PORT "323"
27 # define CHRONY_DEFAULT_TIMEOUT 2
29 /* Copied from chrony/candm.h */
31 #define PROTO_VERSION_NUMBER 6
33 #define REQ_N_SOURCES 14
34 #define REQ_SOURCE_DATA 15
35 #define REQ_SOURCE_STATS 34
37 #define PKT_TYPE_CMD_REQUEST 1
38 #define PKT_TYPE_CMD_REPLY 2
41 #define RPY_N_SOURCES 2
42 #define RPY_SOURCE_DATA 3
43 #define RPY_MANUAL_TIMESTAMP 4
44 #define RPY_TRACKING 5
45 #define RPY_SOURCE_STATS 6
48 #define IPADDR_UNSPEC 0
49 #define IPADDR_INET4 1
50 #define IPADDR_INET6 2
52 #define ATTRIB_PACKED __attribute__((packed))
53 typedef struct ATTRIB_PACKED
69 STT_ACCESSALLOWED = 8,
71 STT_NOHOSTACCESS = 10,
72 STT_SOURCEALREADYKNOWN = 11,
73 STT_TOOMANYSOURCES = 12,
79 STT_BADPKTVERSION = 18,
80 STT_BADPKTLENGTH = 19,
83 typedef struct ATTRIB_PACKED
88 typedef struct ATTRIB_PACKED
92 } tChrony_Req_Source_data;
94 typedef struct ATTRIB_PACKED
98 } tChrony_Req_Source_stats;
100 #define IPV6_STR_MAX_SIZE 40
101 typedef struct ATTRIB_PACKED
111 typedef struct ATTRIB_PACKED
119 uint16_t dummy; /* FIXME: Strange dummy space. Needed on gcc 4.8.3 on x86_64 */
120 uint16_t f_reachability;
122 uint32_t f_since_sample;
123 Float f_origin_latest_meas;
125 Float f_latest_meas_err;
126 } tChrony_Resp_Source_data;
128 typedef struct ATTRIB_PACKED
132 uint16_t dummy; /* FIXME: Strange dummy space. Needed on gcc 4.8.3 on x86_64 */
133 uint32_t f_n_samples; //Number of measurements done
134 uint32_t f_n_runs; //How many measurements to come
135 uint32_t f_span_seconds; //For how long we're measuring
136 Float f_rtc_seconds_fast;
137 Float f_rtc_gain_rate_ppm; //Estimated relative frequency error
138 Float f_skew_ppm; //Clock skew
139 Float f_est_offset; //Estimated offset of source
140 Float f_est_offset_err; //Error of estimation
141 } tChrony_Resp_Source_stats;
144 typedef struct ATTRIB_PACKED
158 } header; /* Packed: 20Bytes */
161 tChrony_N_Sources n_sources; /* Packed: 4 Bytes */
162 tChrony_Req_Source_data source_data;
163 tChrony_Req_Source_stats source_stats;
165 uint8_t padding[4+16]; /* Padding to match minimal response size */
168 typedef struct ATTRIB_PACKED
185 } header; /* Packed: 28 Bytes */
191 tChrony_N_Sources n_sources;
192 tChrony_Resp_Source_data source_data;
193 tChrony_Resp_Source_stats source_stats;
196 uint8_t padding[1024];
199 static int g_is_connected = 0;
200 static int g_chrony_socket = -1;
201 static time_t g_chrony_timeout = 0;
202 static char *g_chrony_host = NULL;
203 static char *g_chrony_port = NULL;
204 static uint32_t g_chrony_seq = 0;
206 /*****************************************************************************/
207 /* Internal functions */
208 /*****************************************************************************/
209 /* Code from: http://long.ccaba.upc.edu/long/045Guidelines/eva/ipv6.html#daytimeClient6 */
212 connect_client (const char *hostname,
217 struct addrinfo hints, *res, *ressave;
220 memset(&hints, 0, sizeof(struct addrinfo));
222 hints.ai_family = family;
223 hints.ai_socktype = socktype;
225 n = getaddrinfo(hostname, service, &hints, &res);
229 ERROR ("chrony plugin: getaddrinfo error:: [%s]", gai_strerror(n));
238 sockfd = socket(res->ai_family,
244 if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
255 freeaddrinfo(ressave);
258 /*Code originally from: https://github.com/mlichvar/chrony/blob/master/util.c */
259 /*char * UTI_IPToString(IPAddr *addr)*/
260 char * niptoha(const tChrony_IPAddr *addr,char *p_buf, size_t p_buf_size)
262 unsigned long a, b, c, d, ip;
265 switch (ntohs(addr->f_family))
268 snprintf(p_buf, p_buf_size, "[UNSPEC]");
271 ip = ntohl(addr->addr.ip4);
276 snprintf(p_buf, p_buf_size, "%ld.%ld.%ld.%ld", a, b, c, d);
279 ip6 = addr->addr.ip6;
281 /* FIXME: Detect little endian systems */
282 snprintf(p_buf, p_buf_size, "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
283 ip6[0], ip6[1], ip6[2], ip6[3], ip6[4], ip6[5], ip6[6], ip6[7],
284 ip6[8], ip6[9], ip6[10], ip6[11], ip6[12], ip6[13], ip6[14], ip6[15]);
286 snprintf(p_buf, p_buf_size, "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
287 ip6[5], ip6[4], ip6[3], ip6[2], ip6[1], ip6[0], ip6[9], ip6[8],
288 ip6[7], ip6[6], ip6[5], ip6[4], ip6[3], ip6[2], ip6[1], ip6[0]);
292 snprintf(p_buf, p_buf_size, "[UNKNOWN]");
298 static int chrony_set_timeout()
301 tv.tv_sec = g_chrony_timeout;
304 assert(g_chrony_socket>=0);
305 if (setsockopt(g_chrony_socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv,sizeof(struct timeval)) < 0)
312 static int chrony_connect()
314 if (g_chrony_host == NULL)
316 g_chrony_host = strdup(CHRONY_DEFAULT_HOST);
318 if (g_chrony_port == NULL)
320 g_chrony_port = strdup(CHRONY_DEFAULT_PORT);
322 if (g_chrony_timeout <= 0)
324 g_chrony_timeout = CHRONY_DEFAULT_TIMEOUT;
328 DEBUG("chrony plugin: Connecting to %s:%s", g_chrony_host, g_chrony_port);
329 int socket = connect_client(g_chrony_host, g_chrony_port, AF_UNSPEC, SOCK_DGRAM);
332 ERROR ("chrony plugin: Error connecting to daemon. Errno = %d", errno);
335 DEBUG("chrony plugin: Connected");
336 g_chrony_socket = socket;
338 if (chrony_set_timeout())
340 ERROR ("chrony plugin: Error setting timeout to %lds. Errno = %d", g_chrony_timeout, errno);
346 static int chrony_send_request(const tChrony_Request *p_req, size_t p_req_size)
348 if (send(g_chrony_socket,p_req,p_req_size,0) < 0)
350 ERROR ("chrony plugin: Error sending packet. Errno = %d", errno);
357 static int chrony_recv_response(tChrony_Response *p_resp, size_t p_resp_max_size, size_t *p_resp_size)
359 ssize_t rc = recv(g_chrony_socket,p_resp,p_resp_max_size,0);
362 ERROR ("chrony plugin: Error receiving packet. Errno = %d", errno);
366 if (rc < p_resp_max_size)
368 ERROR ("chrony plugin: Received too small response packet. (Should: %ld, was: %ld)", p_resp_max_size, rc);
377 static int chrony_query(int p_command, tChrony_Request *p_req, tChrony_Response *p_resp, size_t *p_resp_size)
379 /* Check connection. We simply perform one try as collectd already handles retries */
383 if (g_is_connected == 0)
385 if (chrony_connect() == 0)
389 ERROR ("chrony plugin: Unable to connect. Errno = %d", errno);
397 int valid_command = 0;
398 size_t req_size = sizeof(p_req->header) + sizeof(p_req->padding);
399 size_t resp_size = sizeof(p_resp->header);
400 uint16_t resp_code = RPY_NULL;
404 req_size += sizeof(p_req->body.n_sources);
405 resp_size += sizeof(p_resp->body.n_sources);
406 resp_code = RPY_N_SOURCES;
409 case REQ_SOURCE_DATA:
410 req_size += sizeof(p_req->body.source_data);
411 resp_size += sizeof(p_resp->body.source_data);
412 resp_code = RPY_SOURCE_DATA;
415 case REQ_SOURCE_STATS:
416 req_size += sizeof(p_req->body.source_stats);
417 resp_size += sizeof(p_resp->body.source_stats);
418 resp_code = RPY_SOURCE_STATS;
422 ERROR ("chrony plugin: Unknown request command (Was: %d)", p_command);
426 if (valid_command == 0)
431 p_req->header.f_cmd = htons(p_command);
432 p_req->header.f_cmd_try = 0;
433 p_req->header.f_seq = htonl(g_chrony_seq++);
435 DEBUG("chrony plugin: Sending request");
436 if (chrony_send_request(p_req,req_size) != 0)
441 DEBUG("chrony plugin: Waiting for response");
442 if (chrony_recv_response(p_resp,resp_size,p_resp_size) != 0)
446 DEBUG("chrony plugin: Received response: .version = %u, .type = %u, .cmd = %u, .reply = %u, .status = %u, .seq = %u",p_resp->header.f_version,p_resp->header.f_type,ntohs(p_resp->header.f_cmd),ntohs(p_resp->header.f_reply),ntohs(p_resp->header.f_status),ntohl(p_resp->header.f_seq));
448 if (p_resp->header.f_version != p_req->header.f_version)
450 ERROR("chrony plugin: Wrong protocol version (Was: %d, expected: %d)", p_resp->header.f_version, p_req->header.f_version);
453 if (p_resp->header.f_type != PKT_TYPE_CMD_REPLY)
455 ERROR("chrony plugin: Wrong packet type (Was: %d, expected: %d)", p_resp->header.f_type, PKT_TYPE_CMD_REPLY);
458 if (p_resp->header.f_seq != p_req->header.f_seq)
460 /* FIXME: Implement sequence number handling */
461 ERROR("chrony plugin: Unexpected sequence number (Was: %d, expected: %d)", p_resp->header.f_seq, p_req->header.f_seq);
464 if (p_resp->header.f_cmd != p_req->header.f_cmd)
466 ERROR("chrony plugin: Wrong reply command (Was: %d, expected: %d)", p_resp->header.f_cmd, p_req->header.f_cmd);
470 if (ntohs(p_resp->header.f_reply) != resp_code)
472 ERROR("chrony plugin: Wrong reply code (Was: %d, expected: %d)", ntohs(p_resp->header.f_reply), p_command);
476 switch (p_resp->header.f_status)
479 DEBUG("chrony plugin: Reply packet status STT_SUCCESS");
482 ERROR("chrony plugin: Reply packet contains error status: %d (expected: %d)", p_resp->header.f_status, STT_SUCCESS);
491 static void chrony_init_req(tChrony_Request *p_req)
493 DEBUG("chrony plugin: Clearing %ld bytes",sizeof(*p_req));
494 memset(p_req,0,sizeof(*p_req));
495 p_req->header.f_version = PROTO_VERSION_NUMBER;
496 p_req->header.f_type = PKT_TYPE_CMD_REQUEST;
497 p_req->header.f_dummy0 = 0;
498 p_req->header.f_dummy1 = 0;
499 p_req->header.f_dummy2 = 0;
500 p_req->header.f_dummy3 = 0;
503 /* Code from: https://github.com/mlichvar/chrony/blob/master/util.c (GPLv2) */
505 #define FLOAT_EXP_BITS 7
506 #define FLOAT_EXP_MIN (-(1 << (FLOAT_EXP_BITS - 1)))
507 #define FLOAT_EXP_MAX (-FLOAT_EXP_MIN - 1)
508 #define FLOAT_COEF_BITS ((int)sizeof (int32_t) * 8 - FLOAT_EXP_BITS)
509 #define FLOAT_COEF_MIN (-(1 << (FLOAT_COEF_BITS - 1)))
510 #define FLOAT_COEF_MAX (-FLOAT_COEF_MIN - 1)
512 /* double UTI_FloatNetworkToHost(Float f) */
513 double ntohf(Float f)
515 int32_t exp, coef, x;
518 exp = (x >> FLOAT_COEF_BITS) - FLOAT_COEF_BITS;
519 coef = x << FLOAT_EXP_BITS >> FLOAT_EXP_BITS;
520 return coef * pow(2.0, exp);
524 /* Code from: collectd/src/ntpd.c (MIT) */
526 static void chrony_push_data(char *type, char *type_inst, double value)
529 value_list_t vl = VALUE_LIST_INIT;
531 values[0].gauge = value;
535 sstrncpy (vl.host, hostname_g, sizeof (vl.host));
536 sstrncpy (vl.plugin, "chrony", sizeof (vl.plugin));
537 sstrncpy (vl.plugin_instance, "", sizeof (vl.plugin_instance));
538 sstrncpy (vl.type, type, sizeof (vl.type));
539 sstrncpy (vl.type_instance, type_inst, sizeof (vl.type_instance));
541 plugin_dispatch_values (&vl);
545 /*****************************************************************************/
546 /* Exported functions */
547 /*****************************************************************************/
548 static int chrony_config(const char *p_key, const char *p_value)
552 /* Parse config variables */
553 if (strcasecmp(p_key, "Host") == 0)
555 if (g_chrony_host != NULL)
557 free (g_chrony_host);
559 if ((g_chrony_host = strdup (p_value)) == NULL)
561 ERROR ("chrony plugin: Error duplicating host name");
564 } else if (strcasecmp(p_key, "Port") == 0)
566 if (g_chrony_port != NULL)
568 free (g_chrony_port);
570 if ((g_chrony_port = strdup (p_value)) == NULL)
572 ERROR ("chrony plugin: Error duplicating port name");
575 } else if (strcasecmp(p_key, "Timeout") == 0)
577 time_t tosec = strtol(p_value,NULL,0);
578 g_chrony_timeout = tosec;
580 WARNING("chrony plugin: Unknown configuration variable: %s %s",p_key,p_value);
586 static int chrony_read (void)
589 char ip_addr_str0[ IPV6_STR_MAX_SIZE];
590 char ip_addr_str1[IPV6_STR_MAX_SIZE];
592 size_t chrony_resp_size;
593 tChrony_Request chrony_req,chrony_req1;
594 tChrony_Response chrony_resp,chrony_resp1;
596 DEBUG("chrony plugin: Requesting data");
597 chrony_init_req(&chrony_req);
598 status = chrony_query (REQ_N_SOURCES, &chrony_req, &chrony_resp, &chrony_resp_size);
601 ERROR ("chrony plugin: chrony_query (REQ_N_SOURCES) failed with status %i", status);
605 int n_sources = ntohl(chrony_resp.body.n_sources.f_n_sources);
606 DEBUG("chrony plugin: Getting data of %d clock sources", n_sources);
608 for (now_src = 0; now_src < n_sources; ++now_src)
610 chrony_init_req(&chrony_req);
611 chrony_init_req(&chrony_req1);
612 chrony_req.body.source_data.f_index = htonl(now_src);
613 chrony_req1.body.source_stats.f_index = htonl(now_src);
614 status = chrony_query(REQ_SOURCE_DATA, &chrony_req, &chrony_resp, &chrony_resp_size);
617 ERROR ("chrony plugin: chrony_query (REQ_SOURCE_DATA) failed with status %i", status);
620 status = chrony_query(REQ_SOURCE_STATS, &chrony_req1, &chrony_resp1, &chrony_resp_size);
623 ERROR ("chrony plugin: chrony_query (REQ_SOURCE_STATS) failed with status %i", status);
626 memset(ip_addr_str0,0,sizeof(ip_addr_str0));
627 niptoha(&chrony_resp.body.source_data.addr,ip_addr_str0,sizeof(ip_addr_str0));
628 DEBUG("chrony plugin: Source[%d] data: .addr = %s, .poll = %u, .stratum = %u, .state = %u, .mode = %u, .flags = %u, .reach = %u, .latest_meas_ago = %u, .orig_latest_meas = %f, .latest_meas = %f, .latest_meas_err = %f",
631 ntohs(chrony_resp.body.source_data.f_poll),
632 ntohs(chrony_resp.body.source_data.f_stratum),
633 ntohs(chrony_resp.body.source_data.f_state),
634 ntohs(chrony_resp.body.source_data.f_mode),
635 ntohs(chrony_resp.body.source_data.f_flags),
636 ntohs(chrony_resp.body.source_data.f_reachability),
637 ntohl(chrony_resp.body.source_data.f_since_sample),
638 ntohf(chrony_resp.body.source_data.f_origin_latest_meas),
639 ntohf(chrony_resp.body.source_data.f_latest_meas),
640 ntohf(chrony_resp.body.source_data.f_latest_meas_err));
642 memset(ip_addr_str1,0,sizeof(ip_addr_str1));
643 niptoha(&chrony_resp1.body.source_stats.addr,ip_addr_str1,sizeof(ip_addr_str1));
644 DEBUG("chrony plugin: Source[%d] stat: .addr = %s, .ref_id= %u, .n_samples = %u, .n_runs = %u, .span_seconds = %u, .rtc_seconds_fast = %f, .rtc_gain_rate_ppm = %f, .skew_ppm= %f, .est_offset = %f, .est_offset_err = %f",
647 ntohl(chrony_resp1.body.source_stats.f_ref_id),
648 ntohl(chrony_resp1.body.source_stats.f_n_samples),
649 ntohl(chrony_resp1.body.source_stats.f_n_runs),
650 ntohl(chrony_resp1.body.source_stats.f_span_seconds),
651 ntohf(chrony_resp1.body.source_stats.f_rtc_seconds_fast),
652 ntohf(chrony_resp1.body.source_stats.f_rtc_gain_rate_ppm),
653 ntohf(chrony_resp1.body.source_stats.f_skew_ppm),
654 ntohf(chrony_resp1.body.source_stats.f_est_offset),
655 ntohf(chrony_resp1.body.source_stats.f_est_offset_err));
658 chrony_push_data("clock_stratum", ip_addr_str0,ntohs(chrony_resp.body.source_data.f_stratum));
659 chrony_push_data("clock_state", ip_addr_str0,ntohs(chrony_resp.body.source_data.f_state));
660 chrony_push_data("clock_mode", ip_addr_str0,ntohs(chrony_resp.body.source_data.f_mode));
661 chrony_push_data("clock_reachability",ip_addr_str0,ntohs(chrony_resp.body.source_data.f_reachability));
662 chrony_push_data("clock_last_meas", ip_addr_str0,ntohs(chrony_resp.body.source_data.f_since_sample));
663 chrony_push_data("clock_skew_ppm", ip_addr_str1,ntohf(chrony_resp1.body.source_stats.f_skew_ppm));
664 chrony_push_data("frequency_error", ip_addr_str1,ntohf(chrony_resp1.body.source_stats.f_rtc_gain_rate_ppm)); /* unit: ppm */
665 chrony_push_data("time_offset", ip_addr_str1,ntohf(chrony_resp1.body.source_stats.f_est_offset)); /* unit: s */
671 static int chrony_shutdown()
673 if (g_is_connected != 0)
675 close(g_chrony_socket);
678 if (g_chrony_host != NULL)
680 free (g_chrony_host);
681 g_chrony_host = NULL;
683 if (g_chrony_port != NULL)
685 free (g_chrony_port);
686 g_chrony_port = NULL;
691 void module_register (void)
693 plugin_register_config ("chrony", chrony_config, g_config_keys, g_config_keys_num);
694 plugin_register_read ("chrony", chrony_read);
695 plugin_register_shutdown ("chrony", chrony_shutdown);