1#! @PATH_PERL@ -w 2# 3# $Id: ntpsweep.in,v 1.1.1.1 2009/12/13 16:56:37 kardel Exp $ 4# 5# DISCLAIMER 6# 7# Copyright (C) 1999,2000 Hans Lambermont and Origin B.V. 8# 9# Permission to use, copy, modify and distribute this software and its 10# documentation for any purpose and without fee is hereby granted, 11# provided that the above copyright notice appears in all copies and 12# that both the copyright notice and this permission notice appear in 13# supporting documentation. This software is supported as is and without 14# any express or implied warranties, including, without limitation, the 15# implied warranties of merchantability and fitness for a particular 16# purpose. The name Origin B.V. must not be used to endorse or promote 17# products derived from this software without prior written permission. 18# 19# Hans Lambermont <ntpsweep@lambermont.dyndns.org> 20 21require 5.0; # But actually tested on 5.004 ;) 22use Getopt::Long; # GetOptions() 23use strict; 24 25my $version = 1.3; 26(my $program = $0) =~ s%.*/(.+?)(.pl)?$%$1%; 27 28# Hardcoded paths/program names 29my $ntpdate = "ntpdate"; 30my $ntpq = "ntpq"; 31 32# no STDOUT buffering 33$| = 1; 34 35my ($help, $single_host, $showpeers, $maxlevel, $strip, $askversion); 36my $res = GetOptions("help!" => \$help, 37 "host=s" => \$single_host, 38 "peers!" => \$showpeers, 39 "maxlevel=s" => \$maxlevel, 40 "strip=s" => \$strip, 41 "version!" => \$askversion); 42 43if ($askversion) { 44 print("$version\n"); 45 exit 0; 46} 47 48if ($help || ((@ARGV != 1) && !$single_host)) { 49 warn <<EOF; 50This is $program, version $version 51Copyright (C) 1999,2000 Hans Lambermont and Origin B.V. Disclaimer inside. 52 53Usage: 54 $program [--help|--peers|--strip <string>|--maxlevel <level>|--version] \\ 55 <file>|[--host <hostname>] 56 57Description: 58 $program prints per host given in <file> the NTP stratum level, the 59 clock offset in seconds, the daemon version, the operating system and 60 the processor. Optionally recursing through all peers. 61 62Options: 63--help 64 Print this short help text and exit. 65--version 66 Print version ($version) and exit. 67<file> 68 Specify hosts file. File format is one hostname or ip number per line. 69 Lines beginning with # are considered as comment. 70--host <hostname> 71 Speficy a single host, bypassing the need for a hosts file. 72--peers 73 Recursively list all peers a host synchronizes to. 74 An '= ' before a peer means a loop. Recursion stops here. 75--maxlevel <level> 76 Traverse peers up to this level (4 is a reasonable number). 77--strip <string> 78 Strip <string> from hostnames. 79 80Examples: 81 $program myhosts.txt --strip .foo.com 82 $program --host some.host --peers --maxlevel 4 83EOF 84 exit 1; 85} 86 87my $hostsfile = shift; 88my (@hosts, @known_hosts); 89my (%known_host_info, %known_host_peers); 90 91sub read_hosts() 92{ 93 local *HOSTS; 94 open (HOSTS, $hostsfile) || 95 die "$program: FATAL: unable to read $hostsfile: $!\n"; 96 while (<HOSTS>) { 97 next if /^\s*(#|$)/; # comment/empty 98 chomp; 99 push(@hosts, $_); 100 } 101 close(HOSTS); 102} 103 104# translate IP to hostname if possible 105sub ip2name { 106 my($ip) = @_; 107 my($addr, $name, $aliases, $addrtype, $length, @addrs); 108 $addr = pack('C4', split(/\./, $ip)); 109 ($name, $aliases, $addrtype, $length, @addrs) = gethostbyaddr($addr, 2); 110 if ($name) { 111 # return lower case name 112 return("\L$name"); 113 } else { 114 return($ip); 115 } 116} 117 118# item_in_list($item, @list): returns 1 if $item is in @list, 0 if not 119sub item_in_list { 120 my($item, @list) = @_; 121 my($i); 122 foreach $i (@list) { 123 return 1 if ($item eq $i); 124 } 125 return 0; 126} 127 128sub scan_host($;$;$) { 129 my($host, $level, @trace) = @_; 130 my $stratum = 0; 131 my $offset = 0; 132 my $daemonversion = ""; 133 my $system = ""; 134 my $processor = ""; 135 my @peers; 136 my $known_host = 0; 137 138 if (&item_in_list($host, @known_hosts)) { 139 $known_host = 1; 140 } else { 141 # ntpdate part 142 open(NTPDATE, "$ntpdate -bd $host 2>/dev/null |") || 143 die "Cannot open ntpdate pipe: $!\n"; 144 while (<NTPDATE>) { 145 /^stratum\s+(\d+).*$/ && do { 146 $stratum = $1; 147 }; 148 /^offset\s+([0-9.-]+)$/ && do { 149 $offset = $1; 150 }; 151 } 152 close(NTPDATE); 153 154 # got answers ? If so, go on. 155 if ($stratum) { 156 # ntpq part 157 my $ntpqparams = "-c 'rv 0 processor,system,daemon_version'"; 158 open(NTPQ, "$ntpq $ntpqparams $host 2>/dev/null |") || 159 die "Cannot open ntpq pipe: $!\n"; 160 while (<NTPQ>) { 161 /daemon_version="(.*)"/ && do { 162 $daemonversion = $1; 163 }; 164 /system="([^"]*)"/ && do { 165 $system = $1; 166 }; 167 /processor="([^"]*)"/ && do { 168 $processor = $1; 169 }; 170 } 171 close(NTPQ); 172 173 # Shorten daemon_version string. 174 $daemonversion =~ s/(;|Mon|Tue|Wed|Thu|Fri|Sat|Sun).*$//; 175 $daemonversion =~ s/version=//; 176 $daemonversion =~ s/(x|)ntpd //; 177 $daemonversion =~ s/(\(|\))//g; 178 $daemonversion =~ s/beta/b/; 179 $daemonversion =~ s/multicast/mc/; 180 181 # Shorten system string 182 $system =~ s/UNIX\///; 183 $system =~ s/RELEASE/r/; 184 $system =~ s/CURRENT/c/; 185 186 # Shorten processor string 187 $processor =~ s/unknown//; 188 } 189 190 # got answers ? If so, go on. 191 if ($daemonversion) { 192 # ntpq again, find out the peers this time 193 if ($showpeers) { 194 my $ntpqparams = "-pn"; 195 open(NTPQ, "$ntpq $ntpqparams $host 2>/dev/null |") || 196 die "Cannot open ntpq pipe: $!\n"; 197 while (<NTPQ>) { 198 /^No association ID's returned$/ && do { 199 last; 200 }; 201 /^ remote/ && do { 202 next; 203 }; 204 /^==/ && do { 205 next; 206 }; 207 /^( |x|\.|-|\+|#|\*|o)([^ ]+)/ && do { 208 push(@peers, ip2name($2)); 209 next; 210 }; 211 print "ERROR: $_"; 212 } 213 close(NTPQ); 214 } 215 } 216 217 # Add scanned host to known_hosts array 218 push(@known_hosts, $host); 219 if ($stratum) { 220 $known_host_info{$host} = sprintf("%2d %9.3f %-11s %-12s %s", 221 $stratum, $offset, substr($daemonversion,0,11), 222 substr($system,0,12), substr($processor,0,9)); 223 } else { 224 # Stratum level 0 is consider invalid 225 $known_host_info{$host} = sprintf(" ?"); 226 } 227 $known_host_peers{$host} = [@peers]; 228 } 229 230 if ($stratum || $known_host) { # Valid or known host 231 my $printhost = ' ' x $level . $host; 232 # Shorten host string 233 if ($strip) { 234 $printhost =~ s/$strip//; 235 } 236 # append number of peers in brackets if requested and valid 237 if ($showpeers && ($known_host_info{$host} ne " ?")) { 238 $printhost .= " (" . @{$known_host_peers{$host}} . ")"; 239 } 240 # Finally print complete host line 241 printf("%-32s %s\n", 242 substr($printhost,0,32), $known_host_info{$host}); 243 if ($showpeers && (eval($maxlevel ? $level < $maxlevel : 1))) { 244 my $peer; 245 push(@trace, $host); 246 # Loop through peers 247 foreach $peer (@{$known_host_peers{$host}}) { 248 if (&item_in_list($peer, @trace)) { 249 # we've detected a loop ! 250 $printhost = ' ' x ($level + 1) . "= " . $peer; 251 # Shorten host string 252 if ($strip) { 253 $printhost =~ s/$strip//; 254 } 255 printf("%-32s %s\n", 256 substr($printhost,0,32)); 257 } else { 258 if (substr($peer,0,3) ne "127") { 259 &scan_host($peer, $level + 1, @trace); 260 } 261 } 262 } 263 } 264 } else { # We did not get answers from this host 265 my $printhost = ' ' x $level . $host; 266 # Shorten host string 267 if ($strip) { 268 $printhost =~ s/$strip//; 269 } 270 printf("%-32s ?\n", substr($printhost,0,32)); 271 } 272} 273 274sub scan_hosts() 275{ 276 my $host; 277 for $host (@hosts) { 278 my @trace; 279 push(@trace, $host); 280 scan_host($host, 0, @trace); 281 } 282} 283 284# Main program 285 286if ($single_host) { 287 push(@hosts, $single_host); 288} else { 289 &read_hosts($hostsfile); 290} 291 292# Print header 293print <<EOF; 294Host st offset(s) version system processor 295--------------------------------+--+---------+-----------+------------+--------- 296EOF 297 298&scan_hosts(); 299 300exit 0; 301