Repository
Munin (contrib)
Last change
2021-04-05
Graph Categories
Family
auto
Capabilities
Keywords
Language
Perl
License
NTP
Authors

chrony_

Example graph: day

Name

chrony_ - Wildcard plugin to monitor chrony statistics for a particular remote NTP source

Configuration

This is a wildcard plugin. The wildcard suffix in the symlink is the hostname, IPv4, or IPv6 address of the NTP sources that you want to monitor. The IP address must be one which appears in chronyc sources output. If given a hostname, it must resolve to an IP address which appears in chronyc -n sources output; this plugin will try all of the A or AAAA records returned. If you use a dynamic association method, such as “pool” or one of the broadcast or multicast methods, this plugin will probably not work very well for you, as your NTP sources could be changing frequently.

Examples:

  • chrony_time.example.com
  • chrony_203.0.113.1
  • chrony_2001:db8::1

Plugin configuration parameters

# path to the crhonyc executable when it is not in the default path env.chronycpath /usr/local/bin/chronyc

# graphlimit sets the horizontal axis for seconds and PPM the # displayed range is from -graphlimit to +graphlimit env.graphlimit 1.0

Author

Based on the similar ntp_ wildcard plugin of which the original author unknown and which was rewritten by Kenyon Ralph kenyon@kenyonralph.com.

