Tutorial: Writing Perl Client Applications

This tutorial explains how to write a Perl client application that requests operational or configuration information from the NETCONF server or loads configuration information onto a device. The following sections use the sample scripts included in the NETCONF XML Protocol Perl distribution as examples:

Import Perl Modules and Declare Constants

Include the following statement at the start of the application. This statement imports the functions provided by the Net::Netconf::Manager object, which the application uses to connect to the NETCONF server on a device.

use Net::Netconf::Manager;

Include statements to import other Perl modules as appropriate for your application. For example, several of the sample scripts import the following standard Perl modules, which include functions that handle input from the command line:

If the application uses constants, declare their values at this point. For example, the sample diagnose_bgp.pl script includes the following statement to declare a constant for the access method:

use constant VALID_ACCESS_METHOD => 'ssh';

The edit_configuration.pl script includes the following statements to declare constants for reporting return codes and the status of the configuration database:

use constant REPORT_SUCCESS => 1;
use constant REPORT_FAILURE => 0;
use constant STATE_CONNECTED => 1;
use constant STATE_LOCKED => 2;
use constant STATE_CONFIG_LOADED => 3;

Connect to the NETCONF Server

The following sections explain how to use the NET::Netconf::Manager object to connect to the NETCONF server on a device:

Satisfy Protocol Prerequisites

The NETCONF server supports several access protocols. For each connection to the NETCONF server on a device, the application must specify the protocol it is using. Client Perl applications can communicate with the NETCONF server via SSH only.

Before your application can run, you must satisfy the prerequisites for SSH. This involves enabling NETCONF on the device (set system services netconf ssh).

Group Requests

Establishing a connection to the NETCONF server on a device is one of the more time-intensive and resource-intensive functions performed by an application. If the application sends multiple requests to a device, it makes sense to send all of them within the context of one connection. If your application sends the same requests to multiple devices, you can structure the script to iterate through either the set of devices or the set of requests. Keep in mind, however, that your application can effectively send only one request to one NETCONF server at a time. This is because the NET::Netconf::Manager object does not return control to the application until it receives the closing </rpc-reply> tag that represents the end of the NETCONF server's response to the current request.

Obtain and Record Parameters Required by the NET::Netconf::Manager Object

The NET::Netconf::Manager object takes the following required parameters, specified as keys in a Perl hash:

The sample scripts record the parameters in a Perl hash called %deviceinfo, declared as follows:

my %deviceinfo = (
        'access' => $access,
        'login' => $login,
        'password' => $password,
        'hostname' => $hostname,
);

The sample scripts obtain the parameters from options entered on the command line by a user. Your application can also obtain values for the parameters from a file or database, or you can hardcode one or more of the parameters into the application code if they are constant.

Example: Collect Parameters Interactively

Each sample script obtains the parameters required by the NET::Netconf::Manager object from command-line options provided by the user who invokes the script. The script records the options in a Perl hash called %opt, using the getopts function defined in the Getopt::Std Perl module to read the options from the command line. (Scripts used in production environments probably do not obtain parameters interactively, so this section is important mostly for understanding the sample scripts.)

In the following example from the get_chassis_inventory.pl script, the first parameter to the getopts function defines the acceptable options, which vary depending on the application. A colon after the option letter indicates that it takes an argument.

The second parameter, \%opt, specifies that the values are recorded in the %opt hash. If the user does not provide at least one option, provides an invalid option, or provides the -h option, the script invokes the output_usage subroutine, which prints a usage message to the screen:

my %opt;
getopts('l:p:d:x:f:m:o:h', \%opt) || output_usage();
output_usage() if $opt{'h'};

The following code defines the output_usage subroutine for the get_chassis_inventory.pl script. The contents of the my $usage definition and the Where and Options sections are specific to the script, and differ for each application.

sub output_usage
{
    my $usage = "Usage: $0 [options] <target>

Where:

  <target>   The hostname of the target device.

Options:

  -l <login>    A login name accepted by the target device.
  -p <password> The password for the login name.
  -m <access>   Access method. The only supported method is 'ssh'.
  -x <format>   The name of the XSL file to display the response.
                Default: xsl/chassis_inventory.xsl
  -f <xmlfile>  The name of the XML file to print server response to.
                Default: xsl/chassis_inventory.xml
  -o <filename> output is written to this file instead of standard output.
  -d <level>    Debug level [1-6]\n\n";

    croak $usage;
}

