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:
		
							parent
							
								
									3f4eff598c
								
							
						
					
					
						commit
						ec9b6cc5aa
					
				| @ -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`. | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -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. | ||||||
|  * |  * | ||||||
|  | |||||||
| @ -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']; | ||||||
|  | |||||||
							
								
								
									
										87
									
								
								lib/do_instance/do_resize.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								lib/do_instance/do_resize.js
									
									
									
									
									
										Normal 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; | ||||||
| @ -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'); | ||||||
|  | |||||||
| @ -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; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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>" | ||||||
| } | } | ||||||
|  | |||||||
| @ -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]; | ||||||
|  | |||||||
| @ -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, | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user