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:
parent
696439f1ae
commit
ad7d608011
77
CHANGES.md
77
CHANGES.md
@ -7,6 +7,83 @@ Known issues:
|
||||
|
||||
## not yet released
|
||||
|
||||
- **BREAKING CHANGE for module usage of node-triton.**
|
||||
To implement joyent/node-triton#108, the way a TritonApi client is
|
||||
setup for use has changed from being (unrealistically) sync to async.
|
||||
|
||||
Client preparation is now a multi-step process:
|
||||
|
||||
1. create the client object;
|
||||
2. initialize it (mainly involves finding the SSH key identified by the
|
||||
`keyId`); and,
|
||||
3. optionally unlock the SSH key (if it is passphrase-protected and not in
|
||||
an ssh-agent).
|
||||
|
||||
`createClient` has changed to take a callback argument. It will create and
|
||||
init the client (steps 1 and 2) and takes an optional `unlockKeyFn` parameter
|
||||
to handle step 3. A new `mod_triton.promptPassphraseUnlockKey` export can be
|
||||
used for `unlockKeyFn` for command-line tools to handle prompting for a
|
||||
passphrase on stdin, if required. Therefore what used to be:
|
||||
|
||||
var mod_triton = require('triton');
|
||||
try {
|
||||
var client = mod_triton.createClient({ # No longer works.
|
||||
profileName: 'env'
|
||||
});
|
||||
} catch (initErr) {
|
||||
// handle err
|
||||
}
|
||||
|
||||
// use `client`
|
||||
|
||||
is now:
|
||||
|
||||
var mod_triton = require('triton');
|
||||
mod_triton.createClient({
|
||||
profileName: 'env',
|
||||
unlockKeyFn: triton.promptPassphraseUnlockKey
|
||||
}, function (err, client) {
|
||||
if (err) {
|
||||
// handle err
|
||||
}
|
||||
|
||||
// use `client`
|
||||
});
|
||||
|
||||
See [the examples/ directory](examples/) for more complete examples.
|
||||
|
||||
Low-level/raw handling of the three steps above is possible as follows
|
||||
(error handling is elided):
|
||||
|
||||
var mod_bunyan = require('bunyan');
|
||||
var mod_triton = require('triton');
|
||||
|
||||
// 1. create
|
||||
var client = mod_triton.createTritonApiClient({
|
||||
log: mod_bunyan.createLogger({name: 'my-tool'}),
|
||||
config: {},
|
||||
profile: mod_triton.loadProfile('env')
|
||||
});
|
||||
|
||||
// 2. init
|
||||
client.init(function (initErr) {
|
||||
// 3. unlock key
|
||||
// See top-comment in "lib/tritonapi.js".
|
||||
});
|
||||
|
||||
- [joyent/node-triton#108] Support for passphrase-protected private keys.
|
||||
Before this work, an encrypted private SSH key (i.e. protected by a
|
||||
passphrase) would have to be loaded in an ssh-agent for the `triton`
|
||||
CLI to use it. Now `triton` will prompt for the passphrase to unlock
|
||||
the private key (in memory), if needed. For example:
|
||||
|
||||
$ triton package list
|
||||
Enter passphrase for id_rsa:
|
||||
SHORTID NAME MEMORY SWAP DISK VCPUS
|
||||
14ad9d54 g4-highcpu-128M 128M 512M 3G -
|
||||
14ae2634 g4-highcpu-256M 256M 1G 5G -
|
||||
...
|
||||
|
||||
- [joyent/node-triton#143] Fix duplicate output from 'triton rbac key ...'.
|
||||
|
||||
## 4.15.0
|
||||
|
51
README.md
51
README.md
@ -234,19 +234,27 @@ documentation](https://apidocs.joyent.com/docker) for more information.)
|
||||
## `TritonApi` Module Usage
|
||||
|
||||
Node-triton can also be used as a node module for your own node.js tooling.
|
||||
A basic example:
|
||||
A basic example appropriate for a command-line tool is:
|
||||
|
||||
var triton = require('triton');
|
||||
```javascript
|
||||
var mod_bunyan = require('bunyan');
|
||||
var mod_triton = require('triton');
|
||||
|
||||
// See `createClient` block comment for full usage details:
|
||||
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
|
||||
var client = triton.createClient({
|
||||
profile: {
|
||||
url: URL,
|
||||
account: ACCOUNT,
|
||||
keyId: KEY_ID
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
client.listImages(function (err, images) {
|
||||
client.close(); // Remember to close the client to close TCP conn.
|
||||
if (err) {
|
||||
@ -255,7 +263,14 @@ A basic example:
|
||||
console.log(JSON.stringify(images, null, 4));
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
See the following for more details:
|
||||
- The block-comment for `createClient` in [lib/index.js](lib/index.js).
|
||||
- Some module-usage examples in [examples/](examples/).
|
||||
- The lower-level details in the top-comment in
|
||||
[lib/tritonapi.js](lib/tritonapi.js).
|
||||
|
||||
|
||||
## Configuration
|
||||
@ -280,24 +295,6 @@ are in "etc/defaults.json" and can be overriden for the CLI in
|
||||
catching up and is much more friendly to use.
|
||||
|
||||
|
||||
## cloudapi2.js differences with node-smartdc/lib/cloudapi.js
|
||||
|
||||
The old node-smartdc module included an lib for talking directly to the SDC
|
||||
Cloud API (node-smartdc/lib/cloudapi.js). Part of this module (node-triton) is a
|
||||
re-write of the Cloud API lib with some backward incompatibilities. The
|
||||
differences and backward incompatibilities are discussed here.
|
||||
|
||||
- Currently no caching options in cloudapi2.js (this should be re-added in
|
||||
some form). The `noCache` option to many of the cloudapi.js methods will not
|
||||
be re-added, it was a wart.
|
||||
- The leading `account` option to each cloudapi.js method has been dropped. It
|
||||
was redundant for the constructor `account` option.
|
||||
- "account" is now "user" in the CloudAPI constructor.
|
||||
- All (all? at least at the time of this writing) methods in cloudapi2.js have
|
||||
a signature of `function (options, callback)` instead of the sometimes
|
||||
haphazard extra arguments.
|
||||
|
||||
|
||||
## Development Hooks
|
||||
|
||||
Before commiting be sure to, at least:
|
||||
|
@ -1,42 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Example using cloudapi2.js to call cloudapi's GetAccount endpoint.
|
||||
* Example creating a Triton API client and using it to get account info.
|
||||
*
|
||||
* Usage:
|
||||
* ./example-get-account.js | bunyan
|
||||
* ./example-get-account.js
|
||||
*
|
||||
* # With trace-level logging
|
||||
* LOG_LEVEL=trace ./example-get-account.js 2>&1 | bunyan
|
||||
*/
|
||||
|
||||
var p = console.log;
|
||||
var auth = require('smartdc-auth');
|
||||
var bunyan = require('bunyan');
|
||||
var cloudapi = require('../lib/cloudapi2');
|
||||
var path = require('path');
|
||||
var triton = require('../'); // typically `require('triton');`
|
||||
|
||||
var log = bunyan.createLogger({
|
||||
name: 'example-get-account',
|
||||
level: 'trace'
|
||||
})
|
||||
|
||||
var ACCOUNT = process.env.SDC_ACCOUNT || 'bob';
|
||||
var USER = process.env.SDC_USER;
|
||||
var KEY_ID = process.env.SDC_KEY_ID || 'b4:f0:b4:6c:18:3b:44:63:b4:4e:58:22:74:43:d4:bc';
|
||||
|
||||
var sign = auth.cliSigner({
|
||||
keyId: KEY_ID,
|
||||
user: ACCOUNT,
|
||||
log: log
|
||||
});
|
||||
var client = cloudapi.createClient({
|
||||
url: 'https://us-sw-1.api.joyent.com',
|
||||
account: ACCOUNT,
|
||||
user: USER,
|
||||
version: '*',
|
||||
sign: sign,
|
||||
agent: false, // don't want KeepAlive
|
||||
log: log
|
||||
name: path.basename(__filename),
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
stream: process.stderr
|
||||
});
|
||||
|
||||
log.info('start')
|
||||
client.getAccount(function (err, account) {
|
||||
p('getAccount: err', err)
|
||||
p('getAccount: account', account)
|
||||
triton.createClient({
|
||||
log: log,
|
||||
// Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a
|
||||
// `profile` object.
|
||||
profileName: 'env',
|
||||
unlockKeyFn: triton.promptPassphraseUnlockKey
|
||||
}, function createdClient(err, client) {
|
||||
if (err) {
|
||||
console.error('error creating Triton client: %s\n%s', err, err.stack);
|
||||
process.exitStatus = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Eventually the top-level TritonApi will have `.getAccount()`.
|
||||
client.cloudapi.getAccount(function (err, account) {
|
||||
client.close(); // Remember to close the client to close TCP conn.
|
||||
if (err) {
|
||||
console.error('getAccount error: %s\n%s', err, err.stack);
|
||||
process.exitStatus = 1;
|
||||
} else {
|
||||
console.log(JSON.stringify(account, null, 4));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,46 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Example using cloudapi2.js to call cloudapi's ListMachines endpoint.
|
||||
* Example creating a Triton API client and using it to list instances.
|
||||
*
|
||||
* Usage:
|
||||
* ./example-list-images.js | bunyan
|
||||
* ./example-list-instances.js
|
||||
*
|
||||
* # With trace-level logging
|
||||
* LOG_LEVEL=trace ./example-list-instances.js 2>&1 | bunyan
|
||||
*/
|
||||
|
||||
var p = console.log;
|
||||
var bunyan = require('bunyan');
|
||||
var path = require('path');
|
||||
var triton = require('../'); // typically `require('triton');`
|
||||
|
||||
|
||||
var URL = process.env.SDC_URL || 'https://us-sw-1.api.joyent.com';
|
||||
var ACCOUNT = process.env.SDC_ACCOUNT || 'bob';
|
||||
var KEY_ID = process.env.SDC_KEY_ID || 'b4:f0:b4:6c:18:3b:44:63:b4:4e:58:22:74:43:d4:bc';
|
||||
|
||||
|
||||
var log = bunyan.createLogger({
|
||||
name: 'test-list-instances',
|
||||
level: process.env.LOG_LEVEL || 'trace'
|
||||
name: path.basename(__filename),
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
stream: process.stderr
|
||||
});
|
||||
|
||||
/*
|
||||
* More details on `createClient` options here:
|
||||
* https://github.com/joyent/node-triton/blob/master/lib/index.js#L18-L61
|
||||
* For example, if you want to use an existing `triton` CLI profile, you can
|
||||
* pass that profile name in.
|
||||
*/
|
||||
var client = triton.createClient({
|
||||
triton.createClient({
|
||||
log: log,
|
||||
profile: {
|
||||
url: URL,
|
||||
account: ACCOUNT,
|
||||
keyId: KEY_ID
|
||||
// Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a
|
||||
// `profile` object.
|
||||
profileName: 'env',
|
||||
unlockKeyFn: triton.promptPassphraseUnlockKey
|
||||
}, function createdClient(err, client) {
|
||||
if (err) {
|
||||
console.error('error creating Triton client: %s\n%s', err, err.stack);
|
||||
process.exitStatus = 1;
|
||||
return;
|
||||
}
|
||||
});
|
||||
// TODO: Eventually the top-level TritonApi will have `.listInstances()` to use.
|
||||
|
||||
// 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 err:', err);
|
||||
console.error('listInstances error: %s\n%s', err, err.stack);
|
||||
process.exitStatus = 1;
|
||||
} else {
|
||||
console.log(JSON.stringify(insts, null, 4));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
55
lib/cli.js
55
lib/cli.js
@ -27,7 +27,7 @@ var vasync = require('vasync');
|
||||
var common = require('./common');
|
||||
var mod_config = require('./config');
|
||||
var errors = require('./errors');
|
||||
var tritonapi = require('./tritonapi');
|
||||
var lib_tritonapi = require('./tritonapi');
|
||||
|
||||
|
||||
|
||||
@ -158,7 +158,7 @@ var OPTIONS = [
|
||||
help: 'A cloudapi API version, or semver range, to attempt to use. ' +
|
||||
'This is passed in the "Accept-Version" header. ' +
|
||||
'See `triton cloudapi /--ping` to list supported versions. ' +
|
||||
'The default is "' + tritonapi.CLOUDAPI_ACCEPT_VERSION + '". ' +
|
||||
'The default is "' + lib_tritonapi.CLOUDAPI_ACCEPT_VERSION + '". ' +
|
||||
'*This is intended for development use only. It could cause ' +
|
||||
'`triton` processing of responses to break.*',
|
||||
hidden: true
|
||||
@ -302,16 +302,16 @@ CLI.prototype.init = function (opts, args, callback) {
|
||||
return self._profile;
|
||||
});
|
||||
|
||||
this.__defineGetter__('tritonapi', function getTritonapi() {
|
||||
if (self._tritonapi === undefined) {
|
||||
self._tritonapi = tritonapi.createClient({
|
||||
try {
|
||||
self.tritonapi = lib_tritonapi.createClient({
|
||||
log: self.log,
|
||||
profile: self.profile,
|
||||
config: self.config
|
||||
});
|
||||
} catch (createErr) {
|
||||
callback(createErr);
|
||||
return;
|
||||
}
|
||||
return self._tritonapi;
|
||||
});
|
||||
|
||||
if (process.env.TRITON_COMPLETE) {
|
||||
/*
|
||||
@ -326,21 +326,21 @@ CLI.prototype.init = function (opts, args, callback) {
|
||||
* Example usage:
|
||||
* TRITON_COMPLETE=images triton -p my-profile create
|
||||
*/
|
||||
this._emitCompletions(process.env.TRITON_COMPLETE, function (err) {
|
||||
self._emitCompletions(process.env.TRITON_COMPLETE, function (err) {
|
||||
callback(err || false);
|
||||
});
|
||||
} else {
|
||||
// Cmdln class handles `opts.help`.
|
||||
Cmdln.prototype.init.apply(this, arguments);
|
||||
Cmdln.prototype.init.call(self, opts, args, callback);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
CLI.prototype.fini = function fini(subcmd, err, cb) {
|
||||
this.log.trace({err: err, subcmd: subcmd}, 'cli fini');
|
||||
if (this._tritonapi) {
|
||||
this._tritonapi.close();
|
||||
delete this._tritonapi;
|
||||
if (this.tritonapi) {
|
||||
this.tritonapi.close();
|
||||
delete this.tritonapi;
|
||||
}
|
||||
cb();
|
||||
};
|
||||
@ -361,7 +361,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
||||
|
||||
var cacheFile = path.join(this.tritonapi.cacheDir, type + '.completions');
|
||||
var ttl = 5 * 60 * 1000; // timeout of cache file info (ms)
|
||||
var cloudapi = this.tritonapi.cloudapi;
|
||||
var tritonapi = this.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
function tryCacheFile(arg, next) {
|
||||
@ -377,13 +377,25 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
||||
}
|
||||
});
|
||||
},
|
||||
function initAuth(args, next) {
|
||||
tritonapi.init(function (initErr) {
|
||||
if (initErr) {
|
||||
next(initErr);
|
||||
}
|
||||
if (tritonapi.keyPair.isLocked()) {
|
||||
next(new errors.TritonError(
|
||||
'cannot unlock keys during completion'));
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
function gather(arg, next) {
|
||||
var completions;
|
||||
|
||||
switch (type) {
|
||||
case 'packages':
|
||||
cloudapi.listPackages({}, function (err, pkgs) {
|
||||
tritonapi.cloudapi.listPackages({}, function (err, pkgs) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
@ -402,7 +414,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
||||
});
|
||||
break;
|
||||
case 'images':
|
||||
cloudapi.listImages({}, function (err, imgs) {
|
||||
tritonapi.cloudapi.listImages({}, function (err, imgs) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
@ -424,7 +436,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
||||
});
|
||||
break;
|
||||
case 'instances':
|
||||
cloudapi.listMachines({}, function (err, insts) {
|
||||
tritonapi.cloudapi.listMachines({}, function (err, insts) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
@ -449,7 +461,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
||||
* on that is that with the additional prefixes, there would
|
||||
* be too many.
|
||||
*/
|
||||
cloudapi.listMachines({}, function (err, insts) {
|
||||
tritonapi.cloudapi.listMachines({}, function (err, insts) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
@ -470,7 +482,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
||||
});
|
||||
break;
|
||||
case 'networks':
|
||||
cloudapi.listNetworks({}, function (err, nets) {
|
||||
tritonapi.cloudapi.listNetworks({}, function (err, nets) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
@ -489,7 +501,8 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
||||
});
|
||||
break;
|
||||
case 'fwrules':
|
||||
cloudapi.listFirewallRules({}, function (err, fwrules) {
|
||||
tritonapi.cloudapi.listFirewallRules({}, function (err,
|
||||
fwrules) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
@ -503,7 +516,7 @@ CLI.prototype._emitCompletions = function _emitCompletions(type, cb) {
|
||||
});
|
||||
break;
|
||||
case 'keys':
|
||||
cloudapi.listKeys({}, function (err, keys) {
|
||||
tritonapi.cloudapi.listKeys({}, function (err, keys) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
@ -602,7 +615,7 @@ CLI.prototype.tritonapiFromProfileName =
|
||||
'tritonapiFromProfileName: loaded profile');
|
||||
}
|
||||
|
||||
return tritonapi.createClient({
|
||||
return lib_tritonapi.createClient({
|
||||
log: this.log,
|
||||
profile: profile,
|
||||
config: this.config
|
||||
|
@ -41,6 +41,7 @@ var os = require('os');
|
||||
var querystring = require('querystring');
|
||||
var vasync = require('vasync');
|
||||
var auth = require('smartdc-auth');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var bunyannoop = require('./bunyannoop');
|
||||
var common = require('./common');
|
||||
@ -64,10 +65,7 @@ var OS_PLATFORM = os.platform();
|
||||
*
|
||||
* @param options {Object}
|
||||
* - {String} url (required) Cloud API base url
|
||||
* - {String} account (required) The account login name.
|
||||
* - {Function} sign (required) An http-signature auth signing function
|
||||
* - {String} user (optional) The RBAC user login name.
|
||||
* - {Array of String} roles (optional) RBAC role(s) to take up.
|
||||
* - Authentication options (see below)
|
||||
* - {String} version (optional) Used for the accept-version header. This
|
||||
* defaults to '*', meaning that over time you could experience breaking
|
||||
* changes. Specifying a value is strongly recommended. E.g. '~7.1'.
|
||||
@ -78,6 +76,28 @@ var OS_PLATFORM = os.platform();
|
||||
* {Boolean} agent Set to `false` to not get KeepAlive. You want
|
||||
* this for CLIs.
|
||||
* TODO doc the backoff/retry available options
|
||||
*
|
||||
* Authentication options can be given in two ways - either with a
|
||||
* smartdc-auth KeyPair (the preferred method), or with a signer function
|
||||
* (deprecated, retained for compatibility).
|
||||
*
|
||||
* Either (prefered):
|
||||
* - {String} account (required) The account login name this cloudapi
|
||||
* client will operate upon.
|
||||
* - {Object} principal (required)
|
||||
* - {String} account (required) The account login name for
|
||||
* authentication.
|
||||
* - {Object} keyPair (required) A smartdc-auth KeyPair object
|
||||
* - {String} user (optional) RBAC sub-user login name
|
||||
* - {Array of String} roles (optional) RBAC role(s) to take up.
|
||||
*
|
||||
* Or (backwards compatible):
|
||||
* - {String} account (required) The account login name used both for
|
||||
* authentication and as the account being operated upon.
|
||||
* - {Function} sign (required) An http-signature auth signing function.
|
||||
* - {String} user (optional) The RBAC user login name.
|
||||
* - {Array of String} roles (optional) RBAC role(s) to take up.
|
||||
*
|
||||
* @throws {TypeError} on bad input.
|
||||
* @constructor
|
||||
*
|
||||
@ -90,17 +110,30 @@ function CloudApi(options) {
|
||||
assert.object(options, 'options');
|
||||
assert.string(options.url, 'options.url');
|
||||
assert.string(options.account, 'options.account');
|
||||
assert.func(options.sign, 'options.sign');
|
||||
assert.optionalString(options.user, 'options.user');
|
||||
|
||||
assert.optionalArrayOfString(options.roles, 'options.roles');
|
||||
assert.optionalString(options.version, 'options.version');
|
||||
assert.optionalObject(options.log, 'options.log');
|
||||
|
||||
assert.optionalObject(options.principal, 'options.principal');
|
||||
this.principal = options.principal;
|
||||
if (options.principal === undefined) {
|
||||
this.principal = {};
|
||||
this.principal.account = options.account;
|
||||
assert.optionalString(options.user, 'options.user');
|
||||
if (options.user !== undefined)
|
||||
this.principal.user = options.user;
|
||||
assert.func(options.sign, 'options.sign');
|
||||
this.principal.sign = options.sign;
|
||||
} else {
|
||||
assert.string(this.principal.account, 'principal.account');
|
||||
assert.object(this.principal.keyPair, 'principal.keyPair');
|
||||
assert.optionalString(this.principal.user, 'principal.user');
|
||||
}
|
||||
|
||||
this.url = options.url;
|
||||
this.account = options.account;
|
||||
this.user = options.user; // optional RBAC subuser
|
||||
this.roles = options.roles;
|
||||
this.sign = options.sign;
|
||||
this.log = options.log || new bunyannoop.BunyanNoopLogger();
|
||||
if (!options.version) {
|
||||
options.version = '*';
|
||||
@ -128,16 +161,33 @@ CloudApi.prototype.close = function close(callback) {
|
||||
this.client.close();
|
||||
};
|
||||
|
||||
CloudApi.prototype._getAuthHeaders =
|
||||
function _getAuthHeaders(method, path, callback) {
|
||||
|
||||
CloudApi.prototype._getAuthHeaders = function _getAuthHeaders(callback) {
|
||||
assert.string(method, 'method');
|
||||
assert.string(path, 'path');
|
||||
assert.func(callback, 'callback');
|
||||
var self = this;
|
||||
|
||||
var headers = {};
|
||||
|
||||
var rs = auth.requestSigner({
|
||||
sign: self.sign
|
||||
var rs;
|
||||
if (this.principal.sign !== undefined) {
|
||||
rs = auth.requestSigner({
|
||||
sign: this.principal.sign
|
||||
});
|
||||
} else if (this.principal.keyPair !== undefined) {
|
||||
try {
|
||||
rs = this.principal.keyPair.createRequestSigner({
|
||||
user: this.principal.account,
|
||||
subuser: this.principal.user
|
||||
});
|
||||
} catch (signerErr) {
|
||||
callback(new errors.SigningError(signerErr));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
rs.writeTarget(method, path);
|
||||
headers.date = rs.writeDateHeader();
|
||||
|
||||
// TODO: token auth support
|
||||
@ -222,14 +272,8 @@ CloudApi.prototype._request = function _request(opts, cb) {
|
||||
|
||||
var method = (opts.method || 'GET').toLowerCase();
|
||||
assert.ok(['get', 'post', 'put', 'delete', 'head'].indexOf(method) >= 0,
|
||||
'invalid method given');
|
||||
switch (method) {
|
||||
case 'delete':
|
||||
method = 'del';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
'invalid HTTP method given');
|
||||
var clientFnName = (method === 'delete' ? 'del' : method);
|
||||
|
||||
if (self.roles && self.roles.length > 0) {
|
||||
if (opts.path.indexOf('?') !== -1) {
|
||||
@ -239,7 +283,7 @@ CloudApi.prototype._request = function _request(opts, cb) {
|
||||
}
|
||||
}
|
||||
|
||||
self._getAuthHeaders(function (err, headers) {
|
||||
self._getAuthHeaders(method, opts.path, function (err, headers) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
@ -252,9 +296,9 @@ CloudApi.prototype._request = function _request(opts, cb) {
|
||||
headers: headers
|
||||
};
|
||||
if (opts.data)
|
||||
self.client[method](reqOpts, opts.data, cb);
|
||||
self.client[clientFnName](reqOpts, opts.data, cb);
|
||||
else
|
||||
self.client[method](reqOpts, cb);
|
||||
self.client[clientFnName](reqOpts, cb);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -12,6 +12,7 @@ var assert = require('assert-plus');
|
||||
var child_process = require('child_process');
|
||||
var crypto = require('crypto');
|
||||
var fs = require('fs');
|
||||
var getpass = require('getpass');
|
||||
var os = require('os');
|
||||
var path = require('path');
|
||||
var read = require('read');
|
||||
@ -678,6 +679,99 @@ function promptField(field, cb) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A utility method to unlock a private key on a TritonApi client instance,
|
||||
* if necessary.
|
||||
*
|
||||
* If the client's key is locked, this will prompt for the passphrase on the
|
||||
* TTY (via the `getpass` module) and attempt to unlock.
|
||||
*
|
||||
* @param opts {Object}
|
||||
* - opts.tritonapi {Object} An `.init()`ialized TritonApi instance.
|
||||
* @param cb {Function} `function (err)`
|
||||
*/
|
||||
function promptPassphraseUnlockKey(opts, cb) {
|
||||
assert.object(opts.tritonapi, 'opts.tritonapi');
|
||||
|
||||
var kp = opts.tritonapi.keyPair;
|
||||
if (!kp) {
|
||||
cb(new errors.InternalError('TritonApi instance given to '
|
||||
+ 'promptPassphraseUnlockKey is not initialized'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!kp.isLocked()) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
var keyDesc;
|
||||
if (kp.source !== undefined) {
|
||||
keyDesc = kp.source;
|
||||
} else if (kp.comment !== undefined && kp.comment.length > 1) {
|
||||
keyDesc = kp.getPublicKey().type.toUpperCase() +
|
||||
' key for ' + kp.comment;
|
||||
} else {
|
||||
keyDesc = kp.getPublicKey().type.toUpperCase() +
|
||||
' key ' + kp.getKeyId();
|
||||
}
|
||||
var getpassOpts = {
|
||||
prompt: 'Enter passphrase for ' + keyDesc
|
||||
};
|
||||
|
||||
var tryPass = function (err, pass) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
kp.unlock(pass);
|
||||
} catch (unlockErr) {
|
||||
getpassOpts.prompt = 'Bad passphrase, try again for ' + keyDesc;
|
||||
getpass.getPass(getpassOpts, tryPass);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(null);
|
||||
};
|
||||
|
||||
getpass.getPass(getpassOpts, tryPass);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* A utility for the `triton` CLI subcommands to `init()`ialize a
|
||||
* `tritonapi` instance and ensure that the profile's key is unlocked
|
||||
* (prompting on a TTY if necessary). This is typically the CLI's
|
||||
* `tritonapi` instance, but a `tritonapi` can also be passed in
|
||||
* directly.
|
||||
*
|
||||
* @param opts.cli {Object}
|
||||
* @param opts.tritonapi {Object}
|
||||
* @param cb {Function} `function (err)`
|
||||
*/
|
||||
function cliSetupTritonApi(opts, cb) {
|
||||
assert.optionalObject(opts.cli, 'opts.cli');
|
||||
assert.optionalObject(opts.tritonapi, 'opts.tritonapi');
|
||||
var tritonapi = opts.tritonapi || opts.cli.tritonapi;
|
||||
assert.object(tritonapi, 'tritonapi');
|
||||
|
||||
tritonapi.init(function (initErr) {
|
||||
if (initErr) {
|
||||
cb(initErr);
|
||||
return;
|
||||
}
|
||||
|
||||
promptPassphraseUnlockKey({
|
||||
tritonapi: tritonapi
|
||||
}, function (keyErr) {
|
||||
cb(keyErr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Edit the given text in $EDITOR (defaulting to `vi`) and return the edited
|
||||
* text.
|
||||
@ -984,6 +1078,8 @@ module.exports = {
|
||||
promptYesNo: promptYesNo,
|
||||
promptEnter: promptEnter,
|
||||
promptField: promptField,
|
||||
promptPassphraseUnlockKey: promptPassphraseUnlockKey,
|
||||
cliSetupTritonApi: cliSetupTritonApi,
|
||||
editInEditor: editInEditor,
|
||||
ansiStylize: ansiStylize,
|
||||
indent: indent,
|
||||
|
@ -21,7 +21,12 @@ function do_get(subcmd, opts, args, callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.top.tritonapi.cloudapi.getAccount(function (err, account) {
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
tritonapi.cloudapi.getAccount(function (err, account) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
@ -44,6 +49,7 @@ function do_get(subcmd, opts, args, callback) {
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
|
@ -30,7 +30,9 @@ function do_update(subcmd, opts, args, callback) {
|
||||
var log = this.log;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
function gatherDataArgs(ctx, next) {
|
||||
if (opts.file) {
|
||||
next();
|
||||
|
@ -31,8 +31,13 @@ function do_datacenters(subcmd, opts, args, callback) {
|
||||
|
||||
var columns = opts.o.split(',');
|
||||
var sort = opts.s.split(',');
|
||||
var tritonapi = this.tritonapi;
|
||||
|
||||
this.tritonapi.cloudapi.listDatacenters(function (err, datacenters) {
|
||||
common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
tritonapi.cloudapi.listDatacenters(function (err, datacenters) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
@ -62,6 +67,7 @@ function do_datacenters(subcmd, opts, args, callback) {
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_datacenters.options = [
|
||||
|
@ -45,8 +45,13 @@ function do_create(subcmd, opts, args, cb) {
|
||||
createOpts.description = opts.description;
|
||||
}
|
||||
|
||||
this.top.tritonapi.cloudapi.createFirewallRule(createOpts,
|
||||
function (err, fwrule) {
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
tritonapi.cloudapi.createFirewallRule(
|
||||
createOpts, function (err, fwrule) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
@ -55,6 +60,7 @@ function do_create(subcmd, opts, args, cb) {
|
||||
(!fwrule.enabled ? ' (disabled)' : ''));
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -31,10 +31,11 @@ function do_delete(subcmd, opts, args, cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
var ruleIds = args;
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function confirm(_, next) {
|
||||
if (opts.force) {
|
||||
return next();
|
||||
@ -61,7 +62,7 @@ function do_delete(subcmd, opts, args, cb) {
|
||||
vasync.forEachParallel({
|
||||
inputs: ruleIds,
|
||||
func: function deleteOne(id, nextId) {
|
||||
cli.tritonapi.deleteFirewallRule({
|
||||
tritonapi.deleteFirewallRule({
|
||||
id: id
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
|
@ -30,12 +30,15 @@ function do_disable(subcmd, opts, args, cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: function disableOne(id, nextId) {
|
||||
cli.tritonapi.disableFirewallRule({ id: id }, function (err) {
|
||||
tritonapi.disableFirewallRule({ id: id }, function (err) {
|
||||
if (err) {
|
||||
nextId(err);
|
||||
return;
|
||||
@ -46,6 +49,7 @@ function do_disable(subcmd, opts, args, cb) {
|
||||
});
|
||||
}
|
||||
}, cb);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -30,12 +30,15 @@ function do_enable(subcmd, opts, args, cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: function enableOne(id, nextId) {
|
||||
cli.tritonapi.enableFirewallRule({ id: id }, function (err) {
|
||||
tritonapi.enableFirewallRule({ id: id }, function (err) {
|
||||
if (err) {
|
||||
nextId(err);
|
||||
return;
|
||||
@ -46,6 +49,7 @@ function do_enable(subcmd, opts, args, cb) {
|
||||
});
|
||||
}
|
||||
}, cb);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -33,9 +33,13 @@ function do_get(subcmd, opts, args, cb) {
|
||||
}
|
||||
|
||||
var id = args[0];
|
||||
var cli = this.top;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
cli.tritonapi.getFirewallRule(id, function onRule(err, fwrule) {
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
tritonapi.getFirewallRule(id, function onRule(err, fwrule) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
@ -49,6 +53,7 @@ function do_get(subcmd, opts, args, cb) {
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -54,6 +54,10 @@ function do_instances(subcmd, opts, args, cb) {
|
||||
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
vasync.parallel({funcs: [
|
||||
function getTheImages(next) {
|
||||
tritonapi.listImages({
|
||||
@ -82,9 +86,9 @@ function do_instances(subcmd, opts, args, cb) {
|
||||
}
|
||||
]}, 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.
|
||||
* 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;
|
||||
@ -102,7 +106,8 @@ function do_instances(subcmd, opts, args, cb) {
|
||||
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.img = imgmap[inst.image] ||
|
||||
common.uuidToShortId(inst.image);
|
||||
inst.shortid = inst.id.split('-', 1)[0];
|
||||
var flags = [];
|
||||
if (inst.docker) flags.push('D');
|
||||
@ -124,6 +129,7 @@ function do_instances(subcmd, opts, args, cb) {
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_instances.options = [
|
||||
|
@ -35,8 +35,12 @@ function do_list(subcmd, opts, args, cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cli = this.top;
|
||||
cli.tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) {
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
tritonapi.cloudapi.listFirewallRules({}, function onRules(err, rules) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
@ -70,6 +74,7 @@ function do_list(subcmd, opts, args, cb) {
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -37,7 +37,9 @@ function do_update(subcmd, opts, args, cb) {
|
||||
|
||||
var id = args.shift();
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
function gatherDataArgs(ctx, next) {
|
||||
if (opts.file) {
|
||||
next();
|
||||
|
@ -26,7 +26,6 @@ var mat = require('../metadataandtags');
|
||||
// ---- the command
|
||||
|
||||
function do_create(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
@ -37,9 +36,10 @@ function do_create(subcmd, opts, args, cb) {
|
||||
}
|
||||
|
||||
var log = this.top.log;
|
||||
var cloudapi = this.top.tritonapi.cloudapi;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function loadTags(ctx, next) {
|
||||
mat.tagsFromCreateOpts(opts, log, function (err, tags) {
|
||||
if (err) {
|
||||
@ -76,7 +76,7 @@ function do_create(subcmd, opts, args, cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.top.tritonapi.getInstance(id, function (err, inst) {
|
||||
tritonapi.getInstance(id, function (err, inst) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
@ -113,9 +113,11 @@ function do_create(subcmd, opts, args, cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
cloudapi.createImageFromMachine(createOpts, function (err, img) {
|
||||
tritonapi.cloudapi.createImageFromMachine(
|
||||
createOpts, function (err, img) {
|
||||
if (err) {
|
||||
next(new errors.TritonError(err, 'error creating image'));
|
||||
next(new errors.TritonError(err,
|
||||
'error creating image'));
|
||||
return;
|
||||
}
|
||||
ctx.img = img;
|
||||
@ -147,8 +149,8 @@ function do_create(subcmd, opts, args, cb) {
|
||||
ctx.img.state = 'running';
|
||||
waitCb(null, ctx.img);
|
||||
}, 5000);
|
||||
}
|
||||
: cloudapi.waitForImageStates.bind(cloudapi));
|
||||
} : tritonapi.cloudapi.waitForImageStates.bind(
|
||||
tritonapi.cloudapi));
|
||||
|
||||
waiter({
|
||||
id: ctx.img.id,
|
||||
|
@ -26,7 +26,8 @@ function do_delete(subcmd, opts, args, cb) {
|
||||
}
|
||||
var ids = args;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
/*
|
||||
* Lookup images, if not given UUIDs: we'll need to do it anyway
|
||||
* for the DeleteImage call(s), and doing so explicitly here allows
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
var format = require('util').format;
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
@ -24,7 +25,12 @@ function do_get(subcmd, opts, args, callback) {
|
||||
'incorrect number of args (%d)', args.length)));
|
||||
}
|
||||
|
||||
this.top.tritonapi.getImage(args[0], function onRes(err, img) {
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
tritonapi.getImage(args[0], function onRes(err, img) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
@ -36,6 +42,7 @@ function do_get(subcmd, opts, args, callback) {
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
|
@ -63,7 +63,12 @@ function do_list(subcmd, opts, args, callback) {
|
||||
listOpts.state = 'all';
|
||||
}
|
||||
|
||||
this.top.tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
@ -100,6 +105,7 @@ function do_list(subcmd, opts, args, callback) {
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_list.options = [
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var distractions = require('../distractions');
|
||||
var errors = require('../errors');
|
||||
|
||||
@ -34,7 +35,8 @@ function do_wait(subcmd, opts, args, cb) {
|
||||
var done = 0;
|
||||
var imgFromId = {};
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function getImgs(_, next) {
|
||||
vasync.forEachParallel({
|
||||
inputs: ids,
|
||||
|
@ -28,9 +28,14 @@ function do_info(subcmd, opts, args, callback) {
|
||||
|
||||
var out = {};
|
||||
var i = 0;
|
||||
var tritonapi = this.tritonapi;
|
||||
|
||||
this.tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
|
||||
this.tritonapi.cloudapi.listMachines(cb.bind('machines')); i++;
|
||||
common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
|
||||
tritonapi.cloudapi.listMachines(cb.bind('machines')); i++;
|
||||
|
||||
function cb(err, data) {
|
||||
if (err) {
|
||||
@ -91,6 +96,7 @@ function do_info(subcmd, opts, args, callback) {
|
||||
}
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
do_info.options = [
|
||||
|
@ -27,7 +27,6 @@ var sortDefault = 'id,time';
|
||||
|
||||
|
||||
function do_audit(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
@ -51,12 +50,14 @@ function do_audit(subcmd, opts, args, cb) {
|
||||
|
||||
var arg = args[0];
|
||||
var uuid;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (common.isUUID(arg)) {
|
||||
uuid = arg;
|
||||
go1();
|
||||
} else {
|
||||
self.top.tritonapi.getInstance(arg, function (err, inst) {
|
||||
tritonapi.getInstance(arg, function (err, inst) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
@ -64,10 +65,10 @@ function do_audit(subcmd, opts, args, cb) {
|
||||
uuid = inst.id;
|
||||
go1();
|
||||
});
|
||||
}
|
||||
}});
|
||||
|
||||
function go1() {
|
||||
self.top.tritonapi.cloudapi.machineAudit(uuid, function (err, audit) {
|
||||
tritonapi.cloudapi.machineAudit(uuid, function (err, audit) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
|
@ -22,7 +22,6 @@ var mat = require('../metadataandtags');
|
||||
|
||||
|
||||
function do_create(subcmd, opts, args, cb) {
|
||||
var self = this;
|
||||
if (opts.help) {
|
||||
this.do_help('help', {}, [subcmd], cb);
|
||||
return;
|
||||
@ -31,9 +30,10 @@ function do_create(subcmd, opts, args, cb) {
|
||||
}
|
||||
|
||||
var log = this.top.log;
|
||||
var cloudapi = this.top.tritonapi.cloudapi;
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
/* BEGIN JSSTYLED */
|
||||
/*
|
||||
* Parse --affinity options for validity to `ctx.affinities`.
|
||||
@ -158,7 +158,7 @@ function do_create(subcmd, opts, args, cb) {
|
||||
nearFar.push(aff.val);
|
||||
nextAff();
|
||||
} else {
|
||||
self.top.tritonapi.getInstance({
|
||||
tritonapi.getInstance({
|
||||
id: aff.val,
|
||||
fields: ['id']
|
||||
}, function (err, inst) {
|
||||
@ -222,7 +222,7 @@ function do_create(subcmd, opts, args, cb) {
|
||||
name: args[0],
|
||||
useCache: true
|
||||
};
|
||||
self.top.tritonapi.getImage(_opts, function (err, img) {
|
||||
tritonapi.getImage(_opts, function (err, img) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
@ -243,7 +243,7 @@ function do_create(subcmd, opts, args, cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.top.tritonapi.getPackage(id, function (err, pkg) {
|
||||
tritonapi.getPackage(id, function (err, pkg) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
@ -261,7 +261,7 @@ function do_create(subcmd, opts, args, cb) {
|
||||
vasync.forEachPipeline({
|
||||
inputs: opts.network,
|
||||
func: function getOneNetwork(name, nextNet) {
|
||||
self.top.tritonapi.getNetwork(name, function (err, net) {
|
||||
tritonapi.getNetwork(name, function (err, net) {
|
||||
if (err) {
|
||||
nextNet(err);
|
||||
} else {
|
||||
@ -316,7 +316,7 @@ function do_create(subcmd, opts, args, cb) {
|
||||
return next();
|
||||
}
|
||||
|
||||
cloudapi.createMachine(createOpts, function (err, inst) {
|
||||
tritonapi.cloudapi.createMachine(createOpts, function (err, inst) {
|
||||
if (err) {
|
||||
next(new errors.TritonError(err,
|
||||
'error creating instance'));
|
||||
@ -352,8 +352,8 @@ function do_create(subcmd, opts, args, cb) {
|
||||
ctx.inst.state = 'running';
|
||||
waitCb(null, ctx.inst);
|
||||
}, 5000);
|
||||
}
|
||||
: cloudapi.waitForMachineStates.bind(cloudapi));
|
||||
} : tritonapi.cloudapi.waitForMachineStates.bind(
|
||||
tritonapi.cloudapi));
|
||||
|
||||
waiter({
|
||||
id: ctx.inst.id,
|
||||
|
@ -14,6 +14,7 @@ var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
@ -50,6 +51,10 @@ function do_disable_firewall(subcmd, opts, args, cb) {
|
||||
});
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: function disableOne(name, nextInst) {
|
||||
@ -73,6 +78,7 @@ function do_disable_firewall(subcmd, opts, args, cb) {
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@ var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
@ -50,6 +51,10 @@ function do_enable_firewall(subcmd, opts, args, cb) {
|
||||
});
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
vasync.forEachParallel({
|
||||
inputs: args,
|
||||
func: function enableOne(name, nextInst) {
|
||||
@ -73,6 +78,7 @@ function do_enable_firewall(subcmd, opts, args, cb) {
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -41,6 +41,10 @@ function do_fwrules(subcmd, opts, args, cb) {
|
||||
var id = args[0];
|
||||
|
||||
var cli = this.top;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
cli.tritonapi.listInstanceFirewallRules({
|
||||
id: id
|
||||
}, function onRules(err, rules) {
|
||||
@ -77,6 +81,7 @@ function do_fwrules(subcmd, opts, args, cb) {
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -19,7 +19,12 @@ function do_get(subcmd, opts, args, cb) {
|
||||
return cb(new Error('invalid args: ' + args));
|
||||
}
|
||||
|
||||
this.top.tritonapi.getInstance(args[0], function (err, inst) {
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
tritonapi.getInstance(args[0], function (err, inst) {
|
||||
if (inst) {
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(inst));
|
||||
@ -29,6 +34,7 @@ function do_get(subcmd, opts, args, cb) {
|
||||
}
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
var format = require('util').format;
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
@ -29,6 +30,10 @@ function do_ip(subcmd, opts, args, cb) {
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
cli.tritonapi.getInstance(args[0], function (err, inst) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
@ -44,6 +49,7 @@ function do_ip(subcmd, opts, args, cb) {
|
||||
console.log(inst.primaryIp);
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_ip.options = [
|
||||
|
@ -74,6 +74,10 @@ function do_list(subcmd, opts, args, callback) {
|
||||
var imgs = [];
|
||||
var insts;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
vasync.parallel({funcs: [
|
||||
function getTheImages(next) {
|
||||
self.top.tritonapi.listImages({
|
||||
@ -83,11 +87,13 @@ function do_list(subcmd, opts, args, callback) {
|
||||
if (err) {
|
||||
if (err.statusCode === 403) {
|
||||
/*
|
||||
* This could be a authorization error due to RBAC
|
||||
* on a subuser. We don't want to fail `triton inst ls`
|
||||
* if the subuser can ListMachines, but not ListImages.
|
||||
* This could be a authorization error due
|
||||
* to RBAC on a subuser. We don't want to
|
||||
* fail `triton inst ls` if the subuser
|
||||
* can ListMachines, but not ListImages.
|
||||
*/
|
||||
log.debug(err,
|
||||
log.debug(
|
||||
err,
|
||||
'authz error listing images for insts info');
|
||||
next();
|
||||
} else {
|
||||
@ -100,7 +106,8 @@ function do_list(subcmd, opts, args, callback) {
|
||||
});
|
||||
},
|
||||
function getTheMachines(next) {
|
||||
self.top.tritonapi.cloudapi.listMachines(listOpts,
|
||||
self.top.tritonapi.cloudapi.listMachines(
|
||||
listOpts,
|
||||
function (err, _insts) {
|
||||
if (err) {
|
||||
next(err);
|
||||
@ -112,9 +119,9 @@ function do_list(subcmd, opts, args, callback) {
|
||||
}
|
||||
]}, 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.
|
||||
* 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;
|
||||
@ -132,7 +139,8 @@ function do_list(subcmd, opts, args, callback) {
|
||||
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.img = imgmap[inst.image] ||
|
||||
common.uuidToShortId(inst.image);
|
||||
inst.shortid = inst.id.split('-', 1)[0];
|
||||
var flags = [];
|
||||
if (inst.docker) flags.push('D');
|
||||
@ -153,6 +161,7 @@ function do_list(subcmd, opts, args, callback) {
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_list.options = [
|
||||
|
@ -47,7 +47,8 @@ function do_create(subcmd, opts, args, cb) {
|
||||
createOpts.name = opts.name;
|
||||
}
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function createSnapshot(ctx, next) {
|
||||
ctx.start = Date.now();
|
||||
|
||||
|
@ -61,7 +61,8 @@ function do_delete(subcmd, opts, args, cb) {
|
||||
});
|
||||
}
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function confirm(_, next) {
|
||||
if (opts.force) {
|
||||
return next();
|
||||
|
@ -36,6 +36,10 @@ function do_get(subcmd, opts, args, cb) {
|
||||
var name = args[1];
|
||||
var cli = this.top;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
cli.tritonapi.getInstanceSnapshot({
|
||||
id: id,
|
||||
name: name
|
||||
@ -53,6 +57,7 @@ function do_get(subcmd, opts, args, cb) {
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
|
@ -40,6 +40,10 @@ function do_list(subcmd, opts, args, cb) {
|
||||
var cli = this.top;
|
||||
var machineId = args[0];
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
cli.tritonapi.listInstanceSnapshots({
|
||||
id: machineId
|
||||
}, function onSnapshots(err, snapshots) {
|
||||
@ -70,6 +74,7 @@ function do_list(subcmd, opts, args, cb) {
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -38,6 +38,10 @@ function do_ssh(subcmd, opts, args, callback) {
|
||||
id = id.substr(i + 1);
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
cli.tritonapi.getInstance(id, function (err, inst) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
@ -60,15 +64,17 @@ function do_ssh(subcmd, opts, args, callback) {
|
||||
*/
|
||||
if (!opts.no_disable_mux) {
|
||||
/*
|
||||
* A simple `-o ControlMaster=no` doesn't work. With just that
|
||||
* option, a `ControlPath` option (from ~/.ssh/config) will still
|
||||
* be used if it exists. Our hack is to set a ControlPath we
|
||||
* know should not exist. Using '/dev/null' wasn't a good
|
||||
* alternative because `ssh` tries "$ControlPath.$somerandomnum"
|
||||
* A simple `-o ControlMaster=no` doesn't work. With
|
||||
* just that option, a `ControlPath` option (from
|
||||
* ~/.ssh/config) will still be used if it exists. Our
|
||||
* hack is to set a ControlPath we know should not
|
||||
* exist. Using '/dev/null' wasn't a good alternative
|
||||
* because `ssh` tries "$ControlPath.$somerandomnum"
|
||||
* and also because Windows.
|
||||
*/
|
||||
var nullSshControlPath = path.resolve(
|
||||
cli.tritonapi.config._configDir, 'tmp', 'nullSshControlPath');
|
||||
cli.tritonapi.config._configDir, 'tmp',
|
||||
'nullSshControlPath');
|
||||
args = [
|
||||
'-o', 'ControlMaster=no',
|
||||
'-o', 'ControlPath='+nullSshControlPath
|
||||
@ -86,6 +92,7 @@ function do_ssh(subcmd, opts, args, callback) {
|
||||
process.exit(code);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_ssh.options = [
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
@ -29,6 +30,10 @@ function do_delete(subcmd, opts, args, cb) {
|
||||
}
|
||||
var waitTimeoutMs = opts.wait_timeout * 1000; /* seconds to ms */
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
if (opts.all) {
|
||||
self.top.tritonapi.deleteAllInstanceTags({
|
||||
id: args[0],
|
||||
@ -44,8 +49,8 @@ function do_delete(subcmd, opts, args, cb) {
|
||||
args.slice(1).forEach(function (arg) { names[arg] = true; });
|
||||
names = Object.keys(names);
|
||||
|
||||
// TODO: Instead of waiting for each delete, let's delete them all then
|
||||
// wait for the set.
|
||||
// TODO: Instead of waiting for each delete, let's delete
|
||||
// them all then wait for the set.
|
||||
vasync.forEachPipeline({
|
||||
inputs: names,
|
||||
func: function deleteOne(name, next) {
|
||||
@ -64,6 +69,7 @@ function do_delete(subcmd, opts, args, cb) {
|
||||
}
|
||||
}, cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
do_delete.options = [
|
||||
|
@ -10,6 +10,7 @@
|
||||
* `triton instance tag get ...`
|
||||
*/
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
|
||||
@ -23,6 +24,10 @@ function do_get(subcmd, opts, args, cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
self.top.tritonapi.getInstanceTag({
|
||||
id: args[0],
|
||||
tag: args[1]
|
||||
@ -38,6 +43,7 @@ function do_get(subcmd, opts, args, cb) {
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
|
@ -10,6 +10,7 @@
|
||||
* `triton instance tag list ...`
|
||||
*/
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
|
||||
function do_list(subcmd, opts, args, cb) {
|
||||
@ -22,7 +23,12 @@ function do_list(subcmd, opts, args, cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.top.tritonapi.listInstanceTags({id: args[0]}, function (err, tags) {
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
self.top.tritonapi.listInstanceTags(
|
||||
{id: args[0]}, function (err, tags) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
@ -34,6 +40,7 @@ function do_list(subcmd, opts, args, cb) {
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_list.options = [
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
var mat = require('../../metadataandtags');
|
||||
|
||||
@ -27,7 +28,8 @@ function do_replace_all(subcmd, opts, args, cb) {
|
||||
}
|
||||
var log = self.log;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function gatherTags(ctx, next) {
|
||||
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
|
||||
if (err) {
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../../common');
|
||||
var errors = require('../../errors');
|
||||
var mat = require('../../metadataandtags');
|
||||
|
||||
@ -27,7 +28,8 @@ function do_set(subcmd, opts, args, cb) {
|
||||
}
|
||||
var log = self.log;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function gatherTags(ctx, next) {
|
||||
mat.tagsFromSetArgs(opts, args.slice(1), log, function (err, tags) {
|
||||
if (err) {
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var distractions = require('../distractions');
|
||||
var errors = require('../errors');
|
||||
|
||||
@ -34,7 +35,8 @@ function do_wait(subcmd, opts, args, cb) {
|
||||
var done = 0;
|
||||
var instFromId = {};
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function getInsts(_, next) {
|
||||
vasync.forEachParallel({
|
||||
inputs: ids,
|
||||
|
@ -83,8 +83,6 @@ function gen_do_ACTION(opts) {
|
||||
function _doTheAction(action, subcmd, opts, args, callback) {
|
||||
var self = this;
|
||||
|
||||
var now = Date.now();
|
||||
|
||||
var command, state;
|
||||
switch (action) {
|
||||
case 'start':
|
||||
@ -116,7 +114,17 @@ function _doTheAction(action, subcmd, opts, args, callback) {
|
||||
callback(new errors.UsageError('missing INST arg(s)'));
|
||||
return;
|
||||
}
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
_doOnEachInstance(self, action, command, state, args, opts, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function _doOnEachInstance(self, action, command, state, instances,
|
||||
opts, callback) {
|
||||
var now = Date.now();
|
||||
vasync.forEachParallel({
|
||||
func: function (arg, cb) {
|
||||
var alias, uuid;
|
||||
@ -190,7 +198,7 @@ function _doTheAction(action, subcmd, opts, args, callback) {
|
||||
});
|
||||
}
|
||||
},
|
||||
inputs: args
|
||||
inputs: instances
|
||||
}, function (err, results) {
|
||||
var e = err ? (new Error('command failure')) : null;
|
||||
callback(e);
|
||||
|
@ -40,7 +40,8 @@ function do_add(subcmd, opts, args, cb) {
|
||||
var filePath = args[0];
|
||||
var cli = this.top;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function gatherDataStdin(ctx, next) {
|
||||
if (filePath !== '-') {
|
||||
return next();
|
||||
|
@ -35,7 +35,8 @@ function do_delete(subcmd, opts, args, cb) {
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
vasync.pipeline({arg: {cli: this.top}, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
function confirm(_, next) {
|
||||
if (opts.yes) {
|
||||
return next();
|
||||
|
@ -35,9 +35,13 @@ function do_get(subcmd, opts, args, cb) {
|
||||
var id = args[0];
|
||||
var cli = this.top;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
cli.tritonapi.cloudapi.getKey({
|
||||
// Currently `cloudapi.getUserKey` isn't picky about the `name` being
|
||||
// passed in as the `opts.fingerprint` arg.
|
||||
// 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) {
|
||||
@ -52,6 +56,7 @@ function do_get(subcmd, opts, args, cb) {
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -37,6 +37,10 @@ function do_list(subcmd, opts, args, cb) {
|
||||
|
||||
var cli = this.top;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
@ -69,6 +73,7 @@ function do_list(subcmd, opts, args, cb) {
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -25,7 +25,13 @@ function do_get(subcmd, opts, args, cb) {
|
||||
'incorrect number of args (%d)', args.length)));
|
||||
}
|
||||
|
||||
this.top.tritonapi.getNetwork(args[0], function (err, net) {
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
cb(setupErr);
|
||||
}
|
||||
tritonapi.getNetwork(args[0], function (err, net) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
@ -37,6 +43,7 @@ function do_get(subcmd, opts, args, cb) {
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
|
@ -49,8 +49,13 @@ function do_list(subcmd, opts, args, callback) {
|
||||
columns = columns.split(',');
|
||||
|
||||
var sort = opts.s.split(',');
|
||||
var tritonapi = this.top.tritonapi;
|
||||
|
||||
this.top.tritonapi.cloudapi.listNetworks(function (err, networks) {
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
tritonapi.cloudapi.listNetworks(function (err, networks) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
@ -72,6 +77,7 @@ function do_list(subcmd, opts, args, callback) {
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_list.options = [
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
var format = require('util').format;
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
|
||||
|
||||
@ -24,7 +25,12 @@ function do_get(subcmd, opts, args, callback) {
|
||||
'incorrect number of args (%d)', args.length)));
|
||||
}
|
||||
|
||||
this.top.tritonapi.getPackage(args[0], function onRes(err, pkg) {
|
||||
var tritonapi = this.top.tritonapi;
|
||||
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
tritonapi.getPackage(args[0], function onRes(err, pkg) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
@ -36,6 +42,7 @@ function do_get(subcmd, opts, args, callback) {
|
||||
}
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_get.options = [
|
||||
@ -63,7 +70,7 @@ do_get.help = [
|
||||
'',
|
||||
'Where PACKAGE is a package id (full UUID), exact name, or short id.',
|
||||
'',
|
||||
'Note: Currently this dumps prettified JSON by default. That might change',
|
||||
'Note: Currently this dumps perttified JSON by default. That might change',
|
||||
'in the future. Use "-j" to explicitly get JSON output.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
var tabula = require('tabula');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
|
||||
@ -68,16 +69,31 @@ function do_list(subcmd, opts, args, callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.top.tritonapi.cloudapi.listPackages(listOpts, function (err, pkgs) {
|
||||
var context = {
|
||||
cli: this.top
|
||||
};
|
||||
vasync.pipeline({arg: context, funcs: [
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
function getThem(arg, next) {
|
||||
arg.cli.tritonapi.cloudapi.listPackages(listOpts,
|
||||
function (err, pkgs) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
arg.pkgs = pkgs;
|
||||
next();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
function display(arg, next) {
|
||||
if (opts.json) {
|
||||
common.jsonStream(pkgs);
|
||||
common.jsonStream(arg.pkgs);
|
||||
} else {
|
||||
for (i = 0; i < pkgs.length; i++) {
|
||||
var pkg = pkgs[i];
|
||||
for (i = 0; i < arg.pkgs.length; i++) {
|
||||
var pkg = arg.pkgs[i];
|
||||
pkg.shortid = pkg.id.split('-', 1)[0];
|
||||
|
||||
/*
|
||||
@ -127,14 +143,15 @@ function do_list(subcmd, opts, args, callback) {
|
||||
}
|
||||
});
|
||||
}
|
||||
tabula(pkgs, {
|
||||
tabula(arg.pkgs, {
|
||||
skipHeader: opts.H,
|
||||
columns: columns,
|
||||
sort: sort
|
||||
});
|
||||
}
|
||||
callback();
|
||||
});
|
||||
next();
|
||||
}
|
||||
]}, callback);
|
||||
}
|
||||
|
||||
do_list.options = [
|
||||
|
@ -9,6 +9,7 @@ var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var sshpk = require('sshpk');
|
||||
var vasync = require('vasync');
|
||||
var auth = require('smartdc-auth');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
@ -101,17 +102,15 @@ function _createProfile(opts, cb) {
|
||||
'create profile: stdout is not a TTY'));
|
||||
}
|
||||
|
||||
var kr = new auth.KeyRing();
|
||||
var keyChoices = {};
|
||||
|
||||
var defaults = {};
|
||||
if (ctx.copy) {
|
||||
defaults = ctx.copy;
|
||||
delete defaults.name; // we don't copy a profile name
|
||||
} else {
|
||||
defaults.url = 'https://us-sw-1.api.joyent.com';
|
||||
|
||||
var possibleDefaultFp = '~/.ssh/id_rsa';
|
||||
if (fs.existsSync(common.tildeSync(possibleDefaultFp))) {
|
||||
defaults.keyId = possibleDefaultFp;
|
||||
}
|
||||
}
|
||||
|
||||
var fields = [ {
|
||||
@ -156,11 +155,10 @@ function _createProfile(opts, cb) {
|
||||
valCb();
|
||||
}
|
||||
}, {
|
||||
desc: 'The fingerprint of the SSH key you have registered ' +
|
||||
'for your account. Alternatively, You may enter a local ' +
|
||||
'path to a public or private SSH key to have the ' +
|
||||
'fingerprint calculated for you.',
|
||||
default: defaults.keyId,
|
||||
desc: 'The fingerprint of the SSH key you want to use, or ' +
|
||||
'its index in the list above. If the key you want to ' +
|
||||
'use is not listed, make sure it is either saved in your ' +
|
||||
'SSH keys directory or loaded into the SSH agent.',
|
||||
key: 'keyId',
|
||||
validate: function validateKeyId(value, valCb) {
|
||||
// First try as a fingerprint.
|
||||
@ -170,44 +168,14 @@ function _createProfile(opts, cb) {
|
||||
} catch (fpErr) {
|
||||
}
|
||||
|
||||
// Try as a local path.
|
||||
var keyPath = common.tildeSync(value);
|
||||
fs.stat(keyPath, function (statErr, stats) {
|
||||
if (statErr || !stats.isFile()) {
|
||||
return valCb(new Error(format(
|
||||
'"%s" is neither a valid fingerprint, ' +
|
||||
'nor an existing file', value)));
|
||||
}
|
||||
fs.readFile(keyPath, function (readErr, keyData) {
|
||||
if (readErr) {
|
||||
return valCb(readErr);
|
||||
}
|
||||
var keyType = (keyPath.slice(-4) === '.pub'
|
||||
? 'ssh' : 'pem');
|
||||
try {
|
||||
var key = sshpk.parseKey(keyData, keyType);
|
||||
} catch (keyErr) {
|
||||
return valCb(keyErr);
|
||||
// Try as a list index
|
||||
if (keyChoices[value] !== undefined) {
|
||||
return valCb(null, keyChoices[value]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Save the user's explicit given key path. We will
|
||||
* using it later for Docker setup. Trying to use
|
||||
* the same format as node-smartdc's loadSSHKey
|
||||
* `keyPaths` param.
|
||||
*/
|
||||
ctx.keyPaths = {};
|
||||
if (keyType === 'ssh') {
|
||||
ctx.keyPaths.public = keyPath;
|
||||
} else {
|
||||
ctx.keyPaths.private = keyPath;
|
||||
}
|
||||
|
||||
var newVal = key.fingerprint('md5').toString();
|
||||
console.log('Fingerprint: %s', newVal);
|
||||
valCb(null, newVal);
|
||||
});
|
||||
});
|
||||
valCb(new Error(format(
|
||||
'"%s" is neither a valid fingerprint, not an index ' +
|
||||
'from the list of available keys', value)));
|
||||
}
|
||||
} ];
|
||||
|
||||
@ -234,12 +202,51 @@ function _createProfile(opts, cb) {
|
||||
vasync.forEachPipeline({
|
||||
inputs: fields,
|
||||
func: function getField(field, nextField) {
|
||||
if (field.key !== 'name') console.log();
|
||||
if (field.key !== 'name')
|
||||
console.log();
|
||||
if (field.key === 'keyId') {
|
||||
kr.list(function (err, pairs) {
|
||||
if (err) {
|
||||
nextField(err);
|
||||
return;
|
||||
}
|
||||
var choice = 1;
|
||||
console.log('Available SSH keys:');
|
||||
Object.keys(pairs).forEach(function (keyId) {
|
||||
var valid = pairs[keyId].filter(function (kp) {
|
||||
return (kp.canSign());
|
||||
});
|
||||
if (valid.length < 1)
|
||||
return;
|
||||
var pub = valid[0].getPublicKey();
|
||||
console.log(
|
||||
' %d. %d-bit %s key with fingerprint %s',
|
||||
choice, pub.size, pub.type.toUpperCase(),
|
||||
keyId);
|
||||
pairs[keyId].forEach(function (kp) {
|
||||
var comment = kp.comment ||
|
||||
kp.getPublicKey().comment;
|
||||
console.log(' * [in %s] %s %s %s',
|
||||
kp.plugin, comment,
|
||||
(kp.source ? kp.source : ''),
|
||||
(kp.isLocked() ? '[locked]' : ''));
|
||||
});
|
||||
console.log();
|
||||
keyChoices[choice] = keyId;
|
||||
++choice;
|
||||
});
|
||||
common.promptField(field, function (err2, value) {
|
||||
data[field.key] = value;
|
||||
nextField(err2);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
common.promptField(field, function (err, value) {
|
||||
data[field.key] = value;
|
||||
nextField(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, function (err) {
|
||||
console.log();
|
||||
next(err);
|
||||
|
@ -8,6 +8,7 @@ var assert = require('assert-plus');
|
||||
var auth = require('smartdc-auth');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var getpass = require('getpass');
|
||||
var https = require('https');
|
||||
var mkdirp = require('mkdirp');
|
||||
var path = require('path');
|
||||
@ -143,14 +144,38 @@ function profileDockerSetup(opts, cb) {
|
||||
assert.optionalObject(opts.keyPaths, 'opts.keyPaths');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
var implicit = Boolean(opts.implicit);
|
||||
var cli = opts.cli;
|
||||
var log = cli.log;
|
||||
var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name});
|
||||
|
||||
var implicit = Boolean(opts.implicit);
|
||||
var log = cli.log;
|
||||
|
||||
var profile = tritonapi.profile;
|
||||
var dockerHost;
|
||||
|
||||
vasync.pipeline({arg: {}, funcs: [
|
||||
vasync.pipeline({arg: {tritonapi: tritonapi}, funcs: [
|
||||
function dockerKeyWarning(arg, next) {
|
||||
console.log(wordwrap(
|
||||
'\nWARNING: Docker uses TLS-based authentication with a ' +
|
||||
'different security model from SSH keys. As a result, the ' +
|
||||
'Docker client cannot currently support encrypted ' +
|
||||
'(password protected) keys or SSH agents. If you ' +
|
||||
'continue, the Triton CLI will attempt to format a copy ' +
|
||||
'of your SSH *private* key as an unencrypted TLS cert ' +
|
||||
'and place the copy in ~/.triton/docker for use by the ' +
|
||||
'Docker client.'));
|
||||
common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) {
|
||||
if (answer !== 'y') {
|
||||
console.error('Aborting');
|
||||
next(true);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
common.cliSetupTritonApi,
|
||||
|
||||
function checkCloudapiStatus(arg, next) {
|
||||
tritonapi.cloudapi.ping({}, function (err, pong, res) {
|
||||
if (!res) {
|
||||
@ -222,69 +247,16 @@ function profileDockerSetup(opts, cb) {
|
||||
next();
|
||||
},
|
||||
|
||||
function findSshPrivKey_keyPaths(arg, next) {
|
||||
if (!opts.keyPaths) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
var privKeyPath = opts.keyPaths.private;
|
||||
if (!privKeyPath) {
|
||||
assert.string(opts.keyPaths.public);
|
||||
assert.ok(opts.keyPaths.public.slice(-4) === '.pub');
|
||||
privKeyPath = opts.keyPaths.public.slice(0, -4);
|
||||
if (!fs.existsSync(privKeyPath)) {
|
||||
cb(new errors.SetupError(format('could not find SSH '
|
||||
+ 'private key file from public key file "%s": "%s" '
|
||||
+ 'does not exist', opts.keyPaths.public,
|
||||
privKeyPath)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
arg.sshKeyPaths = {
|
||||
private: privKeyPath,
|
||||
public: opts.keyPaths.public
|
||||
};
|
||||
|
||||
fs.readFile(privKeyPath, function (readErr, keyData) {
|
||||
if (readErr) {
|
||||
cb(readErr);
|
||||
return;
|
||||
}
|
||||
function checkSshPrivKey(arg, next) {
|
||||
try {
|
||||
arg.sshPrivKey = sshpk.parseKey(keyData, 'pem');
|
||||
} catch (keyErr) {
|
||||
cb(keyErr);
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem');
|
||||
common.execPlus({
|
||||
cmd: format('openssl rsa -in %s -out %s -outform pem',
|
||||
arg.sshKeyPaths.private, arg.keyPath),
|
||||
log: log
|
||||
}, next);
|
||||
},
|
||||
function genClientCert_csr(arg, next) {
|
||||
arg.csrPath = path.resolve(arg.dockerCertPath, 'csr.pem');
|
||||
common.execPlus({
|
||||
cmd: format('openssl req -new -key %s -out %s -subj "/CN=%s"',
|
||||
arg.keyPath, arg.csrPath, profile.account),
|
||||
log: log
|
||||
}, next);
|
||||
var data = tritonapi.keyPair.getPrivateKey().toBuffer('pkcs1');
|
||||
fs.writeFile(arg.keyPath, data, function (err) {
|
||||
if (err) {
|
||||
next(new errors.SetupError(err, format(
|
||||
'error writing file %s', arg.keyPath)));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
function genClientCert_cert(arg, next) {
|
||||
arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem');
|
||||
common.execPlus({
|
||||
cmd: format(
|
||||
'openssl x509 -req -days 365 -in %s -signkey %s -out %s',
|
||||
arg.csrPath, arg.keyPath, arg.certPath),
|
||||
log: log
|
||||
}, next);
|
||||
},
|
||||
function genClientCert_deleteCsr(arg, next) {
|
||||
rimraf(arg.csrPath, next);
|
||||
|
||||
var privKey = tritonapi.keyPair.getPrivateKey();
|
||||
var id = sshpk.identityFromDN('CN=' + profile.account);
|
||||
var cert = sshpk.createSelfSignedCertificate(id, privKey);
|
||||
var data = cert.toBuffer('pem');
|
||||
|
||||
fs.writeFile(arg.certPath, data, function (err) {
|
||||
if (err) {
|
||||
next(new errors.SetupError(err, format(
|
||||
'error writing file %s', arg.keyPath)));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
function getServerCa(arg, next) {
|
||||
|
@ -31,8 +31,13 @@ function do_services(subcmd, opts, args, callback) {
|
||||
|
||||
var columns = opts.o.split(',');
|
||||
var sort = opts.s.split(',');
|
||||
var tritonapi = this.tritonapi;
|
||||
|
||||
this.tritonapi.cloudapi.listServices(function (err, services) {
|
||||
common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
|
||||
if (setupErr) {
|
||||
callback(setupErr);
|
||||
}
|
||||
tritonapi.cloudapi.listServices(function (err, services) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
@ -64,6 +69,7 @@ function do_services(subcmd, opts, args, callback) {
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
do_services.options = [
|
||||
|
214
lib/index.js
214
lib/index.js
@ -5,64 +5,112 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright 2015 Joyent, Inc.
|
||||
* Copyright 2016 Joyent, Inc.
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var bunyannoop = require('./bunyannoop');
|
||||
var mod_common = require('./common');
|
||||
var mod_config = require('./config');
|
||||
var tritonapi = require('./tritonapi');
|
||||
var mod_cloudapi2 = require('./cloudapi2');
|
||||
var mod_tritonapi = require('./tritonapi');
|
||||
|
||||
|
||||
/* BEGIN JSSTYLED */
|
||||
/**
|
||||
* A convenience wrapper around `tritonapi.createClient` to for simpler usage.
|
||||
* A convenience wrapper around `tritonapi.TritonApi` for simpler usage.
|
||||
* Conveniences are:
|
||||
* - It wraps up the 3-step process of TritonApi client preparation into
|
||||
* this one call.
|
||||
* - It accepts optional `profileName` and `configDir` parameters that will
|
||||
* load a profile by name and load a config, respectively.
|
||||
*
|
||||
* Minimally this only requires that one of `profileName` or `profile` be
|
||||
* specified. Examples:
|
||||
* Client preparation is a 3-step process:
|
||||
*
|
||||
* var triton = require('triton');
|
||||
* var client = triton.createClient({
|
||||
* 1. create the client object;
|
||||
* 2. initialize it (mainly involves finding the SSH key identified by the
|
||||
* `keyId`); and,
|
||||
* 3. optionally unlock the SSH key (if it is passphrase-protected and not in
|
||||
* an ssh-agent).
|
||||
*
|
||||
* The simplest usage that handles all of these is:
|
||||
*
|
||||
* var mod_triton = require('triton');
|
||||
* mod_triton.createClient({
|
||||
* profileName: 'env',
|
||||
* unlockKeyFn: triton.promptPassphraseUnlockKey
|
||||
* }, function (err, client) {
|
||||
* if (err) {
|
||||
* // handle err
|
||||
* }
|
||||
*
|
||||
* // use `client`
|
||||
* });
|
||||
*
|
||||
* Minimally, only of `profileName` or `profile` is required. Examples:
|
||||
*
|
||||
* // Manually specify profile parameters.
|
||||
* mod_triton.createClient({
|
||||
* profile: {
|
||||
* url: "<cloudapi url>",
|
||||
* account: "<account login for this cloud>",
|
||||
* keyId: "<ssh key fingerprint for one of account's keys>"
|
||||
* }
|
||||
* });
|
||||
* --
|
||||
* }, function (err, client) { ... });
|
||||
*
|
||||
* // Loading a profile from the environment (the `TRITON_*` and/or
|
||||
* // `SDC_*` environment variables).
|
||||
* var client = triton.createClient({profileName: 'env'});
|
||||
* --
|
||||
* var client = triton.createClient({
|
||||
* configDir: '~/.triton', // use the CLI's config dir ...
|
||||
* profileName: 'east1' // ... to find named profiles
|
||||
* });
|
||||
* --
|
||||
* triton.createClient({profileName: 'env'},
|
||||
* function (err, client) { ... });
|
||||
*
|
||||
* // Use one of the named profiles from the `triton` CLI.
|
||||
* triton.createClient({
|
||||
* configDir: '~/.triton',
|
||||
* profileName: 'east1'
|
||||
* }, function (err, client) { ... });
|
||||
*
|
||||
* // The same thing using the underlying APIs.
|
||||
* var client = triton.createClient({
|
||||
* config: triton.loadConfig({configDir: '~/.triton'},
|
||||
* triton.createClient({
|
||||
* config: triton.loadConfig({configDir: '~/.triton'}),
|
||||
* profile: triton.loadProfile({name: 'east1', configDir: '~/.triton'})
|
||||
* });
|
||||
*
|
||||
* A more complete example an app using triton internally might want:
|
||||
*
|
||||
* var triton = require('triton');
|
||||
* var bunyan = require('bunyan');
|
||||
*
|
||||
* var appConfig = {
|
||||
* // However the app handles its config.
|
||||
* };
|
||||
* var log = bunyan.createLogger({name: 'myapp', component: 'triton'});
|
||||
* var client = triton.createClient({
|
||||
* log: log,
|
||||
* profile: appConfig.tritonProfile
|
||||
* });
|
||||
*
|
||||
* }, function (err, client) { ... });
|
||||
*
|
||||
* TODO: The story for an app wanting to specify some Triton config but NOT
|
||||
* have to have a triton $configDir/config.json is poor.
|
||||
*
|
||||
*
|
||||
* # What is that `unlockKeyFn` about?
|
||||
*
|
||||
* Triton uses HTTP-Signature auth: an SSH private key is used to sign requests.
|
||||
* The server-side authenticates by verifying that signature using the
|
||||
* previously uploaded public key. For the client to sign a request it needs an
|
||||
* unlocked private key: an SSH private key that (a) is not
|
||||
* passphrase-protected, (b) is loaded in an ssh-agent, or (c) for which we
|
||||
* have a passphrase.
|
||||
*
|
||||
* If `createClient` finds that its key is locked, it will use `unlockKeyFn`
|
||||
* as follows to attempt to unlock it:
|
||||
*
|
||||
* unlockKeyFn({
|
||||
* tritonapi: client
|
||||
* }, function (unlockErr) {
|
||||
* // ...
|
||||
* });
|
||||
*
|
||||
* This package exports a convenience `promptPassphraseUnlockKey` function that
|
||||
* will prompt the user for a passphrase on stdin. Your tooling can use this
|
||||
* function, provide your own, or skip key unlocking.
|
||||
*
|
||||
* The failure mode for a locked key is an error like this:
|
||||
*
|
||||
* SigningError: error signing request: SSH private key id_rsa is locked (encrypted/password-protected). It must be unlocked before use.
|
||||
* at SigningError._TritonBaseVError (/Users/trentm/tmp/node-triton/lib/errors.js:55:12)
|
||||
* at new SigningError (/Users/trentm/tmp/node-triton/lib/errors.js:173:23)
|
||||
* at CloudApi._getAuthHeaders (/Users/trentm/tmp/node-triton/lib/cloudapi2.js:185:22)
|
||||
*
|
||||
*
|
||||
* @param opts {Object}:
|
||||
* - @param profile {Object} A *Triton profile* object that includes the
|
||||
* information required to connect to a CloudAPI -- minimally this:
|
||||
@ -91,14 +139,24 @@ var tritonapi = require('./tritonapi');
|
||||
* One may not specify both `configDir` and `config`.
|
||||
* - @param log {Bunyan Logger} Optional. A Bunyan logger. If not provided,
|
||||
* a stub that does no logging will be used.
|
||||
* - @param {Function} unlockKeyFn - Optional. A function to handle
|
||||
* unlocking the SSH key found for this profile, if necessary. It must
|
||||
* be of the form `function (opts, cb)` where `opts.tritonapi` is the
|
||||
* initialized TritonApi client. If the caller is a command-line
|
||||
* interface, then `triton.promptPassphraseUnlockKey` can be used to
|
||||
* prompt on stdin for the SSH key passphrase, if needed.
|
||||
* @param {Function} cb - `function (err, client)`
|
||||
*/
|
||||
function createClient(opts) {
|
||||
/* END JSSTYLED */
|
||||
function createClient(opts, cb) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.optionalObject(opts.profile, 'opts.profile');
|
||||
assert.optionalString(opts.profileName, 'opts.profileName');
|
||||
assert.optionalObject(opts.config, 'opts.config');
|
||||
assert.optionalString(opts.configDir, 'opts.configDir');
|
||||
assert.optionalObject(opts.log, 'opts.log');
|
||||
assert.optionalFunc(opts.unlockKeyFn, 'opts.unlockKeyFn');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
assert.ok(!(opts.profile && opts.profileName),
|
||||
'cannot specify both opts.profile and opts.profileName');
|
||||
@ -113,42 +171,87 @@ function createClient(opts) {
|
||||
'must provide opts.configDir for opts.profileName!="env"');
|
||||
}
|
||||
|
||||
var log = opts.log;
|
||||
if (!opts.log) {
|
||||
log = new bunyannoop.BunyanNoopLogger();
|
||||
var log;
|
||||
var client;
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
function theSyncPart(_, next) {
|
||||
log = opts.log || new bunyannoop.BunyanNoopLogger();
|
||||
|
||||
var config;
|
||||
if (opts.config) {
|
||||
config = opts.config;
|
||||
} else {
|
||||
try {
|
||||
config = mod_config.loadConfig(
|
||||
{configDir: opts.configDir});
|
||||
} catch (configErr) {
|
||||
next(configErr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var config = opts.config;
|
||||
if (!config) {
|
||||
config = mod_config.loadConfig({configDir: opts.configDir});
|
||||
var profile;
|
||||
if (opts.profile) {
|
||||
profile = opts.profile;
|
||||
/*
|
||||
* Don't require one to arbitrarily have a profile.name if
|
||||
* manually creating it.
|
||||
*/
|
||||
if (!profile.name) {
|
||||
// TODO: might want this to be a hash/slug of params.
|
||||
profile.name = '_';
|
||||
}
|
||||
|
||||
var profile = opts.profile;
|
||||
if (!profile) {
|
||||
} else {
|
||||
try {
|
||||
profile = mod_config.loadProfile({
|
||||
name: opts.profileName,
|
||||
configDir: config._configDir
|
||||
});
|
||||
} catch (profileErr) {
|
||||
next(profileErr);
|
||||
return;
|
||||
}
|
||||
// Don't require one to arbitrarily have a profile.name if manually
|
||||
// creating it.
|
||||
if (!profile.name) {
|
||||
// TODO: might want this to be hash or slug of profile params.
|
||||
profile.name = '_';
|
||||
}
|
||||
try {
|
||||
mod_config.validateProfile(profile);
|
||||
} catch (valErr) {
|
||||
next(valErr);
|
||||
return;
|
||||
}
|
||||
|
||||
var client = tritonapi.createClient({
|
||||
client = mod_tritonapi.createClient({
|
||||
log: log,
|
||||
config: config,
|
||||
profile: profile
|
||||
});
|
||||
return client;
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
createClient: createClient,
|
||||
promptPassphraseUnlockKey: mod_common.promptPassphraseUnlockKey,
|
||||
|
||||
/**
|
||||
* `createClient` provides convenience parameters to not *have* to call
|
||||
@ -159,7 +262,10 @@ module.exports = {
|
||||
loadProfile: mod_config.loadProfile,
|
||||
loadAllProfiles: mod_config.loadAllProfiles,
|
||||
|
||||
createTritonApiClient: tritonapi.createClient,
|
||||
// For those wanting a lower-level raw CloudAPI client.
|
||||
createCloudApiClient: require('./cloudapi2').createClient
|
||||
/*
|
||||
* For those wanting a lower-level TritonApi createClient, or an
|
||||
* even *lower*-level raw CloudAPI client.
|
||||
*/
|
||||
createTritonApiClient: mod_tritonapi.createClient,
|
||||
createCloudApiClient: mod_cloudapi2.createClient
|
||||
};
|
||||
|
170
lib/tritonapi.js
170
lib/tritonapi.js
@ -6,10 +6,81 @@
|
||||
|
||||
/*
|
||||
* Copyright 2016 Joyent, Inc.
|
||||
*
|
||||
* Core TritonApi client driver class.
|
||||
*/
|
||||
|
||||
/* BEGIN JSSTYLED */
|
||||
/*
|
||||
* Core `TritonApi` client class. A TritonApi client object is a wrapper around
|
||||
* a lower-level `CloudApi` client that makes raw calls to
|
||||
* [Cloud API](https://apidocs.joyent.com/cloudapi/). The wrapper provides
|
||||
* some conveniences, for example:
|
||||
* - referring to resources by "shortId" (8-char UUID prefixes) or "name"
|
||||
* (e.g. an VM instance has a unique name for an account, but the raw
|
||||
* Cloud API only supports lookup by full UUID);
|
||||
* - filling in of image details for instances which only have an "image_uuid"
|
||||
* in Cloud API responses;
|
||||
* - support for waiting for async operations to complete via "wait" parameters;
|
||||
* - profile handling.
|
||||
*
|
||||
* Preparing a TritonApi is a three-step process. (Note: Some users might
|
||||
* prefer to use the `createClient` convenience function in "index.js" that
|
||||
* wraps up all three steps into a single call.)
|
||||
*
|
||||
* 1. Create the client object.
|
||||
* 2. Initialize it (mainly involves finding the SSH key identified by the
|
||||
* `keyId`).
|
||||
* 3. Optionally, unlock the SSH key (if it is passphrase-protected and not in
|
||||
* an ssh-agent). If you know that your key is not passphrase-protected
|
||||
* or is an ssh-agent, then you can skip this step. The failure mode for
|
||||
* a locked key looks like this:
|
||||
* SigningError: error signing request: SSH private key id_rsa is locked (encrypted/password-protected). It must be unlocked before use.
|
||||
* at SigningError._TritonBaseVError (/Users/trentm/tmp/node-triton/lib/errors.js:55:12)
|
||||
* at new SigningError (/Users/trentm/tmp/node-triton/lib/errors.js:173:23)
|
||||
* at CloudApi._getAuthHeaders (/Users/trentm/tmp/node-triton/lib/cloudapi2.js:185:22)
|
||||
*
|
||||
* Usage:
|
||||
* var mod_triton = require('triton');
|
||||
*
|
||||
* // 1. Create the TritonApi instance.
|
||||
* var client = mod_triton.createTritonApiClient({
|
||||
* log: log,
|
||||
* profile: profile, // See mod_triton.loadProfile
|
||||
* config: config // See mod_triton.loadConfig
|
||||
* });
|
||||
*
|
||||
* // 2. Call `init` to setup the profile. This involves finding the SSH
|
||||
* // key identified by the profile's keyId.
|
||||
* client.init(function (initErr) {
|
||||
* if (initErr) boom(initErr);
|
||||
*
|
||||
* // 3. Unlock the SSH key, if necessary. Possibilities are:
|
||||
* // (a) Skip this step. If the key is locked, you will get a
|
||||
* // "SigningError" at first attempt to sign. See example above.
|
||||
* // (b) The key is not locked.
|
||||
* // `client.keyPair.isLocked() === false`
|
||||
* // (c) You have a passphrase for the key:
|
||||
* if (client.keyPair.isLocked()) {
|
||||
* // This throws if the passphrase is incorrect.
|
||||
* client.keyPair.unlock(passphrase);
|
||||
* }
|
||||
*
|
||||
* // (d) Or you use a function that will prompt for a passphrase
|
||||
* // and unlock with that. E.g., `promptPassphraseUnlockKey`
|
||||
* // is one provided by this package that with prompt on stdin.
|
||||
* mod_triton.promptPassphraseUnlockKey({
|
||||
* tritonapi: client
|
||||
* }, function (unlockErr) {
|
||||
* if (unlockErr) boom(unlockErr);
|
||||
*
|
||||
* // 4. Now you can finally make an API call. For example:
|
||||
* client.listImages(function (err, imgs) {
|
||||
* // ...
|
||||
* });
|
||||
* });
|
||||
* });
|
||||
*/
|
||||
/* END JSSTYLED */
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var auth = require('smartdc-auth');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
@ -24,6 +95,7 @@ var restifyBunyanSerializers =
|
||||
require('restify-clients/lib/helpers/bunyan').serializers;
|
||||
var tabula = require('tabula');
|
||||
var vasync = require('vasync');
|
||||
var sshpk = require('sshpk');
|
||||
|
||||
var cloudapi = require('./cloudapi2');
|
||||
var common = require('./common');
|
||||
@ -116,6 +188,14 @@ function _stepFwRuleId(arg, next) {
|
||||
/**
|
||||
* Create a TritonApi client.
|
||||
*
|
||||
* Public properties (TODO: doc all of these):
|
||||
* - profile
|
||||
* - config
|
||||
* - log
|
||||
* - cacheDir (only available if configured with a configDir)
|
||||
* - keyPair (available after init)
|
||||
* - cloudapi (available after init)
|
||||
*
|
||||
* @param opts {Object}
|
||||
* - log {Bunyan Logger}
|
||||
* ...
|
||||
@ -128,6 +208,7 @@ function TritonApi(opts) {
|
||||
|
||||
this.profile = opts.profile;
|
||||
this.config = opts.config;
|
||||
this.keyPair = null;
|
||||
|
||||
// Make sure a given bunyan logger has reasonable client_re[qs] serializers.
|
||||
// Note: This was fixed in restify, then broken again in
|
||||
@ -147,29 +228,43 @@ function TritonApi(opts) {
|
||||
this.config.cacheDir,
|
||||
common.profileSlug(this.profile));
|
||||
this.log.trace({cacheDir: this.cacheDir}, 'cache dir');
|
||||
// TODO perhaps move this to an async .init()
|
||||
if (!fs.existsSync(this.cacheDir)) {
|
||||
try {
|
||||
mkdirp.sync(this.cacheDir);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.cloudapi = this._cloudapiFromProfile(this.profile);
|
||||
}
|
||||
|
||||
|
||||
TritonApi.prototype.close = function close() {
|
||||
if (this.cloudapi) {
|
||||
this.cloudapi.close();
|
||||
delete this.cloudapi;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
TritonApi.prototype._cloudapiFromProfile =
|
||||
function _cloudapiFromProfile(profile)
|
||||
{
|
||||
TritonApi.prototype.init = function init(cb) {
|
||||
var self = this;
|
||||
if (this.cacheDir) {
|
||||
fs.exists(this.cacheDir, function (exists) {
|
||||
if (!exists) {
|
||||
mkdirp(self.cacheDir, function (err) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
self._setupProfile(cb);
|
||||
});
|
||||
} else {
|
||||
self._setupProfile(cb);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self._setupProfile(cb);
|
||||
}
|
||||
};
|
||||
|
||||
TritonApi.prototype._setupProfile = function _setupProfile(cb) {
|
||||
var self = this;
|
||||
var profile = this.profile;
|
||||
|
||||
assert.object(profile, 'profile');
|
||||
assert.string(profile.account, 'profile.account');
|
||||
assert.optionalString(profile.actAsAccount, 'profile.actAsAccount');
|
||||
@ -185,32 +280,39 @@ TritonApi.prototype._cloudapiFromProfile =
|
||||
? true : !profile.insecure);
|
||||
var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION;
|
||||
|
||||
var sign;
|
||||
if (profile.privKey) {
|
||||
sign = auth.privateKeySigner({
|
||||
user: profile.account,
|
||||
subuser: profile.user,
|
||||
keyId: profile.keyId,
|
||||
key: profile.privKey
|
||||
});
|
||||
} else {
|
||||
sign = auth.cliSigner({
|
||||
keyId: profile.keyId,
|
||||
user: profile.account,
|
||||
subuser: profile.user
|
||||
});
|
||||
}
|
||||
var client = cloudapi.createClient({
|
||||
var opts = {
|
||||
url: profile.url,
|
||||
account: profile.actAsAccount || profile.account,
|
||||
user: profile.user,
|
||||
principal: {
|
||||
account: profile.account,
|
||||
user: profile.user
|
||||
},
|
||||
roles: profile.roles,
|
||||
version: acceptVersion,
|
||||
rejectUnauthorized: rejectUnauthorized,
|
||||
sign: sign,
|
||||
log: this.log
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
return client;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
"bunyan": "1.5.1",
|
||||
"cmdln": "4.1.2",
|
||||
"extsprintf": "1.0.2",
|
||||
"getpass": "0.1.6",
|
||||
"lomstream": "1.1.0",
|
||||
"mkdirp": "0.5.1",
|
||||
"node-uuid": "1.4.3",
|
||||
@ -19,8 +20,9 @@
|
||||
"restify-errors": "3.0.0",
|
||||
"rimraf": "2.4.4",
|
||||
"semver": "5.1.0",
|
||||
"smartdc-auth": "git+https://github.com/joyent/node-smartdc-auth.git#05d9077",
|
||||
"sshpk": "1.7.x",
|
||||
"smartdc-auth": "2.5.2",
|
||||
"sshpk": "1.10.1",
|
||||
"sshpk-agent": "1.4.2",
|
||||
"strsplit": "1.0.0",
|
||||
"tabula": "1.7.0",
|
||||
"vasync": "1.6.3",
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 2015, Joyent, Inc.
|
||||
* Copyright 2016 Joyent, Inc.
|
||||
*/
|
||||
|
||||
/*
|
||||
@ -29,10 +29,12 @@ test('TritonApi images', function (tt) {
|
||||
|
||||
var client;
|
||||
tt.test(' setup: client', function (t) {
|
||||
client = h.createClient();
|
||||
t.ok(client, 'client');
|
||||
h.createClient(function (err, client_) {
|
||||
t.error(err);
|
||||
client = client_;
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
var testOpts = {};
|
||||
var img;
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 2015, Joyent, Inc.
|
||||
* Copyright 2016 Joyent, Inc.
|
||||
*/
|
||||
|
||||
/*
|
||||
@ -15,24 +15,26 @@
|
||||
var h = require('./helpers');
|
||||
var test = require('tape');
|
||||
|
||||
var common = require('../../lib/common');
|
||||
|
||||
|
||||
// --- Globals
|
||||
|
||||
|
||||
var CLIENT;
|
||||
var INST;
|
||||
|
||||
|
||||
// --- Tests
|
||||
|
||||
|
||||
test('TritonApi packages', function (tt) {
|
||||
tt.test(' setup', function (t) {
|
||||
CLIENT = h.createClient();
|
||||
t.ok(CLIENT, 'client');
|
||||
|
||||
tt.test(' setup', function (t) {
|
||||
h.createClient(function (err, client_) {
|
||||
t.error(err);
|
||||
CLIENT = client_;
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
tt.test(' setup: inst', function (t) {
|
||||
CLIENT.cloudapi.listMachines(function (err, insts) {
|
||||
if (h.ifErr(t, err))
|
||||
return t.end();
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 2015, Joyent, Inc.
|
||||
* Copyright 2016 Joyent, Inc.
|
||||
*/
|
||||
|
||||
/*
|
||||
@ -15,28 +15,28 @@
|
||||
var h = require('./helpers');
|
||||
var test = require('tape');
|
||||
|
||||
var common = require('../../lib/common');
|
||||
|
||||
|
||||
// --- Globals
|
||||
|
||||
|
||||
var CLIENT;
|
||||
var NET;
|
||||
|
||||
|
||||
// --- Tests
|
||||
|
||||
|
||||
test('TritonApi networks', function (tt) {
|
||||
tt.test(' setup', function (t) {
|
||||
CLIENT = h.createClient();
|
||||
t.ok(CLIENT, 'client');
|
||||
h.createClient(function (err, client_) {
|
||||
t.error(err);
|
||||
CLIENT = client_;
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
tt.test(' setup: net', function (t) {
|
||||
var opts = {
|
||||
account: CLIENT.profile.account
|
||||
};
|
||||
|
||||
CLIENT.cloudapi.listNetworks(opts, function (err, nets) {
|
||||
if (h.ifErr(t, err))
|
||||
return t.end();
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 2015, Joyent, Inc.
|
||||
* Copyright 2016 Joyent, Inc.
|
||||
*/
|
||||
|
||||
/*
|
||||
@ -30,9 +30,14 @@ var PKG;
|
||||
|
||||
test('TritonApi packages', function (tt) {
|
||||
tt.test(' setup', function (t) {
|
||||
CLIENT = h.createClient();
|
||||
t.ok(CLIENT, 'client');
|
||||
h.createClient(function (err, client_) {
|
||||
t.error(err);
|
||||
CLIENT = client_;
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
tt.test(' setup: pkg', function (t) {
|
||||
CLIENT.cloudapi.listPackages(function (err, pkgs) {
|
||||
if (h.ifErr(t, err))
|
||||
return t.end();
|
||||
|
@ -248,12 +248,14 @@ function jsonStreamParse(s) {
|
||||
/*
|
||||
* Create a TritonApi client using the CLI.
|
||||
*/
|
||||
function createClient() {
|
||||
return mod_triton.createClient({
|
||||
function createClient(cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
mod_triton.createClient({
|
||||
log: LOG,
|
||||
profile: CONFIG.profile,
|
||||
configDir: '~/.triton' // piggy-back on Triton CLI config dir
|
||||
});
|
||||
}, cb);
|
||||
}
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user