joyent/node-triton#108 support passphrase protected keys

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

View File

@ -7,6 +7,83 @@ Known issues:
## not yet released
- **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

View File

@ -234,19 +234,27 @@ documentation](https://apidocs.joyent.com/docker) for more information.)
## `TritonApi` Module Usage
Node-triton can also be used as a node module for your own node.js tooling.
A basic example:
A basic example appropriate for a command-line tool is:
var triton = require('triton');
```javascript
var mod_bunyan = require('bunyan');
var mod_triton = require('triton');
var log = mod_bunyan.createLogger({name: 'my-tool'});
// See the `createClient` block comment for full usage details:
// https://github.com/joyent/node-triton/blob/master/lib/index.js
mod_triton.createClient({
log: log,
// Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a
// `profile` object.
profileName: 'env',
unlockKeyFn: mod_triton.promptPassphraseUnlockKey
}, function (err, client) {
if (err) {
// handle err
}
// See `createClient` block comment for full usage details:
// https://github.com/joyent/node-triton/blob/master/lib/index.js
var client = triton.createClient({
profile: {
url: URL,
account: ACCOUNT,
keyId: KEY_ID
}
});
client.listImages(function (err, images) {
client.close(); // Remember to close the client to close TCP conn.
if (err) {
@ -255,7 +263,14 @@ A basic example:
console.log(JSON.stringify(images, null, 4));
}
});
});
```
See the following for more details:
- The block-comment for `createClient` in [lib/index.js](lib/index.js).
- Some module-usage examples in [examples/](examples/).
- The lower-level details in the top-comment in
[lib/tritonapi.js](lib/tritonapi.js).
## Configuration
@ -280,24 +295,6 @@ are in "etc/defaults.json" and can be overriden for the CLI in
catching up and is much more friendly to use.
## cloudapi2.js differences with node-smartdc/lib/cloudapi.js
The old node-smartdc module included an lib for talking directly to the SDC
Cloud API (node-smartdc/lib/cloudapi.js). Part of this module (node-triton) is a
re-write of the Cloud API lib with some backward incompatibilities. The
differences and backward incompatibilities are discussed here.
- Currently no caching options in cloudapi2.js (this should be re-added in
some form). The `noCache` option to many of the cloudapi.js methods will not
be re-added, it was a wart.
- The leading `account` option to each cloudapi.js method has been dropped. It
was redundant for the constructor `account` option.
- "account" is now "user" in the CloudAPI constructor.
- All (all? at least at the time of this writing) methods in cloudapi2.js have
a signature of `function (options, callback)` instead of the sometimes
haphazard extra arguments.
## Development Hooks
Before commiting be sure to, at least:

View File

@ -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));
}
});
});

View File

@ -1,46 +1,46 @@
#!/usr/bin/env node
/**
* Example using cloudapi2.js to call cloudapi's ListMachines endpoint.
* Example creating a Triton API client and using it to list instances.
*
* Usage:
* ./example-list-images.js | bunyan
* ./example-list-instances.js
*
* # With trace-level logging
* LOG_LEVEL=trace ./example-list-instances.js 2>&1 | bunyan
*/
var p = console.log;
var bunyan = require('bunyan');
var path = require('path');
var triton = require('../'); // typically `require('triton');`
var URL = process.env.SDC_URL || 'https://us-sw-1.api.joyent.com';
var ACCOUNT = process.env.SDC_ACCOUNT || 'bob';
var KEY_ID = process.env.SDC_KEY_ID || 'b4:f0:b4:6c:18:3b:44:63:b4:4e:58:22:74:43:d4:bc';
var log = bunyan.createLogger({
name: 'test-list-instances',
level: process.env.LOG_LEVEL || 'trace'
name: path.basename(__filename),
level: process.env.LOG_LEVEL || 'info',
stream: process.stderr
});
/*
* More details on `createClient` options here:
* https://github.com/joyent/node-triton/blob/master/lib/index.js#L18-L61
* For example, if you want to use an existing `triton` CLI profile, you can
* pass that profile name in.
*/
var client = triton.createClient({
triton.createClient({
log: log,
profile: {
url: URL,
account: ACCOUNT,
keyId: KEY_ID
}
});
// TODO: Eventually the top-level TritonApi will have `.listInstances()` to use.
client.cloudapi.listMachines(function (err, insts) {
client.close(); // Remember to close the client to close TCP conn.
// Use 'env' to pick up 'TRITON_/SDC_' env vars. Or manually specify a
// `profile` object.
profileName: 'env',
unlockKeyFn: triton.promptPassphraseUnlockKey
}, function createdClient(err, client) {
if (err) {
console.error('listInstances err:', err);
} else {
console.log(JSON.stringify(insts, null, 4));
console.error('error creating Triton client: %s\n%s', err, err.stack);
process.exitStatus = 1;
return;
}
});
// TODO: Eventually the top-level TritonApi will have `.listInstances()`.
client.cloudapi.listMachines(function (err, insts) {
client.close(); // Remember to close the client to close TCP conn.
if (err) {
console.error('listInstances error: %s\n%s', err, err.stack);
process.exitStatus = 1;
} else {
console.log(JSON.stringify(insts, null, 4));
}
});
});

View File

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

View File

@ -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);
});
};

View File

@ -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,

View File

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

View File

@ -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();

View File

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

View File

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

View File

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

View File

