Browse Source

joyent/node-triton#108 support passphrase protected keys

Reviewed by: Trent Mick <trent.mick@joyent.com>
Approved by: Trent Mick <trent.mick@joyent.com>
pull/139/merge
Chris Burroughs 2 years ago
parent
commit
ad7d608011
63 changed files with 1873 additions and 1225 deletions
  1. 77
    0
      CHANGES.md
  2. 27
    30
      README.md
  3. 33
    30
      examples/example-get-account.js
  4. 31
    31
      examples/example-list-instances.js
  5. 39
    26
      lib/cli.js
  6. 68
    24
      lib/cloudapi2.js
  7. 96
    0
      lib/common.js
  8. 26
    20
      lib/do_account/do_get.js
  9. 3
    1
      lib/do_account/do_update.js
  10. 32
    26
      lib/do_datacenters.js
  11. 14
    8
      lib/do_fwrule/do_create.js
  12. 5
    4
      lib/do_fwrule/do_delete.js
  13. 19
    15
      lib/do_fwrule/do_disable.js
  14. 19
    15
      lib/do_fwrule/do_enable.js
  15. 18
    13
      lib/do_fwrule/do_get.js
  16. 71
    65
      lib/do_fwrule/do_instances.js
  17. 34
    29
      lib/do_fwrule/do_list.js
  18. 3
    1
      lib/do_fwrule/do_update.js
  19. 22
    20
      lib/do_image/do_create.js
  20. 2
    1
      lib/do_image/do_delete.js
  21. 16
    9
      lib/do_image/do_get.js
  22. 38
    32
      lib/do_image/do_list.js
  23. 3
    1
      lib/do_image/do_wait.js
  24. 64
    58
      lib/do_info.js
  25. 15
    14
      lib/do_instance/do_audit.js
  26. 10
    10
      lib/do_instance/do_create.js
  27. 27
    21
      lib/do_instance/do_disable_firewall.js
  28. 27
    21
      lib/do_instance/do_enable_firewall.js
  29. 35
    30
      lib/do_instance/do_fwrules.js
  30. 14
    8
      lib/do_instance/do_get.js
  31. 17
    11
      lib/do_instance/do_ip.js
  32. 80
    71
      lib/do_instance/do_list.js
  33. 2
    1
      lib/do_instance/do_snapshot/do_create.js
  34. 2
    1
      lib/do_instance/do_snapshot/do_delete.js
  35. 20
    15
      lib/do_instance/do_snapshot/do_get.js
  36. 31
    26
      lib/do_instance/do_snapshot/do_list.js
  37. 47
    40
      lib/do_instance/do_ssh.js
  38. 40
    34
      lib/do_instance/do_tag/do_delete.js
  39. 19
    13
      lib/do_instance/do_tag/do_get.js
  40. 17
    10
      lib/do_instance/do_tag/do_list.js
  41. 3
    1
      lib/do_instance/do_tag/do_replace_all.js
  42. 3
    1
      lib/do_instance/do_tag/do_set.js
  43. 3
    1
      lib/do_instance/do_wait.js
  44. 11
    3
      lib/do_instance/gen_do_ACTION.js
  45. 2
    1
      lib/do_key/do_add.js
  46. 2
    1
      lib/do_key/do_delete.js
  47. 20
    15
      lib/do_key/do_get.js
  48. 33
    28
      lib/do_key/do_list.js
  49. 16
    9
      lib/do_network/do_get.js
  50. 24
    18
      lib/do_network/do_list.js
  51. 17
    10
      lib/do_package/do_get.js
  52. 79
    62
      lib/do_package/do_list.js
  53. 59
    52
      lib/do_profile/do_create.js
  54. 59
    86
      lib/do_profile/profilecommon.js
  55. 33
    27
      lib/do_services.js
  56. 168
    62
      lib/index.js
  57. 138
    36
      lib/tritonapi.js
  58. 4
    2
      package.json
  59. 6
    4
      test/integration/api-images.test.js
  60. 9
    7
      test/integration/api-instances.test.js
  61. 8
    8
      test/integration/api-networks.test.js
  62. 8
    3
      test/integration/api-packages.test.js
  63. 5
    3
      test/integration/helpers.js

