diff --git a/plugins/chrony/chrony_ b/plugins/chrony/chrony_ new file mode 100755 index 00000000..038a086b --- /dev/null +++ b/plugins/chrony/chrony_ @@ -0,0 +1,300 @@ +#!/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 +output. If given a hostname, it must resolve to an IP address which +appears in C 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 +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 +. + +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 ISSSUES + +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{'chrony'} || "/usr/local/bin/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. diff --git a/plugins/chrony/chrony_sourcestats b/plugins/chrony/chrony_sourcestats new file mode 100755 index 00000000..5b86813a --- /dev/null +++ b/plugins/chrony/chrony_sourcestats @@ -0,0 +1,375 @@ +#!/usr/bin/perl -w +# -*- mode: cperl; cperl-indent-level: 8; -*- + +=head1 NAME + +chrony_sourcestatss - Plugin to monitor Chrony offsets for various hosts. + +=head1 CONFIGURATION + +Plugin configuration parameters + +[chrony_*] + env.chronycpath /usr/local/bin/chronyc + + path to the chronyc executable + + env.timesources ntp1.example.org ntp2.example.com ntp1.example.net + + timesources - servers and peers that are used by the monitored + chrony instance run the plugin as with 'suggest' as argument to see + what timesoures might are available. e.g: + + munin-run --servicedir /etc/munin/plugins/ chrony_offsets suggest + + will produce a set of servers that can be used in the timesources + environment variable. + + Note: use the same names as in the server and peer directives in + your chrony configuration. + + env.freqlimit 0.7 + env.freqskewlimit 0.3 + env.offsetlimit 0.005 + env.stddevlimit 0.001 + + By default the graphs are drawn using automatic scaling with these + limits set the vertical scale of the graph will be bounded + (rigidly). Note the vallues above are (reasonable) example vallues, + not the default. + +=head1 VERSION + + VERSION 0.1.1 - 2 Nov 2020 + +=head1 AUTHOR + +Copyright (C) 2020 Olaf Kolkman + +=head1 LICENSE + +MIT + +=head1 MAGIC MARKERS +Used by munin-node-configure. + + #%# family=manual + #%# capabilities=multigraph + +=head1 KNOWN ISSSUES + +There may be some issues when IP addresses are used instead of +hostnames in the timesources environment. Also, the names should match +those in the chrony config file. + +=cut + + +use Munin::Plugin; +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(); + } +} + + + +need_multigraph(); + + +my $chronyc = $ENV{'chrony'} || "/usr/local/bin/chronyc"; +my $freqskewlimit = $ENV{'freqskewlimit'}; +my $freqlimit = $ENV{'freqlimit'}; +my $offsetlimit = $ENV{'offsetlimit'}; +my $stddevlimit = $ENV{'stddevlimit'}; + +my @timesources= split(/\s+/,$ENV{'timesources'}); + + +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") { + print "env.timesources "; + 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`){ + chop $hostname; + 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; + } + + } + } + print "\n"; + exit 0; +} + + + +$0 =~ /chrony_(.+)*$/; +my $name = $1; + + +die "You should set env.timesources (try running munin-run --servicedir /etc/munin/plugins/ chrony_offsets suggest)" unless @timesources; + +if ($ARGV[0] and $ARGV[0] eq "config") { + + print "multigraph chrony_freq\n"; + # Using Chrony Sourcestats: title to enforce an order in the presentation. + print "graph_title CHRONY Sourcestats: 1. Residual Frequency\n"; + print "graph_args --base 1000 --vertical-label PPM "; + if ($freqlimit) { + print "--lower-limit -$freqlimit --upper-limit $freqlimit --rigid\n"; + }else{ + print "\n"; + } + + print "graph_category time\n"; + foreach my $source (@timesources){ + my $fldname=clean_fieldname($source)."_"; + print "$fldname"."freq.label $source \n"; + print "$fldname"."freq.cdef $fldname"."freq,1,*\n"; + + } + + + print "multigraph chrony_freqsk\n"; + print "graph_title CHRONY Sourcestats: 2. Frequency Skew\n"; + print "graph_args --base 1000 --vertical-label PPM --lower-limit 0 "; + if ($freqskewlimit) { + print "--upper-limit $freqskewlimit --rigid\n"; + }else{ + print "\n"; + } + print "graph_category time\n"; + foreach my $source (@timesources){ + my $fldname=clean_fieldname($source)."_"; + print "$fldname"."freqsk.label $source \n"; + print "$fldname"."freqsk.cdef $fldname"."freqsk,1,*\n"; + + } + + + + print "multigraph chrony_offset\n"; + print "graph_title CHRONY Sourcestats: 3. Offset\n"; + print "graph_args --base 1000 --vertical-label seconds "; + if ($offsetlimit) { + print "--lower-limit -$offsetlimit --upper-limit $offsetlimit --rigid\n"; + }else{ + print "--lower-limit 0\n"; + } + + print "graph_category time\n"; + foreach my $source (@timesources){ + my $fldname=clean_fieldname($source)."_"; + print "$fldname"."offset.label $source \n"; + print "$fldname"."offset.cdef $fldname". "offset,1,*\n"; + + } + + print "multigraph chrony_stdev\n"; + print "graph_title CHRONY Sourcestats: 4 Estimated Standard Deviation\n"; + print "graph_args --base 1000 --vertical-label seconds --lower-limit 0 "; + if ($stddevlimit) { + print "--upper-limit $stddevlimit --rigid\n"; + }else{ + print "\n"; + } + + print "graph_category time\n"; + foreach my $source (@timesources){ + my $fldname=clean_fieldname($source)."_"; + print "$fldname"."stdev.label $source \n"; + print "$fldname"."stdev.cdef $fldname"."stdev,1,*\n"; + + } + + + + exit 0; + +} + + + +my $datastore; + +my @associations = `$chronyc -n sourcestats`; + +foreach my $line (@associations) { + if ($line =~ m/^??\s+\S+\s+\d+/) { + my ( $srcadr , undef, undef, undef, $freq, $freqsk, $offset, $stddev) = split(/\s+/, $line); + my $srcname=match_sourcename($srcadr); + @{$datastore->{$srcname}}=($freq,$freqsk,$offset,$stddev) if $srcname; + } +} + + + +sub match_sourcename { + my $srcadr=shift; + my $matched = 0; + my $sourcename=""; + if ( is_ip($srcadr) ) { + $sourcename=`$chronyc sourcename $srcadr`; + chop $sourcename; + return $sourcename if $sourcename; + }; + # return the src address and deal with it later. + return $srcadr; +} + + +print "multigraph chrony_freq\n"; +foreach (@timesources){ + if (exists $datastore->{$_}){ + my $fldname=clean_fieldname($_)."_"; + my $freq=$datastore->{$_}->[0]; + print "$fldname"."freq.value $freq \n"; + } +} + +print "\n\n"; + + +print "multigraph chrony_freqsk\n"; +foreach (@timesources){ + if (exists $datastore->{$_}){ + my $fldname=clean_fieldname($_)."_"; + my $freqsk=$datastore->{$_}->[1]; + print "$fldname"."freqsk.value $freqsk \n"; + } + +} + + +print "\n\n"; + + +print "multigraph chrony_offset\n"; +foreach (@timesources){ + if (exists $datastore->{$_}){ + my $fldname=clean_fieldname($_)."_"; + my $offset=$datastore->{$_}->[2]; + 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"; + } + print "$fldname"."offset.value $offset\n"; + } + +} + + +print "\n\n"; + + +print "multigraph chrony_stdev\n"; +foreach (@timesources){ + if (exists $datastore->{$_}){ + my $fldname=clean_fieldname($_)."_"; + my $stdev=$datastore->{$_}->[3]; + if ($stdev =~ /(.?\d+)(\S+)/){ + $stdev=$1*1e-3 if $2 eq "ms"; + $stdev=$1*1e-6 if $2 eq "us"; + $stdev=$1*1e-9 if $2 eq "ns"; + } + print "$fldname"."stdev.value $stdev\n"; + } + +} + + + + + + + + +exit 0; + + +# MIT License +# +# Copyright (c) 2020 Olaf M. Kolkman +# +# 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. diff --git a/plugins/chrony/example-graphs/chrony_sourcestat_freq.png b/plugins/chrony/example-graphs/chrony_sourcestat_freq.png new file mode 100644 index 00000000..a6a22582 Binary files /dev/null and b/plugins/chrony/example-graphs/chrony_sourcestat_freq.png differ diff --git a/plugins/chrony/example-graphs/chrony_sourcestats_freqsk.png b/plugins/chrony/example-graphs/chrony_sourcestats_freqsk.png new file mode 100644 index 00000000..cc8261ce Binary files /dev/null and b/plugins/chrony/example-graphs/chrony_sourcestats_freqsk.png differ diff --git a/plugins/chrony/example-graphs/chrony_sourcestats_offset.png b/plugins/chrony/example-graphs/chrony_sourcestats_offset.png new file mode 100644 index 00000000..2658f593 Binary files /dev/null and b/plugins/chrony/example-graphs/chrony_sourcestats_offset.png differ diff --git a/plugins/chrony/example-graphs/chrony_sourcestats_stdev.png b/plugins/chrony/example-graphs/chrony_sourcestats_stdev.png new file mode 100644 index 00000000..73ad7712 Binary files /dev/null and b/plugins/chrony/example-graphs/chrony_sourcestats_stdev.png differ diff --git a/plugins/chrony/example-graphs/chrony_time_example_com.png b/plugins/chrony/example-graphs/chrony_time_example_com.png new file mode 100644 index 00000000..02527d36 Binary files /dev/null and b/plugins/chrony/example-graphs/chrony_time_example_com.png differ