#!/usr/bin/perl -w # # check_iferrors_pct - Nagios(r) network traffic monitor plugin # Copyright (C) 2012 Peter Harrison (www.linuxhomenetworking.com) # # Based on check_iferrors.pl, copyright (C) 2004 Gerd Mueller / Netways GmbH # based on check_traffic from Adrian Wieczorek, # # Send us bug reports, questions and comments about this plugin. # Latest version of this software: http://www.nagiosexchange.org # # INSTALLATION: # Make sure $cache_dir exists and is writable by the nagios user. # # BUGS: # You cannot use multiple instances of this check on the same device. # # 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; either version 2 # of the License, or (at your option) any later version. # # 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307 # # # # Based on check_all_interfaces.pl use strict; use warnings; use Net::SNMP; use Nagios::Plugin; use File::Path qw(make_path); use Getopt::Long; &Getopt::Long::config('bundling'); use constant VERSION => "0.1"; &main(); exit; sub main { # default values; my $warn_usage = 0.0075; my $crit_usage = 0.0100; my $interface_name; my $opt_h = 0; my $more_help = 0; my $ifIndex = 0; my $do_32 = 0; my %error_rate = (); my %ifMetaData = (); # SNMP Specific my %snmp_vars = (); my %snmp_results = (); $snmp_vars{port} = 161; $snmp_vars{version} = 2; $snmp_vars{community} = "public"; # Create path to tmp files my $cache_dir = "/var/cache/nagios"; if (!(-d $cache_dir)) { make_path($cache_dir, { verbose => 0, mode => 0755, }); } # Get usage parameters GetOptions( "h|help" => \$opt_h, "m|morehelp" => \$more_help, "w|warning=s" => \$warn_usage, "c|critical=s" => \$crit_usage, "i|interface=s" => \$interface_name, "p|port=i" => \$snmp_vars{port}, "H|hostname=s" => \$snmp_vars{hostname}, "C|community=s" => \$snmp_vars{community}, "s|username=s" => \$snmp_vars{username}, "v|version=i" => \$snmp_vars{version}, "T|authpassword=s" => \$snmp_vars{authpassword}, "t|authprotocol=s" => \$snmp_vars{authprotocol}, "X|privpassword=s" => \$snmp_vars{privpassword}, "x|privprotocol=s" => \$snmp_vars{privprotocol} ); # Show help if needed if($more_help){&moreusage();} if($opt_h){&usage_and_exit();} if(!$snmp_vars{hostname}){&usage_and_exit();} # Do the snmp query assign results my ($tmp_snmp_results, $tmp_do_32) = &do_snmpwalk(\%snmp_vars,$interface_name); %snmp_results = %$tmp_snmp_results; $do_32 = $tmp_do_32; # Process the data my ($tmp_error_rate, $tmp_ifMetaData) = &process_data(\%snmp_results, $cache_dir,$snmp_vars{hostname}, $do_32, $interface_name ); %error_rate = %$tmp_error_rate; %ifMetaData = %$tmp_ifMetaData; # Do Nagios report &nagios_report( \%error_rate, \%ifMetaData, $snmp_vars{hostname}, $warn_usage, $crit_usage ); } # sub main sub process_data{ # Passing multiple arrays into subroutine my %if_new_data = %{$_[0]}; my $cache_dir = $_[1]; my $hostname = $_[2]; my $do_32 = $_[3]; my $interface_name = $_[4]; # # Get old error values, if any # my $last_check_time = 0; my %if_old_data = (); my %error_rate = (); my %ifMetaData = (); my $tmp_ifname = $interface_name; my $now = 0; # Define the filename we are planning to use if ($tmp_ifname){ $tmp_ifname =~ s/[^\w]//g; $tmp_ifname = "_" . $tmp_ifname; } else{ $tmp_ifname = ""; } my $filename = $cache_dir . "/check_iferrors_pct_clvr_" . $hostname . $tmp_ifname; # Does file exist and is it readable? if ( (-e $filename) && (-r $filename) && (-w $filename) ){ # Read all the stored data in the cache file open(INFILE, "<", $filename) or die("Could not open file $filename for reading : $!\n"); my $row = ; if ($row) { chomp $row; $last_check_time = $row; while ($row = ) { chomp $row; my ( $ifDescr, $file_in_errors, $file_out_errors, $file_in_discards, $file_out_discards, $file_in_packets, $file_out_packets ) = split(/\t/, $row); $if_old_data{$ifDescr}{in_errors} = $file_in_errors; $if_old_data{$ifDescr}{out_errors} = $file_out_errors; $if_old_data{$ifDescr}{in_discards} = $file_in_discards; $if_old_data{$ifDescr}{out_discards} = $file_out_discards; $if_old_data{$ifDescr}{in_packets} = $file_in_packets; $if_old_data{$ifDescr}{out_packets} = $file_out_packets; } } close(INFILE); } elsif ( (-e $filename) && (!(-r $filename) || !(-w $filename) ) ){ # File exists but is not readable or writable &nagios_exit("File " . $filename . " is not readable or writable by Nagios / Icinga", "CRITICAL"); } # # Check for errors and write out new error values # $now = time(); open(OUTFILE,">", $filename) or die("Could not open file $filename for writing : $!\n"); print OUTFILE "$now\n"; # The sort makes sure the interfaces are sorted in the cache file foreach my $ifDescr ( sort keys %if_new_data) { # Not all interfaces have error and packet OIDs. Eliminate these if( !defined($if_new_data{$ifDescr}{in_errors}) || !defined($if_new_data{$ifDescr}{out_errors}) || !defined($if_new_data{$ifDescr}{in_discards}) || !defined($if_new_data{$ifDescr}{out_discards}) || !defined($if_new_data{$ifDescr}{in_packets}) || !defined($if_new_data{$ifDescr}{out_packets}) ){next;} # Define some variables for output to an updated cache file my $in_errors = $if_new_data{$ifDescr}{in_errors}; my $out_errors = $if_new_data{$ifDescr}{out_errors}; my $in_discards = $if_new_data{$ifDescr}{in_discards}; my $out_discards = $if_new_data{$ifDescr}{out_discards}; my $in_packets = $if_new_data{$ifDescr}{in_packets}; my $out_packets = $if_new_data{$ifDescr}{out_packets}; my $ifIndex = $if_new_data{$ifDescr}{ifIndex}; # If !$last_check_time then set it to $now-1 $last_check_time ||= $now-1; # Initialize some other variables my $last_in_errors = 0; my $last_out_errors = 0; my $in_error_rate = 0; my $out_error_rate = 0; my $last_in_discards = 0; my $last_out_discards = 0; my $in_discard_rate = 0; my $out_discard_rate = 0; my $last_in_packets = 0; my $last_out_packets = 0; # Define what the old data values are. # If a new interface, then make them equal to new values if (exists $if_old_data{$ifDescr}) { $last_in_errors = $if_old_data{$ifDescr}{in_errors}; $last_out_errors = $if_old_data{$ifDescr}{out_errors}; $last_in_discards = $if_old_data{$ifDescr}{in_discards}; $last_out_discards = $if_old_data{$ifDescr}{out_discards}; $last_in_packets = $if_old_data{$ifDescr}{in_packets}; $last_out_packets = $if_old_data{$ifDescr}{out_packets}; } else { $last_in_errors = $in_errors; $last_out_errors = $out_errors; $last_in_discards = $in_discards; $last_out_discards = $out_discards; $last_in_packets = $in_packets; $last_out_packets = $out_packets; } # Populate the cache file print OUTFILE "$ifDescr\t$in_errors\t$out_errors\t$in_discards\t$out_discards\t$in_packets\t$out_packets\n"; # Calculate the error rates $error_rate{$ifIndex}{in_errors} = &get_error_rate($in_errors, $last_in_errors, $in_packets, $last_in_packets, $do_32); $error_rate{$ifIndex}{out_errors} = &get_error_rate($out_errors, $last_out_errors, $out_packets, $last_out_packets, $do_32); $error_rate{$ifIndex}{in_discards} = &get_error_rate($in_discards, $last_in_discards, $in_packets, $last_in_packets, $do_32); $error_rate{$ifIndex}{out_discards} = &get_error_rate($out_discards, $last_out_discards, $out_packets, $last_out_packets, $do_32); $ifMetaData{$ifIndex}{ifDescr} = $ifDescr; } close(OUTFILE); # Return error rates return(\%error_rate, \%ifMetaData); } sub nagios_report { # Passing multiple arrays into subroutine my %error_rate = %{$_[0]}; my %ifMetaData = %{$_[1]}; my $hostname = $_[2]; my $warn_usage = $_[3]; my $crit_usage = $_[4]; # Define nagios pointers and other variables my $np_threshold = 0; my @nagios_value = ""; my %nagios_hash = (); my $count = -1; my $easy_in_traffic = 0; my $easy_out_traffic = 0; # Define the message we will be giving my $np_message = "Interface error rates for host " . $hostname; # Create a Nagios object my $np = Nagios::Plugin->new(shortname => "IFERRORS"); foreach my $ifIndex (sort {$a <=> $b} keys(%error_rate)){ # No Null interfaces if($ifMetaData{$ifIndex}{ifDescr} =~ /Null/i){next;} foreach my $error_key ( keys %{ $error_rate{$ifIndex} } ) { # Increment count $count +=1; # Define the values we are going to test against $nagios_value[$count] = $error_rate{$ifIndex}{$error_key}; # Set up the thresholds we are interested in if( ($warn_usage >= 0) && ($crit_usage >= 0) ){ $np_threshold = $np->set_thresholds( warning => $warn_usage , critical => $crit_usage); } elsif($crit_usage >= 0){ $np_threshold = $np->set_thresholds( critical => $crit_usage); } elsif($warn_usage >= 0){ $np_threshold = $np->set_thresholds( warning => $warn_usage); } # Define the performance data. $nagios_value[0] $np->add_perfdata( label => $ifMetaData{$ifIndex}{ifDescr}. "_" . $error_key, value => $nagios_value[$count], threshold => $np_threshold, uom => "%" ); } } # All done. Remove trailing comma from $message $np_message =~ s/\,+\s+$//g; # Process and exit $np->nagios_exit( return_code => $np->check_threshold(check => \@nagios_value), message => $np_message ); } sub get_error_rate { # Passing multiple arrays into subroutine my $now_errors = $_[0]; my $last_errors = $_[1]; my $now_packets = $_[2]; my $last_packets = $_[3]; my $do_32 = $_[4]; # Initialize some variables my $max_counter = 0; my $delta_errors = 0; my $delta_packets = 0; # Set the rollover value for the counters if ( !$do_32 ) { $max_counter = 18446744073709551615; } else { $max_counter = 4294967295; }; # Calculate $delta_packets if ( $now_packets < $last_packets ) { # Counter rolls over $delta_packets = $now_packets + ($max_counter - $last_packets); } else { $delta_packets = $now_packets - $last_packets; } # Calculate $delta_errors if ( $now_errors < $last_errors ) { # Counter rolls over $delta_errors = $now_errors + ($max_counter - $last_errors); } else { $delta_errors = $now_errors - $last_errors; } # Return error rate as a percentage, not decimal if(!$delta_packets){$delta_packets = 1;} return (($delta_errors / $delta_packets) * 100); } sub do_snmpwalk { # Passing multiple arrays into subroutine my %snmp_vars = %{$_[0]}; my $interface_name = $_[1]; # Initialize variables my ($session, $error); my %iface_descr = (); my %in_errors = (); my %out_errors = (); my %in_discards = (); my %out_discards = (); my %in_packets = (); my %out_packets = (); my %oids_out_packets = (); my %oids_in_packets = (); my %return_hash = (); my $do_32 = 0; my $hard_fail = 1; # No errors tolerated from snmpwalks my $ifIndexSingle; # Define the IfDescr oid my %oids = (); $oids{snmpIfDescr} = "1.3.6.1.2.1.2.2.1.2"; # Get the SNMP session pointer $session = &get_snmp_session(\%snmp_vars); # # Get interface descriptions and current error values # # Determine the oids to use (32 bit or 64) my ($tmp_oids_in_packets, $tmp_oids_out_packets, $tmp_do_32)= &snmp_32_bit($session); %oids_out_packets = %$tmp_oids_out_packets; %oids_in_packets = %$tmp_oids_in_packets; $do_32 = $tmp_do_32; # Get Interface descriptions if($interface_name){ my ($tmpifIndex) = &get_ifindex( $session, \%oids, $interface_name); $ifIndexSingle = $tmpifIndex; $iface_descr{$ifIndexSingle} = $interface_name; } else { %iface_descr = &get_table($session,$hard_fail,$oids{snmpIfDescr}); } # Get errors my($tmp_in_errors,$tmp_out_errors,$tmp_in_discards,$tmp_out_discards,) = &get_errors($session,$ifIndexSingle); %in_errors = %$tmp_in_errors; %out_errors = %$tmp_out_errors; %in_discards = %$tmp_in_discards; %out_discards = %$tmp_out_discards; my($tmp_in_packets) = &get_packets($session,\%oids_in_packets,$ifIndexSingle); %in_packets = %$tmp_in_packets; my($tmp_out_packets) = &get_packets($session,\%oids_out_packets,$ifIndexSingle); %out_packets = %$tmp_out_packets; $session->close(); # Sort results by $iface_number, key by ifName foreach my $ifIndex (keys %iface_descr) { my $ifDescr = $iface_descr{$ifIndex}; $return_hash{$ifDescr}{in_errors} = $in_errors{$ifIndex}; $return_hash{$ifDescr}{out_errors} = $out_errors{$ifIndex}; $return_hash{$ifDescr}{in_discards} = $in_discards{$ifIndex}; $return_hash{$ifDescr}{out_discards} = $out_discards{$ifIndex}; $return_hash{$ifDescr}{in_packets} = $in_packets{$ifIndex}; $return_hash{$ifDescr}{out_packets} = $out_packets{$ifIndex}; $return_hash{$ifDescr}{ifIndex} = $ifIndex; } return(\%return_hash, $do_32); } sub get_snmp_session { # Passing multiple arrays into subroutine my %snmp_vars = %{$_[0]}; # Define the SNMP session variable my $session; my $error; # # Check for missing options # # Check missing host name if (!$snmp_vars{hostname}){ &nagios_exit("Missing host address!","UNKNOWN"); } # Make some variables lower case if ($snmp_vars{privprotocol}){$snmp_vars{privprotocol} = lc($snmp_vars{privprotocol});} if ($snmp_vars{authprotocol}){$snmp_vars{authprotocol} = lc($snmp_vars{authprotocol});} # Check SNMP version compatibility (Versions 1 & 2) if ( $snmp_vars{version} =~ /[12]/ ) { ( $session, $error ) = Net::SNMP->session( -hostname => $snmp_vars{hostname}, -community => $snmp_vars{community}, -port => $snmp_vars{port}, -version => $snmp_vars{version} ); } # / SNMP version 1 or 2 # Check SNMP version compatibility (version 3) elsif ( $snmp_vars{version} =~ /3/ ) { # AuthPriv mode if($snmp_vars{privpassword}){ ($session, $error) = Net::SNMP->session( -username => $snmp_vars{username}, -authpassword => $snmp_vars{authpassword}, -authprotocol => $snmp_vars{authprotocol}, -privpassword => $snmp_vars{privpassword}, -privprotocol => $snmp_vars{privprotocol}, -hostname => $snmp_vars{hostname}, -port => $snmp_vars{port}, -version => $snmp_vars{version} ); } # /AuthPriv mode # AuthNoPriv options else{ # AuthNoPriv mode if($snmp_vars{authprotocol}){ ($session, $error) = Net::SNMP->session( -username => $snmp_vars{username}, -authpassword => $snmp_vars{authpassword}, -authprotocol => $snmp_vars{authprotocol}, -hostname => $snmp_vars{hostname}, -port => $snmp_vars{port}, -version => $snmp_vars{version} ); } # /AuthNoPriv mode # noAuthNoPriv mode else{ ($session, $error) = Net::SNMP->session( -username => $snmp_vars{username}, -authpassword => $snmp_vars{authpassword}, -hostname => $snmp_vars{hostname}, -port => $snmp_vars{port}, -version => $snmp_vars{version} ); } # /noAuthNoPriv mode } # /AuthNoPriv options } # /SNMP version 3 # Some other version of SNMP else { &nagios_exit("Unknown SNMP v" . $snmp_vars{version},"UNKNOWN"); }; # Things failed, so DIE! if ( !defined($session) ) { &nagios_exit($error,"UNKNOWN"); }; # Return the session pointer return($session); } sub snmp_32_bit { my ( $session ) = @_; # Initialize oids my %oids_32_in = (); my %oids_32_out = (); my %oids_64_in = (); my %oids_64_out = (); $oids_32_in{InUcastPkts} = "1.3.6.1.2.1.2.2.1.11"; $oids_32_in{InMulticastPkts} = "1.3.6.1.2.1.31.1.1.1.2"; $oids_32_in{InBroadcastPkts} = "1.3.6.1.2.1.31.1.1.1.3"; $oids_32_out{OutUcastPkts} = "1.3.6.1.2.1.2.2.1.17"; $oids_32_out{OutMulticastPkts} = "1.3.6.1.2.1.31.1.1.1.4"; $oids_32_out{OutBroadcastPkts} = "1.3.6.1.2.1.31.1.1.1.5"; $oids_64_in{InUcastPkts} = "1.3.6.1.2.1.31.1.1.1.7"; $oids_64_in{InMulticastPkts} = "1.3.6.1.2.1.31.1.1.1.8"; $oids_64_in{InBroadcastPkts} = "1.3.6.1.2.1.31.1.1.1.9"; $oids_64_out{OutUcastPkts} = "1.3.6.1.2.1.31.1.1.1.11"; $oids_64_out{OutMulticastPkts} = "1.3.6.1.2.1.31.1.1.1.12"; $oids_64_out{OutBroadcastPkts} = "1.3.6.1.2.1.31.1.1.1.13"; # Initalize other variables my $response; my @request; my $answer; my $oid_value = 0; my $do_32 = 0; $request[0] = $oids_64_in{InUcastPkts} . ".1"; # Quit if query completely fails if ( !defined( $response = $session->get_request(@request) ) ) { $answer = $session->error; $session->close; &nagios_exit("$answer", "CRITICAL"); } # If there is no response, then we have 32 bit counters $oid_value = $response->{ $request[0] }; if (!$oid_value){ # We are using 32 bit counters $do_32 = 1; } else { # We are using 64 bit counters $do_32 = 0; } # Return the appropriate array if($do_32){ return(\%oids_32_in,\%oids_32_out,$do_32); } else { return(\%oids_64_in,\%oids_64_out,$do_32); } } sub nagios_exit { my ($message, $status) = @_; $status = uc($status); $message = $status . ": " . $message; if ($status eq "UNKNOWN"){ my $np = Nagios::Plugin->new(shortname => "UNKNOWN"); $np->nagios_exit(UNKNOWN, $message); } elsif ($status eq "WARNING"){ my $np = Nagios::Plugin->new(shortname => "WARNING"); $np->nagios_exit( WARNING, $message ) } elsif ($status eq "CRITICAL"){ my $np = Nagios::Plugin->new(shortname => "CRITICAL"); $np->nagios_exit( CRITICAL, $message ) } } # # Read a table through snmp # sub get_table { my ($session, $hard_fail, $oid) = @_; my $snmp_pointer; my %response = (); my %oid_result = (); # Do the SNMP query if ( !defined($snmp_pointer = $session->get_table($oid) ) ) { # If we want a hard failure, then fail, if not return an empty hash if ($hard_fail){ &nagios_exit("Could not read table " . $oid . " by SNMP: " . $session->error ,"CRITICAL"); } else{ return (%oid_result); } } # Fix the indexes %response = %$snmp_pointer; foreach my $key ( keys %response ) { my $fixed_key = $key; $fixed_key =~ s/.*\.(\d+)$/$1/; $oid_result{$fixed_key} = $response{$key}; } return (%oid_result); } sub get_oid{ my ($session, $hard_fail, $oid) = @_; my $snmp_pointer; my %response = (); my %oid_result = (); # Do the SNMP query if ( !defined($snmp_pointer = $session->get_request($oid) ) ) { # If we want a hard failure, then fail, if not return an empty hash if ($hard_fail){ &nagios_exit("Could not read oid " . $oid . " by SNMP: " . $session->error ,"CRITICAL"); } else{ return (%oid_result); } } # Fix the indexes %response = %$snmp_pointer; foreach my $key ( keys %response ) { if($response{$key} =~ /noSuchInstance/i){ # # For some reason the null pointer test above doesn't work # # If we want a hard failure, then fail, if not return an empty hash if ($hard_fail){ &nagios_exit("Could not read oid " . $oid . " by SNMP: " . $session->error ,"CRITICAL"); } else{ return (%oid_result); } } else{ my $fixed_key = $key; $fixed_key =~ s/.*\.(\d+)$/$1/; $oid_result{$fixed_key} = $response{$key}; } } return (%oid_result); } sub get_errors { my ($session,$ifIndexSingle) = @_; # Initialize variables my %in_errors = (); my %out_errors = (); my %in_discards = (); my %out_discards = (); my %oids = (); my $hard_fail = 1; # Define the oids we are interested in for errors $oids{IfInErrors} = "1.3.6.1.2.1.2.2.1.14"; $oids{ifInDiscards} = "1.3.6.1.2.1.2.2.1.13"; $oids{IfOutErrors} = "1.3.6.1.2.1.2.2.1.20"; $oids{ifOutDiscards} = "1.3.6.1.2.1.2.2.1.19"; if($ifIndexSingle){ # Populate the hashes %in_errors = &get_oid($session,$hard_fail,$oids{IfInErrors}.".".sprintf("%d",$ifIndexSingle)); %out_errors = &get_oid($session,$hard_fail,$oids{IfOutErrors}.".".sprintf("%d",$ifIndexSingle)); %in_discards = &get_oid($session,$hard_fail,$oids{ifInDiscards}.".".sprintf("%d",$ifIndexSingle)); %out_discards = &get_oid($session,$hard_fail,$oids{ifOutDiscards}.".".sprintf("%d",$ifIndexSingle)); } else { # Populate the hashes %in_errors = &get_table($session,$hard_fail,$oids{IfInErrors}); %out_errors = &get_table($session,$hard_fail,$oids{IfOutErrors}); %in_discards = &get_table($session,$hard_fail,$oids{ifInDiscards}); %out_discards = &get_table($session,$hard_fail,$oids{ifOutDiscards}); } # Return values return(\%in_errors,\%out_errors,\%in_discards,\%out_discards,); } sub get_packets { # Passing multiple arrays into subroutine my $session = $_[0]; my %oids = %{$_[1]}; my $ifIndexSingle = $_[2]; # Initialize hash my %return_hash = (); my %get_table_hash = (); my $hard_fail = 0; # There may be errors from unsupported OIDs foreach my $key (sort keys %oids){ # Get oid(s) if ($ifIndexSingle){ (%get_table_hash) = &get_oid($session,$hard_fail,$oids{$key}.".".sprintf("%d",$ifIndexSingle)); } else { (%get_table_hash) = &get_table($session,$hard_fail,$oids{$key}); } # If the hash is not empty, then add the packet counts. # This protects against adding packet counts for oids that are not supported # by older hardware if (keys %get_table_hash) { foreach my $inner_key (sort {$a <=> $b} keys %get_table_hash){ $return_hash{$inner_key} += $get_table_hash{$inner_key}; } } } # Return values return(\%return_hash); } sub get_ifindex { # Passing multiple arrays into subroutine my $session = $_[0]; my %oids = %{$_[1]}; my $interface_name = $_[2]; my $response; my $ifIndex; my $answer; my $key; # Lower case for $interface_name $interface_name = lc($interface_name); if ( !defined( $response = $session->get_table($oids{snmpIfDescr}) ) ) { $answer = $session->error; $session->close; &nagios_exit($answer, "UNKNOWN"); }; foreach $key ( keys %{$response} ) { # Get first entry, strip out the ifIndex and exit # added 20070816 by oer: remove trailing 0 Byte for Windows :-( my $oid_pair=$response->{$key}; $oid_pair =~ s/\x00//; if(lc($oid_pair) =~ /$interface_name/){ $ifIndex = $key; $ifIndex =~ s/.*\.(\d+)$/$1/; last; } } if ( !defined ($ifIndex )) { &nagios_exit("Could not match " . $interface_name . " specified on command line", "CRITICAL"); } return $ifIndex; } sub usage_and_exit { print <