From da5f3bade8398c02553c75de1dcbfd14a1814825 Mon Sep 17 00:00:00 2001 From: Mike Zeller Date: Tue, 24 Oct 2017 09:32:39 -0700 Subject: [PATCH] PUBAPI-1452 Add ListNetwork IP and GetNetworkIP to node-triton Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 5 + lib/cloudapi2.js | 34 ++++++ lib/do_network/do_ip/do_get.js | 81 +++++++++++++++ lib/do_network/do_ip/do_list.js | 106 +++++++++++++++++++ lib/do_network/do_ip/index.js | 49 +++++++++ lib/do_network/index.js | 4 +- lib/tritonapi.js | 107 +++++++++++++++++++ package.json | 2 +- test/integration/api-ips.test.js | 104 +++++++++++++++++++ test/integration/cli-ips.test.js | 173 +++++++++++++++++++++++++++++++ 10 files changed, 663 insertions(+), 2 deletions(-) create mode 100644 lib/do_network/do_ip/do_get.js create mode 100644 lib/do_network/do_ip/do_list.js create mode 100644 lib/do_network/do_ip/index.js create mode 100644 test/integration/api-ips.test.js create mode 100644 test/integration/cli-ips.test.js diff --git a/CHANGES.md b/CHANGES.md index 1580b03..ff1850f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,11 @@ Known issues: (nothing yet) +## 5.5.0 + +- [PUBAPI-1452] Add ip subcommand to network, e.g. + `triton network ip`. + ## 5.4.0 - [joyent/node-triton#74, TOOLS-1872] Filter instance list by tag, e.g. diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 78fd4a6..a409e18 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -386,7 +386,41 @@ CloudApi.prototype.getNetwork = function getNetwork(id, cb) { }); }; +/** + * + * + * @param {String} - UUID + * @param {Function} callback of the form `function (err, ips, res)` + */ +CloudApi.prototype.listNetworkIps = function listNetworkIps(id, cb) { + assert.uuid(id, 'id'); + assert.func(cb, 'cb'); + var endpoint = this._path(format('/%s/networks/%s/ips', this.account, id)); + this._request(endpoint, function (err, req, res, body) { + cb(err, body, res); + }); +}; + +/** + * + * + * @param {Object} opts + * - {String} opts.id The network UUID, name, or shortID. Required. + * - {String} opts.ip The IP. Required. + * @param {Function} callback of the form `function (err, ip, res)` + */ +CloudApi.prototype.getNetworkIp = function getNetworkIp(opts, cb) { + assert.uuid(opts.id, 'id'); + assert.string(opts.ip, 'ip'); + assert.func(cb, 'cb'); + + var endpoint = this._path(format('/%s/networks/%s/ips/%s', + this.account, opts.id, opts.ip)); + this._request(endpoint, function (err, req, res, body) { + cb(err, body, res); + }); +}; // ---- datacenters diff --git a/lib/do_network/do_ip/do_get.js b/lib/do_network/do_ip/do_get.js new file mode 100644 index 0000000..9c66cac --- /dev/null +++ b/lib/do_network/do_ip/do_get.js @@ -0,0 +1,81 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2017 Joyent, Inc. + * + * `triton network ip get ...` + */ + +var format = require('util').format; + +var common = require('../../common'); +var errors = require('../../errors'); + +function do_get(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } else if (args.length !== 2) { + return cb(new errors.UsageError(format( + 'incorrect number of args (%d)', args.length))); + } + + var tritonapi = this.top.tritonapi; + + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + return; + } + + var getIpOpts = { + id: args[0], + ip: args[1] + }; + + tritonapi.getNetworkIp(getIpOpts, function (err, ip, res) { + if (err) { + return cb(err); + } + + if (opts.json) { + console.log(JSON.stringify(ip)); + } else { + console.log(JSON.stringify(ip, null, 4)); + } + cb(); + }); + }); +} + +do_get.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON output.' + } +]; + +do_get.synopses = ['{{name}} {{cmd}} NETWORK IP']; + +do_get.help = [ + 'Show a network ip.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where NETWORK is a network id, and IP is the ip address you want to get.' +].join('\n'); + +do_get.completionArgtypes = ['tritonnetwork', 'tritonnetworkip', 'none']; + +module.exports = do_get; diff --git a/lib/do_network/do_ip/do_list.js b/lib/do_network/do_ip/do_list.js new file mode 100644 index 0000000..854045d --- /dev/null +++ b/lib/do_network/do_ip/do_list.js @@ -0,0 +1,106 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2017 Joyent, Inc. + * + * `triton network ip list ...` + */ + +var format = require('util').format; + +var tabula = require('tabula'); +var vasync = require('vasync'); + +var common = require('../../common'); +var errors = require('../../errors'); + + +// columns default without -o +var columnsDefault = 'ip,managed,reserved,owner_uuid,belongs_to_uuid'; + +// sort default with -s +var sortDefault = 'ip'; + +function do_list(subcmd, opts, args, callback) { + var self = this; + if (opts.help) { + this.do_help('help', {}, [subcmd], callback); + return; + } else if (args.length !== 1) { + return callback(new errors.UsageError(format( + 'incorrect number of args (%d)', args.length))); + } + + var columns = columnsDefault; + if (opts.o) { + columns = opts.o; + } + columns = columns.split(','); + + var sort = opts.s.split(','); + + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, + + function listIps(arg, next) { + self.top.tritonapi.listNetworkIps(args[0], + function (err, ips, res) { + if (err) { + next(err); + return; + } + arg.ips = ips; + next(); + }); + }, + + function doneIps(arg, next) { + var ips = arg.ips; + if (opts.json) { + common.jsonStream(ips); + } else { + tabula(ips, { + skipHeader: opts.H, + columns: columns, + sort: sort + }); + } + next(); + } + ]}, callback); +} + +do_list.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +].concat(common.getCliTableOptions({ + includeLong: true, + sortDefault: sortDefault +})); + +do_list.synopses = ['{{name}} {{cmd}} NETWORK']; + +do_list.help = [ + 'List network IPs.', + '', + '{{usage}}', + '', + '{{options}}', + 'Fields (most are self explanatory, the significant ones are as follows):', + ' managed IP is manged by Triton and cannot be modified directly.', + '', + 'See https://apidocs.joyent.com/cloudapi/#ListNetworkIPs for a full' + + ' listing.' +].join('\n'); + +do_list.aliases = ['ls']; +do_list.completionArgtypes = ['tritonnetwork', 'none']; + +module.exports = do_list; diff --git a/lib/do_network/do_ip/index.js b/lib/do_network/do_ip/index.js new file mode 100644 index 0000000..8a24488 --- /dev/null +++ b/lib/do_network/do_ip/index.js @@ -0,0 +1,49 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2017 Joyent, Inc. + * + * `triton network ip...` + */ + +var Cmdln = require('cmdln').Cmdln; +var util = require('util'); + + + +// ---- CLI class + +function IpCLI(top) { + this.top = top.top; + Cmdln.call(this, { + name: top.name + ' ip', + /* BEGIN JSSTYLED */ + desc: [ + 'List and manage Triton network IPs.' + ].join('\n'), + /* END JSSTYLED */ + helpOpts: { + minHelpCol: 24 /* line up with option help */ + }, + helpSubcmds: [ + 'help', + 'list', + 'get' + ] + }); +} +util.inherits(IpCLI, Cmdln); + +IpCLI.prototype.init = function init(opts, args, cb) { + this.log = this.top.log; + Cmdln.prototype.init.apply(this, arguments); +}; + +IpCLI.prototype.do_list = require('./do_list'); +IpCLI.prototype.do_get = require('./do_get'); + +module.exports = IpCLI; diff --git a/lib/do_network/index.js b/lib/do_network/index.js index e22c576..53d5e49 100644 --- a/lib/do_network/index.js +++ b/lib/do_network/index.js @@ -32,7 +32,8 @@ function NetworkCLI(top) { helpSubcmds: [ 'help', 'list', - 'get' + 'get', + 'ip' ] }); } @@ -45,6 +46,7 @@ NetworkCLI.prototype.init = function init(opts, args, cb) { NetworkCLI.prototype.do_list = require('./do_list'); NetworkCLI.prototype.do_get = require('./do_get'); +NetworkCLI.prototype.do_ip = require('./do_ip'); module.exports = NetworkCLI; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 517eedb..a0e8ea7 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -276,6 +276,32 @@ function _stepFwRuleId(arg, next) { } } +/** + * A function appropriate for `vasync.pipeline` funcs that takes a `arg.network` + * (or `arg.id` if there is no `arg.network`) network name, shortid or uuid, + * and determines the network id (setting it as `arg.netId`). + */ +function _stepNetId(arg, next) { + assert.object(arg.client, 'arg.client'); + + var id = arg.network || arg.id; + assert.string(id, 'arg.network || arg.id'); + + if (common.isUUID(id)) { + arg.netId = id; + next(); + } else { + arg.client.getNetwork(id, function (err, net) { + if (err) { + next(err); + } else { + arg.netId = net.id; + next(); + } + }); + } +} + //---- TritonApi class /** @@ -863,6 +889,87 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) { } }; +/** + * List an network's IPs. + * + * @param {String} name The network UUID, name, or short ID. Required. + * @param {Function} cb `function (err, ip, res)` + * On failure `err` is an error instance, else it is null. + * On success: `net` is an array of ip objects + */ +TritonApi.prototype.listNetworkIps = function listNetworkIps(name, cb) { + assert.string(name, 'name'); + assert.func(cb, 'cb'); + + var self = this; + var ipArray; + var res; + + vasync.pipeline({arg: {client: self, id: name}, funcs: [ + _stepNetId, + + function getIp(arg, next) { + self.cloudapi.listNetworkIps(arg.netId, + function (err, ips, _res) { + res = _res; + ipArray = ips; + + if (err && err.restCode === 'ResourceNotFound' && + err.exitStatus !== 3) { + // Wrap with *our* ResourceNotFound for exitStatus=3. + err = new errors.ResourceNotFoundError(err, + format('network with id %s was not found', name)); + } + next(err); + }); + } + ]}, function (err) { + cb(err, ipArray, res); + }); +}; + +/** + * Get an network IP. + * + * @param {Object} opts + * - {String} opts.id The network UUID, name, or shortID. Required. + * - {String} opts.ip The IP. Required. + * @param {Function} cb `function (err, ip, res)` + * On failure `err` is an error instance, else it is null. + * On success: `ip` is an ip object + */ +TritonApi.prototype.getNetworkIp = function getNetworkIp(opts, cb) { + assert.string(opts.id, 'id'); + assert.string(opts.ip, 'userIp'); + assert.func(cb, 'cb'); + + var self = this; + var ipObj; + var res; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepNetId, + + function getIp(arg, next) { + self.cloudapi.getNetworkIp({id: arg.netId, + ip: opts.ip}, function (err, ip, _res) { + res = _res; + ipObj = ip; + + if (err && err.restCode === 'ResourceNotFound' && + err.exitStatus !== 3) { + // Wrap with *our* ResourceNotFound for exitStatus=3. + err = new errors.ResourceNotFoundError(err, + format('network with id %s was not found', opts.id)); + } + next(err); + }); + } + ]}, function (err) { + cb(err, ipObj, res); + }); +}; + /** * Get an instance. diff --git a/package.json b/package.json index fc973f8..4a681c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "triton", "description": "Joyent Triton CLI and client (https://www.joyent.com/triton)", - "version": "5.4.0", + "version": "5.5.0", "author": "Joyent (joyent.com)", "homepage": "https://github.com/joyent/node-triton", "dependencies": { diff --git a/test/integration/api-ips.test.js b/test/integration/api-ips.test.js new file mode 100644 index 0000000..5609e26 --- /dev/null +++ b/test/integration/api-ips.test.js @@ -0,0 +1,104 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2017 Joyent, Inc. + */ + +/* + * Integration tests for using ip-related APIs as a module. + */ + +var h = require('./helpers'); +var test = require('tape'); + + +// --- Globals + +var CLIENT; +var NET; +var IP; + + +// --- Tests + +test('TritonApi network ips', function (tt) { + tt.test(' setup', function (t) { + h.createClient(function (err, client_) { + t.error(err); + CLIENT = client_; + t.end(); + }); + }); + + tt.test(' setup: net', function (t) { + var opts = { + account: CLIENT.profile.account + }; + CLIENT.cloudapi.listNetworks(opts, function (err, nets) { + if (h.ifErr(t, err)) + return t.end(); + + t.ok(Array.isArray(nets), 'networks'); + + // Array.find() is only in newer node versions + while (nets.length > 0) { + var elm = nets.shift(); + if (elm.fabric === true) { + NET = elm; + break; + } + } + t.ok(NET, 'fabric network required'); + + t.end(); + }); + }); + + tt.test(' TritonApi listIps', function (t) { + if (!NET) { + return t.end(); + } + + CLIENT.listNetworkIps(NET.id, function (err, ips) { + if (h.ifErr(t, err)) + return t.end(); + + t.ok(Array.isArray(ips), 'ips'); + + IP = ips[0]; + + t.end(); + }); + }); + + + tt.test(' TritonApi getIp', function (t) { + if (!NET || !IP) { + return t.end(); + } + + var opts = { + id: NET.id, + ip: IP.ip + }; + + CLIENT.getNetworkIp(opts, function (err, ip) { + if (h.ifErr(t, err, 'no err')) + return t.end(); + + t.deepEqual(ip, IP, 'ip'); + + t.end(); + }); + }); + + + tt.test(' teardown: client', function (t) { + CLIENT.close(); + t.end(); + }); +}); diff --git a/test/integration/cli-ips.test.js b/test/integration/cli-ips.test.js new file mode 100644 index 0000000..2a90a1c --- /dev/null +++ b/test/integration/cli-ips.test.js @@ -0,0 +1,173 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2017 Joyent, Inc. + */ + +/* + * Integration tests for `triton network ip` + */ + +var h = require('./helpers'); +var test = require('tape'); + +var common = require('../../lib/common'); + + +// --- Globals + +var networks; +var ips; + + +// --- Tests + +test('triton network ip list', function (tt) { + + tt.test(' triton network ip list -h', function (t) { + h.triton('network ip list -h', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/Usage:\s+triton network ip list NETWORK/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton networks -j', function (t) { + h.triton('networks -j', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + networks = []; + stdout.split('\n').forEach(function (line) { + if (!line.trim()) { + return; + } + var net = JSON.parse(line); + if (net.fabric === true) { + networks.push(net); + } + }); + t.ok(networks.length > 0, 'have at least one fabric network'); + t.ok(common.isUUID(networks[0].id)); + t.end(); + }); + }); + + tt.test(' triton network ip list', function (t) { + h.triton('network ip list', function (err, stdout, stderr) { + t.ok(err); + t.ok(/error \(Usage\)/.test(stderr)); + t.end(); + }); + }); + + tt.test(' triton network ip list ID', function (t) { + h.triton('network ip list ' + networks[0].id, + function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/^IP\b/.test(stdout)); + t.ok(/\bMANAGED\b/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton network ip list SHORTID', function (t) { + var shortid = networks[0].id.split('-')[0]; + h.triton('network ip list ' + shortid, function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/^IP\b/.test(stdout)); + t.ok(/\bMANAGED\b/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton network ip list -j', function (t) { + h.triton('network ip list -j ' + networks[0].id, + function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + ips = []; + stdout.split('\n').forEach(function (line) { + if (!line.trim()) { + return; + } + ips.push(JSON.parse(line)); + }); + t.ok(ips.length > 0, 'have at least one ip'); + t.ok(ips[0].ip, 'ip obj has an ip'); + t.end(); + }); + }); + +}); + + +test('triton network ip get', function (tt) { + + tt.test(' triton network ip get -h', function (t) { + h.triton('network ip get -h', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/Usage:\s+triton network ip\b/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton network ip help get', function (t) { + h.triton('network ip help get', function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + t.ok(/Usage:\s+triton network ip get\b/.test(stdout)); + t.end(); + }); + }); + + tt.test(' triton network ip get', function (t) { + h.triton('network ip get', function (err, stdout, stderr) { + t.ok(err); + t.ok(/error \(Usage\)/.test(stderr)); + t.end(); + }); + }); + + tt.test(' triton network ip get ID IP', function (t) { + h.triton('network ip get ' + networks[0].id + ' ' + + ips[0].ip, function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + var ip = JSON.parse(stdout); + t.equal(ip.ip, ips[0].ip); + t.end(); + }); + }); + + tt.test(' triton network ip get SHORTID IP', function (t) { + var shortid = networks[0].id.split('-')[0]; + h.triton('network ip get ' + shortid + ' ' + ips[0].ip, + function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + var ip = JSON.parse(stdout); + t.equal(ip.ip, ips[0].ip); + t.end(); + }); + }); + + tt.test(' triton network ip get NAME IP', function (t) { + h.triton('network ip get ' + networks[0].name + ' ' + + ips[0].ip, function (err, stdout, stderr) { + if (h.ifErr(t, err)) + return t.end(); + var ip = JSON.parse(stdout); + t.equal(ip.ip, ips[0].ip); + t.end(); + }); + }); + +});