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.
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.
The steps for the recipe are as follows:
- 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
- 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');
- 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"
];
};
- 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;
}
- 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];
- 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"
]
);
- 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); })
- 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")
});
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:
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.