Repository: couchdb-nmo
Updated Branches:
  refs/heads/master 0d89e6389 -> 308a4ada0


add new command couch-config get/set

Get - will get all the configs settings for all nodes in a cluster and
display them for easy viewing

Set - will set the config for all nodes in a cluster


Project: http://git-wip-us.apache.org/repos/asf/couchdb-nmo/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-nmo/commit/320105fe
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-nmo/tree/320105fe
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-nmo/diff/320105fe

Branch: refs/heads/master
Commit: 320105fe93c74362ec56a3968f607c56bdc1d61f
Parents: 0d89e63
Author: Garren Smith <[email protected]>
Authored: Tue Sep 8 12:36:07 2015 +0200
Committer: Garren Smith <[email protected]>
Committed: Wed Sep 9 17:43:56 2015 +0200

----------------------------------------------------------------------
 doc/api/nmo-couch-config.md |  21 +++
 doc/cli/nmo-couch-config.md |  26 ++++
 package.json                |   1 +
 src/couch-config.js         | 159 +++++++++++++++++++++++
 src/nmo.js                  |   3 +-
 test/couch-config.js        | 274 +++++++++++++++++++++++++++++++++++++++
 6 files changed, 483 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-nmo/blob/320105fe/doc/api/nmo-couch-config.md
----------------------------------------------------------------------
diff --git a/doc/api/nmo-couch-config.md b/doc/api/nmo-couch-config.md
new file mode 100644
index 0000000..45be4e2
--- /dev/null
+++ b/doc/api/nmo-couch-config.md
@@ -0,0 +1,21 @@
+nmo-config(3) -- configuration
+==============================
+
+## SYNOPSIS
+
+    nmo.commands.couch-config.set(cluster, nodes, section, key, value)
+    nmo.commands.couch-config.get(cluster, nodes, [section])
+
+
+
+## DESCRIPTION
+
+Manage the nmo configuration.
+
+  - set:
+
+Sets the value for a key of a CouchDB config section for each node in a 
cluster.
+
+  - get:
+
+Gets the config for each node in a cluster and displays it for easy viewing

http://git-wip-us.apache.org/repos/asf/couchdb-nmo/blob/320105fe/doc/cli/nmo-couch-config.md
----------------------------------------------------------------------
diff --git a/doc/cli/nmo-couch-config.md b/doc/cli/nmo-couch-config.md
new file mode 100644
index 0000000..a075084
--- /dev/null
+++ b/doc/cli/nmo-couch-config.md
@@ -0,0 +1,26 @@
+nmo-couch-config(1) -- Set/Get couch configuration for a cluster
+=================================
+
+## SYNOPSIS
+
+    nmo couch-config get <cluster> [<key>][--json]
+    nmo couch-config set <cluster> <section> <key> <value>
+
+## DESCRIPTION
+
+- get:
+
+Gets the set configuration for the whole cluster or a specified node.
+If a key is specified it will only get the configuration for that section
+
+- set:
+
+Set the value for a given key of a section of the config. This will update the 
config for all nodes in a cluster.
+The cluster must be specified in the .nmorc file.
+
+## EXAMPLE
+
+    nmo couch-config get mycluster
+    nmo couch-config get mycluster couch_httpd_auth
+
+    nmo couch-config set mycluster uuids max_count 2000

http://git-wip-us.apache.org/repos/asf/couchdb-nmo/blob/320105fe/package.json
----------------------------------------------------------------------
diff --git a/package.json b/package.json
index ce4dad3..079e5f3 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
     "nopt": "~3.0.1",
     "npmlog": "~1.2.0",
     "osenv": "~0.1.0",
+    "prettyjson": "^1.1.3",
     "valid-url": "~1.0.9",
     "wreck": "~5.6.0",
     "xtend": "~4.0.0"

