vortex
Version:
Virtual machine management toolkit.
708 lines (652 loc) • 23.3 kB
JavaScript
(function() {
var async, aws_sdk, fs, logsmith, path_extra, portchecker;
fs = require('fs');
async = require('async');
aws_sdk = require('aws-sdk');
logsmith = require('logsmith');
path_extra = require('path-extra');
portchecker = require('portchecker');
exports.Provider = (function() {
/*
This class exposes Amazon as a provider to Vortex.
*/
function _Class(manifest) {
this.manifest = manifest;
/*
The provider accepts a manifest as a parameter by specification.
*/
aws_sdk.config.update(this.extract_client_options());
}
_Class.prototype.get_node = function(node_name) {
/*
This method returns a node by looking up its name. It throws an error if the node is not found.
*/
if ((this.manifest.nodes != null) && (this.manifest.nodes[node_name] != null)) {
return this.manifest.nodes[node_name];
}
throw new Error("node " + node_name + " does not exist");
};
_Class.prototype.extract_property = function(property_name, node_name) {
/*
Extracts a property by looking into a node and upper layers of the manifest.
*/
var e, node, _ref, _ref1;
try {
node = this.get_node(node_name);
} catch (_error) {
e = _error;
node = null;
}
if ((node != null ? (_ref = node.amazon) != null ? _ref[property_name] : void 0 : void 0) != null) {
return node.amazon[property_name];
}
if (((_ref1 = this.manifest.amazon) != null ? _ref1[property_name] : void 0) != null) {
return this.manifest.amazon[property_name];
}
return null;
};
_Class.prototype.extract_access_key_id = function(node_name) {
return this.extract_property('accessKeyId', node_name);
};
_Class.prototype.extract_secret_access_key = function(node_name) {
return this.extract_property('secretAccessKey', node_name);
};
_Class.prototype.extract_region = function(node_name) {
return this.extract_property('region', node_name);
};
_Class.prototype.extract_max_retries = function(node_name) {
return this.extract_property('maxRetries', node_name);
};
_Class.prototype.extract_image_id = function(node_name) {
return this.extract_property('imageId', node_name);
};
_Class.prototype.extract_instance_type = function(node_name) {
return this.extract_property('instanceType', node_name);
};
_Class.prototype.extract_key_name = function(node_name) {
return this.extract_property('keyName', node_name);
};
_Class.prototype.extract_security_groups = function(node_name) {
return this.extract_property('securityGroups', node_name);
};
_Class.prototype.extract_user_data = function(node_name) {
return this.extract_property('userData', node_name);
};
_Class.prototype.extract_disable_api_termination = function(node_name) {
return this.extract_property('disableApiTermination', node_name);
};
_Class.prototype.extract_username = function(node_name) {
return this.extract_property('username', node_name);
};
_Class.prototype.extract_password = function(node_name) {
return this.extract_property('password', node_name);
};
_Class.prototype.extract_private_key = function(node_name) {
return this.extract_property('privateKey', node_name);
};
_Class.prototype.extract_passphrase = function(node_name) {
return this.extract_property('passphrase', node_name);
};
_Class.prototype.extract_ssh_port = function(node_name) {
return this.extract_property('sshPort', node_name);
};
_Class.prototype.extract_namespace = function(node_name) {
/*
Extracts a namespace by looking it up in the node itself and upper layers of the manifest
*/
var node;
try {
node = this.get_node(node_name);
} catch (_error) {
node = null;
}
if ((node != null ? node.namespace : void 0) != null) {
return node.namespace;
}
if (this.manifest.namespace != null) {
return this.manifest.namespace;
}
};
_Class.prototype.extract_client_options = function(node_name) {
/*
Extracts options related to the AWS client.
*/
var access_key_id, max_retries, options, region, secret_access_key;
access_key_id = this.extract_access_key_id(node_name);
secret_access_key = this.extract_secret_access_key(node_name);
region = this.extract_region(node_name);
max_retries = this.extract_max_retries(node_name);
options = {};
if (access_key_id) {
options.accessKeyId = access_key_id;
}
if (secret_access_key) {
options.secretAccessKey = secret_access_key;
}
if (region) {
options.region = region;
}
if (max_retries) {
options.maxRetries = max_retries;
}
return options;
};
_Class.prototype.extract_instance_options = function(node_name) {
/*
Extracts options related to AWS instances.
*/
var disable_api_termination, image_id, instance_type, key_name, options, security_groups, user_data;
image_id = this.extract_image_id(node_name);
instance_type = this.extract_instance_type(node_name);
key_name = this.extract_key_name(node_name);
security_groups = this.extract_security_groups(node_name);
user_data = this.extract_user_data(node_name);
disable_api_termination = this.extract_disable_api_termination(node_name);
options = {};
if (image_id) {
options.ImageId = image_id;
}
if (instance_type) {
options.InstanceType = instance_type;
}
if (key_name) {
options.KeyName = key_name;
}
if (security_groups) {
options.SecurityGroups = security_groups;
}
if (user_data) {
options.UserData = user_data;
}
if (disable_api_termination) {
options.DisableApiTermination = disable_api_termination;
}
return options;
};
_Class.prototype.get_client = function(node_name) {
/*
Obtain a client for EC2.
*/
return new aws_sdk.EC2(this.extract_client_options(node_name));
};
_Class.prototype.create_error = function(error, node_name) {
/*
Creates a friendlier error message.
*/
var message, parts, tokens, type;
if (error.code === 'NetworkingError') {
return error;
} else {
tokens = error.toString().split(':');
type = tokens[0];
message = tokens[1].trim();
parts = message.split('.');
message = parts.shift().toLowerCase().trim();
if (node_name) {
message = "" + message + " for node " + node_name;
}
if (parts.length > 0) {
message = "" + message + " (" + (parts.join('.').trim()) + ")";
}
message = message.replace(/\s'(\w+)'\s/, function(match, group) {
var param;
param = group.toLowerCase();
switch (param) {
case 'accesskeyid':
param = 'accessKeyId';
break;
case 'secretaccesskey':
param = 'secretAccessKey';
break;
case 'region':
param = 'region';
break;
case 'maxretries':
param = 'maxRetries';
break;
case 'imageid':
param = 'imageId';
break;
case 'instancetype':
param = 'instanceType';
break;
case 'keyname':
param = 'keyName';
break;
case 'securitygroups':
param = 'securityGroups';
break;
case 'userdata':
param = 'userData';
break;
case 'disableapitermination':
param = 'disableApiTermination';
}
return ' "' + param + '" ';
});
message = message[0] + message.substring(1, message.length);
return new Error(message);
}
};
_Class.prototype.bootstrap = function(node_name, callback) {
/*
Provider-specific method for bootstrapping a node.
*/
var obtain_shell_spec, prepare_exposed, verify_status,
_this = this;
verify_status = function(callback) {
return _this.status(node_name, function(err, state, address) {
if (err) {
return callback(err);
}
if (state !== 'running') {
return callback(new Error("node " + node_name + " is not ready"));
}
return callback(null);
});
};
obtain_shell_spec = function(callback) {
return _this.shell_spec(node_name, function(err, spec) {
if (err) {
return callback(err);
}
return callback(null, spec);
});
};
prepare_exposed = function(spec, callback) {
var dst, e, handle_exposure, node, src;
try {
node = _this.get_node(node_name);
} catch (_error) {
e = _error;
node = null;
}
if ((node != null ? node.expose : void 0) == null) {
return callback(null);
}
handle_exposure = function(exposure, callback) {
var source_path;
source_path = path_extra.resolve(path_extra.dirname(_this.manifest.meta.location), exposure.src);
return fs.stat(source_path, function(err, stats) {
if (err) {
return callback(new Error("cannot expose " + exposure.src + " because it does not exist"));
}
return callback(null);
});
};
return async.eachSeries((function() {
var _ref, _results;
_ref = node.expose;
_results = [];
for (src in _ref) {
dst = _ref[src];
_results.push({
src: src,
dst: dst
});
}
return _results;
})(), handle_exposure, callback);
};
return async.waterfall([verify_status, obtain_shell_spec, prepare_exposed], function(err, state, address) {
if (err) {
return callback(err);
}
return callback(null);
});
};
_Class.prototype.status = function(node_name, callback) {
/*
Provider-specific method for checking the status of a node.
*/
var client, e, options,
_this = this;
try {
client = this.get_client(node_name);
} catch (_error) {
e = _error;
return callback(this.create_error(e, node_name));
}
options = {
Filters: [
{
Name: 'tag:vortex-node-name',
Values: [node_name]
}, {
Name: 'tag:vortex-node-namespace',
Values: [this.extract_namespace(node_name)]
}
]
};
logsmith.debug('describe instances with options', options);
return client.describeInstances(options, function(err, result) {
var address, instance, instances, reservation, selected_instance, state, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2;
if (err) {
return callback(_this.create_error(err, node_name));
}
instances = [];
_ref = result.Reservations;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
reservation = _ref[_i];
_ref1 = reservation.Instances;
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
instance = _ref1[_j];
instances.push({
id: instance.InstanceId,
state: instance.State.Name,
address: instance.PublicDnsName
});
}
}
if (instances.length === 0) {
return callback(null, 'stopped');
}
logsmith.debug('discovered instances', instances);
selected_instance = instances[instances.length - 1];
if (!selected_instance) {
return callback(new Error("could not obtain instance for node " + node_name));
}
logsmith.debug('selected instance', selected_instance);
for (_k = 0, _len2 = instances.length; _k < _len2; _k++) {
instance = instances[_k];
if (((_ref2 = instance.state) !== 'shutting-down' && _ref2 !== 'terminated' && _ref2 !== 'stopping' && _ref2 !== 'stopped') && selected_instance !== instance) {
logsmith.warn("duplicate node " + node_name + " with instance id " + instance.id + " detected");
}
}
state = (function() {
switch (selected_instance.state) {
case 'pending':
return 'booting';
case 'running':
return 'running';
case 'stopped':
return 'stopped';
case 'stopping':
return 'halting';
case 'terminated':
return 'stopped';
case 'shutting-down':
return 'halting';
default:
return null;
}
})();
if (!state) {
return callback(new Error("undefined state for node " + node_name));
}
logsmith.debug("node " + node_name + " with instance id " + selected_instance.id + " has state " + state);
address = selected_instance.address;
if (!address) {
state = 'booting';
}
if (state !== 'running') {
address = null;
}
return callback(null, state, address, selected_instance.id);
});
};
_Class.prototype.boot = function(node_name, callback) {
/*
Provider-specific method for booting a node.
*/
var client, e, map_tags, run_instance, verify_status,
_this = this;
try {
client = this.get_client(node_name);
} catch (_error) {
e = _error;
return callback(this.create_error(e, node_name));
}
verify_status = function(callback) {
return _this.status(node_name, function(err, state, address) {
if (err) {
return callback(err);
}
if (state === 'booting') {
return callback(new Error("node " + node_name + " is already booting"));
}
if (state === 'running') {
return callback(new Error("node " + node_name + " is already running"));
}
if (state === 'halting') {
return callback(new Error("node " + node_name + " is halting"));
}
return callback(null);
});
};
run_instance = function(callback) {
var options;
options = _this.extract_instance_options(node_name);
options.MinCount = 1;
options.MaxCount = 1;
logsmith.debug('run instances with options', options);
return client.runInstances(options, function(err, result) {
var instance, instances, selected_instance, _i, _j, _len, _len1, _ref;
if (err) {
return callback(_this.create_error(err, node_name));
}
instances = [];
_ref = result.Instances;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
instance = _ref[_i];
instances.push({
id: instance.InstanceId
});
}
if (instances.length === 0) {
return callback(new Error("no instances run for node " + node_name));
}
logsmith.debug('ran instances', instances);
selected_instance = instances[instances.length - 1];
if (!selected_instance) {
return callback(new Error("could not create instance for node " + node_name));
}
logsmith.debug('selected instance', selected_instance);
for (_j = 0, _len1 = instances.length; _j < _len1; _j++) {
instance = instances[_j];
if (selected_instance !== instance) {
logsmith.warn("duplicate node " + node_name + " with instance id " + instance_id + " detected");
}
}
return callback(null, selected_instance.id);
});
};
map_tags = function(instance_id, callback) {
var options;
options = {
Resources: [instance_id],
Tags: [
{
Key: 'vortex-node-name',
Value: node_name
}, {
Key: 'vortex-node-namespace',
Value: _this.extract_namespace(node_name)
}
]
};
logsmith.debug('create tags with options', options);
return client.createTags(options, function(err, result) {
if (err) {
return callback(_this.create_error(err, node_name));
}
return callback(null, instance_id);
});
};
return async.waterfall([verify_status, run_instance, map_tags], function(err) {
if (err) {
return callback(err);
}
return _this.status(node_name, callback);
});
};
_Class.prototype.halt = function(node_name, callback) {
/*
Provider-specific method for halting a node.
*/
var client, e, terminate_instance, unmap_tags, verify_status,
_this = this;
try {
client = this.get_client(node_name);
} catch (_error) {
e = _error;
return callback(this.create_error(e, node_name));
}
verify_status = function(callback) {
return _this.status(node_name, function(err, state, address, instance_id) {
if (err) {
return callback(err);
}
if (state === 'halting') {
return callback(new Error("" + node_name + " is already halting"));
}
if (state === 'stopped') {
return callback(new Error("" + node_name + " is already stopped"));
}
return callback(null, instance_id);
});
};
terminate_instance = function(instance_id, callback) {
var options;
options = {
InstanceIds: [instance_id]
};
logsmith.debug('terminate instances with options', options);
return client.terminateInstances(options, function(err, result) {
if (err) {
return callback(_this.create_error(err, node_name));
}
return callback(null, instance_id);
});
};
unmap_tags = function(instance_id, callback) {
var options;
options = {
Resources: [instance_id],
Tags: [
{
Key: 'vortex-node-name',
Value: node_name
}, {
Key: 'vortex-node-namespace',
Value: _this.extract_namespace(node_name)
}
]
};
logsmith.debug('delete tags with options', options);
return client.deleteTags(options, function(err, result) {
if (err) {
return callback(_this.create_error(err, node_name));
}
return callback(null, instance_id);
});
};
return async.waterfall([verify_status, terminate_instance, unmap_tags], function(err) {
if (err) {
return callback(err);
}
return _this.status(node_name, callback);
});
};
_Class.prototype.pause = function(node_name, callback) {
/*
Provider-specific method for pausing a machine.
*/
return callback(new Error("cannot pause node " + node_name + " due to pause not implemented"));
};
_Class.prototype.resume = function(node_name, callback) {
/*
Provider-specific method for resuming a machine.
*/
return callback(new Error("cannot resume node " + node_name + " due to resume not implemented"));
};
_Class.prototype.shell_spec = function(node_name, callback) {
/*
Provider-specific method for obtaining a shell spec from a node.
*/
var build_spec, ensure_port, obtain_status, passphrase, password, private_key, ssh_port, username,
_this = this;
password = this.extract_password(node_name);
private_key = this.extract_private_key(node_name);
if (!password && !private_key) {
return callback(new Error("no password or privateKey provided for node " + node_name));
}
ssh_port = this.extract_ssh_port(node_name);
if (ssh_port) {
ssh_port = parseInt(ssh_port, 10);
if (isNaN(ssh_port || ssh_port < 1)) {
return callback(new Error("ssh port for node " + node_name + " is incorrect"));
}
} else {
ssh_port = 22;
}
username = this.extract_username(node_name);
if (!username) {
username = 'vortex';
}
passphrase = this.extract_passphrase(node_name);
obtain_status = function(callback) {
return _this.status(node_name, function(err, state, address) {
if (err) {
return callback(err);
}
if (state === 'halting') {
return callback(new Error("node " + node_name + " is halting"));
}
if (state === 'stopped') {
return callback(new Error("node " + node_name + " is stopped"));
}
if (!address) {
return callback(new Error("cannot find network address for node " + node_name));
}
return callback(null, address);
});
};
ensure_port = function(address, callback) {
return portchecker.isOpen(ssh_port, address, function(is_open) {
var callee, milliseconds, timeout;
if (is_open) {
return callback(null, address);
}
callee = arguments.callee;
milliseconds = 10000;
timeout = function() {
return portchecker.isOpen(ssh_port, address, callee);
};
logsmith.debug("repeat check for ssh port open for node " + node_name + " in " + milliseconds + " milliseconds");
return setTimeout(timeout, milliseconds);
});
};
build_spec = function(address, callback) {
var parts, spec, spec_options;
parts = [];
parts.push('ssh://');
parts.push(encodeURIComponent(username));
if (password) {
parts.push(':' + encodeURIComponent(password));
}
parts.push('@');
parts.push(address);
parts.push(':' + ssh_port);
if (private_key) {
parts.push(';privateKey=' + encodeURIComponent(private_key));
}
if (passphrase) {
parts.push(';passphrase=' + encodeURIComponent(passphrase));
}
spec = parts.join('');
spec_options = {
username: username,
password: password,
host: address,
port: ssh_port,
privateKey: private_key,
passphrase: passphrase
};
return callback(null, spec, spec_options);
};
return async.waterfall([obtain_status, ensure_port, build_spec], callback);
};
return _Class;
})();
}).call(this);