376 lines
9.1 KiB
Perl
Executable File
376 lines
9.1 KiB
Perl
Executable File
#!/usr/bin/perl -w
|
|
# -*- mode: cperl; cperl-indent-level: 8; -*-
|
|
|
|
=head1 NAME
|
|
|
|
chrony_sourcestats - 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 values above are (reasonable) example values,
|
|
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 ISSUES
|
|
|
|
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: <no> 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.
|