joyent/node-triton#157 support resizing of instances

Reviewed by: Trent Mick <trentm@gmail.com>
Approved by: Trent Mick <trentm@gmail.com>
This commit is contained in:
andrew 2017-02-16 17:00:32 -08:00 committed by Trent Mick
parent 3f4eff598c
commit ec9b6cc5aa
10 changed files with 270 additions and 11 deletions

View File

@ -7,6 +7,9 @@ Known issues:
## not yet released ## not yet released
- [joyent/node-triton#157] Add `triton instance resize ...` command and
`TritonApi.resizeInstance` method.
- [joyent/node-triton#129] Fix `triton reboot --wait` to properly wait. Before - [joyent/node-triton#129] Fix `triton reboot --wait` to properly wait. Before
it would often return immediately, before the instance started rebooting. it would often return immediately, before the instance started rebooting.
Add `--wait-timeout N` option to `triton reboot`. Add `--wait-timeout N` option to `triton reboot`.

View File

@ -368,7 +368,8 @@ test-integration`). Integration tests require a config file, by default at
"profileName": "east3b", "profileName": "east3b",
"allowWriteActions": true, "allowWriteActions": true,
"image": "minimal-64", "image": "minimal-64",
"package": "t4-standard-128M" "package": "g4-highcpu-128M",
"resizePackage": "g4-highcpu-256M"
} }
See "test/config.json.sample" for a description of all config vars. Minimally See "test/config.json.sample" for a description of all config vars. Minimally

View File

@ -772,6 +772,31 @@ CloudApi.prototype.getMachine = function getMachine(opts, cb) {
}); });
}; };
/**
* resize a machine by id.
*
* @param {Object} opts
* - id {UUID} Required. The machine id.
* - {UUID} package. A package id, as returned from listPackages
* @param {Function} callback of the form `function (err, body, res)`
*/
CloudApi.prototype.resizeMachine = function resizeMachine(opts, callback) {
assert.uuid(opts.id, 'opts.id');
assert.uuid(opts.package, 'opts.package');
var data = {
action: 'resize',
package: opts.package
};
this._request({
method: 'POST',
path: format('/%s/machines/%s', this.account, opts.id),
data: data
}, function (err, req, res, body) {
callback(err, body, res);
});
};
/** /**
* rename a machine by id. * rename a machine by id.
* *

View File

@ -78,8 +78,7 @@ do_rename.help = [
'and "NEWNAME" is an instance name.', 'and "NEWNAME" is an instance name.',
'', '',
'Changing an instance name is asynchronous.', 'Changing an instance name is asynchronous.',
'Use "--wait" to not return until', 'Use "--wait" to not return until the changes are completed.'
'the changes are completed.'
].join('\n'); ].join('\n');
do_rename.completionArgtypes = ['tritoninstance', 'none']; do_rename.completionArgtypes = ['tritoninstance', 'none'];

View File

@ -0,0 +1,87 @@
/*
* 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/.
*/
var common = require('../common');
var errors = require('../errors');
function do_resize(subcmd, opts, args, callback) {
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
} else if (args.length < 1) {
callback(new errors.UsageError('missing INST arg'));
return;
} else if (args.length < 2) {
callback(new errors.UsageError('missing PACKAGE arg'));
return;
}
var id = args[0];
var pkg = args[1];
console.log('Resizing instance %s to "%s"', id, pkg);
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
callback(setupErr);
}
tritonapi.resizeInstance({
id: id,
package: pkg,
wait: opts.wait,
waitTimeout: opts.wait_timeout * 1000
}, function (err) {
if (err) {
callback(err);
return;
}
console.log('Resized instance %s to "%s"', id, pkg);
callback();
});
});
}
do_resize.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Block until resizing instance is complete.'
},
{
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.'
}
];
do_resize.synopses = ['{{name}} resize [OPTIONS] INST PACKAGE'];
do_resize.help = [
'Resize an instance.',
'',
'{{usage}}',
'',
'{{options}}',
'Where "INST" is an instance name, id, or short id',
'and "PACKAGE" is a package name, id, or short id.',
'',
'Changing an instance package is asynchronous.',
'Use "--wait" to not return until the changes are completed.'
].join('\n');
do_resize.completionArgtypes = ['tritoninstance', 'tritonpackage', 'none'];
module.exports = do_resize;

View File

@ -34,6 +34,7 @@ function InstanceCLI(top) {
'get', 'get',
'create', 'create',
'delete', 'delete',
'resize',
'rename', 'rename',
{ group: '' }, { group: '' },
'start', 'start',
@ -64,6 +65,7 @@ InstanceCLI.prototype.do_list = require('./do_list');
InstanceCLI.prototype.do_get = require('./do_get'); InstanceCLI.prototype.do_get = require('./do_get');
InstanceCLI.prototype.do_create = require('./do_create'); InstanceCLI.prototype.do_create = require('./do_create');
InstanceCLI.prototype.do_delete = require('./do_delete'); InstanceCLI.prototype.do_delete = require('./do_delete');
InstanceCLI.prototype.do_resize = require('./do_resize');
InstanceCLI.prototype.do_rename = require('./do_rename'); InstanceCLI.prototype.do_rename = require('./do_rename');
InstanceCLI.prototype.do_start = require('./do_start'); InstanceCLI.prototype.do_start = require('./do_start');

View File

@ -184,6 +184,27 @@ function _stepInstId(arg, next) {
} }
} }
/**
* 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
* as `arg.pkgId`). Also sets `arg.pkgName` so that we can use this to test when
* the instance has been updated.
*/
function _stepPkgId(arg, next) {
assert.object(arg.client, 'arg.client');
assert.string(arg.package, 'arg.package');
arg.client.getPackage(arg.package, function (err, pkg) {
if (err) {
next(err);
} else {
arg.pkgId = pkg.id;
arg.pkgName = pkg.name;
next();
}
});
}
/** /**
* A function appropriate for `vasync.pipeline` funcs that takes a `arg.id` * A function appropriate for `vasync.pipeline` funcs that takes a `arg.id`
* fwrule shortid or uuid, and determines the fwrule id (setting it * fwrule shortid or uuid, and determines the fwrule id (setting it
@ -2410,6 +2431,64 @@ TritonApi.prototype.rebootInstance = function rebootInstance(opts, cb) {
}; };
/**
* Resize a machine by id.
*
* @param {Object} opts
* - {String} id: Required. The instance name, short id, or id (a UUID).
* - {String} package: Required. The new package name, shortId,
* or id (a UUID).
* - {Boolean} wait: Wait (via polling) until the rename is complete.
* Warning: A concurrent resize of the same instance can result in this
* polling being unable to notice the change. Use `waitTimeout` to
* put an upper bound.
* - {Number} waitTimeout: The number of milliseconds after which to
* timeout (call `cb` with a timeout error) waiting. Only relevant if
* `opts.wait === true`. Default is Infinity (i.e. it doesn't timeout).
* @param {Function} callback of the form `function (err, _, res)`
*/
TritonApi.prototype.resizeInstance = function resizeInstance(opts, cb) {
assert.string(opts.id, 'opts.id');
assert.string(opts.package, 'opts.package');
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, package: opts.package}, funcs: [
_stepInstId,
_stepPkgId,
function resizeMachine(arg, next) {
self.cloudapi.resizeMachine({id: arg.instId, package: arg.pkgId},
function (err, _res) {
res = _res;
next(err);
});
},
function waitForSizeChanges(arg, next) {
if (!opts.wait) {
next();
return;
}
self._waitForInstanceUpdate({
id: arg.instId,
timeout: opts.waitTimeout,
isUpdated: function (machine) {
return arg.pkgName === machine.package;
}
}, next);
}
]}, function (err) {
cb(err, null, res);
});
};
/** /**
* rename a machine by id. * rename a machine by id.
* *
@ -2451,10 +2530,12 @@ TritonApi.prototype.renameInstance = function renameInstance(opts, cb) {
next(); next();
return; return;
} }
self._waitForInstanceRename({ self._waitForInstanceUpdate({
id: arg.instId, id: arg.instId,
timeout: opts.waitTimeout, timeout: opts.waitTimeout,
name: opts.name isUpdated: function (machine) {
return opts.name === machine.name;
}
}, next); }, next);
} }
]}, function (err) { ]}, function (err) {
@ -2466,22 +2547,24 @@ TritonApi.prototype.renameInstance = function renameInstance(opts, cb) {
* Shared implementation for any methods to change instance name. * Shared implementation for any methods to change instance name.
* *
* @param {Object} opts * @param {Object} opts
* - {String} id: The instance ID Required. * - {String} id: Required. The instance ID Required.
* - {String} name: Required change new name * - {Function} isUpdated: Required. A function which is passed the
* machine data, should check if the change has been applied and
* return a Boolean.
* - {Number} timeout: The number of milliseconds after which to * - {Number} timeout: The number of milliseconds after which to
* timeout (call `cb` with a timeout error) waiting. * timeout (call `cb` with a timeout error) waiting.
* Default is Infinity (i.e. it doesn't timeout). * Default is Infinity (i.e. it doesn't timeout).
* @param {Function} cb: `function (err)` * @param {Function} cb: `function (err)`
*/ */
TritonApi.prototype._waitForInstanceRename = TritonApi.prototype._waitForInstanceUpdate =
function _waitForInstanceRename(opts, cb) { function _waitForInstanceUpdate(opts, cb) {
var self = this; var self = this;
assert.object(opts, 'opts'); assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id'); assert.uuid(opts.id, 'opts.id');
assert.func(opts.isUpdated, 'opts.isUpdated');
assert.optionalNumber(opts.timeout, 'opts.timeout'); assert.optionalNumber(opts.timeout, 'opts.timeout');
var timeout = opts.hasOwnProperty('timeout') ? opts.timeout : Infinity; var timeout = opts.hasOwnProperty('timeout') ? opts.timeout : Infinity;
assert.ok(timeout > 0, 'opts.timeout must be greater than zero'); assert.ok(timeout > 0, 'opts.timeout must be greater than zero');
assert.string(opts.name, 'opts.name');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
/* /*
@ -2499,7 +2582,7 @@ function _waitForInstanceRename(opts, cb) {
cb(err); cb(err);
return; return;
} }
if (opts.name === machine.name) { if (opts.isUpdated(machine)) {
cb(); cb();
return; return;

View File

@ -25,5 +25,6 @@
// The params used for test provisions. By default the tests use: // The params used for test provisions. By default the tests use:
// the smallest RAM package, the latest base* image. // the smallest RAM package, the latest base* image.
"package": "<package name or uuid>", "package": "<package name or uuid>",
"resizePackage": "<package name>",
"image": "<image uuid, name or name@version>" "image": "<image uuid, name or name@version>"
} }

View File

@ -68,6 +68,15 @@ test('triton manage workflow', opts, function (tt) {
}); });
}); });
var resizePkgName;
tt.test(' setup: find resize test package', function (t) {
h.getResizeTestPkg(t, function (err, pkgName_) {
t.ifError(err, 'getResizeTestPkg' + (err ? ': ' + err : ''));
resizePkgName = pkgName_;
t.end();
});
});
// create a test machine (blocking) and output JSON // create a test machine (blocking) and output JSON
tt.test(' setup: triton create', function (t) { tt.test(' setup: triton create', function (t) {
var argv = [ var argv = [
@ -255,6 +264,27 @@ test('triton manage workflow', opts, function (tt) {
}); });
}); });
// resize the instance
tt.test(' triton inst resize', function (t) {
var args = ['inst', 'resize', '-w', instance.id, resizePkgName];
h.safeTriton(t, args, function (err, stdout) {
t.ok(stdout.match(/^Resizing instance/m),
'"Resizing instance" in stdout');
t.ok(stdout.match(/^Resized instance/m),
'"Resized instance" in stdout');
t.end();
});
});
tt.test(' confirm resized', function (t) {
h.safeTriton(t, {json: true, args: ['inst', 'get', '-j',
INST_ALIAS]},
function (err, inst) {
t.equal(inst.package, resizePkgName, 'instance was resized');
t.end();
});
});
// rename the instance // rename the instance
tt.test(' triton inst rename', function (t) { tt.test(' triton inst rename', function (t) {
var args = ['inst', 'rename', '-w', instance.id, INST_ALIAS_NEWNAME]; var args = ['inst', 'rename', '-w', instance.id, INST_ALIAS_NEWNAME];

View File

@ -231,6 +231,33 @@ function getTestPkg(t, cb) {
}); });
} }
/*
* Find and return second smallest package name that can be used for
* test provisions.
*
* @param {Tape} t - tape test object
* @param {Function} cb - `function (err, {pkgs})`
* where `pkgs` is an Array of 2 test packages to use.
*/
function getResizeTestPkg(t, cb) {
if (CONFIG.resizePackage) {
t.ok(CONFIG.resizePackage, 'resizePackage from config: ' +
CONFIG.resizePackage);
cb(null, CONFIG.resizePackage);
return;
}
safeTriton(t, ['pkg', 'ls', '-j'], function (err, stdout) {
var pkgs = jsonStreamParse(stdout);
// Smallest RAM first.
tabula.sortArrayOfObjects(pkgs, ['memory']);
var pkg = pkgs[1];
t.ok(pkg.name, f('second smallest (RAM) available package: %s (%s)',
pkg.id, pkg.name));
cb(null, pkg.name);
});
}
function jsonStreamParse(s) { function jsonStreamParse(s) {
var results = []; var results = [];
@ -344,6 +371,7 @@ module.exports = {
deleteTestInst: deleteTestInst, deleteTestInst: deleteTestInst,
getTestImg: getTestImg, getTestImg: getTestImg,
getTestPkg: getTestPkg, getTestPkg: getTestPkg,
getResizeTestPkg: getResizeTestPkg,
jsonStreamParse: jsonStreamParse, jsonStreamParse: jsonStreamParse,
printConfig: printConfig, printConfig: printConfig,