├── .gitignore ├── .jscsrc ├── .jshintrc ├── LICENSE ├── README.md ├── Vagrantfile ├── changes.txt ├── examples ├── README.md ├── data.json ├── insecure_ssh.key ├── netconf-client.js ├── netconf-facts.js ├── netconf-get.js ├── netconf-getarp.js ├── netconf-multiple-routers.js ├── netconf-multiple.js ├── pipeline.js ├── render.js ├── set-data.txt └── template.hb ├── lib └── netconf.js ├── package.json └── test └── core.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "crockford", 3 | "requireMultipleVarDecl": null, 4 | "requireVarDeclFirst": null, 5 | "disallowSpacesInsideArrayBrackets": null, 6 | "maximumLineLength": {"value": 100, "allExcept": ["regex"]}, 7 | "requireCamelCaseOrUpperCaseIdentifiers": {"ignoreProperties": true}, 8 | "requireSpacesInsideObjectBrackets": "all", 9 | "requireSpacesInsideArrayBrackets": "all" 10 | } 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "latedef" : false, // true: Require variables/functions to be defined before being used 16 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 20 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 21 | "plusplus" : false, // true: Prohibit use of `++` and `--` 22 | "quotmark" : false, // Quotation mark consistency: 23 | // false : do nothing (default) 24 | // true : ensure whatever is used is consistent 25 | // "single" : require single quotes 26 | // "double" : require double quotes 27 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 28 | "unused" : true, // Unused variables: 29 | // true : all variables, last function parameter 30 | // "vars" : all variables only 31 | // "strict" : all variables, all function parameters 32 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 33 | "maxparams" : false, // {int} Max number of formal params allowed per function 34 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 35 | "maxstatements" : false, // {int} Max number statements per function 36 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 37 | "maxlen" : false, // {int} Max number of characters per line 38 | "varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed. 39 | 40 | // Relaxing 41 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 42 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 43 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 44 | "eqnull" : false, // true: Tolerate use of `== null` 45 | //"es5" : true, // true: Allow ES5 syntax (ex: getters and setters) 46 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 47 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 48 | // (ex: `for each`, multiple try/catch, function expression…) 49 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 50 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 51 | "funcscope" : false, // true: Tolerate defining variables inside control statements 52 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 53 | "iterator" : false, // true: Tolerate using the `__iterator__` property 54 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 55 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 56 | "laxcomma" : false, // true: Tolerate comma-first style coding 57 | "loopfunc" : false, // true: Tolerate functions being defined in loops 58 | "multistr" : false, // true: Tolerate multi-line strings 59 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 60 | "notypeof" : false, // true: Tolerate invalid typeof operator values 61 | "proto" : false, // true: Tolerate using the `__proto__` property 62 | "scripturl" : false, // true: Tolerate script-targeted URLs 63 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 64 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 65 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 66 | "validthis" : false, // true: Tolerate using this in a non-constructor function 67 | 68 | // Environments 69 | "browser" : false, // Web Browser (window, document, etc) 70 | "browserify" : false, // Browserify (node.js code in the browser) 71 | "couch" : false, // CouchDB 72 | "devel" : true, // Development/debugging (alert, confirm, etc) 73 | "dojo" : false, // Dojo Toolkit 74 | "jasmine" : false, // Jasmine 75 | "jquery" : false, // jQuery 76 | "mocha" : false, // Mocha 77 | "mootools" : false, // MooTools 78 | "node" : true, // Node.js 79 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 80 | "phantom" : false, // PhantomJS 81 | "prototypejs" : false, // Prototype and Scriptaculous 82 | "qunit" : false, // QUnit 83 | "rhino" : false, // Rhino 84 | "shelljs" : false, // ShellJS 85 | "typed" : false, // Globals for typed array constructions 86 | "worker" : false, // Web Workers 87 | "wsh" : false, // Windows Scripting Host 88 | "yui" : false, // Yahoo User Interface 89 | 90 | // Custom Globals 91 | "globals" : {} // additional predefined global variables 92 | } 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Daryl Turner 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-netconf 2 | Pure JavaScript NETCONF library for Node.js 3 | 4 | This module was created to abstract the events and streams away from handling a NETCONF session in Node.js. Event management, message IDs and associating requests with replies is taken care of by the module and exposes core functions via requests and callbacks. 5 | 6 | The core parts of the code focus on the transport and messaging layers. The operation layer is implemented as wrapper functions and can be easily expanded. 7 | 8 | Multiple endpoints are supported and multiple asynchronous non-blocking requests can be made to each client. 9 | 10 | Developed/tested against Juniper devices. 11 | 12 | ## ARCHIVED. This package is no longer maintained. Dependencies are quite out of date and I do not recommend using this package directly. If no alternatives are available please fork and update package.json dependencies. Some require a major version bump and their APIs may have changed. 13 | 14 | ## Example 15 | ```js 16 | 17 | const router = new netconf.Client({ 18 | host: '172.28.128.3', 19 | username: 'vagrant', 20 | pkey: fs.readFileSync('insecure_ssh.key', { encoding: 'utf8' }) 21 | }) 22 | 23 | router.open((err) => { 24 | if (err) { 25 | throw err; 26 | } 27 | 28 | router.rpc('get-arp-table-information', (err, reply) => { 29 | router.close() 30 | if (err) { 31 | throw err; 32 | } 33 | 34 | console.log(JSON.stringify(reply)) 35 | }) 36 | }) 37 | ``` 38 | Checkout examples on github for more usage examples. 39 | 40 | ## Usage 41 | 42 | ### Connecting to endpoint 43 | 44 | Create a new Client object by passing in the connection parameters via a JavaScript object. Both password and private key authentication methods are supported. 45 | 46 | The NETCONF session can then be opened using the ```.open()``` method. 47 | 48 | *Function* 49 | router.open(callback); 50 | *Callback* 51 | function (err) {...} 52 | 53 | The callback function will be called once the SSH and NETCONF session has connected and hello and capability messages have been exchanged. The only argument passed to the callback function is an error instance. 54 | 55 | ```js 56 | const router = new netconf.Client({ 57 | host: '172.28.128.4', 58 | username: 'vagrant', 59 | password: null, 60 | pkey: privateKey 61 | }) 62 | 63 | router.open((err) => { 64 | if (err) { 65 | throw err 66 | } 67 | console.log('Connected') 68 | }) 69 | ``` 70 | 71 | ### Sending requests 72 | 73 | Requests are sent using the ```.rpc()``` method. 74 | 75 | **Simple Requests** 76 | *Function* 77 | router.rpc('request', callback); 78 | *Callback* 79 | function (err, reply) {...} 80 | 81 | For simple requests where only a NETCONF method is required with no arguments, then the method can be passed as a string. The string will be used to create the xml2js object dynamically. 82 | 83 | A message-id is automatically added to the request and the callback function will be invoked once the corresponding reply has been received. 84 | 85 | **Advanced Usage** 86 | *Function* 87 | router.rpc({ request: { arg1: 'value', arg2: 'value' } }, callback); 88 | 89 | For advanced usage where arguments are required to the NETCONF method then an object can be passed directly to the xml2js builder. The message-id will be automatically added. 90 | 91 | Examples of advanced usage can be found in the test suite, the examples and main library. 92 | 93 | **JunOS Examples** 94 | Juniper make it very simple to find the XML-RPC equivalent of it's CLI commands. 95 | 96 | For example, the method used to gather chassis info can be found as such: 97 | ```xml 98 | user@router> show chassis hardware | display xml rpc 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | ``` 109 | 110 | This can be used to retrieve this information using NETCONF. 111 | ```js 112 | router.rpc('get-chassis-inventory', (err, reply) => { 113 | ... 114 | }) 115 | ``` 116 | And for gathering interface information: 117 | ```xml 118 | user@router> show interfaces ge-1/0/1 | display xml rpc 119 | 120 | 121 | 122 | ge-1/0/1 123 | 124 | 125 | 126 | 127 | 128 | 129 | ``` 130 | ```js 131 | router.rpc({ 'get-interface-information': { 'interface-name': 'ge-1/0/1' } }, (err, reply) => { 132 | ... 133 | } 134 | ) 135 | ``` 136 | 137 | ### Closing the session 138 | 139 | The session can be gracefully closed using the ```.close()``` method. 140 | 141 | *Function* 142 | router.close([callback]); 143 | *Callback* 144 | function (err) {...} 145 | 146 | ### Options 147 | 148 | **XML Parsing** 149 | xml2js parsing options can be viewed/modified via ```.parseOpts``` in the client object. 150 | The default options (I believe) should cover most use cases. 151 | See xml2js documentation for different parsing options. https://www.npmjs.com/package/xml2js 152 | 153 | **Raw XML** 154 | The raw response from the server can be included by setting ```.raw = true``` in the client object. 155 | The raw XML will be embedded in the reply message under ```reply.raw```. 156 | 157 | ### Utility functions 158 | 159 | Utility functions for common JunOS operations have been added to make working with these devices easier. 160 | I'm happy to take pull requests for any added utility functions. 161 | 162 | Currently implemented are: 163 | commit, rollback, compare, load and facts. 164 | 165 | **Commit** 166 | Commit candidate configuration to device. 167 | 168 | *Function* 169 | router.commit(callback); 170 | *Callback* 171 | function (err, reply) {...} 172 | 173 | **Rollback** 174 | Discard candidate configuration on device. 175 | 176 | *Function* 177 | router.rollback(callback); 178 | *Callback* 179 | function (err, reply) {...} 180 | 181 | **Compare** 182 | Show difference between running and candidate-config. Equivalent to "show | compare". 183 | 184 | *Function* 185 | router.compare(callback); 186 | *Callback* 187 | function (err, diff) {...} 188 | 189 | **Load** 190 | Load configuration data into candidate-config using NETCONF. Default options are equivalent to "load merge" and would expect configuration data in JunOS curly-brace format. 191 | 192 | *Function* 193 | router.load(configData, callback); 194 | *Callback* 195 | function (err, reply) {...} 196 | 197 | The default load options can be overridden by supplying an options object in the format: 198 | ```js 199 | options = { 200 | config: configData, //required 201 | action: 'merge'|'replace'|'override'|'update'|'set', //default merge 202 | format: 'text'|'xml' //default text 203 | } 204 | ``` 205 | and called as such: 206 | 207 | *Function* 208 | router.load(options, callback) 209 | 210 | **Facts** 211 | The facts method collects some useful information from several RPC calls and presents the results back as a JavaScript object. 212 | 213 | The following is collected: hostname, uptime, model, serial number and software version. 214 | 215 | *Function* 216 | router.facts(callback) {...} 217 | *Callback* 218 | function (err, facts) 219 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # Vagrant file for quickly spinning up a couple of test NETCONF 2 | # boxes. Uses Juniper vSRX, requires the SSH private key in the 3 | # examples directory. 4 | 5 | srx = "juniper/ffp-12.1X47-D15.4-packetmode" 6 | 7 | Vagrant.configure(2) do |config| 8 | config.vm.define "router1" do |router| 9 | router.vm.box = srx 10 | router.vm.hostname = "router1" 11 | router.ssh.insert_key = false 12 | router.vm.network "private_network", type: "dhcp" 13 | end 14 | config.vm.define "router2" do |router| 15 | router.vm.box = srx 16 | router.vm.hostname = "router2" 17 | router.ssh.insert_key = false 18 | router.vm.network "private_network", type: "dhcp" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /changes.txt: -------------------------------------------------------------------------------- 1 | Major version bump due to change in return value (error structure). 2 | 3 | Version 2: 4 | - Create custom rpcError type and embed returned message in that instead of sending both. The old behaviour breaks some promise libraries. 5 | - Replaced use of var with const and let. 6 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # netconf-client example 2 | 3 | This is how i like to upload configuration to my devices. By splitting out the data and template it's easy to reuse the existing script for multiple purposes. 4 | 5 | ```shell 6 | cat data.json | ./render.js template.hb | ./netconf-client.js 7 | ``` 8 | 9 | or if configuration is done by set commands directly instead of template based. 10 | 11 | ```shell 12 | cat set-data.txt | ./netconf-client.js 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "descrip1": "vagrant management interface", 3 | "descrip2": "dev machine facing host-only interface" 4 | } 5 | -------------------------------------------------------------------------------- /examples/insecure_ssh.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzI 3 | w+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoP 4 | kcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2 5 | hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NO 6 | Td0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcW 7 | yLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQIBIwKCAQEA4iqWPJXtzZA68mKd 8 | ELs4jJsdyky+ewdZeNds5tjcnHU5zUYE25K+ffJED9qUWICcLZDc81TGWjHyAqD1 9 | Bw7XpgUwFgeUJwUlzQurAv+/ySnxiwuaGJfhFM1CaQHzfXphgVml+fZUvnJUTvzf 10 | TK2Lg6EdbUE9TarUlBf/xPfuEhMSlIE5keb/Zz3/LUlRg8yDqz5w+QWVJ4utnKnK 11 | iqwZN0mwpwU7YSyJhlT4YV1F3n4YjLswM5wJs2oqm0jssQu/BT0tyEXNDYBLEF4A 12 | sClaWuSJ2kjq7KhrrYXzagqhnSei9ODYFShJu8UWVec3Ihb5ZXlzO6vdNQ1J9Xsf 13 | 4m+2ywKBgQD6qFxx/Rv9CNN96l/4rb14HKirC2o/orApiHmHDsURs5rUKDx0f9iP 14 | cXN7S1uePXuJRK/5hsubaOCx3Owd2u9gD6Oq0CsMkE4CUSiJcYrMANtx54cGH7Rk 15 | EjFZxK8xAv1ldELEyxrFqkbE4BKd8QOt414qjvTGyAK+OLD3M2QdCQKBgQDtx8pN 16 | CAxR7yhHbIWT1AH66+XWN8bXq7l3RO/ukeaci98JfkbkxURZhtxV/HHuvUhnPLdX 17 | 3TwygPBYZFNo4pzVEhzWoTtnEtrFueKxyc3+LjZpuo+mBlQ6ORtfgkr9gBVphXZG 18 | YEzkCD3lVdl8L4cw9BVpKrJCs1c5taGjDgdInQKBgHm/fVvv96bJxc9x1tffXAcj 19 | 3OVdUN0UgXNCSaf/3A/phbeBQe9xS+3mpc4r6qvx+iy69mNBeNZ0xOitIjpjBo2+ 20 | dBEjSBwLk5q5tJqHmy/jKMJL4n9ROlx93XS+njxgibTvU6Fp9w+NOFD/HvxB3Tcz 21 | 6+jJF85D5BNAG3DBMKBjAoGBAOAxZvgsKN+JuENXsST7F89Tck2iTcQIT8g5rwWC 22 | P9Vt74yboe2kDT531w8+egz7nAmRBKNM751U/95P9t88EDacDI/Z2OwnuFQHCPDF 23 | llYOUI+SpLJ6/vURRbHSnnn8a/XG+nzedGH5JGqEJNQsz+xT2axM0/W/CRknmGaJ 24 | kda/AoGANWrLCz708y7VYgAtW2Uf1DPOIYMdvo6fxIB5i9ZfISgcJ/bbCUkFrhoH 25 | +vq/5CIWxCPp0f85R4qxxQ5ihxJ0YDQT9Jpx4TMss4PSavPaBH3RXow5Ohe+bYoQ 26 | NE5OgEXk2wVfZczCZpigBKbKZHNYcelXtTt/nP3rsCuGcM4h53s= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/netconf-client.js: -------------------------------------------------------------------------------- 1 | #!/opt/pkg/bin/node 2 | var fs = require('fs'); 3 | var process = require('process'); 4 | var netconf = require('../lib/netconf'); 5 | var pipeline = require('./pipeline'); 6 | var util = require('util'); 7 | 8 | function pprint(object) { 9 | console.log(util.inspect(object, {depth:null, colors:true})); 10 | } 11 | 12 | function configureRouter(configData) { 13 | router.open(function(err) { 14 | if (!err) { 15 | router.load({config: configData, action: 'replace', format: 'text'}, 16 | commitConf); 17 | } else { 18 | throw(err); 19 | } 20 | }); 21 | } 22 | 23 | function commitConf(err, reply) { 24 | if (!err) { 25 | router.compare(function(err, reply) { 26 | console.log('Configuration Diff:'); 27 | console.log(reply); 28 | if (process.argv[2] === '-c') { 29 | commitRollback(true); 30 | } else { 31 | commitRollback(false); 32 | } 33 | }); 34 | } else { 35 | pprint(reply); 36 | throw (err); 37 | } 38 | } 39 | 40 | function commitRollback(value) { 41 | if (value === true) { 42 | console.log('Commiting configuration.'); 43 | router.commit(function(err, result) { 44 | if (result.rpc_reply.commit_results.routing_engine.rpc_error) { 45 | router.rollback(function (err, rollback_result) { 46 | pprint(result); 47 | console.log('Commit error, rolling back.') 48 | router.close(); 49 | process.exit(1); 50 | }); 51 | } else { 52 | router.close(); 53 | } 54 | }); 55 | } else { 56 | console.log('Rolling back changes. Run with "-c" flag to commit.'); 57 | router.rollback(function(err, result) { 58 | router.close(); 59 | }); 60 | } 61 | } 62 | 63 | 64 | var params = { 65 | host: '172.28.128.3', 66 | username: 'vagrant', 67 | pkey: fs.readFileSync('insecure_ssh.key', {encoding: 'utf8'}) 68 | }; 69 | var router = new netconf.Client(params); 70 | 71 | pipeline.read(function renderTemplate(err, data) { 72 | if (err) { 73 | throw (err); 74 | } else { 75 | configureRouter(data); 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /examples/netconf-facts.js: -------------------------------------------------------------------------------- 1 | var netconf = require('../lib/netconf'); 2 | var fs = require('fs'); 3 | 4 | var router = new netconf.Client({ 5 | host: '172.28.128.3', 6 | username: 'vagrant', 7 | pkey: fs.readFileSync('insecure_ssh.key', {encoding: 'utf8'}) 8 | }); 9 | router.parseOpts.ignoreAttrs = true; 10 | 11 | router.open(function afterOpen(err) { 12 | if (!err) { 13 | router.facts(function (err, facts) { 14 | router.close(); 15 | if (err) { throw (err); } 16 | console.log(JSON.stringify(facts)); 17 | }); 18 | } else { throw err; } 19 | }); 20 | -------------------------------------------------------------------------------- /examples/netconf-get.js: -------------------------------------------------------------------------------- 1 | var netconf = require('../lib/netconf'); 2 | var util = require('util'); 3 | var fs = require('fs'); 4 | 5 | function pprint(object) { 6 | console.log(util.inspect(object, {depth:null, colors:true})); 7 | } 8 | 9 | var router = new netconf.Client({ 10 | host: '172.28.128.3', 11 | username: 'vagrant', 12 | pkey: fs.readFileSync('insecure_ssh.key', {encoding: 'utf8'}) 13 | }); 14 | router.parseOpts.ignoreAttrs = false; 15 | router.raw = true; 16 | 17 | router.open(function afterOpen(err) { 18 | if (!err) { 19 | router.rpc({ 'get-config': { source: { running: null } } }, function (err, results) { 20 | router.close(); 21 | if (err) { 22 | pprint(results); 23 | throw (err); 24 | } 25 | // pprint(results); 26 | console.log(results.raw); 27 | }); 28 | } else { 29 | throw err; 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /examples/netconf-getarp.js: -------------------------------------------------------------------------------- 1 | var netconf = require('../lib/netconf'); 2 | var util = require('util'); 3 | var fs = require('fs'); 4 | 5 | function pprint(object) { 6 | console.log(util.inspect(object, {depth:null, colors:true})); 7 | } 8 | 9 | function processResults(err, reply) { 10 | if (err) { 11 | pprint(reply); 12 | throw err; 13 | } else { 14 | var arpInfo = reply.rpc_reply.arp_table_information.arp_table_entry; 15 | console.log(JSON.stringify(arpInfo)); 16 | router.close(); 17 | } 18 | } 19 | 20 | var router = new netconf.Client({ 21 | host: '172.28.128.3', 22 | username: 'vagrant', 23 | pkey: fs.readFileSync('insecure_ssh.key', {encoding: 'utf8'}) 24 | }); 25 | 26 | router.open(function afterOpen(err) { 27 | if (!err) { 28 | // console.log(router.remoteCapabilities); 29 | // console.log(router.sessionID); 30 | router.rpc('get-arp-table-information', processResults); 31 | } else { 32 | throw err; 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /examples/netconf-multiple-routers.js: -------------------------------------------------------------------------------- 1 | var netconf = require('../lib/netconf'); 2 | var util = require('util'); 3 | var fs = require('fs'); 4 | 5 | function pprint(object) { 6 | console.log(util.inspect(object, {depth:null, colors: true})); 7 | } 8 | 9 | var param1 = { 10 | host: '172.28.128.4', 11 | username: 'vagrant', 12 | pkey: fs.readFileSync('insecure_ssh.key', {encoding: 'utf8'}) 13 | }; 14 | var param2 = { 15 | host: '172.28.128.3', 16 | username: 'vagrant', 17 | pkey: fs.readFileSync('insecure_ssh.key', {encoding: 'utf8'}) 18 | }; 19 | var router1 = new netconf.Client(param1); 20 | var router2 = new netconf.Client(param2); 21 | 22 | var routers = [ router1, router2 ]; 23 | 24 | var rpcGet = { 25 | 'get-config': { 26 | source: { running: null }, 27 | filter: { configuration: { system: { 'host-name': null } } } 28 | } 29 | } 30 | 31 | routers.forEach(function (router) { 32 | router.open(function (err) { 33 | if (!err) { 34 | router.rpc(rpcGet, function(err, reply) { 35 | var hostname = reply.rpc_reply.data.configuration.system.host_name; 36 | router.rpc('get-route-information', function(err, reply) { 37 | console.log(`------------ ${hostname} -------------`); 38 | pprint(reply); 39 | router.close(); 40 | }); 41 | }); 42 | } else { 43 | throw(err); 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /examples/netconf-multiple.js: -------------------------------------------------------------------------------- 1 | var netconf = require('../lib/netconf'); 2 | var util = require('util'); 3 | var fs = require('fs'); 4 | 5 | // example of multiple async requests 6 | var results = 0; 7 | 8 | var params = { 9 | host: '172.28.128.3', 10 | username: 'vagrant', 11 | pkey: fs.readFileSync('insecure_ssh.key', {encoding: 'utf8'}) 12 | }; 13 | var router = new netconf.Client(params); 14 | 15 | router.open(function afterOpen(err) { 16 | if (!err) { 17 | console.log('request 1'); 18 | router.rpc('get-configuration', processResults); 19 | console.log('request 2'); 20 | router.rpc('get-arp-table-information', processResults); 21 | } else { 22 | throw err; 23 | } 24 | }); 25 | 26 | function processResults(err, reply) { 27 | console.log(util.inspect(reply, {depth:null, colors: true})); 28 | results += 1; 29 | if (results === 2) { 30 | router.close(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/pipeline.js: -------------------------------------------------------------------------------- 1 | module.exports.read = function (callback) { 2 | var data = ''; 3 | process.stdin.on('readable', function() { 4 | var chunk = process.stdin.read(); 5 | if (chunk != null) { 6 | data += chunk; 7 | } 8 | }).on('end', function() { 9 | callback(null, data); 10 | }).on('error', function(err) { 11 | callback(err, null); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /examples/render.js: -------------------------------------------------------------------------------- 1 | #!/opt/pkg/bin/node 2 | var fs = require('fs'); 3 | var process = require('process'); 4 | var hb = require('handlebars'); 5 | var pipeline = require('./pipeline'); 6 | 7 | function render(data, template_file) { 8 | fs.readFile(template_file, function (err, buffer) { 9 | if (!err) { 10 | var template = hb.compile(buffer.toString()); 11 | var result = template(data); 12 | console.log(result); 13 | } else { 14 | throw (err); 15 | } 16 | }); 17 | } 18 | 19 | pipeline.read(function (err, data) { 20 | if (!err) { 21 | render(JSON.parse(data), process.argv[2]); 22 | } else { 23 | throw (err); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /examples/set-data.txt: -------------------------------------------------------------------------------- 1 | set interfaces ge-0/0/0 unit 0 description "set using data from set.txt" 2 | set interfaces ge-0/0/1 unit 0 description "set using data from set.txt" 3 | -------------------------------------------------------------------------------- /examples/template.hb: -------------------------------------------------------------------------------- 1 | interfaces { 2 | ge-0/0/0 { 3 | unit 0 { 4 | description "{{descrip1}}" 5 | family inet { 6 | dhcp; 7 | } 8 | } 9 | } 10 | ge-0/0/1 { 11 | unit 0 { 12 | description "{{descrip2}}" 13 | family inet { 14 | dhcp; 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/netconf.js: -------------------------------------------------------------------------------- 1 | const ssh = require('ssh2'); 2 | const xml2js = require('xml2js'); 3 | const vasync = require('vasync'); 4 | 5 | const DELIM = ']]>]]>'; 6 | 7 | function objectHelper(name) { 8 | // Replaces characters that prevent dot-style object navigation. 9 | return name.replace(/-|:/g, '_'); 10 | } 11 | 12 | function createError(msg, type) { 13 | const err = new Error(msg); 14 | err.name = type; 15 | 16 | Error.captureStackTrace(err, createError); 17 | return err; 18 | } 19 | 20 | function Client(params) { 21 | // Constructor paramaters 22 | this.host = params.host; 23 | this.username = params.username; 24 | this.port = params.port || 22; 25 | this.password = params.password; 26 | this.pkey = params.pkey; 27 | 28 | // Debug and informational 29 | this.connected = false; 30 | this.sessionID = null; 31 | this.remoteCapabilities = [ ]; 32 | this.idCounter = 100; 33 | this.rcvBuffer = ''; 34 | this.debug = params.debug; 35 | 36 | // Runtime option tweaks 37 | this.raw = false; 38 | this.parseOpts = { 39 | trim: true, 40 | explicitArray: false, 41 | emptyTag: true, 42 | ignoreAttrs: false, 43 | tagNameProcessors: [ objectHelper ], 44 | attrNameProcessors: [ objectHelper ], 45 | valueProcessors: [ xml2js.processors.parseNumbers ], 46 | attrValueProcessors: [ xml2js.processors.parseNumbers ] 47 | }; 48 | this.algorithms = params.algorithms 49 | } 50 | Client.prototype = { 51 | // Message and transport functions. 52 | // Operation functions defined below as wrappers to rpc function. 53 | rpc: function (request, callback) { 54 | const messageID = this.idCounter += 1; 55 | 56 | const object = { }; 57 | const defaultAttr = { 58 | 'message-id': messageID, 59 | 'xmlns': 'urn:ietf:params:xml:ns:netconf:base:1.0' 60 | }; 61 | if (typeof (request) === 'string') { 62 | object.rpc = { 63 | $: defaultAttr, 64 | [request]: null 65 | }; 66 | } else if (typeof (request) === 'object') { 67 | object.rpc = request; 68 | if (object.rpc.$) { 69 | object.rpc.$['message-id'] = messageID; 70 | } else { 71 | object.rpc.$ = defaultAttr; 72 | } 73 | } 74 | 75 | const builder = new xml2js.Builder({ headless: false, allowEmpty: true }); 76 | let xml; 77 | try { 78 | xml = builder.buildObject(object) + '\n' + DELIM; 79 | } catch (err) { 80 | return callback(err); 81 | } 82 | this.send(xml, messageID, callback); 83 | }, 84 | send: function (xml, messageID, callback) { 85 | const self = this; 86 | this.netconf.write(xml, function startReplyHandler() { 87 | const rpcReply = new RegExp(`()\\n?]]>]]>\\s*`); 88 | // Add an event handler to search for our message on data events. 89 | self.netconf.on('data', function replyHandler() { 90 | const replyFound = self.rcvBuffer.search(rpcReply) !== -1; 91 | 92 | if (replyFound) { 93 | const message = self.rcvBuffer.match(rpcReply); 94 | self.parse(message[1], callback); 95 | // Tidy up, remove matched message from buffer and 96 | // remove this messages replyHandler. 97 | self.rcvBuffer = self.rcvBuffer.replace(message[0], ''); 98 | self.netconf.removeListener('data', replyHandler); 99 | } 100 | }); 101 | }); 102 | }, 103 | parse: function (xml, callback) { 104 | const self = this; 105 | xml2js.parseString(xml, this.parseOpts, function checkRPCErrors(err, message) { 106 | if (err) { 107 | return callback(err, null); 108 | } 109 | if (message.hasOwnProperty('hello')) { 110 | return callback(null, message); 111 | } 112 | if (self.raw) { 113 | message.raw = xml; 114 | } 115 | if (message.rpc_reply.hasOwnProperty('rpc_error')) { 116 | return callback(createError(JSON.stringify(message), 'rpcError') , null); 117 | } 118 | return callback(null, message); 119 | }); 120 | }, 121 | open: function (callback) { 122 | const self = this; 123 | this.sshConn = ssh.Client(); 124 | this.sshConn.on('ready', function invokeNETCONF() { 125 | vasync.waterfall([ 126 | function getStream(next) { 127 | self.sshConn.subsys('netconf', next); 128 | }, 129 | function handleStream(stream, next) { 130 | self.netconf = stream; 131 | self.sendHello(); 132 | stream.on('data', function buffer(chunk) { 133 | self.rcvBuffer += chunk; 134 | self.emit('data'); 135 | }).on('error', function streamErr(err) { 136 | self.sshConn.end(); 137 | self.connected = false; 138 | self.emit('error'); 139 | throw (err); 140 | }).on('close', function handleClose() { 141 | self.sshConn.end(); 142 | self.connected = false; 143 | self.emit('close'); 144 | }).on('data', function handleHello() { 145 | if (self.rcvBuffer.match(DELIM)) { 146 | const helloMessage = self.rcvBuffer.replace(DELIM, ''); 147 | self.rcvBuffer = ''; 148 | self.netconf.removeListener('data', handleHello); 149 | next(null, helloMessage); 150 | } 151 | }); 152 | }, 153 | function parseHello(helloMessage, next) { 154 | self.parse(helloMessage, function assignSession(err, message) { 155 | if (err) { 156 | return next(err); 157 | } 158 | if (message.hello.session_id > 0) { 159 | self.remoteCapabilities = message.hello.capabilities.capability; 160 | self.sessionID = message.hello.session_id; 161 | self.connected = true; 162 | next(null); 163 | } else { 164 | next(new Error('NETCONF session not established')); 165 | } 166 | }); 167 | } 168 | ], 169 | function (err) { 170 | if (err) { 171 | return callback(err); 172 | } 173 | return callback(null); 174 | }); 175 | }).on('error', function (err) { 176 | self.connected = false; 177 | callback(err); 178 | }).connect({ 179 | host: this.host, 180 | username: this.username, 181 | password: this.password, 182 | port: this.port, 183 | privateKey: this.pkey, 184 | debug: this.debug, 185 | algorithms: this.algorithms 186 | }); 187 | 188 | return self; 189 | }, 190 | sendHello: function () { 191 | const message = { 192 | hello: { 193 | $: { xmlns: 'urn:ietf:params:xml:ns:netconf:base:1.0' }, 194 | capabilities: { 195 | capability: ['urn:ietf:params:xml:ns:netconf:base:1.0','urn:ietf:params:netconf:base:1.0'] 196 | } 197 | } 198 | }; 199 | const builder = new xml2js.Builder(); 200 | const xml = builder.buildObject(message) + '\n' + DELIM; 201 | this.netconf.write(xml); 202 | } 203 | }; 204 | 205 | // Operation layer. Wrappers around RPC calls. 206 | Client.prototype.close = function (callback) { 207 | this.rpc('close-session', function closeSocket(err, reply) { 208 | if (!callback) { 209 | return; 210 | } 211 | if (err) { 212 | return callback(err, reply); 213 | } 214 | return callback(null, reply); 215 | }); 216 | }; 217 | 218 | // Cisco specific operations. 219 | Client.prototype.IOSClose = function (callback) { // Cisco does not send a disconnect so you have to submit something after you close the session. Model: WS-C2960S-48FPD-L SW Version: 12.2(58)SE2 220 | const self = this; 221 | this.rpc('close-session', function closeSocket(err, reply) { 222 | self.rpc('close-session', function closeSocket(err, reply) { 223 | return callback(null, reply); 224 | }); 225 | }); 226 | }; 227 | 228 | // Juniper specific operations. 229 | Client.prototype.JunosLoad = function (args, callback) { 230 | let loadOpts = { }; 231 | if (typeof (args) === 'string') { // Backwards compatible with 0.1.0 232 | loadOpts = { config: args, action: 'merge', format: 'text' }; 233 | } else if (typeof (args) === 'object') { 234 | loadOpts = { 235 | config: args.config, 236 | action: args.action || 'merge', 237 | format: args.format || 'text' 238 | }; 239 | } 240 | 241 | if (typeof (loadOpts.config) === 'undefined') { 242 | return callback(new Error('configuraton undefined'), null); 243 | } 244 | 245 | let configTag; 246 | if (loadOpts.action === 'set') { 247 | configTag = 'configuration-set'; 248 | } else if (loadOpts.format === 'xml') { 249 | configTag = 'configuration'; 250 | } else { 251 | configTag = 'configuration-text'; 252 | } 253 | 254 | const rpcLoad = { 255 | 'load-configuration': { 256 | $: { action: loadOpts.action, format: loadOpts.format }, 257 | [configTag]: loadOpts.config 258 | } 259 | }; 260 | this.rpc(rpcLoad, function checkErrors(err, reply) { 261 | if (err) { 262 | return callback(err, reply); 263 | } 264 | // Load errors aren't found in the top-level reply so need to check seperately. 265 | const rpcError = reply.rpc_reply.load_configuration_results.hasOwnProperty('rpc_error'); 266 | if (rpcError) { 267 | return callback(createError(JSON.stringify(reply), 'rpcError'), null); 268 | } 269 | return callback(null, reply); 270 | }); 271 | }; 272 | Client.prototype.JunosCommit = function (callback) { 273 | this.rpc('commit-configuration', function checkErrors(err, reply) { 274 | if (err) { 275 | return callback(err, reply); 276 | } 277 | // Load errors aren't found in the top-level reply so need to check seperately. 278 | const rpcError = result.rpc_reply.commit_results.routing_engine.hasOwnProperty('rpc_error'); 279 | if (rpcError) { 280 | return callback(createError(JSON.stringify(reply), 'rpcError'), null); 281 | } 282 | return callback(null, reply); 283 | }); 284 | }; 285 | Client.prototype.JunosOpenPrivate = function (callback) { 286 | const rpcOpen = { 287 | 'open-configuration': {'private' : ""} 288 | }; 289 | this.rpc(rpcOpen, callback); 290 | }; 291 | Client.prototype.JunosClosePrivate = function (callback) { 292 | this.rpc('close-configuration', callback); 293 | }; 294 | Client.prototype.JunosCompare = function (callback) { 295 | const rpcCompare = { 296 | 'get-configuration': { 297 | $: { compare: 'rollback', format: 'text' } 298 | } 299 | }; 300 | this.rpc(rpcCompare, function parseDiff(err, reply) { 301 | if (err) { 302 | return callback(err, reply); 303 | } 304 | const text = reply.rpc_reply.configuration_information.configuration_output; 305 | return callback(null, text); 306 | }); 307 | }; 308 | Client.prototype.JunosRollback = function (callback) { 309 | this.rpc('discard-changes', callback); 310 | }; 311 | Client.prototype.JunosFacts = function (callback) { 312 | const self = this; 313 | vasync.parallel({ 314 | funcs: [ 315 | function getSoftwareInfo(callback) { 316 | self.rpc('get-software-information', callback); 317 | }, 318 | function getRE(callback) { 319 | self.rpc('get-route-engine-information', callback); 320 | }, 321 | function getChassis(callback) { 322 | self.rpc('get-chassis-inventory', callback); 323 | } 324 | ] 325 | }, function compileResults(err, results) { 326 | if (err) { 327 | return callback(err, null); 328 | } 329 | const softwareInfo = results.operations[0].result.rpc_reply.software_information; 330 | const reInfo = results.operations[1].result.rpc_reply.route_engine_information.route_engine; 331 | const chassisInfo = results.operations[2].result.rpc_reply.chassis_inventory.chassis; 332 | const facts = { 333 | hostname: softwareInfo.host_name, 334 | version: softwareInfo.package_information, 335 | model: softwareInfo.product_model, 336 | uptime: reInfo.up_time, 337 | serial: chassisInfo.serial_number 338 | }; 339 | return callback(null, facts); 340 | }); 341 | }; 342 | 343 | module.exports.Client = Client; 344 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netconf", 3 | "version": "2.0.1", 4 | "description": "Pure JavaScript NETCONF library.", 5 | "main": "lib/netconf.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/darylturner/node-netconf.git" 15 | }, 16 | "keywords": [ 17 | "xml-rpc", 18 | "netconf", 19 | "juniper" 20 | ], 21 | "author": { 22 | "name": "Daryl Turner", 23 | "email": "daryl@layer-eight.uk" 24 | }, 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/darylturner/node-netconf/issues" 28 | }, 29 | "dependencies": { 30 | "ssh2": "^0.4.12", 31 | "xml2js": "^0.4.15", 32 | "vasync": "^1.6.3" 33 | }, 34 | "homepage": "https://github.com/darylturner/node-netconf#readme" 35 | } 36 | -------------------------------------------------------------------------------- /test/core.js: -------------------------------------------------------------------------------- 1 | var netconf = require('../lib/netconf'); 2 | var fs = require('fs'); 3 | var assert = require('assert'); 4 | 5 | var testServ = { 6 | host: '172.28.128.3', 7 | username: 'vagrant', 8 | pkey: fs.readFileSync('../examples/insecure_ssh.key', { encoding: 'utf8' }) 9 | }; 10 | var client; 11 | 12 | describe('core functions', function () { 13 | // before(function vagrantStart() { ... }); 14 | before(function openConnection(done) { 15 | client = new netconf.Client(testServ); 16 | client.open(done); 17 | }); 18 | it('should establish connection to server', function () { 19 | assert.ok(client.connected); 20 | }); 21 | it('should receive remote capabilities', function () { 22 | assert.ok(client.remoteCapabilities.length); 23 | }); 24 | it('should be assigned a session id', function () { 25 | assert.ok(client.sessionID > 0); 26 | }); 27 | it('should be able to send and receive rpc messages', function (done) { 28 | client.rpc('get-software-information', function (err, reply) { 29 | if (err) return done(err); 30 | var platform = reply.rpc_reply.software_information.package_information.name; 31 | return done(assert.ok(platform === 'junos')); 32 | }); 33 | }); 34 | it('should be able to send and receive simultaneous rpc messages', function(done) { 35 | var interfaces = [ 'ge-0/0/0', 'ge-0/0/1' ]; 36 | var results = 0; 37 | interfaces.forEach(function (int) { 38 | client.rpc({ 'get-interface-information': { 'interface-name': [ int ], 'media': null } }, 39 | function (err, reply) { 40 | if (err) return done(err); 41 | results += 1; 42 | try { 43 | var returnedInt = reply.rpc_reply.interface_information.physical_interface.name; 44 | assert.ok(returnedInt === int); 45 | } catch (e) { 46 | return done(e); 47 | } 48 | if (results === interfaces.length) { 49 | done(); 50 | } 51 | } 52 | ); 53 | }); 54 | }); 55 | it('should raise a rpcError for bad RPC methods', function (done) { 56 | client.rpc('get-foobarbaz', (err, reply) => { 57 | assert.ok(!reply, 'reply should be empty'); 58 | if (err) { 59 | assert.ok(err.name === 'rpcError', err); 60 | } else { 61 | return done(Error('rpcError not found on bad method')); 62 | } 63 | return done(); 64 | }); 65 | }); 66 | after(function closeConnection(done) { 67 | client.close(done); 68 | }); 69 | // after(function vagrantStop() { ... }); 70 | }); 71 | --------------------------------------------------------------------------------