The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

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

Net::Appliance::Session
Net::Telnet
Exception::Class

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.