NAME

Net::Appliance::Session::Cookbook::Recipe06 - Storing Device Configurations in Files

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 telnet or SSH to all of your Cisco IOS devices and pull off their configurations so that you can build up a historic record of configuration files across your network.

SOLUTION

Well, if this is a cookbook, then this particular section is a 6 course banquet. This is a huge script compared to the previous examples we have looked at.

It brings together all of the areas we have looked at previously, but I have pulled in the services of a number of other Perl modules to try to provide a usable utility that could perhaps be used on your network to gather your Cisco device configurations.

All of the credentials that are required to access your devices are now stored in a CSV file, so devices can be added or removed as required. As different devices may have differing access requirements (e.g. transports, username/passwords required etc.) each device is configured with its own set of access credentials. CSV files (comma seperated values) are easy to update using many spreadsheet applications, making it easy to maintain the device credential data for this script.

Also, the script uses an external intialization file (.ini) to configure the operation of the script (e.g. where configuration files will be dumped), rather than having to hard-code values in the body of the script itself.

The script comprises two files:

Recipe_06.pl

The file below, in this Cookbook.

setup.ini

The initialization file that must be in the same directory as Recipe_06.pl.

device_credentials.csv

The CSV file whose location is configured in setup.ini.

The main body of the script should be relatively easy to follow, as it is very similar to previous examples. Where new functions are introduced (e.g. reading and parsing a CSV file), then those functions have (in the main) been separated off in to additional subroutines.

