TRITON-30 Add UpdateNetworkIP to node-triton

Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
This commit is contained in:
Dave Eddy 2017-12-22 17:49:52 -05:00
parent da5f3bade8
commit ab564177b5
7 changed files with 382 additions and 12 deletions

View File

@ -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.

View File

@ -389,7 +389,7 @@ CloudApi.prototype.getNetwork = function getNetwork(id, cb) {
/**
* <http://apidocs.joyent.com/cloudapi/#ListNetworkIPs>
*
* @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) {
* <http://apidocs.joyent.com/cloudapi/#GetNetworkIP>
*
* @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) {
});
};
/**
* <http://apidocs.joyent.com/cloudapi/#UpdateNetworkIP>
*
* @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);
});
};
// <updatable network ip field> -> <expected typeof>
CloudApi.prototype.UPDATE_NETWORK_IP_FIELDS = {
reserved: 'boolean'
};
// ---- datacenters
/**

View File

@ -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:

View File

@ -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;

View File

@ -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;

View File

@ -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.

View File

@ -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": {