2014-02-07 23:21:24 +02:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2014, Joyent, Inc. All rights reserved.
|
|
|
|
*
|
2014-02-12 22:22:08 +02:00
|
|
|
* Core SDC driver class.
|
2014-02-07 23:21:24 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
var p = console.log;
|
|
|
|
var assert = require('assert-plus');
|
|
|
|
var async = require('async');
|
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-07-26 08:45:20 +03:00
|
|
|
var once = require('once');
|
2014-02-07 23:21:24 +02:00
|
|
|
var path = require('path');
|
2014-02-20 05:52:58 +02:00
|
|
|
var restify = require('restify');
|
2015-07-26 08:45:20 +03:00
|
|
|
var sprintf = require('util').format;
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
|
2014-02-12 22:22:08 +02:00
|
|
|
//---- SDC class
|
2014-02-07 23:21:24 +02:00
|
|
|
|
|
|
|
/**
|
2014-02-12 22:22:08 +02:00
|
|
|
* Create a SDC client.
|
2014-02-07 23:21:24 +02:00
|
|
|
*
|
|
|
|
* @param options {Object}
|
|
|
|
* - log {Bunyan Logger}
|
|
|
|
* - profile {String} Optional. Name of profile to use. Defaults to
|
|
|
|
* 'defaultProfile' in the config.
|
|
|
|
*/
|
2014-02-12 22:22:08 +02:00
|
|
|
function SDC(options) {
|
2014-02-07 23:21:24 +02:00
|
|
|
assert.object(options, 'options');
|
|
|
|
assert.object(options.log, 'options.log');
|
|
|
|
assert.optionalString(options.profile, 'options.profile');
|
|
|
|
|
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({
|
|
|
|
serializers: restify.bunyan.serializers
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.log = options.log;
|
|
|
|
}
|
2014-02-07 23:21:24 +02:00
|
|
|
this.config = loadConfigSync();
|
|
|
|
this.profiles = this.config.profiles;
|
|
|
|
this.profile = this.getProfile(
|
|
|
|
options.profile || this.config.defaultProfile);
|
|
|
|
this.log.trace({profile: this.profile}, 'profile data');
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-02-12 22:22:08 +02:00
|
|
|
SDC.prototype.setDefaultProfile =
|
2014-02-08 02:49:07 +02:00
|
|
|
function setDefaultProfile(name, callback) {
|
2014-02-07 23:21:24 +02:00
|
|
|
if (!this.getProfile(name)) {
|
|
|
|
return callback(new Error('no such profile: ' + name));
|
|
|
|
}
|
|
|
|
this.defaultProfileName = this.config.defaultProfile = name;
|
|
|
|
common.saveConfigSync(this.config);
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
2014-02-12 22:22:08 +02:00
|
|
|
SDC.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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create or update a profile.
|
|
|
|
*
|
|
|
|
* @param profile {Object}
|
|
|
|
* @param options {Object}
|
|
|
|
* - setDefault {Boolean}
|
|
|
|
* @param callback {Function} `function (err)`
|
|
|
|
*/
|
2014-02-12 22:22:08 +02:00
|
|
|
SDC.prototype.createOrUpdateProfile = function createOrUpdateProfile(
|
2014-02-07 23:21:24 +02:00
|
|
|
profile, options, callback) {
|
|
|
|
assert.object(profile, 'profile');
|
2014-02-08 02:49:07 +02:00
|
|
|
if (typeof (options) === 'function') {
|
2014-02-07 23:21:24 +02:00
|
|
|
callback = options;
|
|
|
|
options = {};
|
|
|
|
}
|
2014-02-08 02:49:07 +02:00
|
|
|
assert.object(options, 'options');
|
|
|
|
assert.optionalBool(options.setDefault, 'options.setDefault');
|
|
|
|
assert.func(callback, 'callback');
|
2014-02-07 23:21:24 +02:00
|
|
|
|
|
|
|
var found = false;
|
|
|
|
for (var i = 0; i < this.profiles.length; i++) {
|
|
|
|
if (this.profiles[i].name === profile.name) {
|
|
|
|
this.profiles[i] = profile;
|
|
|
|
found = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!found) {
|
|
|
|
this.profiles.push(profile);
|
|
|
|
}
|
|
|
|
if (options.setDefault) {
|
|
|
|
this.defaultProfileName = this.config.defaultProfile = profile.name;
|
|
|
|
}
|
|
|
|
common.saveConfigSync(this.config);
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
2014-02-12 22:22:08 +02:00
|
|
|
SDC.prototype.deleteProfile = function deleteProfile(name, callback) {
|
2014-02-07 23:21:24 +02:00
|
|
|
var found = false;
|
|
|
|
for (var i = 0; i < this.profiles.length; i++) {
|
|
|
|
if (this.profiles[i].name === name) {
|
|
|
|
found = true;
|
|
|
|
this.profiles.splice(i, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!found) {
|
|
|
|
return callback(new Error('no such profile: ' + name));
|
|
|
|
}
|
|
|
|
if (this.defaultProfileName === name) {
|
|
|
|
this.defaultProfileName = this.config.defaultProfile = null;
|
|
|
|
}
|
|
|
|
common.saveConfigSync(this.config);
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-02-12 22:22:08 +02:00
|
|
|
SDC.prototype._clientFromDc = function _clientFromDc(dc) {
|
2014-02-07 23:21:24 +02:00
|
|
|
assert.string(dc, 'dc');
|
|
|
|
|
|
|
|
if (!this._clientFromDcCache) {
|
|
|
|
this._clientFromDcCache = {};
|
|
|
|
}
|
|
|
|
if (!this._clientFromDcCache[dc]) {
|
|
|
|
var prof = this.profile;
|
|
|
|
var sign;
|
|
|
|
if (prof.privKey) {
|
2014-02-20 05:52:58 +02:00
|
|
|
sign = auth.privateKeySigner({
|
|
|
|
user: prof.user,
|
2014-02-07 23:21:24 +02:00
|
|
|
keyId: prof.keyId,
|
|
|
|
key: prof.privKey
|
|
|
|
});
|
|
|
|
} else {
|
2014-02-20 05:52:58 +02:00
|
|
|
sign = auth.cliSigner({
|
|
|
|
keyId: prof.keyId,
|
|
|
|
user: prof.user
|
|
|
|
});
|
2014-02-07 23:21:24 +02:00
|
|
|
}
|
2014-02-20 05:52:58 +02:00
|
|
|
var client = cloudapi.createClient({
|
2014-02-07 23:21:24 +02:00
|
|
|
url: this.config.dcs[dc],
|
2014-02-20 05:52:58 +02:00
|
|
|
user: prof.user,
|
2014-02-07 23:21:24 +02:00
|
|
|
version: '*',
|
|
|
|
rejectUnauthorized: Boolean(prof.rejectUnauthorized),
|
|
|
|
sign: sign,
|
|
|
|
log: this.log
|
|
|
|
});
|
|
|
|
this._clientFromDcCache[dc] = client;
|
|
|
|
}
|
|
|
|
return this._clientFromDcCache[dc];
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2015-07-26 08:45:20 +03:00
|
|
|
/**
|
|
|
|
* Return the resolved array of `{name: <dc-name>, url: <dc-url>}` for all
|
|
|
|
* DCs for the current profile.
|
|
|
|
*
|
|
|
|
* @throws {Error} If an unknown DC name is encountered.
|
|
|
|
* XXX make that UnknownDcError.
|
|
|
|
*/
|
|
|
|
SDC.prototype.dcs = function dcs() {
|
|
|
|
var self = this;
|
|
|
|
var aliases = self.config.dcAlias || {};
|
|
|
|
var resolved = [];
|
|
|
|
(self.profile.dcs || Object.keys(self.config.dcs)).forEach(function (n) {
|
|
|
|
var names = aliases[n] || [n];
|
|
|
|
names.forEach(function (name) {
|
|
|
|
if (!self.config.dcs[name]) {
|
|
|
|
throw new Error(sprintf('unknown dc "%s" for "%s" profile',
|
|
|
|
name, self.profile.name));
|
|
|
|
}
|
|
|
|
resolved.push({
|
|
|
|
name: name,
|
|
|
|
url: self.config.dcs[name]
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return resolved;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find a machine in the set of DCs for the current profile.
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* @param {Object} options
|
|
|
|
* - {String} machine (required) The machine id.
|
|
|
|
* XXX support name matching, prefix, etc.
|
|
|
|
* @param {Function} callback `function (err, machine, dc)`
|
|
|
|
* Returns the machine object (as from cloudapi GetMachine) and the `dc`,
|
|
|
|
* e.g. "us-west-1".
|
|
|
|
*/
|
|
|
|
SDC.prototype.findMachine = function findMachine(options, callback) {
|
|
|
|
//XXX Eventually this can be cached for a *full* uuid. Arguably for a
|
|
|
|
// uuid prefix or machine alias match, it cannot be cached, because an
|
|
|
|
// ambiguous machine could have been added.
|
|
|
|
var self = this;
|
|
|
|
assert.object(options, 'options');
|
|
|
|
assert.string(options.machine, 'options.machine');
|
|
|
|
assert.func(callback, 'callback');
|
|
|
|
var callback = once(callback);
|
|
|
|
|
|
|
|
var errs = [];
|
|
|
|
var foundMachine;
|
|
|
|
var foundDc;
|
|
|
|
async.each(
|
|
|
|
self.dcs(),
|
|
|
|
function oneDc(dc, next) {
|
|
|
|
var client = self._clientFromDc(dc.name);
|
|
|
|
client.getMachine({id: options.machine}, function (err, machine) {
|
|
|
|
if (err) {
|
|
|
|
errs.push(err);
|
|
|
|
} else if (machine) {
|
|
|
|
foundMachine = machine;
|
|
|
|
foundDc = dc.name;
|
|
|
|
// Return early on an unambiguous match.
|
|
|
|
// XXX When other than full 'id' is supported, this isn't unambiguous.
|
|
|
|
callback(null, foundMachine, foundDc);
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function done(surpriseErr) {
|
|
|
|
if (surpriseErr) {
|
|
|
|
callback(surpriseErr);
|
|
|
|
} else if (foundMachine) {
|
|
|
|
callback(null, foundMachine, foundDc)
|
|
|
|
} else if (errs.length) {
|
|
|
|
callback(errs.length === 1 ?
|
|
|
|
errs[0] : new errors.MultiError(errs));
|
|
|
|
} else {
|
|
|
|
callback(new errors.InternalError(
|
|
|
|
'unexpected error finding machine ' + options.id));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2014-02-07 23:21:24 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* List machines for the current profile.
|
|
|
|
*
|
2014-02-08 10:15:26 +02:00
|
|
|
* var res = this.jc.listMachines();
|
|
|
|
* res.on('data', function (dc, dcMachines) {
|
|
|
|
* //...
|
|
|
|
* });
|
|
|
|
* res.on('dcError', function (dc, dcErr) {
|
|
|
|
* //...
|
|
|
|
* });
|
|
|
|
* res.on('end', function () {
|
|
|
|
* //...
|
|
|
|
* });
|
|
|
|
*
|
2014-02-07 23:21:24 +02:00
|
|
|
* @param {Object} options Optional
|
|
|
|
*/
|
2014-02-12 22:22:08 +02:00
|
|
|
SDC.prototype.listMachines = function listMachines(options) {
|
2014-02-07 23:21:24 +02:00
|
|
|
var self = this;
|
2014-02-08 10:15:26 +02:00
|
|
|
if (options === undefined) {
|
2014-02-08 02:49:07 +02:00
|
|
|
options = {};
|
2014-02-07 23:21:24 +02:00
|
|
|
}
|
|
|
|
assert.object(options, 'options');
|
|
|
|
|
2014-02-08 10:15:26 +02:00
|
|
|
var emitter = new EventEmitter();
|
2014-02-07 23:21:24 +02:00
|
|
|
async.each(
|
2015-07-26 08:45:20 +03:00
|
|
|
self.dcs(),
|
2014-02-07 23:21:24 +02:00
|
|
|
function oneDc(dc, next) {
|
2015-07-26 08:45:20 +03:00
|
|
|
var client = self._clientFromDc(dc.name);
|
2014-02-07 23:21:24 +02:00
|
|
|
client.listMachines(function (err, machines) {
|
|
|
|
if (err) {
|
2015-07-26 08:45:20 +03:00
|
|
|
emitter.emit('dcError', dc.name, err);
|
2014-02-07 23:21:24 +02:00
|
|
|
} else {
|
2015-07-26 08:45:20 +03:00
|
|
|
emitter.emit('data', dc.name, machines);
|
2014-02-07 23:21:24 +02:00
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
function done(err) {
|
2014-02-08 10:15:26 +02:00
|
|
|
emitter.emit('end');
|
2014-02-07 23:21:24 +02:00
|
|
|
}
|
|
|
|
);
|
2014-02-08 10:15:26 +02:00
|
|
|
return emitter;
|
2014-02-07 23:21:24 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2015-07-26 08:45:20 +03:00
|
|
|
/**
|
|
|
|
* Return the audit for the given machine.
|
|
|
|
*
|
|
|
|
* @param {Object} options
|
|
|
|
* - {String} machine (required) The machine id.
|
|
|
|
* XXX support `machine` being more than just the UUID.
|
|
|
|
* @param {Function} callback of the form `function (err, audit, dc)`
|
|
|
|
*/
|
|
|
|
SDC.prototype.machineAudit = function machineAudit(options, callback) {
|
|
|
|
var self = this;
|
|
|
|
assert.object(options, 'options');
|
|
|
|
assert.string(options.machine, 'options.machine');
|
|
|
|
|
|
|
|
self.findMachine({machine: options.machine}, function (err, machine, dc) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
var client = self._clientFromDc(dc);
|
|
|
|
client.machineAudit({id: machine.id}, function (err, audit) {
|
|
|
|
callback(err, audit, dc);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2014-02-07 23:21:24 +02:00
|
|
|
|
|
|
|
//---- exports
|
|
|
|
|
2014-02-12 22:22:08 +02:00
|
|
|
module.exports = SDC;
|