The get_chassis_inventory.pl script includes the following code to obtain values from the command line for the parameters required by the NET::Netconf::Manager object. A detailed discussion of the various functional units follows the complete code sample.

# Get the hostname
my $hostname = shift || output_usage();

# Get the access method, can be ssh only
my $access = $opt{'m'} || 'ssh';
use constant VALID_ACCESS_METHOD => 'ssh';
output_usage() unless (VALID_ACCESS_METHOD =~ /$access/);

# Check for login name. If not provided, prompt for it
my $login = "";
if ($opt{'l'}) {
    $login = $opt{'l'};
} else {
    print STDERR "login: ";
    $login = ReadLine 0;
    chomp $login;
}

# Check for password. If not provided, prompt for it
my $password = "";
if ($opt{'p'}) {
    $password = $opt{'p'};
} else {
    print STDERR "password: ";
    ReadMode 'noecho';
    $password = ReadLine 0;
    chomp $password;
    ReadMode 'normal';
    print STDERR "\n";
}

In the first line of the preceding code sample, the script uses the Perl shift function to read the hostname from the end of the command line. If the hostname is missing, the script invokes the output_usage subroutine to print the usage message, which specifies that a hostname is required:

my $hostname = shift || output_usage();

The script next determines which access protocol to use, setting the $access variable to the value of the -m command-line option. If the specified value does not match the only valid value defined by the VALID_ACCESSES constant, the script invokes the output_usage subroutine to print the usage message.

my $access = $opt{'m'} || 'ssh';
use constant VALID_ACCESS_METHOD => 'ssh';
output_usage() unless (VALID_ACCESS_METHOD =~ /$access/);

The script then determines the username, setting the $login variable to the value of the -l command-line option. If the option is not provided, the script prompts for it and uses the ReadLine function (defined in the standard Perl Term::ReadKey module) to read it from the command line:

my $login = "";
if ($opt{'l'}) {
    $login = $opt{'l'};
} else {
    print STDERR "login: ";
    $login = ReadLine 0;
    chomp $login;
}

The script finally determines the password for the username, setting the $password variable to the value of the -p command-line option. If the option is not provided, the script prompts for it. It uses the ReadMode function (defined in the standard Perl Term::ReadKey module) twice: first to prevent the password from echoing visibly on the screen and then to return the shell to normal (echo) mode after it reads the password:

my $password = "";
if ($opt{'p'}) {
    $password = $opt{'p'};
} else {
    print STDERR "password: ";
    ReadMode 'noecho';
    $password = ReadLine 0;
    chomp $password;
    ReadMode 'normal';
    print STDERR "\n";
}

Obtain Application-Specific Parameters

In addition to the parameters required by the NET::Netconf::Manager object, applications might need to define other parameters, such as the name of the file to which to write the data returned by the NETCONF server in response to a request, or the name of the Extensible Stylesheet Transformation Language (XSLT) file to use for transforming the data.

As with the parameters required by the NET::Netconf::Manager object, your application can hardcode the values in the application code, obtain them from a file, or obtain them interactively. The sample scripts obtain values for these parameters from command-line options in the same manner as they obtain the parameters required by the NET::Netconf::Manager object. Several examples follow.

The following line enables a debugging trace if the user includes the -d command-line option.

my $debug_level = $opt{'d'};

The following line sets the $outputfile variable to the value specified by the -o command-line option. It names the local file to which the NETCONF server's response is written. If the -o option is not provided, the variable is set to the empty string.

my $outputfile = $opt{'o'} || "";

The following code from the diagnose_bgp.pl script defines which XSLT file to use to transform the NETCONF server's response. The first line sets the $xslfile variable to the value specified by the -x command-line option. If the option is not provided, the script uses the text.xsl file supplied with the script, which transforms the data to ASCII text. The if statement verifies that the specified XSLT file exists; the script terminates if it does not.

# Get the xsl file
my $xslfile = $opt{'x'} || "xsl/bgp.xsl";