http://git-wip-us.apache.org/repos/asf/couchdb-nmo/blob/320105fe/src/couch-config.js
----------------------------------------------------------------------
diff --git a/src/couch-config.js b/src/couch-config.js
new file mode 100644
index 0000000..4bc2733
--- /dev/null
+++ b/src/couch-config.js
@@ -0,0 +1,159 @@
+import * as utils from './utils.js';
+import log from 'npmlog';
+import Wreck from 'wreck';
+import Promise from 'bluebird';
+import prettyjson from 'prettyjson';
+import nmo from './nmo.js';
+import isonline, { getClusterUrls } from './isonline.js';
+
+export function cli (cmd, cluster, section, key, value) {
+  return new Promise((resolve, reject) => {
+
+    if (!cmd || !cluster || !exports[cmd]) {
+      const err = new Error('Usage: nmo couch-config add/set <cluster> ...');
+      err.type = 'EUSAGE';
+      return reject(err);
+    }
+
+    exports[cmd].apply(exports[cmd], [cluster, getClusterNodes(cluster), 
section, key, value])
+      .then(resolve)
+      .catch(reject);
+  });
+}
+
+export function getClusterNodes (clusterName) {
+  const nodes = nmo.config.get(clusterName);
+  if (!nodes) {
+    const err = new Error('Cluster does not exist');
+    err.type = 'EUSAGE';
+    throw err;
+  }
+
+  return nodes;
+}
+
+export function get (cluster, nodes, section) {
+  const promise = Promise.reduce(Object.keys(nodes), (obj, node) => {
+    const url = buildConfigUrl(node, nodes[node], section);
+    return getConfig(node, url).then(({node, config}) => {
+      obj[node] = config;
+      return obj;
+    });
+  }, {});
+
+  promise.then((nodeConfigs) => {
+    Object.keys(nodeConfigs).forEach(node => {
+      console.log('NODE:', node);
+      console.log(prettyjson.render(nodeConfigs[node], {}));
+    });
+  });
+
+  return promise;
+}
+
+export function set(cluster, nodes, section, key, value) {
+  const urls = getClusterUrls(cluster);
+  return isonline.apply(isonline, urls).then(results => {
+    const offline = Object.keys(results).filter(node => {
+      if (!results[node]) {
+        return true;
+      }
+
+      return false
+    });
+
+    if (offline.length > 0) {
+      const msg = offline.map(node => 'Node ' + offline + ' is 
offline.').join('');
+      const err = new Error(msg);
+      err.type = 'EUSAGE';
+      throw err;
+    }
+
+    const promises = Object.keys(nodes).map(node => {
+      return setConfig(node, buildConfigUrl(node, nodes[node], section, key), 
value);
+    });
+
+    const allPromise = Promise.all(promises);
+    allPromise
+      .then((resp) => {
+        console.log(prettyjson.render(resp));
+      })
+      .catch((err) => {
+        throw err;
+      });
+
+    return allPromise;
+  });
+}
+
+export function setConfig (node, url, value) {
+  return new Promise((resolve, reject) => {
+    let er = utils.checkUrl(url);
+
+    if (!er && !/^(http:|https:)/.test(url)) {
+      er = new Error('invalid protocol, must be https or http');
+    }
+
+    if (er) {
+      er.type = 'EUSAGE';
+      return reject(er);
+    }
+    const cleanedUrl = utils.removeUsernamePw(url);
+    log.http('request', 'PUT', cleanedUrl);
+
+    Wreck.put(url, {payload: JSON.stringify(value)}, (err, res, payload) => {
+      if (err) {
+        const error = new Error('Error on set config for node ' + node + ' ' + 
err);
+        error.type = 'EUSAGE';
+        return reject(error);
+      }
+
+      log.http(res.statusCode, cleanedUrl);
+      resolve({
+        node: node,
+        oldvalue: JSON.parse(payload),
+        newvalue: value
+      });
+    });
+  });
+}
+
+export function buildConfigUrl (node, url, section, key) {
+  let configUrl = url + '/_node/' + node + '/_config';
+
+  if (section) {
+    configUrl += '/' + section;
+  }
+
+  if (key) {
+    configUrl += '/' + key;
+  }
+
+  return configUrl;
+}
+
+export function getConfig (node, url) {
+  return new Promise((resolve, reject) => {
+    let er = utils.checkUrl(url);
+
+    if (!er && !/^(http:|https:)/.test(url)) {
+      er = new Error('invalid protocol, must be https or http');
+    }
+
+    if (er) {
+      er.type = 'EUSAGE';
+      return reject(er);
+    }
+    const cleanedUrl = utils.removeUsernamePw(url);
+    log.http('request', 'GET', cleanedUrl);
+
+    Wreck.get(url, (err, res, payload) => {
+      if (err) {
+        return reject(err);
+      }
+
+      log.http(res.statusCode, cleanedUrl);
+      resolve({node: node, config: JSON.parse(payload)});
+    });
+  });
+}

http://git-wip-us.apache.org/repos/asf/couchdb-nmo/blob/320105fe/src/nmo.js
----------------------------------------------------------------------
diff --git a/src/nmo.js b/src/nmo.js
index c60c038..60309e6 100644
--- a/src/nmo.js
+++ b/src/nmo.js
@@ -8,7 +8,8 @@ const commands = [
   'config',
   'cluster',
   'v',
-  'import-csv'
+  'import-csv',
+  'couch-config'
 ];
 
 const nmo = {

http://git-wip-us.apache.org/repos/asf/couchdb-nmo/blob/320105fe/test/couch-config.js
----------------------------------------------------------------------
diff --git a/test/couch-config.js b/test/couch-config.js
new file mode 100644
index 0000000..a533eb7
--- /dev/null
+++ b/test/couch-config.js
@@ -0,0 +1,274 @@
+import assert from 'assert';
+
+import Lab from 'lab';
+export const lab = Lab.script();
+import nock from 'nock';
+import nmo from '../src/nmo.js';
+import {cli, setConfig, getClusterNodes, buildConfigUrl, getConfig, get, set} 
from '../src/couch-config.js';
+
+lab.experiment('couch-config', () => {
+  lab.beforeEach((done) => {
+    nmo
+      .load({nmoconf: __dirname + '/fixtures/randomini'})
+      .then(() => done())
+      .catch(() => done());
+  });
+
+  lab.experiment('cli', () => {
+
+    lab.test('no arguments', (done) => {
+
+      cli().catch(err => {
+        assert.ok(/Usage/.test(err.message));
+        done();
+      });
+
+    });
+
+    lab.test('non-existing command', (done) => {
+
+      cli('wrong', 'command').catch(err => {
+        assert.ok(/Usage/.test(err.message));
+        done();
+      });
+
+    });
+
+    lab.test('error on missing cluster', (done) => {
+
+      cli('get').catch(err => {
+        assert.ok(/Usage/.test(err.message));
+        done();
+      });
+
+    });
+
+    lab.test('error on non-existing cluster', (done) => {
+
+      cli('get', 'not-exist').catch(err => {
+        assert.ok(/Cluster/.test(err.message));
+        done();
+      });
+
+    });
+
+  });
+
+  lab.experiment('api', () => {
+
+    lab.test('getClusterNodes returns existing nodes', (done) => {
+      const nodes = getClusterNodes('clusterone');
+
+      assert.deepEqual(nodes, {
+        node0: 'http://127.0.0.1',
+        node1: 'http://192.168.0.1'
+      });
+
+      done();
+    });
+
+    lab.test('buildConfigUrl builds correctly with node and url', (done) => {
+      const url = buildConfigUrl('node', 'http://127.0.0.1');
+      assert.deepEqual(url, 'http://127.0.0.1/_node/node/_config');
+      done();
+    });
+
+    lab.test('buildConfigUrl builds correctly with node, url and section', 
(done) => {
+      const url = buildConfigUrl('node', 'http://127.0.0.1', 'a-section');
+      assert.deepEqual(url, 'http://127.0.0.1/_node/node/_config/a-section');
+      done();
+    });
+
+    lab.test('getConfig throws error on bad url', (done) => {
+      getConfig('node1', 'bad-url')
+      .catch(err => {
+        assert.ok(/not a valid url/.test(err.message));
+        done();
+      });
+    });
+
+    lab.test('getConfig throws error on invalid protocol', (done) => {
+      getConfig('node1', 'ftp://bad-url')
+      .catch(err => {
+        assert.ok(/invalid protocol/.test(err.message));
+        done();
+      });
+    });
+
+    lab.test('gets config bad url returns false', (done) => {
+      getConfig('node1', 'http://127.0.0.2/')
+      .catch(err => {
+        assert.deepEqual(err.code, 'ECONNREFUSED');
+        done();
+      });
+    });
+
+    lab.test('gets config for node', (done) => {
+      const resp = {
+        config1: 'hello',
+        config2: 'boom'
+      };
+
+      nock('http://127.0.0.1')
+        .get('/_node/node1/_config/uuid')
+        .reply(200, resp);
+
+      getConfig('node1', 'http://127.0.0.1/_node/node1/_config/uuid')
+      .then(config => {
+        assert.deepEqual(config, {
+          node: 'node1',
+          config: resp
+        });
+        done();
+      });
+    });
+
+  });
+
+  lab.experiment('get cmd', () => {
+    let oldConsole = console.log;
+
+    lab.afterEach(done => {
+      console.log = oldConsole;
+      done();
+    });
+
+    lab.test('get returns config', (done) => {
+      const nodes = {
+        node1: 'http://127.0.0.1'
+      };
+
+      const resp = {
+        config1: 'hello',
+        config2: 'boom'
+      };
+
+      nock('http://127.0.0.1')
+        .get('/_node/node1/_config/uuid')
+        .reply(200, resp);
+
+      get('cluster', nodes, 'uuid')
+      .then(config => {
+        assert.deepEqual(config, {
+          node1: {
+          config1: 'hello',
+          config2: 'boom'
+        }
+       });
+       done();
+      });
+    });
+
+    lab.test('get prints config', (done) => {
+      const nodes = {
+        node1: 'http://127.0.0.1'
+      };
+
+      const resp = {
+        config1: 'hello',
+        config2: 'boom'
+      };
+
+      nock('http://127.0.0.1')
+        .get('/_node/node1/_config/uuid')
+        .reply(200, resp);
+
+      console.log = (msg) => {
+        if (/NODE:/.test(msg)) {
+          return;
+        }
+
+        assert.ok(/config1/.test(msg));
+        assert.ok(/config2/.test(msg));
+
+        done();
+      };
+
+      get('cluster', nodes, 'uuid');
+    });
+  });
+
+  lab.experiment('set cmd', () => {
+
+    lab.test('returns error if all nodes are not online', done => {
+      nock('http://127.0.0.1')
+      .get('/')
+      .reply(500);
+
+      nock('http://192.168.0.1')
+      .get('/')
+      .reply(500);
+
+      set('clusterone', 'nodes', 'section', 'key', 'value')
+      .catch(err => {
+        console.log('ERR', err);
+        assert.ok(/is offline/.test(err.message));
+        done();
+      });
+
+    });
+
+    lab.test('sets config on all nodes for cluster', done => {
+      //isonline
+      nock('http://127.0.0.1')
+      .get('/')
+      .reply(200);
+
+      nock('http://192.168.0.1')
+      .get('/')
+      .reply(200);
+
+      //config update
+      nock('http://127.0.0.1')
+      .put('/_node/node0/_config/section/key', JSON.stringify('value'))
+      .reply(200, JSON.stringify("oldvalue"));
+
+      nock('http://192.168.0.1')
+      .put('/_node/node1/_config/section/key', JSON.stringify('value'))
+      .reply(200, JSON.stringify("oldvalue"));
+
+      set('clusterone', getClusterNodes('clusterone'), 'section', 'key', 
'value')
+      .then(resp => {
+        assert.deepEqual(resp, [
+          { node: 'node0', oldvalue: 'oldvalue', newvalue: 'value' },
+          { node: 'node1', oldvalue: 'oldvalue', newvalue: 'value' } ]);
+        done();
+      });
+
+    });
+
+    lab.test('sets config throws error', done => {
+      //isonline
+      nock('http://127.0.0.1')
+      .get('/')
+      .reply(200);
+
+      nock('http://192.168.0.1')
+      .get('/')
+      .reply(200);
+
+      //config update
+      nock('http://127.0.0.1')
+      .put('/_node/node0/_config/section/key', JSON.stringify('value'))
+      .reply(200, JSON.stringify("oldvalue"));
+
+      set('clusterone', getClusterNodes('clusterone'), 'section', 'key', 
'value')
+      .catch(err => {
+        assert.ok(/Error on set config for node/.test(err.message));
+        done();
+      });
+
+    });
+
+    lab.test('setsConfig warns on incorrect url', done => {
+
+      setConfig('node1', 'ftp://127.0.0.1', 'section', 'key', 'value')
+      .catch(err => {
+        assert.ok(/invalid protocol/.test(err.message));
+        done();
+      });
+
+    });
+
+  });
+});

Reply via email to