more changes according to first code review

This commit is contained in:
Julien Gilli 2017-03-20 18:55:59 -07:00
parent d2fce639dd
commit c152fa77e4
8 changed files with 170 additions and 70 deletions

View File

@ -9,10 +9,16 @@ Known issues:
## 5.2.0 ## 5.2.0
- [joyent/mode-triton#173] Add support for listing and getting triton nfs - Add support for creating and managing NFS shared volumes. New `triton volume`
volumes. commands are available:
- [joyent/mode-triton#174] Add support for creating triton nfs volumes.
- [joyent/mode-triton#175] Add support for deleting triton nfs volumes. * `triton volume create` to create NFS shared volumes
* `triton volume list` to list existing volumes
* `triton volume get` to get information about a given volume
* `triton volume delete` to delete one or more volumes
Use `triton volume --help` to get help on all of these commands.
- [joyent/node-triton#183] `triton profile create` will no longer use ANSI - [joyent/node-triton#183] `triton profile create` will no longer use ANSI
codes for styling if stdout isn't a TTY. codes for styling if stdout isn't a TTY.

View File

@ -2287,9 +2287,7 @@ CloudApi.prototype.getVolume = function getVolume(opts, cb) {
assert.uuid(opts.id, 'opts.id'); assert.uuid(opts.id, 'opts.id');
var endpoint = format('/%s/volumes/%s', this.account, opts.id); var endpoint = format('/%s/volumes/%s', this.account, opts.id);
this._request(endpoint, function (err, req, res, body) { this._passThrough(endpoint, cb);
cb(err, body, res);
});
}; };
/** /**
@ -2308,25 +2306,33 @@ CloudApi.prototype.listVolumes = function listVolumes(options, cb) {
* *
* @param {Object} options * @param {Object} options
* - name {String} Optional: the name of the volume to be created * - name {String} Optional: the name of the volume to be created
* - size {String} Optional: a string representing the size of the volume * - size {Number} Optional: a number representing the size of the volume
* to be created * to be created in mebibytes.
* - networks {Array} Optional: an array that contains all the networks * - networks {Array} Optional: an array that contains all the networks
* that should be reachable from the newly created volume * that should be reachable from the newly created volume
* - type {String}: the type of the volume. Currently, only "tritonnfs" is
* supported.
* @param {Function} callback - called like `function (err, volume, res)` * @param {Function} callback - called like `function (err, volume, res)`
*/ */
CloudApi.prototype.createVolume = function createVolume(options, callback) { CloudApi.prototype.createVolume = function createVolume(options, cb) {
assert.object(options, 'options'); assert.object(options, 'options');
assert.optionalString(options.name, 'options.name'); assert.optionalString(options.name, 'options.name');
assert.optionalString(options.size, 'options.size'); assert.optionalNumber(options.size, 'options.size');
assert.optionalArrayOfUuid(options.networks, 'options.networks'); assert.optionalArrayOfUuid(options.networks, 'options.networks');
assert.func(callback, 'callback'); assert.string(options.type, 'options.type');
assert.func(cb, 'cb');
this._request({ this._request({
method: 'POST', method: 'POST',
path: format('/%s/volumes', this.account), path: format('/%s/volumes', this.account),
data: options data: {
name: options.name,
size: options.size,
networks: options.networks,
type: options.type
}
}, function (err, req, res, body) { }, function (err, req, res, body) {
callback(err, body, res); cb(err, body, res);
}); });
}; };
@ -2336,15 +2342,15 @@ CloudApi.prototype.createVolume = function createVolume(options, callback) {
* @param {String} volumeUuid * @param {String} volumeUuid
* @param {Function} callback - called like `function (err, volume, res)` * @param {Function} callback - called like `function (err, volume, res)`
*/ */
CloudApi.prototype.deleteVolume = function deleteVolume(volumeUuid, callback) { CloudApi.prototype.deleteVolume = function deleteVolume(volumeUuid, cb) {
assert.uuid(volumeUuid, 'volumeUuid'); assert.uuid(volumeUuid, 'volumeUuid');
assert.func(callback, 'callback'); assert.func(cb, 'cb');
this._request({ this._request({
method: 'DELETE', method: 'DELETE',
path: format('/%s/volumes/%s', this.account, volumeUuid) path: format('/%s/volumes/%s', this.account, volumeUuid)
}, function (err, req, res, body) { }, function (err, req, res, body) {
callback(err, body, res); cb(err, res);
}); });
}; };
@ -2374,13 +2380,13 @@ function waitForVolumeStates(opts, callback) {
function poll() { function poll() {
self.getVolume({ self.getVolume({
id: opts.id id: opts.id
}, function (err, volume, res) { }, function (err, vol, res) {
if (err) { if (err) {
callback(err, null, res); callback(err, null, res);
return; return;
} }
if (opts.states.indexOf(volume.state) !== -1) { if (opts.states.indexOf(vol.state) !== -1) {
callback(null, volume, res); callback(null, vol, res);
return; return;
} }
setTimeout(poll, interval); setTimeout(poll, interval);

View File

@ -160,19 +160,15 @@ function kvToObj(kvs, valid) {
* given an array of key=value pairs, break them into a JSON predicate * given an array of key=value pairs, break them into a JSON predicate
* *
* @param {Array} kvs - an array of key=value pairs * @param {Array} kvs - an array of key=value pairs
* @param {Array} valid (optional) - an array to validate pairs * @param {Array} validKeys (optional) - an array representing valid keys that
* can be used in the first argument "kvs".
* @param {String} compositionType - the way each key/value pair will be * @param {String} compositionType - the way each key/value pair will be
* combined to form a JSON predicate. Valid values are 'or' and 'and' * combined to form a JSON predicate. Valid values are 'or' and 'and'.
* *
*/ */
function kvToJSONPredicate(kvs, valid, compositionType) { function jsonPredFromKv(kvs, validKeys, compositionType) {
if (typeof ('compositionType') === 'undefined') {
compositionType = valid;
valid = undefined;
}
assert.arrayOfString(kvs, 'kvs'); assert.arrayOfString(kvs, 'kvs');
assert.optionalArrayOfString(valid, 'valid'); assert.arrayOfString(validKeys, 'validKeys');
assert.string(compositionType, 'string'); assert.string(compositionType, 'string');
assert.ok(compositionType === 'or' || compositionType === 'and', assert.ok(compositionType === 'or' || compositionType === 'and',
'compositionType'); 'compositionType');
@ -183,16 +179,18 @@ function kvToJSONPredicate(kvs, valid, compositionType) {
for (var i = 0; i < kvs.length; i++) { for (var i = 0; i < kvs.length; i++) {
var kv = kvs[i]; var kv = kvs[i];
var idx = kv.indexOf('='); var idx = kv.indexOf('=');
if (idx === -1) if (idx === -1) {
throw new errors.UsageError(format( throw new errors.UsageError(format(
'invalid filter: "%s" (must be of the form "field=value")', 'invalid filter: "%s" (must be of the form "field=value")',
kv)); kv));
}
var k = kv.slice(0, idx); var k = kv.slice(0, idx);
var v = kv.slice(idx + 1); var v = kv.slice(idx + 1);
if (valid && valid.indexOf(k) === -1) if (validKeys && validKeys.indexOf(k) === -1) {
throw new errors.UsageError(format( throw new errors.UsageError(format(
'invalid filter name: "%s" (must be one of "%s")', 'invalid filter name: "%s" (must be one of "%s")',
k, valid.join('", "'))); k, validKeys.join('", "')));
}
predicate[compositionType].push({eq: [k, v]}); predicate[compositionType].push({eq: [k, v]});
} }
@ -1159,6 +1157,6 @@ module.exports = {
deepEqual: deepEqual, deepEqual: deepEqual,
tildeSync: tildeSync, tildeSync: tildeSync,
objFromKeyValueArgs: objFromKeyValueArgs, objFromKeyValueArgs: objFromKeyValueArgs,
kvToJSONPredicate: kvToJSONPredicate jsonPredFromKv: jsonPredFromKv
}; };
// vim: set softtabstop=4 shiftwidth=4: // vim: set softtabstop=4 shiftwidth=4:

View File

@ -17,19 +17,39 @@ var vasync = require('vasync');
var common = require('../common'); var common = require('../common');
var distractions = require('../distractions'); var distractions = require('../distractions');
var errors = require('../errors'); var errors = require('../errors');
var mod_volumes = require('../volumes');
function do_create(subcmd, opts, args, cb) {
function do_create(subcmd, opts, args, callback) {
var self = this; var self = this;
if (opts.help) { if (opts.help) {
this.do_help('help', {}, [subcmd], callback); this.do_help('help', {}, [subcmd], cb);
return;
}
if (args.length !== 0) {
cb(new errors.UsageError('incorrect number of args'));
return; return;
} }
var context = {}; var context = {};
vasync.pipeline({funcs: [ vasync.pipeline({funcs: [
function validateVolumeSize(ctx, next) {
if (opts.size === undefined) {
next();
return;
}
try {
ctx.size = mod_volumes.parseVolumeSize(opts.size);
} catch (parseSizeErr) {
next(parseSizeErr);
return;
}
next();
},
function setup(ctx, next) { function setup(ctx, next) {
common.cliSetupTritonApi({ common.cliSetupTritonApi({
cli: self.top cli: self.top
@ -63,7 +83,7 @@ function do_create(subcmd, opts, args, callback) {
type: 'tritonnfs', type: 'tritonnfs',
name: opts.name, name: opts.name,
networks: ctx.networks, networks: ctx.networks,
size: opts.size size: ctx.size
}; };
if (opts.type) { if (opts.type) {
@ -102,7 +122,7 @@ function do_create(subcmd, opts, args, callback) {
console.log(JSON.stringify(ctx.volume, null, 4)); console.log(JSON.stringify(ctx.volume, null, 4));
} }
} }
], arg: context}, callback); ], arg: context}, cb);
} }
do_create.options = [ do_create.options = [
@ -127,11 +147,15 @@ do_create.options = [
help: 'Volume type. Default is "tritonnfs".' help: 'Volume type. Default is "tritonnfs".'
}, },
{ {
names: ['size', 'S'], names: ['size', 's'],
type: 'string', type: 'string',
helpArg: 'SIZE', helpArg: 'SIZE',
help: '', help: 'The `size` input parameter must match the following regular ' +
completionType: 'volumesize' 'expression: /(\d+)(g|m|G|M|gb|mb|GB|MB)/ All units are in ' +
'ibibytes (mebibytes and gibibytes). `g`, `G`, `gb` and `GB` ' +
'stand for "gibibytes". `m`, `M`, `mb` and `MB` stand for ' +
'"mebibytes".',
completionType: 'tritonvolumesize'
}, },
{ {
names: ['network', 'N'], names: ['network', 'N'],

View File

@ -7,7 +7,7 @@
/* /*
* Copyright 2017 Joyent, Inc. * Copyright 2017 Joyent, Inc.
* *
* `triton volume del ...` * `triton volume delete ...`
*/ */
var assert = require('assert-plus'); var assert = require('assert-plus');
@ -18,16 +18,14 @@ var common = require('../common');
var distractions = require('../distractions'); var distractions = require('../distractions');
var errors = require('../errors'); var errors = require('../errors');
function perror(err) {
console.error('error: %s', err.message);
}
function deleteVolume(volumeName, options, callback) {
assert.string(volumeName, 'volumeName');
assert.object(options, 'options');
assert.object(options.tritonapi, 'options.tritonapi');
assert.func(callback, 'callback');
var tritonapi = options.tritonapi; function deleteVolume(volumeName, opts, cb) {
assert.string(volumeName, 'volumeName');
assert.object(opts, 'opts');
assert.object(opts.tritonapi, 'opts.tritonapi');
assert.func(cb, 'cb');
var tritonapi = opts.tritonapi;
vasync.pipeline({funcs: [ vasync.pipeline({funcs: [
function getVolume(ctx, next) { function getVolume(ctx, next) {
@ -52,12 +50,12 @@ function deleteVolume(volumeName, options, callback) {
var distraction; var distraction;
var volumeId = ctx.volume.id; var volumeId = ctx.volume.id;
if (!options.wait) { if (!opts.wait) {
next(); next();
return; return;
} }
distraction = distractions.createDistraction(options.wait.length); distraction = distractions.createDistraction(opts.wait.length);
tritonapi.cloudapi.waitForVolumeStates({ tritonapi.cloudapi.waitForVolumeStates({
id: volumeId, id: volumeId,
@ -67,17 +65,17 @@ function deleteVolume(volumeName, options, callback) {
next(waitErr); next(waitErr);
}); });
} }
], arg: {}}, callback); ], arg: {}}, cb);
} }
function do_delete(subcmd, opts, args, callback) { function do_delete(subcmd, opts, args, cb) {
var self = this; var self = this;
if (opts.help) { if (opts.help) {
self.do_help('help', {}, [subcmd], callback); self.do_help('help', {}, [subcmd], cb);
return; return;
} else if (args.length < 1) { } else if (args.length < 1) {
callback(new errors.UsageError('missing VOLUME arg(s)')); cb(new errors.UsageError('missing VOLUME arg(s)'));
return; return;
} }
@ -85,7 +83,7 @@ function do_delete(subcmd, opts, args, callback) {
volumeIds: args volumeIds: args
}; };
vasync.pipeline({funcs: [ vasync.pipeline({arg: context, funcs: [
function setup(ctx, next) { function setup(ctx, next) {
common.cliSetupTritonApi({ common.cliSetupTritonApi({
cli: self.top cli: self.top
@ -104,16 +102,15 @@ function do_delete(subcmd, opts, args, callback) {
inputs: ctx.volumeIds inputs: ctx.volumeIds
}, next); }, next);
} }
], arg: context}, function onDone(err) { ]}, function onDone(err) {
if (err) { if (err) {
perror(err); cb(err);
callback(err);
return; return;
} }
console.log('%s volume %s', common.capitalize('delete'), args); console.log('%s volume %s', common.capitalize('delete'), args);
callback(); cb();
}); });
} }

View File

@ -7,7 +7,7 @@
/* /*
* Copyright 2016 Joyent, Inc. * Copyright 2016 Joyent, Inc.
* *
* `triton image list ...` * `triton volume list ...`
*/ */
var format = require('util').format; var format = require('util').format;
@ -25,13 +25,13 @@ var validFilters = [
]; ];
// columns default without -o // columns default without -o
var columnsDefault = 'shortid,name,size,type,state'; var columnsDefault = 'shortid,name,size,type,state,age';
// columns default with -l // columns default with -l
var columnsDefaultLong = 'id,name,size,type,state'; var columnsDefaultLong = 'id,name,size,type,state,created';
// sort default with -s // sort default with -s
var sortDefault = 'create_timestamp'; var sortDefault = 'created';
function do_list(subcmd, opts, args, callback) { function do_list(subcmd, opts, args, callback) {
var self = this; var self = this;
@ -56,7 +56,7 @@ function do_list(subcmd, opts, args, callback) {
if (args) { if (args) {
try { try {
filterPredicate = common.kvToJSONPredicate(args, validFilters, filterPredicate = common.jsonPredFromKv(args, validFilters,
'and'); 'and');
} catch (e) { } catch (e) {
callback(e); callback(e);
@ -85,6 +85,8 @@ function do_list(subcmd, opts, args, callback) {
} }
self.top.tritonapi.cloudapi.listVolumes(listOpts, self.top.tritonapi.cloudapi.listVolumes(listOpts,
function onRes(listVolsErr, volumes, res) { function onRes(listVolsErr, volumes, res) {
var now;
if (listVolsErr) { if (listVolsErr) {
return callback(listVolsErr); return callback(listVolsErr);
} }
@ -92,9 +94,12 @@ function do_list(subcmd, opts, args, callback) {
if (opts.json) { if (opts.json) {
common.jsonStream(volumes); common.jsonStream(volumes);
} else { } else {
now = new Date();
for (var i = 0; i < volumes.length; i++) { for (var i = 0; i < volumes.length; i++) {
var volume = volumes[i]; var volume = volumes[i];
volume.shortid = volume.id.split('-', 1)[0]; volume.shortid = volume.id.split('-', 1)[0];
volume.created = new Date(volume.create_timestamp);
volume.age = common.longAgo(volume.created, now);
} }
tabula(volumes, { tabula(volumes, {
@ -119,7 +124,7 @@ do_list.options = [
sortDefault: sortDefault sortDefault: sortDefault
})); }));
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS]']; do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]'];
do_list.help = [ do_list.help = [
/* BEGIN JSSTYLED */ /* BEGIN JSSTYLED */

View File

@ -10,6 +10,8 @@
* `triton volumes ...` bwcompat shortcut for `triton volumes list ...`. * `triton volumes ...` bwcompat shortcut for `triton volumes list ...`.
*/ */
var targ = require('./do_volume/do_list');
function do_volumes(subcmd, opts, args, callback) { function do_volumes(subcmd, opts, args, callback) {
this.handlerFromSubcmd('volume').dispatch({ this.handlerFromSubcmd('volume').dispatch({
subcmd: 'list', subcmd: 'list',
@ -18,9 +20,11 @@ function do_volumes(subcmd, opts, args, callback) {
}, callback); }, callback);
} }
do_volumes.help = 'A shortcut for "triton volumes list".'; do_volumes.help = 'A shortcut for "triton volumes list".\n' + targ.help;
do_volumes.aliases = ['vols']; do_volumes.aliases = ['vols'];
do_volumes.hidden = true; do_volumes.hidden = true;
do_volumes.options = require('./do_volume/do_list').options; do_volumes.options = targ.options;
do_volumes.completionArgtypes = targ.completionArgtypes;
do_volumes.synopses = targ.synopses;
module.exports = do_volumes; module.exports = do_volumes;

60
lib/volumes.js Normal file
View File

@ -0,0 +1,60 @@
/*
* 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 (c) 2017, Joyent, Inc.
*/
var assert = require('assert-plus');
function throwInvalidSize(size) {
assert.string(size, 'size');
throw new Error('size "' + size + '" is not a valid volume size');
}
/*
* Returns the number of MiBs (Mebibytes) represented by the string "size". That
* string can have different format suffixes, such as "100GB", "100G", etc.
* If "size" is not a valid size string, an error is thrown.
*/
function parseVolumeSize(size) {
assert.optionalString(size, 'size');
var MIBS_IN_GB = 1024;
var MULTIPLIERS_TABLE = {
g: MIBS_IN_GB,
GB: MIBS_IN_GB,
m: 1,
MB: 1
};
var multiplierSymbol, multiplier;
var baseValue;
if (size === undefined) {
return undefined;
}
var matches = size.match(/(\d+)(g|m|G|M|gb|mb|GB|MB)/);
if (!matches) {
throwInvalidSize(size);
}
multiplierSymbol = matches[2];
multiplier = MULTIPLIERS_TABLE[multiplierSymbol];
baseValue = Number(matches[1]);
if (isNaN(baseValue) || multiplier === undefined) {
throwInvalidSize(size);
}
return baseValue * multiplier;
}
module.exports = {
parseVolumeSize: parseVolumeSize
};