Here is the full solution:

 use strict;
 use warnings;
 use Carp;
 
 use Net::Appliance::Session;
 use Config::INI::Simple;
 use Text::CSV_XS;
 use File::Basename;
 
 # create Config::INI::Simple object and read in our ini file data
 my $ini_filename = dirname($0) . "/setup.ini"; # where we can find ini file
 my %ini_data = parse_ini_file($ini_filename);
 
 # get the device credentials for our devices
 my @device_data = parse_data_file($ini_data{device_csv_file});
 
 # step through each device and try to get and store our configs 
 DEVICE:
 for my $device_ref (@device_data) {
     
     my $device_name = $device_ref->{device_name} || $device_ref->{device_ip};
     
     # set up some logging
     my $debug_log = "$ini_data{debug_dir}/$device_name.debug.log";
     my $error_log = "$ini_data{error_dir}/$device_name.error.log";
     
     # create our config file name
     my $file_timestamp = file_timestamp();
     my $running_config_file = "$ini_data{repository_dir}/$device_name.$file_timestamp.conf";
     
     # create our Net::Appliance::Session with the transport for this device
     my $session_obj = Net::Appliance::Session->new(
         Host      => $device_ref->{device_ip},
         Transport => $device_ref->{transport},
     );
     
     # send the debug for this session to a device-specific file
     $session_obj->input_log($debug_log);
     my @running_config;
     
     # generate the required fields for the priv_array subroutine
     my @priv_array = priv_array($device_ref);
     
     # tell our session object we don't need enable password if none supplied
     unless ($priv_array[0]) {
         $session_obj->do_privileged_mode(0);
     }
     
     # do our interactive (Telnet/SSH) stuff...
     eval {
     
         # try to login to the ios device, ignoring host check
         $session_obj->connect( connect_hash($device_ref), SHKC => 0 );
           
         if ( $priv_array[0] ) {
     
             # if we need to use some enable credentials, supply them
             $session_obj->begin_privileged( @priv_array );
         }
         
         # get our running config
         @running_config =  $session_obj->cmd('show running');
     };
     
     # did we get an error ?
     if ($@) {
         
         # log error to file and move on to next device
         log_error( error_report($@, $device_name), $error_log );
         next DEVICE;
     }
     
     # chop out the extra info top and bottom of the config
     @running_config = @running_config[ 2 .. (@running_config -1)];
     
     # dump the config to a file
     open(CONFIG , " > $running_config_file")
        or warn("Unable to open config file for : $device_name : $!");
     print CONFIG @running_config;
     close CONFIG;
     
     # close down our session
     $session_obj->close;
 }   
 
 #####################################
 # Subroutines
 #####################################
 sub parse_ini_file {
 
     # parse our ini file to get the parameters we need in to
     # some convenient variables
     
     my $ini_filename = shift or croak("No ini file name passed");
     
     my $config_obj = Config::INI::Simple->new();
 
     $config_obj->read($ini_filename) or die("Cannot open ini file : $ini_filename (reason: $!)");
     
     my %ini_data; # variable to use as data hash to hold all ini file data
     
     # set up some variables for later use
     $ini_data{error_dir}        = $config_obj->{Logs}->{error_dir};
     $ini_data{debug_dir}        = $config_obj->{Logs}->{debug_dir};
     $ini_data{device_csv_file}  = $config_obj->{CSV}->{device_csv_file};
     $ini_data{repository_dir}   = $config_obj->{Repository}->{repository_dir};
     $ini_data{timestamp_format} = $config_obj->{Repository}->{timestamp_format};
 
     return %ini_data;
 }
 
 sub parse_data_file {
     
     # parse the CSV data file we are using to hold our  device
     # credential data
     
     my $device_csv_file = shift or croak("No csv file named passed");
 
     # create our csv object ready to parse in the data from our csv file
     my $csv_obj = Text::CSV_XS->new();
     
     #read in our csv file
     open my $csv_fh, "< $device_csv_file"
        or croak("Cannot open device csv file : $device_csv_file (reason: $!)");
     
     # take off the top row that has the field names
     my $top_row = $csv_obj->getline($csv_fh); 
     
     my @device_data;
     
     # take each entry in the CSV file and massage it into a complex
     # data structure
     while (my $data_row = $csv_obj->getline($csv_fh)) {
         my $hash_ref;
         map { ($hash_ref->{$_} = shift @$data_row) } @$top_row;
         
         push(@device_data, $hash_ref);
     }
     
     close $csv_fh;
     return @device_data;
 }
 
 sub connect_hash {
     
     # depending on the combination of credentials supplied, determine
     # the combination of login username/password to use
     
     my $device_ref = shift or croak("No device credentials ref passed !");
 
     # decide which set of credentials we have
     if ( exists($device_ref->{username}) && exists($device_ref->{password}) ) {
         
         # username & password supplied
         return ( Name => $device_ref->{username}, Password => $device_ref->{password} );
     }
     elsif ( exists($device_ref->{password}) ) {
         
         # password only supplied
         return ( Password => $device_ref->{password} );
     }
     else {
         croak("Invalid or missing credentials to log in to this device : "
                    . $device_ref->{device_name} );
     }
 }
 
 sub priv_array {
     
     # depending on the combination of credentials supplied, determine
     # the combination of enable username/password to use
     
     my $device_ref = shift or croak("No device credentials ref passed !");
 
     # decide which set of priv credentials we have
     if ( $device_ref->{enable_username} && $device_ref->{enable_password} ) {
         
         # username & password supplied
         return( $device_ref->{enable_username}, $device_ref->{enable_password} );
     }
     elsif ( $device_ref->{enable_password} ) {
         
         # password only supplied
         return( $device_ref->{enable_password} );
     }    
     elsif ( $device_ref->{enable_username} ) {
         
         # username only supplied - error !
         croak( "Invalid enable login credentials provided (only username provided !" );
     }    
     else {
         
         # no enble pwd required (assume drop straight in to enable mode)
         return 0;
     }
 }
 
 sub error_report {
     
     # standard subroutine used to extract failure info when
     # interactive session fails
     
     my $err         = shift or croak("No err !");
     my $device_name = shift or croak("No device name !");
     
     my $report; # holder for report message to return to caller
     
     if ( UNIVERSAL::isa($err, 'Net::Appliance::Session::Exception') ) {
             
             # fault description from Net::Appliance::Session
             $report  =  "We had an error during our Telnet/SSH session to device  : $device_name \n"; 
             $report .= $err->message . " \n";
                 
             # message from Net::Telnet
             $report .= "Net::Telnet message : " . $err->errmsg . "\n"; 
                 
             # last line of output from your appliance  
             $report .=  "Last line of output from device : " . $err->lastline . "\n\n";
 
         }
         elsif (UNIVERSAL::isa($err, 'Net::Appliance::Session::Error') ) {
             
             # fault description from Net::Appliance::Session
             $report  = "We had an issue during program execution to device : $device_name \n";
             $report .=  $err->message . " \n";
     
         }
         else {
             
             # we had some other error that wasn't a deliberately created exception
             $report  = "We had an issue when accessing the device : $device_name \n";
             $report .= "The reported error was : $err \n";
         }
         
         return $report;
 }
 
 sub log_error {
     
     # log an error message to a file
     
     my $error_message = shift;
     my $file_name     = shift;
     
     open(ERR , " > $file_name") or carp("Unable to error file : $file_name : $!");
     print ERR $error_message;
     close ERR;
 }
 
 sub file_timestamp {
 
     # create a timestamp to add to the conf files created
 
     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
     
     if ($ini_data{timestamp_format} eq 'uk') {
     
         # UK format
         return sprintf( "%02d-%02d-%4d-%02d%02d", $mday, ($mon + 1), ($year + 1900), $hour, $min );
     }
     else {
         
         # US format
         return sprintf( "%02d-%02d-%4d-%02d%02d", ($mon + 1), $mday, ($year + 1900), $hour, $min );   
     }
 }

