From a5213658fad3c9c5b3a1b6c3b98c5dc8a968907f Mon Sep 17 00:00:00 2001 From: Dave Eddy Date: Wed, 26 Aug 2015 12:59:12 -0400 Subject: [PATCH] config, cache images --- lib/cli.js | 20 ++++++++++++- lib/config.js | 12 ++++---- lib/do_images.js | 2 +- lib/do_instances.js | 38 ++++++++++++++++++++++-- lib/triton.js | 71 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 131 insertions(+), 12 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index 927b34a..2ef3956 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -14,6 +14,7 @@ var cmdln = require('cmdln'), var fs = require('fs'); var util = require('util'), format = util.format; +var path = require('path'); var vasync = require('vasync'); var common = require('./common'); @@ -99,7 +100,24 @@ CLI.prototype.init = function (opts, args, callback) { this.__defineGetter__('triton', function () { if (self._triton === undefined) { - self._triton = new Triton({log: log, profile: opts.profile}); + var userConfigPath = require('./config').DEFAULT_USER_CONFIG_PATH; + var dir = path.dirname(userConfigPath); + var cacheDir = path.join(dir, 'cache'); + + [dir, cacheDir].forEach(function (d) { + try { + fs.mkdirSync(d); + } catch (e) { + log.info({err: e}, 'failed to make dir %s', d); + } + }); + + self._triton = new Triton({ + log: log, + profile: opts.profile, + config: userConfigPath, + cachedir: cacheDir + }); } return self._triton; }); diff --git a/lib/config.js b/lib/config.js index 1b80b5a..336a862 100755 --- a/lib/config.js +++ b/lib/config.js @@ -13,7 +13,7 @@ var common = require('./common'); var errors = require('./errors'); -var CONFIG_PATH = path.resolve(process.env.HOME, '.triton', 'config.json'); +var DEFAULT_USER_CONFIG_PATH = path.resolve(process.env.HOME, '.triton', 'config.json'); var DEFAULTS_PATH = path.resolve(__dirname, '..', 'etc', 'defaults.json'); var OVERRIDE_KEYS = []; // config object keys to do a one-level deep override @@ -24,17 +24,17 @@ 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() { +function loadConfigSync(configPath) { var c = fs.readFileSync(DEFAULTS_PATH, 'utf8'); var _defaults = JSON.parse(c); var config = JSON.parse(c); - if (fs.existsSync(CONFIG_PATH)) { - c = fs.readFileSync(CONFIG_PATH, 'utf8'); + if (configPath && fs.existsSync(configPath)) { + c = fs.readFileSync(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', CONFIG_PATH)); + sprintf('"%s" is not an object', configPath)); } // These special keys are merged into the key of the same name in the // base "defaults.json". @@ -88,7 +88,7 @@ function updateUserConfigSync(config, updates) { //---- exports module.exports = { - CONFIG_PATH: CONFIG_PATH, + DEFAULT_USER_CONFIG_PATH: DEFAULT_USER_CONFIG_PATH, loadConfigSync: loadConfigSync }; // vim: set softtabstop=4 shiftwidth=4: diff --git a/lib/do_images.js b/lib/do_images.js index 2a9f942..9556da0 100644 --- a/lib/do_images.js +++ b/lib/do_images.js @@ -41,7 +41,7 @@ function do_images(subcmd, opts, args, callback) { listOpts.state = 'all'; } - this.triton.cloudapi.listImages(listOpts, function onRes(err, imgs, res) { + this.triton.listImages(listOpts, function onRes(err, imgs, res) { if (err) { return callback(err); } diff --git a/lib/do_instances.js b/lib/do_instances.js index d3626d6..08a8678 100644 --- a/lib/do_instances.js +++ b/lib/do_instances.js @@ -4,6 +4,8 @@ * `triton instances ...` */ +var f = require('util').format; + var tabula = require('tabula'); var common = require('./common'); @@ -33,6 +35,7 @@ var validFields = [ 'updated', 'package', 'image', + 'img', 'ago' ]; @@ -56,17 +59,46 @@ function do_instances(subcmd, opts, args, callback) { return; } - this.triton.cloudapi.listMachines(listOpts, function (err, machines) { + var i = 0; + + i++; + var images; + this.triton.listImages({usecache: true}, function (err, _images) { if (err) { callback(err); return; } + images = _images; + done(); + }); + + i++; + var machines; + this.triton.cloudapi.listMachines(listOpts, function (err, _machines) { + if (err) { + callback(err); + return; + } + machines = _machines; + done(); + }); + + function done() { + if (--i > 0) + return; + + // map "uuid" => "image_name" + var imgmap = {}; + images.forEach(function (image) { + imgmap[image.id] = f('%s@%s', image.name, image.version); + }); // add extra fields for nice output 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; }); if (opts.json) { @@ -80,7 +112,7 @@ function do_instances(subcmd, opts, args, callback) { }); } callback(); - }); + } } do_instances.options = [ @@ -97,7 +129,7 @@ do_instances.options = [ { names: ['o'], type: 'string', - default: 'id,name,state,type,image,memory,disk,ago', + default: 'id,name,state,type,img,memory,disk,ago', help: 'Specify fields (columns) to output.', helpArg: 'field1,...' }, diff --git a/lib/triton.js b/lib/triton.js index ebe6859..34095a9 100644 --- a/lib/triton.js +++ b/lib/triton.js @@ -36,6 +36,8 @@ 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'); // Make sure a given bunyan logger has reasonable client_re[qs] serializers. // Note: This was fixed in restify, then broken again in @@ -50,11 +52,12 @@ function Triton(options) { } else { this.log = options.log; } - this.config = loadConfigSync(); + this.config = loadConfigSync(options.config); this.profiles = this.config.profiles; this.profile = this.getProfile( options.profile || this.config.defaultProfile); this.log.trace({profile: this.profile}, 'profile data'); + this.cachedir = options.cachedir; this.cloudapi = this._cloudapiFromProfile(this.profile); } @@ -104,6 +107,72 @@ Triton.prototype._cloudapiFromProfile = function _cloudapiFromProfile(profile) { return client; }; +/** + * cloudapi listImages wrapper with optional caching + */ +Triton.prototype.listImages = function listImages(opts, cb) { + var self = this; + if (cb === undefined) { + cb = opts; + opts = {}; + } + assert.object(opts, 'opts'); + assert.func(cb, 'cb'); + + 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')); + 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 (err) { + self.log.info({err: err}, 'failed to read cache file %s', cachefile); + fetch(); + return; + } + var data; + try { + data = JSON.parse(out); + } catch (e) { + self.log.info({err: e}, 'failed to parse cache file %s', cachefile); + fetch(); + return; + } + + cb(null, data, {}); + }); + return; + } + + fetch(); + function fetch() { + self.cloudapi.listImages(function (err, imgs, res) { + if (!err && self.cachedir) { + // cache the results + var data = JSON.stringify(imgs); + fs.writeFile(cachefile, data, {encoding: 'utf8'}, function (err) { + if (err) + self.log.info({err: err}, 'error caching images results'); + done(); + }); + } else { + done(); + } + + + function done() { + cb(err, imgs, res); + } + }); + } +}; /** * Get an image by ID, exact name, or short ID, in that order.