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 --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
|
||||
|
@ -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) {
|
||||
|
106
lib/cloudapi2.js
106
lib/cloudapi2.js
@ -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
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
|
||||
* 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,
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
Reference in New Issue
Block a user