+ 77
- 0
CHANGES.md 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

+ 27
- 30
README.md 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:

var triton = require('triton');
A basic example appropriate for a command-line tool is:

```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:

+ 33
- 30
examples/example-get-account.js 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));
}
});
});

+ 31
- 31
examples/example-list-instances.js 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));
}
});
});

+ 39
- 26
lib/cli.js 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

+ 68
- 24
lib/cloudapi2.js 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);
});
};


+ 96
- 0
lib/common.js 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');
@@ -679,6 +680,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,

+ 26
- 20
lib/do_account/do_get.js 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();
});
});
}


+ 3
- 1
lib/do_account/do_update.js 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();

+ 32
- 26
lib/do_datacenters.js 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();
});
});
}


+ 14
- 8
lib/do_fwrule/do_create.js 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();
});
});
}


+ 5
- 4
lib/do_fwrule/do_delete.js 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);

+ 19
- 15
lib/do_fwrule/do_disable.js 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);
});
}



+ 19
- 15
lib/do_fwrule/do_enable.js 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);
});
}



+ 18
- 13
lib/do_fwrule/do_get.js 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);
}

if (opts.json) {
console.log(JSON.stringify(fwrule));
} else {
console.log(JSON.stringify(fwrule, null, 4));
}

cb();
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));
}

cb();
});
});
}


+ 71
- 65
lib/do_fwrule/do_instances.js 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
// 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;
});
}

cb();
if (opts.json) {
common.jsonStream(insts);
} else {
tabula(insts, {
skipHeader: opts.H,
columns: columns,
sort: sort,
dottedLookup: true
});
}

cb();
});
});
}


+ 34
- 29
lib/do_fwrule/do_list.js 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 (columns.indexOf('shortid') !== -1) {
rules.forEach(function (rule) {
rule.shortid = common.uuidToShortId(rule.id);
if (opts.json) {
common.jsonStream(rules);
} else {
var columns = COLUMNS_DEFAULT;

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


+ 3
- 1
lib/do_fwrule/do_update.js 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();

+ 22
- 20
lib/do_image/do_create.js 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,

+ 2
- 1
lib/do_image/do_delete.js 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

+ 16
- 9
lib/do_image/do_get.js 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();
});
});
}


+ 38
- 32
lib/do_image/do_list.js 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);
}
tritonapi.listImages(listOpts, function onRes(err, imgs, res) {
if (err) {
return callback(err);
}

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;
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;
}
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();
tabula(imgs, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
callback();
});
});
}


+ 3
- 1
lib/do_image/do_wait.js 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,

+ 64
- 58
lib/do_info.js 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);
}
tritonapi.cloudapi.getAccount(cb.bind('account')); i++;
tritonapi.cloudapi.listMachines(cb.bind('machines')); i++;

function cb(err, data) {
if (err) {
callback(err);
return;
}
out[this.toString()] = data;
if (--i === 0)
done();
}
out[this.toString()] = data;
if (--i === 0)
done();
}

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 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();
}
callback();
}
});
}

do_info.options = [

+ 15
- 14
lib/do_instance/do_audit.js 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;

+ 10
- 10
lib/do_instance/do_create.js 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,

+ 27
- 21
lib/do_instance/do_disable_firewall.js 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);
});
});
}


+ 27
- 21
lib/do_instance/do_enable_firewall.js 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);
});
});
}


+ 35
- 30
lib/do_instance/do_fwrules.js 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 (columns.indexOf('shortid') !== -1) {
rules.forEach(function (rule) {
rule.shortid = common.normShortId(rule.id);
if (opts.json) {
common.jsonStream(rules);
} else {
var columns = COLUMNS_DEFAULT;

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


+ 14
- 8
lib/do_instance/do_get.js 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));