joyent/node-triton#108 support passphrase protected keys

Reviewed by: Trent Mick <trent.mick@joyent.com>
Approved by: Trent Mick <trent.mick@joyent.com>
This commit is contained in:
Chris Burroughs 2016-12-13 13:04:41 -05:00
parent 696439f1ae
commit ad7d608011
63 changed files with 1872 additions and 1224 deletions

View File

@ -7,6 +7,83 @@ Known issues:
## not yet released ## 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 ...'. - [joyent/node-triton#143] Fix duplicate output from 'triton rbac key ...'.
## 4.15.0 ## 4.15.0

View File

@ -234,19 +234,27 @@ documentation](https://apidocs.joyent.com/docker) for more information.)
## `TritonApi` Module Usage ## `TritonApi` Module Usage
Node-triton can also be used as a node module for your own node.js tooling. 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.listImages(function (err, images) {
client.close(); // Remember to close the client to close TCP conn. client.close(); // Remember to close the client to close TCP conn.
if (err) { if (err) {
@ -255,7 +263,14 @@ A basic example:
console.log(JSON.stringify(images, null, 4)); 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 ## 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. 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 ## Development Hooks
Before commiting be sure to, at least: Before commiting be sure to, at least:

View File

@ -1,42 +1,45 @@
#!/usr/bin/env node #!/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: * 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 bunyan = require('bunyan');
var cloudapi = require('../lib/cloudapi2'); var path = require('path');
var triton = require('../'); // typically `require('triton');`
var log = bunyan.createLogger({ var log = bunyan.createLogger({
name: 'example-get-account', name: path.basename(__filename),
level: 'trace' level: process.env.LOG_LEVEL || 'info',
}) stream: process.stderr
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
}); });
log.info('start') triton.createClient({
client.getAccount(function (err, account) { log: log,
p('getAccount: err', err) // Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a
p('getAccount: account', account) // `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));
}
});
}); });

View File

