From ec9b6cc5aae21b9ee9502afa1da116f2da4e9910 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 16 Feb 2017 17:00:32 -0800 Subject: [PATCH] joyent/node-triton#157 support resizing of instances Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 3 + README.md | 3 +- lib/cloudapi2.js | 25 +++++ lib/do_instance/do_rename.js | 3 +- lib/do_instance/do_resize.js | 87 +++++++++++++++++ lib/do_instance/index.js | 2 + lib/tritonapi.js | 99 ++++++++++++++++++-- test/config.json.sample | 1 + test/integration/cli-manage-workflow.test.js | 30 ++++++ test/integration/helpers.js | 28 ++++++ 10 files changed, 270 insertions(+), 11 deletions(-) create mode 100644 lib/do_instance/do_resize.js diff --git a/CHANGES.md b/CHANGES.md index acd460d..fc83792 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,9 @@ Known issues: ## 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 it would often return immediately, before the instance started rebooting. Add `--wait-timeout N` option to `triton reboot`. diff --git a/README.md b/README.md index f8d67e6..0fe770a 100644 --- a/README.md +++ b/README.md @@ -368,7 +368,8 @@ test-integration`). Integration tests require a config file, by default at "profileName": "east3b", "allowWriteActions": true, "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 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index f28624c..f872baa 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -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. * diff --git a/lib/do_instance/do_rename.js b/lib/do_instance/do_rename.js index af24b02..8c25cab 100644 --- a/lib/do_instance/do_rename.js +++ b/lib/do_instance/do_rename.js @@ -78,8 +78,7 @@ do_rename.help = [ 'and "NEWNAME" is an instance name.', '', 'Changing an instance name is asynchronous.', - 'Use "--wait" to not return until', - 'the changes are completed.' + 'Use "--wait" to not return until the changes are completed.' ].join('\n'); do_rename.completionArgtypes = ['tritoninstance', 'none']; diff --git a/lib/do_instance/do_resize.js b/lib/do_instance/do_resize.js new file mode 100644 index 0000000..965849d --- /dev/null +++ b/lib/do_instance/do_resize.js @@ -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; diff --git a/lib/do_instance/index.js b/lib/do_instance/index.js index f2554bd..d8f1513 100644 --- a/lib/do_instance/index.js +++ b/lib/do_instance/index.js @@ -34,6 +34,7 @@ function InstanceCLI(top) { 'get', 'create', 'delete', + 'resize', 'rename', { group: '' }, 'start', @@ -64,6 +65,7 @@ InstanceCLI.prototype.do_list = require('./do_list'); InstanceCLI.prototype.do_get = require('./do_get'); InstanceCLI.prototype.do_create = require('./do_create'); InstanceCLI.prototype.do_delete = require('./do_delete'); +InstanceCLI.prototype.do_resize = require('./do_resize'); InstanceCLI.prototype.do_rename = require('./do_rename'); InstanceCLI.prototype.do_start = require('./do_start'); diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 3f5fe72..c5f159a 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -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` * 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. * @@ -2451,10 +2530,12 @@ TritonApi.prototype.renameInstance = function renameInstance(opts, cb) { next(); return; } - self._waitForInstanceRename({ + self._waitForInstanceUpdate({ id: arg.instId, timeout: opts.waitTimeout, - name: opts.name + isUpdated: function (machine) { + return opts.name === machine.name; + } }, next); } ]}, function (err) { @@ -2466,22 +2547,24 @@ TritonApi.prototype.renameInstance = function renameInstance(opts, cb) { * Shared implementation for any methods to change instance name. * * @param {Object} opts - * - {String} id: The instance ID Required. - * - {String} name: Required change new name + * - {String} id: Required. The instance ID Required. + * - {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 * timeout (call `cb` with a timeout error) waiting. * Default is Infinity (i.e. it doesn't timeout). * @param {Function} cb: `function (err)` */ -TritonApi.prototype._waitForInstanceRename = -function _waitForInstanceRename(opts, cb) { +TritonApi.prototype._waitForInstanceUpdate = +function _waitForInstanceUpdate(opts, cb) { var self = this; assert.object(opts, 'opts'); assert.uuid(opts.id, 'opts.id'); + assert.func(opts.isUpdated, 'opts.isUpdated'); assert.optionalNumber(opts.timeout, 'opts.timeout'); var timeout = opts.hasOwnProperty('timeout') ? opts.timeout : Infinity; assert.ok(timeout > 0, 'opts.timeout must be greater than zero'); - assert.string(opts.name, 'opts.name'); assert.func(cb, 'cb'); /* @@ -2499,7 +2582,7 @@ function _waitForInstanceRename(opts, cb) { cb(err); return; } - if (opts.name === machine.name) { + if (opts.isUpdated(machine)) { cb(); return; diff --git a/test/config.json.sample b/test/config.json.sample index 44e2e65..8768ff9 100644 --- a/test/config.json.sample +++ b/test/config.json.sample @@ -25,5 +25,6 @@ // The params used for test provisions. By default the tests use: // the smallest RAM package, the latest base* image. "package": "", + "resizePackage": "", "image": "" } diff --git a/test/integration/cli-manage-workflow.test.js b/test/integration/cli-manage-workflow.test.js index 6928978..bde2484 100644 --- a/test/integration/cli-manage-workflow.test.js +++ b/test/integration/cli-manage-workflow.test.js @@ -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 tt.test(' setup: triton create', function (t) { 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 tt.test(' triton inst rename', function (t) { var args = ['inst', 'rename', '-w', instance.id, INST_ALIAS_NEWNAME]; diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 0e096fc..8a573a6 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -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) { var results = []; @@ -344,6 +371,7 @@ module.exports = { deleteTestInst: deleteTestInst, getTestImg: getTestImg, getTestPkg: getTestPkg, + getResizeTestPkg: getResizeTestPkg, jsonStreamParse: jsonStreamParse, printConfig: printConfig,