# Check for the existence of the given file
if (! -f $xslfile) {
    croak "XSL file $xslfile does not exist.";
}

Establishing the Connection

After obtaining values for the parameters required for the NET::Netconf::Manager object, each sample script records them in the %deviceinfo hash:

my %deviceinfo = ( 
      'access' => $access,
      'login' => $login,
      'password' => $password,
      'hostname' => $hostname,
);

The script then invokes the NETCONF-specific new subroutine to create a NET::Netconf::Manager object and establish a connection to the specified routing, switching, or security platform. If the connection attempt fails (as tested by the ref operator), the script exits.

my $jnx = new Net::Netconf::Manager(%deviceinfo);
unless (ref $jnx) {
    croak "ERROR: $deviceinfo{hostname}: failed to connect.\n";
}

Submitting a Request to the NETCONF Server

After establishing a connection to a NETCONF server (see Submitting a Request to the NETCONF Server), your application can submit one or more requests by invoking the Perl methods that are supported in the version of the NETCONF XML protocol and Junos XML API used by the application:

See the following sections for more information:

Providing Method Options or Attributes

Many Perl methods have one or more options or attributes. The following list describes the notation used to define a method's options in the lib/Net/Netconf/Plugins.pm and lib/Net/Netconf/release/package_methods.pl files, and the notation that an application uses when invoking the method:

A method can have a combination of fixed-form options, options with variable values, attributes, and a set of configuration statements. For example, the get_forwarding_table_information method has four fixed-form options and five options with variable values:

## Method : <get-forwarding-table-information>## Returns: <forwarding-table-information>## Command: "show route forwarding-table"get_forwarding_table_information => {detail => $TOGGLE,extensive => $TOGGLE,multicast => $TOGGLE,family => $STRING,vpn => $STRING,summary => $TOGGLE,matching => $STRING,destination => $STRING,label => $STRING,},

Submitting a Request

The following code is the recommended way to send a request to the NETCONF server and shows how to handle error conditions. The $jnx variable is defined to be a NET::Netconf::Manager object.

my $res; # Netconf server response

# connect to the Netconf server
my $jnx = new Net::Netconf::Manager(%deviceinfo);
unless (ref $jnx) {
    croak "ERROR: $deviceinfo{hostname}: failed to connect.\n";
}

# Lock the configuration database before making any changes
print "Locking configuration database ...\n";
my %queryargs = ( 'target' => 'candidate' );
$res = $jnx->lock_config(%queryargs);

# See if you got an error
if ($jnx->has_error) {
    print "ERROR: in processing request \n $jnx->{'request'} \n";
    graceful_shutdown($jnx, STATE_CONNECTED, REPORT_FAILURE);
}

# Load the configuration from the given XML file
print "Loading configuration from $xmlfile \n";
if (! -f $xmlfile) {
    print "ERROR: Cannot load configuration in $xmlfile\n";
    graceful_shutdown($jnx, STATE_LOCKED, REPORT_FAILURE);    
}

# Read in the XML file
my $config = read_xml_file($xmlfile);
print "\n\n$config \n\n";

%queryargs = ( 
    'target' => 'candidate', 
    'config' => $config 
);
$res = $jnx->edit_config(%queryargs);

# See if you got an error
if ($jnx->has_error) {
    print "ERROR: in processing request \n $jnx->{'request'} \n";
    # Get the error
    my $error = $jnx->get_first_error();
    get_error_info(%$error);
    # Disconnect
    graceful_shutdown($jnx, STATE_LOCKED, REPORT_FAILURE);
}

# Commit the changes
print "Committing the edit-config changes ...\n";
$jnx->commit();
if ($jnx->has_error) {
    print "ERROR: Failed to commit the configuration.\n";
    graceful_shutdown($jnx, STATE_CONFIG_LOADED, REPORT_FAILURE);
}

# Unlock the configuration database and 
# disconnect from the Netconf server
print "Disconnecting from the Netconf server ...\n";
graceful_shutdown($jnx, STATE_LOCKED, REPORT_SUCCESS);

Example: Get an Inventory of Hardware Components

