=pod

=head1 NAME

Net::Appliance::Session::Cookbook::Recipe04 - Not-So Simple Error Handling

=head1 NOTE

This Cookbook was contributed to the L<Net::Appliance::Session> project by
Nigel Bowden. Source code from the Cookbook is shipped in the C<examples>
folder of this module's distribution.

=head1 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.

=head1 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
 }

=head1 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 C<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 C<$@> 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 C<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 C<else> statement) and simply print out the error
message contained in C<$@>.

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.

=head1 SEE ALSO

=over 4

=item L<Net::Appliance::Session>

=item L<Net::Telnet>

=item L<Exception::Class>

=back

=head1 AUTHOR

Nigel Bowden, with POD formatting by Oliver Gorwits.

=head1 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.

=cut
