2014-02-07 23:21:24 +02:00
|
|
|
/*
|
2015-08-25 23:11:40 +03:00
|
|
|
* Copyright (c) 2015, Joyent, Inc. All rights reserved.
|
2014-02-07 23:21:24 +02:00
|
|
|
*
|
2015-08-25 23:11:40 +03:00
|
|
|
* Core Triton client driver class.
|
2014-02-07 23:21:24 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
var p = console.log;
|
|
|
|
var assert = require('assert-plus');
|
2014-02-20 05:52:58 +02:00
|
|
|
var auth = require('smartdc-auth');
|
2014-02-08 10:15:26 +02:00
|
|
|
var EventEmitter = require('events').EventEmitter;
|
2014-02-07 23:21:24 +02:00
|
|
|
var fs = require('fs');
|
2015-08-26 20:02:01 +03:00
|
|
|
var format = require('util').format;
|
2015-07-26 08:45:20 +03:00
|
|
|
var once = require('once');
|
2014-02-07 23:21:24 +02:00
|
|
|
var path = require('path');
|
2015-08-25 23:11:40 +03:00
|
|
|
var restifyClients = require('restify-clients');
|
2015-08-26 06:53:48 +03:00
|
|
|
var tabula = require('tabula');
|
2015-08-27 03:21:27 +03:00
|
|
|
var vasync = require('vasync');
|
2014-02-07 23:21:24 +02:00
|
|
|
|
2014-02-20 05:52:58 +02:00
|
|
|
var cloudapi = require('./cloudapi2');
|
2014-02-07 23:21:24 +02:00
|
|
|
var common = require('./common');
|
2015-07-26 08:45:20 +03:00
|
|
|
var errors = require('./errors');
|
2014-02-07 23:21:24 +02:00
|
|
|
var loadConfigSync = require('./config').loadConfigSync;
|
|
|
|
|
|
|
|
|
|
|
|
|
2015-08-25 23:11:40 +03:00
|
|
|
//---- Triton class
|
2014-02-07 23:21:24 +02:00
|
|
|
|
|
|
|
/**
|
2015-08-25 23:11:40 +03:00
|
|
|
* Create a Triton client.
|
2014-02-07 23:21:24 +02:00
|
|
|
*
|
|
|
|
* @param options {Object}
|
|
|
|
* - log {Bunyan Logger}
|
2015-08-27 03:21:27 +03:00
|
|
|
* - profileName {String} Optional. Name of profile to use. Defaults to
|
2014-02-07 23:21:24 +02:00
|
|
|
* 'defaultProfile' in the config.
|
2015-08-27 03:21:27 +03:00
|
|
|
* - envProfile {Object} Optional. A starter 'env' profile object. Missing
|
|
|
|
* fields will be filled in from standard SDC_* envvars.
|
|
|
|
* ...
|
2014-02-07 23:21:24 +02:00
|
|
|
*/
|
2015-08-25 23:11:40 +03:00
|
|
|
function Triton(options) {
|
2014-02-07 23:21:24 +02:00
|
|
|
assert.object(options, 'options');
|
|
|
|
assert.object(options.log, 'options.log');
|
2015-08-27 03:21:27 +03:00
|
|
|
assert.optionalString(options.profileName, 'options.profileName');
|
|
|
|
assert.optionalString(options.configPath, 'options.configPath');
|
|
|
|
assert.optionalString(options.cacheDir, 'options.cacheDir');
|
|
|
|
assert.optionalObject(options.envProfile, 'options.envProfile');
|
2014-02-07 23:21:24 +02:00
|
|
|
|
2014-02-20 05:52:58 +02:00
|
|
|
// Make sure a given bunyan logger has reasonable client_re[qs] serializers.
|
|
|
|
// Note: This was fixed in restify, then broken again in
|
|
|
|
// https://github.com/mcavage/node-restify/pull/501
|
|
|
|
if (options.log.serializers &&
|
|
|
|
(!options.log.serializers.client_req ||
|
|
|
|
!options.log.serializers.client_req)) {
|
|
|
|
this.log = options.log.child({
|
2015-08-25 23:11:40 +03:00
|
|
|
// XXX cheating. restify-clients should export its 'bunyan'.
|
|
|
|
serializers: require('restify-clients/lib/helpers/bunyan').serializers
|
2014-02-20 05:52:58 +02:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.log = options.log;
|
|
|
|
}
|
2015-08-27 03:21:27 +03:00
|
|
|
this.config = loadConfigSync({
|
|
|
|
configPath: options.configPath,
|
|
|
|
envProfile: options.envProfile
|
|
|
|
});
|
2014-02-07 23:21:24 +02:00
|
|
|
this.profiles = this.config.profiles;
|
|
|
|
this.profile = this.getProfile(
|
2015-08-27 03:21:27 +03:00
|
|
|
options.profileName || this.config.defaultProfile);
|
2014-02-07 23:21:24 +02:00
|
|
|
this.log.trace({profile: this.profile}, 'profile data');
|
2015-08-27 03:21:27 +03:00
|
|
|
this.cacheDir = options.cacheDir;
|
2015-08-25 23:11:40 +03:00
|
|
|
|
|
|
|
this.cloudapi = this._cloudapiFromProfile(this.profile);
|
2014-02-07 23:21:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2015-08-25 23:11:40 +03:00
|
|
|
Triton.prototype.getProfile = function getProfile(name) {
|
2014-02-07 23:21:24 +02:00
|
|
|
for (var i = 0; i < this.profiles.length; i++) {
|
|
|
|
if (this.profiles[i].name === name) {
|
|
|
|
return this.profiles[i];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2015-08-25 23:11:40 +03:00
|
|
|
Triton.prototype._cloudapiFromProfile = function _cloudapiFromProfile(profile) {
|
|
|
|
assert.object(profile, 'profile');
|
|
|
|
assert.string(profile.account, 'profile.account');
|
|
|
|
assert.string(profile.keyId, 'profile.keyId');
|
|
|
|
assert.string(profile.url, 'profile.url');
|
|
|
|
assert.optionalString(profile.privKey, 'profile.privKey');
|
|
|
|
assert.optionalBool(profile.insecure, 'profile.insecure');
|
|
|
|
var rejectUnauthorized = (profile.insecure === undefined
|
|
|
|
? true : !profile.insecure);
|
|
|
|
|
|
|
|
var sign;
|
|
|
|
if (profile.privKey) {
|
|
|
|
sign = auth.privateKeySigner({
|
|
|
|
user: profile.account,
|
|
|
|
keyId: profile.keyId,
|
|
|
|
key: profile.privKey
|
2014-02-07 23:21:24 +02:00
|
|
|
});
|
2015-08-25 23:11:40 +03:00
|
|
|
} else {
|
|
|
|
sign = auth.cliSigner({
|
|
|
|
keyId: profile.keyId,
|
|
|
|
user: profile.account
|
2015-07-26 08:45:20 +03:00
|
|
|
});
|
2015-08-25 23:11:40 +03:00
|
|
|
}
|
|
|
|
var client = cloudapi.createClient({
|
|
|
|
url: profile.url,
|
|
|
|
user: profile.account,
|
|
|
|
version: '*',
|
|
|
|
rejectUnauthorized: rejectUnauthorized,
|
|
|
|
sign: sign,
|
|
|
|
log: this.log
|
2015-07-26 08:45:20 +03:00
|
|
|
});
|
2015-08-25 23:11:40 +03:00
|
|
|
return client;
|
2015-07-26 08:45:20 +03:00
|
|
|
};
|
|
|
|
|
2015-08-26 19:59:12 +03:00
|
|
|
/**
|
|
|
|
* 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');
|
2015-08-27 03:21:27 +03:00
|
|
|
assert.optionalBool(opts.useCache, 'opts.useCache');
|
2015-08-26 19:59:12 +03:00
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
2015-08-27 03:21:27 +03:00
|
|
|
var cacheFile;
|
|
|
|
if (self.cacheDir)
|
|
|
|
cacheFile = path.join(self.cacheDir, 'images.json');
|
2015-08-26 19:59:12 +03:00
|
|
|
|
2015-08-27 03:21:27 +03:00
|
|
|
if (opts.useCache && !cacheFile) {
|
|
|
|
cb(new Error('opts.useCache set but no cacheDir found'));
|
2015-08-26 19:59:12 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// try to read the cache if the user wants it
|
|
|
|
// if this fails for any reason we fallback to hitting the cloudapi
|
2015-08-27 03:21:27 +03:00
|
|
|
if (opts.useCache) {
|
|
|
|
fs.readFile(cacheFile, 'utf8', function (err, out) {
|
2015-08-26 19:59:12 +03:00
|
|
|
if (err) {
|
2015-08-27 03:21:27 +03:00
|
|
|
self.log.info({err: err}, 'failed to read cache file %s', cacheFile);
|
2015-08-26 19:59:12 +03:00
|
|
|
fetch();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var data;
|
|
|
|
try {
|
|
|
|
data = JSON.parse(out);
|
|
|
|
} catch (e) {
|
2015-08-27 03:21:27 +03:00
|
|
|
self.log.info({err: e}, 'failed to parse cache file %s', cacheFile);
|
2015-08-26 19:59:12 +03:00
|
|
|
fetch();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
cb(null, data, {});
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
fetch();
|
|
|
|
function fetch() {
|
2015-08-26 20:05:50 +03:00
|
|
|
self.cloudapi.listImages(opts, function (err, imgs, res) {
|
2015-08-27 03:21:27 +03:00
|
|
|
if (!err && self.cacheDir) {
|
2015-08-26 19:59:12 +03:00
|
|
|
// cache the results
|
|
|
|
var data = JSON.stringify(imgs);
|
2015-08-27 03:21:27 +03:00
|
|
|
fs.writeFile(cacheFile, data, {encoding: 'utf8'}, function (err) {
|
2015-08-26 19:59:12 +03:00
|
|
|
if (err)
|
|
|
|
self.log.info({err: err}, 'error caching images results');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
done();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function done() {
|
|
|
|
cb(err, imgs, res);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
2015-07-26 08:45:20 +03:00
|
|
|
|
|
|
|
/**
|
2015-08-26 19:15:17 +03:00
|
|
|
* Get an image by ID, exact name, or short ID, in that order.
|
|
|
|
*
|
|
|
|
* If there is more than one image with that name, then the latest
|
|
|
|
* (by published_at) is returned.
|
2015-07-26 08:45:20 +03:00
|
|
|
*/
|
2015-08-26 06:53:48 +03:00
|
|
|
Triton.prototype.getImage = function getImage(name, cb) {
|
|
|
|
assert.string(name, 'name');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
if (common.UUID_RE.test(name)) {
|
|
|
|
this.cloudapi.getImage({id: name}, function (err, img) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
} else if (img.state !== 'active') {
|
|
|
|
cb(new Error(format('image %s is not active', name)));
|
|
|
|
} else {
|
|
|
|
cb(null, img);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.cloudapi.listImages(function (err, imgs) {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
2015-08-26 20:02:01 +03:00
|
|
|
|
2015-08-26 06:53:48 +03:00
|
|
|
var nameMatches = [];
|
2015-08-26 19:15:17 +03:00
|
|
|
var shortIdMatches = [];
|
2015-08-26 06:53:48 +03:00
|
|
|
for (var i = 0; i < imgs.length; i++) {
|
2015-08-26 19:15:17 +03:00
|
|
|
var img = imgs[i];
|
|
|
|
if (img.name === name) {
|
|
|
|
nameMatches.push(img);
|
|
|
|
}
|
|
|
|
if (img.id.slice(0, 8) === name) {
|
|
|
|
shortIdMatches.push(img);
|
2015-07-26 08:45:20 +03:00
|
|
|
}
|
2015-08-26 06:53:48 +03:00
|
|
|
}
|
2015-08-26 20:02:01 +03:00
|
|
|
|
2015-08-26 19:15:17 +03:00
|
|
|
if (nameMatches.length === 1) {
|
2015-08-26 06:53:48 +03:00
|
|
|
cb(null, nameMatches[0]);
|
2015-08-26 19:15:17 +03:00
|
|
|
} else if (nameMatches.length > 1) {
|
2015-08-26 06:53:48 +03:00
|
|
|
tabula.sortArrayOfObjects(nameMatches, 'published_at');
|
|
|
|
cb(null, nameMatches[nameMatches.length - 1]);
|
2015-08-26 19:15:17 +03:00
|
|
|
} else if (shortIdMatches.length === 1) {
|
|
|
|
cb(null, shortIdMatches[0]);
|
|
|
|
} else if (shortIdMatches.length === 0) {
|
|
|
|
cb(new Error(format(
|
|
|
|
'no image with name or shortId "%s" was found', name)));
|
|
|
|
} else {
|
|
|
|
cb(new Error(format('no image with name "%s" was found '
|
|
|
|
+ 'and "%s" is an ambiguous shortId', name)));
|
2015-07-26 08:45:20 +03:00
|
|
|
}
|
2015-08-26 06:53:48 +03:00
|
|
|
});
|
2014-02-07 23:21:24 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2015-07-26 08:45:20 +03:00
|
|
|
/**
|
2015-08-26 20:02:01 +03:00
|
|
|
* Get an active package by ID, exact name, or short ID, in that order.
|
|
|
|
*
|
|
|
|
* If there is more than one package with that name, then this errors out.
|
2015-07-26 08:45:20 +03:00
|
|
|
*/
|
2015-08-26 06:53:48 +03:00
|
|
|
Triton.prototype.getPackage = function getPackage(name, cb) {
|
|
|
|
assert.string(name, 'name');
|
|
|
|
assert.func(cb, 'cb');
|
|
|
|
|
|
|
|
if (common.UUID_RE.test(name)) {
|
|
|
|
this.cloudapi.getPackage({id: name}, function (err, pkg) {
|
|
|
|
if (err) {
|
|
|
|
cb(err);
|
|
|
|
} else if (!pkg.active) {
|
|
|
|
cb(new Error(format('image %s is not active', name)));
|
|
|
|
} else {
|
|
|
|
cb(null, pkg);
|
|
|
|
}
|
2015-07-26 08:45:20 +03:00
|
|
|
});
|
2015-08-26 06:53:48 +03:00
|
|
|
} else {
|
|
|
|
this.cloudapi.listPackages(function (err, pkgs) {
|
|
|
|
if (err) {
|
|
|
|
return cb(err);
|
|
|
|
}
|
2015-08-26 20:02:01 +03:00
|
|
|
|
2015-08-26 06:53:48 +03:00
|
|
|
var nameMatches = [];
|
2015-08-26 20:02:01 +03:00
|
|
|
var shortIdMatches = [];
|
2015-08-26 06:53:48 +03:00
|
|
|
for (var i = 0; i < pkgs.length; i++) {
|
2015-08-26 20:02:01 +03:00
|
|
|
var pkg = pkgs[i];
|
|
|
|
if (pkg.name === name) {
|
|
|
|
nameMatches.push(pkg);
|
|
|
|
}
|
|
|
|
if (pkg.id.slice(0, 8) === name) {
|
|
|
|
shortIdMatches.push(pkg);
|
2015-08-26 06:53:48 +03:00
|
|
|
}
|
|
|
|
}
|
2015-08-26 20:02:01 +03:00
|
|
|
|
|
|
|
if (nameMatches.length === 1) {
|
2015-08-26 06:53:48 +03:00
|
|
|
cb(null, nameMatches[0]);
|
2015-08-26 20:02:01 +03:00
|
|
|
} else if (nameMatches.length > 1) {
|
2015-08-26 06:53:48 +03:00
|
|
|
cb(new Error(format(
|
|
|
|
'package name "%s" is ambiguous: matches %d packages',
|
|
|
|
name, nameMatches.length)));
|
2015-08-26 20:02:01 +03:00
|
|
|
} else if (shortIdMatches.length === 1) {
|
|
|
|
cb(null, shortIdMatches[0]);
|
|
|
|
} else if (shortIdMatches.length === 0) {
|
|
|
|
cb(new Error(format(
|
|
|
|
'no package with name or shortId "%s" was found', name)));
|
|
|
|
} else {
|
|
|
|
cb(new Error(format('no package with name "%s" was found '
|
|
|
|
+ 'and "%s" is an ambiguous shortId', name)));
|
2015-08-26 06:53:48 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2015-07-26 08:45:20 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2015-08-26 03:27:46 +03:00
|
|
|
/**
|
2015-08-27 03:21:27 +03:00
|
|
|
* Get an instance by ID, exact name, or short ID, in that order.
|
2015-08-26 03:27:46 +03:00
|
|
|
*
|
2015-08-27 03:21:27 +03:00
|
|
|
* @param {String} name
|
|
|
|
* @param {Function} callback `function (err, inst)`
|
2015-08-26 03:27:46 +03:00
|
|
|
*/
|
2015-08-27 03:21:27 +03:00
|
|
|
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();
|
|
|
|
}
|
2015-08-26 03:27:46 +03:00
|
|
|
}
|
2015-08-27 03:21:27 +03:00
|
|
|
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)));
|
2015-08-26 03:27:46 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2014-02-07 23:21:24 +02:00
|
|
|
|
|
|
|
//---- exports
|
|
|
|
|
2015-08-25 23:11:40 +03:00
|
|
|
module.exports = Triton;
|