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