The get_chassis_inventory.pl script retrieves and displays a detailed inventory of the hardware components installed in a routing, swtiching, or security platform. It is equivalent to issuing the show chassis hardware detail command.

After establishing a connection to the NETCONF server, the script defines get_chassis_inventory as the request to send and includes the detail argument:

my $query = "get_chassis_inventory";
my %queryargs = ( 'detail' => 1 );

The script sends the query and assigns the results to the $res variable. It performs two tests on the results, and prints an error message if it cannot send the request or if errors occurred when executing it. If no errors occurred, the script uses XSLT to transform the results.

# send the command and get the server response
my $res = $jnx->$query(%queryargs);
print "Server request: \n $jnx->{'request'}\n Server response: \n $jnx->{'server_response'} \n";

# print the server response into xmlfile
print_response($xmlfile, $jnx->{'server_response'});

# See if you got an error
if ($jnx->has_error) {
    croak "ERROR: in processing request \n $jnx->{'request'} \n";
} else {
    # Transform the server response using XSL file
    my $res = new Net::Netconf::Transform();
    print "Transforming ...\n";
    my $nm = $res->translateXSLtoRelease('xmlns:lc', $xslfile,
                                         "$xslfile.tmp",
                                         $xmlfile);
    if ($nm) {
        format_by_xslt($nm, $xmlfile, );
    } else {
        print STDERR "ERROR: Invalid XSL File $xslfile\n";
    }
}

# Disconnect from the Netconf server
$jnx->disconnect();

Example: Edit Configuration Statements

The edit_configuration.pl script edits configuration statements and loads the configuration onto a device. It uses the basic structure for sending requests but also defines a graceful_shutdown subroutine that handles errors. The following sections describe the different functions that the script performs:

Handling Error Conditions

The graceful_shutdown subroutine in the edit_configuration.pl script handles errors in a slightly more elaborate manner than the generic structure described in Handling Error Conditions . It employs the following additional constants:

# query execution status constants
use constant REPORT_SUCCESS => 1;
use constant REPORT_FAILURE => 0;
use constant STATE_CONNECTED => 1;
use constant STATE_LOCKED => 2;
use constant STATE_CONFIG_LOADED => 3;

The first two if statements in the subroutine refer to the STATE_CONFIG_LOADED and STATE_LOCKED conditions, which apply specifically to loading a configuration in the edit_configuration.pl script.

sub graceful_shutdown
{
   my ($jnx, $state, $success) = @_;
   if ($state >= STATE_CONFIG_LOADED) {
       # We have already done an <edit-config> operation
       # - Discard the changes
       print "Discarding the changes made ...\n";
       $jnx->discard_changes();
       if ($jnx->has_error) {
           print "Unable to discard <edit-config> changes\n";
       }
   }

   if ($state >= STATE_LOCKED) {
       # Unlock the configuration database
       $jnx->unlock_config();
       if ($jnx->has_error) {
           print "Unable to unlock the candidate configuration\n";
       }
   }

   if ($state >= STATE_CONNECTED) {
       # Disconnect from the Netconf server
       $jnx->disconnect();
   }

   if ($success) {
       print "REQUEST succeeded !!\n";
   } else {
       print "REQUEST failed !!\n";
   }

   exit;
}

Locking the Configuration

The main section of the edit_configuration.pl script begins by establishing a connection to a NETCONF server. It then invokes the lock_configuration method to lock the configuration database. In case of error, the script invokes the graceful_shutdown subroutine described in Handling Error Conditions .

print "Locking configuration database ...\n";
my %queryargs = ( 'target' => 'candidate' );
$res = $jnx->lock_config(%queryargs);
# See if you got an error
if ($jnx->has_error) {
    print "ERROR: in processing request \n $jnx->{'request'} \n";
    graceful_shutdown($jnx, STATE_CONNECTED, REPORT_FAILURE);
}

Reading In the Configuration Data

In the following code sample, the edit_configuration.pl script reads in and parses a file that contains Junos XML configuration tag elements or ASCII-formatted statements. A detailed discussion of the functional subsections follows the complete code sample.

# Load the configuration from the given XML file
print "Loading configuration from $xmlfile \n";
if (! -f $xmlfile) {
    print "ERROR: Cannot load configuration in $xmlfile\n";
    graceful_shutdown($jnx, STATE_LOCKED, REPORT_FAILURE);    
}

