diff --git a/lib/cli.js b/lib/cli.js index 3838516..2c7d2c7 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -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'); diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 009100e..832b47a 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -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); + }); +}; /** diff --git a/lib/common.js b/lib/common.js index 24e47fb..127e0fb 100755 --- a/lib/common.js +++ b/lib/common.js @@ -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: diff --git a/lib/do_cloudapi.js b/lib/do_cloudapi.js new file mode 100644 index 0000000..447c0bf --- /dev/null +++ b/lib/do_cloudapi.js @@ -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}} \n' + + '\n' + + '{{options}}' +); + + +module.exports = do_cloudapi; diff --git a/lib/do_images.js b/lib/do_images.js index cf0b8d6..2c37892 100644 --- a/lib/do_images.js +++ b/lib/do_images.js @@ -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. diff --git a/lib/do_packages.js b/lib/do_packages.js new file mode 100644 index 0000000..cf3f55a --- /dev/null +++ b/lib/do_packages.js @@ -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;