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 static int g_is_connected = 0;
84 static int g_chrony_socket = -1;
85 static time_t g_chrony_timeout = 0;
86 static char *g_chrony_host = NULL;
87 static char *g_chrony_port = NULL;
88 static uint32_t g_chrony_seq = 0;
89 //static char ntpd_port[16];
91 typedef struct ATTRIB_PACKED
96 typedef struct ATTRIB_PACKED
100 } tChrony_Req_Source_data;
102 typedef struct ATTRIB_PACKED
105 uint8_t f_dummy0[56];
106 } tChrony_Req_Source_stats;
108 #define IPV6_STR_MAX_SIZE 40
109 typedef struct ATTRIB_PACKED
119 typedef struct ATTRIB_PACKED
127 uint16_t dummy; //FIXME: Strange dummy space. Needed on gcc 4.8.3 on x86_64
128 uint16_t f_reachability;
130 uint32_t f_since_sample;
131 Float f_origin_latest_meas;
133 Float f_latest_meas_err;
134 } tChrony_Resp_Source_data;
136 typedef struct ATTRIB_PACKED
140 uint16_t dummy; //FIXME: Strange dummy space. Needed on gcc 4.8.3 on x86_64
141 uint32_t f_n_samples; //Number of measurements done
142 uint32_t f_n_runs; //How many measurements to come
143 uint32_t f_span_seconds; //For how long we're measuring
144 Float f_rtc_seconds_fast;
145 Float f_rtc_gain_rate_ppm; //Estimated relative frequency error
146 Float f_skew_ppm; //Clock skew
147 Float f_est_offset; //Estimated offset of source
148 Float f_est_offset_err; //Error of estimation
149 } tChrony_Resp_Source_stats;
152 typedef struct ATTRIB_PACKED
166 } header; //Packed: 20Bytes
169 tChrony_N_Sources n_sources; //Packed: 8 Bytes
170 tChrony_Req_Source_data source_data;
171 tChrony_Req_Source_stats source_stats;
173 uint8_t padding[4+16]; //Padding to match minimal response size
176 typedef struct ATTRIB_PACKED
193 } header; //Packed: 24 Bytes
199 tChrony_N_Sources n_sources;
200 tChrony_Resp_Source_data source_data;
201 tChrony_Resp_Source_stats source_stats;
204 uint8_t padding[1024];
207 /*****************************************************************************/
208 /* Internal functions */
209 /*****************************************************************************/
210 /* Code from: http://long.ccaba.upc.edu/long/045Guidelines/eva/ipv6.html#daytimeClient6 */
213 connect_client (const char *hostname,
218 struct addrinfo hints, *res, *ressave;
221 memset(&hints, 0, sizeof(struct addrinfo));
223 hints.ai_family = family;
224 hints.ai_socktype = socktype;
226 n = getaddrinfo(hostname, service, &hints, &res);
230 ERROR ("chrony plugin: getaddrinfo error:: [%s]", gai_strerror(n));
239 sockfd = socket(res->ai_family,
245 if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
256 freeaddrinfo(ressave);
259 /*Code originally from: https://github.com/mlichvar/chrony/blob/master/util.c */
260 /*char * UTI_IPToString(IPAddr *addr)*/
261 char * niptoha(const tChrony_IPAddr *addr,char *p_buf, size_t p_buf_size)
263 unsigned long a, b, c, d, ip;
266 switch (ntohs(addr->f_family))
269 snprintf(p_buf, p_buf_size, "[UNSPEC]");
272 ip = ntohl(addr->addr.ip4);
277 snprintf(p_buf, p_buf_size, "%ld.%ld.%ld.%ld", a, b, c, d);
280 ip6 = addr->addr.ip6;
282 //FIXME: Detect little endian systems
283 snprintf(p_buf, p_buf_size, "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
284 ip6[0], ip6[1], ip6[2], ip6[3], ip6[4], ip6[5], ip6[6], ip6[7],
285 ip6[8], ip6[9], ip6[10], ip6[11], ip6[12], ip6[13], ip6[14], ip6[15]);
287 snprintf(p_buf, p_buf_size, "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
288 ip6[5], ip6[4], ip6[3], ip6[2], ip6[1], ip6[0], ip6[9], ip6[8],
289 ip6[7], ip6[6], ip6[5], ip6[4], ip6[3], ip6[2], ip6[1], ip6[0]);
293 snprintf(p_buf, p_buf_size, "[UNKNOWN]");
299 static int chrony_set_timeout()
302 tv.tv_sec = g_chrony_timeout;
305 assert(g_chrony_socket>=0);
306 if (setsockopt(g_chrony_socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv,sizeof(struct timeval)) < 0)
313 static int chrony_connect()
315 if (g_chrony_host == NULL)
317 g_chrony_host = strdup(CHRONY_DEFAULT_HOST);
319 if (g_chrony_port == NULL)
321 g_chrony_port = strdup(CHRONY_DEFAULT_PORT);
323 if (g_chrony_timeout <= 0)
325 g_chrony_timeout = CHRONY_DEFAULT_TIMEOUT;
329 DEBUG("chrony plugin: Connecting to %s:%s", g_chrony_host, g_chrony_port);
330 int socket = connect_client(g_chrony_host, g_chrony_port, AF_UNSPEC, SOCK_DGRAM);
333 ERROR ("chrony plugin: Error connecting to daemon. Errno = %d", errno);
336 //TODO: Set timeouts!
337 DEBUG("chrony plugin: Connected");
338 g_chrony_socket = socket;
340 if (chrony_set_timeout())
342 ERROR ("chrony plugin: Error setting timeout to %lds. Errno = %d", g_chrony_timeout, errno);
348 static int chrony_send_request(const tChrony_Request *p_req, size_t p_req_size)
350 if (send(g_chrony_socket,p_req,p_req_size,0) < 0)
352 ERROR ("chrony plugin: Error sending packet. Errno = %d", errno);
359 static int chrony_recv_response(tChrony_Response *p_resp, size_t p_resp_max_size, size_t *p_resp_size)
361 ssize_t rc = recv(g_chrony_socket,p_resp,p_resp_max_size,0);
364 ERROR ("chrony plugin: Error receiving packet. Errno = %d", errno);
368 if (rc < p_resp_max_size)
370 ERROR ("chrony plugin: Received too small response packet. (Should: %ld, was: %ld)", p_resp_max_size, rc);
379 static int chrony_query(int p_command, tChrony_Request *p_req, tChrony_Response *p_resp, size_t *p_resp_size)
381 /* Check connection. We simply perform one try as collectd already handles retries */
385 if (g_is_connected == 0)
387 if (chrony_connect() == 0)
391 ERROR ("chrony plugin: Unable to connect. Errno = %d", errno);
399 int valid_command = 0;
400 size_t req_size = sizeof(p_req->header) + sizeof(p_req->padding);
401 size_t resp_size = sizeof(p_resp->header);
402 uint16_t resp_code = RPY_NULL;
406 req_size += sizeof(p_req->body.n_sources);
407 resp_size += sizeof(p_resp->body.n_sources);
408 resp_code = RPY_N_SOURCES;
411 case REQ_SOURCE_DATA:
412 req_size += sizeof(p_req->body.source_data);
413 resp_size += sizeof(p_resp->body.source_data);
414 resp_code = RPY_SOURCE_DATA;
417 case REQ_SOURCE_STATS:
418 req_size += sizeof(p_req->body.source_stats);
419 resp_size += sizeof(p_resp->body.source_stats);
420 resp_code = RPY_SOURCE_STATS;
424 ERROR ("chrony plugin: Unknown request command (Was: %d)", p_command);
428 if (valid_command == 0)
433 p_req->header.f_cmd = htons(p_command);
434 p_req->header.f_cmd_try = 0;
435 p_req->header.f_seq = htonl(g_chrony_seq++);
437 DEBUG("chrony plugin: Sending request");
438 if (chrony_send_request(p_req,req_size) != 0)
443 DEBUG("chrony plugin: Waiting for response");
444 if (chrony_recv_response(p_resp,resp_size,p_resp_size) != 0)
448 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));
450 if (p_resp->header.f_version != p_req->header.f_version)
452 ERROR("chrony plugin: Wrong protocol version (Was: %d, expected: %d)", p_resp->header.f_version, p_req->header.f_version);
455 if (p_resp->header.f_type != PKT_TYPE_CMD_REPLY)
457 ERROR("chrony plugin: Wrong packet type (Was: %d, expected: %d)", p_resp->header.f_type, PKT_TYPE_CMD_REPLY);
460 if (p_resp->header.f_seq != p_req->header.f_seq)
462 //FIXME: Implement sequence number handling
463 ERROR("chrony plugin: Unexpected sequence number (Was: %d, expected: %d)", p_resp->header.f_seq, p_req->header.f_seq);
466 if (p_resp->header.f_cmd != p_req->header.f_cmd)
468 ERROR("chrony plugin: Wrong reply command (Was: %d, expected: %d)", p_resp->header.f_cmd, p_req->header.f_cmd);
472 if (ntohs(p_resp->header.f_reply) != resp_code)
474 ERROR("chrony plugin: Wrong reply code (Was: %d, expected: %d)", ntohs(p_resp->header.f_reply), p_command);
478 switch (p_resp->header.f_status)
481 DEBUG("chrony plugin: Reply packet status STT_SUCCESS");
484 ERROR("chrony plugin: Reply packet contains error status: %d (expected: %d)", p_resp->header.f_status, STT_SUCCESS);
493 static void chrony_init_req(tChrony_Request *p_req)
495 DEBUG("chrony plugin: Clearing %ld bytes",sizeof(*p_req));
496 memset(p_req,0,sizeof(*p_req));
497 p_req->header.f_version = PROTO_VERSION_NUMBER;
498 p_req->header.f_type = PKT_TYPE_CMD_REQUEST;
499 p_req->header.f_dummy0 = 0;
500 p_req->header.f_dummy1 = 0;
501 p_req->header.f_dummy2 = 0;
502 p_req->header.f_dummy3 = 0;
505 //Code from: https://github.com/mlichvar/chrony/blob/master/util.c (GPLv2)
507 #define FLOAT_EXP_BITS 7
508 #define FLOAT_EXP_MIN (-(1 << (FLOAT_EXP_BITS - 1)))
509 #define FLOAT_EXP_MAX (-FLOAT_EXP_MIN - 1)
510 #define FLOAT_COEF_BITS ((int)sizeof (int32_t) * 8 - FLOAT_EXP_BITS)
511 #define FLOAT_COEF_MIN (-(1 << (FLOAT_COEF_BITS - 1)))
512 #define FLOAT_COEF_MAX (-FLOAT_COEF_MIN - 1)
514 //double UTI_FloatNetworkToHost(Float f)
515 double ntohf(Float f)
517 int32_t exp, coef, x;
520 exp = (x >> FLOAT_COEF_BITS) - FLOAT_COEF_BITS;
521 coef = x << FLOAT_EXP_BITS >> FLOAT_EXP_BITS;
522 return coef * pow(2.0, exp);
526 /* Code from: collectd/src/ntpd.c (MIT) */
528 static void chrony_push_data(char *type, char *type_inst, double value)
531 value_list_t vl = VALUE_LIST_INIT;
533 values[0].gauge = value;
537 sstrncpy (vl.host, hostname_g, sizeof (vl.host));
538 sstrncpy (vl.plugin, "chrony", sizeof (vl.plugin));
539 sstrncpy (vl.plugin_instance, "", sizeof (vl.plugin_instance));
540 sstrncpy (vl.type, type, sizeof (vl.type));
541 sstrncpy (vl.type_instance, type_inst, sizeof (vl.type_instance));
543 plugin_dispatch_values (&vl);
547 /*****************************************************************************/
548 /* Exported functions */
549 /*****************************************************************************/
550 static int chrony_config(const char *p_key, const char *p_value)
554 //Parse config variables
555 if (strcasecmp(p_key, "Host") == 0)
557 if (g_chrony_host != NULL)
559 free (g_chrony_host);
561 if ((g_chrony_host = strdup (p_value)) == NULL)
563 ERROR ("chrony plugin: Error duplicating host name");
566 } else if (strcasecmp(p_key, "Port") == 0)
568 if (g_chrony_port != NULL)
570 free (g_chrony_port);
572 if ((g_chrony_port = strdup (p_value)) == NULL)
574 ERROR ("chrony plugin: Error duplicating port name");
577 } else if (strcasecmp(p_key, "Timeout") == 0)
579 time_t tosec = strtol(p_value,NULL,0);
580 g_chrony_timeout = tosec;
582 WARNING("chrony plugin: Unknown configuration variable: %s %s",p_key,p_value);
588 static int chrony_read (void)
590 //plugin_dispatch_values (&vl);
592 char ip_addr_str0[ IPV6_STR_MAX_SIZE];
593 char ip_addr_str1[IPV6_STR_MAX_SIZE];
595 size_t chrony_resp_size;
596 tChrony_Request chrony_req,chrony_req1;
597 tChrony_Response chrony_resp,chrony_resp1;
599 DEBUG("chrony plugin: Requesting data");
600 chrony_init_req(&chrony_req);
601 status = chrony_query (REQ_N_SOURCES, &chrony_req, &chrony_resp, &chrony_resp_size);
604 ERROR ("chrony plugin: chrony_query (REQ_N_SOURCES) failed with status %i", status);
608 int n_sources = ntohl(chrony_resp.body.n_sources.f_n_sources);
609 DEBUG("chrony plugin: Getting data of %d clock sources", n_sources);
611 for (now_src = 0; now_src < n_sources; ++now_src)
613 chrony_init_req(&chrony_req);
614 chrony_init_req(&chrony_req1);
615 chrony_req.body.source_data.f_index = htonl(now_src);
616 chrony_req1.body.source_stats.f_index = htonl(now_src);
617 status = chrony_query(REQ_SOURCE_DATA, &chrony_req, &chrony_resp, &chrony_resp_size);
620 ERROR ("chrony plugin: chrony_query (REQ_SOURCE_DATA) failed with status %i", status);
623 status = chrony_query(REQ_SOURCE_STATS, &chrony_req1, &chrony_resp1, &chrony_resp_size);
626 ERROR ("chrony plugin: chrony_query (REQ_SOURCE_STATS) failed with status %i", status);
629 memset(ip_addr_str0,0,sizeof(ip_addr_str0));
630 niptoha(&chrony_resp.body.source_data.addr,ip_addr_str0,sizeof(ip_addr_str0));
631 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",
634 ntohs(chrony_resp.body.source_data.f_poll),
635 ntohs(chrony_resp.body.source_data.f_stratum),
636 ntohs(chrony_resp.body.source_data.f_state),
637 ntohs(chrony_resp.body.source_data.f_mode),
638 ntohs(chrony_resp.body.source_data.f_flags),
639 ntohs(chrony_resp.body.source_data.f_reachability),
640 ntohl(chrony_resp.body.source_data.f_since_sample),
641 ntohf(chrony_resp.body.source_data.f_origin_latest_meas),
642 ntohf(chrony_resp.body.source_data.f_latest_meas),
643 ntohf(chrony_resp.body.source_data.f_latest_meas_err));
645 memset(ip_addr_str1,0,sizeof(ip_addr_str1));
646 niptoha(&chrony_resp1.body.source_stats.addr,ip_addr_str1,sizeof(ip_addr_str1));
647 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",
650 ntohl(chrony_resp1.body.source_stats.f_ref_id),
651 ntohl(chrony_resp1.body.source_stats.f_n_samples),
652 ntohl(chrony_resp1.body.source_stats.f_n_runs),
653 ntohl(chrony_resp1.body.source_stats.f_span_seconds),
654 ntohf(chrony_resp1.body.source_stats.f_rtc_seconds_fast),
655 ntohf(chrony_resp1.body.source_stats.f_rtc_gain_rate_ppm),
656 ntohf(chrony_resp1.body.source_stats.f_skew_ppm),
657 ntohf(chrony_resp1.body.source_stats.f_est_offset),
658 ntohf(chrony_resp1.body.source_stats.f_est_offset_err));
661 chrony_push_data("clock_stratum", ip_addr_str0,ntohs(chrony_resp.body.source_data.f_stratum));
662 chrony_push_data("clock_state", ip_addr_str0,ntohs(chrony_resp.body.source_data.f_state));
663 chrony_push_data("clock_mode", ip_addr_str0,ntohs(chrony_resp.body.source_data.f_mode));
664 chrony_push_data("clock_reachability",ip_addr_str0,ntohs(chrony_resp.body.source_data.f_reachability));
665 chrony_push_data("clock_last_meas", ip_addr_str0,ntohs(chrony_resp.body.source_data.f_since_sample));
666 chrony_push_data("clock_skew_ppm", ip_addr_str1,ntohf(chrony_resp1.body.source_stats.f_skew_ppm));
667 chrony_push_data("frequency_error", ip_addr_str1,ntohf(chrony_resp1.body.source_stats.f_rtc_gain_rate_ppm)); //unit: ppm
668 chrony_push_data("time_offset", ip_addr_str1,ntohf(chrony_resp1.body.source_stats.f_est_offset)); //unit: s
674 static int chrony_shutdown()
676 if (g_is_connected != 0)
678 close(g_chrony_socket);
681 if (g_chrony_host != NULL)
683 free (g_chrony_host);
684 g_chrony_host = NULL;
686 if (g_chrony_port != NULL)
688 free (g_chrony_port);
689 g_chrony_port = NULL;
694 void module_register (void)
696 plugin_register_config ("chrony", chrony_config, g_config_keys, g_config_keys_num);
697 plugin_register_read ("chrony", chrony_read);
698 plugin_register_shutdown ("chrony", chrony_shutdown);