@ -1,46 +1,46 @@
#!/usr/bin/env node #!/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: * 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 bunyan = require('bunyan');
var path = require('path');
var triton = require('../'); // typically `require('triton');` 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({ var log = bunyan.createLogger({
name: 'test-list-instances', name: path.basename(__filename),
level: process.env.LOG_LEVEL || 'trace' level: process.env.LOG_LEVEL || 'info',
stream: process.stderr
}); });
/* triton.createClient({
* 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({
log: log, log: log,
profile: { // Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a
url: URL, // `profile` object.
account: ACCOUNT, profileName: 'env',
keyId: KEY_ID unlockKeyFn: triton.promptPassphraseUnlockKey
} }, function createdClient(err, client) {
});
// 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.
if (err) { if (err) {
console.error('listInstances err:', err); console.error('error creating Triton client: %s\n%s', err, err.stack);
} else { process.exitStatus = 1;
console.log(JSON.stringify(insts, null, 4)); return;
} }
});
// 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));
}
});
});

View File

@ -27,7 +27,7 @@ var vasync = require('vasync');
var common = require('./common'); var common = require('./common');
var mod_config = require('./config'); var mod_config = require('./config');
var errors = require('./errors'); 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. ' + help: 'A cloudapi API version, or semver range, to attempt to use. ' +
'This is passed in the "Accept-Version" header. ' + 'This is passed in the "Accept-Version" header. ' +
'See `triton cloudapi /--ping` to list supported versions. ' + '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 ' + '*This is intended for development use only. It could cause ' +
'`triton` processing of responses to break.*', '`triton` processing of responses to break.*',
hidden: true hidden: true
@ -302,16 +302,16 @@ CLI.prototype.init = function (opts, args, callback) {
return self._profile; return self._profile;
}); });
this.__defineGetter__('tritonapi', function getTritonapi() { try {
if (self._tritonapi === undefined) { self.tritonapi = lib_tritonapi.createClient({
self._tritonapi = tritonapi.createClient({ log: self.log,
log: self.log, profile: self.profile,
profile: self.profile, config: self.config
config: self.config });
}); } catch (createErr) {
} callback(createErr);
return self._tritonapi; return;
}); }
if (process.env.TRITON_COMPLETE) { if (process.env.TRITON_COMPLETE) {
/* /*
@ -326,21 +326,21 @@ CLI.prototype.init = function (opts, args, callback) {
* Example usage: * Example usage:
* TRITON_COMPLETE=images triton -p my-profile create * 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); callback(err || false);
}); });
} else { } else {
// Cmdln class handles `opts.help`. // 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) { CLI.prototype.fini = function fini(subcmd, err, cb) {
this.log.trace({err: err, subcmd: subcmd}, 'cli fini'); this.log.trace({err: err, subcmd: subcmd}, 'cli fini');
if (this._tritonapi) { if (this.tritonapi) {
this._tritonapi.close(); this.tritonapi.close();
delete this._tritonapi; delete this.tritonapi;
} }
cb(); cb();
}; };
@ -361,7 +361,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
var cacheFile = path.join(this.tritonapi.cacheDir, type + '.completions'); var cacheFile = path.join(this.tritonapi.cacheDir, type + '.completions');
var ttl = 5 * 60 * 1000; // timeout of cache file info (ms) var ttl = 5 * 60 * 1000; // timeout of cache file info (ms)
var cloudapi = this.tritonapi.cloudapi; var tritonapi = this.tritonapi;
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {}, funcs: [
function tryCacheFile(arg, next) { 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) { function gather(arg, next) {
var completions; var completions;
switch (type) { switch (type) {
case 'packages': case 'packages':
cloudapi.listPackages({}, function (err, pkgs) { tritonapi.cloudapi.listPackages({}, function (err, pkgs) {
if (err) { if (err) {
next(err); next(err);
return; return;
@ -402,7 +414,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
}); });
break; break;
case 'images': case 'images':
cloudapi.listImages({}, function (err, imgs) { tritonapi.cloudapi.listImages({}, function (err, imgs) {
if (err) { if (err) {
next(err); next(err);
return; return;
@ -424,7 +436,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
}); });
break; break;
case 'instances': case 'instances':
cloudapi.listMachines({}, function (err, insts) { tritonapi.cloudapi.listMachines({}, function (err, insts) {
if (err) { if (err) {
next(err); next(err);
return; return;
@ -449,7 +461,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
* on that is that with the additional prefixes, there would * on that is that with the additional prefixes, there would
* be too many. * be too many.
*/ */
cloudapi.listMachines({}, function (err, insts) { tritonapi.cloudapi.listMachines({}, function (err, insts) {
if (err) { if (err) {
next(err); next(err);
return; return;
@ -470,7 +482,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
}); });
break; break;
case 'networks': case 'networks':
cloudapi.listNetworks({}, function (err, nets) { tritonapi.cloudapi.listNetworks({}, function (err, nets) {
if (err) { if (err) {
next(err); next(err);
return; return;
@ -489,7 +501,8 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
}); });
break; break;
case 'fwrules': case 'fwrules':
cloudapi.listFirewallRules({}, function (err, fwrules) { tritonapi.cloudapi.listFirewallRules({}, function (err,
fwrules) {
if (err) { if (err) {
next(err); next(err);
return; return;
@ -503,7 +516,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
}); });
break; break;
case 'keys': case 'keys':
cloudapi.listKeys({}, function (err, keys) { tritonapi.cloudapi.listKeys({}, function (err, keys) {
if (err) { if (err) {
next(err); next(err);
return; return;
@ -602,7 +615,7 @@ CLI.prototype.tritonapiFromProfileName =
'tritonapiFromProfileName: loaded profile'); 'tritonapiFromProfileName: loaded profile');
} }
return tritonapi.createClient({ return lib_tritonapi.createClient({
log: this.log, log: this.log,
profile: profile, profile: profile,
config: this.config config: this.config

View File

@ -41,6 +41,7 @@ var os = require('os');
var querystring = require('querystring'); var querystring = require('querystring');
var vasync = require('vasync'); var vasync = require('vasync');
var auth = require('smartdc-auth'); var auth = require('smartdc-auth');
var EventEmitter = require('events').EventEmitter;
var bunyannoop = require('./bunyannoop'); var bunyannoop = require('./bunyannoop');
var common = require('./common'); var common = require('./common');
@ -64,10 +65,7 @@ var OS_PLATFORM = os.platform();
* *
* @param options {Object} * @param options {Object}
* - {String} url (required) Cloud API base url * - {String} url (required) Cloud API base url
* - {String} account (required) The account login name. * - Authentication options (see below)
* - {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.
* - {String} version (optional) Used for the accept-version header. This * - {String} version (optional) Used for the accept-version header. This
* defaults to '*', meaning that over time you could experience breaking * defaults to '*', meaning that over time you could experience breaking
* changes. Specifying a value is strongly recommended. E.g. '~7.1'. * 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 * {Boolean} agent Set to `false` to not get KeepAlive. You want
* this for CLIs. * this for CLIs.
* TODO doc the backoff/retry available options * 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. * @throws {TypeError} on bad input.
* @constructor * @constructor
* *
@ -90,17 +110,30 @@ function CloudApi(options) {
assert.object(options, 'options'); assert.object(options, 'options');
assert.string(options.url, 'options.url'); assert.string(options.url, 'options.url');
assert.string(options.account, 'options.account'); 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.optionalArrayOfString(options.roles, 'options.roles');
assert.optionalString(options.version, 'options.version'); assert.optionalString(options.version, 'options.version');
assert.optionalObject(options.log, 'options.log'); 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.url = options.url;
this.account = options.account; this.account = options.account;
this.user = options.user; // optional RBAC subuser
this.roles = options.roles; this.roles = options.roles;
this.sign = options.sign;
this.log = options.log || new bunyannoop.BunyanNoopLogger(); this.log = options.log || new bunyannoop.BunyanNoopLogger();
if (!options.version) { if (!options.version) {
options.version = '*'; options.version = '*';
@ -128,16 +161,33 @@ CloudApi.prototype.close = function close(callback) {
this.client.close(); 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'); assert.func(callback, 'callback');
var self = this;
var headers = {}; var headers = {};
var rs = auth.requestSigner({ var rs;
sign: self.sign 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(); headers.date = rs.writeDateHeader();
// TODO: token auth support // TODO: token auth support
@ -222,14 +272,8 @@ CloudApi.prototype._request = function _request(opts, cb) {
var method = (opts.method || 'GET').toLowerCase(); var method = (opts.method || 'GET').toLowerCase();
assert.ok(['get', 'post', 'put', 'delete', 'head'].indexOf(method) >= 0, assert.ok(['get', 'post', 'put', 'delete', 'head'].indexOf(method) >= 0,
'invalid method given'); 'invalid HTTP method given');
switch (method) { var clientFnName = (method === 'delete' ? 'del' : method);
case 'delete':
method = 'del';
break;
default:
break;
}
if (self.roles && self.roles.length > 0) { if (self.roles && self.roles.length > 0) {
if (opts.path.indexOf('?') !== -1) { 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) { if (err) {
cb(err); cb(err);
return; return;
@ -252,9 +296,9 @@ CloudApi.prototype._request = function _request(opts, cb) {
headers: headers headers: headers
}; };
if (opts.data) if (opts.data)
self.client[method](reqOpts, opts.data, cb); self.client[clientFnName](reqOpts, opts.data, cb);
else else
self.client[method](reqOpts, cb); self.client[clientFnName](reqOpts, cb);
}); });
}; };

View File

@ -12,6 +12,7 @@ var assert = require('assert-plus');
var child_process = require('child_process'); var child_process = require('child_process');
var crypto = require('crypto'); var crypto = require('crypto');
var fs = require('fs'); var fs = require('fs');
var getpass = require('getpass');
var os = require('os'); var os = require('os');
var path = require('path'); var path = require('path');
var read = require('read'); 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 * Edit the given text in $EDITOR (defaulting to `vi`) and return the edited
* text. * text.
@ -984,6 +1078,8 @@ module.exports = {
promptYesNo: promptYesNo, promptYesNo: promptYesNo,
promptEnter: promptEnter, promptEnter: promptEnter,
promptField: promptField, promptField: promptField,
promptPassphraseUnlockKey: promptPassphraseUnlockKey,
cliSetupTritonApi: cliSetupTritonApi,
editInEditor: editInEditor, editInEditor: editInEditor,
ansiStylize: ansiStylize, ansiStylize: ansiStylize,
indent: indent, indent: indent,

View File

@ -21,28 +21,34 @@ function do_get(subcmd, opts, args, callback) {
return; return;
} }
this.top.tritonapi.cloudapi.getAccount(function (err, account) { var tritonapi = this.top.tritonapi;
if (err) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
callback(err); if (setupErr) {
return; callback(setupErr);
} }
tritonapi.cloudapi.getAccount(function (err, account) {
if (err) {
callback(err);
return;
}
if (opts.json) { if (opts.json) {
console.log(JSON.stringify(account)); console.log(JSON.stringify(account));
} else { } else {
// pretty print // pretty print
var dates = ['updated', 'created']; var dates = ['updated', 'created'];
Object.keys(account).forEach(function (key) { Object.keys(account).forEach(function (key) {
var val = account[key]; var val = account[key];
if (dates.indexOf(key) >= 0) { if (dates.indexOf(key) >= 0) {
console.log('%s: %s (%s)', key, val, console.log('%s: %s (%s)', key, val,
common.longAgo(new Date(val))); common.longAgo(new Date(val)));
} else { } else {
console.log('%s: %s', key, val); console.log('%s: %s', key, val);
} }
}); });
} }
callback(); callback();
});
}); });
} }

View File

@ -30,7 +30,9 @@ function do_update(subcmd, opts, args, callback) {
var log = this.log; var log = this.log;
var tritonapi = this.top.tritonapi; var tritonapi = this.top.tritonapi;
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function gatherDataArgs(ctx, next) { function gatherDataArgs(ctx, next) {
if (opts.file) { if (opts.file) {
next(); next();

View File

@ -31,36 +31,42 @@ function do_datacenters(subcmd, opts, args, callback) {
var columns = opts.o.split(','); var columns = opts.o.split(',');
var sort = opts.s.split(','); var sort = opts.s.split(',');
var tritonapi = this.tritonapi;
this.tritonapi.cloudapi.listDatacenters(function (err, datacenters) { common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
if (err) { if (setupErr) {
callback(err); callback(setupErr);
return;
} }
tritonapi.cloudapi.listDatacenters(function (err, datacenters) {
if (err) {
callback(err);
return;
}
if (opts.json) { if (opts.json) {
console.log(JSON.stringify(datacenters)); console.log(JSON.stringify(datacenters));
} else { } else {
/* /*
* datacenters are returned in the form of: * datacenters are returned in the form of:
* {name: 'url', name2: 'url2', ...} * {name: 'url', name2: 'url2', ...}
* we "normalize" them for use by tabula by making them an array * we "normalize" them for use by tabula by making them an array
*/ */
var dcs = []; var dcs = [];
Object.keys(datacenters).forEach(function (key) { Object.keys(datacenters).forEach(function (key) {
dcs.push({ dcs.push({
name: key, name: key,
url: datacenters[key] url: datacenters[key]
});
}); });
}); tabula(dcs, {
tabula(dcs, { skipHeader: opts.H,
skipHeader: opts.H, columns: columns,
columns: columns, sort: sort,
sort: sort, dottedLookup: true
dottedLookup: true });
}); }
} callback();
callback(); });
}); });
} }

View File

@ -45,15 +45,21 @@ function do_create(subcmd, opts, args, cb) {
createOpts.description = opts.description; createOpts.description = opts.description;
} }
this.top.tritonapi.cloudapi.createFirewallRule(createOpts, var tritonapi = this.top.tritonapi;
function (err, fwrule) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (err) { if (setupErr) {
cb(err); cb(setupErr);
return;
} }
console.log('Created firewall rule %s%s', fwrule.id, tritonapi.cloudapi.createFirewallRule(
(!fwrule.enabled ? ' (disabled)' : '')); createOpts, function (err, fwrule) {
cb(); if (err) {
cb(err);
return;
}
console.log('Created firewall rule %s%s', fwrule.id,
(!fwrule.enabled ? ' (disabled)' : ''));
cb();
});
}); });
} }

View File

@ -31,10 +31,11 @@ function do_delete(subcmd, opts, args, cb) {
return; return;
} }
var cli = this.top; var tritonapi = this.top.tritonapi;
var ruleIds = args; var ruleIds = args;
vasync.pipeline({funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function confirm(_, next) { function confirm(_, next) {
if (opts.force) { if (opts.force) {
return next(); return next();
@ -61,8 +62,8 @@ function do_delete(subcmd, opts, args, cb) {
vasync.forEachParallel({ vasync.forEachParallel({
inputs: ruleIds, inputs: ruleIds,
func: function deleteOne(id, nextId) { func: function deleteOne(id, nextId) {
cli.tritonapi.deleteFirewallRule({ tritonapi.deleteFirewallRule({
id: id id: id
}, function (err) { }, function (err) {
if (err) { if (err) {
nextId(err); nextId(err);

View File

@ -30,22 +30,26 @@ function do_disable(subcmd, opts, args, cb) {
return; return;
} }
var cli = this.top; var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
vasync.forEachParallel({ if (setupErr) {
inputs: args, cb(setupErr);
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();
});
} }
}, 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);
});
} }

View File

@ -30,22 +30,26 @@ function do_enable(subcmd, opts, args, cb) {
return; return;
} }
var cli = this.top; var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
vasync.forEachParallel({ if (setupErr) {
inputs: args, cb(setupErr);
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();
});
} }
}, 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);
});
} }

View File

@ -33,21 +33,26 @@ function do_get(subcmd, opts, args, cb) {
} }
var id = args[0]; var id = args[0];
var cli = this.top; var tritonapi = this.top.tritonapi;
cli.tritonapi.getFirewallRule(id, function onRule(err, fwrule) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (err) { if (setupErr) {
cb(err); cb(setupErr);
return;
} }
tritonapi.getFirewallRule(id, function onRule(err, fwrule) {
if (err) {
cb(err);
return;
}
if (opts.json) { if (opts.json) {
console.log(JSON.stringify(fwrule)); console.log(JSON.stringify(fwrule));
} else { } else {
console.log(JSON.stringify(fwrule, null, 4)); console.log(JSON.stringify(fwrule, null, 4));
} }
cb(); cb();
});
}); });
} }

View File

@ -54,75 +54,81 @@ function do_instances(subcmd, opts, args, cb) {
var tritonapi = this.top.tritonapi; var tritonapi = this.top.tritonapi;
vasync.parallel({funcs: [ common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
function getTheImages(next) { if (setupErr) {
tritonapi.listImages({ cb(setupErr);
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);
} }
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" // map "uuid" => "image_name"
var imgmap = {}; var imgmap = {};
imgs.forEach(function (img) { imgs.forEach(function (img) {
imgmap[img.id] = format('%s@%s', img.name, img.version); 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();
}); });
} }

View File

@ -35,40 +35,45 @@ function do_list(subcmd, opts, args, cb) {
return; return;
} }
var cli = this.top; var tritonapi = this.top.tritonapi;
cli.tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (err) { if (setupErr) {
cb(err); cb(setupErr);
return;
} }
tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) {
if (opts.json) { if (err) {
common.jsonStream(rules); cb(err);
} else { return;
var columns = COLUMNS_DEFAULT;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_LONG;
} }
columns = columns.toLowerCase().split(','); if (opts.json) {
var sort = opts.s.toLowerCase().split(','); common.jsonStream(rules);
} else {
var columns = COLUMNS_DEFAULT;
if (columns.indexOf('shortid') !== -1) { if (opts.o) {
rules.forEach(function (rule) { columns = opts.o;
rule.shortid = common.uuidToShortId(rule.id); } 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();
});
}); });
} }

View File

@ -37,7 +37,9 @@ function do_update(subcmd, opts, args, cb) {
var id = args.shift(); var id = args.shift();
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function gatherDataArgs(ctx, next) { function gatherDataArgs(ctx, next) {
if (opts.file) { if (opts.file) {
next(); next();

View File

@ -26,7 +26,6 @@ var mat = require('../metadataandtags');
// ---- the command // ---- the command
function do_create(subcmd, opts, args, cb) { function do_create(subcmd, opts, args, cb) {
var self = this;
if (opts.help) { if (opts.help) {
this.do_help('help', {}, [subcmd], cb); this.do_help('help', {}, [subcmd], cb);
return; return;
@ -37,9 +36,10 @@ function do_create(subcmd, opts, args, cb) {
} }
var log = this.top.log; 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) { function loadTags(ctx, next) {
mat.tagsFromCreateOpts(opts, log, function (err, tags) { mat.tagsFromCreateOpts(opts, log, function (err, tags) {
if (err) { if (err) {
@ -76,7 +76,7 @@ function do_create(subcmd, opts, args, cb) {
return; return;
} }
self.top.tritonapi.getInstance(id, function (err, inst) { tritonapi.getInstance(id, function (err, inst) {
if (err) { if (err) {
next(err); next(err);
return; return;
@ -113,20 +113,22 @@ function do_create(subcmd, opts, args, cb) {
return; return;
} }
cloudapi.createImageFromMachine(createOpts, function (err, img) { tritonapi.cloudapi.createImageFromMachine(
if (err) { createOpts, function (err, img) {
next(new errors.TritonError(err, 'error creating image')); if (err) {
return; next(new errors.TritonError(err,
} 'error creating image'));
ctx.img = img; return;
if (opts.json) { }
console.log(JSON.stringify(img)); ctx.img = img;
} else { if (opts.json) {
console.log('Creating image %s@%s (%s)', console.log(JSON.stringify(img));
img.name, img.version, img.id); } else {
} console.log('Creating image %s@%s (%s)',
next(); img.name, img.version, img.id);
}); }
next();
});
}, },
function maybeWait(ctx, next) { function maybeWait(ctx, next) {
if (!opts.wait) { if (!opts.wait) {
@ -147,8 +149,8 @@ function do_create(subcmd, opts, args, cb) {
ctx.img.state = 'running'; ctx.img.state = 'running';
waitCb(null, ctx.img); waitCb(null, ctx.img);
}, 5000); }, 5000);
} } : tritonapi.cloudapi.waitForImageStates.bind(
: cloudapi.waitForImageStates.bind(cloudapi)); tritonapi.cloudapi));
waiter({ waiter({
id: ctx.img.id, id: ctx.img.id,

View File

@ -26,7 +26,8 @@ function do_delete(subcmd, opts, args, cb) {
} }
var ids = args; 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 * Lookup images, if not given UUIDs: we'll need to do it anyway
* for the DeleteImage call(s), and doing so explicitly here allows * for the DeleteImage call(s), and doing so explicitly here allows

View File

@ -12,6 +12,7 @@
var format = require('util').format; var format = require('util').format;
var common = require('../common');
var errors = require('../errors'); var errors = require('../errors');
@ -24,17 +25,23 @@ function do_get(subcmd, opts, args, callback) {
'incorrect number of args (%d)', args.length))); 'incorrect number of args (%d)', args.length)));
} }
this.top.tritonapi.getImage(args[0], function onRes(err, img) { var tritonapi = this.top.tritonapi;
if (err) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
return callback(err); if (setupErr) {
callback(setupErr);
} }
tritonapi.getImage(args[0], function onRes(err, img) {
if (err) {
return callback(err);
}
if (opts.json) { if (opts.json) {
console.log(JSON.stringify(img)); console.log(JSON.stringify(img));
} else { } else {
console.log(JSON.stringify(img, null, 4)); console.log(JSON.stringify(img, null, 4));
} }
callback(); callback();
});
}); });
} }

View File

@ -63,42 +63,48 @@ function do_list(subcmd, opts, args, callback) {
listOpts.state = 'all'; listOpts.state = 'all';
} }
this.top.tritonapi.listImages(listOpts, function onRes(err, imgs, res) { var tritonapi = this.top.tritonapi;
if (err) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
return callback(err); if (setupErr) {
callback(setupErr);
} }
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (opts.json) { if (err) {
common.jsonStream(imgs); return callback(err);
} 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, { if (opts.json) {
skipHeader: opts.H, common.jsonStream(imgs);
columns: columns, } else {
sort: sort // Add some convenience fields
}); // Added fields taken from imgapi-cli.git.
} for (var i = 0; i < imgs.length; i++) {
callback(); 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();
});
}); });
} }

