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