'triton image create' et al
Fixes #76: `triton image create ...` and `triton image wait ...` Fixes #72: want `triton image` to still return image details even when it is not in 'active' state
This commit is contained in:
parent
d199ed8503
commit
8d235b8e28
@ -1,8 +1,9 @@
|
|||||||
# node-triton changelog
|
# node-triton changelog
|
||||||
|
|
||||||
## 4.2.1 (not yet released)
|
## 4.3.0 (not yet released)
|
||||||
|
|
||||||
(nothing yet)
|
- #76 `triton image create ...` and `triton image wait ...`
|
||||||
|
- #72 want `triton image` to still return image details even when it is not in 'active' state
|
||||||
|
|
||||||
|
|
||||||
## 4.2.0
|
## 4.2.0
|
||||||
|
102
lib/cloudapi2.js
102
lib/cloudapi2.js
@ -540,6 +540,85 @@ CloudApi.prototype.getImage = function getImage(opts, cb) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <http://apidocs.joyent.com/cloudapi/#CreateImageFromMachine>
|
||||||
|
*
|
||||||
|
* @param {Object} opts
|
||||||
|
* - {UUID} machine Required. The ID of the machine from which to create
|
||||||
|
* the image.
|
||||||
|
* - {String} name Required. The image name.
|
||||||
|
* - {String} version Required. The image version.
|
||||||
|
* - {String} description Optional. A short description.
|
||||||
|
* - {String} homepage Optional. Homepage URL.
|
||||||
|
* - {String} eula Optional. EULA URL.
|
||||||
|
* - {Array} acl Optional. An array of account UUIDs to which to give
|
||||||
|
* access. "Access Control List."
|
||||||
|
* - {Object} tags Optional.
|
||||||
|
* @param {Function} cb of the form `function (err, image, res)`
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.createImageFromMachine =
|
||||||
|
function createImageFromMachine(opts, cb) {
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.uuid(opts.machine, 'opts.machine');
|
||||||
|
assert.string(opts.name, 'opts.name');
|
||||||
|
assert.string(opts.version, 'opts.version');
|
||||||
|
assert.optionalString(opts.description, 'opts.description');
|
||||||
|
assert.optionalString(opts.homepage, 'opts.homepage');
|
||||||
|
assert.optionalString(opts.eula, 'opts.eula');
|
||||||
|
assert.optionalArrayOfUuid(opts.acl, 'opts.acl');
|
||||||
|
assert.optionalObject(opts.tags, 'opts.tags');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
this._request({
|
||||||
|
method: 'POST',
|
||||||
|
path: format('/%s/images', this.account),
|
||||||
|
data: opts
|
||||||
|
}, function (err, req, res, body) {
|
||||||
|
cb(err, body, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for an image to go one of a set of specfic states.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
* - {String} id - machine UUID
|
||||||
|
* - {Array of String} states - desired state
|
||||||
|
* - {Number} interval (optional) - Time in ms to poll. Default is 1000ms.
|
||||||
|
* @param {Function} cb - `function (err, image, res)`
|
||||||
|
* Called when state is reached or on error
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.waitForImageStates =
|
||||||
|
function waitForImageStates(opts, cb) {
|
||||||
|
var self = this;
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.uuid(opts.id, 'opts.id');
|
||||||
|
assert.arrayOfString(opts.states, 'opts.states');
|
||||||
|
assert.optionalNumber(opts.interval, 'opts.interval');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
var interval = (opts.interval === undefined ? 1000 : opts.interval);
|
||||||
|
assert.ok(interval > 0, 'interval must be a positive number');
|
||||||
|
|
||||||
|
poll();
|
||||||
|
|
||||||
|
function poll() {
|
||||||
|
self.getImage({id: opts.id}, function (err, img, res) {
|
||||||
|
if (err) {
|
||||||
|
cb(err, null, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (opts.states.indexOf(img.state) !== -1) {
|
||||||
|
cb(null, img, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(poll, interval);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// ---- packages
|
// ---- packages
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -583,14 +662,20 @@ CloudApi.prototype.getPackage = function getPackage(opts, cb) {
|
|||||||
* XXX add getCredentials equivalent
|
* XXX add getCredentials equivalent
|
||||||
* XXX cloudapi docs don't doc the credentials=true option
|
* XXX cloudapi docs don't doc the credentials=true option
|
||||||
*
|
*
|
||||||
* @param {String} uuid (required) The machine id.
|
* For backwards compat, calling with `getMachine(id, cb)` is allowed.
|
||||||
* @param {Function} callback of the form `function (err, machine, res)`
|
*
|
||||||
|
* @param {Object} opts
|
||||||
|
* - id {UUID} Required. The machine id.
|
||||||
|
* @param {Function} cb of the form `function (err, machine, res)`
|
||||||
*/
|
*/
|
||||||
CloudApi.prototype.getMachine = function getMachine(id, cb) {
|
CloudApi.prototype.getMachine = function getMachine(opts, cb) {
|
||||||
assert.uuid(id, 'id');
|
if (typeof (opts) === 'string') {
|
||||||
assert.func(cb, 'cb');
|
opts = {id: opts};
|
||||||
|
}
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.uuid(opts.id, 'opts.id');
|
||||||
|
|
||||||
var endpoint = format('/%s/machines/%s', this.account, id);
|
var endpoint = format('/%s/machines/%s', this.account, opts.id);
|
||||||
this._request(endpoint, function (err, req, res, body) {
|
this._request(endpoint, function (err, req, res, body) {
|
||||||
cb(err, body, res);
|
cb(err, body, res);
|
||||||
});
|
});
|
||||||
@ -677,14 +762,15 @@ CloudApi.prototype._doMachine = function _doMachine(action, uuid, callback) {
|
|||||||
* @param {Function} callback - called when state is reached or on error
|
* @param {Function} callback - called when state is reached or on error
|
||||||
*/
|
*/
|
||||||
CloudApi.prototype.waitForMachineStates =
|
CloudApi.prototype.waitForMachineStates =
|
||||||
function waitForMachineStates(opts, callback) {
|
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.uuid(opts.id, 'opts.id');
|
||||||
assert.arrayOfString(opts.states, 'opts.states');
|
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);
|
||||||
|
assert.ok(interval > 0, 'interval must be a positive number');
|
||||||
|
|
||||||
poll();
|
poll();
|
||||||
|
|
||||||
|
270
lib/do_image/do_create.js
Normal file
270
lib/do_image/do_create.js
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
/*
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2015 Joyent, Inc.
|
||||||
|
*
|
||||||
|
* `triton image create ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
var assert = require('assert-plus');
|
||||||
|
var format = require('util').format;
|
||||||
|
var fs = require('fs');
|
||||||
|
var strsplit = require('strsplit');
|
||||||
|
var tabula = require('tabula');
|
||||||
|
var tilde = require('tilde-expansion');
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
|
var distractions = require('../distractions');
|
||||||
|
var errors = require('../errors');
|
||||||
|
var mat = require('../metadataandtags');
|
||||||
|
|
||||||
|
|
||||||
|
// ---- the command
|
||||||
|
|
||||||
|
function do_create(subcmd, opts, args, cb) {
|
||||||
|
var self = this;
|
||||||
|
if (opts.help) {
|
||||||
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
|
return;
|
||||||
|
} else if (args.length !== 3) {
|
||||||
|
cb(new errors.UsageError(
|
||||||
|
'incorrect number of args: expect 3, got ' + args.length));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var log = this.top.log;
|
||||||
|
var cloudapi = this.top.tritonapi.cloudapi;
|
||||||
|
|
||||||
|
vasync.pipeline({arg: {}, funcs: [
|
||||||
|
function loadTags(ctx, next) {
|
||||||
|
mat.tagsFromOpts(opts, log, function (err, tags) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tags) {
|
||||||
|
log.trace({tags: tags}, 'tags loaded from opts');
|
||||||
|
ctx.tags = tags;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function loadAcl(ctx, next) {
|
||||||
|
if (!opts.acl) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < opts.acl.length; i++) {
|
||||||
|
if (!common.isUUID(opts.acl[i])) {
|
||||||
|
next(new errors.UsageError(format(
|
||||||
|
'invalid --acl: "%s" is not a UUID', opts.acl[i])));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.acl = opts.acl;
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
function getInst(ctx, next) {
|
||||||
|
var id = args[0];
|
||||||
|
if (common.isUUID(id)) {
|
||||||
|
ctx.inst = {id: id};
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.top.tritonapi.getInstance(id, function (err, inst) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.trace({inst: inst}, 'image create: inst');
|
||||||
|
ctx.inst = inst;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function createImg(ctx, next) {
|
||||||
|
var createOpts = {
|
||||||
|
machine: ctx.inst.id,
|
||||||
|
name: args[1],
|
||||||
|
version: args[2],
|
||||||
|
description: opts.description,
|
||||||
|
homepage: opts.homepage,
|
||||||
|
eula: opts.eula,
|
||||||
|
acl: ctx.acl,
|
||||||
|
tags: ctx.tags
|
||||||
|
};
|
||||||
|
|
||||||
|
log.trace({dryRun: opts.dry_run, createOpts: createOpts},
|
||||||
|
'image create createOpts');
|
||||||
|
ctx.start = Date.now();
|
||||||
|
if (opts.dry_run) {
|
||||||
|
ctx.inst = {
|
||||||
|
id: 'cafecafe-4c0e-11e5-86cd-a7fd38d2a50b',
|
||||||
|
name: 'this-is-a-dry-run'
|
||||||
|
};
|
||||||
|
console.log('Creating image %s@%s from instance %s%s',
|
||||||
|
createOpts.name, createOpts.version, ctx.inst.id,
|
||||||
|
(ctx.inst.name ? ' ('+ctx.inst.name+')' : ''));
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudapi.createImageFromMachine(createOpts, function (err, img) {
|
||||||
|
if (err) {
|
||||||
|
next(new errors.TritonError(err, 'error creating image'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.img = img;
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(img));
|
||||||
|
} else {
|
||||||
|
console.log('Creating image %s@%s (%s)',
|
||||||
|
img.name, img.version, img.id);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function maybeWait(ctx, next) {
|
||||||
|
if (!opts.wait) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 'wait': no distraction.
|
||||||
|
// >1 'wait': distraction, pass in the N.
|
||||||
|
var distraction;
|
||||||
|
if (process.stderr.isTTY && opts.wait.length > 1) {
|
||||||
|
distraction = distractions.createDistraction(opts.wait.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dry-run: fake wait for a few seconds.
|
||||||
|
var waiter = (opts.dry_run ?
|
||||||
|
function dryWait(waitOpts, waitCb) {
|
||||||
|
setTimeout(function () {
|
||||||
|
ctx.img.state = 'running';
|
||||||
|
waitCb(null, ctx.img);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
: cloudapi.waitForImageStates.bind(cloudapi));
|
||||||
|
|
||||||
|
waiter({
|
||||||
|
id: ctx.img.id,
|
||||||
|
states: ['active', 'failed']
|
||||||
|
}, function (err, img) {
|
||||||
|
if (distraction) {
|
||||||
|
distraction.destroy();
|
||||||
|
}
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
if (opts.json) {
|
||||||
|
console.log(JSON.stringify(img));
|
||||||
|
} else if (img.state === 'active') {
|
||||||
|
var dur = Date.now() - ctx.start;
|
||||||
|
console.log('Created image %s (%s@%s) in %s',
|
||||||
|
img.id, img.name, img.version,
|
||||||
|
common.humanDurationFromMs(dur));
|
||||||
|
}
|
||||||
|
if (img.state !== 'active') {
|
||||||
|
next(new Error(format('failed to create image %s (%s@%s)%s',
|
||||||
|
img.id, img.name, img.version,
|
||||||
|
(img.error ? format(': (%s) %s',
|
||||||
|
img.error.code, img.error.message): ''))));
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]}, function (err) {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
do_create.options = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Show this help.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Create options'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['description', 'd'],
|
||||||
|
type: 'string',
|
||||||
|
helpArg: 'DESC',
|
||||||
|
help: 'A short description of the image.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['homepage'],
|
||||||
|
type: 'string',
|
||||||
|
helpArg: 'URL',
|
||||||
|
help: 'A homepage URL for the image.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['eula'],
|
||||||
|
type: 'string',
|
||||||
|
helpArg: 'DESC',
|
||||||
|
help: 'A URL for an End User License Agreement (EULA) for the image.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['acl'],
|
||||||
|
type: 'arrayOfString',
|
||||||
|
helpArg: 'ID',
|
||||||
|
help: 'Access Control List. The ID of an account to which to give ' +
|
||||||
|
'access to this private image. This option can be used multiple ' +
|
||||||
|
'times to give access to multiple accounts.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['tag', 't'],
|
||||||
|
type: 'arrayOfString',
|
||||||
|
helpArg: 'TAG',
|
||||||
|
help: 'Add a tag when creating the image. Tags are ' +
|
||||||
|
'key/value pairs available on the image API object as the ' +
|
||||||
|
'"tags" field. TAG is one of: a "key=value" string (bool and ' +
|
||||||
|
'numeric "value" are converted to that type), a JSON object ' +
|
||||||
|
'(if first char is "{"), or a "@FILE" to have tags be ' +
|
||||||
|
'loaded from FILE. This option can be used multiple times.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Other options'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['dry-run'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Go through the motions without actually creating.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['wait', 'w'],
|
||||||
|
type: 'arrayOfBool',
|
||||||
|
help: 'Wait for the creation to complete. Use multiple times for a ' +
|
||||||
|
'spinner.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['json', 'j'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'JSON stream output.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
do_create.help = (
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
|
'Create a new instance.\n' +
|
||||||
|
'\n' +
|
||||||
|
'Usage:\n' +
|
||||||
|
' {{name}} create [<options>] INSTANCE IMAGE-NAME IMAGE-VERSION\n' +
|
||||||
|
'\n' +
|
||||||
|
'{{options}}'
|
||||||
|
/* END JSSTYLED */
|
||||||
|
);
|
||||||
|
|
||||||
|
do_create.helpOpts = {
|
||||||
|
maxHelpCol: 20
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = do_create;
|
142
lib/do_image/do_wait.js
Normal file
142
lib/do_image/do_wait.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Joyent, Inc.
|
||||||
|
*
|
||||||
|
* `triton image wait ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var distractions = require('../distractions');
|
||||||
|
var errors = require('../errors');
|
||||||
|
|
||||||
|
|
||||||
|
function do_wait(subcmd, opts, args, cb) {
|
||||||
|
var self = this;
|
||||||
|
if (opts.help) {
|
||||||
|
return this.do_help('help', {}, [subcmd], cb);
|
||||||
|
} else if (args.length < 1) {
|
||||||
|
return cb(new errors.UsageError('missing IMAGE arg(s)'));
|
||||||
|
}
|
||||||
|
var ids = args;
|
||||||
|
var states = [];
|
||||||
|
opts.states.forEach(function (s) {
|
||||||
|
/* JSSTYLED */
|
||||||
|
states = states.concat(s.trim().split(/\s*,\s*/g));
|
||||||
|
});
|
||||||
|
|
||||||
|
var distraction;
|
||||||
|
var done = 0;
|
||||||
|
var imgFromId = {};
|
||||||
|
|
||||||
|
vasync.pipeline({funcs: [
|
||||||
|
function getImgs(_, next) {
|
||||||
|
vasync.forEachParallel({
|
||||||
|
inputs: ids,
|
||||||
|
func: function getImg(id, nextImg) {
|
||||||
|
self.top.tritonapi.getImage(id, function (err, img) {
|
||||||
|
if (err) {
|
||||||
|
return nextImg(err);
|
||||||
|
}
|
||||||
|
if (states.indexOf(img.state) !== -1) {
|
||||||
|
console.log('%d/%d: Image %s (%s@%s) already %s',
|
||||||
|
++done, ids.length, img.id, img.name,
|
||||||
|
img.version, img.state);
|
||||||
|
} else {
|
||||||
|
imgFromId[img.id] = img;
|
||||||
|
}
|
||||||
|
nextImg();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
|
||||||
|
function waitForImgs(_, next) {
|
||||||
|
var idsToWaitFor = Object.keys(imgFromId);
|
||||||
|
if (idsToWaitFor.length === 0) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idsToWaitFor.length === 1) {
|
||||||
|
var img2 = imgFromId[idsToWaitFor[0]];
|
||||||
|
console.log(
|
||||||
|
'Waiting for image %s (%s@%s) to enter state (states: %s)',
|
||||||
|
img2.id, img2.name, img2.version, states.join(', '));
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'Waiting for %d images to enter state (states: %s)',
|
||||||
|
idsToWaitFor.length, states.join(', '));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: need BigSpinner.log first.
|
||||||
|
* TODO: Also when adding a spinner, we need an equiv option to
|
||||||
|
* `triton create -wwww` to trigger the spinner (and size). By
|
||||||
|
* default: no spinner.
|
||||||
|
*/
|
||||||
|
if (false &&
|
||||||
|
process.stderr.isTTY)
|
||||||
|
{
|
||||||
|
distraction = distractions.createDistraction();
|
||||||
|
}
|
||||||
|
|
||||||
|
vasync.forEachParallel({
|
||||||
|
inputs: idsToWaitFor,
|
||||||
|
func: function waitForImg(id, nextImg) {
|
||||||
|
self.top.tritonapi.cloudapi.waitForImageStates({
|
||||||
|
id: id,
|
||||||
|
states: states
|
||||||
|
}, function (err, img, res) {
|
||||||
|
if (err) {
|
||||||
|
return nextImg(err);
|
||||||
|
}
|
||||||
|
console.log('%d/%d: Image %s (%s@%s) moved to state %s',
|
||||||
|
++done, ids.length, img.id, img.name,
|
||||||
|
img.version, img.state);
|
||||||
|
nextImg();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
]}, function (err) {
|
||||||
|
if (distraction) {
|
||||||
|
distraction.destroy();
|
||||||
|
}
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
do_wait.help = [
|
||||||
|
'Wait for images to change to a particular state.',
|
||||||
|
'',
|
||||||
|
'Usage:',
|
||||||
|
' {{name}} wait [-s STATES] IMAGE [IMAGE ...]',
|
||||||
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
'Where "states" is a comma-separated list of target instance states,',
|
||||||
|
'by default "active,failed". In other words, "triton img wait foo0" will',
|
||||||
|
'wait for image "foo0" to complete creation.'
|
||||||
|
].join('\n');
|
||||||
|
do_wait.options = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Show this help.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['states', 's'],
|
||||||
|
type: 'arrayOfString',
|
||||||
|
default: ['active', 'failed'],
|
||||||
|
helpArg: 'STATES',
|
||||||
|
help: 'Instance states on which to wait. Default is "active,failed". '
|
||||||
|
+ 'Values can be comma-separated or multiple uses of the option.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = do_wait;
|
@ -32,7 +32,9 @@ function ImageCLI(top) {
|
|||||||
helpSubcmds: [
|
helpSubcmds: [
|
||||||
'help',
|
'help',
|
||||||
'list',
|
'list',
|
||||||
'get'
|
'get',
|
||||||
|
'create',
|
||||||
|
'wait'
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -45,6 +47,8 @@ ImageCLI.prototype.init = function init(opts, args, cb) {
|
|||||||
|
|
||||||
ImageCLI.prototype.do_list = require('./do_list');
|
ImageCLI.prototype.do_list = require('./do_list');
|
||||||
ImageCLI.prototype.do_get = require('./do_get');
|
ImageCLI.prototype.do_get = require('./do_get');
|
||||||
|
ImageCLI.prototype.do_create = require('./do_create');
|
||||||
|
ImageCLI.prototype.do_wait = require('./do_wait');
|
||||||
|
|
||||||
|
|
||||||
ImageCLI.aliases = ['img'];
|
ImageCLI.aliases = ['img'];
|
||||||
|
@ -12,287 +12,15 @@
|
|||||||
|
|
||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
var fs = require('fs');
|
|
||||||
var strsplit = require('strsplit');
|
|
||||||
var tabula = require('tabula');
|
var tabula = require('tabula');
|
||||||
var tilde = require('tilde-expansion');
|
|
||||||
var vasync = require('vasync');
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var common = require('../common');
|
var common = require('../common');
|
||||||
var distractions = require('../distractions');
|
var distractions = require('../distractions');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
|
var mat = require('../metadataandtags');
|
||||||
|
|
||||||
|
|
||||||
// ---- loading/parsing metadata (and tags) from relevant options
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Load and validate metadata from these options:
|
|
||||||
* -m,--metadata DATA
|
|
||||||
* -M,--metadata-file KEY=FILE
|
|
||||||
* --script FILE
|
|
||||||
*
|
|
||||||
* <https://github.com/joyent/sdc-vmapi/blob/master/docs/index.md#vm-metadata>
|
|
||||||
* says values may be string, num or bool.
|
|
||||||
*/
|
|
||||||
function metadataFromOpts(opts, log, cb) {
|
|
||||||
assert.arrayOfObject(opts._order, 'opts._order');
|
|
||||||
assert.object(log, 'log');
|
|
||||||
assert.func(cb, 'cb');
|
|
||||||
|
|
||||||
var metadata = {};
|
|
||||||
|
|
||||||
vasync.forEachPipeline({
|
|
||||||
inputs: opts._order,
|
|
||||||
func: function metadataFromOpt(o, next) {
|
|
||||||
log.trace({opt: o}, 'metadataFromOpt');
|
|
||||||
if (o.key === 'metadata') {
|
|
||||||
if (!o.value) {
|
|
||||||
next(new errors.UsageError(
|
|
||||||
'empty metadata option value'));
|
|
||||||
return;
|
|
||||||
} else if (o.value[0] === '{') {
|
|
||||||
_addMetadataFromJsonStr(
|
|
||||||
'metadata', metadata, o.value, null, next);
|
|
||||||
} else if (o.value[0] === '@') {
|
|
||||||
_addMetadataFromFile(
|
|
||||||
'metadata', metadata, o.value.slice(1), next);
|
|
||||||
} else {
|
|
||||||
_addMetadataFromKvStr(
|
|
||||||
'metadata', metadata, o.value, null, next);
|
|
||||||
}
|
|
||||||
} else if (o.key === 'metadata_file') {
|
|
||||||
_addMetadataFromKfStr(
|
|
||||||
'metadata', metadata, o.value, null, next);
|
|
||||||
} else if (o.key === 'script') {
|
|
||||||
_addMetadatumFromFile('metadata', metadata,
|
|
||||||
'user-script', o.value, o.value, next);
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, function (err) {
|
|
||||||
if (err) {
|
|
||||||
cb(err);
|
|
||||||
} else if (Object.keys(metadata).length) {
|
|
||||||
cb(null, metadata);
|
|
||||||
} else {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Load and validate tags from these options:
|
|
||||||
* -t,--tag DATA
|
|
||||||
*
|
|
||||||
* <https://github.com/joyent/sdc-vmapi/blob/master/docs/index.md#vm-metadata>
|
|
||||||
* says values may be string, num or bool.
|
|
||||||
*/
|
|
||||||
function tagsFromOpts(opts, log, cb) {
|
|
||||||
assert.arrayOfObject(opts._order, 'opts._order');
|
|
||||||
assert.object(log, 'log');
|
|
||||||
assert.func(cb, 'cb');
|
|
||||||
|
|
||||||
var tags = {};
|
|
||||||
|
|
||||||
vasync.forEachPipeline({
|
|
||||||
inputs: opts._order,
|
|
||||||
func: function tagsFromOpt(o, next) {
|
|
||||||
log.trace({opt: o}, 'tagsFromOpt');
|
|
||||||
if (o.key === 'tag') {
|
|
||||||
if (!o.value) {
|
|
||||||
next(new errors.UsageError(
|
|
||||||
'empty tag option value'));
|
|
||||||
return;
|
|
||||||
} else if (o.value[0] === '{') {
|
|
||||||
_addMetadataFromJsonStr('tag', tags, o.value, null, next);
|
|
||||||
} else if (o.value[0] === '@') {
|
|
||||||
_addMetadataFromFile('tag', tags, o.value.slice(1), next);
|
|
||||||
} else {
|
|
||||||
_addMetadataFromKvStr('tag', tags, o.value, null, next);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, function (err) {
|
|
||||||
if (err) {
|
|
||||||
cb(err);
|
|
||||||
} else if (Object.keys(tags).length) {
|
|
||||||
cb(null, tags);
|
|
||||||
} else {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var allowedTypes = ['string', 'number', 'boolean'];
|
|
||||||
function _addMetadatum(ilk, metadata, key, value, from, cb) {
|
|
||||||
assert.string(ilk, 'ilk');
|
|
||||||
assert.object(metadata, 'metadata');
|
|
||||||
assert.string(key, 'key');
|
|
||||||
assert.optionalString(from, 'from');
|
|
||||||
assert.func(cb, 'cb');
|
|
||||||
|
|
||||||
if (allowedTypes.indexOf(typeof (value)) === -1) {
|
|
||||||
cb(new errors.UsageError(format(
|
|
||||||
'invalid %s value type%s: must be one of %s: %s=%j',
|
|
||||||
ilk, (from ? ' (from ' + from + ')' : ''),
|
|
||||||
allowedTypes.join(', '), key, value)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.hasOwnProperty(key)) {
|
|
||||||
var valueStr = value.toString();
|
|
||||||
console.error(
|
|
||||||
'warning: %s "%s=%s"%s replaces earlier value for "%s"',
|
|
||||||
ilk,
|
|
||||||
key,
|
|
||||||
(valueStr.length > 10
|
|
||||||
? valueStr.slice(0, 7) + '...' : valueStr),
|
|
||||||
(from ? ' (from ' + from + ')' : ''),
|
|
||||||
key);
|
|
||||||
}
|
|
||||||
metadata[key] = value;
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
|
|
||||||
function _addMetadataFromObj(ilk, metadata, obj, from, cb) {
|
|
||||||
assert.string(ilk, 'ilk');
|
|
||||||
assert.object(metadata, 'metadata');
|
|
||||||
assert.object(obj, 'obj');
|
|
||||||
assert.optionalString(from, 'from');
|
|
||||||
assert.func(cb, 'cb');
|
|
||||||
|
|
||||||
vasync.forEachPipeline({
|
|
||||||
inputs: Object.keys(obj),
|
|
||||||
func: function _oneField(key, next) {
|
|
||||||
_addMetadatum(ilk, metadata, key, obj[key], from, next);
|
|
||||||
}
|
|
||||||
}, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _addMetadataFromJsonStr(ilk, metadata, s, from, cb) {
|
|
||||||
assert.string(ilk, 'ilk');
|
|
||||||
try {
|
|
||||||
var obj = JSON.parse(s);
|
|
||||||
} catch (parseErr) {
|
|
||||||
cb(new errors.TritonError(parseErr,
|
|
||||||
format('%s%s is not valid JSON', ilk,
|
|
||||||
(from ? ' (from ' + from + ')' : ''))));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_addMetadataFromObj(ilk, metadata, obj, from, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _addMetadataFromFile(ilk, metadata, file, cb) {
|
|
||||||
assert.string(ilk, 'ilk');
|
|
||||||
tilde(file, function (metaPath) {
|
|
||||||
fs.stat(metaPath, function (statErr, stats) {
|
|
||||||
if (statErr || !stats.isFile()) {
|
|
||||||
cb(new errors.TritonError(format(
|
|
||||||
'"%s" is not an existing file', file)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fs.readFile(metaPath, 'utf8', function (readErr, data) {
|
|
||||||
if (readErr) {
|
|
||||||
cb(readErr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* The file is either a JSON object (first non-space
|
|
||||||
* char is '{'), or newline-separated key=value
|
|
||||||
* pairs.
|
|
||||||
*/
|
|
||||||
var dataTrim = data.trim();
|
|
||||||
if (dataTrim.length && dataTrim[0] === '{') {
|
|
||||||
_addMetadataFromJsonStr(ilk, metadata, dataTrim, file, cb);
|
|
||||||
} else {
|
|
||||||
var lines = dataTrim.split(/\r?\n/g).filter(
|
|
||||||
function (line) { return line.trim(); });
|
|
||||||
vasync.forEachPipeline({
|
|
||||||
inputs: lines,
|
|
||||||
func: function oneLine(line, next) {
|
|
||||||
_addMetadataFromKvStr(
|
|
||||||
ilk, metadata, line, file, next);
|
|
||||||
}
|
|
||||||
}, cb);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _addMetadataFromKvStr(ilk, metadata, s, from, cb) {
|
|
||||||
assert.string(ilk, 'ilk');
|
|
||||||
|
|
||||||
var parts = strsplit(s, '=', 2);
|
|
||||||
if (parts.length !== 2) {
|
|
||||||
cb(new errors.UsageError(format(
|
|
||||||
'invalid KEY=VALUE %s argument: %s', ilk, s)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var value = parts[1];
|
|
||||||
var valueTrim = value.trim();
|
|
||||||
if (valueTrim === 'true') {
|
|
||||||
value = true;
|
|
||||||
} else if (valueTrim === 'false') {
|
|
||||||
value = false;
|
|
||||||
} else {
|
|
||||||
var num = Number(value);
|
|
||||||
if (!isNaN(num)) {
|
|
||||||
value = num;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_addMetadatum(ilk, metadata, parts[0].trim(), value, from, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Add metadata from `KEY=FILE` argument.
|
|
||||||
* Here "Kf" stands for "key/file".
|
|
||||||
*/
|
|
||||||
function _addMetadataFromKfStr(ilk, metadata, s, from, cb) {
|
|
||||||
assert.string(ilk, 'ilk');
|
|
||||||
|
|
||||||
var parts = strsplit(s, '=', 2);
|
|
||||||
if (parts.length !== 2) {
|
|
||||||
cb(new errors.UsageError(format(
|
|
||||||
'invalid KEY=FILE %s argument: %s', ilk, s)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var key = parts[0].trim();
|
|
||||||
var file = parts[1];
|
|
||||||
|
|
||||||
_addMetadatumFromFile(ilk, metadata, key, file, file, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _addMetadatumFromFile(ilk, metadata, key, file, from, cb) {
|
|
||||||
assert.string(ilk, 'ilk');
|
|
||||||
|
|
||||||
tilde(file, function (filePath) {
|
|
||||||
fs.stat(filePath, function (statErr, stats) {
|
|
||||||
if (statErr || !stats.isFile()) {
|
|
||||||
cb(new errors.TritonError(format(
|
|
||||||
'%s path "%s" is not an existing file', ilk, file)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fs.readFile(filePath, 'utf8', function (readErr, content) {
|
|
||||||
if (readErr) {
|
|
||||||
cb(readErr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_addMetadatum(ilk, metadata, key, content, from, cb);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---- the command
|
|
||||||
|
|
||||||
function do_create(subcmd, opts, args, cb) {
|
function do_create(subcmd, opts, args, cb) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
@ -307,7 +35,7 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
|
|
||||||
vasync.pipeline({arg: {}, funcs: [
|
vasync.pipeline({arg: {}, funcs: [
|
||||||
function loadMetadata(ctx, next) {
|
function loadMetadata(ctx, next) {
|
||||||
metadataFromOpts(opts, log, function (err, metadata) {
|
mat.metadataFromOpts(opts, log, function (err, metadata) {
|
||||||
if (err) {
|
if (err) {
|
||||||
next(err);
|
next(err);
|
||||||
return;
|
return;
|
||||||
@ -321,7 +49,7 @@ function do_create(subcmd, opts, args, cb) {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
function loadTags(ctx, next) {
|
function loadTags(ctx, next) {
|
||||||
tagsFromOpts(opts, log, function (err, tags) {
|
mat.tagsFromOpts(opts, log, function (err, tags) {
|
||||||
if (err) {
|
if (err) {
|
||||||
next(err);
|
next(err);
|
||||||
return;
|
return;
|
||||||
@ -604,5 +332,3 @@ do_create.helpOpts = {
|
|||||||
|
|
||||||
|
|
||||||
module.exports = do_create;
|
module.exports = do_create;
|
||||||
do_create.metadataFromOpts = metadataFromOpts; // export for testing
|
|
||||||
do_create.tagsFromOpts = tagsFromOpts; // export for testing
|
|
||||||
|
@ -354,9 +354,6 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
|
|||||||
]}, function done(err) {
|
]}, function done(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
} else if (img.state !== 'active') {
|
|
||||||
cb(new errors.TritonError(
|
|
||||||
format('image %s is not active', opts.name)));
|
|
||||||
} else {
|
} else {
|
||||||
cb(null, img);
|
cb(null, img);
|
||||||
}
|
}
|
||||||
@ -366,7 +363,10 @@ TritonApi.prototype.getImage = function getImage(opts, cb) {
|
|||||||
var name = s[0];
|
var name = s[0];
|
||||||
var version = s[1];
|
var version = s[1];
|
||||||
|
|
||||||
var listOpts = {};
|
var listOpts = {
|
||||||
|
// Explicitly include inactive images.
|
||||||
|
state: 'all'
|
||||||
|
};
|
||||||
if (version) {
|
if (version) {
|
||||||
listOpts.name = name;
|
listOpts.name = name;
|
||||||
listOpts.version = version;
|
listOpts.version = version;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "triton",
|
"name": "triton",
|
||||||
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
|
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
|
||||||
"version": "4.2.1",
|
"version": "4.3.0",
|
||||||
"author": "Joyent (joyent.com)",
|
"author": "Joyent (joyent.com)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"assert-plus": "0.2.0",
|
"assert-plus": "0.2.0",
|
||||||
|
@ -225,8 +225,10 @@ test('triton manage workflow', opts, function (tt) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// remove instance
|
// Remove instance. Add a test timeout, because '-w' on delete doesn't
|
||||||
tt.test(' triton delete', function (t) {
|
// have a way to know if the attempt failed or if it is just taking a
|
||||||
|
// really long time.
|
||||||
|
tt.test(' triton delete', {timeout: 10 * 60 * 1000}, function (t) {
|
||||||
h.safeTriton(t, ['delete', '-w', instance.id], function (stdout) {
|
h.safeTriton(t, ['delete', '-w', instance.id], function (stdout) {
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
@ -17,8 +17,7 @@ var cmdln = require('cmdln');
|
|||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
var test = require('tape');
|
var test = require('tape');
|
||||||
|
|
||||||
var metadataFromOpts =
|
var metadataFromOpts = require('../../lib/metadataandtags').metadataFromOpts;
|
||||||
require('../../lib/do_instance/do_create').metadataFromOpts;
|
|
||||||
|
|
||||||
|
|
||||||
// ---- globals
|
// ---- globals
|
||||||
|
@ -17,7 +17,7 @@ var cmdln = require('cmdln');
|
|||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
var test = require('tape');
|
var test = require('tape');
|
||||||
|
|
||||||
var tagsFromOpts = require('../../lib/do_instance/do_create').tagsFromOpts;
|
var tagsFromOpts = require('../../lib/metadataandtags').tagsFromOpts;
|
||||||
|
|
||||||
|
|
||||||
// ---- globals
|
// ---- globals
|
||||||
|
Reference in New Issue
Block a user