more changes according to discussion after initial code review

This commit is contained in:
Julien Gilli 2017-03-22 16:54:36 -07:00
parent 08b7dd088f
commit 00f92d67a5
5 changed files with 212 additions and 34 deletions

View File

@ -2362,6 +2362,9 @@ CloudApi.prototype.deleteVolume = function deleteVolume(volumeUuid, cb) {
* - {String} id - machine UUID * - {String} id - machine UUID
* - {Array of String} states - desired state * - {Array of String} states - desired state
* - {Number} interval (optional) - time in ms to poll * - {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 * @param {Function} callback - called when state is reached or on error
*/ */
CloudApi.prototype.waitForVolumeStates = CloudApi.prototype.waitForVolumeStates =
@ -2371,16 +2374,25 @@ function waitForVolumeStates(opts, callback) {
assert.uuid(opts.id, 'opts.id'); assert.uuid(opts.id, 'opts.id');
assert.arrayOfString(opts.states, 'opts.states'); assert.arrayOfString(opts.states, 'opts.states');
assert.optionalNumber(opts.interval, 'opts.interval'); assert.optionalNumber(opts.interval, 'opts.interval');
assert.optionalNumber(opts.timeout, 'opts.timeout');
assert.func(callback, 'callback'); assert.func(callback, 'callback');
var interval = (opts.interval === undefined ? 1000 : opts.interval); var interval = (opts.interval === undefined ? 1000 : opts.interval);
interval = Math.min(interval, opts.timeout);
assert.ok(interval > 0, 'interval must be a positive number'); assert.ok(interval > 0, 'interval must be a positive number');
var startTime = process.hrtime();
var timeout = opts.timeout;
poll(); poll();
function poll() { function poll() {
self.getVolume({ self.getVolume({
id: opts.id id: opts.id
}, function (err, vol, res) { }, function (err, vol, res) {
var elapsedTime;
var timedOut = false;
if (err) { if (err) {
callback(err, null, res); callback(err, null, res);
return; return;
@ -2388,8 +2400,24 @@ function waitForVolumeStates(opts, callback) {
if (opts.states.indexOf(vol.state) !== -1) { if (opts.states.indexOf(vol.state) !== -1) {
callback(null, vol, res); callback(null, vol, res);
return; 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); setTimeout(poll, interval);
return;
}
}
}); });
} }
}; };

View File

@ -1120,6 +1120,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;
}
//---- exports //---- exports
@ -1157,6 +1176,7 @@ module.exports = {
deepEqual: deepEqual, deepEqual: deepEqual,
tildeSync: tildeSync, tildeSync: tildeSync,
objFromKeyValueArgs: objFromKeyValueArgs, objFromKeyValueArgs: objFromKeyValueArgs,
jsonPredFromKv: jsonPredFromKv jsonPredFromKv: jsonPredFromKv,
monotonicTimeDiffMs: monotonicTimeDiffMs
}; };
// vim: set softtabstop=4 shiftwidth=4: // vim: set softtabstop=4 shiftwidth=4:

View File

@ -32,9 +32,7 @@ function do_create(subcmd, opts, args, cb) {
return; return;
} }
var context = {}; vasync.pipeline({arg: {cli: this.top}, funcs: [
vasync.pipeline({funcs: [
function validateVolumeSize(ctx, next) { function validateVolumeSize(ctx, next) {
if (opts.size === undefined) { if (opts.size === undefined) {
next(); next();
@ -50,13 +48,7 @@ function do_create(subcmd, opts, args, cb) {
next(); next();
}, },
function setup(ctx, next) { common.cliSetupTritonApi,
common.cliSetupTritonApi({
cli: self.top
}, function onSetup(setupErr) {
next(setupErr);
});
},
function getNetworks(ctx, next) { function getNetworks(ctx, next) {
if (!opts.network) { if (!opts.network) {
return next(); return next();
@ -97,18 +89,26 @@ function do_create(subcmd, opts, args, cb) {
}); });
}, },
function maybeWait(ctx, next) { function maybeWait(ctx, next) {
var distraction;
if (!opts.wait) { if (!opts.wait) {
next(); next();
return; return;
} }
var distraction = distractions.createDistraction(opts.wait.length); if (process.stderr.isTTY && opts.wait.length > 1) {
distraction = distractions.createDistraction(opts.wait.length);
}
self.top.tritonapi.cloudapi.waitForVolumeStates({ self.top.tritonapi.cloudapi.waitForVolumeStates({
id: ctx.volume.id, id: ctx.volume.id,
states: ['ready', 'failed'] states: ['ready', 'failed'],
timeout: opts.wait_timeout * 1000
}, function onWaitDone(waitErr, volume) { }, function onWaitDone(waitErr, volume) {
if (distraction) {
distraction.destroy(); distraction.destroy();
}
ctx.volume = volume; ctx.volume = volume;
next(waitErr); next(waitErr);
}); });
@ -122,7 +122,7 @@ function do_create(subcmd, opts, args, cb) {
console.log(JSON.stringify(ctx.volume, null, 4)); console.log(JSON.stringify(ctx.volume, null, 4));
} }
} }
], arg: context}, cb); ]}, cb);
} }
do_create.options = [ do_create.options = [
@ -178,6 +178,13 @@ do_create.options = [
type: 'arrayOfBool', type: 'arrayOfBool',
help: 'Wait for the creation to complete. Use multiple times for a ' + help: 'Wait for the creation to complete. Use multiple times for a ' +
'spinner.' '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.'
} }
]; ];

