'triton profile{,s}' all except 'triton profile -a'

This commit is contained in:
Trent Mick 2015-09-25 12:19:29 -07:00
parent 403e4bd204
commit bf21ac467a
7 changed files with 664 additions and 128 deletions

View File

@ -59,8 +59,7 @@ var OPTIONS = [
type: 'string',
env: 'TRITON_PROFILE',
helpArg: 'NAME',
help: 'Triton client profile to use.',
hidden: true // TODO: hidden until profiles impl is complete
help: 'Triton client profile to use.'
},
{
@ -145,8 +144,8 @@ function CLI() {
},
helpSubcmds: [
'help',
// TODO: hide until the command is fully implemented:
// 'profiles',
'profiles',
'profile',
{ group: 'Other Commands' },
'info',
'account',
@ -259,6 +258,7 @@ CLI.prototype._applyProfileOverrides =
// Meta
CLI.prototype.do_completion = require('./do_completion');
CLI.prototype.do_profiles = require('./do_profiles');
CLI.prototype.do_profile = require('./do_profile');
// Other
CLI.prototype.do_account = require('./do_account');

View File

@ -9,6 +9,12 @@
*/
var assert = require('assert-plus');
var child_process = require('child_process');
var fs = require('fs');
var os = require('os');
var path = require('path');
var read = require('read');
var tty = require('tty');
var util = require('util'),
format = util.format;
@ -16,10 +22,6 @@ var errors = require('./errors'),
InternalError = errors.InternalError;
// ---- globals
var p = console.log;
// ---- support stuff
function objCopy(obj, target) {
@ -417,6 +419,158 @@ function getCliTableOptions(opts) {
}
/**
* Prompt a user for a y/n answer.
*
* cb('y') user entered in the affirmative
* cb('n') user entered in the negative
* cb(false) user ^C'd
*
* Dev Note: Borrowed from imgadm's common.js. If this starts showing issues,
* we should consider using the npm 'read' module.
*/
function promptYesNo(opts_, cb) {
assert.object(opts_, 'opts');
assert.string(opts_.msg, 'opts.msg');
assert.optionalString(opts_.default, 'opts.default');
var opts = objCopy(opts_);
// Setup stdout and stdin to talk to the controlling terminal if
// process.stdout or process.stdin is not a TTY.
var stdout;
if (opts.stdout) {
stdout = opts.stdout;
} else if (process.stdout.isTTY) {
stdout = process.stdout;
} else {
opts.stdout_fd = fs.openSync('/dev/tty', 'r+');
stdout = opts.stdout = new tty.WriteStream(opts.stdout_fd);
}
var stdin;
if (opts.stdin) {
stdin = opts.stdin;
} else if (process.stdin.isTTY) {
stdin = process.stdin;
} else {
opts.stdin_fd = fs.openSync('/dev/tty', 'r+');
stdin = opts.stdin = new tty.ReadStream(opts.stdin_fd);
}
stdout.write(opts.msg);
stdin.setEncoding('utf8');
stdin.setRawMode(true);
stdin.resume();
var input = '';
stdin.on('data', onData);
function postInput() {
stdin.setRawMode(false);
stdin.pause();
stdin.write('\n');
stdin.removeListener('data', onData);
}
function finish(rv) {
if (opts.stdout_fd !== undefined) {
stdout.end();
delete opts.stdout_fd;
}
if (opts.stdin_fd !== undefined) {
stdin.end();
delete opts.stdin_fd;
}
cb(rv);
}
function onData(ch) {
ch = ch + '';
switch (ch) {
case '\n':
case '\r':
case '\u0004':
// They've finished typing their answer
postInput();
var answer = input.toLowerCase();
if (answer === '' && opts.default) {
finish(opts.default);
} else if (answer === 'yes' || answer === 'y') {
finish('y');
} else if (answer === 'no' || answer === 'n') {
finish('n');
} else {
stdout.write('Please enter "y", "yes", "n" or "no".\n');
promptYesNo(opts, cb);
return;
}
break;
case '\u0003':
// Ctrl C
postInput();
finish(false);
break;
default:
// More plaintext characters
stdout.write(ch);
input += ch;
break;
}
}
}
/*
* Prompt and wait for <Enter> or Ctrl+C. Usage:
*
* common.promptEnter('Press <Enter> to re-edit, Ctrl+C to abort.',
* function (err) {
* if (err) {
* // User hit Ctrl+C
* } else {
* // User hit Enter
* }
* }
* );
*/
function promptEnter(prompt, cb) {
read({
prompt: prompt
}, function (err, result, isDefault) {
cb(err);
});
}
/**
* Edit the given text in $EDITOR (defaulting to `vi`) and return the edited
* text.
*
* This callback with `cb(err, updatedText, changed)` where `changed`
* is a boolean true if the text was changed.
*/
function editInEditor(opts, cb) {
assert.string(opts.text, 'opts.text');
assert.optionalString(opts.filename, 'opts.filename');
assert.func(cb, 'cb');
var tmpPath = path.resolve(os.tmpDir(),
format('triton-%s-edit-%s', process.pid, opts.filename || 'text'));
fs.writeFileSync(tmpPath, opts.text, 'utf8');
// TODO: want '-f' opt for vi? What about others?
var editor = process.env.EDITOR || '/usr/bin/vi';
var kid = child_process.spawn(editor, [tmpPath], {stdio: 'inherit'});
kid.on('exit', function (code) {
if (code) {
return (cb(code));
}
var afterText = fs.readFileSync(tmpPath, 'utf8');
fs.unlinkSync(tmpPath);
cb(null, afterText, (afterText !== opts.text));
});
}
//---- exports
module.exports = {
@ -434,6 +588,9 @@ module.exports = {
normShortId: normShortId,
uuidToShortId: uuidToShortId,
slug: slug,
getCliTableOptions: getCliTableOptions
getCliTableOptions: getCliTableOptions,
promptYesNo: promptYesNo,
promptEnter: promptEnter,
editInEditor: editInEditor
};
// vim: set softtabstop=4 shiftwidth=4:

View File

@ -307,6 +307,39 @@ function loadAllProfiles(opts) {
return profiles;
}
function deleteProfile(opts) {
assert.string(opts.configDir, 'opts.configDir');
assert.string(opts.name, 'opts.name');
if (opts.name === 'env') {
throw new Error('cannot delete "env" profile');
}
var profilePath = path.resolve(opts.configDir, 'profiles.d',
opts.name + '.json');
fs.unlinkSync(profilePath);
}
function saveProfileSync(opts) {
assert.string(opts.configDir, 'opts.configDir');
assert.string(opts.name, 'opts.name');
assert.object(opts.profile, 'opts.profile');
if (opts.name === 'env') {
throw new Error('cannot save "env" profile');
}
_validateProfile(opts.profile);
var toSave = common.objCopy(opts.profile);
delete toSave.name;
var profilePath = path.resolve(opts.configDir, 'profiles.d',
opts.name + '.json');
fs.writeFileSync(profilePath, JSON.stringify(toSave, null, 4), 'utf8');
console.log('Saved profile "%s"', opts.name);
}
//---- exports
@ -314,6 +347,8 @@ module.exports = {
loadConfig: loadConfig,
setConfigVar: setConfigVar,
loadProfile: loadProfile,
loadAllProfiles: loadAllProfiles
loadAllProfiles: loadAllProfiles,
deleteProfile: deleteProfile,
saveProfileSync: saveProfileSync
};
// vim: set softtabstop=4 shiftwidth=4:

410
lib/do_profile.js Normal file
View File

@ -0,0 +1,410 @@
/*
* Copyright (c) 2015 Joyent Inc.
*
* `triton profile ...`
*/
var assert = require('assert-plus');
var format = require('util').format;
var strsplit = require('strsplit');
var vasync = require('vasync');
var common = require('./common');
var errors = require('./errors');
var mod_config = require('./config');
function _showProfile(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.name, 'opts.name');
assert.func(cb, 'cb');
var cli = opts.cli;
try {
var profile = mod_config.loadProfile({
configDir: cli.configDir,
name: opts.name
});
} catch (err) {
return cb(err);
}
if (profile.name === cli.tritonapi.profile.name) {
cli._applyProfileOverrides(profile);
profile.curr = true;
} else {
profile.curr = false;
}
if (opts.json) {
console.log(JSON.stringify(profile));
} else {
Object.keys(profile).sort().forEach(function (key) {
var val = profile[key];
console.log('%s: %s', key, val);
});
}
}
function _currentProfile(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.name, 'opts.name');
assert.func(cb, 'cb');
var cli = opts.cli;
try {
var profile = mod_config.loadProfile({
configDir: cli.configDir,
name: opts.name
});
} catch (err) {
return cb(err);
}
if (cli.tritonapi.profile.name === profile.name) {
console.log('"%s" is already the current profile', profile.name);
return cb();
}
mod_config.setConfigVar({
configDir: cli.configDir,
name: 'profile',
value: profile.name
}, function (err) {
if (err) {
return cb(err);
}
console.log('Set "%s" as current profile', profile.name);
cb();
});
}
function _yamlishFromProfile(profile) {
assert.object(profile, 'profile');
var keys = [];
var skipKeys = ['curr', 'name'];
Object.keys(profile).forEach(function (key) {
if (skipKeys.indexOf(key) === -1) {
keys.push(key);
}
});
keys = keys.sort();
var lines = [];
keys.forEach(function (key) {
lines.push(format('%s: %s', key, profile[key]));
});
return lines.join('\n') + '\n';
}
function _profileFromYamlish(yamlish) {
assert.string(yamlish, 'yamlish');
var profile = {};
var bools = ['insecure'];
var lines = yamlish.split(/\n/g);
lines.forEach(function (line) {
var commentIdx = line.indexOf('#');
if (commentIdx !== -1) {
line = line.slice(0, commentIdx);
}
line = line.trim();
if (!line) {
return;
}
var parts = strsplit(line, ':', 2);
var key = parts[0].trim();
var value = parts[1].trim();
if (bools.indexOf(key) !== -1) {
value = common.boolFromString(value);
}
profile[key] = value;
});
return profile;
}
function _editProfile(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.name, 'opts.name');
assert.func(cb, 'cb');
var cli = opts.cli;
if (opts.name === 'env') {
return cb(new errors.UsageError('cannot edit "env" profile'));
}
try {
var profile = mod_config.loadProfile({
configDir: cli.configDir,
name: opts.name
});
} catch (err) {
return cb(err);
}
var filename = format('profile-%s.txt', profile.name);
var origText = _yamlishFromProfile(profile);
function editAttempt(text) {
common.editInEditor({
text: text,
filename: filename
}, function (err, afterText, changed) {
if (err) {
return cb(new errors.TritonError(err));
} else if (!changed) {
console.log('No change to profile');
return cb();
}
try {
var editedProfile = _profileFromYamlish(afterText);
editedProfile.name = profile.name;
if (_yamlishFromProfile(editedProfile) === origText) {
// This YAMLish is the closest to a canonical form we have.
console.log('No change to profile');
return cb();
}
mod_config.saveProfileSync({
configDir: cli.configDir,
name: opts.name,
profile: editedProfile
});
} catch (textErr) {
console.error('Error with your changes: %s', textErr);
common.promptEnter(
'Press <Enter> to re-edit, Ctrl+C to abort.',
function (aborted) {
if (aborted) {
console.log('\nAborting. ' +
'No change made to profile');
cb();
} else {
editAttempt(afterText);
}
});
return;
}
cb();
});
}
editAttempt(origText);
}
function _deleteProfile(opts, cb) {
assert.object(opts.cli, 'opts.cli');
assert.string(opts.name, 'opts.name');
assert.bool(opts.force, 'opts.force');
assert.func(cb, 'cb');
var cli = opts.cli;
if (opts.name === 'env') {
return cb(new errors.UsageError('cannot delete "env" profile'));
}
try {
var profile = mod_config.loadProfile({
configDir: cli.configDir,
name: opts.name
});
} catch (err) {
if (opts.force) {
cb();
} else {
cb(err);
}
return;
}
if (profile.name === cli.tritonapi.profile.name && !opts.force) {
return cb(new errors.TritonError(
'cannot delete the current profile (use --force to override)'));
}
vasync.pipeline({funcs: [
function confirm(_, next) {
if (opts.force) {
return next();
}
common.promptYesNo({
msg: 'Delete profile "' + opts.name + '"? [y/n] '
}, function (answer) {
if (answer !== 'y') {
console.error('Aborting');
next(true); // early abort signal
} else {
next();
}
});
},
function handleConfigVar(_, next) {
if (profile.name === cli.tritonapi.profile.name) {
_currentProfile({name: 'env', cli: cli}, next);
} else {
next();
}
},
function deleteIt(_, next) {
try {
mod_config.deleteProfile({
configDir: cli.configDir,
name: opts.name
});
} catch (delErr) {
return next(delErr);
}
console.log('Deleted profile "%s"', opts.name);
next();
}
]}, function (err) {
if (err === true) {
err = null;
}
cb(err);
});
}
function _addProfile(opts, cb) {
//XXX
cb(new errors.InternalError('_addProfile not yet implemented'));
}
function do_profile(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
}
// Which action?
var actions = [];
if (opts.add) { actions.push('add'); }
if (opts.current) { actions.push('current'); }
if (opts.edit) { actions.push('edit'); }
if (opts['delete']) { actions.push('delete'); }
var action;
if (actions.length === 0) {
action = 'show';
} else if (actions.length > 1) {
return cb(new errors.UsageError(
'only one action option may be used at once'));
} else {
action = actions[0];
}
// Arg count validation.
if (args.length > 1) {
return cb(new errors.UsageError('too many arguments'));
} else if (args.length === 0 &&
['current', 'delete'].indexOf(action) !== -1)
{
return cb(new errors.UsageError('NAME argument is required'));
}
switch (action) {
case 'show':
_showProfile({
cli: this,
name: args[0] || this.tritonapi.config.profile,
json: opts.json
}, cb);
break;
case 'current':
_currentProfile({cli: this, name: args[0]}, cb);
break;
case 'edit':
_editProfile({
cli: this,
name: args[0] || this.tritonapi.config.profile
}, cb);
break;
case 'delete':
_deleteProfile({
cli: this,
name: args[0] || this.tritonapi.config.profile,
force: Boolean(opts.force)
}, cb);
break;
case 'add':
_addProfile({cli: this, file: args[0]}, cb);
break;
default:
return cb(new errors.InternalError('unknown action: ' + action));
}
}
do_profile.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON output when showing a profile.'
},
{
names: ['force', 'f'],
type: 'bool',
help: 'Force deletion.'
},
{
group: 'Action Options'
},
{
names: ['current', 'c'],
type: 'bool',
help: 'Switch to the named profile.'
},
{
names: ['edit', 'e'],
type: 'bool',
help: 'Edit the named profile in your $EDITOR.'
},
{
names: ['add', 'a'],
type: 'bool',
help: 'Add a new profile.'
},
{
names: ['delete', 'd'],
type: 'bool',
help: 'Delete the named profile.'
}
];
do_profile.help = [
'Show, add, edit and delete `triton` CLI profiles.',
'',
'A profile is a configured Triton CloudAPI endpoint. I.e. the',
'url, account, key, etc. information required to call a CloudAPI.',
'You can then switch between profiles with `triton -p PROFILE`',
'or the TRITON_PROFILE environment variable.',
'',
'Usage:',
' {{name}} profile [NAME] # show NAME or current profile',
' {{name}} profile -e|--edit [NAME] # edit a profile in $EDITOR',
' {{name}} profile -c|--current NAME # set NAME as current profile',
' {{name}} profile -d|--delete NAME # delete a profile',
' {{name}} profile -a|--add [FILE] # add a new profile',
'',
'{{options}}'
].join('\n');
module.exports = do_profile;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015 Joyent Inc. All rights reserved.
* Copyright (c) 2015 Joyent Inc.
*
* `triton profiles ...`
*/
@ -14,7 +14,7 @@ var sortDefault = 'name';
var columnsDefault = 'name,curr,account,url';
var columnsDefaultLong = 'name,curr,account,url,insecure,keyId';
function _listProfiles(opts, args, cb) {
function _listProfiles(cli, opts, args, cb) {
var columns = columnsDefault;
if (opts.o) {
columns = opts.o;
@ -29,8 +29,8 @@ function _listProfiles(opts, args, cb) {
var profiles;
try {
profiles = mod_config.loadAllProfiles({
configDir: this.tritonapi.config._configDir,
log: this.log
configDir: cli.tritonapi.config._configDir,
log: cli.log
});
} catch (e) {
return cb(e);
@ -39,8 +39,8 @@ function _listProfiles(opts, args, cb) {
// Current profile: Set 'curr' field. Apply CLI overrides.
for (i = 0; i < profiles.length; i++) {
var profile = profiles[i];
if (profile.name === this.tritonapi.profile.name) {
this._applyProfileOverrides(profile);
if (profile.name === cli.tritonapi.profile.name) {
cli._applyProfileOverrides(profile);
if (opts.json) {
profile.curr = true;
} else {
@ -69,19 +69,19 @@ function _listProfiles(opts, args, cb) {
cb();
}
function _currentProfile(opts, args, cb) {
function _currentProfile(cli, opts, args, cb) {
var profile = mod_config.loadProfile({
configDir: this.configDir,
configDir: cli.configDir,
name: opts.current
});
if (this.tritonapi.profile.name === profile.name) {
if (cli.tritonapi.profile.name === profile.name) {
console.log('"%s" is already the current profile', profile.name);
return cb();
}
mod_config.setConfigVar({
configDir: this.configDir,
configDir: cli.configDir,
name: 'profile',
value: profile.name
}, function (err) {
@ -93,69 +93,18 @@ function _currentProfile(opts, args, cb) {
});
}
// TODO: finish the implementation
//function _addProfile(profile, opts, cb) {
//}
//
//function _editProfile(profile, opts, cb) {
//}
//
//function _deleteProfile(profile, opts, cb) {
//}
function do_profiles(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
return this.do_help('help', {}, [subcmd], cb);
} else if (args.length > 0) {
return cb(new errors.UsageError('too many args'));
}
// Which action?
var actions = [];
if (opts.add) { actions.push('add'); }
if (opts.current) { actions.push('current'); }
if (opts.edit) { actions.push('edit'); }
if (opts['delete']) { actions.push('delete'); }
var action;
if (actions.length === 0) {
action = 'list';
} else if (actions.length > 1) {
return cb(new errors.UsageError(
'only one action option may be used at once'));
if (opts.current) {
_currentProfile(this, opts, args, cb);
} else {
action = actions[0];
_listProfiles(this, opts, args, cb);
}
// Arg count validation.
switch (action) {
//case 'add':
// if (args.length === 1) {
// name = args[0];
// } else if (args.length > 1) {
// return cb(new errors.UsageError('too many args'));
// }
// break;
case 'list':
case 'current':
//case 'edit':
//case 'delete':
if (args.length > 0) {
return cb(new errors.UsageError('too many args'));
}
break;
default:
throw new Error('unknown action: ' + action);
}
var func = {
list: _listProfiles,
current: _currentProfile
// TODO: finish the implementation
//add: _addProfile,
//edit: _editProfile,
//'delete': _deleteProfile
}[action].bind(this);
func(opts, args, cb);
}
do_profiles.options = [
@ -163,41 +112,13 @@ do_profiles.options = [
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
group: 'Action Options'
},
{
names: ['current', 'c'],
type: 'string',
helpArg: 'NAME',
help: 'Switch to the given profile.'
}
// TODO: finish the implementation
//{
// names: ['add', 'a'],
// type: 'bool',
// help: 'Add a new profile.'
//},
//{
// names: ['edit', 'e'],
// type: 'string',
// helpArg: 'NAME',
// help: 'Edit profile NAME in your $EDITOR.'
//},
//{
// names: ['delete', 'd'],
// type: 'string',
// helpArg: 'NAME',
// help: 'Delete profile NAME.'
//}
].concat(common.getCliTableOptions({
includeLong: true,
sortDefault: sortDefault
}));
do_profiles.help = [
'List and update `triton` CLI profiles.',
'List `triton` CLI profiles.',
'',
'A profile is a configured Triton CloudAPI endpoint. I.e. the',
'url, account, key, etc. information required to call a CloudAPI.',
@ -207,17 +128,11 @@ do_profiles.help = [
'The "CURR" column indicates which profile is the current one.',
'',
'Usage:',
' {{name}} profiles # list profiles',
' {{name}} profiles -c|--current NAME # set NAME as current profile',
// TODO: finish the implementation
//' {{name}} profiles -a|--add [NAME] # add a new profile',
//' {{name}} profiles -e|--edit NAME # edit a profile in $EDITOR',
//' {{name}} profiles -d|--delete NAME # delete a profile',
' {{name}} profiles',
'',
'{{options}}'
].join('\n');
do_profiles.hidden = true; // TODO: until -a,-e,-d are implemented
module.exports = do_profiles;

View File

@ -24,10 +24,10 @@ var verror = require('verror'),
* Base error. Instances will always have a string `message` and
* a string `code` (a CamelCase string).
*/
function TritonError(options) {
function _TritonBaseError(options) {
assert.object(options, 'options');
assert.string(options.message, 'options.message');
assert.string(options.code, 'options.code');
assert.optionalString(options.code, 'options.code');
assert.optionalObject(options.cause, 'options.cause');
assert.optionalNumber(options.statusCode, 'options.statusCode');
var self = this;
@ -43,7 +43,25 @@ function TritonError(options) {
self[k] = options[k];
});
}
util.inherits(TritonError, VError);
util.inherits(_TritonBaseError, VError);
/*
* A generic (i.e. a cop out) code-less error.
*/
function TritonError(cause, message) {
if (message === undefined) {
message = cause;
cause = undefined;
}
assert.string(message);
_TritonBaseError.call(this, {
cause: cause,
message: message,
exitStatus: 1
});
}
util.inherits(TritonError, _TritonBaseError);
function InternalError(cause, message) {
if (message === undefined) {
@ -51,14 +69,14 @@ function InternalError(cause, message) {
cause = undefined;
}
assert.string(message);
TritonError.call(this, {
_TritonBaseError.call(this, {
cause: cause,
message: message,
code: 'InternalError',
exitStatus: 1
});
}
util.inherits(InternalError, TritonError);
util.inherits(InternalError, _TritonBaseError);
/**
@ -70,14 +88,14 @@ function ConfigError(cause, message) {
cause = undefined;
}
assert.string(message);
TritonError.call(this, {
_TritonBaseError.call(this, {
cause: cause,
message: message,
code: 'Config',
exitStatus: 1
});
}
util.inherits(ConfigError, TritonError);
util.inherits(ConfigError, _TritonBaseError);
/**
@ -89,28 +107,28 @@ function UsageError(cause, message) {
cause = undefined;
}
assert.string(message);
TritonError.call(this, {
_TritonBaseError.call(this, {
cause: cause,
message: message,
code: 'Usage',
exitStatus: 1
});
}
util.inherits(UsageError, TritonError);
util.inherits(UsageError, _TritonBaseError);
/**
* An error signing a request.
*/
function SigningError(cause) {
TritonError.call(this, {
_TritonBaseError.call(this, {
cause: cause,
message: 'error signing request',
code: 'Signing',
exitStatus: 1
});
}
util.inherits(SigningError, TritonError);
util.inherits(SigningError, _TritonBaseError);
/**
@ -120,14 +138,14 @@ function SelfSignedCertError(cause, url) {
var msg = format('could not access CloudAPI %s because it uses a ' +
'self-signed TLS certificate and your current profile is not ' +
'configured for insecure access', url);
TritonError.call(this, {
_TritonBaseError.call(this, {
cause: cause,
message: msg,
code: 'SelfSignedCert',
exitStatus: 1
});
}
util.inherits(SelfSignedCertError, TritonError);
util.inherits(SelfSignedCertError, _TritonBaseError);
/**
@ -140,7 +158,7 @@ function MultiError(errs) {
var err = errs[i];
lines.push(format(' error (%s): %s', err.code, err.message));
}
TritonError.call(this, {
_TritonBaseError.call(this, {
cause: errs[0],
message: lines.join('\n'),
code: 'MultiError',
@ -148,7 +166,7 @@ function MultiError(errs) {
});
}
MultiError.description = 'Multiple errors.';
util.inherits(MultiError, TritonError);
util.inherits(MultiError, _TritonBaseError);

View File

@ -16,6 +16,7 @@
"mkdirp": "0.5.1",
"node-uuid": "1.4.3",
"once": "1.3.2",
"read": "1.0.7",
"restify-clients": "1.1.0",
"restify-errors": "3.0.0",
"smartdc-auth": "git+https://github.com/joyent/node-smartdc-auth.git#3be3c1e",