joyent/node-triton#226 add new `triton volume sizes` subcommand

Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
This commit is contained in:
Julien Gilli 2017-08-17 17:50:46 -07:00
parent 057a784dc3
commit 8438f446cc
7 changed files with 216 additions and 32 deletions

View File

@ -7,7 +7,7 @@ Known issues:
## not yet released
(nothing yet)
- [joyent/node-triton#226] added new `triton volume sizes` subcommand.
## 5.3.1

View File

@ -2326,6 +2326,17 @@ CloudApi.prototype.listVolumes = function listVolumes(options, cb) {
this._passThrough(endpoint, options, cb);
};
/**
* List the account's volume sizes.
*
* @param {Object} options
* @param {Function} callback - called like `function (err, volumeSizes)`
*/
CloudApi.prototype.listVolumeSizes = function listVolumeSizes(options, cb) {
var endpoint = format('/%s/volumesizes', this.account);
this._passThrough(endpoint, options, cb);
};
/**
* Create a volume for the account.
*

View File

@ -63,6 +63,25 @@ function do_create(subcmd, opts, args, cb) {
self.top.tritonapi.createVolume(createVolumeParams,
function onRes(volCreateErr, volume) {
/*
* VolumeSizeNotAvailable errors include additional
* information in their message
* about available volume sizes using units that are
* different than the units node-triton users have to use
* when specifying volume sizes on the command line
* (mebibytes vs gibibytes).
* As a result, we override this type of error to provide a
* simpler message that is less confusing, and users can use
* the "triton volume sizes" command to find out which
* sizes are available.
*/
if (volCreateErr &&
volCreateErr.name === 'VolumeSizeNotAvailableError') {
next(new Error('volume size not available, use ' +
'triton volume sizes command for available sizes'));
return;
}
if (!volCreateErr && !opts.json) {
console.log('Creating volume %s (%s)', volume.name,
volume.id);
@ -144,11 +163,11 @@ do_create.options = [
names: ['size', 's'],
type: 'string',
helpArg: 'SIZE',
help: 'The size of the volume to create, in the form ' +
'`<integer><unit>`, e.g. `20G`. <integer> must be > 0. Supported ' +
'units are `G` or `g` for gibibytes and `M` or `m` for mebibytes.' +
' If a size is not specified, the newly created volume will have ' +
'a default size corresponding to the smallest size available.',
help: 'The size of the volume to create, in gibibytes, in the form ' +
'`<integer>G`, e.g. `20G`. <integer> must be > 0. If a size is ' +
'not specified, the newly created volume will have a default ' +
'size corresponding to the smallest size available. Available ' +
'volume sizes can be listed via the "volume sizes" sub-command.',
completionType: 'tritonvolumesize'
},
{

90
lib/do_volume/do_sizes.js Normal file
View File

@ -0,0 +1,90 @@
/*
* 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 volume sizes ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var jsprim = require('jsprim');
var tabula = require('tabula');
var VError = require('verror');
var common = require('../common');
var errors = require('../errors');
var COLUMNS = ['type', {name: 'SIZE', lookup: 'sizeHuman', align: 'right'}];
var MIBS_IN_GIB = 1024;
// sort default with -s
var sortDefault = 'size';
function do_sizes(subcmd, opts, args, callback) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
}
var sort = opts.s.split(',');
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
callback(setupErr);
}
self.top.tritonapi.cloudapi.listVolumeSizes(
function onRes(listVolSizesErr, volumeSizes, res) {
if (listVolSizesErr) {
return callback(listVolSizesErr);
}
if (opts.json) {
common.jsonStream(volumeSizes);
} else {
volumeSizes =
volumeSizes.map(function renderVolSize(volumeSize) {
volumeSize.sizeHuman =
volumeSize.size / MIBS_IN_GIB + 'G';
return volumeSize;
});
tabula(volumeSizes, {
skipHeader: opts.H,
columns: COLUMNS,
sort: sort
});
}
callback();
});
});
}
do_sizes.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
sortDefault: sortDefault
}));
do_sizes.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
do_sizes.help = [
'List volume sizes.',
'',
'{{usage}}',
'',
'{{options}}'
].join('\n');
module.exports = do_sizes;

View File

@ -30,7 +30,8 @@ function VolumeCLI(top) {
'list',
'get',
'create',
'delete'
'delete',
'sizes'
]
});
}
@ -45,6 +46,7 @@ VolumeCLI.prototype.do_list = require('./do_list');
VolumeCLI.prototype.do_get = require('./do_get');
VolumeCLI.prototype.do_create = require('./do_create');
VolumeCLI.prototype.do_delete = require('./do_delete');
VolumeCLI.prototype.do_sizes = require('./do_sizes');
VolumeCLI.aliases = ['vol'];

View File

@ -19,48 +19,30 @@ function throwInvalidSize(size) {
/*
* Returns the number of MiBs (Mebibytes) represented by the string "size". That
* string has the following format: <integer><unit>. The integer must be > 0.
* Unit format suffixes are 'G' or 'g' for gibibytes and 'M' or 'm' for
* mebibytes.
* Unit format suffix can only be 'G' for gibibytes.
*
* Examples:
* - the strings '100m' and '100M' represent 100 mebibytes
* - the strings '100g' and '100G' represent 100 gibibytes
* Example: the strings '100G' represents 100 gibibytes
*
* If "size" is not a valid size string, an error is thrown.
*/
function parseVolumeSize(size) {
assert.string(size, 'size');
var baseValue;
var MIBS_IN_GB = 1024;
var MULTIPLIERS_TABLE = {
g: MIBS_IN_GB,
G: MIBS_IN_GB,
m: 1,
M: 1
};
var multiplier;
var multiplierSymbol;
var baseValue;
var matches = size.match(/^([1-9]\d*)(g|m|G|M)$/);
var matches = size.match(/^([1-9]\d*)G$/);
if (!matches) {
throwInvalidSize(size);
}
multiplierSymbol = matches[2];
if (multiplierSymbol) {
multiplier = MULTIPLIERS_TABLE[multiplierSymbol];
}
baseValue = Number(matches[1]);
if (isNaN(baseValue) || multiplier === undefined) {
if (isNaN(baseValue)) {
throwInvalidSize(size);
}
return baseValue * multiplier;
return baseValue * MIBS_IN_GB;
}
module.exports = {

View File

@ -19,8 +19,10 @@ var vasync = require('vasync');
var common = require('../../lib/common');
var h = require('./helpers');
var mod_volumes = require('../../lib/volumes');
var FABRIC_NETWORKS = [];
var MIBS_IN_GIB = 1024;
var testOpts = {
skip: !(h.CONFIG.allowWriteActions && h.CONFIG.allowVolumesTests)
@ -35,7 +37,7 @@ test('triton volume create with non-default size...', testOpts, function (tt) {
var validVolumeName =
h.makeResourceName('node-triton-test-volume-create-non-default-' +
'size');
var validVolumeSize = '20g';
var validVolumeSize = '20G';
var validVolumeSizeInMib = 20 * 1024;
tt.comment('Test config:');
@ -106,4 +108,82 @@ test('triton volume create with non-default size...', testOpts, function (tt) {
t.end();
});
});
});
test('triton volume create with unavailable size...', testOpts, function (tt) {
var validVolumeName =
h.makeResourceName('node-triton-test-volume-create-unavailable-' +
'size');
tt.test(' triton volume create volume with unavailable size',
function (t) {
vasync.pipeline({arg: {}, funcs: [
function getVolumeSizes(ctx, next) {
h.triton(['volume', 'sizes', '-j'],
function onVolSizesListed(volSizesListErr, sizes) {
var largestSizeInMib;
t.notOk(volSizesListErr,
'listing volume sizes should not error');
if (volSizesListErr) {
next(volSizesListErr);
return;
}
t.ok(typeof (sizes) === 'string',
'sizes should be a string');
sizes = sizes.trim().split('\n');
t.ok(sizes.length > 0,
'there should be at least one available ' +
'volume size');
if (sizes.length === 0) {
next(new Error('no volume size available'));
return;
}
largestSizeInMib =
JSON.parse(sizes[sizes.length - 1]).size;
ctx.unavailableVolumeSize =
(largestSizeInMib / MIBS_IN_GIB + 1) + 'G';
next();
});
},
function createVolWithUnavailableSize(ctx, next) {
h.triton([
'volume',
'create',
'--name',
validVolumeName,
'--size',
ctx.unavailableVolumeSize,
'-w'
].join(' '), function (volCreateErr, stdout, stderr) {
var actualErrMsg;
var expectedErrMsg = 'volume size not available';
t.ok(volCreateErr,
'volume creation with unavailable size ' +
ctx.unavailableVolumeSize +
' should error');
if (volCreateErr) {
actualErrMsg = stderr;
t.notEqual(actualErrMsg.indexOf(expectedErrMsg), -1,
'error message should include: ' +
expectedErrMsg + ' and got: ' +
actualErrMsg);
}
next();
});
}
]}, function onTestDone(err) {
t.end();
});
});
});