triton images

This commit is contained in:
Trent Mick 2015-08-25 14:25:30 -07:00
parent 8f7fa3ac90
commit 6b3ea63571
7 changed files with 317 additions and 16 deletions

View File

@ -35,6 +35,29 @@ 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.
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

View File

@ -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_images = require('./do_images');
CLI.prototype.do_provision = function (subcmd, opts, args, callback) {

View File

@ -28,6 +28,7 @@ var p = console.log;
var assert = require('assert-plus');
var auth = require('smartdc-auth');
var format = require('util').format;
var os = require('os');
var querystring = require('querystring');
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
@ -157,7 +213,7 @@ CloudAPI.prototype._getAuthHeaders = function _getAuthHeaders(callback) {
* @param {Object} options (optional)
* @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;
if (callback === undefined) {
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
/**

172
lib/do_images.js Normal file
View 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;

View File

@ -19,7 +19,7 @@ var verror = require('verror'),
* Base error. Instances will always have a string `message` and
* a string `code` (a CamelCase string).
*/
function SDCError(options) {
function TritonError(options) {
assert.object(options, 'options');
assert.string(options.message, 'options.message');
assert.string(options.code, 'options.code');
@ -38,7 +38,7 @@ function SDCError(options) {
self[k] = options[k];
});
}
util.inherits(SDCError, VError);
util.inherits(TritonError, VError);
function InternalError(cause, message) {
if (message === undefined) {
@ -46,14 +46,14 @@ function InternalError(cause, message) {
cause = undefined;
}
assert.string(message);
SDCError.call(this, {
TritonError.call(this, {
cause: cause,
message: message,
code: 'InternalError',
exitStatus: 1
});
}
util.inherits(InternalError, SDCError);
util.inherits(InternalError, TritonError);
/**
@ -65,14 +65,14 @@ function ConfigError(cause, message) {
cause = undefined;
}
assert.string(message);
SDCError.call(this, {
TritonError.call(this, {
cause: cause,
message: message,
code: 'Config',
exitStatus: 1
});
}
util.inherits(ConfigError, SDCError);
util.inherits(ConfigError, TritonError);
/**
@ -84,28 +84,28 @@ function UsageError(cause, message) {
cause = undefined;
}
assert.string(message);
SDCError.call(this, {
TritonError.call(this, {
cause: cause,
message: message,
code: 'Usage',
exitStatus: 1
});
}
util.inherits(UsageError, SDCError);
util.inherits(UsageError, TritonError);
/**
* An error signing a request.
*/
function SigningError(cause) {
SDCError.call(this, {
TritonError.call(this, {
cause: cause,
message: 'error signing request',
code: 'Signing',
exitStatus: 1
});
}
util.inherits(SigningError, SDCError);
util.inherits(SigningError, TritonError);
/**
@ -118,7 +118,7 @@ function MultiError(errs) {
var err = errs[i];
lines.push(format(' error (%s): %s', err.code, err.message));
}
SDCError.call(this, {
TritonError.call(this, {
cause: errs[0],
message: lines.join('\n'),
code: 'MultiError',
@ -126,14 +126,14 @@ function MultiError(errs) {
});
}
MultiError.description = 'Multiple errors.';
util.inherits(MultiError, SDCError);
util.inherits(MultiError, TritonError);
// ---- exports
module.exports = {
SDCError: SDCError,
TritonError: TritonError,
InternalError: InternalError,
ConfigError: ConfigError,
UsageError: UsageError,

View File

@ -42,7 +42,6 @@ function Triton(options) {
if (options.log.serializers &&
(!options.log.serializers.client_req ||
!options.log.serializers.client_req)) {
console.log('XXX here');
this.log = options.log.child({
// XXX cheating. restify-clients should export its 'bunyan'.
serializers: require('restify-clients/lib/helpers/bunyan').serializers

View File

@ -16,7 +16,8 @@
"once": "1.3.2",
"restify-clients": "1.0.0",
"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"
},
"engines": {