add packages, remove tabulate, put stuff in common

This commit is contained in:
Dave Eddy 2015-08-25 18:30:25 -04:00
parent 8ff2fcb53c
commit 7afaadbd29
6 changed files with 260 additions and 159 deletions

View File

@ -107,6 +107,11 @@ CLI.prototype.do_create = require('./do_create');
CLI.prototype.do_instances = require('./do_instances');
CLI.prototype.do_instance_audit = require('./do_instance_audit');
// Packages
CLI.prototype.do_packages = require('./do_packages');
// Row Cloud API
CLI.prototype.do_cloudapi = require('./do_cloudapi');

View File

@ -153,25 +153,18 @@ CloudAPI.prototype._getAuthHeaders = function _getAuthHeaders(callback) {
* 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
CloudAPI.prototype._qs = function _qs(/* fields1, ...*/) {
var fields = Array.prototype.slice.call(arguments);
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];
fields.forEach(function (field) {
Object.keys(field).forEach(function (key) {
var value = field[key];
if (value !== undefined && value !== null) {
query[key] = value;
}
});
}
});
if (Object.keys(query).length === 0) {
return '';
@ -189,20 +182,47 @@ CloudAPI.prototype._qs = function _qs(fields, fields2) {
* 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) {
CloudAPI.prototype._path = function _path(subpath /*, qparams, ... */) {
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);
}
var qparams = Array.prototype.slice.call(arguments, 1);
path += this._qs.apply(this, qparams);
return path;
};
/**
* cloud API requset wrapper - modeled after http.request
*
* @param {Object|String} options - object or string for endpoint
* - {String} path - URL endpoint to hit
* - {String} method - HTTP(s) request method
* @param {Function} callback passed via the restify client
*/
CloudAPI.prototype.request = function _request(options, callback) {
var self = this;
if (typeof options === 'string')
options = {path: options};
assert.object(options, 'options');
assert.func(callback, 'callback');
var method = (options.method || 'GET').toLowerCase();
assert.ok(['get', 'post', 'delete', 'head'].indexOf(method) >= 0,
'invalid method given');
self._getAuthHeaders(function (err, headers) {
if (err) {
callback(err);
return;
}
var opts = {
path: options.path,
headers: headers
};
self.client[method](opts, callback);
});
};
// ---- accounts
@ -408,9 +428,25 @@ CloudAPI.prototype.listMachines = function listMachines(options, callback) {
callback(null, machines, responses);
}
}
)
);
};
CloudAPI.prototype.listPackages = function listPackages(options, callback) {
var self = this;
if (typeof (options) === 'function') {
callback = options;
options = {};
}
var endpoint = self._path(format('/%s/packages', self.user), options);
self.request(endpoint, function (err, req, res, body) {
if (err) {
callback(err);
return;
}
callback(null, body);
});
};
/**

View File

@ -70,127 +70,41 @@ function boolFromString(value, default_, errName) {
}
}
/**
* Print a table of the given items.
*
* @params items {Array} of row objects.
* @params options {Object}
* - `columns` {String} of comma-separated field names for columns
* - `skipHeader` {Boolean} Default false.
* - `sort` {String} of comma-separate fields on which to alphabetically
* sort the rows. Optional.
* - `validFields` {String} valid fields for `columns` and `sort`
* given an array return a string with each element
* JSON-stringifed separated by newlines
*/
function tabulate(items, options) {
assert.arrayOfObject(items, 'items');
assert.object(options, 'options');
assert.string(options.columns, 'options.columns');
assert.optionalBool(options.skipHeader, 'options.skipHeader');
assert.optionalString(options.sort, 'options.sort');
assert.optionalString(options.validFields, 'options.validFields');
if (items.length === 0) {
return;
}
// Validate.
var validFields = options.validFields && options.validFields.split(',');
var columns = options.columns.split(',');
var sort = options.sort ? options.sort.split(',') : [];
if (validFields) {
columns.forEach(function (c) {
if (validFields.indexOf(c) === -1) {
throw new TypeError(sprintf('invalid output field: "%s"', c));
}
});
}
sort.forEach(function (s) {
if (s[0] === '-') s = s.slice(1);
if (validFields && validFields.indexOf(s) === -1) {
throw new TypeError(sprintf('invalid sort field: "%s"', s));
}
});
// Function to lookup each column field in a row.
var colFuncs = columns.map(function (lookup) {
return new Function(
'try { return (this["' + lookup + '"]); } catch (e) {}');
});
// Determine columns and widths.
var widths = {};
columns.forEach(function (c) { widths[c] = c.length; });
items.forEach(function (item) {
for (var j = 0; j < columns.length; j++) {
var col = columns[j];
var cell = colFuncs[j].call(item);
if (cell === null || cell === undefined) {
continue;
}
widths[col] = Math.max(
widths[col], (cell ? String(cell).length : 0));
}
});
var template = '';
for (var i = 0; i < columns.length; i++) {
if (i === columns.length - 1) {
// Last column, don't have trailing whitespace.
template += '%s';
} else {
template += '%-' + String(widths[columns[i]]) + 's ';
}
}
function cmp(a, b) {
for (var j = 0; j < sort.length; j++) {
var field = sort[j];
var invert = false;
if (field[0] === '-') {
invert = true;
field = field.slice(1);
}
assert.ok(field.length, 'zero-length sort field: ' + options.sort);
var a_cmp = Number(a[field]);
var b_cmp = Number(b[field]);
if (isNaN(a_cmp) || isNaN(b_cmp)) {
a_cmp = a[field] || '';
b_cmp = b[field] || '';
}
if (a_cmp < b_cmp) {
return (invert ? 1 : -1);
} else if (a_cmp > b_cmp) {
return (invert ? -1 : 1);
}
}
return 0;
}
if (sort.length) {
items.sort(cmp);
}
if (!options.skipHeader) {
var header = columns.map(function (c) { return c.toUpperCase(); });
header.unshift(template);
console.log(sprintf.apply(null, header));
}
items.forEach(function (item) {
var row = [];
for (var j = 0; j < colFuncs.length; j++) {
var cell = colFuncs[j].call(item);
if (cell === null || cell === undefined) {
row.push('-');
} else {
row.push(String(cell));
}
}
row.unshift(template);
console.log(sprintf.apply(null, row));
});
function jsonStream(arr) {
return arr.map(function (elem) {
return JSON.stringify(elem);
}).join('\n');
}
/**
* given an array of key=value pairs, break them into an object
*
* @param {Array} kvs - an array of key=value pairs
* @param {Array} valid (optional) - an array to validate pairs
*/
function kvToObj(kvs, valid) {
var o = {};
for (var i = 0; i < kvs.length; i++) {
var kv = kvs[i];
var idx = kv.indexOf('=');
if (idx === -1)
throw new errors.UsageError(format(
'invalid filter: "%s" (must be of the form "field=value")',
kv));
var k = kv.slice(0, idx);
var v = kv.slice(idx + 1);
if (valid.indexOf(k) === -1)
throw new errors.UsageError(format(
'invalid filter name: "%s" (must be one of "%s")',
k, valid.join('", "')));
o[k] = v;
}
return o;
}
//---- exports
@ -199,6 +113,7 @@ module.exports = {
deepObjCopy: deepObjCopy,
zeroPad: zeroPad,
boolFromString: boolFromString,
tabulate: tabulate
jsonStream: jsonStream,
kvToObj: kvToObj
};
// vim: set softtabstop=4 shiftwidth=4:

68
lib/do_cloudapi.js Normal file
View File

@ -0,0 +1,68 @@
/*
* Copyright 2015 Joyent Inc.
*
* `triton cloudapi ...`
*/
var http = require('http');
function do_cloudapi (subcmd, opts, args, callback) {
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
} else if (args.length !== 2) {
callback(new Error('invalid arguments'));
return;
}
var reqopts = {
method: args[0].toLowerCase(),
path: args[1]
};
this.triton.cloudapi.request(reqopts, function (err, req, res, body) {
if (err) {
callback(err);
return;
}
if (opts.headers || reqopts.method === 'head') {
console.error('%s/%s %d %s',
req.connection.encrypted ? 'HTTPS' : 'HTTP',
res.httpVersion,
res.statusCode,
http.STATUS_CODES[res.statusCode]);
Object.keys(res.headers).forEach(function (key) {
console.error('%s: %s', key, res.headers[key]);
});
console.error();
}
if (reqopts.method !== 'head')
console.log(JSON.stringify(body, null, 4));
callback();
});
}
do_cloudapi.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['headers', 'i'],
type: 'bool',
help: 'Print response headers to stderr.'
}
];
do_cloudapi.help = (
'Raw cloudapi request.\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} <method> <endpoint>\n'
+ '\n'
+ '{{options}}'
);
module.exports = do_cloudapi;

