Book Image

JUNOS Automation Cookbook

By : Adam Chappell
Book Image

JUNOS Automation Cookbook

By: Adam Chappell

Overview of this book

The JUNOS Automation Cookbook is a companion guide for the complex field of automating tasks on JUNOS devices. With a foundation in industry-standrd XML, JUNOS provides an ideal environment for programmatic interation, allowing you to build upon the capabilities provided by Juniper, with your own original code. You will begin by learning about, and setting up, the industry-standard NETCONF remote procedure call mechanisms on your device. After initial setup, you'll walk through SLAX - Juniper's foundation scripting language - for manipulating XML representations of JUNOS concepts and elements. You'll learn how to write your own SLAX scripts to customise the operating environment, and also how to write proactive event handlers that deal with situations as they happen. You'll then delve into PyEZ - Juniper's bridging framework to make automation accessible to Python code - allowing you to build automation applications in the popular scripting language. You'll witness some examples of how to write applications that can monitor configuration changes, implement BGP security policies and implement ad-hoc routing protocols, for those really tricky situations. You'll also leaarn how asynchronous I/O frameworks like Node.js can be used to implement automation applications that present an acceptable web interface. Along with way, you'll explore how to make use of the latest RESTful APIs that JUNOS provides, how to visualize aspects of your JUNOS network, and how to integrate your automation capabilities with enterprise-wide orchestration systems like Ansible. By the end of the book, you'll be able to tackle JUNOS automation challenges with confidence and understanding, and without hassle.
Table of Contents (10 chapters)

Processing NETCONF with Node.js

Node.js is a popular JavaScript-based language used originally in the server-side web environment, but now common in many application spaces. Its key benefit is its modern JavaScript-dialect allowing object-oriented and prototypal object inheritance models, fused together with Google's efficient V8 JavaScript engine, and an asynchronous, event-based programming framework from the get-go. The asynchronous nature of Node.js makes it ideal for advanced automation and control applications where one needs to communicate with multiple elements at once.

In this recipe, we explore the use of a simple Node.js application acting as a NETCONF client in a similar manner to the previous Python and Expect/TCL applications.

Getting ready

In order to complete this recipe, you should have already completed the JUNOS NETCONF over SSH setup recipe and have a working JUNOS OS NETCONF host. You also need a Node.js installation on the operating system of your choice. For our testing, we used a variety of versions, from v0.10.35 through v6.10.0.

How to do it...

The steps for the recipe are as follows:

  1. Firstly, install a viable XML parsing library. Out of the box, Node.js ships with no XML parsing capability within its standard modules, so make use of the popular xml2js library, written by Marek Kubica, and install it using the npm package management tool:
    unix$ npm install xml2js
  1. Import the required Node.js modules to operate. In this case, we make use of the child_process module in order to control a child process and the XML parsing module:
    #!/usr/bin/env node

const util = require("util");
const child_process = require('child_process');
const xml2js = require('xml2js');
  1. Define some program constants that we can refer to consistently later, including the XML phrase for the command RPC and the invaluable NETCONF delimiter that denotes the space between XML messages:
    const DELIMITER="]]>]]>";

const xmlRpcCommand = function(command) {
return [
"<rpc>\n",
"<command format=\"text\">\n",
command,
"</command>",
"</rpc>\n",
DELIMITER,
"\n"
];
};
  1. Define a convenience utility subroutine for accessing the nested JavaScript object dictionaries, which will be the result of parsing the XML.
  var walk = function(obj, path) {
var result = obj;
path.forEach(function (cur, ind, array) {
if (result) result=result[cur];
});
return result;
}
  1. Parse the command-line arguments to determine a target hostname and a command:
  if (process.argv.length!=4) {
console.warn("Usage: netconf.js user@hostname command\n");
process.exit(1);
}
var hostname = process.argv[2];
var command = process.argv[3];
  1. Start up a child process in order to run the SSH client to connect to the JUNOS OS host:
  var child = child_process.spawn(
"/usr/bin/ssh", [
"auto@"+hostname,
"-q",
"-p", "830",
"-i", "JUNOS_auto_id_rsa",
"-s", "netconf"
]
);
  1. Define the important event handlers to deal with the runtime interaction with the SSH session, including handling things like reading data from the SSH session and handling error conditions:
