shortid support for instances; --url,--account et al top-level options

This commit is contained in:
Trent Mick 2015-08-26 17:21:27 -07:00
parent 5b60fffc04
commit 9e3df02a5e
11 changed files with 327 additions and 108 deletions

View File

@ -1,5 +1,6 @@
TritonApi
"shortid" instead of full UUID "id" in default output, and then allow lookup
by that shortid. Really nice for 80 columns.
- insts

View File

@ -1,8 +1,6 @@
#!/usr/bin/env node
/*
* Copyright (c) 2015 Joyent Inc. All rights reserved.
*
* triton command
*/
var p = console.log;

View File

@ -36,6 +36,84 @@ var log = bunyan.createLogger({
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
@ -44,15 +122,7 @@ function CLI() {
Cmdln.call(this, {
name: pkg.name,
desc: pkg.description,
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.'}
],
options: OPTIONS,
helpOpts: {
includeEnv: true,
minHelpCol: 30
@ -102,6 +172,7 @@ CLI.prototype.init = function (opts, args, callback) {
if (opts.verbose) {
log.level('trace');
log.src = true;
this.showErrStack = true;
}
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({
log: log,
profile: opts.profile,
config: userConfigPath,
cachedir: cacheDir
profileName: opts.profile,
envProfile: envProfile,
configPath: userConfigPath,
cacheDir: cacheDir
});
}
return self._triton;

View File

@ -197,6 +197,54 @@ function capitalize(s) {
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
@ -212,6 +260,7 @@ module.exports = {
isUUID: isUUID,
humanDurationFromMs: humanDurationFromMs,
humanSizeFromBytes: humanSizeFromBytes,
capitalize: capitalize
capitalize: capitalize,
normShortId: normShortId
};
// vim: set softtabstop=4 shiftwidth=4:

View File

@ -1,13 +1,12 @@
#!/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 format = require('util').format;
var fs = require('fs');
var path = require('path');
var sprintf = require('extsprintf').sprintf;
var common = require('./common');
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 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
* 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.
*/
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 _defaults = JSON.parse(c);
var config = JSON.parse(c);
if (configPath && fs.existsSync(configPath)) {
c = fs.readFileSync(configPath, 'utf8');
if (opts.configPath && fs.existsSync(opts.configPath)) {
c = fs.readFileSync(opts.configPath, 'utf8');
var _user = JSON.parse(c);
var userConfig = JSON.parse(c);
if (typeof(userConfig) !== 'object' || Array.isArray(userConfig)) {
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
// base "defaults.json".
@ -56,18 +61,13 @@ function loadConfigSync(configPath) {
}
config._defaults = _defaults;
// Add 'env' profile.
if (!config.profiles) {
config.profiles = [];
// Add 'env' profile, if given.
if (opts.envProfile) {
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;
}

View File

@ -6,36 +6,25 @@
var common = require('./common');
function do_instance(subcmd, opts, args, callback) {
function do_instance(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], callback);
return;
return this.do_help('help', {}, [subcmd], cb);
} else if (args.length !== 1) {
callback(new Error('invalid args: ' + args));
return;
return cb(new Error('invalid args: ' + args));
}
var id = args[0];
if (common.isUUID(id)) {
this.triton.cloudapi.getMachine(id, cb);
} else {
this.triton.getMachineByAlias(id, cb);
}
function cb(err, machine) {
this.triton.getInstance(args[0], function (err, inst) {
if (err) {
callback(err);
return;
return cb(err);
}
if (opts.json) {
console.log(JSON.stringify(machine));
console.log(JSON.stringify(inst));
} else {
console.log(JSON.stringify(machine, null, 4));
console.log(JSON.stringify(inst, null, 4));
}
callback();
}
cb();
});
}
do_instance.options = [

View File

@ -36,7 +36,8 @@ var validFields = [
'package',
'image',
'img',
'ago'
'ago',
'shortid'
];
function do_instances(subcmd, opts, args, callback) {
@ -45,8 +46,15 @@ function do_instances(subcmd, opts, args, callback) {
return;
}
var columns = opts.o.trim().split(',');
var sort = opts.s.trim().split(',');
var columns = 'shortid,name,state,type,img,memory,disk,ago'.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;
try {
@ -60,7 +68,7 @@ function do_instances(subcmd, opts, args, callback) {
i++;
var images;
this.triton.listImages({usecache: true}, function (err, _images) {
this.triton.listImages({useCache: true}, function (err, _images) {
if (err) {
callback(err);
return;
@ -90,12 +98,15 @@ function do_instances(subcmd, opts, args, callback) {
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();
machines.forEach(function (machine) {
var created = new Date(machine.created);
machine.ago = common.longAgo(created, now);
machine.img = imgmap[machine.image] || machine.image;
machine.shortid = machine.id.split('-', 1)[0];
});
if (opts.json) {
@ -118,6 +129,9 @@ do_instances.options = [
type: 'bool',
help: 'Show this help.'
},
{
group: 'Output options'
},
{
names: ['H'],
type: 'bool',
@ -126,10 +140,14 @@ do_instances.options = [
{
names: ['o'],
type: 'string',
default: 'id,name,state,type,img,memory,disk,ago',
help: 'Specify fields (columns) to output.',
helpArg: 'field1,...'
},
{
names: ['long', 'l'],
type: 'bool',
help: 'Long/wider output. Ignored if "-o ..." is used.'
},
{
names: ['s'],
type: 'string',

View File

@ -19,22 +19,15 @@ function do_ssh(subcmd, opts, args, callback) {
}
var id = args.shift();
if (common.isUUID(id)) {
this.triton.cloudapi.getMachine(id, cb);
} else {
this.triton.getMachineByAlias(id, cb);
}
function cb(err, machine) {
this.triton.getInstance(id, function (err, inst) {
if (err) {
callback(err);
return;
}
var ip = machine.primaryIp;
var ip = inst.primaryIp;
if (!ip) {
callback(new Error('primaryIp not found for machine'));
callback(new Error('primaryIp not found for instance'));
return;
}
@ -45,7 +38,7 @@ function do_ssh(subcmd, opts, args, callback) {
child.on('close', function (code) {
process.exit(code);
});
}
});
}
do_ssh.options = [

View File

@ -83,7 +83,7 @@ function _do_instance(action, subcmd, opts, args, callback) {
uuid = arg;
go1();
} else {
self.triton.getMachineByAlias(arg, function (err, machine) {
self.triton.getInstance(arg, function (err, machine) {
if (err) {
callback(err);
return;

View File

@ -36,7 +36,7 @@ function do_wait_instance(subcmd, opts, args, cb) {
return;
}
self.triton.getMachineByAlias(id, function (err, machine) {
self.triton.getInstance(id, function (err, machine) {
if (err) {
cb(err);
return;

View File

@ -14,6 +14,7 @@ var once = require('once');
var path = require('path');
var restifyClients = require('restify-clients');
var tabula = require('tabula');
var vasync = require('vasync');
var cloudapi = require('./cloudapi2');
var common = require('./common');
@ -29,15 +30,19 @@ var loadConfigSync = require('./config').loadConfigSync;
*
* @param options {Object}
* - 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.
* - envProfile {Object} Optional. A starter 'env' profile object. Missing
* fields will be filled in from standard SDC_* envvars.
* ...
*/
function Triton(options) {
assert.object(options, 'options');
assert.object(options.log, 'options.log');
assert.optionalString(options.profile, 'options.profile');
assert.optionalString(options.config, 'options.config');
assert.optionalString(options.cachedir, 'options.cachedir');
assert.optionalString(options.profileName, 'options.profileName');
assert.optionalString(options.configPath, 'options.configPath');
assert.optionalString(options.cacheDir, 'options.cacheDir');
assert.optionalObject(options.envProfile, 'options.envProfile');
// Make sure a given bunyan logger has reasonable client_re[qs] serializers.
// Note: This was fixed in restify, then broken again in
@ -52,12 +57,15 @@ function Triton(options) {
} else {
this.log = options.log;
}
this.config = loadConfigSync(options.config);
this.config = loadConfigSync({
configPath: options.configPath,
envProfile: options.envProfile
});
this.profiles = this.config.profiles;
this.profile = this.getProfile(
options.profile || this.config.defaultProfile);
options.profileName || this.config.defaultProfile);
this.log.trace({profile: this.profile}, 'profile data');
this.cachedir = options.cachedir;
this.cacheDir = options.cacheDir;
this.cloudapi = this._cloudapiFromProfile(this.profile);
}
@ -117,23 +125,24 @@ Triton.prototype.listImages = function listImages(opts, cb) {
opts = {};
}
assert.object(opts, 'opts');
assert.optionalBool(opts.useCache, 'opts.useCache');
assert.func(cb, 'cb');
var cachefile;
if (self.cachedir)
cachefile = path.join(self.cachedir, 'images.json');
var cacheFile;
if (self.cacheDir)
cacheFile = path.join(self.cacheDir, 'images.json');
if (opts.usecache && !cachefile) {
cb(new Error('opts.usecache set but no cachedir found'));
if (opts.useCache && !cacheFile) {
cb(new Error('opts.useCache set but no cacheDir found'));
return;
}
// try to read the cache if the user wants it
// if this fails for any reason we fallback to hitting the cloudapi
if (opts.usecache) {
fs.readFile(cachefile, 'utf8', function (err, out) {
if (opts.useCache) {
fs.readFile(cacheFile, 'utf8', function (err, out) {
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();
return;
}
@ -141,7 +150,7 @@ Triton.prototype.listImages = function listImages(opts, cb) {
try {
data = JSON.parse(out);
} 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();
return;
}
@ -154,10 +163,10 @@ Triton.prototype.listImages = function listImages(opts, cb) {
fetch();
function fetch() {
self.cloudapi.listImages(opts, function (err, imgs, res) {
if (!err && self.cachedir) {
if (!err && self.cacheDir) {
// cache the results
var data = JSON.stringify(imgs);
fs.writeFile(cachefile, data, {encoding: 'utf8'}, function (err) {
fs.writeFile(cacheFile, data, {encoding: 'utf8'}, function (err) {
if (err)
self.log.info({err: err}, 'error caching images results');
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 {Function} callback `function (err, machine)`
* @param {String} name
* @param {Function} callback `function (err, inst)`
*/
Triton.prototype.getMachineByAlias = function getMachineByAlias(alias, callback) {
this.cloudapi.listMachines({name: alias}, function (err, machines) {
if (err) {
callback(err);
return;
}
var found = false;
machines.forEach(function (machine) {
if (!found && machine.name === alias) {
callback(null, machine);
found = true;
Triton.prototype.getInstance = function getInstance(name, cb) {
var self = this;
assert.string(name, 'name');
assert.func(cb, 'cb');
var shortId;
var inst;
vasync.pipeline({funcs: [
function tryUuid(_, next) {
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();
}
}
});
if (!found) {
callback(new Error('machine ' + alias + ' not found'));
return;
this.cloudapi.getMachine(uuid, function (err, inst) {
inst = inst;
next(err);
});
},
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)));
}
});
};