first pass at 'triton create'

This commit is contained in:
Trent Mick 2015-08-25 20:53:48 -07:00
parent 15ca8ecc32
commit 0d4e93208c
9 changed files with 351 additions and 159 deletions

View File

@ -1,9 +1,6 @@
# today # today
triton instances|insts # list machines
triton instance|inst ID|NAME|UNIQUE-NAME-SUBSTRING # get machine
# -1 for unique match, a la 'vmadm lookup -1'
triton create # triton create-instance triton create # triton create-instance
triton create -p PKG [...] IMG triton create -p PKG [...] IMG
@ -23,38 +20,22 @@ triton image IMAGE # get image
triton packages # list packages triton packages # list packages
triton package PACKAGE triton package PACKAGE
triton instances|insts # list machines
triton instance|inst ID|NAME # get machine
triton cloudapi ...
# maybe today # maybe today
triton config defaultPkg t4-standard-1g
triton login|ssh VM # kexec? triton login|ssh VM # kexec?
triton delete VM|IMAGE # substring matching? too dangerous triton delete VM|IMAGE # substring matching? too dangerous
triton delete --vm VM triton delete --vm VM
triton delete --image IMAGE triton delete --image IMAGE
triton raw|cloudapi ... # raw cloudapi call
Equivalent of:
function cloudapi() {
local now=`date -u "+%a, %d %h %Y %H:%M:%S GMT"` ;
local signature=`echo ${now} | tr -d '\n' | openssl dgst -sha256 -sign ~/.ssh/automation.id_rsa | openssl enc -e -a | tr -d '\n'` ;
local curl_opts=
[[ -n $SDC_TESTING ]] && curl_opts="-k $curl_opts";
[[ -n $TRACE ]] && set -x;
curl -is $curl_opts \
-H "Accept: application/json" -H "api-version: ~7.2" \
-H "Date: ${now}" \
-H "Authorization: Signature keyId=\"/$SDC_ACCOUNT/keys/$SDC_KEY_ID\",algorithm=\"rsa-sha256\" ${signature}" \
--url $SDC_URL$@ ;
[[ -n $TRACE ]] && set +x;
echo "";
}
"shortid" instead of full UUID "id" in default output, and then allow lookup "shortid" instead of full UUID "id" in default output, and then allow lookup
by that shortid. Really nice for 80 columns. by that shortid. Really nice for 80 columns.

View File