DISCUSSION

As discussed above, the main body of this script pulls together most of the principles that are discussed in previous examples. So, I'm not going to discuss the whole script in detail (my fingers can't take the typing!). Instead I'll hightlight the major differences to previous examples and focus more on the features provided by the various subroutines that have been added.

One thing to mention before we go on are the couple of additional modules that we've pulled in to the script to provide us with some valuable features for our solution:

Config::INI::Simple

Allows us to read in a standard Windows-format ini file in to a Perl data structure.

Text::CSV_XS

Allows us to easily read in the contents of a CSV file.

Looking through the main body of the code, after the intial declarations to use the new modules listed above, we call a couple of new subroutines to read in the device credential data in our CSV file and the .ini file that configures the operation of the script:

 use strict;
 use warnings;
 use Carp;
 
 use Net::Appliance::Session;
 use Config::INI::Simple;
 use Text::CSV_XS;
 use File::Basename;
 
 # create Config::INI::Simple object and read in our ini file data
 my $ini_filename = dirname($0) . "/setup.ini"; # where we can find ini file
 my %ini_data = parse_ini_file($ini_filename);
 
 # get the device credentials for our devices
 my @device_data = parse_data_file($ini_data{device_csv_file});

The call to the parse_ini_file subroutine will read in the .ini file that is passed as a parameter and make its data available as a hash.

The call to the parse_data_file subroutine will read in the credentials CSV file that is passed as a parameter and make its data available as an array.

