NAME
Net::Appliance::Session::Cookbook::Recipe04 - Not-So Simple Error Handling
NOTE
This Cookbook was contributed to the Net::Appliance::Session project by Nigel Bowden. Source code from the Cookbook is shipped in the examples
folder of this module's distribution.
PROBLEM
You want to perform an opertation using SSH on a number of Cisco IOS devices, but don't want the script to fail just because an operation on one device failed. In addition, you need more detailed failure information from your script if it should fail.
SOLUTION
This solution builds on the previous recipe 03. I won't be covering the material in that recipe here, so if you haven't read it you might like to go back and take a look.
Again, we assume that the IOS device requires only a username and password to login to the device. At that point, it is assumed that the device is configured to drop a user straight in to enable mode (privilege level 15) when logged in. See recipe 02 for details of how to explicitly drop in to enable mode.
use strict;
use warnings;
use Net::Appliance::Session;
# list of devices to contact
my @ios_device_ip_addresses = ('10.250.249.215', '10.250.249.216');
my $ios_username = 'cisco';
my $ios_password = 'cisco';
# step through each device in turn
DEVICE:
for my $ios_device_ip ( @ios_device_ip_addresses ) {
my @version_info; # array to hold data from device
my $session_obj = Net::Appliance::Session->new(
Host => $ios_device_ip,
Transport => 'SSH',
);
# start eval block to trap errors in interactive session
eval {
# try to login to the ios device, ignoring host check
$session_obj->connect(
Name => $ios_username,
Password => $ios_password,
SHKC => 0
);
# get our running config
@version_info = $session_obj->cmd('show ver');
# close down our session
$session_obj->close;
};
# check if we had an error
if ($@) {
if ( UNIVERSAL::isa($@, 'Net::Appliance::Session::Exception') ) {
# fault description from Net::Appliance::Session
print "We had an error during our Telnet/SSH session to device: $ios_device_ip \n";
print $@->message . " \n";
# message from Net::Telnet
print "Net::Telnet message : " . $@->errmsg . "\n";
# last line of output from your appliance
print "Last line of output from device : " . $@->lastline . "\n\n";
}
elsif (UNIVERSAL::isa($@, 'Net::Appliance::Session::Error') ) {
# fault description from Net::Appliance::Session
print "We had an issue during program execution to device: $ios_device_ip \n";
print $@->message . " \n";
}
else {
# we had some other error that wasn't a deliberately created exception
print "We had an issue when accessing the device : $ios_device_ip \n";
print "The reported error was : $@ \n";
}
next DEVICE;
}
print @version_info;
# end of for loop
}
DISCUSSION
(Don't worry if some of this doesn't make much sense if you aren't too familiar with objects, classes etc. Just jump to the end of this section and see the general event handling subroutine which should be easy enough to use for many scenarios)
Net::Appliance::Session implements exceptions using the Exception::Class module.
This means that instead of a simple die
when an error is detected by Net::Appliance::Session, an exception is raised which provides far more information about the condition that may have caused the failure for our script.
When an exception is raised, the $@
variable becomes an object which can be interrogated to find out more information relating to the failure.
Before we go on, you need some more background information about the operation of Net::Appliance::Session. The Net::Appliance::Session module is effectively built on top of the Net::Telnet module, which produces its own set of error messages when things go wrong. When things go wrong in Net::Appliance::Session (that aren't errors created by Net::Telnet), it also produces its own set of exception messages.
When Net::Telnet flags an error, it is raised as an exception that is a member of the Net::Appliance::Session::Exception class. When Net::Appliance::Session needs to raise an exception, its exceptions are members of the Net::Appliance::Session::Error class.
By testing which class an exception belongs to, we can determine the type of exception generated. In the case of Net::Appliance::Session::Exception class exceptions, we can pull out additional information about what went wrong in Net::Telnet.
There is one additional case that we need to cater for - other errors that aren't deliberately raised by Net::Appliance::Session (e.g. a new bug)
So, we use the following code to test for and report on a failure:
if ($@) {
if ( UNIVERSAL::isa($@, 'Net::Appliance::Session::Exception') ) {
# fault description from Net::Appliance::Session
print "We had an error during our Telnet/SSH session to device : $ios_device_ip \n";
print $@->message . " \n";
# message from Net::Telnet
print "Net::Telnet message : " . $@->errmsg . "\n";
# last line of output from your appliance
print "Last line of output from device : " . $@->lastline . "\n\n";
}
elsif (UNIVERSAL::isa($@, 'Net::Appliance::Session::Error') ) {
# fault description from Net::Appliance::Session
print "We had an issue during program execution to device : $ios_device_ip \n";
print $@->message . " \n";
}
else {
# we had some other error that wasn't a deliberately created exception
print "We had an issue when accessing the device : $ios_device_ip \n";
print "The reported error was : $@ \n";
}
next DEVICE;
}
Looking at the code snippet above, first of all we test to see if we've had a failure at all with the if ($@)
line - obviously if we haven't there's no need to take any action other than carrying on with our script.
If we have an error, we can see what type of exception has been raised. First of all we test to see if the exception belongs to the Net::Appliance::Session::Exception class:
if ( UNIVERSAL::isa($@, 'Net::Appliance::Session::Exception') ) {
If it does belong to the Net::Appliance::Session::Exception class, then we know that this exception has been generated by Net::Telnet. So, we can pull out additional information about the exception.
Alternatively, we can test to see if this exception belongs to the Net::Appliance::Session::Error class (i.e. it has been caused by a Net::Appliance::Session execution error):
elsif (UNIVERSAL::isa($@, 'Net::Appliance::Session::Error') ) {
In this example of handling the error, we take a simplistic action (i.e. we just print the failure message generated by the exception), but in a production script, there may be significant value in being able to distinguish between the types of exception and taking appropriate action.
Finally, we cater for the case when some other type of exception has been raised (using the final else
statement) and simply print out the error message contained in $@
.
All of this might seem a little bit involved and overkill for many simple scripts, but it does serve to show the additional exception reporting that is available.
By way of a demonstration of the difference of the information available, here is the output from our last recipe (03) when trying to connect to a device, but using an incorrect username:
We had an issue when accessing the device : 10.250.249.215
The reported error was : Login failed to remote host at ./3 - Simple Error Handling.pl line 35
Here is the same failure using our more advanced exception reporting:
We had an error during our Telnet/SSH session to device : 10.250.249.215
Login failed to remote host at ./4 - Not-So Simple Error Handling.pl line 36
Net::Telnet message : pattern match timed-out
Last line of output from device : % Authentication failed.
We can see straight-away that the reason that our connection failed is an authentication failure, wheras in the first exception message, we just a general message about failing to login.
Having to include this check explicitly after each interactive session would become a bit of a pain, so here is a suggested subroutine that could return us information about a failure. It will attempt to return the maximum amount of information available, based on the failure type. The returned information is a simple string.
sub err_handler {
my $err = shift;
my $message;
my $errmsg;
my $lastline;
if ( UNIVERSAL::isa($@, 'Net::Appliance::Session::Exception') ) {
# fault description from Net::Appliance::Session
$message = $@->message;
$errmsg = $@->errmsg;
$lastline = $@->lastline;
return $message . "$errmsg \n" . "$lastline \n";
}
else {
# error from Net::Appliance::Session or $@
$message = $@;
return $message;
}
}
This error handler would be used as follows:
# interactive stuff above here in eval braces...
if ($@) {
print err_handler($@);
}
Obviously, we don't have to print the failure information out, we could log the data to a file or do something else useful with the failure information.
SEE ALSO
AUTHOR
Nigel Bowden, with POD formatting by Oliver Gorwits.
COPYRIGHT & LICENSE
Copyright (c) Nigel Bowden 2007. All Rights Reserved.
You may distribute and/or modify this documentation under the same terms as Perl itself.