1#!/usr/local/bin/perl
2##
3## @PACKAGE@ @VERSION@
4## Copyright (c) @COPYYEARS@ by Henry Kilmer and John Heasley
5## All rights reserved.
6##
7## This code is derived from software contributed to and maintained by
8## Henry Kilmer, John Heasley, Andrew Partan,
9## Pete Whiting, Austin Schutz, and Andrew Fort.
10##
11## Redistribution and use in source and binary forms, with or without
12## modification, are permitted provided that the following conditions
13## are met:
14## 1. Redistributions of source code must retain the above copyright
15##    notice, this list of conditions and the following disclaimer.
16## 2. Redistributions in binary form must reproduce the above copyright
17##    notice, this list of conditions and the following disclaimer in the
18##    documentation and/or other materials provided with the distribution.
19## 3. Neither the name of RANCID nor the names of its
20##    contributors may be used to endorse or promote products derived from
21##    this software without specific prior written permission.
22##
23## THIS SOFTWARE IS PROVIDED BY Henry Kilmer, John Heasley AND CONTRIBUTORS
24## ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
25## TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
26## PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COMPANY OR CONTRIBUTORS
27## BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28## CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29## SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30## INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31## CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33## POSSIBILITY OF SUCH DAMAGE.
34##
35## It is the request of the authors, but not a condition of license, that
36## parties packaging or redistributing RANCID NOT distribute altered versions
37## of the etc/rancid.types.base file nor alter how this file is processed nor
38## when in relation to etc/rancid.types.conf.  The goal of this is to help
39## suppress our support costs.  If it becomes a problem, this could become a
40## condition of license.
41#
42#  The expect login scripts were based on Erik Sherk's gwtn, by permission.
43#
44#  The original looking glass software was written by Ed Kern, provided by
45#  permission and modified beyond recognition.
46#
47# This version of rancid tries to deal with F5 BigIPs.
48#
49#  RANCID - Really Awesome New Cisco confIg Differ
50#
51# usage: f5rancid [-dltCV] [-f filename | hostname]
52#
53use Getopt::Std;
54getopts('dflt:CV');
55if ($opt_V) {
56    print "@PACKAGE@ @VERSION@\n";
57    exit(0);
58}
59$log = $opt_l;
60$debug = $opt_d;
61$file = $opt_f;
62$host = $ARGV[0];
63$clean_run = 0;
64$found_end = 0;
65$timeo = 90;				# clogin timeout in seconds
66
67# force a terminal type so as not to confuse the POS
68$ENV{'TERM'} = "vt100-w";
69
70my(@commandtable, %commands, @commands);# command lists
71my($aclsort) = ("ipsort");		# ACL sorting mode
72my($filter_commstr);			# SNMP community string filtering
73my($filter_osc);			# oscillating data filtering mode
74my($filter_pwds);			# password filtering mode
75
76# This routine is used to print out the router configuration
77sub ProcessHistory {
78    my($new_hist_tag,$new_command,$command_string,@string) = (@_);
79    if ((($new_hist_tag ne $hist_tag) || ($new_command ne $command))
80	&& scalar(%history)) {
81	print eval "$command \%history";
82	undef %history;
83    }
84    if (($new_hist_tag) && ($new_command) && ($command_string)) {
85	if ($history{$command_string}) {
86	    $history{$command_string} = "$history{$command_string}@string";
87	} else {
88	    $history{$command_string} = "@string";
89	}
90    } elsif (($new_hist_tag) && ($new_command)) {
91	$history{++$#history} = "@string";
92    } else {
93	print "@string";
94    }
95    $hist_tag = $new_hist_tag;
96    $command = $new_command;
97    1;
98}
99
100sub numerically { $a <=> $b; }
101
102# This is a sort routine that will sort numerically on the
103# keys of a hash as if it were a normal array.
104sub keynsort {
105    local(%lines) = @_;
106    local($i) = 0;
107    local(@sorted_lines);
108    foreach $key (sort numerically keys(%lines)) {
109	$sorted_lines[$i] = $lines{$key};
110	$i++;
111    }
112    @sorted_lines;
113}
114
115# This is a sort routine that will sort on the
116# keys of a hash as if it were a normal array.
117sub keysort {
118    local(%lines) = @_;
119    local($i) = 0;
120    local(@sorted_lines);
121    foreach $key (sort keys(%lines)) {
122	$sorted_lines[$i] = $lines{$key};
123	$i++;
124    }
125    @sorted_lines;
126}
127
128# This is a sort routine that will sort on the
129# values of a hash as if it were a normal array.
130sub valsort{
131    local(%lines) = @_;
132    local($i) = 0;
133    local(@sorted_lines);
134    foreach $key (sort values %lines) {
135	$sorted_lines[$i] = $key;
136	$i++;
137    }
138    @sorted_lines;
139}
140
141# This is a numerical sort routine (ascending).
142sub numsort {
143    local(%lines) = @_;
144    local($i) = 0;
145    local(@sorted_lines);
146    foreach $num (sort {$a <=> $b} keys %lines) {
147	$sorted_lines[$i] = $lines{$num};
148	$i++;
149    }
150    @sorted_lines;
151}
152
153# This is a sort routine that will sort on the
154# ip address when the ip address is anywhere in
155# the strings.
156sub ipsort {
157    local(%lines) = @_;
158    local($i) = 0;
159    local(@sorted_lines);
160    foreach $addr (sort sortbyipaddr keys %lines) {
161	$sorted_lines[$i] = $lines{$addr};
162	$i++;
163    }
164    @sorted_lines;
165}
166
167# These two routines will sort based upon IP addresses
168sub ipaddrval {
169    my(@a) = ($_[0] =~ m#^(\d+)\.(\d+)\.(\d+)\.(\d+)$#);
170    $a[3] + 256 * ($a[2] + 256 * ($a[1] +256 * $a[0]));
171}
172sub sortbyipaddr {
173    &ipaddrval($a) <=> &ipaddrval($b);
174}
175
176# This routine parses "bigpipe base list"
177sub ShowBaseRun {
178    my($line) = (0);
179    print STDERR "    In ShowBaseRun: $_" if ($debug);
180
181    while (<INPUT>) {
182	tr/\015//d;
183	last if (/^$prompt/);
184	next if (/^(\s*|\s*$cmd\s*)$/);
185	return(1) if /^\s*\^\s*$/;
186	return(1) if /(Invalid input detected|Type help or )/;
187	return(-1) if (/command authorization failed/i);
188
189	if (!$line++) {
190	    ProcessHistory("SHOWBASE","","","#\n#base:\n");
191	}
192	# filter oscillating "password crypt", "auth password crypt",
193	# "privacy password crypt"
194	if (/(password crypt) / && $filter_pwds >= 1) {
195	    ProcessHistory("LINE-PASS","","","# $`$1 <removed>\n");
196	    next;
197	}
198	if (/(auth-password-encrypted )\S+/) {
199	    ProcessHistory("SHOWBASE","","","# $1 <removed>");
200	    next;
201	}
202	ProcessHistory("SHOWBASE","","","# $_") && next;
203    }
204    return(0);
205}
206
207# This routine parses "bigpipe db show"
208sub ShowDb {
209    my($line) = (0);
210    print STDERR "    In ShowDb: $_" if ($debug);
211
212    while (<INPUT>) {
213	tr/\015//d;
214	last if (/^$prompt/);
215	next if (/^(\s*|\s*$cmd\s*)$/);
216	return(1) if /^\s*\^\s*$/;
217	return(1) if /(Invalid input detected|Type help or )/;
218	return(-1) if (/command authorization failed/i);
219
220	if (!$line++) {
221	    ProcessHistory("SHOWDB","","","#\n#database:\n");
222	}
223	/UCS.LoadTime/ && next;
224	/Configsync\..*Time/ && next;
225	/Configsync.peerupdatedstatus/ && next;
226	/Failover\..*Time/ && next;
227	/LTM.ConfigTime/ && next;
228
229	if (/^(snmp\..*\.community\..* =) (.+)/i) {
230	    if ($filter_commstr) {
231		ProcessHistory("SHOWDB","","","# $1 <removed>\n") && next;
232	    } else {
233		ProcessHistory("SHOWDB","","","# $1 $2\n") && next;
234	    }
235	}
236
237	ProcessHistory("SHOWDB","","","# $_") && next;
238    }
239    return(0);
240}
241
242# This routine parses "cat /config/bigip.license"
243sub ShowLicense {
244    my($line) = (0);
245    print STDERR "    In ShowLicense: $_" if ($debug);
246
247    while (<INPUT>) {
248	tr/\015//d;
249	# v9 software license does not have CR at EOF
250	s/^#-+($prompt.*)/$1/;
251	last if (/^$prompt/);
252	next if (/^(\s*|\s*$cmd\s*)$/);
253	return(1) if /^\s*\^\s*$/;
254	return(1) if /(Invalid input detected|Type help or )/;
255	return(-1) if (/command authorization failed/i);
256
257	if (!$line++) {
258	    ProcessHistory("LICENSE","","","#\n#/config/bigip.license:\n");
259	}
260	ProcessHistory("LICENSE","","","# $_") && next;
261    }
262    return(0);
263}
264
265# This routine parses "bigpipe monitor list all"
266sub ShowMonitor {
267    my($line) = (0);
268    print STDERR "    In ShowMonitor: $_" if ($debug);
269
270    while (<INPUT>) {
271	tr/\015//d;
272	last if (/^$prompt/);
273	next if (/^(\s*|\s*$cmd\s*)$/);
274	return(1) if /^\s*\^\s*$/;
275	return(1) if /(Invalid input detected|Type help or )/;
276	return(-1) if (/command authorization failed/i);
277
278	if (!$line++) {
279	    ProcessHistory("MONITOR","","","#\n");
280	}
281	if (/^(snmp\.[^ ]+\.community) = (.+)/i) {
282	    if ($filter_commstr) {
283		ProcessHistory("SHOWDB","","","# $1 <removed>\n") && next;
284	    } else {
285		ProcessHistory("SHOWDB","","","# $1 $2\n") && next;
286	    }
287	}
288	if (/^(\s*)password / && $filter_pwds >= 1) {
289	    ProcessHistory("LINE-PASS","","","# $1password <removed>\n");
290	    next;
291	}
292
293	ProcessHistory("MONITOR","","","# $_") && next;
294    }
295    return(0);
296}
297
298# This routine parses "bigpipe platform"
299sub ShowPlatform {
300    print STDERR "    In ShowPlatform: $_" if ($debug);
301
302    while (<INPUT>) {
303	tr/\015//d;
304	last if (/^$prompt/);
305	next if (/^(\s*|\s*$cmd\s*)$/);
306	return(1) if /^\s*\^\s*$/;
307	return(1) if /(Invalid input detected|Type help or )/;
308	return(-1) if (/command authorization failed/i);
309
310	/fan speed/i && next;
311	/chassis temperature/i && next;
312	/degC/ && next;
313	s/\d+rpm//ig;
314	s/^\|//;
315	/Type: / && ProcessHistory("COMMENTS","keysort","A0",
316				   "#Chassis type: $'");
317
318	ProcessHistory("COMMENTS","keysort","B1","#$_") && next;
319    }
320    return(0);
321}
322
323# This routine parses "bigpipe profile list"
324sub ShowProfile {
325    print STDERR "    In ShowProfile: $_" if ($debug);
326
327    while (<INPUT>) {
328	tr/\015//d;
329	last if (/^$prompt/);
330	next if (/^(\s*|\s*$cmd\s*)$/);
331	return(1) if /^\s*\^\s*$/;
332	return(1) if /(Invalid input detected|Type help or )/;
333	return(-1) if (/command authorization failed/i);
334
335	ProcessHistory("PROFILE","",""," $_") && next;
336    }
337    return(0);
338}
339
340# This routine parses "ls --full-time --color=never /config/ssl/ssl.key"
341sub ShowSslKey {
342    print STDERR "    In ShowSslKey: $_" if ($debug);
343
344    while (<INPUT>) {
345	tr/\015//d;
346	# v9 software license does not have CR at EOF
347	s/^#-+($prompt.*)/$1/;
348	last if (/^$prompt/);
349	next if (/^(\s*|\s*$cmd\s*)$/);
350	return(1) if /^\s*\^\s*$/;
351	return(1) if /(Invalid input detected|Type help or )/;
352	return(-1) if (/command authorization failed/i);
353
354	ProcessHistory("SSLKEY","","","# $_") && next;
355    }
356    return(0);
357}
358
359# This routine parses "ls --full-time --color=never /config/ssl/ssl.crt"
360sub ShowSslCrt {
361    print STDERR "    In ShowSslCrt: $_" if ($debug);
362
363    while (<INPUT>) {
364	tr/\015//d;
365	# v9 software license does not have CR at EOF
366	s/^#-+($prompt.*)/$1/;
367	last if (/^$prompt/);
368	next if (/^(\s*|\s*$cmd\s*)$/);
369	return(1) if /^\s*\^\s*$/;
370	return(1) if /(Invalid input detected|Type help or )/;
371	return(-1) if (/command authorization failed/i);
372
373	ProcessHistory("SSLCRT","","","# $_") && next;
374    }
375    return(0);
376}
377
378# This routine parses "bigpipe route static show"
379sub ShowRouteStatic {
380    print STDERR "    In ShowRouteStatic: $_" if ($debug);
381
382    while (<INPUT>) {
383	tr/\015//d;
384	last if (/^$prompt/);
385	next if (/^(\s*|\s*$cmd\s*)$/);
386	return(1) if /^\s*\^\s*$/;
387	return(1) if /(Invalid input detected|Type help or )/;
388	return(-1) if (/command authorization failed/i);
389
390	ProcessHistory("ROUTE","",""," $_") && next;
391    }
392    return(0);
393}
394
395# This routine parses "bigpipe version"
396sub ShowVersion {
397    print STDERR "    In ShowVersion: $_" if ($debug);
398
399    while (<INPUT>) {
400	tr/\015//d;
401	last if (/^$prompt/);
402	next if (/^(\s*|\s*$cmd\s*)$/);
403	return(-1) if (/command authorization failed/i);
404
405	/^kernel:/i && ($_ = <INPUT>) &&
406	    ProcessHistory("COMMENTS","keysort","A3","#Image: Kernel: $_") &&
407	    next;
408	if (/^package:/i) {
409	    my($line);
410
411	    while ($_ = <INPUT>) {
412		tr/\015//d;
413		last if (/:/);
414		last if (/^$prompt/);
415		chomp;
416		$line .= " $_";
417	    }
418	    ProcessHistory("COMMENTS","keysort","A2",
419			   "#Image: Package:$line\n");
420	}
421
422	if (/:/) {
423	    ProcessHistory("COMMENTS","keysort","C1","#$_");
424	} else {
425	    ProcessHistory("COMMENTS","keysort","C1","#\t$_");
426	}
427    }
428    return(0);
429}
430
431# This routine processes a "bigpipe list"
432sub WriteTerm {
433    my($lines) = 0;
434    print STDERR "    In WriteTerm: $_" if ($debug);
435
436    while (<INPUT>) {
437	tr/\015//d;
438	next if (/^\s*$/);
439	# end of config - hopefully.  f5 does not have a reliable end-of-config
440	# tag.
441	if (/^$prompt/) {
442	    $found_end++;
443	    last;
444	}
445	return(-1) if (/command authorization failed/i);
446	# the pager can not be disabled per-session on the PIX
447	s/^<-+ More -+>\s*//;
448	/Non-Volatile memory is in use/  && return(-1); # NvRAM is locked
449	# filter out any RCS/CVS tags to avoid confusing local CVS storage
450	s/\$(Revision|Id):/ $1:/;
451	$lines++;
452
453	if (/^(enable )?(password|passwd) / && $filter_pwds >= 1) {
454	    ProcessHistory("ENABLE","","","! $1$2 <removed>\n");
455	    next;
456	}
457	if (/^(enable secret) / && $filter_pwds >= 2) {
458	    ProcessHistory("ENABLE","","","# $1 <removed>\n");
459	    next;
460	}
461	if (/^username (\S+)(\s.*)? secret /) {
462	    if ($filter_pwds >= 2) {
463		ProcessHistory("USER","keysort","$1","# username $1$2 secret <removed>\n");
464	    } else {
465		ProcessHistory("USER","keysort","$1","$_");
466	    }
467	    next;
468	}
469	if (/^username (\S+)(\s.*)? password ((\d) \S+|\S+)/) {
470	    if ($filter_pwds == 2) {
471		ProcessHistory("USER","keysort","$1","# username $1$2 password <removed>\n");
472	    } elsif ($filter_pwds == 1 && $4 ne "5"){
473		ProcessHistory("USER","keysort","$1","# username $1$2 password <removed>\n");
474	    } else {
475		ProcessHistory("USER","keysort","$1","$_");
476	    }
477	    next;
478	}
479	if (/^(\s*)password / && $filter_pwds >= 1) {
480	    ProcessHistory("LINE-PASS","","","# $1password <removed>\n");
481	    next;
482	}
483	if (/^\s*neighbor (\S*) password / && $filter_pwds >= 1) {
484	    ProcessHistory("","","","# neighbor $1 password <removed>\n");
485	    next;
486	}
487	# order logging statements
488	/^logging (\d+\.\d+\.\d+\.\d+)/ &&
489	    ProcessHistory("LOGGING","ipsort","$1","$_") && next;
490	# order/prune tacacs/radius server statements
491	if (/^(tacacs-server|radius-server) key / && $filter_pwds >= 1) {
492	    ProcessHistory("","","","# $1 key <removed>\n"); next;
493	}
494	# order clns host statements
495	/^clns host \S+ (\S+)/ &&
496	    ProcessHistory("CLNS","keysort","$1","$_") && next;
497	# order alias statements
498	/^alias / && ProcessHistory("ALIAS","keysort","$_","$_") && next;
499	# delete ntp auth password - this md5 is a reversable too
500	if (/^(ntp authentication-key \d+ md5) / && $filter_pwds >= 1) {
501	    ProcessHistory("","","","# $1 <removed>\n"); next;
502	}
503	# order ntp peers/servers
504	if (/^ntp (server|peer) (\d+)\.(\d+)\.(\d+)\.(\d+)/) {
505	    $sortkey = sprintf("$1 %03d%03d%03d%03d",$2,$3,$4,$5);
506	    ProcessHistory("NTP","keysort",$sortkey,"$_");
507	    next;
508	}
509	# order ip host line statements
510	/^ip host line(\d+)/ &&
511	    ProcessHistory("IPHOST","numsort","$1","$_") && next;
512	# order ip nat source static statements
513	/^ip nat (\S+) source static (\S+)/ &&
514	    ProcessHistory("IP NAT $1","ipsort","$2","$_") && next;
515
516	# monitor state
517	/^\s+monitor state (\S+)/ && next;
518
519	# catch anything that wasnt matched above.
520	ProcessHistory("","","","$_");
521    }
522
523    if ($lines < 3) {
524	printf(STDERR "ERROR: $host configuration appears truncated.\n");
525	$found_end = 0;
526	return(-1);
527    }
528
529    return(0);
530}
531
532# Main
533@commandtable = (
534	{'bigpipe version'		=> 'ShowVersion'},
535	{'bigpipe platform'		=> 'ShowPlatform'},
536	{'cat /config/bigip.license'	=> 'ShowLicense'},
537	{'bigpipe monitor list all'	=> 'ShowMonitor'},
538	{'bigpipe profile list'		=> 'ShowProfile'},
539	{'bigpipe base list'		=> 'ShowBaseRun'},
540	{'bigpipe db show'		=> 'ShowDb'},
541	{'bigpipe route static show'	=> 'ShowRouteStatic'},
542	{'ls --full-time --color=never /config/ssl/ssl.crt' => 'ShowSslCrt'},
543	{'ls --full-time --color=never /config/ssl/ssl.key' => 'ShowSslKey'},
544	{'bigpipe list'			=> 'WriteTerm'}
545);
546# Use an array to preserve the order of the commands and a hash for mapping
547# commands to the subroutine and track commands that have been completed.
548@commands = map(keys(%$_), @commandtable);
549%commands = map(%$_, @commandtable);
550$commandcnt = scalar(keys %commands);
551
552$commandstr=join(";",@commands);
553$cmds_regexp = join("|", map quotemeta($_), @commands);
554
555if (length($host) == 0) {
556    if ($file) {
557	print(STDERR "Too few arguments: file name required\n");
558	exit(1);
559    } else {
560	print(STDERR "Too few arguments: host name required\n");
561	exit(1);
562    }
563}
564if ($opt_C) {
565    print "clogin -t $timeo -c\'$commandstr\' $host\n";
566    exit(0);
567}
568open(OUTPUT,">$host.new") || die "Can't open $host.new for writing: $!\n";
569select(OUTPUT);
570# make OUTPUT unbuffered if debugging
571if ($debug) { $| = 1; }
572
573if ($file) {
574    print(STDERR "opening file $host\n") if ($debug || $log);
575    open(INPUT,"<$host") || die "open failed for $host: $!\n";
576} else {
577    print(STDERR "executing clogin -t $timeo -c\"$commandstr\" $host\n") if ($debug || $log);
578    system "clogin -t $timeo -c \"$commandstr\" $host </dev/null > $host.raw 2>&1" || die "clogin failed for $host: $!\n";
579    open(INPUT, "< $host.raw") || die "clogin failed for $host: $!\n";
580}
581
582# determine ACL sorting mode
583if ($ENV{"ACLSORT"} =~ /no/i) {
584    $aclsort = "";
585}
586# determine community string filtering mode
587if (defined($ENV{"NOCOMMSTR"}) &&
588    ($ENV{"NOCOMMSTR"} =~ /yes/i || $ENV{"NOCOMMSTR"} =~ /^$/)) {
589    $filter_commstr = 1;
590} else {
591    $filter_commstr = 0;
592}
593# determine oscillating data filtering mode
594if (defined($ENV{"FILTER_OSC"}) && $ENV{"FILTER_OSC"} =~ /no/i) {
595    $filter_osc = 0;
596} else {
597    $filter_osc = 1;
598}
599# determine password filtering mode
600if ($ENV{"FILTER_PWDS"} =~ /no/i) {
601    $filter_pwds = 0;
602} elsif ($ENV{"FILTER_PWDS"} =~ /all/i) {
603    $filter_pwds = 2;
604} else {
605    $filter_pwds = 1;
606}
607
608ProcessHistory("","","","#RANCID-CONTENT-TYPE: bigip\n#\n");
609ProcessHistory("COMMENTS","keysort","A1","#\n");
610ProcessHistory("COMMENTS","keysort","B0","#\n");
611ProcessHistory("COMMENTS","keysort","C0","#\n");
612TOP: while(<INPUT>) {
613    tr/\015//d;
614    if (/^Error:/) {
615	print STDOUT ("$host clogin error: $_");
616	print STDERR ("$host clogin error: $_") if ($debug);
617	$clean_run=0;
618	last;
619    }
620    while (/#\s*($cmds_regexp)\s*$/) {
621	$cmd = $1;
622	if (!defined($prompt)) {
623	    $prompt = ($_ =~ /^([^#]+#)/)[0];
624	    $prompt =~ s/([][}{)(\\])/\\$1/g;
625	    print STDERR ("PROMPT MATCH: $prompt\n") if ($debug);
626	}
627	print STDERR ("HIT COMMAND:$_") if ($debug);
628	if (! defined($commands{$cmd})) {
629	    print STDERR "$host: found unexpected command - \"$cmd\"\n";
630	    $clean_run = 0;
631	    last TOP;
632	}
633	$rval = &{$commands{$cmd}}(*INPUT, *OUTPUT, $cmd);
634	delete($commands{$cmd});
635	if ($rval == -1) {
636	    $clean_run = 0;
637	    last TOP;
638	}
639    }
640    if (/\#\s?exit$/) {
641	$clean_run=1;
642	last;
643    }
644}
645print STDOUT "Done $logincmd: $_\n" if ($log);
646# Flush History
647ProcessHistory("","","","");
648# Cleanup
649close(INPUT);
650close(OUTPUT);
651
652unlink("$host.raw") if (! $debug);
653
654# check for completeness
655if (scalar(%commands) || !$clean_run || !$found_end) {
656    if (scalar(keys %commands) eq $commandcnt) {
657	printf(STDERR "$host: missed cmd(s): all commands\n");
658    } elsif (scalar(%commands)) {
659	my($count, $i) = 0;
660	for ($i = 0; $i < $#commands; $i++) {
661	    if ($commands{$commands[$i]}) {
662		if (!$count) {
663		    printf(STDERR "$host: missed cmd(s): %s", $commands[$i]);
664		} else {
665		    printf(STDERR ", %s", $commands[$i]);
666		}
667		$count++;
668	    }
669	}
670	if ($count) {
671	    printf(STDERR "\n");
672	}
673    }
674    if (!$clean_run || !$found_end) {
675	print(STDERR "$host: End of run not found\n");
676	if ($debug) {
677	    print(STDERR "$host: clean_run is false\n") if (!$clean_run);
678	    print(STDERR "$host: found_end is false\n") if (!$found_end);
679	}
680	system("/usr/bin/tail -1 $host.new");
681    }
682    unlink "$host.new" if (! $debug);
683}
684