View File

@ -12,6 +12,7 @@
var vasync = require('vasync'); var vasync = require('vasync');
var common = require('../common');
var distractions = require('../distractions'); var distractions = require('../distractions');
var errors = require('../errors'); var errors = require('../errors');
@ -34,7 +35,8 @@ function do_wait(subcmd, opts, args, cb) {
var done = 0; var done = 0;
var imgFromId = {}; var imgFromId = {};
vasync.pipeline({funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function getImgs(_, next) { function getImgs(_, next) {
vasync.forEachParallel({ vasync.forEachParallel({
inputs: ids, inputs: ids,

View File

@ -28,69 +28,75 @@ function do_info(subcmd, opts, args, callback) {
var out = {}; var out = {};
var i = 0; var i = 0;
var tritonapi = this.tritonapi;
this.tritonapi.cloudapi.getAccount(cb.bind('account')); i++; common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
this.tritonapi.cloudapi.listMachines(cb.bind('machines')); i++; if (setupErr) {
callback(setupErr);
function cb(err, data) {
if (err) {
callback(err);
return;
} }
out[this.toString()] = data; tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
if (--i === 0) tritonapi.cloudapi.listMachines(cb.bind('machines')); i++;
done();
}
function done() { function cb(err, data) {
// parse name if (err) {
var name; callback(err);
if (out.account.firstName && out.account.lastName) return;
name = format('%s %s', out.account.firstName, }
out.account.lastName); out[this.toString()] = data;
else if (out.account.firstName) if (--i === 0)
name = out.account.firstName; done();
// 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();
} 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 = [ do_info.options = [

View File

@ -27,7 +27,6 @@ var sortDefault = 'id,time';
function do_audit(subcmd, opts, args, cb) { function do_audit(subcmd, opts, args, cb) {
var self = this;
if (opts.help) { if (opts.help) {
this.do_help('help', {}, [subcmd], cb); this.do_help('help', {}, [subcmd], cb);
return; return;
@ -51,23 +50,25 @@ function do_audit(subcmd, opts, args, cb) {
var arg = args[0]; var arg = args[0];
var uuid; var uuid;
var tritonapi = this.top.tritonapi;
if (common.isUUID(arg)) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
uuid = arg; if (common.isUUID(arg)) {
go1(); uuid = arg;
} else {
self.top.tritonapi.getInstance(arg, function (err, inst) {
if (err) {
cb(err);
return;
}
uuid = inst.id;
go1(); go1();
}); } else {
} tritonapi.getInstance(arg, function (err, inst) {
if (err) {
cb(err);
return;
}
uuid = inst.id;
go1();
});
}});
function go1() { function go1() {
self.top.tritonapi.cloudapi.machineAudit(uuid, function (err, audit) { tritonapi.cloudapi.machineAudit(uuid, function (err, audit) {
if (err) { if (err) {
cb(err); cb(err);
return; return;

View File

@ -22,7 +22,6 @@ var mat = require('../metadataandtags');
function do_create(subcmd, opts, args, cb) { function do_create(subcmd, opts, args, cb) {
var self = this;
if (opts.help) { if (opts.help) {
this.do_help('help', {}, [subcmd], cb); this.do_help('help', {}, [subcmd], cb);
return; return;
@ -31,9 +30,10 @@ function do_create(subcmd, opts, args, cb) {
} }
var log = this.top.log; 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 */ /* BEGIN JSSTYLED */
/* /*
* Parse --affinity options for validity to `ctx.affinities`. * Parse --affinity options for validity to `ctx.affinities`.
@ -158,7 +158,7 @@ function do_create(subcmd, opts, args, cb) {
nearFar.push(aff.val); nearFar.push(aff.val);
nextAff(); nextAff();
} else { } else {
self.top.tritonapi.getInstance({ tritonapi.getInstance({
id: aff.val, id: aff.val,
fields: ['id'] fields: ['id']
}, function (err, inst) { }, function (err, inst) {
@ -222,7 +222,7 @@ function do_create(subcmd, opts, args, cb) {
name: args[0], name: args[0],
useCache: true useCache: true
}; };
self.top.tritonapi.getImage(_opts, function (err, img) { tritonapi.getImage(_opts, function (err, img) {
if (err) { if (err) {
return next(err); return next(err);
} }
@ -243,7 +243,7 @@ function do_create(subcmd, opts, args, cb) {
return; return;
} }
self.top.tritonapi.getPackage(id, function (err, pkg) { tritonapi.getPackage(id, function (err, pkg) {
if (err) { if (err) {
return next(err); return next(err);
} }
@ -261,7 +261,7 @@ function do_create(subcmd, opts, args, cb) {
vasync.forEachPipeline({ vasync.forEachPipeline({
inputs: opts.network, inputs: opts.network,
func: function getOneNetwork(name, nextNet) { func: function getOneNetwork(name, nextNet) {
self.top.tritonapi.getNetwork(name, function (err, net) { tritonapi.getNetwork(name, function (err, net) {
if (err) { if (err) {
nextNet(err); nextNet(err);
} else { } else {
@ -316,7 +316,7 @@ function do_create(subcmd, opts, args, cb) {
return next(); return next();
} }
cloudapi.createMachine(createOpts, function (err, inst) { tritonapi.cloudapi.createMachine(createOpts, function (err, inst) {
if (err) { if (err) {
next(new errors.TritonError(err, next(new errors.TritonError(err,
'error creating instance')); 'error creating instance'));
@ -352,8 +352,8 @@ function do_create(subcmd, opts, args, cb) {
ctx.inst.state = 'running'; ctx.inst.state = 'running';
waitCb(null, ctx.inst); waitCb(null, ctx.inst);
}, 5000); }, 5000);
} } : tritonapi.cloudapi.waitForMachineStates.bind(
: cloudapi.waitForMachineStates.bind(cloudapi)); tritonapi.cloudapi));
waiter({ waiter({
id: ctx.inst.id, id: ctx.inst.id,

View File

@ -14,6 +14,7 @@ var assert = require('assert-plus');
var format = require('util').format; var format = require('util').format;
var vasync = require('vasync'); var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors'); var errors = require('../errors');
@ -50,28 +51,33 @@ function do_disable_firewall(subcmd, opts, args, cb) {
}); });
} }
vasync.forEachParallel({ common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
inputs: args, if (setupErr) {
func: function disableOne(name, nextInst) { cb(setupErr);
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) { vasync.forEachParallel({
cb(err); 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);
});
}); });
} }

View File

@ -14,6 +14,7 @@ var assert = require('assert-plus');
var format = require('util').format; var format = require('util').format;
var vasync = require('vasync'); var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors'); var errors = require('../errors');
@ -50,28 +51,33 @@ function do_enable_firewall(subcmd, opts, args, cb) {
}); });
} }
vasync.forEachParallel({ common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
inputs: args, if (setupErr) {
func: function enableOne(name, nextInst) { cb(setupErr);
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) { vasync.forEachParallel({
cb(err); 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);
});
}); });
} }

View File

@ -41,41 +41,46 @@ function do_fwrules(subcmd, opts, args, cb) {
var id = args[0]; var id = args[0];
var cli = this.top; var cli = this.top;
cli.tritonapi.listInstanceFirewallRules({ common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
id: id if (setupErr) {
}, function onRules(err, rules) { cb(setupErr);
if (err) {
cb(err);
return;
} }
cli.tritonapi.listInstanceFirewallRules({
if (opts.json) { id: id
common.jsonStream(rules); }, function onRules(err, rules) {
} else { if (err) {
var columns = COLUMNS_DEFAULT; cb(err);
return;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_LONG;
} }
columns = columns.toLowerCase().split(','); if (opts.json) {
var sort = opts.s.toLowerCase().split(','); common.jsonStream(rules);
} else {
var columns = COLUMNS_DEFAULT;
if (columns.indexOf('shortid') !== -1) { if (opts.o) {
rules.forEach(function (rule) { columns = opts.o;
rule.shortid = common.normShortId(rule.id); } 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
}); });
} }
cb();
tabula(rules, { });
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
cb();
}); });
} }

View File

@ -19,15 +19,21 @@ function do_get(subcmd, opts, args, cb) {
return cb(new Error('invalid args: ' + args)); return cb(new Error('invalid args: ' + args));
} }
this.top.tritonapi.getInstance(args[0], function (err, inst) { var tritonapi = this.top.tritonapi;
if (inst) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (opts.json) { if (setupErr) {
console.log(JSON.stringify(inst)); cb(setupErr);
} else {
console.log(JSON.stringify(inst, null, 4));
}
} }
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);
});
}); });
} }

View File

@ -12,6 +12,7 @@
var format = require('util').format; var format = require('util').format;
var common = require('../common');
var errors = require('../errors'); var errors = require('../errors');
@ -29,20 +30,25 @@ function do_ip(subcmd, opts, args, cb) {
var cli = this.top; var cli = this.top;
cli.tritonapi.getInstance(args[0], function (err, inst) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (err) { if (setupErr) {
cb(err); cb(setupErr);
return;
} }
cli.tritonapi.getInstance(args[0], function (err, inst) {
if (err) {
cb(err);
return;
}
if (!inst.primaryIp) { if (!inst.primaryIp) {
cb(new errors.TritonError(format( cb(new errors.TritonError(format(
'primaryIp not found for instance "%s"', args[0]))); 'primaryIp not found for instance "%s"', args[0])));
return; return;
} }
console.log(inst.primaryIp); console.log(inst.primaryIp);
cb(); cb();
});
}); });
} }

View File

@ -74,84 +74,93 @@ function do_list(subcmd, opts, args, callback) {
var imgs = []; var imgs = [];
var insts; var insts;
vasync.parallel({funcs: [ common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
function getTheImages(next) { if (setupErr) {
self.top.tritonapi.listImages({ callback(setupErr);
state: 'all', }
useCache: true vasync.parallel({funcs: [
}, function (err, _imgs) { function getTheImages(next) {
if (err) { self.top.tritonapi.listImages({
if (err.statusCode === 403) { state: 'all',
/* useCache: true
* This could be a authorization error due to RBAC }, function (err, _imgs) {
* on a subuser. We don't want to fail `triton inst ls` if (err) {
* if the subuser can ListMachines, but not ListImages. if (err.statusCode === 403) {
*/ /*
log.debug(err, * This could be a authorization error due
'authz error listing images for insts info'); * to RBAC on a subuser. We don't want to
next(); * 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 { } 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) { function (err, _insts) {
if (err) { if (err) {
next(err); next(err);
} else { } else {
insts = _insts; insts = _insts;
next(); 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" // Add extra fields for nice output.
var imgmap = {}; var now = new Date();
imgs.forEach(function (img) { insts.forEach(function (inst) {
imgmap[img.id] = format('%s@%s', img.name, img.version); var created = new Date(inst.created);
}); inst.age = common.longAgo(created, now);
inst.img = imgmap[inst.image] ||
// Add extra fields for nice output. common.uuidToShortId(inst.image);
var now = new Date(); inst.shortid = inst.id.split('-', 1)[0];
insts.forEach(function (inst) { var flags = [];
var created = new Date(inst.created); if (inst.docker) flags.push('D');
inst.age = common.longAgo(created, now); if (inst.firewall_enabled) flags.push('F');
inst.img = imgmap[inst.image] || common.uuidToShortId(inst.image); if (inst.brand === 'kvm') flags.push('K');
inst.shortid = inst.id.split('-', 1)[0]; inst.flags = flags.length ? flags.join('') : undefined;
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
}); });
}
callback(); if (opts.json) {
common.jsonStream(insts);
} else {
tabula(insts, {
skipHeader: opts.H,
columns: columns,
sort: sort,
dottedLookup: true
});
}
callback();
});
}); });
} }

View File

@ -47,7 +47,8 @@ function do_create(subcmd, opts, args, cb) {
createOpts.name = opts.name; createOpts.name = opts.name;
} }
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function createSnapshot(ctx, next) { function createSnapshot(ctx, next) {
ctx.start = Date.now(); ctx.start = Date.now();

View File

@ -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) { function confirm(_, next) {
if (opts.force) { if (opts.force) {
return next(); return next();

View File

@ -36,22 +36,27 @@ function do_get(subcmd, opts, args, cb) {
var name = args[1]; var name = args[1];
var cli = this.top; var cli = this.top;
cli.tritonapi.getInstanceSnapshot({ common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
id: id, if (setupErr) {
name: name cb(setupErr);
}, function onSnapshot(err, snapshot) {
if (err) {
cb(err);
return;
} }
cli.tritonapi.getInstanceSnapshot({
id: id,
name: name
}, function onSnapshot(err, snapshot) {
if (err) {
cb(err);
return;
}
if (opts.json) { if (opts.json) {
console.log(JSON.stringify(snapshot)); console.log(JSON.stringify(snapshot));
} else { } else {
console.log(JSON.stringify(snapshot, null, 4)); console.log(JSON.stringify(snapshot, null, 4));
} }
cb(); cb();
});
}); });
} }

View File

@ -40,35 +40,40 @@ function do_list(subcmd, opts, args, cb) {
var cli = this.top; var cli = this.top;
var machineId = args[0]; var machineId = args[0];
cli.tritonapi.listInstanceSnapshots({ common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
id: machineId if (setupErr) {
}, function onSnapshots(err, snapshots) { cb(setupErr);
if (err) {
cb(err);
return;
} }
cli.tritonapi.listInstanceSnapshots({
if (opts.json) { id: machineId
common.jsonStream(snapshots); }, function onSnapshots(err, snapshots) {
} else { if (err) {
var columns = COLUMNS_DEFAULT; cb(err);
return;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_DEFAULT;
} }
columns = columns.split(','); if (opts.json) {
var sort = opts.s.split(','); common.jsonStream(snapshots);
} else {
var columns = COLUMNS_DEFAULT;
tabula(snapshots, { if (opts.o) {
skipHeader: opts.H, columns = opts.o;
columns: columns, } else if (opts.long) {
sort: sort columns = COLUMNS_DEFAULT;
}); }
}
cb(); columns = columns.split(',');
var sort = opts.s.split(',');
tabula(snapshots, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
cb();
});
}); });
} }

View File

@ -38,52 +38,59 @@ function do_ssh(subcmd, opts, args, callback) {
id = id.substr(i + 1); id = id.substr(i + 1);
} }
cli.tritonapi.getInstance(id, function (err, inst) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (err) { if (setupErr) {
callback(err); callback(setupErr);
return;
} }
cli.tritonapi.getInstance(id, function (err, inst) {
if (err) {
callback(err);
return;
}
var ip = inst.primaryIp; var ip = inst.primaryIp;
if (!ip) { if (!ip) {
callback(new Error('primaryIp not found for instance')); callback(new Error('primaryIp not found for instance'));
return; 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 * By default we disable ControlMaster (aka mux, aka SSH connection
* option, a `ControlPath` option (from ~/.ssh/config) will still * multiplexing) because of
* be used if it exists. Our hack is to set a ControlPath we * https://github.com/joyent/node-triton/issues/52
* 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( if (!opts.no_disable_mux) {
cli.tritonapi.config._configDir, 'tmp', 'nullSshControlPath'); /*
args = [ * A simple `-o ControlMaster=no` doesn't work. With
'-o', 'ControlMaster=no', * just that option, a `ControlPath` option (from
'-o', 'ControlPath='+nullSshControlPath * ~/.ssh/config) will still be used if it exists. Our
].concat(args); * 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'); self.top.log.info({args: args}, 'forking ssh');
var child = spawn('ssh', args, {stdio: 'inherit'}); var child = spawn('ssh', args, {stdio: 'inherit'});
child.on('close', function (code) { child.on('close', function (code) {
/* /*
* Once node 0.10 support is dropped we could instead: * Once node 0.10 support is dropped we could instead:
* process.exitCode = code; * process.exitCode = code;
* callback(); * callback();
*/ */
process.exit(code); process.exit(code);
});
}); });
}); });
} }

View File

@ -12,6 +12,7 @@
var vasync = require('vasync'); var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors'); var errors = require('../../errors');
@ -29,41 +30,46 @@ function do_delete(subcmd, opts, args, cb) {
} }
var waitTimeoutMs = opts.wait_timeout * 1000; /* seconds to ms */ var waitTimeoutMs = opts.wait_timeout * 1000; /* seconds to ms */
if (opts.all) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
self.top.tritonapi.deleteAllInstanceTags({ if (setupErr) {
id: args[0], cb(setupErr);
wait: opts.wait, }
waitTimeout: waitTimeoutMs if (opts.all) {
}, function (err) { self.top.tritonapi.deleteAllInstanceTags({
console.log('Deleted all tags on instance %s', args[0]); id: args[0],
cb(err); wait: opts.wait,
}); waitTimeout: waitTimeoutMs
} else { }, function (err) {
// Uniq'ify the given names. console.log('Deleted all tags on instance %s', args[0]);
var names = {}; cb(err);
args.slice(1).forEach(function (arg) { names[arg] = true; }); });
names = Object.keys(names); } 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 // TODO: Instead of waiting for each delete, let's delete
// wait for the set. // them all then wait for the set.
vasync.forEachPipeline({ vasync.forEachPipeline({
inputs: names, inputs: names,
func: function deleteOne(name, next) { func: function deleteOne(name, next) {
self.top.tritonapi.deleteInstanceTag({ self.top.tritonapi.deleteInstanceTag({
id: args[0], id: args[0],
tag: name, tag: name,
wait: opts.wait, wait: opts.wait,
waitTimeout: waitTimeoutMs waitTimeout: waitTimeoutMs
}, function (err) { }, function (err) {
if (!err) { if (!err) {
console.log('Deleted tag %s on instance %s', console.log('Deleted tag %s on instance %s',
name, args[0]); name, args[0]);
} }
next(err); next(err);
}); });
} }
}, cb); }, cb);
} }
});
} }
do_delete.options = [ do_delete.options = [

View File

@ -10,6 +10,7 @@
* `triton instance tag get ...` * `triton instance tag get ...`
*/ */
var common = require('../../common');
var errors = require('../../errors'); var errors = require('../../errors');
@ -23,20 +24,25 @@ function do_get(subcmd, opts, args, cb) {
return; return;
} }
self.top.tritonapi.getInstanceTag({ common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
id: args[0], if (setupErr) {
tag: args[1] cb(setupErr);
}, function (err, value) {
if (err) {
cb(err);
return;
} }
if (opts.json) { self.top.tritonapi.getInstanceTag({
console.log(JSON.stringify(value)); id: args[0],
} else { tag: args[1]
console.log(value); }, function (err, value) {
} if (err) {
cb(); cb(err);
return;
}
if (opts.json) {
console.log(JSON.stringify(value));
} else {
console.log(value);
}
cb();
});
}); });
} }

View File

@ -10,6 +10,7 @@
* `triton instance tag list ...` * `triton instance tag list ...`
*/ */
var common = require('../../common');
var errors = require('../../errors'); var errors = require('../../errors');
function do_list(subcmd, opts, args, cb) { function do_list(subcmd, opts, args, cb) {
@ -22,17 +23,23 @@ function do_list(subcmd, opts, args, cb) {
return; return;
} }
self.top.tritonapi.listInstanceTags({id: args[0]}, function (err, tags) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (err) { if (setupErr) {
cb(err); cb(setupErr);
return;
} }
if (opts.json) { self.top.tritonapi.listInstanceTags(
console.log(JSON.stringify(tags)); {id: args[0]}, function (err, tags) {
} else { if (err) {
console.log(JSON.stringify(tags, null, 4)); cb(err);
} return;
cb(); }
if (opts.json) {
console.log(JSON.stringify(tags));
} else {
console.log(JSON.stringify(tags, null, 4));
}
cb();
});
}); });
} }