Adopted for chronyc and modified by Olaf Kolkman. (https://github.com/Kolkman)

License

GPL2

Version

VERSION 0.1.1 - 2 Nov 2020

Magic Markers

Used by munin-node-configure.

#%# family=auto
#%# capabilities=autoconf suggest

Known Issues

The plugin will only work with ‘external’ sources. It will not recognize the names of internal refclocks.

#!/usr/bin/perl -w
# -*- mode: cperl; cperl-indent-level: 8; -*-

=head1 NAME

chrony_ - Wildcard plugin to monitor chrony statistics for a
particular remote NTP source

=head1 CONFIGURATION

This is a wildcard plugin. The wildcard suffix in the symlink is the
hostname, IPv4, or IPv6 address of the NTP sources that you want to
monitor. The IP address must be one which appears in C<chronyc sources>
output. If given a hostname, it must resolve to an IP address which
appears in C<chronyc -n sources> output; this plugin will try all of the A or
AAAA records returned. If you use a dynamic association method, such
as "pool" or one of the broadcast or multicast methods, this plugin
will probably not work very well for you, as your NTP sources could be
changing frequently.

Examples:

=over

=item chrony_time.example.com

=item chrony_203.0.113.1

=item chrony_2001:db8::1

=back

Plugin configuration parameters

# path to the crhonyc executable when it is not in the default path
env.chronycpath /usr/local/bin/chronyc

# graphlimit sets the horizontal axis for seconds and PPM the
# displayed range is from -graphlimit to +graphlimit
env.graphlimit 1.0

=head1 AUTHOR

Based on the similar ntp_ wildcard plugin of which the original author
unknown and which was rewritten by Kenyon Ralph
<kenyon@kenyonralph.com>.

Adopted for chronyc and modified by Olaf Kolkman. 
(https://github.com/Kolkman)

=head1 LICENSE

GPL2

=head1 VERSION

 VERSION 0.1.1 - 2 Nov 2020


=head1 MAGIC MARKERS
Used by munin-node-configure.

 #%# family=auto
 #%# capabilities=autoconf suggest


=head1 KNOWN ISSUES

The plugin will only work with 'external' sources. It will not recognize the names of internal refclocks.


=cut

use English qw( -no_match_vars );
use strict;
use warnings;


my $retNetIP;
my $retNetDNS;
my $retDataVal;
BEGIN{
	# Import the namespaces for symbols used globally below
	if (! eval "require Net::IP;") {
		$retNetIP = "Net::IP";
	}else{
		Net::IP->import();
	}

	if (! eval "require Net::DNS;") {
		$retNetDNS = "Net::DNS";
	}

	if (! eval "require Data::Validate::IP;") {
		$retDataVal = "Data::Validate::IP";
	}else{
		Data::Validate::IP->import();
	}


}



my $chronyc = $ENV{'chronycpath'} || `which chronyc`;
my $graphlimit = $ENV{'graphlimit'} || 1.0;


if ($ARGV[0] and $ARGV[0] eq "autoconf") {
        `$chronyc help >/dev/null 2>/dev/null`;
        if ($CHILD_ERROR eq "0") {
		if ($retNetIP || $retNetDNS || $retDataVal){
			print "no (missing perl libraries: ";
			print $retNetIP . " " if $retNetIP;
			print $retNetDNS . " " if $retNetDNS;
			print $retDataVal . " " if $retDataVal;
			print ")\n";
		}
                if (`$chronyc -n sources | wc -l` > 0) {
                        print "yes\n";
                        exit 0;
                } else {
                        print "no (chronyc sources returned no sources)\n";
                        exit 0;
                }
        } else {
                print "no (chronyc not found)\n";
                exit 0;
        }
}

if ($ARGV[0] and $ARGV[0] eq "suggest") {
        foreach my $line (`$chronyc -n sources`) {
                if ($line =~ m/^??\s+\S+\s+\d+/) {
                        my (undef, $peer_addr , undef, undef, undef, undef, undef, undef, undef) = split(/\s+/, $line);
                        unless (( $peer_addr eq "0.0.0.0") ){
				my $hostname;
				if (is_ip($peer_addr) and  $hostname = `$chronyc sourcename $peer_addr`){
					print $hostname;
				}else{
					# Bit of a last resort, not sure if this path is ever triggered.
					my $resolver = Net::DNS::Resolver->new;
					$resolver->tcp_timeout(5);
					$resolver->udp_timeout(5);
					my $query = $resolver->search($peer_addr, "PTR");
					if ($query) {
						foreach my $rr ($query->answer) {
							if ("PTR" eq $rr->type) {
								print $hostname=$rr->ptrdname."\n";
							}
						}
					}
				}
				print $peer_addr."\n" unless $hostname;
			}

                }
        }
        exit 0;
}


$0 =~ /chrony_(.+)*$/;
my $name = $1;

die "No hostname or IP address provided" unless defined $name;

if ($ARGV[0] and $ARGV[0] eq "config") {
        print "graph_title CHRONY statistics for source $name\n";
        print "graph_args --base 1000 --vertical-label (seconds,ppm) --lower-limit -$graphlimit --upper-limit $graphlimit --rigid \n";
        print "graph_category time\n";
        print "freq.label Frequency (ppm) \n";
        print "freq.cdef freq,1,*\n";
        print "freqsk.label Freq Skew (ppm)\n";
        print "freqsk.cdef freqsk,1,*\n";
        print "offset.label Offset (sx10)\n";
        print "offset.cdef offset,10,*\n";
        print "stddev.label Std Deviation (sx100)\n";
        print "stddev.cdef stddev,100,*\n";
        exit 0;
}

my $srcadr;
my $freq;
my $freqsk;
my $offset;
my $stddev;
my @associations = `$chronyc -n sourcestats`;

foreach my $line (@associations) {
	if ($line =~ m/^??\s+\S+\s+\d+/) {
		( $srcadr , undef, undef, undef, $freq, $freqsk, $offset, $stddev) = split(/\s+/, $line);
                last if lc($srcadr) eq lc($name);
		next unless is_ip($srcadr);
		# the sourcename comes with a bonus newline
		last if (lc($name."\n") eq lc (`$chronyc sourcename $srcadr`)) 
        }
}

my $matched = 0;
my $sourcename="";

if ( is_ip($srcadr) ) {
	$sourcename=`$chronyc sourcename $srcadr`;
	chop($sourcename);
};


if (is_ip($srcadr) and (lc($srcadr) ne lc($name)) and (lc($name) ne lc ($sourcename)) ){
	my @addresses;
	my $resolver = Net::DNS::Resolver->new;
	$resolver->tcp_timeout(5);
	$resolver->udp_timeout(5);
	my $query = $resolver->search($name, "AAAA");

	if ($query) {
		foreach my $rr ($query->answer) {
			if ("AAAA" eq $rr->type) {
				push(@addresses, new Net::IP($rr->address));
			}
		}
	}

	$query = $resolver->search($name, "A");
	if ($query) {
		foreach my $rr ($query->answer) {
			if ("A" eq $rr->type) {
				push(@addresses, new Net::IP($rr->address));
			}
		}
	}

      ASSOCS: foreach my $line (@associations) {
		if ($line =~ m/^??\s+\S+\s+\d+/) {
			( $srcadr , undef, undef, undef, $freq, $freqsk, $offset, $stddev) = split(/\s+/, $line);
			next unless is_ip($srcadr);
			my $srcadr_ip = new Net::IP($srcadr);
		      ADDRS: foreach my $addr (@addresses) {

				if (defined($srcadr_ip->overlaps($addr)) and $srcadr_ip->overlaps($addr) == $IP_IDENTICAL) {
					$matched = 1;
					last ASSOCS;
				}
			}
		}
	}
}





if (lc($srcadr) ne lc($name) and  lc($name) ne lc ($sourcename) and $matched == 0) {
	die "$name is not a peer of this chronyd";
}

if ($offset =~ /(.?\d+)(\S+)/){
	$offset=$1*1e-3 if $2 eq "ms";
 	$offset=$1*1e-6 if $2 eq "us";
 	$offset=$1*1e-9 if $2 eq "ns";
}

if ($stddev =~ /(\d+)(\S+)/){
	$stddev=$1*1e-3 if $2 eq "ms";
 	$stddev=$1*1e-6 if $2 eq "us";
 	$stddev=$1*1e-9 if $2 eq "ns";
}






print <<"EOT";
freq.value $freq
freqsk.value $freqsk
offset.value $offset
stddev.value $stddev
EOT

exit 0;

# vim:syntax=perl

#  (c) 2020 Olaf Kolkman
#  (c) ???? Kenyon Ralph
#  (c) ???? ????????????
#
#  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; version 2 dated June, 1991.
#
#  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 Street, Fifth Floor, Boston, MA 02110-1301 USA.