@ -30,22 +30,26 @@ function do_disable(subcmd, opts, args, cb) {
return;
}
var cli = this.top;
vasync.forEachParallel({
inputs: args,
func: function disableOne(id, nextId) {
cli.tritonapi.disableFirewallRule({ id: id }, function (err) {
if (err) {
nextId(err);
return;
}
console.log('Disabled firewall rule %s', id);
nextId();
});
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
}
}, cb);
vasync.forEachParallel({
inputs: args,
func: function disableOne(id, nextId) {
tritonapi.disableFirewallRule({ id: id }, function (err) {
if (err) {
nextId(err);
return;
}
console.log('Disabled firewall rule %s', id);
nextId();
});
}
}, cb);
});
}

View File

@ -30,22 +30,26 @@ function do_enable(subcmd, opts, args, cb) {
return;
}
var cli = this.top;
vasync.forEachParallel({
inputs: args,
func: function enableOne(id, nextId) {
cli.tritonapi.enableFirewallRule({ id: id }, function (err) {
if (err) {
nextId(err);
return;
}
console.log('Enabled firewall rule %s', id);
nextId();
});
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
}
}, cb);
vasync.forEachParallel({
inputs: args,
func: function enableOne(id, nextId) {
tritonapi.enableFirewallRule({ id: id }, function (err) {
if (err) {
nextId(err);
return;
}
console.log('Enabled firewall rule %s', id);
nextId();
});
}
}, cb);
});
}

View File

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

View File

