From ad7d6080113daaca5f40ece575535e41d2343d4e Mon Sep 17 00:00:00 2001 From: Chris Burroughs Date: Tue, 13 Dec 2016 13:04:41 -0500 Subject: [PATCH] joyent/node-triton#108 support passphrase protected keys Reviewed by: Trent Mick Approved by: Trent Mick --- CHANGES.md | 77 ++++++++ README.md | 55 +++--- examples/example-get-account.js | 63 +++--- examples/example-list-instances.js | 62 +++--- lib/cli.js | 65 ++++--- lib/cloudapi2.js | 92 ++++++--- lib/common.js | 96 +++++++++ lib/do_account/do_get.js | 46 +++-- lib/do_account/do_update.js | 4 +- lib/do_datacenters.js | 58 +++--- lib/do_fwrule/do_create.js | 22 ++- lib/do_fwrule/do_delete.js | 9 +- lib/do_fwrule/do_disable.js | 34 ++-- lib/do_fwrule/do_enable.js | 34 ++-- lib/do_fwrule/do_get.js | 27 +-- lib/do_fwrule/do_instances.js | 138 ++++++------- lib/do_fwrule/do_list.js | 61 +++--- lib/do_fwrule/do_update.js | 4 +- lib/do_image/do_create.js | 42 ++-- lib/do_image/do_delete.js | 3 +- lib/do_image/do_get.js | 25 ++- lib/do_image/do_list.js | 72 +++---- lib/do_image/do_wait.js | 4 +- lib/do_info.js | 124 ++++++------ lib/do_instance/do_audit.js | 29 +-- lib/do_instance/do_create.js | 20 +- lib/do_instance/do_disable_firewall.js | 48 +++-- lib/do_instance/do_enable_firewall.js | 48 +++-- lib/do_instance/do_fwrules.js | 63 +++--- lib/do_instance/do_get.js | 22 ++- lib/do_instance/do_ip.js | 28 +-- lib/do_instance/do_list.js | 153 ++++++++------- lib/do_instance/do_snapshot/do_create.js | 3 +- lib/do_instance/do_snapshot/do_delete.js | 3 +- lib/do_instance/do_snapshot/do_get.js | 31 +-- lib/do_instance/do_snapshot/do_list.js | 55 +++--- lib/do_instance/do_ssh.js | 85 ++++---- lib/do_instance/do_tag/do_delete.js | 74 +++---- lib/do_instance/do_tag/do_get.js | 32 +-- lib/do_instance/do_tag/do_list.js | 27 ++- lib/do_instance/do_tag/do_replace_all.js | 4 +- lib/do_instance/do_tag/do_set.js | 4 +- lib/do_instance/do_wait.js | 4 +- lib/do_instance/gen_do_ACTION.js | 14 +- lib/do_key/do_add.js | 3 +- lib/do_key/do_delete.js | 3 +- lib/do_key/do_get.js | 33 ++-- lib/do_key/do_list.js | 59 +++--- lib/do_network/do_get.js | 25 ++- lib/do_network/do_list.js | 44 +++-- lib/do_package/do_get.js | 27 ++- lib/do_package/do_list.js | 141 ++++++++------ lib/do_profile/do_create.js | 111 ++++++----- lib/do_profile/profilecommon.js | 145 ++++++-------- lib/do_services.js | 60 +++--- lib/index.js | 236 ++++++++++++++++------- lib/tritonapi.js | 176 +++++++++++++---- package.json | 6 +- test/integration/api-images.test.js | 10 +- test/integration/api-instances.test.js | 18 +- test/integration/api-networks.test.js | 16 +- test/integration/api-packages.test.js | 11 +- test/integration/helpers.js | 8 +- 63 files changed, 1872 insertions(+), 1224 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a67065a..3aa6258 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,83 @@ Known issues: ## not yet released +- **BREAKING CHANGE for module usage of node-triton.** + To implement joyent/node-triton#108, the way a TritonApi client is + setup for use has changed from being (unrealistically) sync to async. + + Client preparation is now a multi-step process: + + 1. create the client object; + 2. initialize it (mainly involves finding the SSH key identified by the + `keyId`); and, + 3. optionally unlock the SSH key (if it is passphrase-protected and not in + an ssh-agent). + + `createClient` has changed to take a callback argument. It will create and + init the client (steps 1 and 2) and takes an optional `unlockKeyFn` parameter + to handle step 3. A new `mod_triton.promptPassphraseUnlockKey` export can be + used for `unlockKeyFn` for command-line tools to handle prompting for a + passphrase on stdin, if required. Therefore what used to be: + + var mod_triton = require('triton'); + try { + var client = mod_triton.createClient({ # No longer works. + profileName: 'env' + }); + } catch (initErr) { + // handle err + } + + // use `client` + + is now: + + var mod_triton = require('triton'); + mod_triton.createClient({ + profileName: 'env', + unlockKeyFn: triton.promptPassphraseUnlockKey + }, function (err, client) { + if (err) { + // handle err + } + + // use `client` + }); + + See [the examples/ directory](examples/) for more complete examples. + + Low-level/raw handling of the three steps above is possible as follows + (error handling is elided): + + var mod_bunyan = require('bunyan'); + var mod_triton = require('triton'); + + // 1. create + var client = mod_triton.createTritonApiClient({ + log: mod_bunyan.createLogger({name: 'my-tool'}), + config: {}, + profile: mod_triton.loadProfile('env') + }); + + // 2. init + client.init(function (initErr) { + // 3. unlock key + // See top-comment in "lib/tritonapi.js". + }); + +- [joyent/node-triton#108] Support for passphrase-protected private keys. + Before this work, an encrypted private SSH key (i.e. protected by a + passphrase) would have to be loaded in an ssh-agent for the `triton` + CLI to use it. Now `triton` will prompt for the passphrase to unlock + the private key (in memory), if needed. For example: + + $ triton package list + Enter passphrase for id_rsa: + SHORTID NAME MEMORY SWAP DISK VCPUS + 14ad9d54 g4-highcpu-128M 128M 512M 3G - + 14ae2634 g4-highcpu-256M 256M 1G 5G - + ... + - [joyent/node-triton#143] Fix duplicate output from 'triton rbac key ...'. ## 4.15.0 diff --git a/README.md b/README.md index e328875..0ef9051 100644 --- a/README.md +++ b/README.md @@ -234,19 +234,27 @@ documentation](https://apidocs.joyent.com/docker) for more information.) ## `TritonApi` Module Usage Node-triton can also be used as a node module for your own node.js tooling. -A basic example: +A basic example appropriate for a command-line tool is: - var triton = require('triton'); +```javascript +var mod_bunyan = require('bunyan'); +var mod_triton = require('triton'); + +var log = mod_bunyan.createLogger({name: 'my-tool'}); + +// See the `createClient` block comment for full usage details: +// https://github.com/joyent/node-triton/blob/master/lib/index.js +mod_triton.createClient({ + log: log, + // Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a + // `profile` object. + profileName: 'env', + unlockKeyFn: mod_triton.promptPassphraseUnlockKey +}, function (err, client) { + if (err) { + // handle err + } - // See `createClient` block comment for full usage details: - // https://github.com/joyent/node-triton/blob/master/lib/index.js - var client = triton.createClient({ - profile: { - url: URL, - account: ACCOUNT, - keyId: KEY_ID - } - }); client.listImages(function (err, images) { client.close(); // Remember to close the client to close TCP conn. if (err) { @@ -255,7 +263,14 @@ A basic example: console.log(JSON.stringify(images, null, 4)); } }); +}); +``` +See the following for more details: +- The block-comment for `createClient` in [lib/index.js](lib/index.js). +- Some module-usage examples in [examples/](examples/). +- The lower-level details in the top-comment in + [lib/tritonapi.js](lib/tritonapi.js). ## Configuration @@ -280,24 +295,6 @@ are in "etc/defaults.json" and can be overriden for the CLI in catching up and is much more friendly to use. -## cloudapi2.js differences with node-smartdc/lib/cloudapi.js - -The old node-smartdc module included an lib for talking directly to the SDC -Cloud API (node-smartdc/lib/cloudapi.js). Part of this module (node-triton) is a -re-write of the Cloud API lib with some backward incompatibilities. The -differences and backward incompatibilities are discussed here. - -- Currently no caching options in cloudapi2.js (this should be re-added in - some form). The `noCache` option to many of the cloudapi.js methods will not - be re-added, it was a wart. -- The leading `account` option to each cloudapi.js method has been dropped. It - was redundant for the constructor `account` option. -- "account" is now "user" in the CloudAPI constructor. -- All (all? at least at the time of this writing) methods in cloudapi2.js have - a signature of `function (options, callback)` instead of the sometimes - haphazard extra arguments. - - ## Development Hooks Before commiting be sure to, at least: diff --git a/examples/example-get-account.js b/examples/example-get-account.js index 8bcb3cf..2499e9a 100755 --- a/examples/example-get-account.js +++ b/examples/example-get-account.js @@ -1,42 +1,45 @@ #!/usr/bin/env node /** - * Example using cloudapi2.js to call cloudapi's GetAccount endpoint. + * Example creating a Triton API client and using it to get account info. * * Usage: - * ./example-get-account.js | bunyan + * ./example-get-account.js + * + * # With trace-level logging + * LOG_LEVEL=trace ./example-get-account.js 2>&1 | bunyan */ -var p = console.log; -var auth = require('smartdc-auth'); var bunyan = require('bunyan'); -var cloudapi = require('../lib/cloudapi2'); +var path = require('path'); +var triton = require('../'); // typically `require('triton');` var log = bunyan.createLogger({ - name: 'example-get-account', - level: 'trace' -}) - -var ACCOUNT = process.env.SDC_ACCOUNT || 'bob'; -var USER = process.env.SDC_USER; -var KEY_ID = process.env.SDC_KEY_ID || 'b4:f0:b4:6c:18:3b:44:63:b4:4e:58:22:74:43:d4:bc'; - -var sign = auth.cliSigner({ - keyId: KEY_ID, - user: ACCOUNT, - log: log -}); -var client = cloudapi.createClient({ - url: 'https://us-sw-1.api.joyent.com', - account: ACCOUNT, - user: USER, - version: '*', - sign: sign, - agent: false, // don't want KeepAlive - log: log + name: path.basename(__filename), + level: process.env.LOG_LEVEL || 'info', + stream: process.stderr }); -log.info('start') -client.getAccount(function (err, account) { - p('getAccount: err', err) - p('getAccount: account', account) +triton.createClient({ + log: log, + // Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a + // `profile` object. + profileName: 'env', + unlockKeyFn: triton.promptPassphraseUnlockKey +}, function createdClient(err, client) { + if (err) { + console.error('error creating Triton client: %s\n%s', err, err.stack); + process.exitStatus = 1; + return; + } + + // TODO: Eventually the top-level TritonApi will have `.getAccount()`. + client.cloudapi.getAccount(function (err, account) { + client.close(); // Remember to close the client to close TCP conn. + if (err) { + console.error('getAccount error: %s\n%s', err, err.stack); + process.exitStatus = 1; + } else { + console.log(JSON.stringify(account, null, 4)); + } + }); }); diff --git a/examples/example-list-instances.js b/examples/example-list-instances.js index 1f8c272..deb1c61 100755 --- a/examples/example-list-instances.js +++ b/examples/example-list-instances.js @@ -1,46 +1,46 @@ #!/usr/bin/env node /** - * Example using cloudapi2.js to call cloudapi's ListMachines endpoint. + * Example creating a Triton API client and using it to list instances. * * Usage: - * ./example-list-images.js | bunyan + * ./example-list-instances.js + * + * # With trace-level logging + * LOG_LEVEL=trace ./example-list-instances.js 2>&1 | bunyan */ -var p = console.log; var bunyan = require('bunyan'); +var path = require('path'); var triton = require('../'); // typically `require('triton');` - -var URL = process.env.SDC_URL || 'https://us-sw-1.api.joyent.com'; -var ACCOUNT = process.env.SDC_ACCOUNT || 'bob'; -var KEY_ID = process.env.SDC_KEY_ID || 'b4:f0:b4:6c:18:3b:44:63:b4:4e:58:22:74:43:d4:bc'; - - var log = bunyan.createLogger({ - name: 'test-list-instances', - level: process.env.LOG_LEVEL || 'trace' + name: path.basename(__filename), + level: process.env.LOG_LEVEL || 'info', + stream: process.stderr }); -/* - * More details on `createClient` options here: - * https://github.com/joyent/node-triton/blob/master/lib/index.js#L18-L61 - * For example, if you want to use an existing `triton` CLI profile, you can - * pass that profile name in. - */ -var client = triton.createClient({ +triton.createClient({ log: log, - profile: { - url: URL, - account: ACCOUNT, - keyId: KEY_ID - } -}); -// TODO: Eventually the top-level TritonApi will have `.listInstances()` to use. -client.cloudapi.listMachines(function (err, insts) { - client.close(); // Remember to close the client to close TCP conn. + // Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a + // `profile` object. + profileName: 'env', + unlockKeyFn: triton.promptPassphraseUnlockKey +}, function createdClient(err, client) { if (err) { - console.error('listInstances err:', err); - } else { - console.log(JSON.stringify(insts, null, 4)); + console.error('error creating Triton client: %s\n%s', err, err.stack); + process.exitStatus = 1; + return; } -}); \ No newline at end of file + + // TODO: Eventually the top-level TritonApi will have `.listInstances()`. + client.cloudapi.listMachines(function (err, insts) { + client.close(); // Remember to close the client to close TCP conn. + + if (err) { + console.error('listInstances error: %s\n%s', err, err.stack); + process.exitStatus = 1; + } else { + console.log(JSON.stringify(insts, null, 4)); + } + }); +}); diff --git a/lib/cli.js b/lib/cli.js index 2dcf50a..4df29d9 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -27,7 +27,7 @@ var vasync = require('vasync'); var common = require('./common'); var mod_config = require('./config'); var errors = require('./errors'); -var tritonapi = require('./tritonapi'); +var lib_tritonapi = require('./tritonapi'); @@ -158,7 +158,7 @@ var OPTIONS = [ help: 'A cloudapi API version, or semver range, to attempt to use. ' + 'This is passed in the "Accept-Version" header. ' + 'See `triton cloudapi /--ping` to list supported versions. ' + - 'The default is "' + tritonapi.CLOUDAPI_ACCEPT_VERSION + '". ' + + 'The default is "' + lib_tritonapi.CLOUDAPI_ACCEPT_VERSION + '". ' + '*This is intended for development use only. It could cause ' + '`triton` processing of responses to break.*', hidden: true @@ -302,16 +302,16 @@ CLI.prototype.init = function (opts, args, callback) { return self._profile; }); - this.__defineGetter__('tritonapi', function getTritonapi() { - if (self._tritonapi === undefined) { - self._tritonapi = tritonapi.createClient({ - log: self.log, - profile: self.profile, - config: self.config - }); - } - return self._tritonapi; - }); + try { + self.tritonapi = lib_tritonapi.createClient({ + log: self.log, + profile: self.profile, + config: self.config + }); + } catch (createErr) { + callback(createErr); + return; + } if (process.env.TRITON_COMPLETE) { /* @@ -326,21 +326,21 @@ CLI.prototype.init = function (opts, args, callback) { * Example usage: * TRITON_COMPLETE=images triton -p my-profile create */ - this._emitCompletions(process.env.TRITON_COMPLETE, function (err) { + self._emitCompletions(process.env.TRITON_COMPLETE, function (err) { callback(err || false); }); } else { // Cmdln class handles `opts.help`. - Cmdln.prototype.init.apply(this, arguments); + Cmdln.prototype.init.call(self, opts, args, callback); } }; CLI.prototype.fini = function fini(subcmd, err, cb) { this.log.trace({err: err, subcmd: subcmd}, 'cli fini'); - if (this._tritonapi) { - this._tritonapi.close(); - delete this._tritonapi; + if (this.tritonapi) { + this.tritonapi.close(); + delete this.tritonapi; } cb(); }; @@ -361,7 +361,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) { var cacheFile = path.join(this.tritonapi.cacheDir, type + '.completions'); var ttl = 5 * 60 * 1000; // timeout of cache file info (ms) - var cloudapi = this.tritonapi.cloudapi; + var tritonapi = this.tritonapi; vasync.pipeline({arg: {}, funcs: [ function tryCacheFile(arg, next) { @@ -377,13 +377,25 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) { } }); }, + function initAuth(args, next) { + tritonapi.init(function (initErr) { + if (initErr) { + next(initErr); + } + if (tritonapi.keyPair.isLocked()) { + next(new errors.TritonError( + 'cannot unlock keys during completion')); + } + next(); + }); + }, function gather(arg, next) { var completions; switch (type) { case 'packages': - cloudapi.listPackages({}, function (err, pkgs) { + tritonapi.cloudapi.listPackages({}, function (err, pkgs) { if (err) { next(err); return; @@ -402,7 +414,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) { }); break; case 'images': - cloudapi.listImages({}, function (err, imgs) { + tritonapi.cloudapi.listImages({}, function (err, imgs) { if (err) { next(err); return; @@ -424,7 +436,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) { }); break; case 'instances': - cloudapi.listMachines({}, function (err, insts) { + tritonapi.cloudapi.listMachines({}, function (err, insts) { if (err) { next(err); return; @@ -449,7 +461,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) { * on that is that with the additional prefixes, there would * be too many. */ - cloudapi.listMachines({}, function (err, insts) { + tritonapi.cloudapi.listMachines({}, function (err, insts) { if (err) { next(err); return; @@ -470,7 +482,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) { }); break; case 'networks': - cloudapi.listNetworks({}, function (err, nets) { + tritonapi.cloudapi.listNetworks({}, function (err, nets) { if (err) { next(err); return; @@ -489,7 +501,8 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) { }); break; case 'fwrules': - cloudapi.listFirewallRules({}, function (err, fwrules) { + tritonapi.cloudapi.listFirewallRules({}, function (err, + fwrules) { if (err) { next(err); return; @@ -503,7 +516,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) { }); break; case 'keys': - cloudapi.listKeys({}, function (err, keys) { + tritonapi.cloudapi.listKeys({}, function (err, keys) { if (err) { next(err); return; @@ -602,7 +615,7 @@ CLI.prototype.tritonapiFromProfileName = 'tritonapiFromProfileName: loaded profile'); } - return tritonapi.createClient({ + return lib_tritonapi.createClient({ log: this.log, profile: profile, config: this.config diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index eab9822..a347c07 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -41,6 +41,7 @@ var os = require('os'); var querystring = require('querystring'); var vasync = require('vasync'); var auth = require('smartdc-auth'); +var EventEmitter = require('events').EventEmitter; var bunyannoop = require('./bunyannoop'); var common = require('./common'); @@ -64,10 +65,7 @@ var OS_PLATFORM = os.platform(); * * @param options {Object} * - {String} url (required) Cloud API base url - * - {String} account (required) The account login name. - * - {Function} sign (required) An http-signature auth signing function - * - {String} user (optional) The RBAC user login name. - * - {Array of String} roles (optional) RBAC role(s) to take up. + * - Authentication options (see below) * - {String} version (optional) Used for the accept-version header. This * defaults to '*', meaning that over time you could experience breaking * changes. Specifying a value is strongly recommended. E.g. '~7.1'. @@ -78,6 +76,28 @@ var OS_PLATFORM = os.platform(); * {Boolean} agent Set to `false` to not get KeepAlive. You want * this for CLIs. * TODO doc the backoff/retry available options + * + * Authentication options can be given in two ways - either with a + * smartdc-auth KeyPair (the preferred method), or with a signer function + * (deprecated, retained for compatibility). + * + * Either (prefered): + * - {String} account (required) The account login name this cloudapi + * client will operate upon. + * - {Object} principal (required) + * - {String} account (required) The account login name for + * authentication. + * - {Object} keyPair (required) A smartdc-auth KeyPair object + * - {String} user (optional) RBAC sub-user login name + * - {Array of String} roles (optional) RBAC role(s) to take up. + * + * Or (backwards compatible): + * - {String} account (required) The account login name used both for + * authentication and as the account being operated upon. + * - {Function} sign (required) An http-signature auth signing function. + * - {String} user (optional) The RBAC user login name. + * - {Array of String} roles (optional) RBAC role(s) to take up. + * * @throws {TypeError} on bad input. * @constructor * @@ -90,17 +110,30 @@ function CloudApi(options) { assert.object(options, 'options'); assert.string(options.url, 'options.url'); assert.string(options.account, 'options.account'); - assert.func(options.sign, 'options.sign'); - assert.optionalString(options.user, 'options.user'); + assert.optionalArrayOfString(options.roles, 'options.roles'); assert.optionalString(options.version, 'options.version'); assert.optionalObject(options.log, 'options.log'); + assert.optionalObject(options.principal, 'options.principal'); + this.principal = options.principal; + if (options.principal === undefined) { + this.principal = {}; + this.principal.account = options.account; + assert.optionalString(options.user, 'options.user'); + if (options.user !== undefined) + this.principal.user = options.user; + assert.func(options.sign, 'options.sign'); + this.principal.sign = options.sign; + } else { + assert.string(this.principal.account, 'principal.account'); + assert.object(this.principal.keyPair, 'principal.keyPair'); + assert.optionalString(this.principal.user, 'principal.user'); + } + this.url = options.url; this.account = options.account; - this.user = options.user; // optional RBAC subuser this.roles = options.roles; - this.sign = options.sign; this.log = options.log || new bunyannoop.BunyanNoopLogger(); if (!options.version) { options.version = '*'; @@ -128,16 +161,33 @@ CloudApi.prototype.close = function close(callback) { this.client.close(); }; +CloudApi.prototype._getAuthHeaders = + function _getAuthHeaders(method, path, callback) { -CloudApi.prototype._getAuthHeaders = function _getAuthHeaders(callback) { + assert.string(method, 'method'); + assert.string(path, 'path'); assert.func(callback, 'callback'); - var self = this; var headers = {}; - var rs = auth.requestSigner({ - sign: self.sign - }); + var rs; + if (this.principal.sign !== undefined) { + rs = auth.requestSigner({ + sign: this.principal.sign + }); + } else if (this.principal.keyPair !== undefined) { + try { + rs = this.principal.keyPair.createRequestSigner({ + user: this.principal.account, + subuser: this.principal.user + }); + } catch (signerErr) { + callback(new errors.SigningError(signerErr)); + return; + } + } + + rs.writeTarget(method, path); headers.date = rs.writeDateHeader(); // TODO: token auth support @@ -222,14 +272,8 @@ CloudApi.prototype._request = function _request(opts, cb) { var method = (opts.method || 'GET').toLowerCase(); assert.ok(['get', 'post', 'put', 'delete', 'head'].indexOf(method) >= 0, - 'invalid method given'); - switch (method) { - case 'delete': - method = 'del'; - break; - default: - break; - } + 'invalid HTTP method given'); + var clientFnName = (method === 'delete' ? 'del' : method); if (self.roles && self.roles.length > 0) { if (opts.path.indexOf('?') !== -1) { @@ -239,7 +283,7 @@ CloudApi.prototype._request = function _request(opts, cb) { } } - self._getAuthHeaders(function (err, headers) { + self._getAuthHeaders(method, opts.path, function (err, headers) { if (err) { cb(err); return; @@ -252,9 +296,9 @@ CloudApi.prototype._request = function _request(opts, cb) { headers: headers }; if (opts.data) - self.client[method](reqOpts, opts.data, cb); + self.client[clientFnName](reqOpts, opts.data, cb); else - self.client[method](reqOpts, cb); + self.client[clientFnName](reqOpts, cb); }); }; diff --git a/lib/common.js b/lib/common.js index 8f0d84a..93f3b68 100644 --- a/lib/common.js +++ b/lib/common.js @@ -12,6 +12,7 @@ var assert = require('assert-plus'); var child_process = require('child_process'); var crypto = require('crypto'); var fs = require('fs'); +var getpass = require('getpass'); var os = require('os'); var path = require('path'); var read = require('read'); @@ -678,6 +679,99 @@ function promptField(field, cb) { } +/** + * A utility method to unlock a private key on a TritonApi client instance, + * if necessary. + * + * If the client's key is locked, this will prompt for the passphrase on the + * TTY (via the `getpass` module) and attempt to unlock. + * + * @param opts {Object} + * - opts.tritonapi {Object} An `.init()`ialized TritonApi instance. + * @param cb {Function} `function (err)` + */ +function promptPassphraseUnlockKey(opts, cb) { + assert.object(opts.tritonapi, 'opts.tritonapi'); + + var kp = opts.tritonapi.keyPair; + if (!kp) { + cb(new errors.InternalError('TritonApi instance given to ' + + 'promptPassphraseUnlockKey is not initialized')); + return; + } + + if (!kp.isLocked()) { + cb(); + return; + } + + var keyDesc; + if (kp.source !== undefined) { + keyDesc = kp.source; + } else if (kp.comment !== undefined && kp.comment.length > 1) { + keyDesc = kp.getPublicKey().type.toUpperCase() + + ' key for ' + kp.comment; + } else { + keyDesc = kp.getPublicKey().type.toUpperCase() + + ' key ' + kp.getKeyId(); + } + var getpassOpts = { + prompt: 'Enter passphrase for ' + keyDesc + }; + + var tryPass = function (err, pass) { + if (err) { + cb(err); + return; + } + + try { + kp.unlock(pass); + } catch (unlockErr) { + getpassOpts.prompt = 'Bad passphrase, try again for ' + keyDesc; + getpass.getPass(getpassOpts, tryPass); + return; + } + + cb(null); + }; + + getpass.getPass(getpassOpts, tryPass); +} + + +/* + * A utility for the `triton` CLI subcommands to `init()`ialize a + * `tritonapi` instance and ensure that the profile's key is unlocked + * (prompting on a TTY if necessary). This is typically the CLI's + * `tritonapi` instance, but a `tritonapi` can also be passed in + * directly. + * + * @param opts.cli {Object} + * @param opts.tritonapi {Object} + * @param cb {Function} `function (err)` + */ +function cliSetupTritonApi(opts, cb) { + assert.optionalObject(opts.cli, 'opts.cli'); + assert.optionalObject(opts.tritonapi, 'opts.tritonapi'); + var tritonapi = opts.tritonapi || opts.cli.tritonapi; + assert.object(tritonapi, 'tritonapi'); + + tritonapi.init(function (initErr) { + if (initErr) { + cb(initErr); + return; + } + + promptPassphraseUnlockKey({ + tritonapi: tritonapi + }, function (keyErr) { + cb(keyErr); + }); + }); +} + + /** * Edit the given text in $EDITOR (defaulting to `vi`) and return the edited * text. @@ -984,6 +1078,8 @@ module.exports = { promptYesNo: promptYesNo, promptEnter: promptEnter, promptField: promptField, + promptPassphraseUnlockKey: promptPassphraseUnlockKey, + cliSetupTritonApi: cliSetupTritonApi, editInEditor: editInEditor, ansiStylize: ansiStylize, indent: indent, diff --git a/lib/do_account/do_get.js b/lib/do_account/do_get.js index 5901db1..d2619e7 100644 --- a/lib/do_account/do_get.js +++ b/lib/do_account/do_get.js @@ -21,28 +21,34 @@ function do_get(subcmd, opts, args, callback) { return; } - this.top.tritonapi.cloudapi.getAccount(function (err, account) { - if (err) { - callback(err); - return; + var tritonapi = this.top.tritonapi; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); } + tritonapi.cloudapi.getAccount(function (err, account) { + if (err) { + callback(err); + return; + } - if (opts.json) { - console.log(JSON.stringify(account)); - } else { - // pretty print - var dates = ['updated', 'created']; - Object.keys(account).forEach(function (key) { - var val = account[key]; - if (dates.indexOf(key) >= 0) { - console.log('%s: %s (%s)', key, val, - common.longAgo(new Date(val))); - } else { - console.log('%s: %s', key, val); - } - }); - } - callback(); + if (opts.json) { + console.log(JSON.stringify(account)); + } else { + // pretty print + var dates = ['updated', 'created']; + Object.keys(account).forEach(function (key) { + var val = account[key]; + if (dates.indexOf(key) >= 0) { + console.log('%s: %s (%s)', key, val, + common.longAgo(new Date(val))); + } else { + console.log('%s: %s', key, val); + } + }); + } + callback(); + }); }); } diff --git a/lib/do_account/do_update.js b/lib/do_account/do_update.js index fa0f62f..876debc 100644 --- a/lib/do_account/do_update.js +++ b/lib/do_account/do_update.js @@ -30,7 +30,9 @@ function do_update(subcmd, opts, args, callback) { var log = this.log; var tritonapi = this.top.tritonapi; - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, + function gatherDataArgs(ctx, next) { if (opts.file) { next(); diff --git a/lib/do_datacenters.js b/lib/do_datacenters.js index afb45b2..e6935fe 100644 --- a/lib/do_datacenters.js +++ b/lib/do_datacenters.js @@ -31,36 +31,42 @@ function do_datacenters(subcmd, opts, args, callback) { var columns = opts.o.split(','); var sort = opts.s.split(','); + var tritonapi = this.tritonapi; - this.tritonapi.cloudapi.listDatacenters(function (err, datacenters) { - if (err) { - callback(err); - return; + common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); } + tritonapi.cloudapi.listDatacenters(function (err, datacenters) { + if (err) { + callback(err); + return; + } - if (opts.json) { - console.log(JSON.stringify(datacenters)); - } else { - /* - * datacenters are returned in the form of: - * {name: 'url', name2: 'url2', ...} - * we "normalize" them for use by tabula by making them an array - */ - var dcs = []; - Object.keys(datacenters).forEach(function (key) { - dcs.push({ - name: key, - url: datacenters[key] + if (opts.json) { + console.log(JSON.stringify(datacenters)); + } else { + /* + * datacenters are returned in the form of: + * {name: 'url', name2: 'url2', ...} + * we "normalize" them for use by tabula by making them an array + */ + var dcs = []; + Object.keys(datacenters).forEach(function (key) { + dcs.push({ + name: key, + url: datacenters[key] + }); }); - }); - tabula(dcs, { - skipHeader: opts.H, - columns: columns, - sort: sort, - dottedLookup: true - }); - } - callback(); + tabula(dcs, { + skipHeader: opts.H, + columns: columns, + sort: sort, + dottedLookup: true + }); + } + callback(); + }); }); } diff --git a/lib/do_fwrule/do_create.js b/lib/do_fwrule/do_create.js index d1ec831..2de8b87 100644 --- a/lib/do_fwrule/do_create.js +++ b/lib/do_fwrule/do_create.js @@ -45,15 +45,21 @@ function do_create(subcmd, opts, args, cb) { createOpts.description = opts.description; } - this.top.tritonapi.cloudapi.createFirewallRule(createOpts, - function (err, fwrule) { - if (err) { - cb(err); - return; + var tritonapi = this.top.tritonapi; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - console.log('Created firewall rule %s%s', fwrule.id, - (!fwrule.enabled ? ' (disabled)' : '')); - cb(); + tritonapi.cloudapi.createFirewallRule( + createOpts, function (err, fwrule) { + if (err) { + cb(err); + return; + } + console.log('Created firewall rule %s%s', fwrule.id, + (!fwrule.enabled ? ' (disabled)' : '')); + cb(); + }); }); } diff --git a/lib/do_fwrule/do_delete.js b/lib/do_fwrule/do_delete.js index b4369d8..a7b0fdd 100644 --- a/lib/do_fwrule/do_delete.js +++ b/lib/do_fwrule/do_delete.js @@ -31,10 +31,11 @@ function do_delete(subcmd, opts, args, cb) { return; } - var cli = this.top; + var tritonapi = this.top.tritonapi; var ruleIds = args; - vasync.pipeline({funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function confirm(_, next) { if (opts.force) { return next(); @@ -61,8 +62,8 @@ function do_delete(subcmd, opts, args, cb) { vasync.forEachParallel({ inputs: ruleIds, func: function deleteOne(id, nextId) { - cli.tritonapi.deleteFirewallRule({ - id: id + tritonapi.deleteFirewallRule({ + id: id }, function (err) { if (err) { nextId(err); diff --git a/lib/do_fwrule/do_disable.js b/lib/do_fwrule/do_disable.js index 7ea887b..7c51b87 100644 --- a/lib/do_fwrule/do_disable.js +++ b/lib/do_fwrule/do_disable.js @@ -30,22 +30,26 @@ function do_disable(subcmd, opts, args, cb) { return; } - var cli = this.top; - - vasync.forEachParallel({ - inputs: args, - func: function disableOne(id, nextId) { - cli.tritonapi.disableFirewallRule({ id: id }, function (err) { - if (err) { - nextId(err); - return; - } - - console.log('Disabled firewall rule %s', id); - nextId(); - }); + var tritonapi = this.top.tritonapi; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - }, cb); + vasync.forEachParallel({ + inputs: args, + func: function disableOne(id, nextId) { + tritonapi.disableFirewallRule({ id: id }, function (err) { + if (err) { + nextId(err); + return; + } + + console.log('Disabled firewall rule %s', id); + nextId(); + }); + } + }, cb); + }); } diff --git a/lib/do_fwrule/do_enable.js b/lib/do_fwrule/do_enable.js index 4f22747..c5401e9 100644 --- a/lib/do_fwrule/do_enable.js +++ b/lib/do_fwrule/do_enable.js @@ -30,22 +30,26 @@ function do_enable(subcmd, opts, args, cb) { return; } - var cli = this.top; - - vasync.forEachParallel({ - inputs: args, - func: function enableOne(id, nextId) { - cli.tritonapi.enableFirewallRule({ id: id }, function (err) { - if (err) { - nextId(err); - return; - } - - console.log('Enabled firewall rule %s', id); - nextId(); - }); + var tritonapi = this.top.tritonapi; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - }, cb); + vasync.forEachParallel({ + inputs: args, + func: function enableOne(id, nextId) { + tritonapi.enableFirewallRule({ id: id }, function (err) { + if (err) { + nextId(err); + return; + } + + console.log('Enabled firewall rule %s', id); + nextId(); + }); + } + }, cb); + }); } diff --git a/lib/do_fwrule/do_get.js b/lib/do_fwrule/do_get.js index b919036..474c415 100644 --- a/lib/do_fwrule/do_get.js +++ b/lib/do_fwrule/do_get.js @@ -33,21 +33,26 @@ function do_get(subcmd, opts, args, cb) { } var id = args[0]; - var cli = this.top; + var tritonapi = this.top.tritonapi; - cli.tritonapi.getFirewallRule(id, function onRule(err, fwrule) { - if (err) { - cb(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } + tritonapi.getFirewallRule(id, function onRule(err, fwrule) { + if (err) { + cb(err); + return; + } - if (opts.json) { - console.log(JSON.stringify(fwrule)); - } else { - console.log(JSON.stringify(fwrule, null, 4)); - } + if (opts.json) { + console.log(JSON.stringify(fwrule)); + } else { + console.log(JSON.stringify(fwrule, null, 4)); + } - cb(); + cb(); + }); }); } diff --git a/lib/do_fwrule/do_instances.js b/lib/do_fwrule/do_instances.js index 65a3c65..e511bae 100644 --- a/lib/do_fwrule/do_instances.js +++ b/lib/do_fwrule/do_instances.js @@ -54,75 +54,81 @@ function do_instances(subcmd, opts, args, cb) { var tritonapi = this.top.tritonapi; - vasync.parallel({funcs: [ - function getTheImages(next) { - tritonapi.listImages({ - useCache: true, - state: 'all' - }, function (err, _imgs) { - if (err) { - next(err); - } else { - imgs = _imgs; - next(); - } - }); - }, - function getTheMachines(next) { - tritonapi.listFirewallRuleInstances({ - id: id - }, function (err, _insts) { - if (err) { - next(err); - } else { - insts = _insts; - next(); - } - }); - } - ]}, function (err, results) { - /* - * Error handling: vasync.parallel's `err` is always a MultiError. We - * want to prefer the `getTheMachines` err, e.g. if both get a - * self-signed cert error. - */ - if (err) { - err = results.operations[1].err || err; - return cb(err); + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } + vasync.parallel({funcs: [ + function getTheImages(next) { + tritonapi.listImages({ + useCache: true, + state: 'all' + }, function (err, _imgs) { + if (err) { + next(err); + } else { + imgs = _imgs; + next(); + } + }); + }, + function getTheMachines(next) { + tritonapi.listFirewallRuleInstances({ + id: id + }, function (err, _insts) { + if (err) { + next(err); + } else { + insts = _insts; + next(); + } + }); + } + ]}, function (err, results) { + /* + * Error handling: vasync.parallel's `err` is always a + * MultiError. We want to prefer the `getTheMachines` err, + * e.g. if both get a self-signed cert error. + */ + if (err) { + err = results.operations[1].err || err; + return cb(err); + } - // map "uuid" => "image_name" - var imgmap = {}; - imgs.forEach(function (img) { - imgmap[img.id] = format('%s@%s', img.name, img.version); + // map "uuid" => "image_name" + var imgmap = {}; + imgs.forEach(function (img) { + imgmap[img.id] = format('%s@%s', img.name, img.version); + }); + + // Add extra fields for nice output. + var now = new Date(); + insts.forEach(function (inst) { + var created = new Date(inst.created); + inst.age = common.longAgo(created, now); + inst.img = imgmap[inst.image] || + common.uuidToShortId(inst.image); + inst.shortid = inst.id.split('-', 1)[0]; + var flags = []; + if (inst.docker) flags.push('D'); + if (inst.firewall_enabled) flags.push('F'); + if (inst.brand === 'kvm') flags.push('K'); + inst.flags = flags.length ? flags.join('') : undefined; + }); + + if (opts.json) { + common.jsonStream(insts); + } else { + tabula(insts, { + skipHeader: opts.H, + columns: columns, + sort: sort, + dottedLookup: true + }); + } + + cb(); }); - - // Add extra fields for nice output. - var now = new Date(); - insts.forEach(function (inst) { - var created = new Date(inst.created); - inst.age = common.longAgo(created, now); - inst.img = imgmap[inst.image] || common.uuidToShortId(inst.image); - inst.shortid = inst.id.split('-', 1)[0]; - var flags = []; - if (inst.docker) flags.push('D'); - if (inst.firewall_enabled) flags.push('F'); - if (inst.brand === 'kvm') flags.push('K'); - inst.flags = flags.length ? flags.join('') : undefined; - }); - - if (opts.json) { - common.jsonStream(insts); - } else { - tabula(insts, { - skipHeader: opts.H, - columns: columns, - sort: sort, - dottedLookup: true - }); - } - - cb(); }); } diff --git a/lib/do_fwrule/do_list.js b/lib/do_fwrule/do_list.js index 108d4f0..bf6f7e2 100644 --- a/lib/do_fwrule/do_list.js +++ b/lib/do_fwrule/do_list.js @@ -35,40 +35,45 @@ function do_list(subcmd, opts, args, cb) { return; } - var cli = this.top; - cli.tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) { - if (err) { - cb(err); - return; + var tritonapi = this.top.tritonapi; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - - if (opts.json) { - common.jsonStream(rules); - } else { - var columns = COLUMNS_DEFAULT; - - if (opts.o) { - columns = opts.o; - } else if (opts.long) { - columns = COLUMNS_LONG; + tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) { + if (err) { + cb(err); + return; } - columns = columns.toLowerCase().split(','); - var sort = opts.s.toLowerCase().split(','); + if (opts.json) { + common.jsonStream(rules); + } else { + var columns = COLUMNS_DEFAULT; - if (columns.indexOf('shortid') !== -1) { - rules.forEach(function (rule) { - rule.shortid = common.uuidToShortId(rule.id); + if (opts.o) { + columns = opts.o; + } else if (opts.long) { + columns = COLUMNS_LONG; + } + + columns = columns.toLowerCase().split(','); + var sort = opts.s.toLowerCase().split(','); + + if (columns.indexOf('shortid') !== -1) { + rules.forEach(function (rule) { + rule.shortid = common.uuidToShortId(rule.id); + }); + } + + tabula(rules, { + skipHeader: opts.H, + columns: columns, + sort: sort }); - } - - tabula(rules, { - skipHeader: opts.H, - columns: columns, - sort: sort - }); } - cb(); + cb(); + }); }); } diff --git a/lib/do_fwrule/do_update.js b/lib/do_fwrule/do_update.js index 8238c61..9bad3e4 100644 --- a/lib/do_fwrule/do_update.js +++ b/lib/do_fwrule/do_update.js @@ -37,7 +37,9 @@ function do_update(subcmd, opts, args, cb) { var id = args.shift(); - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, + function gatherDataArgs(ctx, next) { if (opts.file) { next(); diff --git a/lib/do_image/do_create.js b/lib/do_image/do_create.js index c63634c..30aca85 100644 --- a/lib/do_image/do_create.js +++ b/lib/do_image/do_create.js @@ -26,7 +26,6 @@ var mat = require('../metadataandtags'); // ---- the command function do_create(subcmd, opts, args, cb) { - var self = this; if (opts.help) { this.do_help('help', {}, [subcmd], cb); return; @@ -37,9 +36,10 @@ function do_create(subcmd, opts, args, cb) { } var log = this.top.log; - var cloudapi = this.top.tritonapi.cloudapi; + var tritonapi = this.top.tritonapi; - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function loadTags(ctx, next) { mat.tagsFromCreateOpts(opts, log, function (err, tags) { if (err) { @@ -76,7 +76,7 @@ function do_create(subcmd, opts, args, cb) { return; } - self.top.tritonapi.getInstance(id, function (err, inst) { + tritonapi.getInstance(id, function (err, inst) { if (err) { next(err); return; @@ -113,20 +113,22 @@ function do_create(subcmd, opts, args, cb) { return; } - cloudapi.createImageFromMachine(createOpts, function (err, img) { - if (err) { - next(new errors.TritonError(err, 'error creating image')); - return; - } - ctx.img = img; - if (opts.json) { - console.log(JSON.stringify(img)); - } else { - console.log('Creating image %s@%s (%s)', - img.name, img.version, img.id); - } - next(); - }); + tritonapi.cloudapi.createImageFromMachine( + createOpts, function (err, img) { + if (err) { + next(new errors.TritonError(err, + 'error creating image')); + return; + } + ctx.img = img; + if (opts.json) { + console.log(JSON.stringify(img)); + } else { + console.log('Creating image %s@%s (%s)', + img.name, img.version, img.id); + } + next(); + }); }, function maybeWait(ctx, next) { if (!opts.wait) { @@ -147,8 +149,8 @@ function do_create(subcmd, opts, args, cb) { ctx.img.state = 'running'; waitCb(null, ctx.img); }, 5000); - } - : cloudapi.waitForImageStates.bind(cloudapi)); + } : tritonapi.cloudapi.waitForImageStates.bind( + tritonapi.cloudapi)); waiter({ id: ctx.img.id, diff --git a/lib/do_image/do_delete.js b/lib/do_image/do_delete.js index a11501b..484841e 100644 --- a/lib/do_image/do_delete.js +++ b/lib/do_image/do_delete.js @@ -26,7 +26,8 @@ function do_delete(subcmd, opts, args, cb) { } var ids = args; - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, /* * Lookup images, if not given UUIDs: we'll need to do it anyway * for the DeleteImage call(s), and doing so explicitly here allows diff --git a/lib/do_image/do_get.js b/lib/do_image/do_get.js index 4e22268..b09075f 100644 --- a/lib/do_image/do_get.js +++ b/lib/do_image/do_get.js @@ -12,6 +12,7 @@ var format = require('util').format; +var common = require('../common'); var errors = require('../errors'); @@ -24,17 +25,23 @@ function do_get(subcmd, opts, args, callback) { 'incorrect number of args (%d)', args.length))); } - this.top.tritonapi.getImage(args[0], function onRes(err, img) { - if (err) { - return callback(err); + var tritonapi = this.top.tritonapi; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); } + tritonapi.getImage(args[0], function onRes(err, img) { + if (err) { + return callback(err); + } - if (opts.json) { - console.log(JSON.stringify(img)); - } else { - console.log(JSON.stringify(img, null, 4)); - } - callback(); + if (opts.json) { + console.log(JSON.stringify(img)); + } else { + console.log(JSON.stringify(img, null, 4)); + } + callback(); + }); }); } diff --git a/lib/do_image/do_list.js b/lib/do_image/do_list.js index d9b917c..cc00b40 100644 --- a/lib/do_image/do_list.js +++ b/lib/do_image/do_list.js @@ -63,42 +63,48 @@ function do_list(subcmd, opts, args, callback) { listOpts.state = 'all'; } - this.top.tritonapi.listImages(listOpts, function onRes(err, imgs, res) { - if (err) { - return callback(err); + var tritonapi = this.top.tritonapi; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); } - - if (opts.json) { - common.jsonStream(imgs); - } 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; + tritonapi.listImages(listOpts, function onRes(err, imgs, res) { + if (err) { + return callback(err); } - tabula(imgs, { - skipHeader: opts.H, - columns: columns, - sort: sort - }); - } - callback(); + if (opts.json) { + common.jsonStream(imgs); + } 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(); + }); }); } diff --git a/lib/do_image/do_wait.js b/lib/do_image/do_wait.js index c887cec..1421ae8 100644 --- a/lib/do_image/do_wait.js +++ b/lib/do_image/do_wait.js @@ -12,6 +12,7 @@ var vasync = require('vasync'); +var common = require('../common'); var distractions = require('../distractions'); var errors = require('../errors'); @@ -34,7 +35,8 @@ function do_wait(subcmd, opts, args, cb) { var done = 0; var imgFromId = {}; - vasync.pipeline({funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function getImgs(_, next) { vasync.forEachParallel({ inputs: ids, diff --git a/lib/do_info.js b/lib/do_info.js index af0f94c..0864366 100644 --- a/lib/do_info.js +++ b/lib/do_info.js @@ -28,69 +28,75 @@ function do_info(subcmd, opts, args, callback) { var out = {}; var i = 0; + var tritonapi = this.tritonapi; - this.tritonapi.cloudapi.getAccount(cb.bind('account')); i++; - this.tritonapi.cloudapi.listMachines(cb.bind('machines')); i++; - - function cb(err, data) { - if (err) { - callback(err); - return; + common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); } - out[this.toString()] = data; - if (--i === 0) - done(); - } + tritonapi.cloudapi.getAccount(cb.bind('account')); i++; + tritonapi.cloudapi.listMachines(cb.bind('machines')); i++; - function done() { - // parse name - var name; - if (out.account.firstName && out.account.lastName) - name = format('%s %s', out.account.firstName, - out.account.lastName); - else if (out.account.firstName) - name = out.account.firstName; - - // parse machine states and accounting - var states = {}; - var disk = 0; - var memory = 0; - out.machines.forEach(function (machine) { - var state = machine.state; - states[state] = states[state] || 0; - states[state]++; - memory += machine.memory; - disk += machine.disk; - }); - disk *= 1000 * 1000; - memory *= 1000 * 1000; - - var data = {}; - data.login = out.account.login; - if (name) - data.name = name; - data.email = out.account.email; - data.url = self.tritonapi.cloudapi.url; - data.totalDisk = disk; - data.totalMemory = memory; - - if (opts.json) { - data.totalInstances = out.machines.length; - data.instances = states; - console.log(JSON.stringify(data)); - } else { - data.totalDisk = common.humanSizeFromBytes(disk); - data.totalMemory = common.humanSizeFromBytes(memory); - Object.keys(data).forEach(function (key) { - console.log('%s: %s', key, data[key]); - }); - console.log('instances: %d', out.machines.length); - Object.keys(states).forEach(function (key) { - console.log(' %s: %d', key, states[key]); - }); + function cb(err, data) { + if (err) { + callback(err); + return; + } + out[this.toString()] = data; + if (--i === 0) + done(); } - callback(); - } + + function done() { + // parse name + var name; + if (out.account.firstName && out.account.lastName) + name = format('%s %s', out.account.firstName, + out.account.lastName); + else if (out.account.firstName) + name = out.account.firstName; + + // parse machine states and accounting + var states = {}; + var disk = 0; + var memory = 0; + out.machines.forEach(function (machine) { + var state = machine.state; + states[state] = states[state] || 0; + states[state]++; + memory += machine.memory; + disk += machine.disk; + }); + disk *= 1000 * 1000; + memory *= 1000 * 1000; + + var data = {}; + data.login = out.account.login; + if (name) + data.name = name; + data.email = out.account.email; + data.url = self.tritonapi.cloudapi.url; + data.totalDisk = disk; + data.totalMemory = memory; + + if (opts.json) { + data.totalInstances = out.machines.length; + data.instances = states; + console.log(JSON.stringify(data)); + } else { + data.totalDisk = common.humanSizeFromBytes(disk); + data.totalMemory = common.humanSizeFromBytes(memory); + Object.keys(data).forEach(function (key) { + console.log('%s: %s', key, data[key]); + }); + console.log('instances: %d', out.machines.length); + Object.keys(states).forEach(function (key) { + console.log(' %s: %d', key, states[key]); + }); + } + callback(); + } + }); } do_info.options = [ diff --git a/lib/do_instance/do_audit.js b/lib/do_instance/do_audit.js index 82b09f8..ce4060f 100644 --- a/lib/do_instance/do_audit.js +++ b/lib/do_instance/do_audit.js @@ -27,7 +27,6 @@ var sortDefault = 'id,time'; function do_audit(subcmd, opts, args, cb) { - var self = this; if (opts.help) { this.do_help('help', {}, [subcmd], cb); return; @@ -51,23 +50,25 @@ function do_audit(subcmd, opts, args, cb) { var arg = args[0]; var uuid; + var tritonapi = this.top.tritonapi; - if (common.isUUID(arg)) { - uuid = arg; - go1(); - } else { - self.top.tritonapi.getInstance(arg, function (err, inst) { - if (err) { - cb(err); - return; - } - uuid = inst.id; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (common.isUUID(arg)) { + uuid = arg; go1(); - }); - } + } else { + tritonapi.getInstance(arg, function (err, inst) { + if (err) { + cb(err); + return; + } + uuid = inst.id; + go1(); + }); + }}); function go1() { - self.top.tritonapi.cloudapi.machineAudit(uuid, function (err, audit) { + tritonapi.cloudapi.machineAudit(uuid, function (err, audit) { if (err) { cb(err); return; diff --git a/lib/do_instance/do_create.js b/lib/do_instance/do_create.js index 0562dc1..cc0c636 100644 --- a/lib/do_instance/do_create.js +++ b/lib/do_instance/do_create.js @@ -22,7 +22,6 @@ var mat = require('../metadataandtags'); function do_create(subcmd, opts, args, cb) { - var self = this; if (opts.help) { this.do_help('help', {}, [subcmd], cb); return; @@ -31,9 +30,10 @@ function do_create(subcmd, opts, args, cb) { } var log = this.top.log; - var cloudapi = this.top.tritonapi.cloudapi; + var tritonapi = this.top.tritonapi; - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, /* BEGIN JSSTYLED */ /* * Parse --affinity options for validity to `ctx.affinities`. @@ -158,7 +158,7 @@ function do_create(subcmd, opts, args, cb) { nearFar.push(aff.val); nextAff(); } else { - self.top.tritonapi.getInstance({ + tritonapi.getInstance({ id: aff.val, fields: ['id'] }, function (err, inst) { @@ -222,7 +222,7 @@ function do_create(subcmd, opts, args, cb) { name: args[0], useCache: true }; - self.top.tritonapi.getImage(_opts, function (err, img) { + tritonapi.getImage(_opts, function (err, img) { if (err) { return next(err); } @@ -243,7 +243,7 @@ function do_create(subcmd, opts, args, cb) { return; } - self.top.tritonapi.getPackage(id, function (err, pkg) { + tritonapi.getPackage(id, function (err, pkg) { if (err) { return next(err); } @@ -261,7 +261,7 @@ function do_create(subcmd, opts, args, cb) { vasync.forEachPipeline({ inputs: opts.network, func: function getOneNetwork(name, nextNet) { - self.top.tritonapi.getNetwork(name, function (err, net) { + tritonapi.getNetwork(name, function (err, net) { if (err) { nextNet(err); } else { @@ -316,7 +316,7 @@ function do_create(subcmd, opts, args, cb) { return next(); } - cloudapi.createMachine(createOpts, function (err, inst) { + tritonapi.cloudapi.createMachine(createOpts, function (err, inst) { if (err) { next(new errors.TritonError(err, 'error creating instance')); @@ -352,8 +352,8 @@ function do_create(subcmd, opts, args, cb) { ctx.inst.state = 'running'; waitCb(null, ctx.inst); }, 5000); - } - : cloudapi.waitForMachineStates.bind(cloudapi)); + } : tritonapi.cloudapi.waitForMachineStates.bind( + tritonapi.cloudapi)); waiter({ id: ctx.inst.id, diff --git a/lib/do_instance/do_disable_firewall.js b/lib/do_instance/do_disable_firewall.js index 370c542..7f51b39 100644 --- a/lib/do_instance/do_disable_firewall.js +++ b/lib/do_instance/do_disable_firewall.js @@ -14,6 +14,7 @@ var assert = require('assert-plus'); var format = require('util').format; var vasync = require('vasync'); +var common = require('../common'); var errors = require('../errors'); @@ -50,28 +51,33 @@ function do_disable_firewall(subcmd, opts, args, cb) { }); } - vasync.forEachParallel({ - inputs: args, - func: function disableOne(name, nextInst) { - cli.tritonapi.disableInstanceFirewall({ - id: name - }, function (err, fauxInst) { - if (err) { - nextInst(err); - return; - } - - console.log('Disabling firewall for instance "%s"', name); - - if (opts.wait) { - wait(name, fauxInst.id, nextInst); - } else { - nextInst(); - } - }); + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - }, function (err) { - cb(err); + vasync.forEachParallel({ + inputs: args, + func: function disableOne(name, nextInst) { + cli.tritonapi.disableInstanceFirewall({ + id: name + }, function (err, fauxInst) { + if (err) { + nextInst(err); + return; + } + + console.log('Disabling firewall for instance "%s"', name); + + if (opts.wait) { + wait(name, fauxInst.id, nextInst); + } else { + nextInst(); + } + }); + } + }, function (err) { + cb(err); + }); }); } diff --git a/lib/do_instance/do_enable_firewall.js b/lib/do_instance/do_enable_firewall.js index ea15610..97f1dfb 100644 --- a/lib/do_instance/do_enable_firewall.js +++ b/lib/do_instance/do_enable_firewall.js @@ -14,6 +14,7 @@ var assert = require('assert-plus'); var format = require('util').format; var vasync = require('vasync'); +var common = require('../common'); var errors = require('../errors'); @@ -50,28 +51,33 @@ function do_enable_firewall(subcmd, opts, args, cb) { }); } - vasync.forEachParallel({ - inputs: args, - func: function enableOne(name, nextInst) { - cli.tritonapi.enableInstanceFirewall({ - id: name - }, function (err, fauxInst) { - if (err) { - nextInst(err); - return; - } - - console.log('Enabling firewall for instance "%s"', name); - - if (opts.wait) { - wait(name, fauxInst.id, nextInst); - } else { - nextInst(); - } - }); + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - }, function (err) { - cb(err); + vasync.forEachParallel({ + inputs: args, + func: function enableOne(name, nextInst) { + cli.tritonapi.enableInstanceFirewall({ + id: name + }, function (err, fauxInst) { + if (err) { + nextInst(err); + return; + } + + console.log('Enabling firewall for instance "%s"', name); + + if (opts.wait) { + wait(name, fauxInst.id, nextInst); + } else { + nextInst(); + } + }); + } + }, function (err) { + cb(err); + }); }); } diff --git a/lib/do_instance/do_fwrules.js b/lib/do_instance/do_fwrules.js index bf06fad..00eac1f 100644 --- a/lib/do_instance/do_fwrules.js +++ b/lib/do_instance/do_fwrules.js @@ -41,41 +41,46 @@ function do_fwrules(subcmd, opts, args, cb) { var id = args[0]; var cli = this.top; - cli.tritonapi.listInstanceFirewallRules({ - id: id - }, function onRules(err, rules) { - if (err) { - cb(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - - if (opts.json) { - common.jsonStream(rules); - } else { - var columns = COLUMNS_DEFAULT; - - if (opts.o) { - columns = opts.o; - } else if (opts.long) { - columns = COLUMNS_LONG; + cli.tritonapi.listInstanceFirewallRules({ + id: id + }, function onRules(err, rules) { + if (err) { + cb(err); + return; } - columns = columns.toLowerCase().split(','); - var sort = opts.s.toLowerCase().split(','); + if (opts.json) { + common.jsonStream(rules); + } else { + var columns = COLUMNS_DEFAULT; - if (columns.indexOf('shortid') !== -1) { - rules.forEach(function (rule) { - rule.shortid = common.normShortId(rule.id); + if (opts.o) { + columns = opts.o; + } else if (opts.long) { + columns = COLUMNS_LONG; + } + + columns = columns.toLowerCase().split(','); + var sort = opts.s.toLowerCase().split(','); + + if (columns.indexOf('shortid') !== -1) { + rules.forEach(function (rule) { + rule.shortid = common.normShortId(rule.id); + }); + } + + tabula(rules, { + skipHeader: opts.H, + columns: columns, + sort: sort }); } - - tabula(rules, { - skipHeader: opts.H, - columns: columns, - sort: sort - }); - } - cb(); + cb(); + }); }); } diff --git a/lib/do_instance/do_get.js b/lib/do_instance/do_get.js index 18afe2d..3663a22 100644 --- a/lib/do_instance/do_get.js +++ b/lib/do_instance/do_get.js @@ -19,15 +19,21 @@ function do_get(subcmd, opts, args, cb) { return cb(new Error('invalid args: ' + args)); } - this.top.tritonapi.getInstance(args[0], function (err, inst) { - if (inst) { - if (opts.json) { - console.log(JSON.stringify(inst)); - } else { - console.log(JSON.stringify(inst, null, 4)); - } + var tritonapi = this.top.tritonapi; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - cb(err); + tritonapi.getInstance(args[0], function (err, inst) { + if (inst) { + if (opts.json) { + console.log(JSON.stringify(inst)); + } else { + console.log(JSON.stringify(inst, null, 4)); + } + } + cb(err); + }); }); } diff --git a/lib/do_instance/do_ip.js b/lib/do_instance/do_ip.js index 9dd5af7..6bfdea7 100644 --- a/lib/do_instance/do_ip.js +++ b/lib/do_instance/do_ip.js @@ -12,6 +12,7 @@ var format = require('util').format; +var common = require('../common'); var errors = require('../errors'); @@ -29,20 +30,25 @@ function do_ip(subcmd, opts, args, cb) { var cli = this.top; - cli.tritonapi.getInstance(args[0], function (err, inst) { - if (err) { - cb(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } + cli.tritonapi.getInstance(args[0], function (err, inst) { + if (err) { + cb(err); + return; + } - if (!inst.primaryIp) { - cb(new errors.TritonError(format( - 'primaryIp not found for instance "%s"', args[0]))); - return; - } + if (!inst.primaryIp) { + cb(new errors.TritonError(format( + 'primaryIp not found for instance "%s"', args[0]))); + return; + } - console.log(inst.primaryIp); - cb(); + console.log(inst.primaryIp); + cb(); + }); }); } diff --git a/lib/do_instance/do_list.js b/lib/do_instance/do_list.js index 79c1b7b..47af52b 100644 --- a/lib/do_instance/do_list.js +++ b/lib/do_instance/do_list.js @@ -74,84 +74,93 @@ function do_list(subcmd, opts, args, callback) { var imgs = []; var insts; - vasync.parallel({funcs: [ - function getTheImages(next) { - self.top.tritonapi.listImages({ - state: 'all', - useCache: true - }, function (err, _imgs) { - if (err) { - if (err.statusCode === 403) { - /* - * This could be a authorization error due to RBAC - * on a subuser. We don't want to fail `triton inst ls` - * if the subuser can ListMachines, but not ListImages. - */ - log.debug(err, - 'authz error listing images for insts info'); - next(); + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); + } + vasync.parallel({funcs: [ + function getTheImages(next) { + self.top.tritonapi.listImages({ + state: 'all', + useCache: true + }, function (err, _imgs) { + if (err) { + if (err.statusCode === 403) { + /* + * This could be a authorization error due + * to RBAC on a subuser. We don't want to + * fail `triton inst ls` if the subuser + * can ListMachines, but not ListImages. + */ + log.debug( + err, + 'authz error listing images for insts info'); + next(); + } else { + next(err); + } } else { - next(err); + imgs = _imgs; + next(); } - } else { - imgs = _imgs; - next(); - } - }); - }, - function getTheMachines(next) { - self.top.tritonapi.cloudapi.listMachines(listOpts, + }); + }, + function getTheMachines(next) { + self.top.tritonapi.cloudapi.listMachines( + listOpts, function (err, _insts) { - if (err) { - next(err); - } else { - insts = _insts; - next(); - } + if (err) { + next(err); + } else { + insts = _insts; + next(); + } + }); + } + ]}, function (err, results) { + /* + * Error handling: vasync.parallel's `err` is always a + * MultiError. We want to prefer the `getTheMachines` err, + * e.g. if both get a self-signed cert error. + */ + if (err) { + err = results.operations[1].err || err; + return callback(err); + } + + // map "uuid" => "image_name" + var imgmap = {}; + imgs.forEach(function (img) { + imgmap[img.id] = format('%s@%s', img.name, img.version); }); - } - ]}, function (err, results) { - /* - * Error handling: vasync.parallel's `err` is always a MultiError. We - * want to prefer the `getTheMachines` err, e.g. if both get a - * self-signed cert error. - */ - if (err) { - err = results.operations[1].err || err; - return callback(err); - } - // map "uuid" => "image_name" - var imgmap = {}; - imgs.forEach(function (img) { - imgmap[img.id] = format('%s@%s', img.name, img.version); - }); - - // Add extra fields for nice output. - var now = new Date(); - insts.forEach(function (inst) { - var created = new Date(inst.created); - inst.age = common.longAgo(created, now); - inst.img = imgmap[inst.image] || common.uuidToShortId(inst.image); - inst.shortid = inst.id.split('-', 1)[0]; - var flags = []; - if (inst.docker) flags.push('D'); - if (inst.firewall_enabled) flags.push('F'); - if (inst.brand === 'kvm') flags.push('K'); - inst.flags = flags.length ? flags.join('') : undefined; - }); - - if (opts.json) { - common.jsonStream(insts); - } else { - tabula(insts, { - skipHeader: opts.H, - columns: columns, - sort: sort, - dottedLookup: true + // Add extra fields for nice output. + var now = new Date(); + insts.forEach(function (inst) { + var created = new Date(inst.created); + inst.age = common.longAgo(created, now); + inst.img = imgmap[inst.image] || + common.uuidToShortId(inst.image); + inst.shortid = inst.id.split('-', 1)[0]; + var flags = []; + if (inst.docker) flags.push('D'); + if (inst.firewall_enabled) flags.push('F'); + if (inst.brand === 'kvm') flags.push('K'); + inst.flags = flags.length ? flags.join('') : undefined; }); - } - callback(); + + if (opts.json) { + common.jsonStream(insts); + } else { + tabula(insts, { + skipHeader: opts.H, + columns: columns, + sort: sort, + dottedLookup: true + }); + } + callback(); + }); }); } diff --git a/lib/do_instance/do_snapshot/do_create.js b/lib/do_instance/do_snapshot/do_create.js index d8aa522..de7b5cf 100644 --- a/lib/do_instance/do_snapshot/do_create.js +++ b/lib/do_instance/do_snapshot/do_create.js @@ -47,7 +47,8 @@ function do_create(subcmd, opts, args, cb) { createOpts.name = opts.name; } - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function createSnapshot(ctx, next) { ctx.start = Date.now(); diff --git a/lib/do_instance/do_snapshot/do_delete.js b/lib/do_instance/do_snapshot/do_delete.js index 99d6c9b..e85237c 100644 --- a/lib/do_instance/do_snapshot/do_delete.js +++ b/lib/do_instance/do_snapshot/do_delete.js @@ -61,7 +61,8 @@ function do_delete(subcmd, opts, args, cb) { }); } - vasync.pipeline({funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function confirm(_, next) { if (opts.force) { return next(); diff --git a/lib/do_instance/do_snapshot/do_get.js b/lib/do_instance/do_snapshot/do_get.js index 437a724..7bf02f3 100644 --- a/lib/do_instance/do_snapshot/do_get.js +++ b/lib/do_instance/do_snapshot/do_get.js @@ -36,22 +36,27 @@ function do_get(subcmd, opts, args, cb) { var name = args[1]; var cli = this.top; - cli.tritonapi.getInstanceSnapshot({ - id: id, - name: name - }, function onSnapshot(err, snapshot) { - if (err) { - cb(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } + cli.tritonapi.getInstanceSnapshot({ + id: id, + name: name + }, function onSnapshot(err, snapshot) { + if (err) { + cb(err); + return; + } - if (opts.json) { - console.log(JSON.stringify(snapshot)); - } else { - console.log(JSON.stringify(snapshot, null, 4)); - } + if (opts.json) { + console.log(JSON.stringify(snapshot)); + } else { + console.log(JSON.stringify(snapshot, null, 4)); + } - cb(); + cb(); + }); }); } diff --git a/lib/do_instance/do_snapshot/do_list.js b/lib/do_instance/do_snapshot/do_list.js index 622f576..162dfc3 100644 --- a/lib/do_instance/do_snapshot/do_list.js +++ b/lib/do_instance/do_snapshot/do_list.js @@ -40,35 +40,40 @@ function do_list(subcmd, opts, args, cb) { var cli = this.top; var machineId = args[0]; - cli.tritonapi.listInstanceSnapshots({ - id: machineId - }, function onSnapshots(err, snapshots) { - if (err) { - cb(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - - if (opts.json) { - common.jsonStream(snapshots); - } else { - var columns = COLUMNS_DEFAULT; - - if (opts.o) { - columns = opts.o; - } else if (opts.long) { - columns = COLUMNS_DEFAULT; + cli.tritonapi.listInstanceSnapshots({ + id: machineId + }, function onSnapshots(err, snapshots) { + if (err) { + cb(err); + return; } - columns = columns.split(','); - var sort = opts.s.split(','); + if (opts.json) { + common.jsonStream(snapshots); + } else { + var columns = COLUMNS_DEFAULT; - tabula(snapshots, { - skipHeader: opts.H, - columns: columns, - sort: sort - }); - } - cb(); + if (opts.o) { + columns = opts.o; + } else if (opts.long) { + columns = COLUMNS_DEFAULT; + } + + columns = columns.split(','); + var sort = opts.s.split(','); + + tabula(snapshots, { + skipHeader: opts.H, + columns: columns, + sort: sort + }); + } + cb(); + }); }); } diff --git a/lib/do_instance/do_ssh.js b/lib/do_instance/do_ssh.js index bdf8fed..be2da84 100644 --- a/lib/do_instance/do_ssh.js +++ b/lib/do_instance/do_ssh.js @@ -38,52 +38,59 @@ function do_ssh(subcmd, opts, args, callback) { id = id.substr(i + 1); } - cli.tritonapi.getInstance(id, function (err, inst) { - if (err) { - callback(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); } + cli.tritonapi.getInstance(id, function (err, inst) { + if (err) { + callback(err); + return; + } - var ip = inst.primaryIp; - if (!ip) { - callback(new Error('primaryIp not found for instance')); - return; - } + var ip = inst.primaryIp; + if (!ip) { + callback(new Error('primaryIp not found for instance')); + return; + } - args = ['-l', user, ip].concat(args); + args = ['-l', user, ip].concat(args); - /* - * By default we disable ControlMaster (aka mux, aka SSH connection - * multiplexing) because of - * https://github.com/joyent/node-triton/issues/52 - * - */ - if (!opts.no_disable_mux) { /* - * A simple `-o ControlMaster=no` doesn't work. With just that - * option, a `ControlPath` option (from ~/.ssh/config) will still - * be used if it exists. Our hack is to set a ControlPath we - * know should not exist. Using '/dev/null' wasn't a good - * alternative because `ssh` tries "$ControlPath.$somerandomnum" - * and also because Windows. + * By default we disable ControlMaster (aka mux, aka SSH connection + * multiplexing) because of + * https://github.com/joyent/node-triton/issues/52 + * */ - var nullSshControlPath = path.resolve( - cli.tritonapi.config._configDir, 'tmp', 'nullSshControlPath'); - args = [ - '-o', 'ControlMaster=no', - '-o', 'ControlPath='+nullSshControlPath - ].concat(args); - } + if (!opts.no_disable_mux) { + /* + * A simple `-o ControlMaster=no` doesn't work. With + * just that option, a `ControlPath` option (from + * ~/.ssh/config) will still be used if it exists. Our + * hack is to set a ControlPath we know should not + * exist. Using '/dev/null' wasn't a good alternative + * because `ssh` tries "$ControlPath.$somerandomnum" + * and also because Windows. + */ + var nullSshControlPath = path.resolve( + cli.tritonapi.config._configDir, 'tmp', + 'nullSshControlPath'); + args = [ + '-o', 'ControlMaster=no', + '-o', 'ControlPath='+nullSshControlPath + ].concat(args); + } - self.top.log.info({args: args}, 'forking ssh'); - var child = spawn('ssh', args, {stdio: 'inherit'}); - child.on('close', function (code) { - /* - * Once node 0.10 support is dropped we could instead: - * process.exitCode = code; - * callback(); - */ - process.exit(code); + self.top.log.info({args: args}, 'forking ssh'); + var child = spawn('ssh', args, {stdio: 'inherit'}); + child.on('close', function (code) { + /* + * Once node 0.10 support is dropped we could instead: + * process.exitCode = code; + * callback(); + */ + process.exit(code); + }); }); }); } diff --git a/lib/do_instance/do_tag/do_delete.js b/lib/do_instance/do_tag/do_delete.js index deeb518..261a9bc 100644 --- a/lib/do_instance/do_tag/do_delete.js +++ b/lib/do_instance/do_tag/do_delete.js @@ -12,6 +12,7 @@ var vasync = require('vasync'); +var common = require('../../common'); var errors = require('../../errors'); @@ -29,41 +30,46 @@ function do_delete(subcmd, opts, args, cb) { } var waitTimeoutMs = opts.wait_timeout * 1000; /* seconds to ms */ - if (opts.all) { - self.top.tritonapi.deleteAllInstanceTags({ - id: args[0], - wait: opts.wait, - waitTimeout: waitTimeoutMs - }, function (err) { - console.log('Deleted all tags on instance %s', args[0]); - cb(err); - }); - } else { - // Uniq'ify the given names. - var names = {}; - args.slice(1).forEach(function (arg) { names[arg] = true; }); - names = Object.keys(names); + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); + } + if (opts.all) { + self.top.tritonapi.deleteAllInstanceTags({ + id: args[0], + wait: opts.wait, + waitTimeout: waitTimeoutMs + }, function (err) { + console.log('Deleted all tags on instance %s', args[0]); + cb(err); + }); + } else { + // Uniq'ify the given names. + var names = {}; + args.slice(1).forEach(function (arg) { names[arg] = true; }); + names = Object.keys(names); - // TODO: Instead of waiting for each delete, let's delete them all then - // wait for the set. - vasync.forEachPipeline({ - inputs: names, - func: function deleteOne(name, next) { - self.top.tritonapi.deleteInstanceTag({ - id: args[0], - tag: name, - wait: opts.wait, - waitTimeout: waitTimeoutMs - }, function (err) { - if (!err) { - console.log('Deleted tag %s on instance %s', - name, args[0]); - } - next(err); - }); - } - }, cb); - } + // TODO: Instead of waiting for each delete, let's delete + // them all then wait for the set. + vasync.forEachPipeline({ + inputs: names, + func: function deleteOne(name, next) { + self.top.tritonapi.deleteInstanceTag({ + id: args[0], + tag: name, + wait: opts.wait, + waitTimeout: waitTimeoutMs + }, function (err) { + if (!err) { + console.log('Deleted tag %s on instance %s', + name, args[0]); + } + next(err); + }); + } + }, cb); + } + }); } do_delete.options = [ diff --git a/lib/do_instance/do_tag/do_get.js b/lib/do_instance/do_tag/do_get.js index 9060300..d78c300 100644 --- a/lib/do_instance/do_tag/do_get.js +++ b/lib/do_instance/do_tag/do_get.js @@ -10,6 +10,7 @@ * `triton instance tag get ...` */ +var common = require('../../common'); var errors = require('../../errors'); @@ -23,20 +24,25 @@ function do_get(subcmd, opts, args, cb) { return; } - self.top.tritonapi.getInstanceTag({ - id: args[0], - tag: args[1] - }, function (err, value) { - if (err) { - cb(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - if (opts.json) { - console.log(JSON.stringify(value)); - } else { - console.log(value); - } - cb(); + self.top.tritonapi.getInstanceTag({ + id: args[0], + tag: args[1] + }, function (err, value) { + if (err) { + cb(err); + return; + } + if (opts.json) { + console.log(JSON.stringify(value)); + } else { + console.log(value); + } + cb(); + }); }); } diff --git a/lib/do_instance/do_tag/do_list.js b/lib/do_instance/do_tag/do_list.js index 60023ef..09d7dba 100644 --- a/lib/do_instance/do_tag/do_list.js +++ b/lib/do_instance/do_tag/do_list.js @@ -10,6 +10,7 @@ * `triton instance tag list ...` */ +var common = require('../../common'); var errors = require('../../errors'); function do_list(subcmd, opts, args, cb) { @@ -22,17 +23,23 @@ function do_list(subcmd, opts, args, cb) { return; } - self.top.tritonapi.listInstanceTags({id: args[0]}, function (err, tags) { - if (err) { - cb(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - if (opts.json) { - console.log(JSON.stringify(tags)); - } else { - console.log(JSON.stringify(tags, null, 4)); - } - cb(); + self.top.tritonapi.listInstanceTags( + {id: args[0]}, function (err, tags) { + if (err) { + cb(err); + return; + } + if (opts.json) { + console.log(JSON.stringify(tags)); + } else { + console.log(JSON.stringify(tags, null, 4)); + } + cb(); + }); }); } diff --git a/lib/do_instance/do_tag/do_replace_all.js b/lib/do_instance/do_tag/do_replace_all.js index fa6f643..97a5dc4 100644 --- a/lib/do_instance/do_tag/do_replace_all.js +++ b/lib/do_instance/do_tag/do_replace_all.js @@ -12,6 +12,7 @@ var vasync = require('vasync'); +var common = require('../../common'); var errors = require('../../errors'); var mat = require('../../metadataandtags'); @@ -27,7 +28,8 @@ function do_replace_all(subcmd, opts, args, cb) { } var log = self.log; - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function gatherTags(ctx, next) { mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) { if (err) { diff --git a/lib/do_instance/do_tag/do_set.js b/lib/do_instance/do_tag/do_set.js index 8040e63..e3e2fbb 100644 --- a/lib/do_instance/do_tag/do_set.js +++ b/lib/do_instance/do_tag/do_set.js @@ -12,6 +12,7 @@ var vasync = require('vasync'); +var common = require('../../common'); var errors = require('../../errors'); var mat = require('../../metadataandtags'); @@ -27,7 +28,8 @@ function do_set(subcmd, opts, args, cb) { } var log = self.log; - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function gatherTags(ctx, next) { mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) { if (err) { diff --git a/lib/do_instance/do_wait.js b/lib/do_instance/do_wait.js index 529acb2..a472238 100644 --- a/lib/do_instance/do_wait.js +++ b/lib/do_instance/do_wait.js @@ -12,6 +12,7 @@ var vasync = require('vasync'); +var common = require('../common'); var distractions = require('../distractions'); var errors = require('../errors'); @@ -34,7 +35,8 @@ function do_wait(subcmd, opts, args, cb) { var done = 0; var instFromId = {}; - vasync.pipeline({funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function getInsts(_, next) { vasync.forEachParallel({ inputs: ids, diff --git a/lib/do_instance/gen_do_ACTION.js b/lib/do_instance/gen_do_ACTION.js index 1e86fc7..d39f254 100644 --- a/lib/do_instance/gen_do_ACTION.js +++ b/lib/do_instance/gen_do_ACTION.js @@ -83,8 +83,6 @@ function gen_do_ACTION(opts) { function _doTheAction(action, subcmd, opts, args, callback) { var self = this; - var now = Date.now(); - var command, state; switch (action) { case 'start': @@ -116,7 +114,17 @@ function _doTheAction(action, subcmd, opts, args, callback) { callback(new errors.UsageError('missing INST arg(s)')); return; } + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); + } + _doOnEachInstance(self, action, command, state, args, opts, callback); + }); +} +function _doOnEachInstance(self, action, command, state, instances, + opts, callback) { + var now = Date.now(); vasync.forEachParallel({ func: function (arg, cb) { var alias, uuid; @@ -190,7 +198,7 @@ function _doTheAction(action, subcmd, opts, args, callback) { }); } }, - inputs: args + inputs: instances }, function (err, results) { var e = err ? (new Error('command failure')) : null; callback(e); diff --git a/lib/do_key/do_add.js b/lib/do_key/do_add.js index 15b5d5a..b868b53 100644 --- a/lib/do_key/do_add.js +++ b/lib/do_key/do_add.js @@ -40,7 +40,8 @@ function do_add(subcmd, opts, args, cb) { var filePath = args[0]; var cli = this.top; - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function gatherDataStdin(ctx, next) { if (filePath !== '-') { return next(); diff --git a/lib/do_key/do_delete.js b/lib/do_key/do_delete.js index 69e77d2..d481201 100644 --- a/lib/do_key/do_delete.js +++ b/lib/do_key/do_delete.js @@ -35,7 +35,8 @@ function do_delete(subcmd, opts, args, cb) { var cli = this.top; - vasync.pipeline({funcs: [ + vasync.pipeline({arg: {cli: this.top}, funcs: [ + common.cliSetupTritonApi, function confirm(_, next) { if (opts.yes) { return next(); diff --git a/lib/do_key/do_get.js b/lib/do_key/do_get.js index 92fd43a..e519358 100644 --- a/lib/do_key/do_get.js +++ b/lib/do_key/do_get.js @@ -35,22 +35,27 @@ function do_get(subcmd, opts, args, cb) { var id = args[0]; var cli = this.top; - cli.tritonapi.cloudapi.getKey({ - // Currently `cloudapi.getUserKey` isn't picky about the `name` being - // passed in as the `opts.fingerprint` arg. - fingerprint: id - }, function onKey(err, key) { - if (err) { - cb(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } + cli.tritonapi.cloudapi.getKey({ + // Currently `cloudapi.getUserKey` isn't picky about the + // `name` being passed in as the `opts.fingerprint` arg. + fingerprint: id + }, function onKey(err, key) { + if (err) { + cb(err); + return; + } - if (opts.json) { - console.log(JSON.stringify(key)); - } else { - console.log(common.chomp(key.key)); - } - cb(); + if (opts.json) { + console.log(JSON.stringify(key)); + } else { + console.log(common.chomp(key.key)); + } + cb(); + }); }); } diff --git a/lib/do_key/do_list.js b/lib/do_key/do_list.js index 69f9c4b..b09063d 100644 --- a/lib/do_key/do_list.js +++ b/lib/do_key/do_list.js @@ -37,37 +37,42 @@ function do_list(subcmd, opts, args, cb) { var cli = this.top; - cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) { - if (err) { - cb(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - - if (opts.json) { - common.jsonStream(keys); - } else if (opts.authorized_keys) { - keys.forEach(function (key) { - console.log(common.chomp(key.key)); - }); - } else { - var columns = COLUMNS_DEFAULT; - - if (opts.o) { - columns = opts.o; - } else if (opts.long) { - columns = COLUMNS_LONG; + cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) { + if (err) { + cb(err); + return; } - columns = columns.split(','); - var sort = opts.s.split(','); + if (opts.json) { + common.jsonStream(keys); + } else if (opts.authorized_keys) { + keys.forEach(function (key) { + console.log(common.chomp(key.key)); + }); + } else { + var columns = COLUMNS_DEFAULT; - tabula(keys, { - skipHeader: false, - columns: columns, - sort: sort - }); - } - cb(); + if (opts.o) { + columns = opts.o; + } else if (opts.long) { + columns = COLUMNS_LONG; + } + + columns = columns.split(','); + var sort = opts.s.split(','); + + tabula(keys, { + skipHeader: false, + columns: columns, + sort: sort + }); + } + cb(); + }); }); } diff --git a/lib/do_network/do_get.js b/lib/do_network/do_get.js index 8d62ed3..02531d2 100644 --- a/lib/do_network/do_get.js +++ b/lib/do_network/do_get.js @@ -25,17 +25,24 @@ function do_get(subcmd, opts, args, cb) { 'incorrect number of args (%d)', args.length))); } - this.top.tritonapi.getNetwork(args[0], function (err, net) { - if (err) { - return cb(err); - } + var tritonapi = this.top.tritonapi; - if (opts.json) { - console.log(JSON.stringify(net)); - } else { - console.log(JSON.stringify(net, null, 4)); + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + cb(setupErr); } - cb(); + tritonapi.getNetwork(args[0], function (err, net) { + if (err) { + return cb(err); + } + + if (opts.json) { + console.log(JSON.stringify(net)); + } else { + console.log(JSON.stringify(net, null, 4)); + } + cb(); + }); }); } diff --git a/lib/do_network/do_list.js b/lib/do_network/do_list.js index b3bd3c9..7cc963c 100644 --- a/lib/do_network/do_list.js +++ b/lib/do_network/do_list.js @@ -49,28 +49,34 @@ function do_list(subcmd, opts, args, callback) { columns = columns.split(','); var sort = opts.s.split(','); + var tritonapi = this.top.tritonapi; - this.top.tritonapi.cloudapi.listNetworks(function (err, networks) { - if (err) { - callback(err); - return; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); } - - if (opts.json) { - common.jsonStream(networks); - } else { - for (var i = 0; i < networks.length; i++) { - var net = networks[i]; - net.shortid = net.id.split('-', 1)[0]; - net.vlan = net.vlan_id; + tritonapi.cloudapi.listNetworks(function (err, networks) { + if (err) { + callback(err); + return; } - tabula(networks, { - skipHeader: opts.H, - columns: columns, - sort: sort - }); - } - callback(); + + if (opts.json) { + common.jsonStream(networks); + } else { + for (var i = 0; i < networks.length; i++) { + var net = networks[i]; + net.shortid = net.id.split('-', 1)[0]; + net.vlan = net.vlan_id; + } + tabula(networks, { + skipHeader: opts.H, + columns: columns, + sort: sort + }); + } + callback(); + }); }); } diff --git a/lib/do_package/do_get.js b/lib/do_package/do_get.js index d02b3a5..4ba4b52 100644 --- a/lib/do_package/do_get.js +++ b/lib/do_package/do_get.js @@ -12,6 +12,7 @@ var format = require('util').format; +var common = require('../common'); var errors = require('../errors'); @@ -24,17 +25,23 @@ function do_get(subcmd, opts, args, callback) { 'incorrect number of args (%d)', args.length))); } - this.top.tritonapi.getPackage(args[0], function onRes(err, pkg) { - if (err) { - return callback(err); + var tritonapi = this.top.tritonapi; + common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); } + tritonapi.getPackage(args[0], function onRes(err, pkg) { + if (err) { + return callback(err); + } - if (opts.json) { - console.log(JSON.stringify(pkg)); - } else { - console.log(JSON.stringify(pkg, null, 4)); - } - callback(); + if (opts.json) { + console.log(JSON.stringify(pkg)); + } else { + console.log(JSON.stringify(pkg, null, 4)); + } + callback(); + }); }); } @@ -63,7 +70,7 @@ do_get.help = [ '', 'Where PACKAGE is a package id (full UUID), exact name, or short id.', '', - 'Note: Currently this dumps prettified JSON by default. That might change', + 'Note: Currently this dumps perttified JSON by default. That might change', 'in the future. Use "-j" to explicitly get JSON output.' /* END JSSTYLED */ ].join('\n'); diff --git a/lib/do_package/do_list.js b/lib/do_package/do_list.js index b7ba5a0..d9166ae 100644 --- a/lib/do_package/do_list.js +++ b/lib/do_package/do_list.js @@ -11,6 +11,7 @@ */ var tabula = require('tabula'); +var vasync = require('vasync'); var common = require('../common'); @@ -68,73 +69,89 @@ function do_list(subcmd, opts, args, callback) { return; } - this.top.tritonapi.cloudapi.listPackages(listOpts, function (err, pkgs) { - if (err) { - callback(err); - return; - } - if (opts.json) { - common.jsonStream(pkgs); - } else { - for (i = 0; i < pkgs.length; i++) { - var pkg = pkgs[i]; - pkg.shortid = pkg.id.split('-', 1)[0]; + var context = { + cli: this.top + }; + vasync.pipeline({arg: context, funcs: [ + common.cliSetupTritonApi, - /* - * We take a slightly "smarter" view of "group" for default - * sorting, to accomodate usage in the JPC. More recent - * common usage is for packages to have "foo-*" naming. - * JPC includes package sets of yore *and* recent that don't - * use the "group" field. We secondarily separate those - * on a possible "foo-" prefix. - */ - pkg._groupPlus = (pkg.group || (pkg.name.indexOf('-') === -1 - ? '' : pkg.name.split('-', 1)[0])); - - if (!opts.p) { - pkg.memoryHuman = common.humanSizeFromBytes({ - precision: 1, - narrow: true - }, pkg.memory * 1024 * 1024); - pkg.swapHuman = common.humanSizeFromBytes({ - precision: 1, - narrow: true - }, pkg.swap * 1024 * 1024); - pkg.diskHuman = common.humanSizeFromBytes({ - precision: 1, - narrow: true - }, pkg.disk * 1024 * 1024); - pkg.vcpusHuman = pkg.vcpus === 0 ? '-' : pkg.vcpus; - } - } - if (!opts.p) { - columns = columns.map(function (c) { - switch (c.lookup || c) { - case 'memory': - return {lookup: 'memoryHuman', name: 'MEMORY', - align: 'right'}; - case 'swap': - return {lookup: 'swapHuman', name: 'SWAP', - align: 'right'}; - case 'disk': - return {lookup: 'diskHuman', name: 'DISK', - align: 'right'}; - case 'vcpus': - return {lookup: 'vcpusHuman', name: 'VCPUS', - align: 'right'}; - default: - return c; + function getThem(arg, next) { + arg.cli.tritonapi.cloudapi.listPackages(listOpts, + function (err, pkgs) { + if (err) { + next(err); + return; } + arg.pkgs = pkgs; + next(); + } + ); + }, + + function display(arg, next) { + if (opts.json) { + common.jsonStream(arg.pkgs); + } else { + for (i = 0; i < arg.pkgs.length; i++) { + var pkg = arg.pkgs[i]; + pkg.shortid = pkg.id.split('-', 1)[0]; + + /* + * We take a slightly "smarter" view of "group" for default + * sorting, to accomodate usage in the JPC. More recent + * common usage is for packages to have "foo-*" naming. + * JPC includes package sets of yore *and* recent that don't + * use the "group" field. We secondarily separate those + * on a possible "foo-" prefix. + */ + pkg._groupPlus = (pkg.group || (pkg.name.indexOf('-') === -1 + ? '' : pkg.name.split('-', 1)[0])); + + if (!opts.p) { + pkg.memoryHuman = common.humanSizeFromBytes({ + precision: 1, + narrow: true + }, pkg.memory * 1024 * 1024); + pkg.swapHuman = common.humanSizeFromBytes({ + precision: 1, + narrow: true + }, pkg.swap * 1024 * 1024); + pkg.diskHuman = common.humanSizeFromBytes({ + precision: 1, + narrow: true + }, pkg.disk * 1024 * 1024); + pkg.vcpusHuman = pkg.vcpus === 0 ? '-' : pkg.vcpus; + } + } + if (!opts.p) { + columns = columns.map(function (c) { + switch (c.lookup || c) { + case 'memory': + return {lookup: 'memoryHuman', name: 'MEMORY', + align: 'right'}; + case 'swap': + return {lookup: 'swapHuman', name: 'SWAP', + align: 'right'}; + case 'disk': + return {lookup: 'diskHuman', name: 'DISK', + align: 'right'}; + case 'vcpus': + return {lookup: 'vcpusHuman', name: 'VCPUS', + align: 'right'}; + default: + return c; + } + }); + } + tabula(arg.pkgs, { + skipHeader: opts.H, + columns: columns, + sort: sort }); } - tabula(pkgs, { - skipHeader: opts.H, - columns: columns, - sort: sort - }); + next(); } - callback(); - }); + ]}, callback); } do_list.options = [ diff --git a/lib/do_profile/do_create.js b/lib/do_profile/do_create.js index f5dcd60..72ae4c6 100644 --- a/lib/do_profile/do_create.js +++ b/lib/do_profile/do_create.js @@ -9,6 +9,7 @@ var format = require('util').format; var fs = require('fs'); var sshpk = require('sshpk'); var vasync = require('vasync'); +var auth = require('smartdc-auth'); var common = require('../common'); var errors = require('../errors'); @@ -101,17 +102,15 @@ function _createProfile(opts, cb) { 'create profile: stdout is not a TTY')); } + var kr = new auth.KeyRing(); + var keyChoices = {}; + var defaults = {}; if (ctx.copy) { defaults = ctx.copy; delete defaults.name; // we don't copy a profile name } else { defaults.url = 'https://us-sw-1.api.joyent.com'; - - var possibleDefaultFp = '~/.ssh/id_rsa'; - if (fs.existsSync(common.tildeSync(possibleDefaultFp))) { - defaults.keyId = possibleDefaultFp; - } } var fields = [ { @@ -156,11 +155,10 @@ function _createProfile(opts, cb) { valCb(); } }, { - desc: 'The fingerprint of the SSH key you have registered ' + - 'for your account. Alternatively, You may enter a local ' + - 'path to a public or private SSH key to have the ' + - 'fingerprint calculated for you.', - default: defaults.keyId, + desc: 'The fingerprint of the SSH key you want to use, or ' + + 'its index in the list above. If the key you want to ' + + 'use is not listed, make sure it is either saved in your ' + + 'SSH keys directory or loaded into the SSH agent.', key: 'keyId', validate: function validateKeyId(value, valCb) { // First try as a fingerprint. @@ -170,44 +168,14 @@ function _createProfile(opts, cb) { } catch (fpErr) { } - // Try as a local path. - var keyPath = common.tildeSync(value); - fs.stat(keyPath, function (statErr, stats) { - if (statErr || !stats.isFile()) { - return valCb(new Error(format( - '"%s" is neither a valid fingerprint, ' + - 'nor an existing file', value))); - } - fs.readFile(keyPath, function (readErr, keyData) { - if (readErr) { - return valCb(readErr); - } - var keyType = (keyPath.slice(-4) === '.pub' - ? 'ssh' : 'pem'); - try { - var key = sshpk.parseKey(keyData, keyType); - } catch (keyErr) { - return valCb(keyErr); - } + // Try as a list index + if (keyChoices[value] !== undefined) { + return valCb(null, keyChoices[value]); + } - /* - * Save the user's explicit given key path. We will - * using it later for Docker setup. Trying to use - * the same format as node-smartdc's loadSSHKey - * `keyPaths` param. - */ - ctx.keyPaths = {}; - if (keyType === 'ssh') { - ctx.keyPaths.public = keyPath; - } else { - ctx.keyPaths.private = keyPath; - } - - var newVal = key.fingerprint('md5').toString(); - console.log('Fingerprint: %s', newVal); - valCb(null, newVal); - }); - }); + valCb(new Error(format( + '"%s" is neither a valid fingerprint, not an index ' + + 'from the list of available keys', value))); } } ]; @@ -234,11 +202,50 @@ function _createProfile(opts, cb) { vasync.forEachPipeline({ inputs: fields, func: function getField(field, nextField) { - if (field.key !== 'name') console.log(); - common.promptField(field, function (err, value) { - data[field.key] = value; - nextField(err); - }); + if (field.key !== 'name') + console.log(); + if (field.key === 'keyId') { + kr.list(function (err, pairs) { + if (err) { + nextField(err); + return; + } + var choice = 1; + console.log('Available SSH keys:'); + Object.keys(pairs).forEach(function (keyId) { + var valid = pairs[keyId].filter(function (kp) { + return (kp.canSign()); + }); + if (valid.length < 1) + return; + var pub = valid[0].getPublicKey(); + console.log( + ' %d. %d-bit %s key with fingerprint %s', + choice, pub.size, pub.type.toUpperCase(), + keyId); + pairs[keyId].forEach(function (kp) { + var comment = kp.comment || + kp.getPublicKey().comment; + console.log(' * [in %s] %s %s %s', + kp.plugin, comment, + (kp.source ? kp.source : ''), + (kp.isLocked() ? '[locked]' : '')); + }); + console.log(); + keyChoices[choice] = keyId; + ++choice; + }); + common.promptField(field, function (err2, value) { + data[field.key] = value; + nextField(err2); + }); + }); + } else { + common.promptField(field, function (err, value) { + data[field.key] = value; + nextField(err); + }); + } } }, function (err) { console.log(); diff --git a/lib/do_profile/profilecommon.js b/lib/do_profile/profilecommon.js index 4d44b82..9a53ef9 100644 --- a/lib/do_profile/profilecommon.js +++ b/lib/do_profile/profilecommon.js @@ -8,6 +8,7 @@ var assert = require('assert-plus'); var auth = require('smartdc-auth'); var format = require('util').format; var fs = require('fs'); +var getpass = require('getpass'); var https = require('https'); var mkdirp = require('mkdirp'); var path = require('path'); @@ -143,14 +144,38 @@ function profileDockerSetup(opts, cb) { assert.optionalObject(opts.keyPaths, 'opts.keyPaths'); assert.func(cb, 'cb'); - var implicit = Boolean(opts.implicit); var cli = opts.cli; - var log = cli.log; var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name}); + + var implicit = Boolean(opts.implicit); + var log = cli.log; + var profile = tritonapi.profile; var dockerHost; - vasync.pipeline({arg: {}, funcs: [ + vasync.pipeline({arg: {tritonapi: tritonapi}, funcs: [ + function dockerKeyWarning(arg, next) { + console.log(wordwrap( + '\nWARNING: Docker uses TLS-based authentication with a ' + + 'different security model from SSH keys. As a result, the ' + + 'Docker client cannot currently support encrypted ' + + '(password protected) keys or SSH agents. If you ' + + 'continue, the Triton CLI will attempt to format a copy ' + + 'of your SSH *private* key as an unencrypted TLS cert ' + + 'and place the copy in ~/.triton/docker for use by the ' + + 'Docker client.')); + common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); + } else { + next(); + } + }); + }, + + common.cliSetupTritonApi, + function checkCloudapiStatus(arg, next) { tritonapi.cloudapi.ping({}, function (err, pong, res) { if (!res) { @@ -222,69 +247,16 @@ function profileDockerSetup(opts, cb) { next(); }, - function findSshPrivKey_keyPaths(arg, next) { - if (!opts.keyPaths) { - next(); + function checkSshPrivKey(arg, next) { + try { + tritonapi.keyPair.getPrivateKey(); + } catch (e) { + next(new errors.SetupError(format('could not obtain SSH ' + + 'private key for keypair with fingerprint "%s" ' + + 'to create Docker certificate.', profile.keyId))); return; } - - var privKeyPath = opts.keyPaths.private; - if (!privKeyPath) { - assert.string(opts.keyPaths.public); - assert.ok(opts.keyPaths.public.slice(-4) === '.pub'); - privKeyPath = opts.keyPaths.public.slice(0, -4); - if (!fs.existsSync(privKeyPath)) { - cb(new errors.SetupError(format('could not find SSH ' - + 'private key file from public key file "%s": "%s" ' - + 'does not exist', opts.keyPaths.public, - privKeyPath))); - return; - } - } - - arg.sshKeyPaths = { - private: privKeyPath, - public: opts.keyPaths.public - }; - - fs.readFile(privKeyPath, function (readErr, keyData) { - if (readErr) { - cb(readErr); - return; - } - try { - arg.sshPrivKey = sshpk.parseKey(keyData, 'pem'); - } catch (keyErr) { - cb(keyErr); - return; - } - log.trace({sshKeyPaths: arg.sshKeyPaths}, - 'profileDockerSetup: findSshPrivKey_keyPaths'); - next(); - }); - }, - function findSshPrivKey_keyId(arg, next) { - if (opts.keyPaths) { - next(); - return; - } - - // TODO: keyPaths here is using a non-#master of node-smartdc-auth. - // Change back to a smartdc-auth release when - // https://github.com/joyent/node-smartdc-auth/pull/5 is in. - auth.loadSSHKey(profile.keyId, function (err, key, keyPaths) { - if (err) { - // TODO: better error message here. - next(err); - } else { - assert.ok(key, 'key from auth.loadSSHKey'); - log.trace({keyId: profile.keyId, sshKeyPaths: keyPaths}, - 'profileDockerSetup: findSshPrivKey'); - arg.sshKeyPaths = keyPaths; - arg.sshPrivKey = key; - next(); - } - }); + next(); }, /* @@ -348,31 +320,32 @@ function profileDockerSetup(opts, cb) { }, function genClientCert_key(arg, next) { arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem'); - common.execPlus({ - cmd: format('openssl rsa -in %s -out %s -outform pem', - arg.sshKeyPaths.private, arg.keyPath), - log: log - }, next); - }, - function genClientCert_csr(arg, next) { - arg.csrPath = path.resolve(arg.dockerCertPath, 'csr.pem'); - common.execPlus({ - cmd: format('openssl req -new -key %s -out %s -subj "/CN=%s"', - arg.keyPath, arg.csrPath, profile.account), - log: log - }, next); + var data = tritonapi.keyPair.getPrivateKey().toBuffer('pkcs1'); + fs.writeFile(arg.keyPath, data, function (err) { + if (err) { + next(new errors.SetupError(err, format( + 'error writing file %s', arg.keyPath))); + } else { + next(); + } + }); }, function genClientCert_cert(arg, next) { arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem'); - common.execPlus({ - cmd: format( - 'openssl x509 -req -days 365 -in %s -signkey %s -out %s', - arg.csrPath, arg.keyPath, arg.certPath), - log: log - }, next); - }, - function genClientCert_deleteCsr(arg, next) { - rimraf(arg.csrPath, next); + + var privKey = tritonapi.keyPair.getPrivateKey(); + var id = sshpk.identityFromDN('CN=' + profile.account); + var cert = sshpk.createSelfSignedCertificate(id, privKey); + var data = cert.toBuffer('pem'); + + fs.writeFile(arg.certPath, data, function (err) { + if (err) { + next(new errors.SetupError(err, format( + 'error writing file %s', arg.keyPath))); + } else { + next(); + } + }); }, function getServerCa(arg, next) { diff --git a/lib/do_services.js b/lib/do_services.js index b8e90be..8850451 100644 --- a/lib/do_services.js +++ b/lib/do_services.js @@ -31,38 +31,44 @@ function do_services(subcmd, opts, args, callback) { var columns = opts.o.split(','); var sort = opts.s.split(','); + var tritonapi = this.tritonapi; - this.tritonapi.cloudapi.listServices(function (err, services) { - if (err) { - callback(err); - return; + common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) { + if (setupErr) { + callback(setupErr); } + tritonapi.cloudapi.listServices(function (err, services) { + if (err) { + callback(err); + return; + } - if (opts.json) { - console.log(JSON.stringify(services)); - } else { - /* - * services are returned in the form of: - * {name: 'endpoint', name2: 'endpoint2', ...} - * we "normalize" them for use by tabula and JSON stream - * by making them an array - */ - var svcs = []; - Object.keys(services).forEach(function (key) { - svcs.push({ - name: key, - endpoint: services[key] + if (opts.json) { + console.log(JSON.stringify(services)); + } else { + /* + * services are returned in the form of: + * {name: 'endpoint', name2: 'endpoint2', ...} + * we "normalize" them for use by tabula and JSON stream + * by making them an array + */ + var svcs = []; + Object.keys(services).forEach(function (key) { + svcs.push({ + name: key, + endpoint: services[key] + }); }); - }); - tabula(svcs, { - skipHeader: opts.H, - columns: columns, - sort: sort, - dottedLookup: true - }); - } + tabula(svcs, { + skipHeader: opts.H, + columns: columns, + sort: sort, + dottedLookup: true + }); + } - callback(); + callback(); + }); }); } diff --git a/lib/index.js b/lib/index.js index c050222..a081288 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,64 +5,112 @@ */ /* - * Copyright 2015 Joyent, Inc. + * Copyright 2016 Joyent, Inc. */ var assert = require('assert-plus'); +var vasync = require('vasync'); var bunyannoop = require('./bunyannoop'); +var mod_common = require('./common'); var mod_config = require('./config'); -var tritonapi = require('./tritonapi'); +var mod_cloudapi2 = require('./cloudapi2'); +var mod_tritonapi = require('./tritonapi'); +/* BEGIN JSSTYLED */ /** - * A convenience wrapper around `tritonapi.createClient` to for simpler usage. + * A convenience wrapper around `tritonapi.TritonApi` for simpler usage. + * Conveniences are: + * - It wraps up the 3-step process of TritonApi client preparation into + * this one call. + * - It accepts optional `profileName` and `configDir` parameters that will + * load a profile by name and load a config, respectively. * - * Minimally this only requires that one of `profileName` or `profile` be - * specified. Examples: + * Client preparation is a 3-step process: * - * var triton = require('triton'); - * var client = triton.createClient({ + * 1. create the client object; + * 2. initialize it (mainly involves finding the SSH key identified by the + * `keyId`); and, + * 3. optionally unlock the SSH key (if it is passphrase-protected and not in + * an ssh-agent). + * + * The simplest usage that handles all of these is: + * + * var mod_triton = require('triton'); + * mod_triton.createClient({ + * profileName: 'env', + * unlockKeyFn: triton.promptPassphraseUnlockKey + * }, function (err, client) { + * if (err) { + * // handle err + * } + * + * // use `client` + * }); + * + * Minimally, only of `profileName` or `profile` is required. Examples: + * + * // Manually specify profile parameters. + * mod_triton.createClient({ * profile: { * url: "", * account: "", * keyId: "" * } - * }); - * -- + * }, function (err, client) { ... }); + * * // Loading a profile from the environment (the `TRITON_*` and/or * // `SDC_*` environment variables). - * var client = triton.createClient({profileName: 'env'}); - * -- - * var client = triton.createClient({ - * configDir: '~/.triton', // use the CLI's config dir ... - * profileName: 'east1' // ... to find named profiles - * }); - * -- + * triton.createClient({profileName: 'env'}, + * function (err, client) { ... }); + * + * // Use one of the named profiles from the `triton` CLI. + * triton.createClient({ + * configDir: '~/.triton', + * profileName: 'east1' + * }, function (err, client) { ... }); + * * // The same thing using the underlying APIs. - * var client = triton.createClient({ - * config: triton.loadConfig({configDir: '~/.triton'}, + * triton.createClient({ + * config: triton.loadConfig({configDir: '~/.triton'}), * profile: triton.loadProfile({name: 'east1', configDir: '~/.triton'}) - * }); - * - * A more complete example an app using triton internally might want: - * - * var triton = require('triton'); - * var bunyan = require('bunyan'); - * - * var appConfig = { - * // However the app handles its config. - * }; - * var log = bunyan.createLogger({name: 'myapp', component: 'triton'}); - * var client = triton.createClient({ - * log: log, - * profile: appConfig.tritonProfile - * }); - * + * }, function (err, client) { ... }); * * TODO: The story for an app wanting to specify some Triton config but NOT * have to have a triton $configDir/config.json is poor. * + * + * # What is that `unlockKeyFn` about? + * + * Triton uses HTTP-Signature auth: an SSH private key is used to sign requests. + * The server-side authenticates by verifying that signature using the + * previously uploaded public key. For the client to sign a request it needs an + * unlocked private key: an SSH private key that (a) is not + * passphrase-protected, (b) is loaded in an ssh-agent, or (c) for which we + * have a passphrase. + * + * If `createClient` finds that its key is locked, it will use `unlockKeyFn` + * as follows to attempt to unlock it: + * + * unlockKeyFn({ + * tritonapi: client + * }, function (unlockErr) { + * // ... + * }); + * + * This package exports a convenience `promptPassphraseUnlockKey` function that + * will prompt the user for a passphrase on stdin. Your tooling can use this + * function, provide your own, or skip key unlocking. + * + * The failure mode for a locked key is an error like this: + * + * SigningError: error signing request: SSH private key id_rsa is locked (encrypted/password-protected). It must be unlocked before use. + * at SigningError._TritonBaseVError (/Users/trentm/tmp/node-triton/lib/errors.js:55:12) + * at new SigningError (/Users/trentm/tmp/node-triton/lib/errors.js:173:23) + * at CloudApi._getAuthHeaders (/Users/trentm/tmp/node-triton/lib/cloudapi2.js:185:22) + * + * * @param opts {Object}: * - @param profile {Object} A *Triton profile* object that includes the * information required to connect to a CloudAPI -- minimally this: @@ -91,14 +139,24 @@ var tritonapi = require('./tritonapi'); * One may not specify both `configDir` and `config`. * - @param log {Bunyan Logger} Optional. A Bunyan logger. If not provided, * a stub that does no logging will be used. + * - @param {Function} unlockKeyFn - Optional. A function to handle + * unlocking the SSH key found for this profile, if necessary. It must + * be of the form `function (opts, cb)` where `opts.tritonapi` is the + * initialized TritonApi client. If the caller is a command-line + * interface, then `triton.promptPassphraseUnlockKey` can be used to + * prompt on stdin for the SSH key passphrase, if needed. + * @param {Function} cb - `function (err, client)` */ -function createClient(opts) { +/* END JSSTYLED */ +function createClient(opts, cb) { assert.object(opts, 'opts'); assert.optionalObject(opts.profile, 'opts.profile'); assert.optionalString(opts.profileName, 'opts.profileName'); assert.optionalObject(opts.config, 'opts.config'); assert.optionalString(opts.configDir, 'opts.configDir'); assert.optionalObject(opts.log, 'opts.log'); + assert.optionalFunc(opts.unlockKeyFn, 'opts.unlockKeyFn'); + assert.func(cb, 'cb'); assert.ok(!(opts.profile && opts.profileName), 'cannot specify both opts.profile and opts.profileName'); @@ -113,42 +171,87 @@ function createClient(opts) { 'must provide opts.configDir for opts.profileName!="env"'); } - var log = opts.log; - if (!opts.log) { - log = new bunyannoop.BunyanNoopLogger(); - } + var log; + var client; - var config = opts.config; - if (!config) { - config = mod_config.loadConfig({configDir: opts.configDir}); - } + vasync.pipeline({funcs: [ + function theSyncPart(_, next) { + log = opts.log || new bunyannoop.BunyanNoopLogger(); - var profile = opts.profile; - if (!profile) { - profile = mod_config.loadProfile({ - name: opts.profileName, - configDir: config._configDir - }); - } - // Don't require one to arbitrarily have a profile.name if manually - // creating it. - if (!profile.name) { - // TODO: might want this to be hash or slug of profile params. - profile.name = '_'; - } - mod_config.validateProfile(profile); + var config; + if (opts.config) { + config = opts.config; + } else { + try { + config = mod_config.loadConfig( + {configDir: opts.configDir}); + } catch (configErr) { + next(configErr); + return; + } + } - var client = tritonapi.createClient({ - log: log, - config: config, - profile: profile + var profile; + if (opts.profile) { + profile = opts.profile; + /* + * Don't require one to arbitrarily have a profile.name if + * manually creating it. + */ + if (!profile.name) { + // TODO: might want this to be a hash/slug of params. + profile.name = '_'; + } + } else { + try { + profile = mod_config.loadProfile({ + name: opts.profileName, + configDir: config._configDir + }); + } catch (profileErr) { + next(profileErr); + return; + } + } + try { + mod_config.validateProfile(profile); + } catch (valErr) { + next(valErr); + return; + } + + client = mod_tritonapi.createClient({ + log: log, + config: config, + profile: profile + }); + next(); + }, + function initTheClient(_, next) { + client.init(next); + }, + function optionallyUnlockKey(_, next) { + if (!opts.unlockKeyFn) { + next(); + return; + } + + opts.unlockKeyFn({tritonapi: client}, next); + } + ]}, function (err) { + log.trace({err: err}, 'createClient complete'); + if (err) { + cb(err); + } else { + cb(null, client); + } }); - return client; } module.exports = { createClient: createClient, + promptPassphraseUnlockKey: mod_common.promptPassphraseUnlockKey, /** * `createClient` provides convenience parameters to not *have* to call @@ -159,7 +262,10 @@ module.exports = { loadProfile: mod_config.loadProfile, loadAllProfiles: mod_config.loadAllProfiles, - createTritonApiClient: tritonapi.createClient, - // For those wanting a lower-level raw CloudAPI client. - createCloudApiClient: require('./cloudapi2').createClient + /* + * For those wanting a lower-level TritonApi createClient, or an + * even *lower*-level raw CloudAPI client. + */ + createTritonApiClient: mod_tritonapi.createClient, + createCloudApiClient: mod_cloudapi2.createClient }; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 36e39fc..b93ae84 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -6,10 +6,81 @@ /* * Copyright 2016 Joyent, Inc. - * - * Core TritonApi client driver class. */ +/* BEGIN JSSTYLED */ +/* + * Core `TritonApi` client class. A TritonApi client object is a wrapper around + * a lower-level `CloudApi` client that makes raw calls to + * [Cloud API](https://apidocs.joyent.com/cloudapi/). The wrapper provides + * some conveniences, for example: + * - referring to resources by "shortId" (8-char UUID prefixes) or "name" + * (e.g. an VM instance has a unique name for an account, but the raw + * Cloud API only supports lookup by full UUID); + * - filling in of image details for instances which only have an "image_uuid" + * in Cloud API responses; + * - support for waiting for async operations to complete via "wait" parameters; + * - profile handling. + * + * Preparing a TritonApi is a three-step process. (Note: Some users might + * prefer to use the `createClient` convenience function in "index.js" that + * wraps up all three steps into a single call.) + * + * 1. Create the client object. + * 2. Initialize it (mainly involves finding the SSH key identified by the + * `keyId`). + * 3. Optionally, unlock the SSH key (if it is passphrase-protected and not in + * an ssh-agent). If you know that your key is not passphrase-protected + * or is an ssh-agent, then you can skip this step. The failure mode for + * a locked key looks like this: + * SigningError: error signing request: SSH private key id_rsa is locked (encrypted/password-protected). It must be unlocked before use. + * at SigningError._TritonBaseVError (/Users/trentm/tmp/node-triton/lib/errors.js:55:12) + * at new SigningError (/Users/trentm/tmp/node-triton/lib/errors.js:173:23) + * at CloudApi._getAuthHeaders (/Users/trentm/tmp/node-triton/lib/cloudapi2.js:185:22) + * + * Usage: + * var mod_triton = require('triton'); + * + * // 1. Create the TritonApi instance. + * var client = mod_triton.createTritonApiClient({ + * log: log, + * profile: profile, // See mod_triton.loadProfile + * config: config // See mod_triton.loadConfig + * }); + * + * // 2. Call `init` to setup the profile. This involves finding the SSH + * // key identified by the profile's keyId. + * client.init(function (initErr) { + * if (initErr) boom(initErr); + * + * // 3. Unlock the SSH key, if necessary. Possibilities are: + * // (a) Skip this step. If the key is locked, you will get a + * // "SigningError" at first attempt to sign. See example above. + * // (b) The key is not locked. + * // `client.keyPair.isLocked() === false` + * // (c) You have a passphrase for the key: + * if (client.keyPair.isLocked()) { + * // This throws if the passphrase is incorrect. + * client.keyPair.unlock(passphrase); + * } + * + * // (d) Or you use a function that will prompt for a passphrase + * // and unlock with that. E.g., `promptPassphraseUnlockKey` + * // is one provided by this package that with prompt on stdin. + * mod_triton.promptPassphraseUnlockKey({ + * tritonapi: client + * }, function (unlockErr) { + * if (unlockErr) boom(unlockErr); + * + * // 4. Now you can finally make an API call. For example: + * client.listImages(function (err, imgs) { + * // ... + * }); + * }); + * }); + */ +/* END JSSTYLED */ + var assert = require('assert-plus'); var auth = require('smartdc-auth'); var EventEmitter = require('events').EventEmitter; @@ -24,6 +95,7 @@ var restifyBunyanSerializers = require('restify-clients/lib/helpers/bunyan').serializers; var tabula = require('tabula'); var vasync = require('vasync'); +var sshpk = require('sshpk'); var cloudapi = require('./cloudapi2'); var common = require('./common'); @@ -116,6 +188,14 @@ function _stepFwRuleId(arg, next) { /** * Create a TritonApi client. * + * Public properties (TODO: doc all of these): + * - profile + * - config + * - log + * - cacheDir (only available if configured with a configDir) + * - keyPair (available after init) + * - cloudapi (available after init) + * * @param opts {Object} * - log {Bunyan Logger} * ... @@ -128,6 +208,7 @@ function TritonApi(opts) { this.profile = opts.profile; this.config = opts.config; + this.keyPair = null; // Make sure a given bunyan logger has reasonable client_re[qs] serializers. // Note: This was fixed in restify, then broken again in @@ -147,29 +228,43 @@ function TritonApi(opts) { this.config.cacheDir, common.profileSlug(this.profile)); this.log.trace({cacheDir: this.cacheDir}, 'cache dir'); - // TODO perhaps move this to an async .init() - if (!fs.existsSync(this.cacheDir)) { - try { - mkdirp.sync(this.cacheDir); - } catch (e) { - throw e; - } - } } - - this.cloudapi = this._cloudapiFromProfile(this.profile); } TritonApi.prototype.close = function close() { - this.cloudapi.close(); - delete this.cloudapi; + if (this.cloudapi) { + this.cloudapi.close(); + delete this.cloudapi; + } }; -TritonApi.prototype._cloudapiFromProfile = - function _cloudapiFromProfile(profile) -{ +TritonApi.prototype.init = function init(cb) { + var self = this; + if (this.cacheDir) { + fs.exists(this.cacheDir, function (exists) { + if (!exists) { + mkdirp(self.cacheDir, function (err) { + if (err) { + cb(err); + return; + } + self._setupProfile(cb); + }); + } else { + self._setupProfile(cb); + } + }); + } else { + self._setupProfile(cb); + } +}; + +TritonApi.prototype._setupProfile = function _setupProfile(cb) { + var self = this; + var profile = this.profile; + assert.object(profile, 'profile'); assert.string(profile.account, 'profile.account'); assert.optionalString(profile.actAsAccount, 'profile.actAsAccount'); @@ -185,32 +280,39 @@ TritonApi.prototype._cloudapiFromProfile = ? true : !profile.insecure); var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION; - var sign; - if (profile.privKey) { - sign = auth.privateKeySigner({ - user: profile.account, - subuser: profile.user, - keyId: profile.keyId, - key: profile.privKey - }); - } else { - sign = auth.cliSigner({ - keyId: profile.keyId, - user: profile.account, - subuser: profile.user - }); - } - var client = cloudapi.createClient({ + var opts = { url: profile.url, account: profile.actAsAccount || profile.account, - user: profile.user, + principal: { + account: profile.account, + user: profile.user + }, roles: profile.roles, version: acceptVersion, rejectUnauthorized: rejectUnauthorized, - sign: sign, log: this.log - }); - return client; + }; + + if (profile.privKey) { + var key = sshpk.parsePrivateKey(profile.privKey); + this.keyPair = + opts.principal.keyPair = + auth.KeyPair.fromPrivateKey(key); + this.cloudapi = cloudapi.createClient(opts); + cb(null); + } else { + var kr = new auth.KeyRing(); + var fp = sshpk.parseFingerprint(profile.keyId); + kr.findSigningKeyPair(fp, function (err, kp) { + if (err) { + cb(err); + return; + } + self.keyPair = opts.principal.keyPair = kp; + self.cloudapi = cloudapi.createClient(opts); + cb(null); + }); + } }; diff --git a/package.json b/package.json index 079b53a..d75237c 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "bunyan": "1.5.1", "cmdln": "4.1.2", "extsprintf": "1.0.2", + "getpass": "0.1.6", "lomstream": "1.1.0", "mkdirp": "0.5.1", "node-uuid": "1.4.3", @@ -19,8 +20,9 @@ "restify-errors": "3.0.0", "rimraf": "2.4.4", "semver": "5.1.0", - "smartdc-auth": "git+https://github.com/joyent/node-smartdc-auth.git#05d9077", - "sshpk": "1.7.x", + "smartdc-auth": "2.5.2", + "sshpk": "1.10.1", + "sshpk-agent": "1.4.2", "strsplit": "1.0.0", "tabula": "1.7.0", "vasync": "1.6.3", diff --git a/test/integration/api-images.test.js b/test/integration/api-images.test.js index 992cc89..a3b8750 100644 --- a/test/integration/api-images.test.js +++ b/test/integration/api-images.test.js @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2015, Joyent, Inc. + * Copyright 2016 Joyent, Inc. */ /* @@ -29,9 +29,11 @@ test('TritonApi images', function (tt) { var client; tt.test(' setup: client', function (t) { - client = h.createClient(); - t.ok(client, 'client'); - t.end(); + h.createClient(function (err, client_) { + t.error(err); + client = client_; + t.end(); + }); }); var testOpts = {}; diff --git a/test/integration/api-instances.test.js b/test/integration/api-instances.test.js index 6934123..3b62164 100644 --- a/test/integration/api-instances.test.js +++ b/test/integration/api-instances.test.js @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2015, Joyent, Inc. + * Copyright 2016 Joyent, Inc. */ /* @@ -15,24 +15,26 @@ var h = require('./helpers'); var test = require('tape'); -var common = require('../../lib/common'); - // --- Globals - var CLIENT; var INST; // --- Tests - test('TritonApi packages', function (tt) { - tt.test(' setup', function (t) { - CLIENT = h.createClient(); - t.ok(CLIENT, 'client'); + tt.test(' setup', function (t) { + h.createClient(function (err, client_) { + t.error(err); + CLIENT = client_; + t.end(); + }); + }); + + tt.test(' setup: inst', function (t) { CLIENT.cloudapi.listMachines(function (err, insts) { if (h.ifErr(t, err)) return t.end(); diff --git a/test/integration/api-networks.test.js b/test/integration/api-networks.test.js index e6d31bf..feaaf70 100644 --- a/test/integration/api-networks.test.js +++ b/test/integration/api-networks.test.js @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2015, Joyent, Inc. + * Copyright 2016 Joyent, Inc. */ /* @@ -15,28 +15,28 @@ var h = require('./helpers'); var test = require('tape'); -var common = require('../../lib/common'); - // --- Globals - var CLIENT; var NET; // --- Tests - test('TritonApi networks', function (tt) { tt.test(' setup', function (t) { - CLIENT = h.createClient(); - t.ok(CLIENT, 'client'); + h.createClient(function (err, client_) { + t.error(err); + CLIENT = client_; + t.end(); + }); + }); + tt.test(' setup: net', function (t) { var opts = { account: CLIENT.profile.account }; - CLIENT.cloudapi.listNetworks(opts, function (err, nets) { if (h.ifErr(t, err)) return t.end(); diff --git a/test/integration/api-packages.test.js b/test/integration/api-packages.test.js index 053711e..1f7d2a3 100644 --- a/test/integration/api-packages.test.js +++ b/test/integration/api-packages.test.js @@ -5,7 +5,7 @@ */ /* - * Copyright (c) 2015, Joyent, Inc. + * Copyright 2016 Joyent, Inc. */ /* @@ -30,9 +30,14 @@ var PKG; test('TritonApi packages', function (tt) { tt.test(' setup', function (t) { - CLIENT = h.createClient(); - t.ok(CLIENT, 'client'); + h.createClient(function (err, client_) { + t.error(err); + CLIENT = client_; + t.end(); + }); + }); + tt.test(' setup: pkg', function (t) { CLIENT.cloudapi.listPackages(function (err, pkgs) { if (h.ifErr(t, err)) return t.end(); diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 7218c7d..f40ab56 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -248,12 +248,14 @@ function jsonStreamParse(s) { /* * Create a TritonApi client using the CLI. */ -function createClient() { - return mod_triton.createClient({ +function createClient(cb) { + assert.func(cb, 'cb'); + + mod_triton.createClient({ log: LOG, profile: CONFIG.profile, configDir: '~/.triton' // piggy-back on Triton CLI config dir - }); + }, cb); }