triton images
This commit is contained in:
parent
8f7fa3ac90
commit
6b3ea63571
23
TODO.txt
23
TODO.txt
@ -35,6 +35,29 @@ 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
|
||||||
|
by that shortid. Really nice for 80 columns.
|
||||||
|
|
||||||
|
triton images
|
||||||
|
Drop 'state' in default columns. Add type to be able to see lx or not
|
||||||
|
for 'linux' ones. That might hit that stupid naming problem.
|
||||||
|
|
||||||
|
|
||||||
# profiles
|
# profiles
|
||||||
|
@ -97,6 +97,8 @@ CLI.prototype.do_foo = function do_foo(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
|
|
||||||
CLI.prototype.do_profile = require('./do_profile');
|
CLI.prototype.do_profile = require('./do_profile');
|
||||||
|
CLI.prototype.do_images = require('./do_images');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CLI.prototype.do_provision = function (subcmd, opts, args, callback) {
|
CLI.prototype.do_provision = function (subcmd, opts, args, callback) {
|
||||||
|
106
lib/cloudapi2.js
106
lib/cloudapi2.js
@ -28,6 +28,7 @@ var p = console.log;
|
|||||||
|
|
||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
var auth = require('smartdc-auth');
|
var auth = require('smartdc-auth');
|
||||||
|
var format = require('util').format;
|
||||||
var os = require('os');
|
var os = require('os');
|
||||||
var querystring = require('querystring');
|
var querystring = require('querystring');
|
||||||
var restifyClients = require('restify-clients');
|
var restifyClients = require('restify-clients');
|
||||||
@ -147,6 +148,61 @@ CloudAPI.prototype._getAuthHeaders = function _getAuthHeaders(callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an appropriate query string *with the leading '?'* from the given
|
||||||
|
* fields. If any of the field values are undefined or null, then they will
|
||||||
|
* be excluded.
|
||||||
|
*/
|
||||||
|
CloudAPI.prototype._qs = function _qs(fields, fields2) {
|
||||||
|
assert.object(fields, 'fields');
|
||||||
|
assert.optionalObject(fields2, 'fields2'); // can be handy to pass in 2 objs
|
||||||
|
|
||||||
|
var query = {};
|
||||||
|
Object.keys(fields).forEach(function (key) {
|
||||||
|
var value = fields[key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
query[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (fields2) {
|
||||||
|
Object.keys(fields2).forEach(function (key) {
|
||||||
|
var value = fields2[key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
query[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(query).length === 0) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return '?' + querystring.stringify(query);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an appropriate full URL *path* given an CloudAPI subpath.
|
||||||
|
* This handles prepending the API's base path, if any: e.g. if the configured
|
||||||
|
* URL is "https://example.com/base/path".
|
||||||
|
*
|
||||||
|
* Optionally an object of query params can be passed in to include a query
|
||||||
|
* string. This just calls `this._qs(...)`.
|
||||||
|
*/
|
||||||
|
CloudAPI.prototype._path = function _path(subpath, qparams, qparams2) {
|
||||||
|
assert.string(subpath, 'subpath');
|
||||||
|
assert.ok(subpath[0] === '/');
|
||||||
|
assert.optionalObject(qparams, 'qparams');
|
||||||
|
assert.optionalObject(qparams2, 'qparams2'); // can be handy to pass in 2
|
||||||
|
|
||||||
|
var path = subpath;
|
||||||
|
if (qparams) {
|
||||||
|
path += this._qs(qparams, qparams2);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---- accounts
|
// ---- accounts
|
||||||
|
|
||||||
@ -157,7 +213,7 @@ CloudAPI.prototype._getAuthHeaders = function _getAuthHeaders(callback) {
|
|||||||
* @param {Object} options (optional)
|
* @param {Object} options (optional)
|
||||||
* @param {Function} callback of the form `function (err, user)`
|
* @param {Function} callback of the form `function (err, user)`
|
||||||
*/
|
*/
|
||||||
CloudAPI.prototype.getAccount = function (options, callback) {
|
CloudAPI.prototype.getAccount = function getAccount(options, callback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (callback === undefined) {
|
if (callback === undefined) {
|
||||||
callback = options;
|
callback = options;
|
||||||
@ -187,6 +243,54 @@ CloudAPI.prototype.getAccount = function (options, callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ---- images
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <http://apidocs.joyent.com/cloudapi/#ListImages>
|
||||||
|
*
|
||||||
|
* @param {Object} options (optional)
|
||||||
|
* XXX document this, see the api doc above :)
|
||||||
|
* @param {Function} callback of the form `function (err, images, res)`
|
||||||
|
*/
|
||||||
|
CloudAPI.prototype.listImages = function listImages(options, callback) {
|
||||||
|
var self = this;
|
||||||
|
if (callback === undefined) {
|
||||||
|
callback = options;
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
assert.object(options, 'options');
|
||||||
|
assert.func(callback, 'callback');
|
||||||
|
|
||||||
|
var query = {
|
||||||
|
name: options.name,
|
||||||
|
os: options.os,
|
||||||
|
version: options.version,
|
||||||
|
public: options.public,
|
||||||
|
state: options.state,
|
||||||
|
owner: options.owner,
|
||||||
|
type: options.type
|
||||||
|
};
|
||||||
|
|
||||||
|
self._getAuthHeaders(function (hErr, headers) {
|
||||||
|
if (hErr) {
|
||||||
|
callback(hErr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var opts = {
|
||||||
|
path: self._path(format('/%s/images', self.user), query),
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
self.client.get(opts, function (err, req, res, body) {
|
||||||
|
if (err) {
|
||||||
|
callback(err, null, res);
|
||||||
|
} else {
|
||||||
|
callback(null, body, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// ---- machines
|
// ---- machines
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
172
lib/do_images.js
Normal file
172
lib/do_images.js
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* `triton images ...`
|
||||||
|
*/
|
||||||
|
|
||||||
|
var format = require('util').format;
|
||||||
|
var tabula = require('tabula');
|
||||||
|
|
||||||
|
var errors = require('./errors');
|
||||||
|
|
||||||
|
|
||||||
|
function do_images(subcmd, opts, args, callback) {
|
||||||
|
if (opts.help) {
|
||||||
|
this.do_help('help', {}, [subcmd], callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JSSTYLED */
|
||||||
|
var columns = opts.o.trim().split(/\s*,\s*/g);
|
||||||
|
/* JSSTYLED */
|
||||||
|
var sort = opts.s.trim().split(/\s*,\s*/g);
|
||||||
|
|
||||||
|
var listOpts = {};
|
||||||
|
var validFilters = [
|
||||||
|
'name', 'os', 'version', 'public', 'state', 'owner', 'type'
|
||||||
|
];
|
||||||
|
for (var i = 0; i < args.length; i++) {
|
||||||
|
var arg = args[i];
|
||||||
|
var idx = arg.indexOf('=');
|
||||||
|
if (idx === -1) {
|
||||||
|
return callback(new errors.UsageError(format(
|
||||||
|
'invalid filter: "%s" (must be of the form "field=value")',
|
||||||
|
arg)));
|
||||||
|
}
|
||||||
|
var k = arg.slice(0, idx);
|
||||||
|
var v = arg.slice(idx + 1);
|
||||||
|
if (validFilters.indexOf(k) === -1) {
|
||||||
|
return callback(new errors.UsageError(format(
|
||||||
|
'invalid filter name: "%s" (must be one of "%s")',
|
||||||
|
k, validFilters.join('", "'))));
|
||||||
|
}
|
||||||
|
listOpts[k] = v;
|
||||||
|
}
|
||||||
|
if (opts.all) {
|
||||||
|
listOpts.state = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.triton.cloudapi.listImages(listOpts, function onRes(err, imgs, res) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
// XXX we should have a common method for all these:
|
||||||
|
// XXX json stream
|
||||||
|
// XXX sorting
|
||||||
|
// XXX if opts.o is given, then filter to just those fields?
|
||||||
|
for (var i = 0; i < imgs.length; i++) {
|
||||||
|
console.log(JSON.stringify(imgs[i]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add some convenience fields
|
||||||
|
// Added fields taken from imgapi-cli.git.
|
||||||
|
for (var i = 0; i < imgs.length; i++) {
|
||||||
|
var img = imgs[i];
|
||||||
|
img.shortid = img.id.split('-', 1)[0];
|
||||||
|
if (img.published_at) {
|
||||||
|
// Just the date.
|
||||||
|
img.pubdate = img.published_at.slice(0, 10);
|
||||||
|
// Normalize on no milliseconds.
|
||||||
|
img.pub = img.published_at.replace(/\.\d+Z$/, 'Z');
|
||||||
|
}
|
||||||
|
if (img.files && img.files[0]) {
|
||||||
|
img.size = img.files[0].size;
|
||||||
|
}
|
||||||
|
var flags = [];
|
||||||
|
if (img.origin) flags.push('I');
|
||||||
|
if (img['public']) flags.push('P');
|
||||||
|
if (img.state !== 'active') flags.push('X');
|
||||||
|
img.flags = flags.length ? flags.join('') : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabula(imgs, {
|
||||||
|
skipHeader: opts.H,
|
||||||
|
columns: columns,
|
||||||
|
sort: sort
|
||||||
|
});
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
do_images.options = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Show this help.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Filtering options'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['all', 'a'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'List all images, not just "active" ones. This ' +
|
||||||
|
'is a shortcut for the "state=all" filter.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Output options'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['H'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Omit table header row.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['o'],
|
||||||
|
type: 'string',
|
||||||
|
default: 'id,name,version,state,flags,os,pubdate',
|
||||||
|
help: 'Specify fields (columns) to output.',
|
||||||
|
helpArg: 'field1,...'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['s'],
|
||||||
|
type: 'string',
|
||||||
|
default: 'published_at',
|
||||||
|
help: 'Sort on the given fields. Default is "published_at".',
|
||||||
|
helpArg: 'field1,...'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['json', 'j'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'JSON stream output.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
do_images.help = (
|
||||||
|
'List images.\n'
|
||||||
|
+ '\n'
|
||||||
|
+ 'Usage:\n'
|
||||||
|
+ ' {{name}} images\n'
|
||||||
|
+ '\n'
|
||||||
|
+ '{{options}}'
|
||||||
|
);
|
||||||
|
do_images.help = (
|
||||||
|
/* BEGIN JSSTYLED */
|
||||||
|
'List images.\n' +
|
||||||
|
'\n' +
|
||||||
|
'Usage:\n' +
|
||||||
|
' {{name}} list [<options>] [<filters>]\n' +
|
||||||
|
'\n' +
|
||||||
|
'Filters:\n' +
|
||||||
|
' FIELD=VALUE Field equality filter. Supported fields: \n' +
|
||||||
|
' account, owner, state, name, os, and type.\n' +
|
||||||
|
' FIELD=true|false Field boolean filter. Supported fields: public.\n' +
|
||||||
|
' FIELD=~SUBSTRING Field substring filter. Supported fields: name\n' +
|
||||||
|
'\n' +
|
||||||
|
'Fields (most are self explanatory, the client adds some for convenience):\n' +
|
||||||
|
' flags This is a set of single letter flags\n' +
|
||||||
|
' summarizing some fields. "P" indicates the\n' +
|
||||||
|
' image is public. "I" indicates an incremental\n' +
|
||||||
|
' image (i.e. has an origin). "X" indicates an\n' +
|
||||||
|
' image with a state *other* than "active".\n' +
|
||||||
|
' pubdate Short form of "published_at" with just the date\n' +
|
||||||
|
' pub Short form of "published_at" elliding milliseconds.\n' +
|
||||||
|
' size The number of bytes of the image file (files.0.size)\n' +
|
||||||
|
'\n' +
|
||||||
|
'{{options}}'
|
||||||
|
/* END JSSTYLED */
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = do_images;
|
@ -19,7 +19,7 @@ var verror = require('verror'),
|
|||||||
* Base error. Instances will always have a string `message` and
|
* Base error. Instances will always have a string `message` and
|
||||||
* a string `code` (a CamelCase string).
|
* a string `code` (a CamelCase string).
|
||||||
*/
|
*/
|
||||||
function SDCError(options) {
|
function TritonError(options) {
|
||||||
assert.object(options, 'options');
|
assert.object(options, 'options');
|
||||||
assert.string(options.message, 'options.message');
|
assert.string(options.message, 'options.message');
|
||||||
assert.string(options.code, 'options.code');
|
assert.string(options.code, 'options.code');
|
||||||
@ -38,7 +38,7 @@ function SDCError(options) {
|
|||||||
self[k] = options[k];
|
self[k] = options[k];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(SDCError, VError);
|
util.inherits(TritonError, VError);
|
||||||
|
|
||||||
function InternalError(cause, message) {
|
function InternalError(cause, message) {
|
||||||
if (message === undefined) {
|
if (message === undefined) {
|
||||||
@ -46,14 +46,14 @@ function InternalError(cause, message) {
|
|||||||
cause = undefined;
|
cause = undefined;
|
||||||
}
|
}
|
||||||
assert.string(message);
|
assert.string(message);
|
||||||
SDCError.call(this, {
|
TritonError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: message,
|
message: message,
|
||||||
code: 'InternalError',
|
code: 'InternalError',
|
||||||
exitStatus: 1
|
exitStatus: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(InternalError, SDCError);
|
util.inherits(InternalError, TritonError);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,14 +65,14 @@ function ConfigError(cause, message) {
|
|||||||
cause = undefined;
|
cause = undefined;
|
||||||
}
|
}
|
||||||
assert.string(message);
|
assert.string(message);
|
||||||
SDCError.call(this, {
|
TritonError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: message,
|
message: message,
|
||||||
code: 'Config',
|
code: 'Config',
|
||||||
exitStatus: 1
|
exitStatus: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(ConfigError, SDCError);
|
util.inherits(ConfigError, TritonError);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,28 +84,28 @@ function UsageError(cause, message) {
|
|||||||
cause = undefined;
|
cause = undefined;
|
||||||
}
|
}
|
||||||
assert.string(message);
|
assert.string(message);
|
||||||
SDCError.call(this, {
|
TritonError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: message,
|
message: message,
|
||||||
code: 'Usage',
|
code: 'Usage',
|
||||||
exitStatus: 1
|
exitStatus: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(UsageError, SDCError);
|
util.inherits(UsageError, TritonError);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An error signing a request.
|
* An error signing a request.
|
||||||
*/
|
*/
|
||||||
function SigningError(cause) {
|
function SigningError(cause) {
|
||||||
SDCError.call(this, {
|
TritonError.call(this, {
|
||||||
cause: cause,
|
cause: cause,
|
||||||
message: 'error signing request',
|
message: 'error signing request',
|
||||||
code: 'Signing',
|
code: 'Signing',
|
||||||
exitStatus: 1
|
exitStatus: 1
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
util.inherits(SigningError, SDCError);
|
util.inherits(SigningError, TritonError);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,7 +118,7 @@ function MultiError(errs) {
|
|||||||
var err = errs[i];
|
var err = errs[i];
|
||||||
lines.push(format(' error (%s): %s', err.code, err.message));
|
lines.push(format(' error (%s): %s', err.code, err.message));
|
||||||
}
|
}
|
||||||
SDCError.call(this, {
|
TritonError.call(this, {
|
||||||
cause: errs[0],
|
cause: errs[0],
|
||||||
message: lines.join('\n'),
|
message: lines.join('\n'),
|
||||||
code: 'MultiError',
|
code: 'MultiError',
|
||||||
@ -126,14 +126,14 @@ function MultiError(errs) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
MultiError.description = 'Multiple errors.';
|
MultiError.description = 'Multiple errors.';
|
||||||
util.inherits(MultiError, SDCError);
|
util.inherits(MultiError, TritonError);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---- exports
|
// ---- exports
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
SDCError: SDCError,
|
TritonError: TritonError,
|
||||||
InternalError: InternalError,
|
InternalError: InternalError,
|
||||||
ConfigError: ConfigError,
|
ConfigError: ConfigError,
|
||||||
UsageError: UsageError,
|
UsageError: UsageError,
|
||||||
|
@ -42,7 +42,6 @@ function Triton(options) {
|
|||||||
if (options.log.serializers &&
|
if (options.log.serializers &&
|
||||||
(!options.log.serializers.client_req ||
|
(!options.log.serializers.client_req ||
|
||||||
!options.log.serializers.client_req)) {
|
!options.log.serializers.client_req)) {
|
||||||
console.log('XXX here');
|
|
||||||
this.log = options.log.child({
|
this.log = options.log.child({
|
||||||
// XXX cheating. restify-clients should export its 'bunyan'.
|
// XXX cheating. restify-clients should export its 'bunyan'.
|
||||||
serializers: require('restify-clients/lib/helpers/bunyan').serializers
|
serializers: require('restify-clients/lib/helpers/bunyan').serializers
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"once": "1.3.2",
|
"once": "1.3.2",
|
||||||
"restify-clients": "1.0.0",
|
"restify-clients": "1.0.0",
|
||||||
"smartdc-auth": "git+ssh://git@github.com:joyent/node-smartdc-auth.git#9f21966",
|
"smartdc-auth": "git+ssh://git@github.com:joyent/node-smartdc-auth.git#9f21966",
|
||||||
"vasync": "*",
|
"tabula": "1.4.2",
|
||||||
|
"vasync": "1.6.3",
|
||||||
"verror": "1.6.0"
|
"verror": "1.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
Reference in New Issue
Block a user