@ -54,75 +54,81 @@ function do_instances(subcmd, opts, args, cb) {
var tritonapi = this.top.tritonapi;
vasync.parallel({funcs: [
function getTheImages(next) {
tritonapi.listImages({
useCache: true,
state: 'all'
}, function (err, _imgs) {
if (err) {
next(err);
} else {
imgs = _imgs;
next();
}
});
},
function getTheMachines(next) {
tritonapi.listFirewallRuleInstances({
id: id
}, function (err, _insts) {
if (err) {
next(err);
} else {
insts = _insts;
next();
}
});
}
]}, function (err, results) {
/*
* Error handling: vasync.parallel's `err` is always a MultiError. We
* want to prefer the `getTheMachines` err, e.g. if both get a
* self-signed cert error.
*/
if (err) {
err = results.operations[1].err || err;
return cb(err);
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
}
vasync.parallel({funcs: [
function getTheImages(next) {
tritonapi.listImages({
useCache: true,
state: 'all'
}, function (err, _imgs) {
if (err) {
next(err);
} else {
imgs = _imgs;
next();
}
});
},
function getTheMachines(next) {
tritonapi.listFirewallRuleInstances({
id: id
}, function (err, _insts) {
if (err) {
next(err);
} else {
insts = _insts;
next();
}
});
}
]}, function (err, results) {
/*
* Error handling: vasync.parallel's `err` is always a
* MultiError. We want to prefer the `getTheMachines` err,
* e.g. if both get a self-signed cert error.
*/
if (err) {
err = results.operations[1].err || err;
return cb(err);
}
// map "uuid" => "image_name"
var imgmap = {};
imgs.forEach(function (img) {
imgmap[img.id] = format('%s@%s', img.name, img.version);
// map "uuid" => "image_name"
var imgmap = {};
imgs.forEach(function (img) {
imgmap[img.id] = format('%s@%s', img.name, img.version);
});
// Add extra fields for nice output.
var now = new Date();
insts.forEach(function (inst) {
var created = new Date(inst.created);
inst.age = common.longAgo(created, now);
inst.img = imgmap[inst.image] ||
common.uuidToShortId(inst.image);
inst.shortid = inst.id.split('-', 1)[0];
var flags = [];
if (inst.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K');
inst.flags = flags.length ? flags.join('') : undefined;
});
if (opts.json) {
common.jsonStream(insts);
} else {
tabula(insts, {
skipHeader: opts.H,
columns: columns,
sort: sort,
dottedLookup: true
});
}
cb();
});
// Add extra fields for nice output.
var now = new Date();
insts.forEach(function (inst) {
var created = new Date(inst.created);
inst.age = common.longAgo(created, now);
inst.img = imgmap[inst.image] || common.uuidToShortId(inst.image);
inst.shortid = inst.id.split('-', 1)[0];
var flags = [];
if (inst.docker) flags.push('D');
if (inst.firewall_enabled) flags.push('F');
if (inst.brand === 'kvm') flags.push('K');
inst.flags = flags.length ? flags.join('') : undefined;
});
if (opts.json) {
common.jsonStream(insts);
} else {
tabula(insts, {
skipHeader: opts.H,
columns: columns,
sort: sort,
dottedLookup: true
});
}
cb();
});
}

View File

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

View File

@ -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();

View File

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

View File

@ -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

View File

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

View File

@ -63,42 +63,48 @@ function do_list(subcmd, opts, args, callback) {
listOpts.state = 'all';
}
this.top.tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (err) {
return callback(err);
var tritonapi = this.top.tritonapi;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
callback(setupErr);
}
if (opts.json) {
common.jsonStream(imgs);
} else {
// Add some convenience fields
// Added fields taken from imgapi-cli.git.
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
img.shortid = img.id.split('-', 1)[0];
if (img.published_at) {
// Just the date.
img.pubdate = img.published_at.slice(0, 10);
// Normalize on no milliseconds.
img.pub = img.published_at.replace(/\.\d+Z$/, 'Z');
}
if (img.files && img.files[0]) {
img.size = img.files[0].size;
}
var flags = [];
if (img.origin) flags.push('I');
if (img['public']) flags.push('P');
if (img.state !== 'active') flags.push('X');
img.flags = flags.length ? flags.join('') : undefined;
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (err) {
return callback(err);
}
tabula(imgs, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
callback();
if (opts.json) {
common.jsonStream(imgs);
} else {
// Add some convenience fields
// Added fields taken from imgapi-cli.git.
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
img.shortid = img.id.split('-', 1)[0];
if (img.published_at) {
// Just the date.
img.pubdate = img.published_at.slice(0, 10);
// Normalize on no milliseconds.
img.pub = img.published_at.replace(/\.\d+Z$/, 'Z');
}
if (img.files && img.files[0]) {
img.size = img.files[0].size;
}
var flags = [];
if (img.origin) flags.push('I');
if (img['public']) flags.push('P');
if (img.state !== 'active') flags.push('X');
img.flags = flags.length ? flags.join('') : undefined;
}
tabula(imgs, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
callback();
});
});
}

View File

@ -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,

View File

@ -28,69 +28,75 @@ function do_info(subcmd, opts, args, callback) {
var out = {};
var i = 0;
var tritonapi = this.tritonapi;
this.tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
this.tritonapi.cloudapi.listMachines(cb.bind('machines')); i++;
function cb(err, data) {
if (err) {
callback(err);
return;
common.cliSetupTritonApi({cli: this}, function onSetup(setupErr) {
if (setupErr) {
callback(setupErr);
}
out[this.toString()] = data;
if (--i === 0)
done();
}
tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
tritonapi.cloudapi.listMachines(cb.bind('machines')); i++;
function done() {
// parse name
var name;
if (out.account.firstName && out.account.lastName)
name = format('%s %s', out.account.firstName,
out.account.lastName);
else if (out.account.firstName)
name = out.account.firstName;
// parse machine states and accounting
var states = {};
var disk = 0;
var memory = 0;
out.machines.forEach(function (machine) {
var state = machine.state;
states[state] = states[state] || 0;
states[state]++;
memory += machine.memory;
disk += machine.disk;
});
disk *= 1000 * 1000;
memory *= 1000 * 1000;
var data = {};
data.login = out.account.login;
if (name)
data.name = name;
data.email = out.account.email;
data.url = self.tritonapi.cloudapi.url;
data.totalDisk = disk;
data.totalMemory = memory;
if (opts.json) {
data.totalInstances = out.machines.length;
data.instances = states;
console.log(JSON.stringify(data));
} else {
data.totalDisk = common.humanSizeFromBytes(disk);
data.totalMemory = common.humanSizeFromBytes(memory);
Object.keys(data).forEach(function (key) {
console.log('%s: %s', key, data[key]);
});
console.log('instances: %d', out.machines.length);
Object.keys(states).forEach(function (key) {
console.log(' %s: %d', key, states[key]);
});
function cb(err, data) {
if (err) {
callback(err);
return;
}
out[this.toString()] = data;
if (--i === 0)
done();
}
callback();
}
function done() {
// parse name
var name;
if (out.account.firstName && out.account.lastName)
name = format('%s %s', out.account.firstName,
out.account.lastName);
else if (out.account.firstName)
name = out.account.firstName;
// parse machine states and accounting
var states = {};
var disk = 0;
var memory = 0;
out.machines.forEach(function (machine) {
var state = machine.state;
states[state] = states[state] || 0;
states[state]++;
memory += machine.memory;
disk += machine.disk;
});
disk *= 1000 * 1000;
memory *= 1000 * 1000;
var data = {};
data.login = out.account.login;
if (name)
data.name = name;
data.email = out.account.email;
data.url = self.tritonapi.cloudapi.url;
data.totalDisk = disk;
data.totalMemory = memory;
if (opts.json) {
data.totalInstances = out.machines.length;
data.instances = states;
console.log(JSON.stringify(data));
} else {
data.totalDisk = common.humanSizeFromBytes(disk);
data.totalMemory = common.humanSizeFromBytes(memory);
Object.keys(data).forEach(function (key) {
console.log('%s: %s', key, data[key]);
});
console.log('instances: %d', out.machines.length);
Object.keys(states).forEach(function (key) {
console.log(' %s: %d', key, states[key]);
});
}
callback();
}
});
}
do_info.options = [

View File

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

View File

@ -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,

View File

@ -14,6 +14,7 @@ var assert = require('assert-plus');
var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
@ -50,28 +51,33 @@ function do_disable_firewall(subcmd, opts, args, cb) {
});
}
vasync.forEachParallel({
inputs: args,
func: function disableOne(name, nextInst) {
cli.tritonapi.disableInstanceFirewall({
id: name
}, function (err, fauxInst) {
if (err) {
nextInst(err);
return;
}
console.log('Disabling firewall for instance "%s"', name);
if (opts.wait) {
wait(name, fauxInst.id, nextInst);
} else {
nextInst();
}
});
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
}
}, function (err) {
cb(err);
vasync.forEachParallel({
inputs: args,
func: function disableOne(name, nextInst) {
cli.tritonapi.disableInstanceFirewall({
id: name
}, function (err, fauxInst) {
if (err) {
nextInst(err);
return;
}
console.log('Disabling firewall for instance "%s"', name);
if (opts.wait) {
wait(name, fauxInst.id, nextInst);
} else {
nextInst();
}
});
}
}, function (err) {
cb(err);
});
});
}

View File

@ -14,6 +14,7 @@ var assert = require('assert-plus');
var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var errors = require('../errors');
@ -50,28 +51,33 @@ function do_enable_firewall(subcmd, opts, args, cb) {
});
}
vasync.forEachParallel({
inputs: args,
func: function enableOne(name, nextInst) {
cli.tritonapi.enableInstanceFirewall({
id: name
}, function (err, fauxInst) {
if (err) {
nextInst(err);
return;
}
console.log('Enabling firewall for instance "%s"', name);
if (opts.wait) {
wait(name, fauxInst.id, nextInst);
} else {
nextInst();
}
});
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
}
}, function (err) {
cb(err);
vasync.forEachParallel({
inputs: args,
func: function enableOne(name, nextInst) {
cli.tritonapi.enableInstanceFirewall({
id: name
}, function (err, fauxInst) {
if (err) {
nextInst(err);
return;
}
console.log('Enabling firewall for instance "%s"', name);
if (opts.wait) {
wait(name, fauxInst.id, nextInst);
} else {
nextInst();
}
});
}
}, function (err) {
cb(err);
});
});
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