View File

@ -7,6 +7,7 @@
var format = require('util').format;
var tabula = require('tabula');
var common = require('./common');
var errors = require('./errors');
@ -21,26 +22,15 @@ function do_images(subcmd, opts, args, callback) {
/* 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;
var listOpts;
try {
listOpts = common.kvToObj(args, validFilters);
} catch (e) {
callback(e);
return;
}
if (opts.all) {
listOpts.state = 'all';
@ -53,12 +43,9 @@ function do_images(subcmd, opts, args, callback) {
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]));
}
console.log(common.jsonStream(imgs));
} else {
// Add some convenience fields
// Added fields taken from imgapi-cli.git.

90
lib/do_packages.js Normal file
View File

@ -0,0 +1,90 @@
/*
* Copyright 2015 Joyent Inc.
*
* `triton packages ...`
*/
var tabula = require('tabula');
var common = require('./common');
function do_packages (subcmd, opts, args, callback) {
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
} else if (args.length > 1) {
callback(new Error('too many args: ' + args));
return;
}
var columns = opts.o.trim().split(',');
var sort = opts.s.trim().split(',');
var validFilters = [
'name', 'memory', 'disk', 'swap', 'lwps', 'version', 'vcpus', 'group'
];
var listOpts;
try {
listOpts = common.kvToObj(args, validFilters);
} catch (e) {
callback(e);
return;
}
this.triton.cloudapi.listPackages(listOpts, function (err, packages) {
if (opts.json) {
console.log(common.jsonStream(packages));
} else {
tabula(packages, {
skipHeader: opts.H,
columns: columns,
sort: sort,
validFields: 'name,memory,disk,swap,vcpus,lwps,default,id,version'.split(',')
});
}
callback();
});
}
do_packages.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['H'],
type: 'bool',
help: 'Omit table header row.'
},
{
names: ['o'],
type: 'string',
default: 'id,name,version,memory,disk',
help: 'Specify fields (columns) to output.',
helpArg: 'field1,...'
},
{
names: ['s'],
type: 'string',
default: 'name',
help: 'Sort on the given fields. Default is "name".',
helpArg: 'field1,...'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output.'
}
];
do_packages.help = (
'List packgaes.\n'
+ '\n'
+ 'Usage:\n'
+ ' {{name}} packages\n'
+ '\n'
+ '{{options}}'
);
module.exports = do_packages;