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
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 -p PKG [...] IMG
@ -23,38 +20,22 @@ triton image IMAGE # get image
triton packages # list packages
triton package PACKAGE
triton instances|insts # list machines
triton instance|inst ID|NAME # get machine
triton cloudapi ...
# maybe today
triton config defaultPkg t4-standard-1g
triton login|ssh VM # kexec?
triton delete VM|IMAGE # substring matching? too dangerous
triton delete --vm VM
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
by that shortid. Really nice for 80 columns.

View File

@ -61,7 +61,7 @@ function CLI() {
{ group: 'Operator Commands' },
'account',
{ group: 'Instances (aka VMs/Machines/Containers)' },
'create',
'create-instance',
'instances',
'instance',
'instance-audit',
@ -117,9 +117,9 @@ CLI.prototype.do_images = require('./do_images');
CLI.prototype.do_image = require('./do_image');
// Instances (aka VMs/containers/machines)
CLI.prototype.do_create = require('./do_create');
CLI.prototype.do_instance = require('./do_instance');
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_stop_instance = require('./do_startstop_instance')('stop');
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
* - {String} id - machine UUID
* - {String} state - desired state
* - {Array of String} states - desired state
* - {Number} interval (optional) - time in ms to poll
* @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;
assert.object(opts, 'opts');
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.func(callback, 'callback');
var interval = (opts.interval === undefined ? 1000 : opts.interval);
@ -429,7 +429,7 @@ CloudAPI.prototype.waitForMachineState = function waitForMachineState(opts, call
callback(err);
return;
}
if (machine.state === opts.state) {
if (opts.states.indexOf(machine.state) !== -1) {
callback(null, machine);
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).
*

View File

@ -1,11 +1,8 @@
#!/usr/bin/env node
/**
* Copyright (c) 2015 Joyent Inc. All rights reserved.
*/
var assert = require('assert-plus');
var sprintf = require('extsprintf').sprintf;
var util = require('util'),
format = util.format;
@ -17,6 +14,7 @@ var errors = require('./errors'),
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
@ -142,9 +140,45 @@ function isUUID(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
module.exports = {
UUID_RE: UUID_RE,
objCopy: objCopy,
deepObjCopy: deepObjCopy,
zeroPad: zeroPad,
@ -153,5 +187,6 @@ module.exports = {
kvToObj: kvToObj,
longAgo: longAgo,
isUUID: isUUID,
humanDurationFromMs: humanDurationFromMs
};
// 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, {
skipHeader: opts.H,
columns: columns,
sort: sort,
validFields: 'name,memory,disk,swap,vcpus,lwps,default,id,version'.split(',')
sort: sort
});
}
callback();
@ -64,7 +63,7 @@ do_packages.options = [
{
names: ['o'],
type: 'string',
default: 'id,name,version,memory,disk',
default: 'id,name,default,memory,disk',
help: 'Specify fields (columns) to output.',
helpArg: 'field1,...'
},

View File

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

View File

@ -13,6 +13,7 @@ var once = require('once');
var path = require('path');
var restifyClients = require('restify-clients');
var sprintf = require('util').format;
var tabula = require('tabula');
var cloudapi = require('./cloudapi2');
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.
*
*
* @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".
* 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.
*/
Triton.prototype.findMachine = function findMachine(options, callback) {
//XXX Eventually this can be cached for a *full* uuid. Arguably for a
// uuid prefix or machine alias match, it cannot be cached, because an
// 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);
Triton.prototype.getImage = function getImage(name, cb) {
assert.string(name, 'name');
assert.func(cb, 'cb');
var errs = [];
var foundMachine;
var foundDc;
async.each(
self.dcs(),
function oneDc(dc, next) {
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));
if (common.UUID_RE.test(name)) {
this.cloudapi.getImage({id: name}, function (err, img) {
if (err) {
cb(err);
} else if (img.state !== 'active') {
cb(new Error(format('image %s is not active', name)));
} else {
callback(new errors.InternalError(
'unexpected error finding machine ' + options.id));
cb(null, img);
}
}
);
};
/**
* 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": {
"assert-plus": "0.1.5",
"backoff": "2.4.1",
"bigspinner": "^3.0.0",
"bunyan": "1.4.0",
"cmdln": "3.2.3",
"dashdash": "1.10.0",