Next, we step through each of the devices whose credentials we retrieved from the CSV file, and create some file names for the various files we may create for this particular device:

 # step through each device and try to get and store our configs 
 DEVICE:
 for my $device_ref (@device_data) {
     
     my $device_name = $device_ref->{device_name} || $device_ref->{device_ip};
     
     # set up some logging
     my $debug_log = "$ini_data{debug_dir}/$device_name.debug.log";
     my $error_log = "$ini_data{error_dir}/$device_name.error.log";
     
     # create our config file name
     my $file_timestamp = file_timestamp();
     my $running_config_file = "$ini_data{repository_dir}/$device_name.$file_timestamp.conf";
     
     # create our Net::Appliance::Session with the transport for this device
     my $session_obj = Net::Appliance::Session->new(
         Host      => $device_ref->{device_ip},
         Transport => $device_ref->{transport},
     );
     
     # send the debug for this session to a device-specific file
     $session_obj->input_log($debug_log);

We will create a debug file for each device, that will be placed in the debug_dir that is defined in our .ini file.

Also, if we experience an error accessing a device, we will also dump an error message in to a device-specific error file in the error_dir that is defined in our .ini file.

One other new item is the file_timestamp subroutine that is called to give us a timestamp to add to our device configuration file. Each time a device configuration is succesfully retrieved, a file based on the device name and the date/time will be created. This allows us to keep a historical view of configurations retrieved from our network devices.

Once we get into our actual interactive session, we have to make a few decisions about how we are going to access each device based on its credentials. For instance, some devices will require a username/login combination for access, others may only require a password.

To cater for these varying access requirements, a couple of new subroutines have been added to provide the correct combination of credentials, dependant upon the credentials provided in our CSV file:

     # generate the required fields for the priv_array subroutine
     my @priv_array = priv_array($device_ref);
     
     # tell our session object we don't need enable password if none supplied
     unless ($priv_array[0]) {
         $session_obj->do_privileged_mode(0);
     }
     
     # do our interactive (Telnet/SSH) stuff...
     eval {
     
         # try to login to the ios device, ignoring host check
         $session_obj->connect( connect_hash($device_ref), SHKC => 0 );
           
         if ( $priv_array[0] ) {
     
             # if we need to use some enable credentials, supply them
             $session_obj->begin_privileged( @priv_array );
         }
         
         # get our running config
         @running_config =  $session_obj->cmd('show running');
     };

The connect_hash subroutine will look at the username and password fields of the credentials CSV file and supply the correct format array to pass to the connect method of our session object.

To get in to enable mode with the begin_privileged method, we need to ensure that we supply the correct credentials for each device. These are provided by the enable_username and enable_password fields of the CSV file. The priv_array subroutine will format an array to ensure that we can access enable mode (if required). If no enable mode credentials are provided, it is assumed that we are already in enable mode, and the session object is advised using the do_privileged_mode method.

Well, that's pretty much it in terms of discussing this script. As I say, much of it has been covered in previous examples.

One footnote I would like to add is that although this is a fully functioning script (apart from the bugs in it I haven't found yet!), it could still use quite a lot of improvements for a production environment. For instance, there is no checking of the type or presence of device credential data in the CSV file. If there is no IP address for a device, the whole thing falls in a heap. But, I had to draw the line somewhere when developing this script to demonstrate how it might be used in the real world, so please bear these limitations in mind.

INSTALLATION

As stated previously, the script comprises three files.

In addition to this script, there is a CSV file that contains the device credential data, and an initialization file that contains various parameters to control the operation of the script.

In case you don't have the two accompanying files, below are examples of the files that you require. If both of the files are placed in to the same directory as this script, then everything should work just fine.

To run the script (once you have all three files configured and together), just run it from the command line:

 $ perl Recipe_06.pl

If all runs well, you can maybe run it from your system scheduler to automate the process. However, don't forget to check for error logs periodically to make sure things are still running OK!

Configuration files that are retrieved should be available in the directory configured by the repository_dir parameter of the setup.ini file.

device_credentials.csv

 device_name,device_ip,username,password,enable_username,enable_password,transport
 rtr_1,10.250.249.215,cisco,cisco,,,Telnet
 rtr_2,10.250.249.216,cisco,cisco,,,Telnet

setup.ini

 [Logs]
 # directory for error logs
 error_dir=/home/nigel/perl/logs
 
 # directory for session debugging logs 
 debug_dir=/home/nigel/perl/logs
 
 [CSV]
 # CSV file with containing device credential info
 device_csv_file=./device_credentials.csv
 
 [Repository]
 # directory where configs are dumped
 repository_dir=/home/nigel/perl/configs
 
 # format of config file timestamp: 'uk' or 'us'
 timestamp_format=uk

SEE ALSO

Net::Appliance::Session

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.