View File

@ -12,6 +12,7 @@
var vasync = require('vasync'); var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors'); var errors = require('../../errors');
var mat = require('../../metadataandtags'); var mat = require('../../metadataandtags');
@ -27,7 +28,8 @@ function do_replace_all(subcmd, opts, args, cb) {
} }
var log = self.log; var log = self.log;
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function gatherTags(ctx, next) { function gatherTags(ctx, next) {
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) { mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
if (err) { if (err) {

View File

@ -12,6 +12,7 @@
var vasync = require('vasync'); var vasync = require('vasync');
var common = require('../../common');
var errors = require('../../errors'); var errors = require('../../errors');
var mat = require('../../metadataandtags'); var mat = require('../../metadataandtags');
@ -27,7 +28,8 @@ function do_set(subcmd, opts, args, cb) {
} }
var log = self.log; var log = self.log;
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function gatherTags(ctx, next) { function gatherTags(ctx, next) {
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) { mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
if (err) { if (err) {

View File

@ -12,6 +12,7 @@
var vasync = require('vasync'); var vasync = require('vasync');
var common = require('../common');
var distractions = require('../distractions'); var distractions = require('../distractions');
var errors = require('../errors'); var errors = require('../errors');
@ -34,7 +35,8 @@ function do_wait(subcmd, opts, args, cb) {
var done = 0; var done = 0;
var instFromId = {}; var instFromId = {};
vasync.pipeline({funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function getInsts(_, next) { function getInsts(_, next) {
vasync.forEachParallel({ vasync.forEachParallel({
inputs: ids, inputs: ids,

View File

@ -83,8 +83,6 @@ function gen_do_ACTION(opts) {
function _doTheAction(action, subcmd, opts, args, callback) { function _doTheAction(action, subcmd, opts, args, callback) {
var self = this; var self = this;
var now = Date.now();
var command, state; var command, state;
switch (action) { switch (action) {
case 'start': case 'start':
@ -116,7 +114,17 @@ function _doTheAction(action, subcmd, opts, args, callback) {
callback(new errors.UsageError('missing INST arg(s)')); callback(new errors.UsageError('missing INST arg(s)'));
return; 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({ vasync.forEachParallel({
func: function (arg, cb) { func: function (arg, cb) {
var alias, uuid; var alias, uuid;
@ -190,7 +198,7 @@ function _doTheAction(action, subcmd, opts, args, callback) {
}); });
} }
}, },
inputs: args inputs: instances
}, function (err, results) { }, function (err, results) {
var e = err ? (new Error('command failure')) : null; var e = err ? (new Error('command failure')) : null;
callback(e); callback(e);

View File

@ -40,7 +40,8 @@ function do_add(subcmd, opts, args, cb) {
var filePath = args[0]; var filePath = args[0];
var cli = this.top; var cli = this.top;
vasync.pipeline({arg: {}, funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function gatherDataStdin(ctx, next) { function gatherDataStdin(ctx, next) {
if (filePath !== '-') { if (filePath !== '-') {
return next(); return next();

View File

@ -35,7 +35,8 @@ function do_delete(subcmd, opts, args, cb) {
var cli = this.top; var cli = this.top;
vasync.pipeline({funcs: [ vasync.pipeline({arg: {cli: this.top}, funcs: [
common.cliSetupTritonApi,
function confirm(_, next) { function confirm(_, next) {
if (opts.yes) { if (opts.yes) {
return next(); return next();

View File

@ -35,22 +35,27 @@ function do_get(subcmd, opts, args, cb) {
var id = args[0]; var id = args[0];
var cli = this.top; var cli = this.top;
cli.tritonapi.cloudapi.getKey({ common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
// Currently `cloudapi.getUserKey` isn't picky about the `name` being if (setupErr) {
// passed in as the `opts.fingerprint` arg. cb(setupErr);
fingerprint: id
}, function onKey(err, key) {
if (err) {
cb(err);
return;
} }
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) { if (opts.json) {
console.log(JSON.stringify(key)); console.log(JSON.stringify(key));
} else { } else {
console.log(common.chomp(key.key)); console.log(common.chomp(key.key));
} }
cb(); cb();
});
}); });
} }

View File

@ -37,37 +37,42 @@ function do_list(subcmd, opts, args, cb) {
var cli = this.top; var cli = this.top;
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (err) { if (setupErr) {
cb(err); cb(setupErr);
return;
} }
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) {
if (opts.json) { if (err) {
common.jsonStream(keys); cb(err);
} else if (opts.authorized_keys) { return;
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;
} }
columns = columns.split(','); if (opts.json) {
var sort = opts.s.split(','); 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, { if (opts.o) {
skipHeader: false, columns = opts.o;
columns: columns, } else if (opts.long) {
sort: sort columns = COLUMNS_LONG;
}); }
}
cb(); columns = columns.split(',');
var sort = opts.s.split(',');
tabula(keys, {
skipHeader: false,
columns: columns,
sort: sort
});
}
cb();
});
}); });
} }

View File

@ -25,17 +25,24 @@ function do_get(subcmd, opts, args, cb) {
'incorrect number of args (%d)', args.length))); 'incorrect number of args (%d)', args.length)));
} }
this.top.tritonapi.getNetwork(args[0], function (err, net) { var tritonapi = this.top.tritonapi;
if (err) {
return cb(err);
}
if (opts.json) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
console.log(JSON.stringify(net)); if (setupErr) {
} else { cb(setupErr);
console.log(JSON.stringify(net, null, 4));
} }
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();
});
}); });
} }

View File

@ -49,28 +49,34 @@ function do_list(subcmd, opts, args, callback) {
columns = columns.split(','); columns = columns.split(',');
var sort = opts.s.split(','); var sort = opts.s.split(',');
var tritonapi = this.top.tritonapi;
this.top.tritonapi.cloudapi.listNetworks(function (err, networks) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (err) { if (setupErr) {
callback(err); callback(setupErr);
return;
} }
tritonapi.cloudapi.listNetworks(function (err, networks) {
if (opts.json) { if (err) {
common.jsonStream(networks); callback(err);
} else { return;
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, if (opts.json) {
columns: columns, common.jsonStream(networks);
sort: sort } else {
}); for (var i = 0; i < networks.length; i++) {
} var net = networks[i];
callback(); net.shortid = net.id.split('-', 1)[0];
net.vlan = net.vlan_id;
}
tabula(networks, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
callback();
});
}); });
} }

View File

@ -12,6 +12,7 @@
var format = require('util').format; var format = require('util').format;
var common = require('../common');
var errors = require('../errors'); var errors = require('../errors');
@ -24,17 +25,23 @@ function do_get(subcmd, opts, args, callback) {
'incorrect number of args (%d)', args.length))); 'incorrect number of args (%d)', args.length)));
} }
this.top.tritonapi.getPackage(args[0], function onRes(err, pkg) { var tritonapi = this.top.tritonapi;
if (err) { common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
return callback(err); if (setupErr) {
callback(setupErr);
} }
tritonapi.getPackage(args[0], function onRes(err, pkg) {
if (err) {
return callback(err);
}
if (opts.json) { if (opts.json) {
console.log(JSON.stringify(pkg)); console.log(JSON.stringify(pkg));
} else { } else {
console.log(JSON.stringify(pkg, null, 4)); console.log(JSON.stringify(pkg, null, 4));
} }
callback(); callback();
});
}); });
} }
@ -63,7 +70,7 @@ do_get.help = [
'', '',
'Where PACKAGE is a package id (full UUID), exact name, or short id.', '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.' 'in the future. Use "-j" to explicitly get JSON output.'
/* END JSSTYLED */ /* END JSSTYLED */
].join('\n'); ].join('\n');