@ -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) {

View File

@ -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,

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

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

View File

@ -37,37 +37,42 @@ function do_list(subcmd, opts, args, cb) {
var cli = this.top;
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) {
if (err) {
cb(err);
return;
common.cliSetupTritonApi({cli: this.top}, function onSetup(setupErr) {
if (setupErr) {
cb(setupErr);
}
if (opts.json) {
common.jsonStream(keys);
} else if (opts.authorized_keys) {
keys.forEach(function (key) {
console.log(common.chomp(key.key));
});
} else {
var columns = COLUMNS_DEFAULT;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_LONG;
cli.tritonapi.cloudapi.listKeys({}, function onKeys(err, keys) {
if (err) {
cb(err);
return;
}
columns = columns.split(',');
var sort = opts.s.split(',');
if (opts.json) {
common.jsonStream(keys);
} else if (opts.authorized_keys) {
keys.forEach(function (key) {
console.log(common.chomp(key.key));
});
} else {
var columns = COLUMNS_DEFAULT;
tabula(keys, {
skipHeader: false,
columns: columns,
sort: sort
});
}
cb();
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = COLUMNS_LONG;
}
columns = columns.split(',');
var sort = opts.s.split(',');
tabula(keys, {
skipHeader: false,
columns: columns,
sort: sort
});
}
cb();
});
});
}

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@
*/
var tabula = require('tabula');
var vasync = require('vasync');
var common = require('../common');
@ -68,73 +69,89 @@ function do_list(subcmd, opts, args, callback) {
return;
}
this.top.tritonapi.cloudapi.listPackages(listOpts, function (err, pkgs) {
if (err) {
callback(err);
return;
}
if (opts.json) {
common.jsonStream(pkgs);
} else {
for (i = 0; i < pkgs.length; i++) {
var pkg = pkgs[i];
pkg.shortid = pkg.id.split('-', 1)[0];
var context = {
cli: this.top
};
vasync.pipeline({arg: context, funcs: [
common.cliSetupTritonApi,
/*
* We take a slightly "smarter" view of "group" for default
* sorting, to accomodate usage in the JPC. More recent
* common usage is for packages to have "foo-*" naming.
* JPC includes package sets of yore *and* recent that don't
* use the "group" field. We secondarily separate those
* on a possible "foo-" prefix.
*/
pkg._groupPlus = (pkg.group || (pkg.name.indexOf('-') === -1
? '' : pkg.name.split('-', 1)[0]));
if (!opts.p) {
pkg.memoryHuman = common.humanSizeFromBytes({
precision: 1,
narrow: true
}, pkg.memory * 1024 * 1024);
pkg.swapHuman = common.humanSizeFromBytes({
precision: 1,
narrow: true
}, pkg.swap * 1024 * 1024);
pkg.diskHuman = common.humanSizeFromBytes({
precision: 1,
narrow: true
}, pkg.disk * 1024 * 1024);
pkg.vcpusHuman = pkg.vcpus === 0 ? '-' : pkg.vcpus;
}
}
if (!opts.p) {
columns = columns.map(function (c) {
switch (c.lookup || c) {
case 'memory':
return {lookup: 'memoryHuman', name: 'MEMORY',
align: 'right'};
case 'swap':
return {lookup: 'swapHuman', name: 'SWAP',
align: 'right'};
case 'disk':
return {lookup: 'diskHuman', name: 'DISK',
align: 'right'};
case 'vcpus':
return {lookup: 'vcpusHuman', name: 'VCPUS',
align: 'right'};
default:
return c;
function getThem(arg, next) {
arg.cli.tritonapi.cloudapi.listPackages(listOpts,
function (err, pkgs) {
if (err) {
next(err);
return;
}
arg.pkgs = pkgs;
next();
}
);
},
function display(arg, next) {
if (opts.json) {
common.jsonStream(arg.pkgs);
} else {
for (i = 0; i < arg.pkgs.length; i++) {
var pkg = arg.pkgs[i];
pkg.shortid = pkg.id.split('-', 1)[0];
/*
* We take a slightly "smarter" view of "group" for default
* sorting, to accomodate usage in the JPC. More recent
* common usage is for packages to have "foo-*" naming.
* JPC includes package sets of yore *and* recent that don't
* use the "group" field. We secondarily separate those
* on a possible "foo-" prefix.
*/
pkg._groupPlus = (pkg.group || (pkg.name.indexOf('-') === -1
? '' : pkg.name.split('-', 1)[0]));
if (!opts.p) {
pkg.memoryHuman = common.humanSizeFromBytes({
precision: 1,
narrow: true
}, pkg.memory * 1024 * 1024);
pkg.swapHuman = common.humanSizeFromBytes({
precision: 1,
narrow: true
}, pkg.swap * 1024 * 1024);
pkg.diskHuman = common.humanSizeFromBytes({
precision: 1,
narrow: true
}, pkg.disk * 1024 * 1024);
pkg.vcpusHuman = pkg.vcpus === 0 ? '-' : pkg.vcpus;
}
}
if (!opts.p) {
columns = columns.map(function (c) {
switch (c.lookup || c) {
case 'memory':
return {lookup: 'memoryHuman', name: 'MEMORY',
align: 'right'};
case 'swap':
return {lookup: 'swapHuman', name: 'SWAP',
align: 'right'};
case 'disk':
return {lookup: 'diskHuman', name: 'DISK',
align: 'right'};
case 'vcpus':
return {lookup: 'vcpusHuman', name: 'VCPUS',
align: 'right'};
default:
return c;
}
});
}
tabula(arg.pkgs, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
tabula(pkgs, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
next();
}
callback();
});
]}, callback);
}
do_list.options = [

View File

@ -9,6 +9,7 @@ var format = require('util').format;
var fs = require('fs');
var sshpk = require('sshpk');
var vasync = require('vasync');
var auth = require('smartdc-auth');
var common = require('../common');
var errors = require('../errors');
@ -101,17 +102,15 @@ function _createProfile(opts, cb) {
'create profile: stdout is not a TTY'));
}
var kr = new auth.KeyRing();
var keyChoices = {};
var defaults = {};
if (ctx.copy) {
defaults = ctx.copy;
delete defaults.name; // we don't copy a profile name
} else {
defaults.url = 'https://us-sw-1.api.joyent.com';
var possibleDefaultFp = '~/.ssh/id_rsa';
if (fs.existsSync(common.tildeSync(possibleDefaultFp))) {
defaults.keyId = possibleDefaultFp;
}
}
var fields = [ {
@ -156,11 +155,10 @@ function _createProfile(opts, cb) {
valCb();
}
}, {
desc: 'The fingerprint of the SSH key you have registered ' +
'for your account. Alternatively, You may enter a local ' +
'path to a public or private SSH key to have the ' +
'fingerprint calculated for you.',
default: defaults.keyId,
desc: 'The fingerprint of the SSH key you want to use, or ' +
'its index in the list above. If the key you want to ' +
'use is not listed, make sure it is either saved in your ' +
'SSH keys directory or loaded into the SSH agent.',
key: 'keyId',
validate: function validateKeyId(value, valCb) {
// First try as a fingerprint.
@ -170,44 +168,14 @@ function _createProfile(opts, cb) {
} catch (fpErr) {
}
// Try as a local path.
var keyPath = common.tildeSync(value);
fs.stat(keyPath, function (statErr, stats) {
if (statErr || !stats.isFile()) {
return valCb(new Error(format(
'"%s" is neither a valid fingerprint, ' +
'nor an existing file', value)));
}
fs.readFile(keyPath, function (readErr, keyData) {
if (readErr) {
return valCb(readErr);
}
var keyType = (keyPath.slice(-4) === '.pub'
? 'ssh' : 'pem');
try {
var key = sshpk.parseKey(keyData, keyType);
} catch (keyErr) {
return valCb(keyErr);
}
// Try as a list index
if (keyChoices[value] !== undefined) {
return valCb(null, keyChoices[value]);
}
/*
* Save the user's explicit given key path. We will
* using it later for Docker setup. Trying to use
* the same format as node-smartdc's loadSSHKey
* `keyPaths` param.
*/
ctx.keyPaths = {};
if (keyType === 'ssh') {
ctx.keyPaths.public = keyPath;
} else {
ctx.keyPaths.private = keyPath;
}
var newVal = key.fingerprint('md5').toString();
console.log('Fingerprint: %s', newVal);
valCb(null, newVal);
});
});
valCb(new Error(format(
'"%s" is neither a valid fingerprint, not an index ' +
'from the list of available keys', value)));
}
} ];
@ -234,11 +202,50 @@ function _createProfile(opts, cb) {
vasync.forEachPipeline({
inputs: fields,
func: function getField(field, nextField) {
if (field.key !== 'name') console.log();
common.promptField(field, function (err, value) {
data[field.key] = value;
nextField(err);
});
if (field.key !== 'name')
console.log();
if (field.key === 'keyId') {
kr.list(function (err, pairs) {
if (err) {
nextField(err);
return;
}
var choice = 1;
console.log('Available SSH keys:');
Object.keys(pairs).forEach(function (keyId) {
var valid = pairs[keyId].filter(function (kp) {
return (kp.canSign());
});
if (valid.length < 1)
return;
var pub = valid[0].getPublicKey();
console.log(
' %d. %d-bit %s key with fingerprint %s',
choice, pub.size, pub.type.toUpperCase(),
keyId);
pairs[keyId].forEach(function (kp) {
var comment = kp.comment ||
kp.getPublicKey().comment;
console.log(' * [in %s] %s %s %s',
kp.plugin, comment,
(kp.source ? kp.source : ''),
(kp.isLocked() ? '[locked]' : ''));
});
console.log();
keyChoices[choice] = keyId;
++choice;
});
common.promptField(field, function (err2, value) {
data[field.key] = value;
nextField(err2);
});
});
} else {
common.promptField(field, function (err, value) {
data[field.key] = value;
nextField(err);
});
}
}
}, function (err) {
console.log();

View File

@ -8,6 +8,7 @@ var assert = require('assert-plus');
var auth = require('smartdc-auth');
var format = require('util').format;
var fs = require('fs');
var getpass = require('getpass');
var https = require('https');
var mkdirp = require('mkdirp');
var path = require('path');
@ -143,14 +144,38 @@ function profileDockerSetup(opts, cb) {
assert.optionalObject(opts.keyPaths, 'opts.keyPaths');
assert.func(cb, 'cb');
var implicit = Boolean(opts.implicit);
var cli = opts.cli;
var log = cli.log;
var tritonapi = cli.tritonapiFromProfileName({profileName: opts.name});
var implicit = Boolean(opts.implicit);
var log = cli.log;
var profile = tritonapi.profile;
var dockerHost;
vasync.pipeline({arg: {}, funcs: [
vasync.pipeline({arg: {tritonapi: tritonapi}, funcs: [
function dockerKeyWarning(arg, next) {
console.log(wordwrap(
'\nWARNING: Docker uses TLS-based authentication with a ' +
'different security model from SSH keys. As a result, the ' +
'Docker client cannot currently support encrypted ' +
'(password protected) keys or SSH agents. If you ' +
'continue, the Triton CLI will attempt to format a copy ' +
'of your SSH *private* key as an unencrypted TLS cert ' +
'and place the copy in ~/.triton/docker for use by the ' +
'Docker client.'));
common.promptYesNo({msg: 'Continue? [y/n] '}, function (answer) {
if (answer !== 'y') {
console.error('Aborting');
next(true);
} else {
next();
}
});
},
common.cliSetupTritonApi,
function checkCloudapiStatus(arg, next) {
tritonapi.cloudapi.ping({}, function (err, pong, res) {
if (!res) {
@ -222,69 +247,16 @@ function profileDockerSetup(opts, cb) {
next();
},
function findSshPrivKey_keyPaths(arg, next) {
if (!opts.keyPaths) {
next();
function checkSshPrivKey(arg, next) {
try {
tritonapi.keyPair.getPrivateKey();
} catch (e) {
next(new errors.SetupError(format('could not obtain SSH ' +
'private key for keypair with fingerprint "%s" ' +
'to create Docker certificate.', profile.keyId)));
return;
}
var privKeyPath = opts.keyPaths.private;
if (!privKeyPath) {
assert.string(opts.keyPaths.public);
assert.ok(opts.keyPaths.public.slice(-4) === '.pub');
privKeyPath = opts.keyPaths.public.slice(0, -4);
if (!fs.existsSync(privKeyPath)) {
cb(new errors.SetupError(format('could not find SSH '
+ 'private key file from public key file "%s": "%s" '
+ 'does not exist', opts.keyPaths.public,
privKeyPath)));
return;
}
}
arg.sshKeyPaths = {
private: privKeyPath,
public: opts.keyPaths.public
};
fs.readFile(privKeyPath, function (readErr, keyData) {
if (readErr) {
cb(readErr);
return;
}
try {
arg.sshPrivKey = sshpk.parseKey(keyData, 'pem');
} catch (keyErr) {
cb(keyErr);
return;
}
log.trace({sshKeyPaths: arg.sshKeyPaths},
'profileDockerSetup: findSshPrivKey_keyPaths');
next();
});
},
function findSshPrivKey_keyId(arg, next) {
if (opts.keyPaths) {
next();
return;
}
// TODO: keyPaths here is using a non-#master of node-smartdc-auth.
// Change back to a smartdc-auth release when
// https://github.com/joyent/node-smartdc-auth/pull/5 is in.
auth.loadSSHKey(profile.keyId, function (err, key, keyPaths) {
if (err) {
// TODO: better error message here.
next(err);
} else {
assert.ok(key, 'key from auth.loadSSHKey');
log.trace({keyId: profile.keyId, sshKeyPaths: keyPaths},
'profileDockerSetup: findSshPrivKey');
arg.sshKeyPaths = keyPaths;
arg.sshPrivKey = key;
next();
}
});
next();
},
/*
@ -348,31 +320,32 @@ function profileDockerSetup(opts, cb) {
},
function genClientCert_key(arg, next) {
arg.keyPath = path.resolve(arg.dockerCertPath, 'key.pem');
common.execPlus({
cmd: format('openssl rsa -in %s -out %s -outform pem',
arg.sshKeyPaths.private, arg.keyPath),
log: log
}, next);
},
function genClientCert_csr(arg, next) {
arg.csrPath = path.resolve(arg.dockerCertPath, 'csr.pem');
common.execPlus({
cmd: format('openssl req -new -key %s -out %s -subj "/CN=%s"',
arg.keyPath, arg.csrPath, profile.account),
log: log
}, next);
var data = tritonapi.keyPair.getPrivateKey().toBuffer('pkcs1');
fs.writeFile(arg.keyPath, data, function (err) {
if (err) {
next(new errors.SetupError(err, format(
'error writing file %s', arg.keyPath)));
} else {
next();
}
});
},
function genClientCert_cert(arg, next) {
arg.certPath = path.resolve(arg.dockerCertPath, 'cert.pem');
common.execPlus({
cmd: format(
'openssl x509 -req -days 365 -in %s -signkey %s -out %s',
arg.csrPath, arg.keyPath, arg.certPath),
log: log
}, next);
},
function genClientCert_deleteCsr(arg, next) {
rimraf(arg.csrPath, next);
var privKey = tritonapi.keyPair.getPrivateKey();
var id = sshpk.identityFromDN('CN=' + profile.account);
var cert = sshpk.createSelfSignedCertificate(id, privKey);
var data = cert.toBuffer('pem');
fs.writeFile(arg.certPath, data, function (err) {
if (err) {
next(new errors.SetupError(err, format(
'error writing file %s', arg.keyPath)));
} else {
next();
}
});
},
function getServerCa(arg, next) {

View File

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

View File

@ -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;
var config = opts.config;
if (!config) {
config = mod_config.loadConfig({configDir: opts.configDir});
}
vasync.pipeline({funcs: [
function theSyncPart(_, next) {
log = opts.log || new bunyannoop.BunyanNoopLogger();
var profile = opts.profile;
if (!profile) {
profile = mod_config.loadProfile({
name: opts.profileName,
configDir: config._configDir
});
}
// Don't require one to arbitrarily have a profile.name if manually
// creating it.
if (!profile.name) {
// TODO: might want this to be hash or slug of profile params.
profile.name = '_';
}
mod_config.validateProfile(profile);
var config;
if (opts.config) {
config = opts.config;
} else {
try {
config = mod_config.loadConfig(
{configDir: opts.configDir});
} catch (configErr) {
next(configErr);
return;
}
}
var client = tritonapi.createClient({
log: log,
config: config,
profile: profile
var profile;
if (opts.profile) {
profile = opts.profile;
/*
* Don't require one to arbitrarily have a profile.name if
* manually creating it.
*/
if (!profile.name) {
// TODO: might want this to be a hash/slug of params.
profile.name = '_';
}
} else {
try {
profile = mod_config.loadProfile({
name: opts.profileName,
configDir: config._configDir
});
} catch (profileErr) {
next(profileErr);
return;
}
}
try {
mod_config.validateProfile(profile);
} catch (valErr) {
next(valErr);
return;
}
client = mod_tritonapi.createClient({
log: log,
config: config,
profile: profile
});
next();
},
function initTheClient(_, next) {
client.init(next);
},
function optionallyUnlockKey(_, next) {
if (!opts.unlockKeyFn) {
next();
return;
}
opts.unlockKeyFn({tritonapi: client}, next);
}
]}, function (err) {
log.trace({err: err}, 'createClient complete');
if (err) {
cb(err);
} else {
cb(null, client);
}
});
return client;
}
module.exports = {
createClient: createClient,
promptPassphraseUnlockKey: mod_common.promptPassphraseUnlockKey,
/**
* `createClient` provides convenience parameters to not *have* to call
@ -159,7 +262,10 @@ module.exports = {
loadProfile: mod_config.loadProfile,
loadAllProfiles: mod_config.loadAllProfiles,
createTritonApiClient: tritonapi.createClient,
// For those wanting a lower-level raw CloudAPI client.
createCloudApiClient: require('./cloudapi2').createClient
/*
* For those wanting a lower-level TritonApi createClient, or an
* even *lower*-level raw CloudAPI client.
*/
createTritonApiClient: mod_tritonapi.createClient,
createCloudApiClient: mod_cloudapi2.createClient
};

View File

@ -6,10 +6,81 @@
/*
* Copyright 2016 Joyent, Inc.
*
* Core TritonApi client driver class.
*/
/* BEGIN JSSTYLED */
/*
* Core `TritonApi` client class. A TritonApi client object is a wrapper around
* a lower-level `CloudApi` client that makes raw calls to
* [Cloud API](https://apidocs.joyent.com/cloudapi/). The wrapper provides
* some conveniences, for example:
* - referring to resources by "shortId" (8-char UUID prefixes) or "name"
* (e.g. an VM instance has a unique name for an account, but the raw
* Cloud API only supports lookup by full UUID);
* - filling in of image details for instances which only have an "image_uuid"
* in Cloud API responses;
* - support for waiting for async operations to complete via "wait" parameters;
* - profile handling.
*
* Preparing a TritonApi is a three-step process. (Note: Some users might
* prefer to use the `createClient` convenience function in "index.js" that
* wraps up all three steps into a single call.)
*
* 1. Create the client object.
* 2. Initialize it (mainly involves finding the SSH key identified by the
* `keyId`).
* 3. Optionally, unlock the SSH key (if it is passphrase-protected and not in
* an ssh-agent). If you know that your key is not passphrase-protected
* or is an ssh-agent, then you can skip this step. The failure mode for
* a locked key looks like this:
* SigningError: error signing request: SSH private key id_rsa is locked (encrypted/password-protected). It must be unlocked before use.
* at SigningError._TritonBaseVError (/Users/trentm/tmp/node-triton/lib/errors.js:55:12)
* at new SigningError (/Users/trentm/tmp/node-triton/lib/errors.js:173:23)
* at CloudApi._getAuthHeaders (/Users/trentm/tmp/node-triton/lib/cloudapi2.js:185:22)
*
* Usage:
* var mod_triton = require('triton');
*
* // 1. Create the TritonApi instance.
* var client = mod_triton.createTritonApiClient({
* log: log,
* profile: profile, // See mod_triton.loadProfile
* config: config // See mod_triton.loadConfig
* });
*
* // 2. Call `init` to setup the profile. This involves finding the SSH
* // key identified by the profile's keyId.
* client.init(function (initErr) {
* if (initErr) boom(initErr);
*
* // 3. Unlock the SSH key, if necessary. Possibilities are:
* // (a) Skip this step. If the key is locked, you will get a
* // "SigningError" at first attempt to sign. See example above.
* // (b) The key is not locked.
* // `client.keyPair.isLocked() === false`
* // (c) You have a passphrase for the key:
* if (client.keyPair.isLocked()) {
* // This throws if the passphrase is incorrect.
* client.keyPair.unlock(passphrase);
* }
*
* // (d) Or you use a function that will prompt for a passphrase
* // and unlock with that. E.g., `promptPassphraseUnlockKey`
* // is one provided by this package that with prompt on stdin.
* mod_triton.promptPassphraseUnlockKey({
* tritonapi: client
* }, function (unlockErr) {
* if (unlockErr) boom(unlockErr);
*
* // 4. Now you can finally make an API call. For example:
* client.listImages(function (err, imgs) {
* // ...
* });
* });
* });
*/
/* END JSSTYLED */
var assert = require('assert-plus');
var auth = require('smartdc-auth');
var EventEmitter = require('events').EventEmitter;
@ -24,6 +95,7 @@ var restifyBunyanSerializers =
require('restify-clients/lib/helpers/bunyan').serializers;
var tabula = require('tabula');
var vasync = require('vasync');
var sshpk = require('sshpk');
var cloudapi = require('./cloudapi2');
var common = require('./common');
@ -116,6 +188,14 @@ function _stepFwRuleId(arg, next) {
/**
* Create a TritonApi client.
*
* Public properties (TODO: doc all of these):
* - profile
* - config
* - log
* - cacheDir (only available if configured with a configDir)
* - keyPair (available after init)
* - cloudapi (available after init)
*
* @param opts {Object}
* - log {Bunyan Logger}
* ...
@ -128,6 +208,7 @@ function TritonApi(opts) {
this.profile = opts.profile;
this.config = opts.config;
this.keyPair = null;
// Make sure a given bunyan logger has reasonable client_re[qs] serializers.
// Note: This was fixed in restify, then broken again in
@ -147,29 +228,43 @@ function TritonApi(opts) {
this.config.cacheDir,
common.profileSlug(this.profile));
this.log.trace({cacheDir: this.cacheDir}, 'cache dir');
// TODO perhaps move this to an async .init()
if (!fs.existsSync(this.cacheDir)) {
try {
mkdirp.sync(this.cacheDir);
} catch (e) {
throw e;
}
}
}
this.cloudapi = this._cloudapiFromProfile(this.profile);
}
TritonApi.prototype.close = function close() {
this.cloudapi.close();
delete this.cloudapi;
if (this.cloudapi) {
this.cloudapi.close();
delete this.cloudapi;
}
};
TritonApi.prototype._cloudapiFromProfile =
function _cloudapiFromProfile(profile)
{
TritonApi.prototype.init = function init(cb) {
var self = this;
if (this.cacheDir) {
fs.exists(this.cacheDir, function (exists) {
if (!exists) {
mkdirp(self.cacheDir, function (err) {
if (err) {
cb(err);
return;
}
self._setupProfile(cb);
});
} else {
self._setupProfile(cb);
}
});
} else {
self._setupProfile(cb);
}
};
TritonApi.prototype._setupProfile = function _setupProfile(cb) {
var self = this;
var profile = this.profile;
assert.object(profile, 'profile');
assert.string(profile.account, 'profile.account');
assert.optionalString(profile.actAsAccount, 'profile.actAsAccount');
@ -185,32 +280,39 @@ TritonApi.prototype._cloudapiFromProfile =
? true : !profile.insecure);
var acceptVersion = profile.acceptVersion || CLOUDAPI_ACCEPT_VERSION;
var sign;
if (profile.privKey) {
sign = auth.privateKeySigner({
user: profile.account,
subuser: profile.user,
keyId: profile.keyId,
key: profile.privKey
});
} else {
sign = auth.cliSigner({
keyId: profile.keyId,
user: profile.account,
subuser: profile.user
});
}
var client = cloudapi.createClient({
var opts = {
url: profile.url,
account: profile.actAsAccount || profile.account,
user: profile.user,
principal: {
account: profile.account,
user: profile.user
},
roles: profile.roles,
version: acceptVersion,
rejectUnauthorized: rejectUnauthorized,
sign: sign,
log: this.log
});
return client;
};
if (profile.privKey) {
var key = sshpk.parsePrivateKey(profile.privKey);
this.keyPair =
opts.principal.keyPair =
auth.KeyPair.fromPrivateKey(key);
this.cloudapi = cloudapi.createClient(opts);
cb(null);
} else {
var kr = new auth.KeyRing();
var fp = sshpk.parseFingerprint(profile.keyId);
kr.findSigningKeyPair(fp, function (err, kp) {
if (err) {
cb(err);
return;
}
self.keyPair = opts.principal.keyPair = kp;
self.cloudapi = cloudapi.createClient(opts);
cb(null);
});
}
};

View File

@ -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",

View File

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

View File

@ -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();

View File

@ -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();

View File

@ -5,7 +5,7 @@
*/
/*
* Copyright (c) 2015, Joyent, Inc.
* Copyright 2016 Joyent, Inc.
*/
/*
@ -30,9 +30,14 @@ var PKG;
test('TritonApi packages', function (tt) {
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();

View File

@ -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);
}