View File

@ -80,36 +80,76 @@ function do_delete(subcmd, opts, args, cb) {
} }
var context = { var context = {
volumeIds: args volumeIds: args,
cli: this.top
}; };
vasync.pipeline({arg: context, funcs: [ vasync.pipeline({arg: context, funcs: [
function setup(ctx, next) { common.cliSetupTritonApi,
common.cliSetupTritonApi({ function confirm(ctx, next) {
cli: self.top var promptMsg;
}, function onSetup(setupErr) {
next(setupErr); 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) { function deleteVolumes(ctx, next) {
vasync.forEachParallel({ vasync.forEachParallel({
func: function doDeleteVolume(volumeId, done) { func: function doDeleteVolume(volumeId, done) {
deleteVolume(volumeId, { self.top.tritonapi.deleteVolume({
wait: opts.wait, id: volumeId,
wait: opts.wait && opts.wait.length > 0,
waitTimeout: opts.wait_timeout * 1000,
tritonapi: self.top.tritonapi tritonapi: self.top.tritonapi
}, done); }, function onVolDeleted(volDelErr) {
if (volDelErr) {
console.error('Error when deleting volume %s, '
+ 'reason: %s', volumeId, volDelErr);
} else {
console.log('Deleted volume %s', volumeId);
}
done();
});
}, },
inputs: ctx.volumeIds inputs: ctx.volumeIds
}, next); }, next);
} }
]}, function onDone(err) { ]}, function onDone(err) {
if (err === true) {
/*
* Answered 'no' to confirmation to delete.
*/
err = null;
}
if (err) { if (err) {
cb(err); cb(err);
return; return;
} }
console.log('%s volume %s', common.capitalize('delete'), args);
cb(); cb();
}); });
} }
@ -128,6 +168,18 @@ do_delete.options = [
type: 'arrayOfBool', type: 'arrayOfBool',
help: 'Wait for the deletion to complete. Use multiple times for a ' + help: 'Wait for the deletion to complete. Use multiple times for a ' +
'spinner.' '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.'
} }
]; ];

View File

@ -2313,11 +2313,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,
@ -2398,7 +2393,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 '
@ -2667,6 +2663,81 @@ TritonApi.prototype.getVolume = function getVolume(name, cb) {
} }
}; };
/**
* 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();
}
});
}
}
/**
* 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: ['deleted', 'failed'],
timeout: opts.waitTimeout
}, next);
}
]}, function onDeletionComplete(err) {
cb(err, null, res);
});
};
//---- exports //---- exports
module.exports = { module.exports = {