shortid support for instances; --url,--account et al top-level options
This commit is contained in:
parent
5b60fffc04
commit
9e3df02a5e
1
TODO.txt
1
TODO.txt
@ -1,5 +1,6 @@
|
|||||||
TritonApi
|
TritonApi
|
||||||
|
|
||||||
|
|
||||||
"shortid" instead of full UUID "id" in default output, and then allow lookup
|
"shortid" instead of full UUID "id" in default output, and then allow lookup
|
||||||
by that shortid. Really nice for 80 columns.
|
by that shortid. Really nice for 80 columns.
|
||||||
- insts
|
- insts
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||||
*
|
|
||||||
* triton command
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var p = console.log;
|
var p = console.log;
|
||||||
|
113
lib/cli.js
113
lib/cli.js
@ -36,6 +36,84 @@ var log = bunyan.createLogger({
|
|||||||
level: 'warn'
|
level: 'warn'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var OPTIONS = [
|
||||||
|
{
|
||||||
|
names: ['help', 'h'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Print this help and exit.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'version',
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Print version and exit.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['verbose', 'v'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Verbose/debug output.'
|
||||||
|
},
|
||||||
|
|
||||||
|
// XXX disable profile selection for now
|
||||||
|
//{names: ['profile', 'p'], type: 'string', env: 'TRITON_PROFILE',
|
||||||
|
// helpArg: 'NAME', help: 'Triton client profile to use.'}
|
||||||
|
|
||||||
|
{
|
||||||
|
group: 'CloudAPI Options'
|
||||||
|
},
|
||||||
|
// XXX SDC_USER support. I don't grok the node-smartdc/README.md discussion
|
||||||
|
// of SDC_USER.
|
||||||
|
{
|
||||||
|
names: ['account', 'a'],
|
||||||
|
type: 'string',
|
||||||
|
env: 'SDC_ACCOUNT',
|
||||||
|
help: 'Triton account (login name)',
|
||||||
|
helpArg: 'ACCOUNT'
|
||||||
|
},
|
||||||
|
// XXX
|
||||||
|
//{
|
||||||
|
// names: ['subuser', 'user'],
|
||||||
|
// type: 'string',
|
||||||
|
// env: 'MANTA_SUBUSER',
|
||||||
|
// help: 'Manta User (login name)',
|
||||||
|
// helpArg: 'USER'
|
||||||
|
//},
|
||||||
|
//{
|
||||||
|
// names: ['role'],
|
||||||
|
// type: 'arrayOfString',
|
||||||
|
// env: 'MANTA_ROLE',
|
||||||
|
// help: 'Assume a role. Use multiple times or once with a list',
|
||||||
|
// helpArg: 'ROLE,ROLE,...'
|
||||||
|
//},
|
||||||
|
{
|
||||||
|
names: ['keyId', 'k'],
|
||||||
|
type: 'string',
|
||||||
|
env: 'SDC_KEY_ID',
|
||||||
|
help: 'SSH key fingerprint',
|
||||||
|
helpArg: 'FINGERPRINT'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['url', 'u'],
|
||||||
|
type: 'string',
|
||||||
|
env: 'SDC_URL',
|
||||||
|
help: 'CloudAPI URL',
|
||||||
|
helpArg: 'URL'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['J'],
|
||||||
|
type: 'string',
|
||||||
|
hidden: true,
|
||||||
|
help: 'Joyent Public Cloud (JPC) datacenter name. This is ' +
|
||||||
|
'a shortcut to the "https://$dc.api.joyent.com" ' +
|
||||||
|
'cloudapi URL.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['insecure', 'i'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Do not validate SSL certificate',
|
||||||
|
'default': false,
|
||||||
|
env: 'SDC_TLS_INSECURE' // Deprecated SDC_TESTING supported below.
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
//---- CLI class
|
//---- CLI class
|
||||||
@ -44,15 +122,7 @@ function CLI() {
|
|||||||
Cmdln.call(this, {
|
Cmdln.call(this, {
|
||||||
name: pkg.name,
|
name: pkg.name,
|
||||||
desc: pkg.description,
|
desc: pkg.description,
|
||||||
options: [
|
options: OPTIONS,
|
||||||
{names: ['help', 'h'], type: 'bool', help: 'Print help and exit.'},
|
|
||||||
{name: 'version', type: 'bool', help: 'Print version and exit.'},
|
|
||||||
{names: ['verbose', 'v'], type: 'bool',
|
|
||||||
help: 'Verbose/debug output.'},
|
|
||||||
// XXX disable profile selection for now
|
|
||||||
//{names: ['profile', 'p'], type: 'string', env: 'TRITON_PROFILE',
|
|
||||||
// helpArg: 'NAME', help: 'Triton client profile to use.'}
|
|
||||||
],
|
|
||||||
helpOpts: {
|
helpOpts: {
|
||||||
includeEnv: true,
|
includeEnv: true,
|
||||||
minHelpCol: 30
|
minHelpCol: 30
|
||||||
@ -102,6 +172,7 @@ CLI.prototype.init = function (opts, args, callback) {
|
|||||||
if (opts.verbose) {
|
if (opts.verbose) {
|
||||||
log.level('trace');
|
log.level('trace');
|
||||||
log.src = true;
|
log.src = true;
|
||||||
|
this.showErrStack = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.__defineGetter__('triton', function () {
|
this.__defineGetter__('triton', function () {
|
||||||
@ -118,11 +189,29 @@ CLI.prototype.init = function (opts, args, callback) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// XXX support keyId being a priv or pub key path, a la imgapi-cli
|
||||||
|
// XXX Add TRITON_* envvars.
|
||||||
|
var envProfile = {
|
||||||
|
name: 'env',
|
||||||
|
account: opts.account,
|
||||||
|
url: opts.url,
|
||||||
|
keyId: opts.keyId,
|
||||||
|
insecure: opts.insecure
|
||||||
|
};
|
||||||
|
if (opts.insecure === undefined && process.env.SDC_TESTING) {
|
||||||
|
opts.insecure = common.boolFromString(process.env.SDC_TESTING);
|
||||||
|
}
|
||||||
|
if (opts.J) {
|
||||||
|
envProfile.url = format('https://%s.api.joyent.com', opts.J);
|
||||||
|
}
|
||||||
|
log.trace({envProfile: envProfile}, 'envProfile');
|
||||||
|
|
||||||
self._triton = new Triton({
|
self._triton = new Triton({
|
||||||
log: log,
|
log: log,
|
||||||
profile: opts.profile,
|
profileName: opts.profile,
|
||||||
config: userConfigPath,
|
envProfile: envProfile,
|
||||||
cachedir: cacheDir
|
configPath: userConfigPath,
|
||||||
|
cacheDir: cacheDir
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return self._triton;
|
return self._triton;
|
||||||
|
@ -197,6 +197,54 @@ function capitalize(s) {
|
|||||||
return s[0].toUpperCase() + s.substr(1);
|
return s[0].toUpperCase() + s.substr(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Normalize a short ID. Returns undefined if the given string isn't a valid
|
||||||
|
* short id.
|
||||||
|
*
|
||||||
|
* Short IDs:
|
||||||
|
* - UUID prefix
|
||||||
|
* - allow '-' to be elided (to support using containers IDs from
|
||||||
|
* docker)
|
||||||
|
* - support docker ID *longer* than a UUID? The curr implementation does.
|
||||||
|
*/
|
||||||
|
function normShortId(s) {
|
||||||
|
var shortIdCharsRe = /^[a-f0-9]+$/;
|
||||||
|
var shortId;
|
||||||
|
if (s.indexOf('-') === -1) {
|
||||||
|
if (!shortIdCharsRe.test(s)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shortId = s.substr(0, 8) + '-'
|
||||||
|
+ s.substr(8, 4) + '-'
|
||||||
|
+ s.substr(12, 4) + '-'
|
||||||
|
+ s.substr(16, 4) + '-'
|
||||||
|
+ s.substr(20, 12);
|
||||||
|
shortId = shortId.replace(/-+$/, '');
|
||||||
|
} else {
|
||||||
|
// UUID prefix.
|
||||||
|
var shortId = '';
|
||||||
|
var chunk;
|
||||||
|
var remaining = s;
|
||||||
|
var spans = [8, 4, 4, 4, 12];
|
||||||
|
for (var i = 0; i < spans.length; i++) {
|
||||||
|
var span = spans[i];
|
||||||
|
head = remaining.slice(0, span);
|
||||||
|
remaining = remaining.slice(span + 1);
|
||||||
|
if (!shortIdCharsRe.test(head)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shortId += head;
|
||||||
|
if (remaining) {
|
||||||
|
shortId += '-';
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shortId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//---- exports
|
//---- exports
|
||||||
|
|
||||||
@ -212,6 +260,7 @@ module.exports = {
|
|||||||
isUUID: isUUID,
|
isUUID: isUUID,
|
||||||
humanDurationFromMs: humanDurationFromMs,
|
humanDurationFromMs: humanDurationFromMs,
|
||||||
humanSizeFromBytes: humanSizeFromBytes,
|
humanSizeFromBytes: humanSizeFromBytes,
|
||||||
capitalize: capitalize
|
capitalize: capitalize,
|
||||||
|
normShortId: normShortId
|
||||||
};
|
};
|
||||||
// vim: set softtabstop=4 shiftwidth=4:
|
// vim: set softtabstop=4 shiftwidth=4:
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Copyright (c) 2014 Joyent Inc. All rights reserved.
|
* Copyright (c) 2015 Joyent Inc. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var p = console.log;
|
|
||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
|
var format = require('util').format;
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var sprintf = require('extsprintf').sprintf;
|
|
||||||
|
|
||||||
var common = require('./common');
|
var common = require('./common');
|
||||||
var errors = require('./errors');
|
var errors = require('./errors');
|
||||||
@ -17,6 +16,8 @@ var DEFAULT_USER_CONFIG_PATH = path.resolve(process.env.HOME, '.triton', 'config
|
|||||||
var DEFAULTS_PATH = path.resolve(__dirname, '..', 'etc', 'defaults.json');
|
var DEFAULTS_PATH = path.resolve(__dirname, '..', 'etc', 'defaults.json');
|
||||||
var OVERRIDE_KEYS = []; // config object keys to do a one-level deep override
|
var OVERRIDE_KEYS = []; // config object keys to do a one-level deep override
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the Triton client config. This is a merge of the built-in "defaults" (at
|
* Load the Triton client config. This is a merge of the built-in "defaults" (at
|
||||||
* etc/defaults.json) and the "user" config (at ~/.triton/config.json if it
|
* etc/defaults.json) and the "user" config (at ~/.triton/config.json if it
|
||||||
@ -24,17 +25,21 @@ var OVERRIDE_KEYS = []; // config object keys to do a one-level deep override
|
|||||||
*
|
*
|
||||||
* This includes some internal data on keys with a leading underscore.
|
* This includes some internal data on keys with a leading underscore.
|
||||||
*/
|
*/
|
||||||
function loadConfigSync(configPath) {
|
function loadConfigSync(opts) {
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.string(opts.configPath, 'opts.configPath');
|
||||||
|
assert.optionalObject(opts.envProfile, 'opts.envProfile');
|
||||||
|
|
||||||
var c = fs.readFileSync(DEFAULTS_PATH, 'utf8');
|
var c = fs.readFileSync(DEFAULTS_PATH, 'utf8');
|
||||||
var _defaults = JSON.parse(c);
|
var _defaults = JSON.parse(c);
|
||||||
var config = JSON.parse(c);
|
var config = JSON.parse(c);
|
||||||
if (configPath && fs.existsSync(configPath)) {
|
if (opts.configPath && fs.existsSync(opts.configPath)) {
|
||||||
c = fs.readFileSync(configPath, 'utf8');
|
c = fs.readFileSync(opts.configPath, 'utf8');
|
||||||
var _user = JSON.parse(c);
|
var _user = JSON.parse(c);
|
||||||
var userConfig = JSON.parse(c);
|
var userConfig = JSON.parse(c);
|
||||||
if (typeof(userConfig) !== 'object' || Array.isArray(userConfig)) {
|
if (typeof(userConfig) !== 'object' || Array.isArray(userConfig)) {
|
||||||
throw new errors.ConfigError(
|
throw new errors.ConfigError(
|
||||||
sprintf('"%s" is not an object', configPath));
|
format('"%s" is not an object', opts.configPath));
|
||||||
}
|
}
|
||||||
// These special keys are merged into the key of the same name in the
|
// These special keys are merged into the key of the same name in the
|
||||||
// base "defaults.json".
|
// base "defaults.json".
|
||||||
@ -56,18 +61,13 @@ function loadConfigSync(configPath) {
|
|||||||
}
|
}
|
||||||
config._defaults = _defaults;
|
config._defaults = _defaults;
|
||||||
|
|
||||||
// Add 'env' profile.
|
// Add 'env' profile, if given.
|
||||||
if (!config.profiles) {
|
if (opts.envProfile) {
|
||||||
config.profiles = [];
|
if (!config.profiles) {
|
||||||
|
config.profiles = [];
|
||||||
|
}
|
||||||
|
config.profiles.push(opts.envProfile);
|
||||||
}
|
}
|
||||||
//XXX Add TRITON_* envvars.
|
|
||||||
config.profiles.push({
|
|
||||||
name: 'env',
|
|
||||||
account: process.env.SDC_USER || process.env.SDC_ACCOUNT,
|
|
||||||
url: process.env.SDC_URL,
|
|
||||||
keyId: process.env.SDC_KEY_ID,
|
|
||||||
insecure: common.boolFromString(process.env.SDC_TESTING)
|
|
||||||
});
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
@ -6,36 +6,25 @@
|
|||||||
|
|
||||||
var common = require('./common');
|
var common = require('./common');
|
||||||
|
|
||||||
function do_instance(subcmd, opts, args, callback) {
|
function do_instance(subcmd, opts, args, cb) {
|
||||||
if (opts.help) {
|
if (opts.help) {
|
||||||
this.do_help('help', {}, [subcmd], callback);
|
return this.do_help('help', {}, [subcmd], cb);
|
||||||
return;
|
|
||||||
} else if (args.length !== 1) {
|
} else if (args.length !== 1) {
|
||||||
callback(new Error('invalid args: ' + args));
|
return cb(new Error('invalid args: ' + args));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var id = args[0];
|
this.triton.getInstance(args[0], function (err, inst) {
|
||||||
|
|
||||||
if (common.isUUID(id)) {
|
|
||||||
this.triton.cloudapi.getMachine(id, cb);
|
|
||||||
} else {
|
|
||||||
this.triton.getMachineByAlias(id, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cb(err, machine) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err);
|
return cb(err);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(machine));
|
console.log(JSON.stringify(inst));
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify(machine, null, 4));
|
console.log(JSON.stringify(inst, null, 4));
|
||||||
}
|
}
|
||||||
callback();
|
cb();
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
do_instance.options = [
|
do_instance.options = [
|
||||||
|
@ -36,7 +36,8 @@ var validFields = [
|
|||||||
'package',
|
'package',
|
||||||
'image',
|
'image',
|
||||||
'img',
|
'img',
|
||||||
'ago'
|
'ago',
|
||||||
|
'shortid'
|
||||||
];
|
];
|
||||||
|
|
||||||
function do_instances(subcmd, opts, args, callback) {
|
function do_instances(subcmd, opts, args, callback) {
|
||||||
@ -45,8 +46,15 @@ function do_instances(subcmd, opts, args, callback) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var columns = opts.o.trim().split(',');
|
var columns = 'shortid,name,state,type,img,memory,disk,ago'.split(',');
|
||||||
var sort = opts.s.trim().split(',');
|
if (opts.o) {
|
||||||
|
/* JSSTYLED */
|
||||||
|
columns = opts.o.trim().split(/\s*,\s*/g);
|
||||||
|
} else if (opts.long) {
|
||||||
|
columns[0] = 'id';
|
||||||
|
}
|
||||||
|
/* JSSTYLED */
|
||||||
|
var sort = opts.s.trim().split(/\s*,\s*/g);
|
||||||
|
|
||||||
var listOpts;
|
var listOpts;
|
||||||
try {
|
try {
|
||||||
@ -60,7 +68,7 @@ function do_instances(subcmd, opts, args, callback) {
|
|||||||
|
|
||||||
i++;
|
i++;
|
||||||
var images;
|
var images;
|
||||||
this.triton.listImages({usecache: true}, function (err, _images) {
|
this.triton.listImages({useCache: true}, function (err, _images) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
@ -90,12 +98,15 @@ function do_instances(subcmd, opts, args, callback) {
|
|||||||
imgmap[image.id] = f('%s@%s', image.name, image.version);
|
imgmap[image.id] = f('%s@%s', image.name, image.version);
|
||||||
});
|
});
|
||||||
|
|
||||||
// add extra fields for nice output
|
// Add extra fields for nice output.
|
||||||
|
// XXX FWIW, the "extra fields" for images and packages are not added
|
||||||
|
// for `opts.json`. Thoughts? We should be consistent there. --TM
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
machines.forEach(function (machine) {
|
machines.forEach(function (machine) {
|
||||||
var created = new Date(machine.created);
|
var created = new Date(machine.created);
|
||||||
machine.ago = common.longAgo(created, now);
|
machine.ago = common.longAgo(created, now);
|
||||||
machine.img = imgmap[machine.image] || machine.image;
|
machine.img = imgmap[machine.image] || machine.image;
|
||||||
|
machine.shortid = machine.id.split('-', 1)[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
@ -118,6 +129,9 @@ do_instances.options = [
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
help: 'Show this help.'
|
help: 'Show this help.'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: 'Output options'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
names: ['H'],
|
names: ['H'],
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
@ -126,10 +140,14 @@ do_instances.options = [
|
|||||||
{
|
{
|
||||||
names: ['o'],
|
names: ['o'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
default: 'id,name,state,type,img,memory,disk,ago',
|
|
||||||
help: 'Specify fields (columns) to output.',
|
help: 'Specify fields (columns) to output.',
|
||||||
helpArg: 'field1,...'
|
helpArg: 'field1,...'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
names: ['long', 'l'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Long/wider output. Ignored if "-o ..." is used.'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
names: ['s'],
|
names: ['s'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -19,22 +19,15 @@ function do_ssh(subcmd, opts, args, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var id = args.shift();
|
var id = args.shift();
|
||||||
|
this.triton.getInstance(id, function (err, inst) {
|
||||||
if (common.isUUID(id)) {
|
|
||||||
this.triton.cloudapi.getMachine(id, cb);
|
|
||||||
} else {
|
|
||||||
this.triton.getMachineByAlias(id, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cb(err, machine) {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ip = machine.primaryIp;
|
var ip = inst.primaryIp;
|
||||||
if (!ip) {
|
if (!ip) {
|
||||||
callback(new Error('primaryIp not found for machine'));
|
callback(new Error('primaryIp not found for instance'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +38,7 @@ function do_ssh(subcmd, opts, args, callback) {
|
|||||||
child.on('close', function (code) {
|
child.on('close', function (code) {
|
||||||
process.exit(code);
|
process.exit(code);
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
do_ssh.options = [
|
do_ssh.options = [
|
||||||
|
@ -83,7 +83,7 @@ function _do_instance(action, subcmd, opts, args, callback) {
|
|||||||
uuid = arg;
|
uuid = arg;
|
||||||
go1();
|
go1();
|
||||||
} else {
|
} else {
|
||||||
self.triton.getMachineByAlias(arg, function (err, machine) {
|
self.triton.getInstance(arg, function (err, machine) {
|
||||||
if (err) {
|
if (err) {
|
||||||
callback(err);
|
callback(err);
|
||||||
return;
|
return;
|
||||||
|
@ -36,7 +36,7 @@ function do_wait_instance(subcmd, opts, args, cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.triton.getMachineByAlias(id, function (err, machine) {
|
self.triton.getInstance(id, function (err, machine) {
|
||||||
if (err) {
|
if (err) {
|
||||||
cb(err);
|
cb(err);
|
||||||
return;
|
return;
|
||||||
|
154
lib/triton.js
154
lib/triton.js
@ -14,6 +14,7 @@ var once = require('once');
|
|||||||
var path = require('path');
|
var path = require('path');
|
||||||
var restifyClients = require('restify-clients');
|
var restifyClients = require('restify-clients');
|
||||||
var tabula = require('tabula');
|
var tabula = require('tabula');
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
var cloudapi = require('./cloudapi2');
|
var cloudapi = require('./cloudapi2');
|
||||||
var common = require('./common');
|
var common = require('./common');
|
||||||
@ -29,15 +30,19 @@ var loadConfigSync = require('./config').loadConfigSync;
|
|||||||
*
|
*
|
||||||
* @param options {Object}
|
* @param options {Object}
|
||||||
* - log {Bunyan Logger}
|
* - log {Bunyan Logger}
|
||||||
* - profile {String} Optional. Name of profile to use. Defaults to
|
* - profileName {String} Optional. Name of profile to use. Defaults to
|
||||||
* 'defaultProfile' in the config.
|
* 'defaultProfile' in the config.
|
||||||
|
* - envProfile {Object} Optional. A starter 'env' profile object. Missing
|
||||||
|
* fields will be filled in from standard SDC_* envvars.
|
||||||
|
* ...
|
||||||
*/
|
*/
|
||||||
function Triton(options) {
|
function Triton(options) {
|
||||||
assert.object(options, 'options');
|
assert.object(options, 'options');
|
||||||
assert.object(options.log, 'options.log');
|
assert.object(options.log, 'options.log');
|
||||||
assert.optionalString(options.profile, 'options.profile');
|
assert.optionalString(options.profileName, 'options.profileName');
|
||||||
assert.optionalString(options.config, 'options.config');
|
assert.optionalString(options.configPath, 'options.configPath');
|
||||||
assert.optionalString(options.cachedir, 'options.cachedir');
|
assert.optionalString(options.cacheDir, 'options.cacheDir');
|
||||||
|
assert.optionalObject(options.envProfile, 'options.envProfile');
|
||||||
|
|
||||||
// Make sure a given bunyan logger has reasonable client_re[qs] serializers.
|
// Make sure a given bunyan logger has reasonable client_re[qs] serializers.
|
||||||
// Note: This was fixed in restify, then broken again in
|
// Note: This was fixed in restify, then broken again in
|
||||||
@ -52,12 +57,15 @@ function Triton(options) {
|
|||||||
} else {
|
} else {
|
||||||
this.log = options.log;
|
this.log = options.log;
|
||||||
}
|
}
|
||||||
this.config = loadConfigSync(options.config);
|
this.config = loadConfigSync({
|
||||||
|
configPath: options.configPath,
|
||||||
|
envProfile: options.envProfile
|
||||||
|
});
|
||||||
this.profiles = this.config.profiles;
|
this.profiles = this.config.profiles;
|
||||||
this.profile = this.getProfile(
|
this.profile = this.getProfile(
|
||||||
options.profile || this.config.defaultProfile);
|
options.profileName || this.config.defaultProfile);
|
||||||
this.log.trace({profile: this.profile}, 'profile data');
|
this.log.trace({profile: this.profile}, 'profile data');
|
||||||
this.cachedir = options.cachedir;
|
this.cacheDir = options.cacheDir;
|
||||||
|
|
||||||
this.cloudapi = this._cloudapiFromProfile(this.profile);
|
this.cloudapi = this._cloudapiFromProfile(this.profile);
|
||||||
}
|
}
|
||||||
@ -117,23 +125,24 @@ Triton.prototype.listImages = function listImages(opts, cb) {
|
|||||||
opts = {};
|
opts = {};
|
||||||
}
|
}
|
||||||
assert.object(opts, 'opts');
|
assert.object(opts, 'opts');
|
||||||
|
assert.optionalBool(opts.useCache, 'opts.useCache');
|
||||||
assert.func(cb, 'cb');
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
var cachefile;
|
var cacheFile;
|
||||||
if (self.cachedir)
|
if (self.cacheDir)
|
||||||
cachefile = path.join(self.cachedir, 'images.json');
|
cacheFile = path.join(self.cacheDir, 'images.json');
|
||||||
|
|
||||||
if (opts.usecache && !cachefile) {
|
if (opts.useCache && !cacheFile) {
|
||||||
cb(new Error('opts.usecache set but no cachedir found'));
|
cb(new Error('opts.useCache set but no cacheDir found'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to read the cache if the user wants it
|
// try to read the cache if the user wants it
|
||||||
// if this fails for any reason we fallback to hitting the cloudapi
|
// if this fails for any reason we fallback to hitting the cloudapi
|
||||||
if (opts.usecache) {
|
if (opts.useCache) {
|
||||||
fs.readFile(cachefile, 'utf8', function (err, out) {
|
fs.readFile(cacheFile, 'utf8', function (err, out) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self.log.info({err: err}, 'failed to read cache file %s', cachefile);
|
self.log.info({err: err}, 'failed to read cache file %s', cacheFile);
|
||||||
fetch();
|
fetch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -141,7 +150,7 @@ Triton.prototype.listImages = function listImages(opts, cb) {
|
|||||||
try {
|
try {
|
||||||
data = JSON.parse(out);
|
data = JSON.parse(out);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
self.log.info({err: e}, 'failed to parse cache file %s', cachefile);
|
self.log.info({err: e}, 'failed to parse cache file %s', cacheFile);
|
||||||
fetch();
|
fetch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -154,10 +163,10 @@ Triton.prototype.listImages = function listImages(opts, cb) {
|
|||||||
fetch();
|
fetch();
|
||||||
function fetch() {
|
function fetch() {
|
||||||
self.cloudapi.listImages(opts, function (err, imgs, res) {
|
self.cloudapi.listImages(opts, function (err, imgs, res) {
|
||||||
if (!err && self.cachedir) {
|
if (!err && self.cacheDir) {
|
||||||
// cache the results
|
// cache the results
|
||||||
var data = JSON.stringify(imgs);
|
var data = JSON.stringify(imgs);
|
||||||
fs.writeFile(cachefile, data, {encoding: 'utf8'}, function (err) {
|
fs.writeFile(cacheFile, data, {encoding: 'utf8'}, function (err) {
|
||||||
if (err)
|
if (err)
|
||||||
self.log.info({err: err}, 'error caching images results');
|
self.log.info({err: err}, 'error caching images results');
|
||||||
done();
|
done();
|
||||||
@ -289,27 +298,100 @@ Triton.prototype.getPackage = function getPackage(name, cb) {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getMachine for an alias
|
* Get an instance by ID, exact name, or short ID, in that order.
|
||||||
*
|
*
|
||||||
* @param {String} alias - the machine alias
|
* @param {String} name
|
||||||
* @param {Function} callback `function (err, machine)`
|
* @param {Function} callback `function (err, inst)`
|
||||||
*/
|
*/
|
||||||
Triton.prototype.getMachineByAlias = function getMachineByAlias(alias, callback) {
|
Triton.prototype.getInstance = function getInstance(name, cb) {
|
||||||
this.cloudapi.listMachines({name: alias}, function (err, machines) {
|
var self = this;
|
||||||
if (err) {
|
assert.string(name, 'name');
|
||||||
callback(err);
|
assert.func(cb, 'cb');
|
||||||
return;
|
|
||||||
}
|
var shortId;
|
||||||
var found = false;
|
var inst;
|
||||||
machines.forEach(function (machine) {
|
|
||||||
if (!found && machine.name === alias) {
|
vasync.pipeline({funcs: [
|
||||||
callback(null, machine);
|
function tryUuid(_, next) {
|
||||||
found = true;
|
var uuid;
|
||||||
|
if (common.isUUID(name)) {
|
||||||
|
uuid = name;
|
||||||
|
} else {
|
||||||
|
shortId = common.normShortId(name);
|
||||||
|
if (shortId && common.isUUID(shortId)) {
|
||||||
|
// E.g. a >32-char docker container ID normalized to a UUID.
|
||||||
|
uuid = shortId;
|
||||||
|
} else {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
this.cloudapi.getMachine(uuid, function (err, inst) {
|
||||||
if (!found) {
|
inst = inst;
|
||||||
callback(new Error('machine ' + alias + ' not found'));
|
next(err);
|
||||||
return;
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function tryName(_, next) {
|
||||||
|
if (inst) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cloudapi.listMachines({name: name}, function (err, insts) {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < insts.length; i++) {
|
||||||
|
if (insts[i].name === name) {
|
||||||
|
inst = insts[i];
|
||||||
|
// Relying on rule that instance name is unique
|
||||||
|
// for a user and DC.
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
function tryShortId(_, next) {
|
||||||
|
if (inst || !shortId) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
var nextOnce = once(next);
|
||||||
|
|
||||||
|
var match;
|
||||||
|
var s = self.cloudapi.createListMachinesStream();
|
||||||
|
s.on('error', function (err) {
|
||||||
|
nextOnce(err);
|
||||||
|
});
|
||||||
|
s.on('readable', function () {
|
||||||
|
var inst;
|
||||||
|
while ((inst = s.read()) !== null) {
|
||||||
|
if (inst.id.slice(0, shortId.length) === shortId) {
|
||||||
|
if (match) {
|
||||||
|
return nextOnce(new Error(
|
||||||
|
'instance short id "%s" is ambiguous',
|
||||||
|
shortId));
|
||||||
|
} else {
|
||||||
|
match = inst;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
s.on('end', function () {
|
||||||
|
if (match) {
|
||||||
|
inst = match;
|
||||||
|
}
|
||||||
|
nextOnce();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]}, function (err) {
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
} else if (inst) {
|
||||||
|
cb(null, inst);
|
||||||
|
} else {
|
||||||
|
cb(new Error(format(
|
||||||
|
'no instance with name or shortId "%s" was found', name)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user