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