From ab564177b59f2b8167cb404b7830863801dca373 Mon Sep 17 00:00:00 2001 From: Dave Eddy Date: Fri, 22 Dec 2017 17:49:52 -0500 Subject: [PATCH] TRITON-30 Add UpdateNetworkIP to node-triton Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 5 + lib/cloudapi2.js | 47 +++++++- lib/common.js | 88 +++++++++++++- lib/do_network/do_ip/do_update.js | 193 ++++++++++++++++++++++++++++++ lib/do_network/do_ip/index.js | 4 +- lib/tritonapi.js | 55 ++++++++- package.json | 2 +- 7 files changed, 382 insertions(+), 12 deletions(-) create mode 100644 lib/do_network/do_ip/do_update.js diff --git a/CHANGES.md b/CHANGES.md index ff1850f..79bd160 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,11 @@ Known issues: (nothing yet) +## 5.6.0 + +- [TRITON-30] Add UpdateNetworkIP to node-triton, e.g. + `triton network ip update` + ## 5.5.0 - [PUBAPI-1452] Add ip subcommand to network, e.g. diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index a409e18..ba43791 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -389,7 +389,7 @@ CloudApi.prototype.getNetwork = function getNetwork(id, cb) { /** * * - * @param {String} - UUID + * @param {String} id - The network UUID. Required. * @param {Function} callback of the form `function (err, ips, res)` */ CloudApi.prototype.listNetworkIps = function listNetworkIps(id, cb) { @@ -406,13 +406,14 @@ CloudApi.prototype.listNetworkIps = function listNetworkIps(id, cb) { * * * @param {Object} opts - * - {String} opts.id The network UUID, name, or shortID. Required. - * - {String} opts.ip The IP. Required. + * - {String} id - The network UUID. Required. + * - {String} 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.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.string(opts.ip, 'opts.ip'); assert.func(cb, 'cb'); var endpoint = this._path(format('/%s/networks/%s/ips/%s', @@ -422,6 +423,42 @@ CloudApi.prototype.getNetworkIp = function getNetworkIp(opts, cb) { }); }; +/** + * + * + * @param {Object} opts + * - {String} id - The network UUID. Required. + * - {String} ip - The IP. Required. + * - {Boolean} reserved - Reserve the IP. Required. + * @param {Function} callback of the form `function (err, body, res)` + */ +CloudApi.prototype.updateNetworkIp = function updateNetworkIp(opts, cb) { + assert.object(opts, 'opts'); + assert.uuid(opts.id, 'opts.id'); + assert.string(opts.ip, 'opts.ip'); + assert.bool(opts.reserved, 'opts.reserved'); + assert.func(cb, 'cb'); + + var endpoint = this._path(format('/%s/networks/%s/ips/%s', + this.account, opts.id, opts.ip)); + var data = { + reserved: opts.reserved + }; + + this._request({ + method: 'PUT', + path: endpoint, + data: data + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + +// -> +CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS = { + reserved: 'boolean' +}; + // ---- datacenters /** diff --git a/lib/common.js b/lib/common.js index 2347242..9db8f30 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1301,7 +1301,91 @@ function argvFromLine(line) { return argv; } +/* + * Read stdin in and callback with it as a string + * + * @param {Function} cb - callback in the form `function (str) {}` + */ +function readStdin(cb) { + assert.func(cb, 'cb'); + var stdin = ''; + process.stdin.setEncoding('utf8'); + process.stdin.resume(); + process.stdin.on('data', function stdinOnData(chunk) { + stdin += chunk; + }); + process.stdin.on('end', function stdinOnEnd() { + cb(stdin); + }); +} + +/* + * Validate an object of values against an object of types. + * + * Example: + * var input = { + * foo: 'hello', + * bar: 42, + * baz: true + * }; + * var valid = { + * foo: 'string', + * bar: 'number', + * baz: 'boolean' + * } + * validateObject(input, valid); + * // no error is thrown + * + * All keys in `input` are check for their matching counterparts in `valid`. + * If the key is not found in `valid`, or the type specified for the key in + * `valid` doesn't match the type of the value in `input` an error is thrown. + * Also an error is thrown (optionally, enabled by default) if the input object + * is empty. Note that any keys found in `valid` not found in `input` are not + * considered an error. + * + * @param {Object} input - Required. Input object of values. + * @param {Object} valid - Required. Validation object of types. + * @param {Object} opts: Optional + * - @param {Boolean} allowEmptyInput - don't consider an empty + * input object an error + * @throws {Error} if the input object contains a key not found in the + * validation object + */ +function validateObject(input, valid, opts) { + opts = opts || {}; + + assert.object(input, 'input'); + assert.object(valid, 'valid'); + assert.object(opts, 'opts'); + assert.optionalBool(opts.allowEmptyInput, 'opts.allowEmptyInput'); + + var validFields = Object.keys(valid).sort().join(', '); + var i = 0; + + Object.keys(input).forEach(function (key) { + var value = input[key]; + var type = valid[key]; + + if (!type) { + throw new errors.UsageError(format('unknown or ' + + 'unupdateable field: %s (updateable fields are: %s)', + key, validFields)); + } + assert.string(type, 'type'); + + if (typeof (value) !== type) { + throw new errors.UsageError(format('field "%s" must be ' + + 'of type "%s", but got a value of type "%s"', + key, type, typeof (value))); + } + i++; + }); + + if (i === 0 && !opts.allowEmptyInput) { + throw new errors.UsageError('Input object must not be empty'); + } +} //---- exports @@ -1339,6 +1423,8 @@ module.exports = { objFromKeyValueArgs: objFromKeyValueArgs, argvFromLine: argvFromLine, jsonPredFromKv: jsonPredFromKv, - monotonicTimeDiffMs: monotonicTimeDiffMs + monotonicTimeDiffMs: monotonicTimeDiffMs, + readStdin: readStdin, + validateObject: validateObject }; // vim: set softtabstop=4 shiftwidth=4: diff --git a/lib/do_network/do_ip/do_update.js b/lib/do_network/do_ip/do_update.js new file mode 100644 index 0000000..6ce8871 --- /dev/null +++ b/lib/do_network/do_ip/do_update.js @@ -0,0 +1,193 @@ +/* + * 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 update ...` + */ + +var format = require('util').format; +var fs = require('fs'); + +var vasync = require('vasync'); + +var common = require('../../common'); +var errors = require('../../errors'); +var UPDATE_NETWORK_IP_FIELDS + = require('../../cloudapi2').CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS; + +function do_update(subcmd, opts, args, callback) { + if (opts.help) { + this.do_help('help', {}, [subcmd], callback); + return; + } else if (args.length < 2) { + callback(new errors.UsageError(format( + 'incorrect number of args (%d)', args.length))); + return; + } + + var log = this.log; + var tritonapi = this.top.tritonapi; + var updateIpOpts = { + id: args.shift(), + ip: args.shift() + }; + + if (args.length === 0 && !opts.file) { + callback(new errors.UsageError( + 'FIELD=VALUE arguments or "-f FILE" must be specified')); + return; + } + + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, + + function gatherDataArgs(ctx, next) { + if (opts.file) { + next(); + return; + } + + try { + ctx.data = common.objFromKeyValueArgs(args, { + disableDotted: true, + typeHintFromKey: UPDATE_NETWORK_IP_FIELDS + }); + } catch (err) { + next(err); + return; + } + + next(); + }, + + function gatherDataFile(ctx, next) { + if (!opts.file || opts.file === '-') { + next(); + return; + } + + var input = fs.readFileSync(opts.file, 'utf8'); + try { + ctx.data = JSON.parse(input); + } catch (err) { + next(new errors.TritonError(format( + 'invalid JSON for network IP update in "%s": %s', + opts.file, err))); + return; + } + next(); + }, + + function gatherDataStdin(ctx, next) { + if (opts.file !== '-') { + next(); + return; + } + + common.readStdin(function gotStdin(stdin) { + try { + ctx.data = JSON.parse(stdin); + } catch (err) { + log.trace({stdin: stdin}, + 'invalid network IP update JSON on stdin'); + next(new errors.TritonError(format( + 'invalid JSON for network IP update on stdin: %s', + err))); + return; + } + next(); + }); + }, + + function validateIt(ctx, next) { + try { + common.validateObject(ctx.data, UPDATE_NETWORK_IP_FIELDS); + } catch (e) { + next(e); + return; + } + + next(); + }, + + function updateNetworkIP(ctx, next) { + Object.keys(ctx.data).forEach(function (key) { + updateIpOpts[key] = ctx.data[key]; + }); + + tritonapi.updateNetworkIp(updateIpOpts, function (err, body, res) { + if (err) { + next(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(body)); + next(); + return; + } + + console.log('Updated network %s IP %s (fields: %s)', + updateIpOpts.id, updateIpOpts.ip, + Object.keys(ctx.data).join(', ')); + next(); + }); + } + ]}, callback); +} + +do_update.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['file', 'f'], + type: 'string', + helpArg: 'FILE', + help: 'A file holding a JSON file of updates, or "-" to read ' + + 'JSON from stdin.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON output.' + } +]; + +do_update.synopses = [ + '{{name}} {{cmd}} NETWORK IP [FIELD=VALUE ...]', + '{{name}} {{cmd}} NETWORK IP -f JSON-FILE' +]; + +do_update.help = [ + /* BEGIN JSSTYLED */ + 'Update a network ip.', + '', + '{{usage}}', + '', + '{{options}}', + 'Where NETWORK is a network id, and IP is the ip address you want to update.', + '', + 'Updateable fields:', + ' ' + Object.keys(UPDATE_NETWORK_IP_FIELDS).sort().map(function (field) { + return field + ' (' + UPDATE_NETWORK_IP_FIELDS[field] + ')'; + }).join('\n '), + + '' + /* END JSSTYLED */ +].join('\n'); + +do_update.completionArgtypes = [ + 'tritonnetwork', + 'tritonnetworkip', + 'tritonupdatenetworkipfield' +]; + +module.exports = do_update; diff --git a/lib/do_network/do_ip/index.js b/lib/do_network/do_ip/index.js index 8a24488..41de719 100644 --- a/lib/do_network/do_ip/index.js +++ b/lib/do_network/do_ip/index.js @@ -32,7 +32,8 @@ function IpCLI(top) { helpSubcmds: [ 'help', 'list', - 'get' + 'get', + 'update' ] }); } @@ -45,5 +46,6 @@ IpCLI.prototype.init = function init(opts, args, cb) { IpCLI.prototype.do_list = require('./do_list'); IpCLI.prototype.do_get = require('./do_get'); +IpCLI.prototype.do_update = require('./do_update'); module.exports = IpCLI; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index a0e8ea7..28e7be9 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -890,7 +890,7 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) { }; /** - * List an network's IPs. + * List a network's IPs. * * @param {String} name The network UUID, name, or short ID. Required. * @param {Function} cb `function (err, ip, res)` @@ -929,11 +929,11 @@ TritonApi.prototype.listNetworkIps = function listNetworkIps(name, cb) { }; /** - * Get an network IP. + * Get a network IP. * * @param {Object} opts - * - {String} opts.id The network UUID, name, or shortID. Required. - * - {String} opts.ip The IP. Required. + * - {String} id - The network UUID, name, or shortID. Required. + * - {String} 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 @@ -970,6 +970,53 @@ TritonApi.prototype.getNetworkIp = function getNetworkIp(opts, cb) { }); }; +/** + * Modify a network IP. + * + * @param {Object} opts + * - {String} id - The network UUID, name, or shortID. Required. + * - {String} ip - The IP. Required. + * # The updateable fields + * - {Boolean} reserved - Reserve the IP. Required. + * @param {Function} cb `function (err, ip, res)` + * On failure `err` is an error instance, else it is null. + * On success: `obj` is an ip object + */ +TritonApi.prototype.updateNetworkIp = function updateNetworkIp(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.string(opts.ip, 'opts.ip'); + assert.bool(opts.reserved, 'opts.reserved'); + assert.func(cb, 'cb'); + + var self = this; + var res; + var body; + + vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [ + _stepNetId, + + function updateIp(arg, next) { + opts.id = arg.netId; + self.cloudapi.updateNetworkIp(opts, function (err, _body, _res) { + res = _res; + body = _body; + + if (err && err.restCode === 'ResourceNotFound' && + err.exitStatus !== 3) { + // Wrap with *our* ResourceNotFound for exitStatus=3. + err = new errors.ResourceNotFoundError(err, + format('IP %s was not found in network %s', + opts.ip, opts.id)); + } + + next(err); + }); + } + ]}, function (err) { + cb(err, body, res); + }); +}; /** * Get an instance. diff --git a/package.json b/package.json index 4a681c0..43a7a58 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.5.0", + "version": "5.6.0", "author": "Joyent (joyent.com)", "homepage": "https://github.com/joyent/node-triton", "dependencies": {