├── .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 |
--------------------------------------------------------------------------------