@ -61,7 +61,7 @@ function CLI() {
{ group: 'Operator Commands' }, { group: 'Operator Commands' },
'account', 'account',
{ group: 'Instances (aka VMs/Machines/Containers)' }, { group: 'Instances (aka VMs/Machines/Containers)' },
'create', 'create-instance',
'instances', 'instances',
'instance', 'instance',
'instance-audit', 'instance-audit',
@ -117,9 +117,9 @@ CLI.prototype.do_images = require('./do_images');
CLI.prototype.do_image = require('./do_image'); CLI.prototype.do_image = require('./do_image');
// Instances (aka VMs/containers/machines) // Instances (aka VMs/containers/machines)
CLI.prototype.do_create = require('./do_create');
CLI.prototype.do_instance = require('./do_instance'); CLI.prototype.do_instance = require('./do_instance');
CLI.prototype.do_instances = require('./do_instances'); CLI.prototype.do_instances = require('./do_instances');
CLI.prototype.do_create_instance = require('./do_create_instance');
CLI.prototype.do_instance_audit = require('./do_instance_audit'); CLI.prototype.do_instance_audit = require('./do_instance_audit');
CLI.prototype.do_stop_instance = require('./do_startstop_instance')('stop'); CLI.prototype.do_stop_instance = require('./do_startstop_instance')('stop');
CLI.prototype.do_start_instance = require('./do_startstop_instance')('start'); CLI.prototype.do_start_instance = require('./do_startstop_instance')('start');

View File

@ -404,19 +404,19 @@ CloudAPI.prototype._doMachine = function _doMachine(action, uuid, callback) {
}; };
/** /**
* wait for a specfic state for a machine * Wait for a machine to go one of a set of specfic states.
* *
* @param {Object} options * @param {Object} options
* - {String} id - machine UUID * - {String} id - machine UUID
* - {String} state - desired state * - {Array of String} states - desired state
* - {Number} interval (optional) - time in ms to poll * - {Number} interval (optional) - time in ms to poll
* @param {Function} callback - called when state is reached or on error * @param {Function} callback - called when state is reached or on error
*/ */
CloudAPI.prototype.waitForMachineState = function waitForMachineState(opts, callback) { CloudAPI.prototype.waitForMachineStates = function waitForMachineStates(opts, callback) {
var self = this; var self = this;
assert.object(opts, 'opts'); assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id'); assert.string(opts.id, 'opts.id');
assert.string(opts.state, 'opts.state'); assert.arrayOfString(opts.states, 'opts.states');
assert.optionalNumber(opts.interval, 'opts.interval'); assert.optionalNumber(opts.interval, 'opts.interval');
assert.func(callback, 'callback'); assert.func(callback, 'callback');
var interval = (opts.interval === undefined ? 1000 : opts.interval); var interval = (opts.interval === undefined ? 1000 : opts.interval);
@ -429,7 +429,7 @@ CloudAPI.prototype.waitForMachineState = function waitForMachineState(opts, call
callback(err); callback(err);
return; return;
} }
if (machine.state === opts.state) { if (opts.states.indexOf(machine.state) !== -1) {
callback(null, machine); callback(null, machine);
return; return;
} }
@ -497,6 +497,26 @@ CloudAPI.prototype.listMachines = function listMachines(options, callback) {
}); });
}; };
CloudAPI.prototype.createMachine = function createMachine(options, callback) {
assert.object(options, 'options');
assert.optionalString(options.name, 'options.name');
assert.uuid(options.image, 'options.image');
assert.uuid(options.package, 'options.package');
assert.optionalArrayOfUuid(options.networks, 'options.networks');
// TODO: assert the other fields
assert.func(callback, 'callback');
// XXX how does options.networks array work here?
this._request({
method: 'POST',
path: this._path(format('/%s/machines', this.user), options)
}, function (err, req, res, body) {
callback(err, body, res);
});
};
/** /**
* List machine audit (successful actions on the machine). * List machine audit (successful actions on the machine).
* *

View File

@ -1,11 +1,8 @@
#!/usr/bin/env node
/** /**
* Copyright (c) 2015 Joyent Inc. All rights reserved. * Copyright (c) 2015 Joyent Inc. All rights reserved.
*/ */
var assert = require('assert-plus'); var assert = require('assert-plus');
var sprintf = require('extsprintf').sprintf;
var util = require('util'), var util = require('util'),
format = util.format; format = util.format;
@ -17,6 +14,7 @@ var errors = require('./errors'),
var p = console.log; var p = console.log;
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
// ---- support stuff // ---- support stuff
@ -142,9 +140,45 @@ function isUUID(s) {
return /^([a-f\d]{8}(-[a-f\d]{4}){3}-[a-f\d]{12}?)$/i.test(s); return /^([a-f\d]{8}(-[a-f\d]{4}){3}-[a-f\d]{12}?)$/i.test(s);
} }
function humanDurationFromMs(ms) {
assert.number(ms, 'ms');
var sizes = [
['ms', 1000, 's'],
['s', 60, 'm'],
['m', 60, 'h'],
['h', 24, 'd']
];
if (ms === 0) {
return '0ms';
}
var bits = [];
var n = ms;
for (var i = 0; i < sizes.length; i++) {
var size = sizes[i];
var remainder = n % size[1];
if (remainder === 0) {
bits.unshift('');
} else {
bits.unshift(format('%d%s', remainder, size[0]));
}
n = Math.floor(n / size[1]);
if (n === 0) {
break;
} else if (size[2] === 'd') {
bits.unshift(format('%d%s', n, size[2]));
break;
}
}
return bits.slice(0, 2).join('');
}
//---- exports //---- exports
module.exports = { module.exports = {
UUID_RE: UUID_RE,
objCopy: objCopy, objCopy: objCopy,
deepObjCopy: deepObjCopy, deepObjCopy: deepObjCopy,
zeroPad: zeroPad, zeroPad: zeroPad,
@ -153,5 +187,6 @@ module.exports = {
kvToObj: kvToObj, kvToObj: kvToObj,
longAgo: longAgo, longAgo: longAgo,
isUUID: isUUID, isUUID: isUUID,
humanDurationFromMs: humanDurationFromMs
}; };
// vim: set softtabstop=4 shiftwidth=4: // vim: set softtabstop=4 shiftwidth=4:

197
lib/do_create_instance.js Normal file
View File

@ -0,0 +1,197 @@
/*
* Copyright (c) 2015 Joyent Inc. All rights reserved.
*
* `triton create ...`
*/
var bigspinner = require('bigspinner');
var format = require('util').format;
var tabula = require('tabula');
var vasync = require('vasync');
var common = require('./common');
var errors = require('./errors');
function do_create_instance(subcmd, opts, args, callback) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
} else if (args.length < 1 || args.length > 2) {
return callback(new errors.UsageError(format(
'incorrect number of args (%d): %s', args.length, args.join(' '))));
}
var log = this.triton.log;
var cloudapi = this.triton.cloudapi;
var cOpts = {};
vasync.pipeline({arg: {}, funcs: [
function getImg(ctx, next) {
// XXX don't get the image object if it is a UUID, waste of time
self.triton.getImage(args[0], function (err, img) {
if (err) {
return next(err);
}
ctx.img = img;
log.trace({img: img}, 'create-instance img');
next();
});
},
function getPkg(ctx, next) {
if (args.length < 2) {
return next();
}
// XXX don't get the package object if it is a UUID, waste of time
self.triton.getPackage(args[1], function (err, pkg) {
if (err) {
return next(err);
}
log.trace({pkg: pkg}, 'create-instance pkg');
ctx.pkg = pkg;
next();
});
},
function getNets(ctx, next) {
if (!opts.networks) {
return next();
}
self.triton.getNetworks(opts.networks, function (err, nets) {
if (err) {
return next(err);
}
ctx.nets = nets;
next();
});
},
function createInst(ctx, next) {
var createOpts = {
name: opts.name,
image: ctx.img.id,
'package': ctx.pkg && ctx.pkg.id,
networks: ctx.nets && ctx.nets.map(
function (net) { return net.id; })
};
log.trace({createOpts: createOpts}, 'create-instance createOpts');
ctx.start = Date.now();
cloudapi.createMachine(createOpts, function (err, inst) {
if (err) {
return next(err);
}
ctx.inst = inst;
if (opts.json) {
console.log(JSON.stringify(inst));
} else {
console.log('Creating instance %s (%s, %s@%s, %s)',
inst.name, inst.id, ctx.img.name, ctx.img.version,
inst.package);
}
next();
});
},
function maybeWait(ctx, next) {
if (!opts.wait) {
return next();
}
var spinner;
if (!opts.quiet && process.stderr.isTTY) {
spinner = bigspinner.createSpinner({
delay: 250,
stream: process.stderr,
height: process.stdout.rows - 2,
width: process.stdout.columns - 1,
hideCursor: true,
fontChar: '#'
});
}
cloudapi.waitForMachineStates({
id: ctx.inst.id,
states: ['running', 'failed']
}, function (err, inst) {
if (spinner) {
spinner.destroy();
}
if (err) {
return next(err);
}
if (opts.json) {
console.log(JSON.stringify(inst));
} else if (inst.state === 'running') {
var dur = Date.now() - ctx.start;
console.log('Created instance %s (%s) in %s',
inst.name, inst.id, common.humanDurationFromMs(dur));
}
if (inst.state !== 'running') {
next(new Error(format('failed to create instance %s (%s)',
inst.name, inst.id)));
} else {
next();
}
});
}
]}, function (err) {
callback(err);
});
};
do_create_instance.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
group: 'Create options'
},
{
names: ['name', 'n'],
type: 'string',
help: 'One or more (comma-separated) networks IDs.'
},
// XXX arrayOfCommaSepString dashdash type
//{
// names: ['networks', 'nets'],
// type: 'arrayOfCommaSepString',
// help: 'One or more (comma-separated) networks IDs.'
//},
// XXX enable-firewall
// XXX locality: near, far
// XXX metadata, metadata-file
// XXX script (user-script)
// XXX tag
{
group: 'Other options'
},
{
names: ['wait', 'w'],
type: 'bool',
help: 'Wait for the creation to complete.'
},
{
names: ['quiet', 'q'],
type: 'bool',
help: 'No progress spinner while waiting.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
}
];
do_create_instance.help = (
/* BEGIN JSSTYLED */
'Create a new instance.\n' +
'\n' +
'Usage:\n' +
' {{name}} create-instance [<options>] IMAGE [PACKAGE]\n' +
'\n' +
'{{options}}'
/* END JSSTYLED */
);
do_create_instance.aliases = ['create'];
module.exports = do_create_instance;

