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
|
|
Successful XML Parse
|
|
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.