# Read in the XML file
my $config = read_xml_file($xmlfile);
print "\n\n$config \n\n";

%queryargs = ( 
                 'target' => 'candidate'
             );

# If we are in text mode, use config-text arg with wrapped
# configuration-text, otherwise use config arg with raw XML
if ($opt{t}) {
  $queryargs{'config-text'} = '<configuration text> . $config . </configuration-text>';
} else {
  $queryargs{'config'} = $config;

The first subsection of the preceding code sample verifies the existence of the file containing configuration data. The name of the file was previously obtained from the command line and assigned to the $xmlfile variable. If the file does not exist, the script invokes the graceful_shutdown subroutine.

print "Loading configuration from $xmlfile \n";
if (! -f $xmlfile) {
    print "ERROR: Cannot load configuration in $xmlfile\n";
    graceful_shutdown($jnx, STATE_LOCKED, REPORT_FAILURE);
}

The script then invokes the read_xml_file subroutine, which opens the file for reading and return its contents in the $config variable. The queryargs key target is set to the value candidate. When the script calls the edit_configuration method, the candidate configuration is edited.

# Read in the XML file
my $config = read_xml_file($xmlfile);
print "\n\n$config \n\n";
 
%queryargs = ( 
                 'target' => 'candidate'
             );

If the -t command-line option was included when the edit_configuration.pl script was invoked, the file referenced by the $xmlfile variable should contain ASCII-formatted configuration statements like those returned by the CLI configuration-mode show command. If the configuration statements are in ASCII-formatted text, the script encloses the configuration stored in the $config variable within the <configuration-text> tag element and stores the result in the value associated with the queryargs hash key config-text.

If the -t command-line option was not included when the edit_configuration.pl script was invoked, the file referenced by the $xmlfile variable contains Junos XML configuration tag elements. In this case, the script stores just the $config variable as the value associated with the queryargs hash key config.

if ($opt{t}) {
  $queryargs{'config-text'} = '<configuration text> . $config . </configuration-text>';
} else {
  $queryargs{'config'} = $config;

Editing the Configuration Data

The script now invokes the edit_config method to edit the candidate configuration on the device. It invokes the graceful_shutdown subroutine if the response from the NETCONF server has errors.

$res = $jnx->edit_config(%queryargs);

# See if you got an error
if ($jnx->has_error) {
    print "ERROR: in processing request \n $jnx->{'request'} \n";
    # Get the error
    my $error = $jnx->get_first_error();
    get_error_info(%$error);
    # Disconnect
    graceful_shutdown($jnx, STATE_LOCKED, REPORT_FAILURE);

Committing the Configuration

If there are no errors, the script invokes the commit method:

# Commit the changes
print "Committing the <edit-config> changes ...\n";
$jnx->commit();
if ($jnx->has_error) {
    print "ERROR: Failed to commit the configuration.\n";
    graceful_shutdown($jnx, STATE_CONFIG_LOADED, REPORT_FAILURE);
}

Parsing and Formatting the Response from the NETCONF Server

As the last step in sending a request, the application verifies that there are no errors with the response from the NETCONF server. It can then write the response to a file, to the screen, or both. If the response is for an operational query, the application usually uses XSLT to transform the output into a more readable format, such as HTML or formatted ASCII. If the response consists of configuration data, the application can store it as XML (the Junos XML tag elements generated by default from the NETCONF server) or transform it into formatted ASCII text.

The following sections discuss parsing and formatting options:

Parsing and Formatting an Operational Response

The following code sample from the diagnose_bgp.pl script uses XSLT to transform an operational response from the NETCONF server into a more readable format. A detailed discussion of the functional subsections follows the complete code sample.

# Get the output file
my $outputfile = $opt{'o'} || "";

# Get the xsl file
my $xslfile = $opt{'x'} || "xsl/bgp.xsl";

# Check for the existence of the given file
if (! -f $xslfile) {
    croak "XSL file $xslfile does not exist.";
}

# Get the xmlfile
my $xmlfile = $opt{'f'} || "xsl/bgp.xml";

# send the command and get the server response
my $res = $jnx->$query();

# print the server response into xmlfile
print_response($xmlfile, $jnx->{'server_response'});

# See if you got an error
if ($jnx->has_error) {
    croak "ERROR: in processing request \n $jnx->{'request'} \n";
} else {
    # Transform the server response using XSL file
    my $res = new Net::Netconf::Transform();
    print "Transforming ...\n";
    my $nm = $res->translateXSLtoRelease('xmlns:lc', $xslfile,
                                         "$xslfile.tmp",
                                         $xmlfile);
    if ($nm) {
        format_by_xslt($nm, $xmlfile, );
    } else {
        print STDERR "ERROR: Invalid XSL File $xslfile\n";
    }
}

The first line of the preceding code sample illustrates how the scripts read the -o option from the command line to obtain the name of the file into which to write the results of the XSLT transformation:

my $outputfile = $opt{'o'} || "";

From the -x command-line option, the scripts obtain the name of the XSLT file to use, setting a default value if the option is not provided. The scripts exit if the specified file does not exist. The following example is from the diagnose_bgp.pl script:

my $xslfile = $opt{'x'} || "xsl/bgp.xsl";
if (! -f $xslfile) {
    croak "XSL file $xslfile does not exist.";
}

For examples of XSLT files, see the following directories in the NETCONF Perl distribution:

The actual parsing operation invokes the translateXSLtoRelease function (defined in the Net::Netconf::Transform module) to alter one of the namespace definitions in the XSLT file.

my $res = new Net::Netconf::Transform();
    print "Transforming ...\n";
    my $nm = $res->translateXSLtoRelease('xmlns:lc', $xslfile, 
                                         "$xslfile.tmp",
                                         $xmlfile);
    if ($nm) {
        format_by_xslt($nm, $xmlfile, );
    } else {
        print STDERR "ERROR: Invalid XSL File $xslfile\n";
    }

This is necessary because the XSLT 1.0 specification requires that every XSLT file define a specific value for each default namespace used in the data being transformed. The xmlns attribute in a NETCONF operational response tag element includes a code representing the Junos OS version, such as10.3R1 for the initial version of Junos OS Release 10.3. Because the same XSLT file can be applied to operational response tag elements from devices running different versions of the Junos OS, the XSLT file cannot predefine an xmlns namespace value that matches all versions. The translateXSLtoRelease function alters the namespace definition in the XSLT file identified by the $xslfile variable to match the value in the NETCONF server's response. It assigns the resulting XSLT file to the $nm variable.

After verifying that the translateXSLtoRelease function succeeded, the script invokes the format_by_xslt function, which builds a command string and assigns it to the $command variable. The first part of the command string invokes the xsltproc command and specifies the names of the XSLT and configuration data files ($xslfile and $xmlfile)::

sub format_by_xslt
{
    my ($xslfile, $xmlfile, $outfile)  = @_;

    print "Transforming $xmlfile with $xslfile...\n" if $outfile;
    my $command = "xsltproc $xslfile $xmlfile";
    $command .= "> $outfile" if $outfile;
    system($command);
    print "Done\n" if $outfile;
    print "See $outfile\n" if $outfile;
}

If the $outfile variable is defined (the file for storing the result of the XSLT transformation exists), the script appends a string to the $command variable to write the results of the xsltproc command to the file. (If the file does not exist, the script writes the results to standard out [stdout].) The script then invokes the system function to execute the command string and prints status messages to stdout.

If the translateXSLtoRelease function fails (the if ($nm) expression evaluates to “false”), the script prints an error:

if ($nm) {
     format_by_xslt($nm, $xmlfile, );
} else {
     print STDERR "ERROR: Invalid XSL File $xslfile\n";
}

Closing the Connection to the NETCONF Server

To end the NETCONF session and close the connection to the device, each sample script invokes the disconnect method. Several of the scripts do this in standalone statements:

$jnx->disconnect();

The edit_configuration.pl script invokes the graceful_shutdown method instead.

graceful_shutdown($jnx, $xmlfile, STATE_LOCKED, REPORT_SUCCESS);

The graceful_shutdown method takes the appropriate actions with regard to the configuration database and then invokes the disconnect method.