View File

@ -11,6 +11,7 @@
*/ */
var tabula = require('tabula'); var tabula = require('tabula');
var vasync = require('vasync');
var common = require('../common'); var common = require('../common');
@ -68,73 +69,89 @@ function do_list(subcmd, opts, args, callback) {
return; return;
} }
this.top.tritonapi.cloudapi.listPackages(listOpts, function (err, pkgs) { var context = {
if (err) { cli: this.top
callback(err); };
return; vasync.pipeline({arg: context, funcs: [
} common.cliSetupTritonApi,
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];
/* function getThem(arg, next) {
* We take a slightly "smarter" view of "group" for default arg.cli.tritonapi.cloudapi.listPackages(listOpts,
* sorting, to accomodate usage in the JPC. More recent function (err, pkgs) {
* common usage is for packages to have "foo-*" naming. if (err) {
* JPC includes package sets of yore *and* recent that don't next(err);
* use the "group" field. We secondarily separate those return;
* 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;
} }
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, { next();
skipHeader: opts.H,
columns: columns,
sort: sort
});
} }
callback(); ]}, callback);
});
} }
do_list.options = [ do_list.options = [

View File

@ -9,6 +9,7 @@ var format = require('util').format;
var fs = require('fs'); var fs = require('fs');
var sshpk = require('sshpk'); var sshpk = require('sshpk');
var vasync = require('vasync'); var vasync = require('vasync');
var auth = require('smartdc-auth');
var common = require('../common'); var common = require('../common');
var errors = require('../errors'); var errors = require('../errors');
@ -101,17 +102,15 @@ function _createProfile(opts, cb) {
'create profile: stdout is not a TTY')); 'create profile: stdout is not a TTY'));
} }
var kr = new auth.KeyRing();
var keyChoices = {};
var defaults = {}; var defaults = {};
if (ctx.copy) { if (ctx.copy) {
defaults = ctx.copy; defaults = ctx.copy;
delete defaults.name; // we don't copy a profile name delete defaults.name; // we don't copy a profile name
} else { } else {
defaults.url = 'https://us-sw-1.api.joyent.com'; 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 = [ { var fields = [ {
@ -156,11 +155,10 @@ function _createProfile(opts, cb) {
valCb(); valCb();
} }
}, { }, {
desc: 'The fingerprint of the SSH key you have registered ' + desc: 'The fingerprint of the SSH key you want to use, or ' +
'for your account. Alternatively, You may enter a local ' + 'its index in the list above. If the key you want to ' +
'path to a public or private SSH key to have the ' + 'use is not listed, make sure it is either saved in your ' +
'fingerprint calculated for you.', 'SSH keys directory or loaded into the SSH agent.',
default: defaults.keyId,
key: 'keyId', key: 'keyId',
validate: function validateKeyId(value, valCb) { validate: function validateKeyId(value, valCb) {
// First try as a fingerprint. // First try as a fingerprint.
@ -170,44 +168,14 @@ function _createProfile(opts, cb) {
} catch (fpErr) { } catch (fpErr) {
} }
// Try as a local path. // Try as a list index
var keyPath = common.tildeSync(value); if (keyChoices[value] !== undefined) {
fs.stat(keyPath, function (statErr, stats) { return valCb(null, keyChoices[value]);
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);
}
/* valCb(new Error(format(
* Save the user's explicit given key path. We will '"%s" is neither a valid fingerprint, not an index ' +
* using it later for Docker setup. Trying to use 'from the list of available keys', value)));
* 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);
});
});
} }
} ]; } ];
@ -234,11 +202,50 @@ function _createProfile(opts, cb) {
vasync.forEachPipeline({ vasync.forEachPipeline({
inputs: fields, inputs: fields,
func: function getField(field, nextField) { func: function getField(field, nextField) {
if (field.key !== 'name') console.log(); if (field.key !== 'name')
common.promptField(field, function (err, value) { console.log();
data[field.key] = value; if (field.key === 'keyId') {
nextField(err); 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) { }, function (err) {
console.log(); console.log();

View File

@ -8,6 +8,7 @@ var assert = require('assert-plus');
var auth = require('smartdc-auth'); var auth = require('smartdc-auth');
var format = require('util').format; var format = require('util').format;
var fs = require('fs'); var fs = require('fs');
var getpass = require('getpass');
var https = require('https'); var https = require('https');
var mkdirp = require('mkdirp'); var mkdirp = require('mkdirp');
var path = require('path'); var path = require('path');
@ -143,14 +144,38 @@ function profileDockerSetup(opts, cb) {
assert.optionalObject(opts.keyPaths, 'opts.keyPaths'); assert.optionalObject(opts.keyPaths, 'opts.keyPaths');
assert.func(cb, 'cb'); assert.func(cb, 'cb');
var implicit = Boolean(opts.implicit);
var cli = opts.cli; var cli = opts.cli;
var log = cli.log;
var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name}); var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name});
var implicit = Boolean(opts.implicit);
var log = cli.log;
var profile = tritonapi.profile; var profile = tritonapi.profile;
var dockerHost; 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) { function checkCloudapiStatus(arg, next) {
tritonapi.cloudapi.ping({}, function (err, pong, res) { tritonapi.cloudapi.ping({}, function (err, pong, res) {
if (!res) { if (!res) {
@ -222,69 +247,16 @@ function profileDockerSetup(opts, cb) {
next(); next();
}, },
function findSshPrivKey_keyPaths(arg, next) { function checkSshPrivKey(arg, next) {
if (!opts.keyPaths) { try {
next(); 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; return;
} }
next();
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();
}
});
}, },
/* /*
@ -348,31 +320,32 @@ function profileDockerSetup(opts, cb) {
}, },
function genClientCert_key(arg, next) { function genClientCert_key(arg, next) {
arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem'); arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem');
common.execPlus({ var data = tritonapi.keyPair.getPrivateKey().toBuffer('pkcs1');
cmd: format('openssl rsa -in %s -out %s -outform pem', fs.writeFile(arg.keyPath, data, function (err) {
arg.sshKeyPaths.private, arg.keyPath), if (err) {
log: log next(new errors.SetupError(err, format(
}, next); 'error writing file %s', arg.keyPath)));
}, } else {
function genClientCert_csr(arg, next) { 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);
}, },
function genClientCert_cert(arg, next) { function genClientCert_cert(arg, next) {
arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem'); arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem');
common.execPlus({
cmd: format( var privKey = tritonapi.keyPair.getPrivateKey();
'openssl x509 -req -days 365 -in %s -signkey %s -out %s', var id = sshpk.identityFromDN('CN=' + profile.account);
arg.csrPath, arg.keyPath, arg.certPath), var cert = sshpk.createSelfSignedCertificate(id, privKey);
log: log var data = cert.toBuffer('pem');
}, next);
}, fs.writeFile(arg.certPath, data, function (err) {
function genClientCert_deleteCsr(arg, next) { if (err) {
rimraf(arg.csrPath, next); next(new errors.SetupError(err, format(
'error writing file %s', arg.keyPath)));
} else {
next();
}
});
}, },
function getServerCa(arg, next) { function getServerCa(arg, next) {

View File

@ -31,38 +31,44 @@ function do_services(subcmd, opts, args, callback) {
var columns = opts.o.split(','); var columns = opts.o.split(',');
var sort = opts.s.split(','); var sort = opts.s.split(',');
var tritonapi = this.tritonapi;
this.tritonapi.cloudapi.listServices(function (err, services) { common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
if (err) { if (setupErr) {
callback(err); callback(setupErr);
return;
} }
tritonapi.cloudapi.listServices(function (err, services) {
if (err) {
callback(err);
return;
}
if (opts.json) { if (opts.json) {
console.log(JSON.stringify(services)); console.log(JSON.stringify(services));
} else { } else {
/* /*
* services are returned in the form of: * services are returned in the form of:
* {name: 'endpoint', name2: 'endpoint2', ...} * {name: 'endpoint', name2: 'endpoint2', ...}
* we "normalize" them for use by tabula and JSON stream * we "normalize" them for use by tabula and JSON stream
* by making them an array * by making them an array
*/ */
var svcs = []; var svcs = [];
Object.keys(services).forEach(function (key) { Object.keys(services).forEach(function (key) {
svcs.push({ svcs.push({
name: key, name: key,
endpoint: services[key] endpoint: services[key]
});
}); });
}); tabula(svcs, {
tabula(svcs, { skipHeader: opts.H,
skipHeader: opts.H, columns: columns,
columns: columns, sort: sort,
sort: sort, dottedLookup: true
dottedLookup: true });
}); }
}
callback(); callback();
});
}); });
} }