View File

@ -42,8 +42,7 @@ function do_packages (subcmd, opts, args, callback) {
tabula(packages, { tabula(packages, {
skipHeader: opts.H, skipHeader: opts.H,
columns: columns, columns: columns,
sort: sort, sort: sort
validFields: 'name,memory,disk,swap,vcpus,lwps,default,id,version'.split(',')
}); });
} }
callback(); callback();
@ -64,7 +63,7 @@ do_packages.options = [
{ {
names: ['o'], names: ['o'],
type: 'string', type: 'string',
default: 'id,name,version,memory,disk', default: 'id,name,default,memory,disk',
help: 'Specify fields (columns) to output.', help: 'Specify fields (columns) to output.',
helpArg: 'field1,...' helpArg: 'field1,...'
}, },

View File

@ -98,11 +98,10 @@ function _do_instance(action, subcmd, opts, args, callback) {
return; return;
} }
var waitOpts = { self.triton.cloudapi.waitForMachineStates({
state: state, id: uuid,
id: uuid states: [state]
}; }, function (err, machine) {
self.triton.cloudapi.waitForMachineState(waitOpts, function (err, machine) {
if (err) { if (err) {
callback(err); callback(err);
return; return;

View File

@ -13,6 +13,7 @@ var once = require('once');
var path = require('path'); var path = require('path');
var restifyClients = require('restify-clients'); var restifyClients = require('restify-clients');
var sprintf = require('util').format; var sprintf = require('util').format;
var tabula = require('tabula');
var cloudapi = require('./cloudapi2'); var cloudapi = require('./cloudapi2');
var common = require('./common'); var common = require('./common');
@ -105,130 +106,89 @@ Triton.prototype._cloudapiFromProfile = function _cloudapiFromProfile(profile) {
/** /**
* Find a machine in the set of DCs for the current profile. * Get an image by ID or name. If there is more than one image with that name,
* * then the latest (by published_at) is returned.
*
* @param {Object} options
* - {String} machine (required) The machine id.
* XXX support name matching, prefix, etc.
* @param {Function} callback `function (err, machine, dc)`
* Returns the machine object (as from cloudapi GetMachine) and the `dc`,
* e.g. "us-west-1".
*/ */
Triton.prototype.findMachine = function findMachine(options, callback) { Triton.prototype.getImage = function getImage(name, cb) {
//XXX Eventually this can be cached for a *full* uuid. Arguably for a assert.string(name, 'name');
// uuid prefix or machine alias match, it cannot be cached, because an assert.func(cb, 'cb');
// ambiguous machine could have been added.
var self = this;
assert.object(options, 'options');
assert.string(options.machine, 'options.machine');
assert.func(callback, 'callback');
var callback = once(callback);
var errs = []; if (common.UUID_RE.test(name)) {
var foundMachine; this.cloudapi.getImage({id: name}, function (err, img) {
var foundDc; if (err) {
async.each( cb(err);
self.dcs(), } else if (img.state !== 'active') {
function oneDc(dc, next) { cb(new Error(format('image %s is not active', name)));
var client = self._clientFromDc(dc.name);
client.getMachine({id: options.machine}, function (err, machine) {
if (err) {
errs.push(err);
} else if (machine) {
foundMachine = machine;
foundDc = dc.name;
// Return early on an unambiguous match.
// XXX When other than full 'id' is supported, this isn't unambiguous.
callback(null, foundMachine, foundDc);
}
next();
});
},
function done(surpriseErr) {
if (surpriseErr) {
callback(surpriseErr);
} else if (foundMachine) {
callback(null, foundMachine, foundDc)
} else if (errs.length) {
callback(errs.length === 1 ?
errs[0] : new errors.MultiError(errs));
} else { } else {
callback(new errors.InternalError( cb(null, img);
'unexpected error finding machine ' + options.id));
} }
}
);
};
/**
* List machines for the current profile.
*
* var res = this.jc.listMachines();
* res.on('data', function (dc, dcMachines) {
* //...
* });
* res.on('dcError', function (dc, dcErr) {
* //...
* });
* res.on('end', function () {
* //...
* });
*
* @param {Object} options Optional
*/
Triton.prototype.listMachines = function listMachines(options) {
var self = this;
if (options === undefined) {
options = {};
}
assert.object(options, 'options');
var emitter = new EventEmitter();
async.each(
self.dcs(),
function oneDc(dc, next) {
var client = self._clientFromDc(dc.name);
client.listMachines(function (err, machines) {
if (err) {
emitter.emit('dcError', dc.name, err);
} else {
emitter.emit('data', dc.name, machines);
}
next();
});
},
function done(err) {
emitter.emit('end');
}
);
return emitter;
};
/**
* Return the audit for the given machine.
*
* @param {Object} options
* - {String} machine (required) The machine id.
* XXX support `machine` being more than just the UUID.
* @param {Function} callback of the form `function (err, audit, dc)`
*/
Triton.prototype.machineAudit = function machineAudit(options, callback) {
var self = this;
assert.object(options, 'options');
assert.string(options.machine, 'options.machine');
self.findMachine({machine: options.machine}, function (err, machine, dc) {
if (err) {
return callback(err);
}
var client = self._clientFromDc(dc);
client.machineAudit({id: machine.id}, function (err, audit) {
callback(err, audit, dc);
}); });
}); } else {
this.cloudapi.listImages(function (err, imgs) {
if (err) {
return cb(err);
}
var nameMatches = [];
for (var i = 0; i < imgs.length; i++) {
if (imgs[i].name === name) {
nameMatches.push(imgs[i]);
}
}
if (nameMatches.length === 0) {
cb(new Error(format('no image with name=%s was found',
name)));
} else if (nameMatches.length === 1) {
cb(null, nameMatches[0]);
} else {
tabula.sortArrayOfObjects(nameMatches, 'published_at');
cb(null, nameMatches[nameMatches.length - 1]);
}
});
}
};
/**
* Get an active package by ID or name. If there is more than one package
* with that name, then this errors out.
*/
Triton.prototype.getPackage = function getPackage(name, cb) {
assert.string(name, 'name');
assert.func(cb, 'cb');
if (common.UUID_RE.test(name)) {
this.cloudapi.getPackage({id: name}, function (err, pkg) {
if (err) {
cb(err);
} else if (!pkg.active) {
cb(new Error(format('image %s is not active', name)));
} else {
cb(null, pkg);
}
});
} else {
this.cloudapi.listPackages(function (err, pkgs) {
if (err) {
return cb(err);
}
var nameMatches = [];
for (var i = 0; i < pkgs.length; i++) {
if (pkgs[i].name === name) {
nameMatches.push(pkgs[i]);
}
}
if (nameMatches.length === 0) {
cb(new Error(format('no package with name=%s was found',
name)));
} else if (nameMatches.length === 1) {
cb(null, nameMatches[0]);
} else {
cb(new Error(format(
'package name "%s" is ambiguous: matches %d packages',
name, nameMatches.length)));
}
});
}
}; };

View File

@ -7,6 +7,7 @@
"dependencies": { "dependencies": {
"assert-plus": "0.1.5", "assert-plus": "0.1.5",
"backoff": "2.4.1", "backoff": "2.4.1",
"bigspinner": "^3.0.0",
"bunyan": "1.4.0", "bunyan": "1.4.0",
"cmdln": "3.2.3", "cmdln": "3.2.3",
"dashdash": "1.10.0", "dashdash": "1.10.0",