joyent/node-triton#173 Add support for listing and getting triton nfs volumes
joyent/node-triton#174 Add support for creating triton nfs volumes joyent/node-triton#175 Add support for deleting triton NFS volumes
This commit is contained in:
parent
87b196ac31
commit
cf80a684aa
14
CHANGES.md
14
CHANGES.md
@ -7,7 +7,19 @@ Known issues:
|
|||||||
|
|
||||||
## not yet released
|
## not yet released
|
||||||
|
|
||||||
(nothing yet)
|
- [joyent/node-triton#173], [joyent/node-triton#174] and
|
||||||
|
[joyent/node-triton#175] Add support for creating and managing NFS shared
|
||||||
|
volumes. New `triton volume` commands are available:
|
||||||
|
|
||||||
|
* `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.
|
||||||
|
|
||||||
|
Note that these commands are hidden for now. They will be made visible by
|
||||||
|
default once the server-side support for volumes is shipped in Triton.
|
||||||
|
|
||||||
## 5.2.1
|
## 5.2.1
|
||||||
|
|
||||||
|
@ -133,6 +133,12 @@ function complete_tritonnetwork {
|
|||||||
compgen $compgen_opts -W "$candidates" -- "$word"
|
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function complete_tritonvolume {
|
||||||
|
local word="$1"
|
||||||
|
candidates=$(_complete_tritondata volumes)
|
||||||
|
compgen $compgen_opts -W "$candidates" -- "$word"
|
||||||
|
}
|
||||||
|
|
||||||
function complete_tritonfwrule {
|
function complete_tritonfwrule {
|
||||||
local word="$1"
|
local word="$1"
|
||||||
candidates=$(_complete_tritondata fwrules)
|
candidates=$(_complete_tritondata fwrules)
|
||||||
|
18
lib/cli.js
18
lib/cli.js
@ -463,6 +463,21 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'volumes':
|
||||||
|
tritonapi.cloudapi.listVolumes({}, function (err, vols) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
completions = [];
|
||||||
|
vols.forEach(function (vol) {
|
||||||
|
completions.push(vol.name);
|
||||||
|
completions.push(vol.id);
|
||||||
|
});
|
||||||
|
arg.completions = completions.join('\n') + '\n';
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'affinityrules':
|
case 'affinityrules':
|
||||||
/*
|
/*
|
||||||
* We exclude ids, in favour of just inst names here. The only
|
* We exclude ids, in favour of just inst names here. The only
|
||||||
@ -695,6 +710,9 @@ CLI.prototype.do_cloudapi = require('./do_cloudapi');
|
|||||||
CLI.prototype.do_badger = require('./do_badger');
|
CLI.prototype.do_badger = require('./do_badger');
|
||||||
CLI.prototype.do_rbac = require('./do_rbac');
|
CLI.prototype.do_rbac = require('./do_rbac');
|
||||||
|
|
||||||
|
// Volumes
|
||||||
|
CLI.prototype.do_volumes = require('./do_volumes');
|
||||||
|
CLI.prototype.do_volume = require('./do_volume');
|
||||||
|
|
||||||
|
|
||||||
//---- mainline
|
//---- mainline
|
||||||
|
147
lib/cloudapi2.js
147
lib/cloudapi2.js
@ -2300,7 +2300,154 @@ CloudApi.prototype.setRoleTags = function setRoleTags(opts, cb) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a volume by id.
|
||||||
|
*
|
||||||
|
* @param {Object} opts
|
||||||
|
* - id {UUID} Required. The volume id.
|
||||||
|
* @param {Function} cb of the form `function (err, volume, res)`
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.getVolume = function getVolume(opts, cb) {
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.uuid(opts.id, 'opts.id');
|
||||||
|
|
||||||
|
var endpoint = format('/%s/volumes/%s', this.account, opts.id);
|
||||||
|
this._passThrough(endpoint, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the account's volumes.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {Function} callback - called like `function (err, volumes)`
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.listVolumes = function listVolumes(options, cb) {
|
||||||
|
var endpoint = format('/%s/volumes', this.account);
|
||||||
|
this._passThrough(endpoint, options, cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a volume for the account.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* - name {String} Optional: the name of the volume to be created
|
||||||
|
* - size {Number} Optional: a number representing the size of the volume
|
||||||
|
* to be created in mebibytes.
|
||||||
|
* - networks {Array} Optional: an array that contains the uuids of all the
|
||||||
|
* networks 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)`
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.createVolume = function createVolume(options, cb) {
|
||||||
|
assert.object(options, 'options');
|
||||||
|
assert.optionalString(options.name, 'options.name');
|
||||||
|
assert.optionalNumber(options.size, 'options.size');
|
||||||
|
assert.optionalArrayOfUuid(options.networks, 'options.networks');
|
||||||
|
assert.string(options.type, 'options.type');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
this._request({
|
||||||
|
method: 'POST',
|
||||||
|
path: format('/%s/volumes', this.account),
|
||||||
|
data: {
|
||||||
|
name: options.name,
|
||||||
|
size: options.size,
|
||||||
|
networks: (options.networks ? options.networks : undefined),
|
||||||
|
type: options.type
|
||||||
|
}
|
||||||
|
}, function (err, req, res, body) {
|
||||||
|
cb(err, body, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an account's volume.
|
||||||
|
*
|
||||||
|
* @param {String} volumeUuid
|
||||||
|
* @param {Function} callback - called like `function (err, volume, res)`
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.deleteVolume = function deleteVolume(volumeUuid, cb) {
|
||||||
|
assert.uuid(volumeUuid, 'volumeUuid');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
this._request({
|
||||||
|
method: 'DELETE',
|
||||||
|
path: format('/%s/volumes/%s', this.account, volumeUuid)
|
||||||
|
}, function (err, req, res, body) {
|
||||||
|
cb(err, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a volume to go one of a set of specfic states.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* - {String} id - machine UUID
|
||||||
|
* - {Array of String} states - desired state
|
||||||
|
* - {Number} interval (optional) - time in ms to poll
|
||||||
|
* - {Number} timeout (optional) - time in ms after which "callback" is
|
||||||
|
* called with an error object if the volume hasn't yet transitioned to
|
||||||
|
* one of the states in "opts.states".
|
||||||
|
* @param {Function} callback - called when state is reached or on error
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.waitForVolumeStates =
|
||||||
|
function waitForVolumeStates(opts, callback) {
|
||||||
|
var self = this;
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.uuid(opts.id, 'opts.id');
|
||||||
|
assert.arrayOfString(opts.states, 'opts.states');
|
||||||
|
assert.optionalNumber(opts.interval, 'opts.interval');
|
||||||
|
assert.optionalNumber(opts.timeout, 'opts.timeout');
|
||||||
|
assert.func(callback, 'callback');
|
||||||
|
|
||||||
|
var interval = (opts.interval === undefined ? 1000 : opts.interval);
|
||||||
|
if (opts.timeout !== undefined) {
|
||||||
|
interval = Math.min(interval, opts.timeout);
|
||||||
|
}
|
||||||
|
assert.ok(interval > 0, 'interval must be a positive number');
|
||||||
|
|
||||||
|
var startTime = process.hrtime();
|
||||||
|
var timeout = opts.timeout;
|
||||||
|
|
||||||
|
poll();
|
||||||
|
|
||||||
|
function poll() {
|
||||||
|
self.getVolume({
|
||||||
|
id: opts.id
|
||||||
|
}, function (err, vol, res) {
|
||||||
|
var elapsedTime;
|
||||||
|
var timedOut = false;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
callback(err, null, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (opts.states.indexOf(vol.state) !== -1) {
|
||||||
|
callback(null, vol, res);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (timeout !== undefined) {
|
||||||
|
elapsedTime = common.monotonicTimeDiffMs(startTime);
|
||||||
|
if (elapsedTime > timeout) {
|
||||||
|
timedOut = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
callback(new errors.TimeoutError(format('timeout waiting '
|
||||||
|
+ 'for state changes on volume %s (elapsed %ds)',
|
||||||
|
opts.id, Math.round(elapsedTime / 1000))));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setTimeout(poll, interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Exports
|
// --- Exports
|
||||||
|
|
||||||
|
203
lib/common.js
203
lib/common.js
@ -126,34 +126,125 @@ function jsonStream(arr, stream) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* given an array of key=value pairs, break them into an object
|
* Parses the string "kv" of the form 'key=value' and returns an object that
|
||||||
|
* represents it with the form {'key': value}. If "key"" in the "kv" string is
|
||||||
|
* not included in the list "validKeys", it throws an error. It also throws an
|
||||||
|
* error if the string "kv" is malformed.
|
||||||
|
*
|
||||||
|
* By default, converts the values as if they were JSON representations of JS
|
||||||
|
* types, e.g the string 'false' is converted to the boolean primitive "false".
|
||||||
|
*
|
||||||
|
* @param {String} kv
|
||||||
|
* @param {String[]} validKeys: Optional
|
||||||
|
* @param {Object} options: Optional
|
||||||
|
* - @param disableTypeConversions {Boolean} Optional. If true, then no
|
||||||
|
* type conversion of values is performed, and all values are returned as
|
||||||
|
* strings.
|
||||||
|
* - @param typeHintFromKey {Object} Optional. Type hints for input keys.
|
||||||
|
* E.g. if parsing 'foo=false' and `typeHintFromKey={foo: 'string'}`,
|
||||||
|
* then we do NOT parse it to a boolean `false`.
|
||||||
|
* - @param failOnEmptyValue {Boolean} Optional - If true, throws an error
|
||||||
|
* if a given key's value is the empty string. Default is false.
|
||||||
|
*/
|
||||||
|
function _parseKeyValue(kv, validKeys, options) {
|
||||||
|
assert.string(kv, 'kv');
|
||||||
|
assert.optionalArrayOfString(validKeys, 'validKeys');
|
||||||
|
assert.optionalObject(options, 'options');
|
||||||
|
options = options || {};
|
||||||
|
assert.optionalBool(options.disableTypeConversions,
|
||||||
|
'options.disableTypeConversions');
|
||||||
|
assert.optionalObject(options.typeHintFromKey, 'options.typeHintFromKey');
|
||||||
|
assert.optionalBool(options.failOnEmptyValue, 'options.failOnEmptyValue');
|
||||||
|
|
||||||
|
var idx = kv.indexOf('=');
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new errors.UsageError(format('invalid key=value: "%s"', kv));
|
||||||
|
}
|
||||||
|
var k = kv.slice(0, idx);
|
||||||
|
var typeHint;
|
||||||
|
var v = kv.slice(idx + 1);
|
||||||
|
|
||||||
|
if (validKeys && validKeys.indexOf(k) === -1) {
|
||||||
|
throw new errors.UsageError(format(
|
||||||
|
'invalid key: "%s" (must be one of "%s")',
|
||||||
|
k, validKeys.join('", "')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v === '' && options.failOnEmptyValue) {
|
||||||
|
throw new Error(format('key "%s" must have a value', k));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.disableTypeConversions !== true) {
|
||||||
|
if (options.typeHintFromKey !== undefined) {
|
||||||
|
typeHint = options.typeHintFromKey[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeHint === 'string') {
|
||||||
|
// Leave `v` a string.
|
||||||
|
/* jsl:pass */
|
||||||
|
} else if (v === '') {
|
||||||
|
v = null;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
v = JSON.parse(v);
|
||||||
|
} catch (e) {
|
||||||
|
/* pass */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: k,
|
||||||
|
value: v
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {Object[]} validKeys - an array of objects representing valid keys
|
||||||
*
|
* that can be used in the first argument "kvs".
|
||||||
* TODO: merge this with objFromKeyValueArgs !
|
* @param {String} compositionType - the way each key/value pair will be
|
||||||
|
* combined to form a JSON predicate. Valid values are 'or' and 'and'.
|
||||||
*/
|
*/
|
||||||
function kvToObj(kvs, valid) {
|
function jsonPredFromKv(kvs, validKeys, compositionType) {
|
||||||
assert.arrayOfString(kvs, 'kvs');
|
assert.arrayOfString(kvs, 'kvs');
|
||||||
assert.optionalArrayOfString(valid, 'valid');
|
assert.arrayOfString(validKeys, 'validKeys');
|
||||||
|
assert.string(compositionType, 'string');
|
||||||
|
assert.ok(compositionType === 'or' || compositionType === 'and',
|
||||||
|
'compositionType');
|
||||||
|
|
||||||
var o = {};
|
var keyName;
|
||||||
for (var i = 0; i < kvs.length; i++) {
|
var predicate = {};
|
||||||
var kv = kvs[i];
|
var parsedKeyValue;
|
||||||
var idx = kv.indexOf('=');
|
var parsedKeyValues;
|
||||||
if (idx === -1)
|
var parseOpts = {
|
||||||
throw new errors.UsageError(format(
|
disableDotted: true,
|
||||||
'invalid filter: "%s" (must be of the form "field=value")',
|
validKeys: validKeys,
|
||||||
kv));
|
failOnEmptyValue: true
|
||||||
var k = kv.slice(0, idx);
|
};
|
||||||
var v = kv.slice(idx + 1);
|
|
||||||
if (valid && valid.indexOf(k) === -1)
|
if (kvs.length === 0) {
|
||||||
throw new errors.UsageError(format(
|
return predicate;
|
||||||
'invalid filter name: "%s" (must be one of "%s")',
|
|
||||||
k, valid.join('", "')));
|
|
||||||
o[k] = v;
|
|
||||||
}
|
}
|
||||||
return o;
|
|
||||||
|
if (kvs.length === 1) {
|
||||||
|
parsedKeyValue = _parseKeyValue(kvs[0], validKeys, parseOpts);
|
||||||
|
predicate.eq = [parsedKeyValue.key, parsedKeyValue.value];
|
||||||
|
} else {
|
||||||
|
predicate[compositionType] = [];
|
||||||
|
parsedKeyValues = objFromKeyValueArgs(kvs, parseOpts);
|
||||||
|
|
||||||
|
for (keyName in parsedKeyValues) {
|
||||||
|
predicate[compositionType].push({
|
||||||
|
eq: [keyName, parsedKeyValues[keyName]]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return predicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1034,6 +1125,10 @@ function tildeSync(s) {
|
|||||||
* - @param typeHintFromKey {Object} Optional. Type hints for input keys.
|
* - @param typeHintFromKey {Object} Optional. Type hints for input keys.
|
||||||
* E.g. if parsing 'foo=false' and `typeHintFromKey={foo: 'string'}`,
|
* E.g. if parsing 'foo=false' and `typeHintFromKey={foo: 'string'}`,
|
||||||
* then we do NOT parse it to a boolean `false`.
|
* then we do NOT parse it to a boolean `false`.
|
||||||
|
* - @param validKeys {String[]} Optional. List of valid keys. By default
|
||||||
|
* all keys are valid.
|
||||||
|
* - @param failOnEmptyValue {Boolean} Optional. If true, then a key with a
|
||||||
|
* value that is the empty string throws an error. Default is false.
|
||||||
*/
|
*/
|
||||||
function objFromKeyValueArgs(args, opts)
|
function objFromKeyValueArgs(args, opts)
|
||||||
{
|
{
|
||||||
@ -1041,45 +1136,31 @@ function objFromKeyValueArgs(args, opts)
|
|||||||
assert.optionalObject(opts, 'opts');
|
assert.optionalObject(opts, 'opts');
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
assert.optionalBool(opts.disableDotted, 'opts.disableDotted');
|
assert.optionalBool(opts.disableDotted, 'opts.disableDotted');
|
||||||
|
assert.optionalBool(opts.disableTypeConversions,
|
||||||
|
'opts.disableTypeConversions');
|
||||||
assert.optionalObject(opts.typeHintFromKey, opts.typeHintFromKey);
|
assert.optionalObject(opts.typeHintFromKey, opts.typeHintFromKey);
|
||||||
var typeHintFromKey = opts.typeHintFromKey || {};
|
assert.optionalArrayOfString(opts.validKeys, 'opts.validKeys');
|
||||||
|
assert.optionalBool(opts.failOnEmptyValue, 'opts.failOnEmptyValue');
|
||||||
|
|
||||||
var obj = {};
|
var obj = {};
|
||||||
args.forEach(function (arg) {
|
args.forEach(function (arg) {
|
||||||
var kv = strsplit(arg, '=', 2);
|
var parsedKeyValue = _parseKeyValue(arg, opts.validKeys, {
|
||||||
if (kv.length < 2) {
|
typeHintFromKey: opts.typeHintFromKey,
|
||||||
throw new TypeError(format('invalid key=value argument: "%s"',
|
disableTypeConversions: opts.disableTypeConversions,
|
||||||
arg));
|
failOnEmptyValue: opts.failOnEmptyValue
|
||||||
}
|
});
|
||||||
|
|
||||||
var k = kv[0];
|
|
||||||
var t = typeHintFromKey[k];
|
|
||||||
|
|
||||||
var v = kv[1];
|
|
||||||
if (t === 'string') {
|
|
||||||
// Leave `v` a string.
|
|
||||||
/* jsl:pass */
|
|
||||||
} else if (v === '') {
|
|
||||||
v = null;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
v = JSON.parse(v);
|
|
||||||
} catch (e) {
|
|
||||||
/* pass */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.disableDotted) {
|
if (opts.disableDotted) {
|
||||||
obj[k] = v;
|
obj[parsedKeyValue.key] = parsedKeyValue.value;
|
||||||
} else {
|
} else {
|
||||||
var dotted = strsplit(k, '.', 2);
|
var dotted = strsplit(parsedKeyValue.key, '.', 2);
|
||||||
if (dotted.length > 1) {
|
if (dotted.length > 1) {
|
||||||
if (!obj[dotted[0]]) {
|
if (!obj[dotted[0]]) {
|
||||||
obj[dotted[0]] = {};
|
obj[dotted[0]] = {};
|
||||||
}
|
}
|
||||||
obj[dotted[0]][dotted[1]] = v;
|
obj[dotted[0]][dotted[1]] = parsedKeyValue.value;
|
||||||
} else {
|
} else {
|
||||||
obj[k] = v;
|
obj[parsedKeyValue.key] = parsedKeyValue.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1087,6 +1168,25 @@ function objFromKeyValueArgs(args, opts)
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the time difference between the current time and the time
|
||||||
|
* represented by "relativeTo" in milliseconds. It doesn't use the built-in
|
||||||
|
* `Date` class internally, and instead uses a node facility that uses a
|
||||||
|
* monotonic clock. Thus, the time difference computed is not subject to time
|
||||||
|
* drifting due to e.g changes in the wall clock system time.
|
||||||
|
*
|
||||||
|
* @param {arrayOfNumber} relativeTo: an array representing the starting time as
|
||||||
|
* returned by `process.hrtime()` from which to compute the
|
||||||
|
* time difference.
|
||||||
|
*/
|
||||||
|
function monotonicTimeDiffMs(relativeTo) {
|
||||||
|
assert.arrayOfNumber(relativeTo, 'relativeTo');
|
||||||
|
|
||||||
|
var diff = process.hrtime(relativeTo);
|
||||||
|
var ms = (diff[0] * 1e3) + (diff[1] / 1e6); // in milliseconds
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Parse the given line into an argument vector, e.g. for use in sending to
|
* Parse the given line into an argument vector, e.g. for use in sending to
|
||||||
@ -1200,7 +1300,6 @@ module.exports = {
|
|||||||
zeroPad: zeroPad,
|
zeroPad: zeroPad,
|
||||||
boolFromString: boolFromString,
|
boolFromString: boolFromString,
|
||||||
jsonStream: jsonStream,
|
jsonStream: jsonStream,
|
||||||
kvToObj: kvToObj,
|
|
||||||
longAgo: longAgo,
|
longAgo: longAgo,
|
||||||
isUUID: isUUID,
|
isUUID: isUUID,
|
||||||
humanDurationFromMs: humanDurationFromMs,
|
humanDurationFromMs: humanDurationFromMs,
|
||||||
@ -1226,6 +1325,8 @@ module.exports = {
|
|||||||
deepEqual: deepEqual,
|
deepEqual: deepEqual,
|
||||||
tildeSync: tildeSync,
|
tildeSync: tildeSync,
|
||||||
objFromKeyValueArgs: objFromKeyValueArgs,
|
objFromKeyValueArgs: objFromKeyValueArgs,
|
||||||
argvFromLine: argvFromLine
|
argvFromLine: argvFromLine,
|
||||||
|
jsonPredFromKv: jsonPredFromKv,
|
||||||
|
monotonicTimeDiffMs: monotonicTimeDiffMs
|
||||||
};
|
};
|
||||||
// vim: set softtabstop=4 shiftwidth=4:
|
// vim: set softtabstop=4 shiftwidth=4:
|
||||||
|
@ -54,7 +54,11 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
var listOpts;
|
var listOpts;
|
||||||
try {
|
try {
|
||||||
listOpts = common.kvToObj(args, validFilters);
|
listOpts = common.objFromKeyValueArgs(args, {
|
||||||
|
disableDotted: true,
|
||||||
|
validKeys: validFilters,
|
||||||
|
disableTypeConversions: true
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback(e);
|
callback(e);
|
||||||
return;
|
return;
|
||||||
|
@ -61,7 +61,11 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
var listOpts;
|
var listOpts;
|
||||||
try {
|
try {
|
||||||
listOpts = common.kvToObj(args, validFilters);
|
listOpts = common.objFromKeyValueArgs(args, {
|
||||||
|
disableDotted: true,
|
||||||
|
validKeys: validFilters,
|
||||||
|
disableTypeConversions: true
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback(e);
|
callback(e);
|
||||||
return;
|
return;
|
||||||
|
@ -48,7 +48,11 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
var sort = opts.s.split(',');
|
var sort = opts.s.split(',');
|
||||||
var filters;
|
var filters;
|
||||||
try {
|
try {
|
||||||
filters = common.kvToObj(args, validFilters);
|
filters = common.objFromKeyValueArgs(args, {
|
||||||
|
disableDotted: true,
|
||||||
|
validKeys: validFilters,
|
||||||
|
disableTypeConversions: true
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback(e);
|
callback(e);
|
||||||
return;
|
return;
|
||||||
|
@ -63,7 +63,11 @@ function do_list(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
var listOpts;
|
var listOpts;
|
||||||
try {
|
try {
|
||||||
listOpts = common.kvToObj(args, validFilters);
|
listOpts = common.objFromKeyValueArgs(args, {
|
||||||
|
disableDotted: true,
|
||||||
|
validKeys: validFilters,
|
||||||
|
disableTypeConversions: true
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
callback(e);
|
callback(e);
|
||||||
return;
|
return;
|
||||||
|
200
lib/do_volume/do_create.js
Normal file
200
lib/do_volume/do_create.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* 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 create ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
var assert = require('assert-plus');
|
||||||
|
var format = require('util').format;
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
|
var distractions = require('../distractions');
|
||||||
|
var errors = require('../errors');
|
||||||
|
var mod_volumes = require('../volumes');
|
||||||
|
|
||||||
|
function do_create(subcmd, opts, args, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (opts.help) {
|
||||||
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length !== 0) {
|
||||||
|
cb(new errors.UsageError('incorrect number of args'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vasync.pipeline({arg: {cli: this.top}, 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();
|
||||||
|
},
|
||||||
|
common.cliSetupTritonApi,
|
||||||
|
function createVolume(ctx, next) {
|
||||||
|
var createVolumeParams = {
|
||||||
|
type: 'tritonnfs',
|
||||||
|
name: opts.name,
|
||||||
|
network: opts.network,
|
||||||
|
size: ctx.size
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.type) {
|
||||||
|
createVolumeParams.type = opts.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.top.tritonapi.createVolume(createVolumeParams,
|
||||||
|
function onRes(volCreateErr, volume) {
|
||||||
|
if (!volCreateErr && !opts.json) {
|
||||||
|
console.log('Creating volume %s (%s)', volume.name,
|
||||||
|
volume.id);
|
||||||
|
}
|
||||||
|
ctx.volume = volume;
|
||||||
|
next(volCreateErr);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function maybeWait(ctx, next) {
|
||||||
|
var distraction;
|
||||||
|
var waitTimeout = opts.wait_timeout === undefined ?
|
||||||
|
undefined : opts.wait_timeout * 1000;
|
||||||
|
|
||||||
|
if (!opts.wait) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.stderr.isTTY && opts.wait.length > 1) {
|
||||||
|
distraction = distractions.createDistraction(opts.wait.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.top.tritonapi.cloudapi.waitForVolumeStates({
|
||||||
|
id: ctx.volume.id,
|
||||||
|
states: ['ready', 'failed'],
|
||||||
|
timeout: waitTimeout
|
||||||
|
}, function onWaitDone(waitErr, volume) {
|
||||||
|
if (distraction) {
|
||||||
|
distraction.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitErr) {
|
||||||
|
next(waitErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.object(volume, 'volume');
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(volume));
|
||||||
|
} else if (volume.state === 'ready') {
|
||||||
|
console.log('Created volume %s (%s)', volume.name,
|
||||||
|
volume.id);
|
||||||
|
} else {
|
||||||
|
next(new Error(format('failed to create volume %s (%s)',
|
||||||
|
volume.name, volume.id)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]}, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
do_create.options = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Show this help.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Create options'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['name', 'n'],
|
||||||
|
helpArg: 'NAME',
|
||||||
|
type: 'string',
|
||||||
|
help: 'Volume name. If not given, one will be generated server-side.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['type', 't'],
|
||||||
|
helpArg: 'TYPE',
|
||||||
|
type: 'string',
|
||||||
|
help: 'Volume type. Default and currently only supported type is ' +
|
||||||
|
'"tritonnfs".'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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.',
|
||||||
|
completionType: 'tritonvolumesize'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['network', 'N'],
|
||||||
|
type: 'string',
|
||||||
|
helpArg: 'NETWORK',
|
||||||
|
help: 'A network (ID, name or short id) to which the newly created ' +
|
||||||
|
'volume will be attached. By default, the newly created volume ' +
|
||||||
|
'will be attached to the account\'s default fabric network.',
|
||||||
|
completionType: 'tritonnetwork'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Other options'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['json', 'j'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'JSON stream output.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['wait', 'w'],
|
||||||
|
type: 'arrayOfBool',
|
||||||
|
help: 'Wait for the creation to complete. Use multiple times for a ' +
|
||||||
|
'spinner.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['wait-timeout'],
|
||||||
|
type: 'positiveInteger',
|
||||||
|
help: 'The number of seconds to wait before timing out with an error.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
do_create.synopses = ['{{name}} {{cmd}} [OPTIONS]'];
|
||||||
|
|
||||||
|
do_create.help = [
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
|
'Create a volume.',
|
||||||
|
'',
|
||||||
|
'{{usage}}',
|
||||||
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
'Note: Currently this dumps prettified JSON by default. That might change',
|
||||||
|
'in the future. Use "-j" to explicitly get JSON output.'
|
||||||
|
/* END JSSTYLED */
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
do_create.completionArgtypes = ['tritonvolume', 'none'];
|
||||||
|
|
||||||
|
module.exports = do_create;
|
157
lib/do_volume/do_delete.js
Normal file
157
lib/do_volume/do_delete.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* 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 delete ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
var assert = require('assert-plus');
|
||||||
|
var format = require('util').format;
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
|
var distractions = require('../distractions');
|
||||||
|
var errors = require('../errors');
|
||||||
|
|
||||||
|
|
||||||
|
function do_delete(subcmd, opts, args, cb) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (opts.help) {
|
||||||
|
self.do_help('help', {}, [subcmd], cb);
|
||||||
|
return;
|
||||||
|
} else if (args.length < 1) {
|
||||||
|
cb(new errors.UsageError('missing VOLUME arg(s)'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = {
|
||||||
|
volumeIds: args,
|
||||||
|
cli: this.top
|
||||||
|
};
|
||||||
|
|
||||||
|
vasync.pipeline({arg: context, funcs: [
|
||||||
|
common.cliSetupTritonApi,
|
||||||
|
function confirm(ctx, next) {
|
||||||
|
var promptMsg;
|
||||||
|
|
||||||
|
if (opts.yes) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.volumeIds.length === 1) {
|
||||||
|
promptMsg = format('Delete volume %s? [y/n] ',
|
||||||
|
ctx.volumeIds[0]);
|
||||||
|
} else {
|
||||||
|
promptMsg = format('Delete %d volumes? [y/n] ',
|
||||||
|
ctx.volumeIds.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
common.promptYesNo({msg: promptMsg},
|
||||||
|
function onPromptAnswered(answer) {
|
||||||
|
if (answer !== 'y') {
|
||||||
|
console.error('Aborting');
|
||||||
|
/*
|
||||||
|
* Early abort signal.
|
||||||
|
*/
|
||||||
|
next(true);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function deleteVolumes(ctx, next) {
|
||||||
|
vasync.forEachParallel({
|
||||||
|
func: function doDeleteVolume(volumeId, done) {
|
||||||
|
if (opts.wait === undefined) {
|
||||||
|
console.log('Deleting volume %s', volumeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.top.tritonapi.deleteVolume({
|
||||||
|
id: volumeId,
|
||||||
|
wait: opts.wait && opts.wait.length > 0,
|
||||||
|
waitTimeout: opts.wait_timeout * 1000
|
||||||
|
}, function onVolDeleted(volDelErr) {
|
||||||
|
if (!volDelErr) {
|
||||||
|
if (opts.wait !== undefined) {
|
||||||
|
console.log('Deleted volume %s', volumeId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Error when deleting volume %s: %s',
|
||||||
|
volumeId, volDelErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
done(volDelErr);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
inputs: ctx.volumeIds
|
||||||
|
}, next);
|
||||||
|
}
|
||||||
|
]}, function onDone(err) {
|
||||||
|
if (err === true) {
|
||||||
|
/*
|
||||||
|
* Answered 'no' to confirmation to delete.
|
||||||
|
*/
|
||||||
|
err = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
do_delete.options = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Show this help.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Other options'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['wait', 'w'],
|
||||||
|
type: 'arrayOfBool',
|
||||||
|
help: 'Wait for the deletion to complete. Use multiple times for a ' +
|
||||||
|
'spinner.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['wait-timeout'],
|
||||||
|
type: 'positiveInteger',
|
||||||
|
default: 120,
|
||||||
|
help: 'The number of seconds to wait before timing out with an error. '
|
||||||
|
+ 'The default is 120 seconds.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['yes', 'y'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Answer yes to confirmation to delete.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
do_delete.synopses = ['{{name}} {{cmd}} [OPTIONS] VOLUME [VOLUME ...]'];
|
||||||
|
|
||||||
|
do_delete.help = [
|
||||||
|
'Deletes a volume.',
|
||||||
|
'',
|
||||||
|
'{{usage}}',
|
||||||
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
'',
|
||||||
|
'Where VOLUME is a volume id (full UUID), exact name, or short id.'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
do_delete.completionArgtypes = ['tritonvolume'];
|
||||||
|
do_delete.aliases = ['rm'];
|
||||||
|
|
||||||
|
module.exports = do_delete;
|
79
lib/do_volume/do_get.js
Normal file
79
lib/do_volume/do_get.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* 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 get ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
var format = require('util').format;
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
|
var errors = require('../errors');
|
||||||
|
|
||||||
|
function do_get(subcmd, opts, args, callback) {
|
||||||
|
if (opts.help) {
|
||||||
|
this.do_help('help', {}, [subcmd], callback);
|
||||||
|
return;
|
||||||
|
} else if (args.length !== 1) {
|
||||||
|
return callback(new errors.UsageError(format(
|
||||||
|
'incorrect number of args (%d)', args.length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var tritonapi = this.top.tritonapi;
|
||||||
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
|
if (setupErr) {
|
||||||
|
callback(setupErr);
|
||||||
|
}
|
||||||
|
tritonapi.getVolume(args[0], function onRes(err, volume) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(volume));
|
||||||
|
} else {
|
||||||
|
console.log(JSON.stringify(volume, null, 4));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
do_get.options = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Show this help.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['json', 'j'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'JSON stream output.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
do_get.synopses = ['{{name}} {{cmd}} [OPTIONS] VOLUME'];
|
||||||
|
|
||||||
|
do_get.help = [
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
|
'Get a volume.',
|
||||||
|
'',
|
||||||
|
'{{usage}}',
|
||||||
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
'',
|
||||||
|
'Where VOLUME is a volume id (full UUID), exact name, or short id.',
|
||||||
|
'',
|
||||||
|
'Note: Currently this dumps prettified JSON by default. That might change',
|
||||||
|
'in the future. Use "-j" to explicitly get JSON output.'
|
||||||
|
/* END JSSTYLED */
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
do_get.completionArgtypes = ['tritonvolume', 'none'];
|
||||||
|
|
||||||
|
module.exports = do_get;
|
151
lib/do_volume/do_list.js
Normal file
151
lib/do_volume/do_list.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* 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 list ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 validFilters = [
|
||||||
|
'name',
|
||||||
|
'size',
|
||||||
|
'state',
|
||||||
|
'owner',
|
||||||
|
'type'
|
||||||
|
];
|
||||||
|
|
||||||
|
// columns default without -o
|
||||||
|
var columnsDefault = 'shortid,name,size,type,state,age';
|
||||||
|
|
||||||
|
// columns default with -l
|
||||||
|
var columnsDefaultLong = 'id,name,size,type,state,created';
|
||||||
|
|
||||||
|
// sort default with -s
|
||||||
|
var sortDefault = 'created';
|
||||||
|
|
||||||
|
function do_list(subcmd, opts, args, callback) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (opts.help) {
|
||||||
|
this.do_help('help', {}, [subcmd], callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var columns = columnsDefault;
|
||||||
|
if (opts.o) {
|
||||||
|
columns = opts.o;
|
||||||
|
} else if (opts.long) {
|
||||||
|
columns = columnsDefaultLong;
|
||||||
|
}
|
||||||
|
columns = columns.split(',');
|
||||||
|
|
||||||
|
var sort = opts.s.split(',');
|
||||||
|
|
||||||
|
var filterPredicate;
|
||||||
|
var listOpts;
|
||||||
|
|
||||||
|
if (args) {
|
||||||
|
try {
|
||||||
|
filterPredicate = common.jsonPredFromKv(args, validFilters, 'and');
|
||||||
|
} catch (e) {
|
||||||
|
callback(new VError(e, 'invalid filters'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsprim.deepEqual(filterPredicate, {})) {
|
||||||
|
filterPredicate = { ne: ['state', 'failed'] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterPredicate) {
|
||||||
|
listOpts = {
|
||||||
|
predicate: JSON.stringify(filterPredicate)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||||
|
if (setupErr) {
|
||||||
|
callback(setupErr);
|
||||||
|
}
|
||||||
|
self.top.tritonapi.cloudapi.listVolumes(listOpts,
|
||||||
|
function onRes(listVolsErr, volumes, res) {
|
||||||
|
var now;
|
||||||
|
|
||||||
|
if (listVolsErr) {
|
||||||
|
return callback(listVolsErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
common.jsonStream(volumes);
|
||||||
|
} else {
|
||||||
|
now = new Date();
|
||||||
|
for (var i = 0; i < volumes.length; i++) {
|
||||||
|
var created;
|
||||||
|
var volume = volumes[i];
|
||||||
|
|
||||||
|
created = new Date(volume.create_timestamp);
|
||||||
|
|
||||||
|
volume.shortid = volume.id.split('-', 1)[0];
|
||||||
|
volume.created = volume.create_timestamp;
|
||||||
|
volume.age = common.longAgo(created, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
tabula(volumes, {
|
||||||
|
skipHeader: opts.H,
|
||||||
|
columns: columns,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
do_list.options = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Show this help.'
|
||||||
|
}
|
||||||
|
].concat(common.getCliTableOptions({
|
||||||
|
includeLong: true,
|
||||||
|
sortDefault: sortDefault
|
||||||
|
}));
|
||||||
|
|
||||||
|
do_list.synopses = ['{{name}} {{cmd}} [OPTIONS] [FILTERS]'];
|
||||||
|
|
||||||
|
do_list.help = [
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
|
'List volumes.',
|
||||||
|
,
|
||||||
|
'{{usage}}',
|
||||||
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
'Filters:',
|
||||||
|
' FIELD=VALUE Equality filter. Supported fields: name, type,',
|
||||||
|
' size, and state',
|
||||||
|
'',
|
||||||
|
'Fields (most are self explanatory, "*" indicates a field added client-side',
|
||||||
|
'for convenience):',
|
||||||
|
' shortid* A short ID prefix.',
|
||||||
|
' age* Approximate time since created, e.g. 1y, 2w.',
|
||||||
|
''
|
||||||
|
/* END JSSTYLED */
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
do_list.aliases = ['ls'];
|
||||||
|
|
||||||
|
module.exports = do_list;
|
53
lib/do_volume/index.js
Normal file
53
lib/do_volume/index.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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 ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Cmdln = require('cmdln').Cmdln;
|
||||||
|
var util = require('util');
|
||||||
|
|
||||||
|
function VolumeCLI(top) {
|
||||||
|
this.top = top;
|
||||||
|
Cmdln.call(this, {
|
||||||
|
name: top.name + ' volume',
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
|
desc: [
|
||||||
|
'List and manage Triton volumes.'
|
||||||
|
].join('\n'),
|
||||||
|
/* END JSSTYLED */
|
||||||
|
helpOpts: {
|
||||||
|
minHelpCol: 24 /* line up with option help */
|
||||||
|
},
|
||||||
|
helpSubcmds: [
|
||||||
|
'help',
|
||||||
|
'list',
|
||||||
|
'get',
|
||||||
|
'create',
|
||||||
|
'delete'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
util.inherits(VolumeCLI, Cmdln);
|
||||||
|
|
||||||
|
VolumeCLI.prototype.init = function init(opts, args, cb) {
|
||||||
|
this.log = this.top.log;
|
||||||
|
Cmdln.prototype.init.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
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.aliases = ['vol'];
|
||||||
|
|
||||||
|
VolumeCLI.hidden = true;
|
||||||
|
|
||||||
|
module.exports = VolumeCLI;
|
30
lib/do_volumes.js
Normal file
30
lib/do_volumes.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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 20167 Joyent, Inc.
|
||||||
|
*
|
||||||
|
* `triton volumes ...` bwcompat shortcut for `triton volumes list ...`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var targ = require('./do_volume/do_list');
|
||||||
|
|
||||||
|
function do_volumes(subcmd, opts, args, callback) {
|
||||||
|
this.handlerFromSubcmd('volume').dispatch({
|
||||||
|
subcmd: 'list',
|
||||||
|
opts: opts,
|
||||||
|
args: args
|
||||||
|
}, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
do_volumes.help = 'A shortcut for "triton volumes list".\n' + targ.help;
|
||||||
|
do_volumes.aliases = ['vols'];
|
||||||
|
do_volumes.hidden = true;
|
||||||
|
do_volumes.options = targ.options;
|
||||||
|
do_volumes.completionArgtypes = targ.completionArgtypes;
|
||||||
|
do_volumes.synopses = targ.synopses;
|
||||||
|
|
||||||
|
module.exports = do_volumes;
|
279
lib/tritonapi.js
279
lib/tritonapi.js
@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 Joyent, Inc.
|
* Copyright 2017 Joyent, Inc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
@ -113,6 +113,7 @@ var auth = require('smartdc-auth');
|
|||||||
var EventEmitter = require('events').EventEmitter;
|
var EventEmitter = require('events').EventEmitter;
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
|
var jsprim = require('jsprim');
|
||||||
var mkdirp = require('mkdirp');
|
var mkdirp = require('mkdirp');
|
||||||
var once = require('once');
|
var once = require('once');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
@ -122,6 +123,7 @@ var restifyBunyanSerializers =
|
|||||||
require('restify-clients/lib/helpers/bunyan').serializers;
|
require('restify-clients/lib/helpers/bunyan').serializers;
|
||||||
var tabula = require('tabula');
|
var tabula = require('tabula');
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
var VError = require('verror');
|
||||||
var sshpk = require('sshpk');
|
var sshpk = require('sshpk');
|
||||||
|
|
||||||
var cloudapi = require('./cloudapi2');
|
var cloudapi = require('./cloudapi2');
|
||||||
@ -184,6 +186,30 @@ function _stepInstId(arg, next) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.id`
|
||||||
|
* volume name, shortid or uuid, and determines the volume id (setting it
|
||||||
|
* as `arg.volId`).
|
||||||
|
*/
|
||||||
|
function _stepVolId(arg, next) {
|
||||||
|
assert.object(arg.client, 'arg.client');
|
||||||
|
assert.string(arg.id, 'arg.id');
|
||||||
|
|
||||||
|
if (common.isUUID(arg.id)) {
|
||||||
|
arg.volId = arg.id;
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
arg.client.getVolume(arg.id, function onGetVolume(getVolErr, vol) {
|
||||||
|
if (getVolErr) {
|
||||||
|
next(getVolErr);
|
||||||
|
} else {
|
||||||
|
arg.volId = vol.id;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.package`
|
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.package`
|
||||||
* package name, short id or uuid, and determines the package id (setting it
|
* package name, short id or uuid, and determines the package id (setting it
|
||||||
@ -2388,11 +2414,6 @@ TritonApi.prototype.rebootInstance = function rebootInstance(opts, cb) {
|
|||||||
function randrange(min, max) {
|
function randrange(min, max) {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
}
|
}
|
||||||
function timeDiffMs(relativeTo) {
|
|
||||||
var diff = process.hrtime(relativeTo);
|
|
||||||
var ms = (diff[0] * 1e3) + (diff[1] / 1e6); // in milliseconds
|
|
||||||
return ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
||||||
_stepInstId,
|
_stepInstId,
|
||||||
@ -2472,7 +2493,8 @@ TritonApi.prototype.rebootInstance = function rebootInstance(opts, cb) {
|
|||||||
|
|
||||||
if (!theRecord) {
|
if (!theRecord) {
|
||||||
if (opts.waitTimeout) {
|
if (opts.waitTimeout) {
|
||||||
var elapsedMs = timeDiffMs(startTime);
|
var elapsedMs =
|
||||||
|
common.monotonicTimeDiffMs(startTime);
|
||||||
if (elapsedMs > opts.waitTimeout) {
|
if (elapsedMs > opts.waitTimeout) {
|
||||||
next(new errors.TimeoutError(format('timeout '
|
next(new errors.TimeoutError(format('timeout '
|
||||||
+ 'waiting for instance %s reboot '
|
+ 'waiting for instance %s reboot '
|
||||||
@ -2676,6 +2698,249 @@ function _waitForInstanceUpdate(opts, cb) {
|
|||||||
setImmediate(poll);
|
setImmediate(poll);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a volume according to the parameters in "params" and calls the
|
||||||
|
* function "cb" when done.
|
||||||
|
*
|
||||||
|
* @param {Object} params
|
||||||
|
* - {String} type: Required. The type of the volume to create. The only
|
||||||
|
* valid value for now is "tritonnfs".
|
||||||
|
* - {String} name: Optional. The name of the volume to create. If not
|
||||||
|
* provided, a name will be automatically generated.
|
||||||
|
* - {String} network: Optional. The network name, short id or id on which
|
||||||
|
* the newly created volume will be reachable.
|
||||||
|
* - {Number} size: Optional. The desired size of the volume in mebibytes.
|
||||||
|
* If no size if provided, the volume will be created with the smallest
|
||||||
|
* possible size as outputted by CloudAPI's ListVolumeSizes endpoint.
|
||||||
|
* @param {Function} cb: `function (err, volume)`
|
||||||
|
*/
|
||||||
|
TritonApi.prototype.createVolume = function createVolume(params, cb) {
|
||||||
|
assert.object(params, 'params');
|
||||||
|
assert.string(params.type, 'params.type');
|
||||||
|
assert.optionalString(params.name, 'params.name');
|
||||||
|
assert.optionalString(params.network, 'params.network');
|
||||||
|
assert.optionalNumber(params.size, 'params.size');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var volumeCreated;
|
||||||
|
|
||||||
|
vasync.pipeline({arg: {client: self}, funcs: [
|
||||||
|
function doGetNetwork(arg, next) {
|
||||||
|
if (params.network === undefined || params.network === null) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
arg.client.getNetwork(params.network,
|
||||||
|
function onGetNetwork(getNetErr, net) {
|
||||||
|
if (getNetErr) {
|
||||||
|
next(getNetErr);
|
||||||
|
} else {
|
||||||
|
arg.networkId = net.id;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function doCreateVolume(arg, next) {
|
||||||
|
var createVolParams = jsprim.deepCopy(params);
|
||||||
|
if (arg.networkId) {
|
||||||
|
createVolParams.networks = [arg.networkId];
|
||||||
|
}
|
||||||
|
|
||||||
|
arg.client.cloudapi.createVolume(createVolParams,
|
||||||
|
function onVolumeCreated(volCreateErr, volume) {
|
||||||
|
volumeCreated = volume;
|
||||||
|
next(volCreateErr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]}, function done(err) {
|
||||||
|
cb(err, volumeCreated);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a volume by ID, exact name, or short ID, in that order.
|
||||||
|
*
|
||||||
|
* If there is more than one volume with that name, then this errors out.
|
||||||
|
*/
|
||||||
|
TritonApi.prototype.getVolume = function getVolume(id, cb) {
|
||||||
|
assert.string(id, 'id');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
var shortId;
|
||||||
|
var self = this;
|
||||||
|
var volume;
|
||||||
|
|
||||||
|
vasync.pipeline({funcs: [
|
||||||
|
function tryUuid(_, next) {
|
||||||
|
var uuid;
|
||||||
|
if (common.isUUID(id)) {
|
||||||
|
uuid = id;
|
||||||
|
} else {
|
||||||
|
shortId = common.normShortId(id);
|
||||||
|
if (shortId && common.isUUID(shortId)) {
|
||||||
|
// E.g. a >32-char docker volume ID normalized to a UUID.
|
||||||
|
uuid = shortId;
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cloudapi.getVolume({id: uuid}, function (err, vol) {
|
||||||
|
if (err) {
|
||||||
|
if (err.restCode === 'ResourceNotFound') {
|
||||||
|
err = new errors.ResourceNotFoundError(err,
|
||||||
|
format('volume with id %s was not found', uuid));
|
||||||
|
} else {
|
||||||
|
err = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
volume = vol;
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function tryName(_, next) {
|
||||||
|
if (volume !== undefined) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cloudapi.listVolumes({
|
||||||
|
predicate: JSON.stringify({
|
||||||
|
and: [
|
||||||
|
{ ne: ['state', 'failed'] },
|
||||||
|
{ eq: ['name', id] }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}, function (listVolumesErr, volumes) {
|
||||||
|
var err;
|
||||||
|
|
||||||
|
if (listVolumesErr) {
|
||||||
|
next(listVolumesErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.arrayOfObject(volumes, 'volumes');
|
||||||
|
|
||||||
|
if (volumes.length === 1) {
|
||||||
|
volume = volumes[0];
|
||||||
|
} else if (volumes.length > 1) {
|
||||||
|
err = new errors.TritonError(format(
|
||||||
|
'volume name "%s" is ambiguous: matches %d volumes',
|
||||||
|
id, volumes.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function tryShortId(_, next) {
|
||||||
|
if (volume !== undefined || !shortId) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cloudapi.listVolumes({
|
||||||
|
predicate: JSON.stringify({
|
||||||
|
ne: ['state', 'failed']
|
||||||
|
})
|
||||||
|
}, function (listVolumesErr, volumes) {
|
||||||
|
var candidate;
|
||||||
|
var candidateIdx = 0;
|
||||||
|
var err;
|
||||||
|
var match;
|
||||||
|
|
||||||
|
if (!listVolumesErr) {
|
||||||
|
for (candidateIdx in volumes) {
|
||||||
|
candidate = volumes[candidateIdx];
|
||||||
|
if (candidate.id.slice(0, shortId.length) === shortId) {
|
||||||
|
if (match) {
|
||||||
|
err = (new errors.TritonError(
|
||||||
|
'instance short id "%s" is ambiguous',
|
||||||
|
shortId));
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
match = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume = match;
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]}, function getVolDone(err) {
|
||||||
|
if (err || volume) {
|
||||||
|
cb(err, volume);
|
||||||
|
} else {
|
||||||
|
cb(new errors.ResourceNotFoundError(format(
|
||||||
|
'no volume with id, name or short id "%s" was found', id)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a volume by ID, exact name, or short ID, in that order.
|
||||||
|
*
|
||||||
|
* If there is more than one volume with that name, then this errors out.
|
||||||
|
*
|
||||||
|
* @param {Object} opts
|
||||||
|
* - {String} id: Required. The volume to delete's id, name or short ID.
|
||||||
|
* - {Boolean} wait: Optional. true if "cb" must be called once the volume
|
||||||
|
* is actually deleted, or deletion failed. If "false", "cb" will be
|
||||||
|
* called as soon as the deletion process is scheduled.
|
||||||
|
* - {Number} waitTimeout: Optional. if "wait" is true, represents the
|
||||||
|
* number of milliseconds after which to timeout (call `cb` with a
|
||||||
|
* timeout error) waiting.
|
||||||
|
* @param {Function} cb: `function (err)`
|
||||||
|
*/
|
||||||
|
TritonApi.prototype.deleteVolume = function deleteVolume(opts, cb) {
|
||||||
|
assert.string(opts.id, 'opts.id');
|
||||||
|
assert.optionalBool(opts.wait, 'opts.wait');
|
||||||
|
assert.optionalNumber(opts.waitTimeout, 'opts.waitTimeout');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var res;
|
||||||
|
|
||||||
|
vasync.pipeline({arg: {client: self, id: opts.id}, funcs: [
|
||||||
|
_stepVolId,
|
||||||
|
|
||||||
|
function doDelete(arg, next) {
|
||||||
|
self.cloudapi.deleteVolume(arg.volId,
|
||||||
|
function onVolDeleted(volDelErr, _, _res) {
|
||||||
|
res = _res;
|
||||||
|
next(volDelErr);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function waitForVolumeDeleted(arg, next) {
|
||||||
|
if (!opts.wait) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.cloudapi.waitForVolumeStates({
|
||||||
|
id: arg.volId,
|
||||||
|
states: ['failed'],
|
||||||
|
timeout: opts.waitTimeout
|
||||||
|
}, function onVolumeStateReached(err) {
|
||||||
|
if (VError.hasCauseWithName(err, 'VolumeNotFoundError')) {
|
||||||
|
// volume is gone, that's not an error
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]}, function onDeletionComplete(err) {
|
||||||
|
cb(err, null, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
//---- exports
|
//---- exports
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
68
lib/volumes.js
Normal file
68
lib/volumes.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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 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.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - 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.
|
||||||
|
*/
|
||||||
|
function parseVolumeSize(size) {
|
||||||
|
assert.string(size, 'size');
|
||||||
|
|
||||||
|
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)$/);
|
||||||
|
if (!matches) {
|
||||||
|
throwInvalidSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplierSymbol = matches[2];
|
||||||
|
if (multiplierSymbol) {
|
||||||
|
multiplier = MULTIPLIERS_TABLE[multiplierSymbol];
|
||||||
|
}
|
||||||
|
|
||||||
|
baseValue = Number(matches[1]);
|
||||||
|
|
||||||
|
if (isNaN(baseValue) || multiplier === undefined) {
|
||||||
|
throwInvalidSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseValue * multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
parseVolumeSize: parseVolumeSize
|
||||||
|
};
|
@ -11,7 +11,8 @@
|
|||||||
"bunyan": "1.5.1",
|
"bunyan": "1.5.1",
|
||||||
"cmdln": "4.1.2",
|
"cmdln": "4.1.2",
|
||||||
"extsprintf": "1.0.2",
|
"extsprintf": "1.0.2",
|
||||||
"getpass": "0.1.6",
|
"getpass": "0.1.6",
|
||||||
|
"jsprim": "1.4.0",
|
||||||
"lomstream": "1.1.0",
|
"lomstream": "1.1.0",
|
||||||
"mkdirp": "0.5.1",
|
"mkdirp": "0.5.1",
|
||||||
"once": "1.3.2",
|
"once": "1.3.2",
|
||||||
@ -26,7 +27,7 @@
|
|||||||
"strsplit": "1.0.0",
|
"strsplit": "1.0.0",
|
||||||
"tabula": "1.9.0",
|
"tabula": "1.9.0",
|
||||||
"vasync": "1.6.3",
|
"vasync": "1.6.3",
|
||||||
"verror": "1.6.0",
|
"verror": "1.10.0",
|
||||||
"which": "1.2.4",
|
"which": "1.2.4",
|
||||||
"wordwrap": "1.0.0"
|
"wordwrap": "1.0.0"
|
||||||
},
|
},
|
||||||
|
@ -97,7 +97,12 @@ var subs = [
|
|||||||
['rbac image-role-tags'],
|
['rbac image-role-tags'],
|
||||||
['rbac network-role-tags'],
|
['rbac network-role-tags'],
|
||||||
['rbac package-role-tags'],
|
['rbac package-role-tags'],
|
||||||
['rbac role-tags']
|
['rbac role-tags'],
|
||||||
|
['volume', 'vol'],
|
||||||
|
['volume list', 'volume ls', 'volumes', 'vols'],
|
||||||
|
['volume delete', 'volume rm'],
|
||||||
|
['volume create'],
|
||||||
|
['volume get']
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- Tests
|
// --- Tests
|
||||||
|
103
test/integration/cli-volumes-size.test.js
Normal file
103
test/integration/cli-volumes-size.test.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Test volume create command's size parameter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var format = require('util').format;
|
||||||
|
var os = require('os');
|
||||||
|
var test = require('tape');
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../../lib/common');
|
||||||
|
var h = require('./helpers');
|
||||||
|
|
||||||
|
var FABRIC_NETWORKS = [];
|
||||||
|
|
||||||
|
var testOpts = {
|
||||||
|
skip: !h.CONFIG.allowWriteActions
|
||||||
|
};
|
||||||
|
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 validVolumeSizeInMib = 20 * 1024;
|
||||||
|
|
||||||
|
tt.comment('Test config:');
|
||||||
|
Object.keys(h.CONFIG).forEach(function (key) {
|
||||||
|
var value = h.CONFIG[key];
|
||||||
|
tt.comment(format('- %s: %j', key, value));
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' cleanup leftover resources', function (t) {
|
||||||
|
h.triton(['volume', 'delete', '-y', '-w', validVolumeName].join(' '),
|
||||||
|
function onDelVolume(delVolErr, stdout, stderr) {
|
||||||
|
// If there was nothing to delete, this will fail so that's the
|
||||||
|
// normal case. Too bad we don't have a --force option.
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' triton volume create volume with non-default size',
|
||||||
|
function (t) {
|
||||||
|
h.triton([
|
||||||
|
'volume',
|
||||||
|
'create',
|
||||||
|
'--name',
|
||||||
|
validVolumeName,
|
||||||
|
'--size',
|
||||||
|
validVolumeSize,
|
||||||
|
'-w'
|
||||||
|
].join(' '), function (volCreateErr, stdout, stderr) {
|
||||||
|
t.equal(volCreateErr, null,
|
||||||
|
'volume creation should not error');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' check volume was created', function (t) {
|
||||||
|
h.safeTriton(t, ['volume', 'get', validVolumeName],
|
||||||
|
function onGetVolume(getVolErr, stdout) {
|
||||||
|
var volume;
|
||||||
|
|
||||||
|
t.equal(getVolErr, null,
|
||||||
|
'Getting volume should not error');
|
||||||
|
|
||||||
|
volume = JSON.parse(stdout);
|
||||||
|
t.equal(volume.size, validVolumeSizeInMib,
|
||||||
|
'volume size should be ' + validVolumeSizeInMib +
|
||||||
|
', got: ' + volume.size);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' delete volume', function (t) {
|
||||||
|
h.safeTriton(t, ['volume', 'delete', '-y', '-w', validVolumeName],
|
||||||
|
function onDelVolume(delVolErr, stdout) {
|
||||||
|
t.equal(delVolErr, null,
|
||||||
|
'Deleting volume should not error');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' check volume was deleted', function (t) {
|
||||||
|
h.triton(['volume', 'get', validVolumeName].join(' '),
|
||||||
|
function onGetVolume(getVolErr, stdout, stderr) {
|
||||||
|
t.ok(getVolErr,
|
||||||
|
'Getting volume ' + validVolumeName + 'after deleting it ' +
|
||||||
|
'should error');
|
||||||
|
t.notEqual(stderr.indexOf('ResourceNotFound'), -1,
|
||||||
|
'Getting volume ' + validVolumeName + 'should not find it');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
251
test/integration/cli-volumes.test.js
Normal file
251
test/integration/cli-volumes.test.js
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Test volume create command.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var format = require('util').format;
|
||||||
|
var os = require('os');
|
||||||
|
var test = require('tape');
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../../lib/common');
|
||||||
|
var h = require('./helpers');
|
||||||
|
|
||||||
|
var FABRIC_NETWORKS = [];
|
||||||
|
|
||||||
|
var testOpts = {
|
||||||
|
skip: !h.CONFIG.allowWriteActions
|
||||||
|
};
|
||||||
|
test('triton volume create ...', testOpts, function (tt) {
|
||||||
|
var currentVolume;
|
||||||
|
var validVolumeName =
|
||||||
|
h.makeResourceName('node-triton-test-volume-create-default');
|
||||||
|
|
||||||
|
tt.comment('Test config:');
|
||||||
|
Object.keys(h.CONFIG).forEach(function (key) {
|
||||||
|
var value = h.CONFIG[key];
|
||||||
|
tt.comment(format('- %s: %j', key, value));
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' cleanup leftover resources', function (t) {
|
||||||
|
h.triton(['volume', 'delete', '-y', '-w', validVolumeName].join(' '),
|
||||||
|
function onDelVolume(delVolErr, stdout, stderr) {
|
||||||
|
// If there was nothing to delete, this will fail so that's the
|
||||||
|
// normal case. Too bad we don't have a --force option.
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' triton volume create with invalid name', function (t) {
|
||||||
|
var invalidVolumeName =
|
||||||
|
h.makeResourceName('node-triton-test-volume-create-invalid-name-' +
|
||||||
|
'!foo!');
|
||||||
|
var expectedErrMsg = 'triton volume create: error (InvalidArgument): ' +
|
||||||
|
'Error: Invalid volume name: ' + invalidVolumeName;
|
||||||
|
|
||||||
|
h.triton([
|
||||||
|
'volume',
|
||||||
|
'create',
|
||||||
|
'--name',
|
||||||
|
invalidVolumeName
|
||||||
|
].join(' '), function (volCreateErr, stdout, stderr) {
|
||||||
|
t.ok(volCreateErr, 'create should have failed' +
|
||||||
|
(volCreateErr ? '' : ', but succeeded'));
|
||||||
|
t.equal(stderr.indexOf(expectedErrMsg), 0,
|
||||||
|
'stderr should include error message: ' + expectedErrMsg);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' triton volume create with invalid size', function (t) {
|
||||||
|
var invalidSize = 'foobar';
|
||||||
|
var expectedErrMsg = 'triton volume create: error: size "' +
|
||||||
|
invalidSize + '" is not a valid volume size';
|
||||||
|
var volumeName =
|
||||||
|
h.makeResourceName('node-triton-test-volume-create-invalid-size');
|
||||||
|
|
||||||
|
h.triton([
|
||||||
|
'volume',
|
||||||
|
'create',
|
||||||
|
'--name',
|
||||||
|
volumeName,
|
||||||
|
'--size',
|
||||||
|
invalidSize
|
||||||
|
].join(' '), function (volCreateErr, stdout, stderr) {
|
||||||
|
t.equal(stderr.indexOf(expectedErrMsg), 0,
|
||||||
|
'stderr should include error message: ' + expectedErrMsg);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' triton volume create with invalid type', function (t) {
|
||||||
|
var invalidType = 'foobar';
|
||||||
|
var volumeName =
|
||||||
|
h.makeResourceName('node-triton-test-volume-create-invalid-type');
|
||||||
|
var expectedErrMsg = 'triton volume create: error (InvalidArgument): ' +
|
||||||
|
'Error: Invalid volume type: ' + invalidType;
|
||||||
|
|
||||||
|
h.triton([
|
||||||
|
'volume',
|
||||||
|
'create',
|
||||||
|
'--name',
|
||||||
|
volumeName,
|
||||||
|
'--type',
|
||||||
|
invalidType
|
||||||
|
].join(' '), function (volCreateErr, stdout, stderr) {
|
||||||
|
t.equal(stderr.indexOf(expectedErrMsg), 0,
|
||||||
|
'stderr should include error message: ' + expectedErrMsg);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' triton volume create with invalid network', function (t) {
|
||||||
|
var volumeName =
|
||||||
|
h.makeResourceName('node-triton-test-volume-create-invalid-' +
|
||||||
|
'network');
|
||||||
|
var invalidNetwork = 'foobar';
|
||||||
|
var expectedErrMsg = 'no network with name or short id "' +
|
||||||
|
invalidNetwork + '" was found';
|
||||||
|
|
||||||
|
h.triton([
|
||||||
|
'volume',
|
||||||
|
'create',
|
||||||
|
'--name',
|
||||||
|
volumeName,
|
||||||
|
'--network',
|
||||||
|
invalidNetwork
|
||||||
|
].join(' '), function (volCreateErr, stdout, stderr) {
|
||||||
|
t.notEqual(stderr.indexOf(expectedErrMsg), -1,
|
||||||
|
'stderr should include error message: ' + expectedErrMsg +
|
||||||
|
', got: ' + volCreateErr);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' triton volume create valid volume', function (t) {
|
||||||
|
h.triton([
|
||||||
|
'volume',
|
||||||
|
'create',
|
||||||
|
'--name',
|
||||||
|
validVolumeName,
|
||||||
|
'-w'
|
||||||
|
].join(' '), function (volCreateErr, stdout, stderr) {
|
||||||
|
t.equal(volCreateErr, null,
|
||||||
|
'volume creation should not error');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' check volume was created', function (t) {
|
||||||
|
h.safeTriton(t, ['volume', 'get', validVolumeName],
|
||||||
|
function onGetVolume(getVolErr, stdout) {
|
||||||
|
t.equal(getVolErr, null,
|
||||||
|
'Getting volume should not error');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' delete volume', function (t) {
|
||||||
|
h.triton(['volume', 'delete', '-y', '-w', validVolumeName].join(' '),
|
||||||
|
function onDelVolume(delVolErr, stdout, stderr) {
|
||||||
|
t.equal(delVolErr, null,
|
||||||
|
'Deleting volume should not error');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' check volume was deleted', function (t) {
|
||||||
|
h.triton(['volume', 'get', validVolumeName].join(' '),
|
||||||
|
function onGetVolume(getVolErr, stdout, stderr) {
|
||||||
|
t.ok(getVolErr,
|
||||||
|
'Getting volume ' + validVolumeName + 'after deleting it ' +
|
||||||
|
'should error');
|
||||||
|
t.notEqual(stderr.indexOf('ResourceNotFound'), -1,
|
||||||
|
'Getting volume ' + validVolumeName + 'should not find it');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that we can create a volume with a valid fabric network and the
|
||||||
|
// volume ends up on that network.
|
||||||
|
|
||||||
|
tt.test(' find fabric network', function (t) {
|
||||||
|
h.triton(['network', 'list', '-j'].join(' '),
|
||||||
|
function onGetNetworks(getNetworksErr, stdout, stderr) {
|
||||||
|
var resultsObj;
|
||||||
|
|
||||||
|
t.ifErr(getNetworksErr, 'should succeed getting network list');
|
||||||
|
|
||||||
|
// turn the JSON lines into a JSON object
|
||||||
|
resultsObj = JSON.parse('[' + stdout.trim().replace(/\n/g, ',')
|
||||||
|
+ ']');
|
||||||
|
|
||||||
|
t.ok(resultsObj.length > 0,
|
||||||
|
'should find at least 1 network, found '
|
||||||
|
+ resultsObj.length);
|
||||||
|
|
||||||
|
FABRIC_NETWORKS = resultsObj.filter(function fabricFilter(net) {
|
||||||
|
// keep only those networks that are marked as fabric=true
|
||||||
|
return (net.fabric === true);
|
||||||
|
});
|
||||||
|
|
||||||
|
t.ok(FABRIC_NETWORKS.length > 0,
|
||||||
|
'should find at least 1 fabric network, found '
|
||||||
|
+ FABRIC_NETWORKS.length);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' triton volume on fabric network', function (t) {
|
||||||
|
h.triton([
|
||||||
|
'volume',
|
||||||
|
'create',
|
||||||
|
'--name',
|
||||||
|
'node-triton-test-volume-create-fabric-network',
|
||||||
|
'--network',
|
||||||
|
FABRIC_NETWORKS[0].id,
|
||||||
|
'-w',
|
||||||
|
'-j'
|
||||||
|
].join(' '), function (volCreateErr, stdout, stderr) {
|
||||||
|
t.ifErr(volCreateErr, 'volume creation should succeed');
|
||||||
|
t.comment('stdout: ' + stdout);
|
||||||
|
t.comment('stderr: ' + stderr);
|
||||||
|
currentVolume = JSON.parse(stdout);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' check volume was created', function (t) {
|
||||||
|
h.safeTriton(t, ['volume', 'get', currentVolume.name],
|
||||||
|
function onGetVolume(getVolErr, stdout) {
|
||||||
|
var volumeObj;
|
||||||
|
|
||||||
|
t.ifError(getVolErr, 'getting volume should succeed');
|
||||||
|
|
||||||
|
volumeObj = JSON.parse(stdout);
|
||||||
|
t.equal(volumeObj.networks[0], FABRIC_NETWORKS[0].id,
|
||||||
|
'expect network to match fabric we passed');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test(' delete volume', function (t) {
|
||||||
|
h.triton(['volume', 'delete', '-y', '-w', currentVolume.name].join(' '),
|
||||||
|
function onDelVolume(delVolErr, stdout, stderr) {
|
||||||
|
t.ifError(delVolErr, 'deleting volume should succeed');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -12,9 +12,10 @@
|
|||||||
* Test helpers for the integration tests
|
* Test helpers for the integration tests
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var error = console.error;
|
|
||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
|
var error = console.error;
|
||||||
var f = require('util').format;
|
var f = require('util').format;
|
||||||
|
var os = require('os');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var tabula = require('tabula');
|
var tabula = require('tabula');
|
||||||
|
|
||||||
@ -488,6 +489,14 @@ function printConfig(t) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns a string that represents a unique resource name for the host on which
|
||||||
|
* this function is called.
|
||||||
|
*/
|
||||||
|
function makeResourceName(prefix) {
|
||||||
|
assert.string(prefix, 'prefix');
|
||||||
|
return prefix + '-' + os.hostname();
|
||||||
|
}
|
||||||
|
|
||||||
// --- exports
|
// --- exports
|
||||||
|
|
||||||
@ -508,6 +517,7 @@ module.exports = {
|
|||||||
getResizeTestPkg: getResizeTestPkg,
|
getResizeTestPkg: getResizeTestPkg,
|
||||||
|
|
||||||
jsonStreamParse: jsonStreamParse,
|
jsonStreamParse: jsonStreamParse,
|
||||||
|
makeResourceName: makeResourceName,
|
||||||
printConfig: printConfig,
|
printConfig: printConfig,
|
||||||
|
|
||||||
ifErr: testcommon.ifErr
|
ifErr: testcommon.ifErr
|
||||||
|
@ -116,7 +116,7 @@ test('jsonStream', function (t) {
|
|||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('kvToObj', function (t) {
|
test('objFromKeyValueArgs', function (t) {
|
||||||
var arr = ['foo=1', 'bar=2', 'baz=3'];
|
var arr = ['foo=1', 'bar=2', 'baz=3'];
|
||||||
var o = {
|
var o = {
|
||||||
foo: '1',
|
foo: '1',
|
||||||
@ -126,21 +126,77 @@ test('kvToObj', function (t) {
|
|||||||
var kv;
|
var kv;
|
||||||
|
|
||||||
// no valid parameter
|
// no valid parameter
|
||||||
kv = common.kvToObj(arr);
|
kv = common.objFromKeyValueArgs(arr, {
|
||||||
|
disableDotted: true,
|
||||||
|
disableTypeConversions: true
|
||||||
|
});
|
||||||
|
|
||||||
t.deepEqual(kv, o);
|
t.deepEqual(kv, o);
|
||||||
|
|
||||||
// valid parameters
|
// valid parameters
|
||||||
kv = common.kvToObj(arr, ['foo', 'bar', 'baz']);
|
kv = common.objFromKeyValueArgs(arr, {
|
||||||
|
validKeys: ['foo', 'bar', 'baz'],
|
||||||
|
disableDotted: true,
|
||||||
|
disableTypeConversions: true
|
||||||
|
});
|
||||||
|
|
||||||
t.deepEqual(kv, o);
|
t.deepEqual(kv, o);
|
||||||
|
|
||||||
// invalid parameters
|
// invalid parameters
|
||||||
t.throws(function () {
|
t.throws(function () {
|
||||||
common.kvToObj(arr, ['uh-oh']);
|
common.objFromKeyValueArgs(arr, {
|
||||||
|
validKeys: ['uh-oh'],
|
||||||
|
disableDotted: true,
|
||||||
|
disableTypeConversions: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('objFromKeyValueArgs failOnEmptyValue', function (t) {
|
||||||
|
var arr = ['foo='];
|
||||||
|
var err;
|
||||||
|
|
||||||
|
try {
|
||||||
|
common.objFromKeyValueArgs(arr, {
|
||||||
|
failOnEmptyValue: true
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
t.ok(err);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default, failOnEmptyValue is not set, so the following should not
|
||||||
|
* throw an error.
|
||||||
|
*/
|
||||||
|
err = null;
|
||||||
|
try {
|
||||||
|
common.objFromKeyValueArgs(arr);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
t.equal(err, null);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Explicitly setting failOnEmptyValue to false should not throw an error
|
||||||
|
* when passing a key/value with an empty value.
|
||||||
|
*/
|
||||||
|
err = null;
|
||||||
|
try {
|
||||||
|
common.objFromKeyValueArgs(arr, {
|
||||||
|
failOnEmptyValue: false
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
t.equal(err, null);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
test('longAgo', function (t) {
|
test('longAgo', function (t) {
|
||||||
var la = common.longAgo;
|
var la = common.longAgo;
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
|
92
test/unit/parseVolumeSize.test.js
Normal file
92
test/unit/parseVolumeSize.test.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Unit tests for `parseVolumeSize` used by `triton volume ...`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var assert = require('assert-plus');
|
||||||
|
var test = require('tape');
|
||||||
|
|
||||||
|
var parseVolumeSize = require('../../lib/volumes').parseVolumeSize;
|
||||||
|
|
||||||
|
test('parseVolumeSize', function (tt) {
|
||||||
|
tt.test('parsing invalid sizes', function (t) {
|
||||||
|
var invalidVolumeSizes = [
|
||||||
|
'foo',
|
||||||
|
'0',
|
||||||
|
'-42',
|
||||||
|
'-42m',
|
||||||
|
'-42g',
|
||||||
|
'',
|
||||||
|
'42Gasdf',
|
||||||
|
'42gasdf',
|
||||||
|
'42asdf',
|
||||||
|
'asdf42G',
|
||||||
|
'asdf42g',
|
||||||
|
'asdf42',
|
||||||
|
'042g',
|
||||||
|
'042G',
|
||||||
|
'042',
|
||||||
|
0,
|
||||||
|
42,
|
||||||
|
-42,
|
||||||
|
42.1,
|
||||||
|
-42.1,
|
||||||
|
undefined,
|
||||||
|
null,
|
||||||
|
{}
|
||||||
|
];
|
||||||
|
|
||||||
|
invalidVolumeSizes.forEach(function parse(invalidVolumeSize) {
|
||||||
|
var parseErr;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseVolumeSize(invalidVolumeSize);
|
||||||
|
} catch (err) {
|
||||||
|
parseErr = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
t.ok(parseErr, 'parsing invalid volume size: ' + invalidVolumeSize +
|
||||||
|
' should throw');
|
||||||
|
});
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
tt.test('parsing valid sizes', function (t) {
|
||||||
|
var validVolumeSizes = [
|
||||||
|
{input: '42m', expectedOutput: 42},
|
||||||
|
{input: '42M', expectedOutput: 42},
|
||||||
|
{input: '42g', expectedOutput: 42 * 1024},
|
||||||
|
{input: '42G', expectedOutput: 42 * 1024}
|
||||||
|
];
|
||||||
|
|
||||||
|
validVolumeSizes.forEach(function parse(validVolumeSize) {
|
||||||
|
var parseErr;
|
||||||
|
var volSizeInMebibytes;
|
||||||
|
|
||||||
|
try {
|
||||||
|
volSizeInMebibytes = parseVolumeSize(validVolumeSize.input);
|
||||||
|
} catch (err) {
|
||||||
|
parseErr = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
t.ifErr(parseErr, 'parsing valid volume size: ' +
|
||||||
|
validVolumeSize.input + ' should not throw');
|
||||||
|
t.equal(validVolumeSize.expectedOutput, volSizeInMebibytes,
|
||||||
|
'parsed volume size for "' + validVolumeSize.input + '" ' +
|
||||||
|
'should equal to ' + validVolumeSize.expectedOutput +
|
||||||
|
' mebibytes');
|
||||||
|
});
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user