View File

@ -5,64 +5,112 @@
*/ */
/* /*
* Copyright 2015 Joyent, Inc. * Copyright 2016 Joyent, Inc.
*/ */
var assert = require('assert-plus'); var assert = require('assert-plus');
var vasync = require('vasync');
var bunyannoop = require('./bunyannoop'); var bunyannoop = require('./bunyannoop');
var mod_common = require('./common');
var mod_config = require('./config'); 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 * Client preparation is a 3-step process:
* specified. Examples:
* *
* var triton = require('triton'); * 1. create the client object;
* var client = triton.createClient({ * 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: { * profile: {
* url: "<cloudapi url>", * url: "<cloudapi url>",
* account: "<account login for this cloud>", * account: "<account login for this cloud>",
* keyId: "<ssh key fingerprint for one of account's keys>" * keyId: "<ssh key fingerprint for one of account's keys>"
* } * }
* }); * }, function (err, client) { ... });
* -- *
* // Loading a profile from the environment (the `TRITON_*` and/or * // Loading a profile from the environment (the `TRITON_*` and/or
* // `SDC_*` environment variables). * // `SDC_*` environment variables).
* var client = triton.createClient({profileName: 'env'}); * triton.createClient({profileName: 'env'},
* -- * function (err, client) { ... });
* var client = triton.createClient({ *
* configDir: '~/.triton', // use the CLI's config dir ... * // Use one of the named profiles from the `triton` CLI.
* profileName: 'east1' // ... to find named profiles * triton.createClient({
* }); * configDir: '~/.triton',
* -- * profileName: 'east1'
* }, function (err, client) { ... });
*
* // The same thing using the underlying APIs. * // The same thing using the underlying APIs.
* var client = triton.createClient({ * triton.createClient({
* config: triton.loadConfig({configDir: '~/.triton'}, * config: triton.loadConfig({configDir: '~/.triton'}),
* profile: triton.loadProfile({name: 'east1', configDir: '~/.triton'}) * profile: triton.loadProfile({name: 'east1', configDir: '~/.triton'})
* }); * }, function (err, client) { ... });
*
* 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
* });
*
* *
* TODO: The story for an app wanting to specify some Triton config but NOT * TODO: The story for an app wanting to specify some Triton config but NOT
* have to have a triton $configDir/config.json is poor. * 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 opts {Object}:
* - @param profile {Object} A *Triton profile* object that includes the * - @param profile {Object} A *Triton profile* object that includes the
* information required to connect to a CloudAPI -- minimally this: * 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`. * One may not specify both `configDir` and `config`.
* - @param log {Bunyan Logger} Optional. A Bunyan logger. If not provided, * - @param log {Bunyan Logger} Optional. A Bunyan logger. If not provided,
* a stub that does no logging will be used. * 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.object(opts, 'opts');
assert.optionalObject(opts.profile, 'opts.profile'); assert.optionalObject(opts.profile, 'opts.profile');
assert.optionalString(opts.profileName, 'opts.profileName'); assert.optionalString(opts.profileName, 'opts.profileName');
assert.optionalObject(opts.config, 'opts.config'); assert.optionalObject(opts.config, 'opts.config');
assert.optionalString(opts.configDir, 'opts.configDir'); assert.optionalString(opts.configDir, 'opts.configDir');
assert.optionalObject(opts.log, 'opts.log'); assert.optionalObject(opts.log, 'opts.log');
assert.optionalFunc(opts.unlockKeyFn, 'opts.unlockKeyFn');
assert.func(cb, 'cb');
assert.ok(!(opts.profile && opts.profileName), assert.ok(!(opts.profile && opts.profileName),
'cannot specify both opts.profile and 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"'); 'must provide opts.configDir for opts.profileName!="env"');
} }
var log = opts.log; var log;
if (!opts.log) { var client;
log = new bunyannoop.BunyanNoopLogger();
}
var config = opts.config; vasync.pipeline({funcs: [
if (!config) { function theSyncPart(_, next) {
config = mod_config.loadConfig({configDir: opts.configDir}); log = opts.log || new bunyannoop.BunyanNoopLogger();
}
var profile = opts.profile; var config;
if (!profile) { if (opts.config) {
profile = mod_config.loadProfile({ config = opts.config;
name: opts.profileName, } else {
configDir: config._configDir try {
}); config = mod_config.loadConfig(
} {configDir: opts.configDir});
// Don't require one to arbitrarily have a profile.name if manually } catch (configErr) {
// creating it. next(configErr);
if (!profile.name) { return;
// TODO: might want this to be hash or slug of profile params. }
profile.name = '_'; }
}
mod_config.validateProfile(profile);
var client = tritonapi.createClient({ var profile;
log: log, if (opts.profile) {
config: config, profile = opts.profile;
profile: 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 = { module.exports = {
createClient: createClient, createClient: createClient,
promptPassphraseUnlockKey: mod_common.promptPassphraseUnlockKey,
/** /**
* `createClient` provides convenience parameters to not *have* to call * `createClient` provides convenience parameters to not *have* to call
@ -159,7 +262,10 @@ module.exports = {
loadProfile: mod_config.loadProfile, loadProfile: mod_config.loadProfile,
loadAllProfiles: mod_config.loadAllProfiles, loadAllProfiles: mod_config.loadAllProfiles,
createTritonApiClient: tritonapi.createClient, /*
// For those wanting a lower-level raw CloudAPI client. * For those wanting a lower-level TritonApi createClient, or an
createCloudApiClient: require('./cloudapi2').createClient * even *lower*-level raw CloudAPI client.
*/
createTritonApiClient: mod_tritonapi.createClient,
createCloudApiClient: mod_cloudapi2.createClient
}; };

View File

@ -6,10 +6,81 @@
/* /*
* Copyright 2016 Joyent, Inc. * 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 assert = require('assert-plus');
var auth = require('smartdc-auth'); var auth = require('smartdc-auth');
var EventEmitter = require('events').EventEmitter; var EventEmitter = require('events').EventEmitter;
@ -24,6 +95,7 @@ var restifyBunyanSerializers =
require('restify-clients/lib/helpers/bunyan').serializers; require('restify-clients/lib/helpers/bunyan').serializers;
var tabula = require('tabula'); var tabula = require('tabula');
var vasync = require('vasync'); var vasync = require('vasync');
var sshpk = require('sshpk');
var cloudapi = require('./cloudapi2'); var cloudapi = require('./cloudapi2');
var common = require('./common'); var common = require('./common');
@ -116,6 +188,14 @@ function _stepFwRuleId(arg, next) {
/** /**
* Create a TritonApi client. * 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} * @param opts {Object}
* - log {Bunyan Logger} * - log {Bunyan Logger}
* ... * ...
@ -128,6 +208,7 @@ function TritonApi(opts) {
this.profile = opts.profile; this.profile = opts.profile;
this.config = opts.config; this.config = opts.config;
this.keyPair = null;
// Make sure a given bunyan logger has reasonable client_re[qs] serializers. // Make sure a given bunyan logger has reasonable client_re[qs] serializers.
// Note: This was fixed in restify, then broken again in // Note: This was fixed in restify, then broken again in
@ -147,29 +228,43 @@ function TritonApi(opts) {
this.config.cacheDir, this.config.cacheDir,
common.profileSlug(this.profile)); common.profileSlug(this.profile));
this.log.trace({cacheDir: this.cacheDir}, 'cache dir'); 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() { TritonApi.prototype.close = function close() {
this.cloudapi.close(); if (this.cloudapi) {
delete this.cloudapi; this.cloudapi.close();
delete this.cloudapi;
}
}; };
TritonApi.prototype._cloudapiFromProfile = TritonApi.prototype.init = function init(cb) {
function _cloudapiFromProfile(profile) 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.object(profile, 'profile');
assert.string(profile.account, 'profile.account'); assert.string(profile.account, 'profile.account');
assert.optionalString(profile.actAsAccount, 'profile.actAsAccount'); assert.optionalString(profile.actAsAccount, 'profile.actAsAccount');
@ -185,32 +280,39 @@ TritonApi.prototype._cloudapiFromProfile =
? true : !profile.insecure); ? true : !profile.insecure);
var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION; var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION;
var sign; var opts = {
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({
url: profile.url, url: profile.url,
account: profile.actAsAccount || profile.account, account: profile.actAsAccount || profile.account,
user: profile.user, principal: {
account: profile.account,
user: profile.user
},
roles: profile.roles, roles: profile.roles,
version: acceptVersion, version: acceptVersion,
rejectUnauthorized: rejectUnauthorized, rejectUnauthorized: rejectUnauthorized,
sign: sign,
log: this.log 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);
});
}
}; };

View File

@ -10,6 +10,7 @@
"bunyan": "1.5.1", "bunyan": "1.5.1",
"cmdln": "4.1.2", "cmdln": "4.1.2",
"extsprintf": "1.0.2", "extsprintf": "1.0.2",
"getpass": "0.1.6",
"lomstream": "1.1.0", "lomstream": "1.1.0",
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
"node-uuid": "1.4.3", "node-uuid": "1.4.3",
@ -19,8 +20,9 @@
"restify-errors": "3.0.0", "restify-errors": "3.0.0",
"rimraf": "2.4.4", "rimraf": "2.4.4",
"semver": "5.1.0", "semver": "5.1.0",
"smartdc-auth": "git+https://github.com/joyent/node-smartdc-auth.git#05d9077", "smartdc-auth": "2.5.2",
"sshpk": "1.7.x", "sshpk": "1.10.1",
"sshpk-agent": "1.4.2",
"strsplit": "1.0.0", "strsplit": "1.0.0",
"tabula": "1.7.0", "tabula": "1.7.0",
"vasync": "1.6.3", "vasync": "1.6.3",

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2015, Joyent, Inc. * Copyright 2016 Joyent, Inc.
*/ */
/* /*
@ -29,9 +29,11 @@ test('TritonApi images', function (tt) {
var client; var client;
tt.test(' setup: client', function (t) { tt.test(' setup: client', function (t) {
client = h.createClient(); h.createClient(function (err, client_) {
t.ok(client, 'client'); t.error(err);
t.end(); client = client_;
t.end();
});
}); });
var testOpts = {}; var testOpts = {};

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2015, Joyent, Inc. * Copyright 2016 Joyent, Inc.
*/ */
/* /*
@ -15,24 +15,26 @@
var h = require('./helpers'); var h = require('./helpers');
var test = require('tape'); var test = require('tape');
var common = require('../../lib/common');
// --- Globals // --- Globals
var CLIENT; var CLIENT;
var INST; var INST;
// --- Tests // --- Tests
test('TritonApi packages', function (tt) { 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) { CLIENT.cloudapi.listMachines(function (err, insts) {
if (h.ifErr(t, err)) if (h.ifErr(t, err))
return t.end(); return t.end();

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2015, Joyent, Inc. * Copyright 2016 Joyent, Inc.
*/ */
/* /*
@ -15,28 +15,28 @@
var h = require('./helpers'); var h = require('./helpers');
var test = require('tape'); var test = require('tape');
var common = require('../../lib/common');
// --- Globals // --- Globals
var CLIENT; var CLIENT;
var NET; var NET;
// --- Tests // --- Tests
test('TritonApi networks', function (tt) { test('TritonApi networks', function (tt) {
tt.test(' setup', function (t) { tt.test(' setup', function (t) {
CLIENT = h.createClient(); h.createClient(function (err, client_) {
t.ok(CLIENT, 'client'); t.error(err);
CLIENT = client_;
t.end();
});
});
tt.test(' setup: net', function (t) {
var opts = { var opts = {
account: CLIENT.profile.account account: CLIENT.profile.account
}; };
CLIENT.cloudapi.listNetworks(opts, function (err, nets) { CLIENT.cloudapi.listNetworks(opts, function (err, nets) {
if (h.ifErr(t, err)) if (h.ifErr(t, err))
return t.end(); return t.end();

View File

@ -5,7 +5,7 @@
*/ */
/* /*
* Copyright (c) 2015, Joyent, Inc. * Copyright 2016 Joyent, Inc.
*/ */
/* /*
@ -30,9 +30,14 @@ var PKG;
test('TritonApi packages', function (tt) { test('TritonApi packages', function (tt) {
tt.test(' setup', function (t) { tt.test(' setup', function (t) {
CLIENT = h.createClient(); h.createClient(function (err, client_) {
t.ok(CLIENT, 'client'); t.error(err);
CLIENT = client_;
t.end();
});
});
tt.test(' setup: pkg', function (t) {
CLIENT.cloudapi.listPackages(function (err, pkgs) { CLIENT.cloudapi.listPackages(function (err, pkgs) {
if (h.ifErr(t, err)) if (h.ifErr(t, err))
return t.end(); return t.end();

View File

@ -248,12 +248,14 @@ function jsonStreamParse(s) {
/* /*
* Create a TritonApi client using the CLI. * Create a TritonApi client using the CLI.
*/ */
function createClient() { function createClient(cb) {
return mod_triton.createClient({ assert.func(cb, 'cb');
mod_triton.createClient({
log: LOG, log: LOG,
profile: CONFIG.profile, profile: CONFIG.profile,
configDir: '~/.triton' // piggy-back on Triton CLI config dir configDir: '~/.triton' // piggy-back on Triton CLI config dir
}); }, cb);
} }