var data="";

child.stderr.on('data', function(chunk) { process.stderr.write(chunk, "utf8"); });
child.stdout.on('data', function(chunk) {
data+=chunk;
if ((index=data.indexOf(DELIMITER))!=-1) {
var xml = data.slice(0, index);
data = data.slice(index + DELIMITER.length);
xml2js.parseString(xml, function(err, result) {
if (err) throw err;
if (result['hello']) return;
if (output=walk(result, ['rpc-reply',
'output', 0])) {
console.log(output);
} else if (config=walk(result, ['rpc-reply',
'configuration-information', 0,
'configuration-output', 0])) {
console.log(config);
} else if (error=walk(result, ['rpc-reply',
'rpc-error', 0,
'error-message', 0])) {
console.log(error);
} else {
console.log("Unexpected empty response");
}
child.stdin.end();
});
}
});

child.on('error', function(err) { console.log("SSH client error: ", err); })
  1. Finally, start the ball rolling by issuing a command RPC for the user-specified CLI command:
xmlRpcCommand(command).forEach(function(cur, ind, array) {               
child.stdin.write(cur, "utf8")
});

How it works...

Step 1 sees us prepare the runtime environment by installing a module dependency. Node.js package management system has application dependencies installed in the same directory as the application, rather than polluting system directories. This makes for a more self-contained application, but be aware that the node_modules directory in your application directory is an integral part of your application.

In step 2, we start the source code and we start by pulling in the necessary Node.js modules that we need to reference in this application. We use the child_process module to manage a child SSH session, and we use the xml2js module to do the heavy work of parsing the XML.

Step 3 defines some foundation constants. In this case, we need to use the NETCONF delimiter, as in our other applications, in order to determine where XML messages start and stop. And we also include an XML template for the command RPC that we will call.

In step 4, we create a helper routine. Because the XML parsing process will leave us with complicated JavaScript dictionaries representing each of the tags in the XML document, we want to make a nice, clean and easy syntax to walk an XML structure. Unfortunately, Node.js isn't particularly tolerant to us de-referencing dictionary elements that are non-existent. For example, if we have an object structured like this:

  routers = { 'paris--1': { version: '14.1R6', hardware: 'MX960' },
'london--1': { version: '15.1F6-S6', hardware: 'MX960' },
'frankfurt--1': { version: '15.1F6-S6', hardware: 'MX960' } }

We might look to query the software version using syntax like this:

 > routers['paris--1']['version']
'14.1R6'

Unfortunately, this fails miserably if we try to reference a device that isn't in the dictionary. Node.js throws a TypeError exception, stopping the application in its track:

> routers['amsterdam--1']['version']
TypeError: Cannot read property 'version' of undefined

Instead, we use the walk routine defined in step 4 to conditionally walk a path through a JavaScript object, returning the undefined sentinel value at the earliest failure. This allows us to deal with the error condition on an aggregate basis, rather than checking validity of every element in the path:

    > walk(routers, [ "paris--1", "version" ])
'14.1R6'
> walk(routers, [ "amsterdam--1", "version" ])
undefined

Step 5 sees us use the JavaScript dialect to parse the command-line arguments, and like the previous recipes, we simply look to glean the target hostname and the command to execute.

Then the Node.js magic is put to work in steps 6 and 7. We start off a child process, which involves the operating system forking and executing an SSH client in a similar manner to the previous recipes. But instead of interacting with the SSH client with a series of read/writes, we instead simply define event handlers for what happens in response to certain events, and let the Node.js event loop do the rest.

In our case, we deal with different events, best described in pseudo code in the following table:

Event

Description

Data is received from the SSH client's standard output

  • Read the data

  • Look for the NETCONF delimiter

  • If it's found, take all the data up to it, and try to parse it as XML

  • If it's not found, just store what we have for the next read

Data is received from the SSH client's standard error

  • Print the same data (probably an error message) to the application standard error channel

Successful XML Parse

  • Print the content of any output, configuration - output, or error-message tags

Step 8 actually solicits the output from the JUNOS OS device by emitting the RPC command which executes the user's command. When the response is received, the prepared event handlers perform their prescribed activities, which results in the output being printed.