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:
parent
057a784dc3
commit
8438f446cc
@ -7,7 +7,7 @@ Known issues:
|
|||||||
|
|
||||||
## not yet released
|
## not yet released
|
||||||
|
|
||||||
(nothing yet)
|
- [joyent/node-triton#226] added new `triton volume sizes` subcommand.
|
||||||
|
|
||||||
## 5.3.1
|
## 5.3.1
|
||||||
|
|
||||||
|
@ -2326,6 +2326,17 @@ CloudApi.prototype.listVolumes = function listVolumes(options, cb) {
|
|||||||
this._passThrough(endpoint, 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.
|
* Create a volume for the account.
|
||||||
*
|
*
|
||||||
|
@ -63,6 +63,25 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
|
|
||||||
self.top.tritonapi.createVolume(createVolumeParams,
|
self.top.tritonapi.createVolume(createVolumeParams,
|
||||||
function onRes(volCreateErr, volume) {
|
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) {
|
if (!volCreateErr && !opts.json) {
|
||||||
console.log('Creating volume %s (%s)', volume.name,
|
console.log('Creating volume %s (%s)', volume.name,
|
||||||
volume.id);
|
volume.id);
|
||||||
@ -144,11 +163,11 @@ do_create.options = [
|
|||||||
names: ['size', 's'],
|
names: ['size', 's'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
helpArg: 'SIZE',
|
helpArg: 'SIZE',
|
||||||
help: 'The size of the volume to create, in the form ' +
|
help: 'The size of the volume to create, in gibibytes, in the form ' +
|
||||||
'`<integer><unit>`, e.g. `20G`. <integer> must be > 0. Supported ' +
|
'`<integer>G`, e.g. `20G`. <integer> must be > 0. If a size is ' +
|
||||||
'units are `G` or `g` for gibibytes and `M` or `m` for mebibytes.' +
|
'not specified, the newly created volume will have a default ' +
|
||||||
' If a size is not specified, the newly created volume will have ' +
|
'size corresponding to the smallest size available. Available ' +
|
||||||
'a default size corresponding to the smallest size available.',
|
'volume sizes can be listed via the "volume sizes" sub-command.',
|
||||||
completionType: 'tritonvolumesize'
|
completionType: 'tritonvolumesize'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
90
lib/do_volume/do_sizes.js
Normal file
90
lib/do_volume/do_sizes.js
Normal 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;
|
@ -30,7 +30,8 @@ function VolumeCLI(top) {
|
|||||||
'list',
|
'list',
|
||||||
'get',
|
'get',
|
||||||
'create',
|
'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_get = require('./do_get');
|
||||||
VolumeCLI.prototype.do_create = require('./do_create');
|
VolumeCLI.prototype.do_create = require('./do_create');
|
||||||
VolumeCLI.prototype.do_delete = require('./do_delete');
|
VolumeCLI.prototype.do_delete = require('./do_delete');
|
||||||
|
VolumeCLI.prototype.do_sizes = require('./do_sizes');
|
||||||
|
|
||||||
VolumeCLI.aliases = ['vol'];
|
VolumeCLI.aliases = ['vol'];
|
||||||
|
|
||||||
|
@ -19,48 +19,30 @@ function throwInvalidSize(size) {
|
|||||||
/*
|
/*
|
||||||
* Returns the number of MiBs (Mebibytes) represented by the string "size". That
|
* Returns the number of MiBs (Mebibytes) represented by the string "size". That
|
||||||
* string has the following format: <integer><unit>. The integer must be > 0.
|
* 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
|
* Unit format suffix can only be 'G' for gibibytes.
|
||||||
* mebibytes.
|
|
||||||
*
|
*
|
||||||
* Examples:
|
* Example: the strings '100G' represents 100 gibibytes
|
||||||
* - the strings '100m' and '100M' represent 100 mebibytes
|
|
||||||
* - the strings '100g' and '100G' represent 100 gibibytes
|
|
||||||
*
|
*
|
||||||
* If "size" is not a valid size string, an error is thrown.
|
* If "size" is not a valid size string, an error is thrown.
|
||||||
*/
|
*/
|
||||||
function parseVolumeSize(size) {
|
function parseVolumeSize(size) {
|
||||||
assert.string(size, 'size');
|
assert.string(size, 'size');
|
||||||
|
|
||||||
|
var baseValue;
|
||||||
var MIBS_IN_GB = 1024;
|
var MIBS_IN_GB = 1024;
|
||||||
|
|
||||||
var MULTIPLIERS_TABLE = {
|
var matches = size.match(/^([1-9]\d*)G$/);
|
||||||
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)$/);
|
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
throwInvalidSize(size);
|
throwInvalidSize(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
multiplierSymbol = matches[2];
|
|
||||||
if (multiplierSymbol) {
|
|
||||||
multiplier = MULTIPLIERS_TABLE[multiplierSymbol];
|
|
||||||
}
|
|
||||||
|
|
||||||
baseValue = Number(matches[1]);
|
baseValue = Number(matches[1]);
|
||||||
|
|
||||||
if (isNaN(baseValue) || multiplier === undefined) {
|
if (isNaN(baseValue)) {
|
||||||
throwInvalidSize(size);
|
throwInvalidSize(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseValue * multiplier;
|
return baseValue * MIBS_IN_GB;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -19,8 +19,10 @@ var vasync = require('vasync');
|
|||||||
|
|
||||||
var common = require('../../lib/common');
|
var common = require('../../lib/common');
|
||||||
var h = require('./helpers');
|
var h = require('./helpers');
|
||||||
|
var mod_volumes = require('../../lib/volumes');
|
||||||
|
|
||||||
var FABRIC_NETWORKS = [];
|
var FABRIC_NETWORKS = [];
|
||||||
|
var MIBS_IN_GIB = 1024;
|
||||||
|
|
||||||
var testOpts = {
|
var testOpts = {
|
||||||
skip: !(h.CONFIG.allowWriteActions && h.CONFIG.allowVolumesTests)
|
skip: !(h.CONFIG.allowWriteActions && h.CONFIG.allowVolumesTests)
|
||||||
@ -35,7 +37,7 @@ test('triton volume create with non-default size...', testOpts, function (tt) {
|
|||||||
var validVolumeName =
|
var validVolumeName =
|
||||||
h.makeResourceName('node-triton-test-volume-create-non-default-' +
|
h.makeResourceName('node-triton-test-volume-create-non-default-' +
|
||||||
'size');
|
'size');
|
||||||
var validVolumeSize = '20g';
|
var validVolumeSize = '20G';
|
||||||
var validVolumeSizeInMib = 20 * 1024;
|
var validVolumeSizeInMib = 20 * 1024;
|
||||||
|
|
||||||
tt.comment('Test config:');
|
tt.comment('Test config:');
|
||||||
@ -106,4 +108,82 @@ test('triton volume create with non-default size...', testOpts, function (tt) {
|
|||||||
t.end();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
Reference in New Issue
Block a user