cloud-blender
Version:
A high level library for cloud compute operations
798 lines (657 loc) • 29.2 kB
JavaScript
var request = require('request'),
underscore = require('underscore');
// This module implements some of the hp cloud compute functionality.
// The rest api is taken from hp cloud documentation (http://hpcloud.com)
// (some of the api is broken - like in createNodes)
// The function names are inspired by libcloud with minor differences.
module.exports = (function() {
// internal members
// ----------------
var proxyURL;
// internal functions
// ------------------
// The connect method is being called in the beginning
// of each high level function (listNodes, createNodes etc.)
// some of these high level function calls to other high level functions
// (polling for example). In order to prevent many unnecessary rest
// calls, we save the identityToken in the identitySettings
// and we check the existence and expiration date of the token.
//
// The best practice is that the caller of this function
// will also save this token in the session (so we can save rest calls)
// parameters:
// -----------
// identitySettings should contain auth object, identity url and tenantid
function connect(identitySettings, callback) {
// the threshold is 1 hour to be on the safe side
// (the token expires every 12hours)
var DIFF_THRESH_HOURS = 1,
expires = '',
hourDiff = 0,
requestSettings;
if ('identityToken' in identitySettings &&
'token' in identitySettings.identityToken &&
'accessToken' in identitySettings.identityToken &&
'expires_at' in identitySettings.identityToken.token) {
expires = identitySettings.identityToken.token.expires_at;
hourDiff = (new Date(expires).getTime() - new Date().getTime()) / 1000 / 60 / 60;
if (hourDiff > DIFF_THRESH_HOURS) {
callback(null, identitySettings.identityToken);
return;
}
}
requestSettings = {
method: 'POST',
url: identitySettings.url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({auth: identitySettings.auth}),
proxy: proxyURL
};
request(requestSettings, function(error, response, bodyString) {
var identityToken,
normalResponseCode = '201';
if (error !== null || (typeof (bodyString) !== 'string') || !response ||
(response.statusCode + '' !== normalResponseCode) ||
(!response.headers['x-subject-token'])) {
callback(new Error('cannot retrieve token from hp cloud. reason: ' +
(response?response.statusCode:' empty response - probably bad tunneling proxy')));
return;
}
identityToken = JSON.parse(bodyString);
// the reason to create this hierarchy is just backward compatibility laziness
identityToken.accessToken = response.headers['x-subject-token'];
identitySettings.identityToken = identityToken;
callback(null, identityToken);
});
}
// called before floating ip binding - and assumes only one network is in use...
function createSimpleNodeData(rawNode) {
var node = {
id: rawNode.id,
status: rawNode.status,
tags: rawNode.metadata,
addresses: [undefined, undefined]
};
underscore.each(rawNode.addresses, function(network) {
if (network[0]) {
node.addresses[0] = network[0].addr;
}
if (network[1]) {
node.addresses[1] = network[1].addr;
}
});
return node;
}
function pollForStatusIp(settings, id, pollingCount, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
var statusSettings;
if (errorConnect) {
callback(errorConnect, {});
return;
}
statusSettings = {
method: 'GET',
url: settings.regionContext.computeSettings.url + '/servers/' + id,
headers: {
'X-Auth-Token': identityToken.accessToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
proxy: proxyURL
};
request(statusSettings, function(error, response, bodyString) {
var normalResponseCode = '200',
finalResult = {},
finalError;
try{
finalResult.rawResult = JSON.parse(bodyString);
}
catch(errorParse) {
}
if (error !== null || (!finalResult.rawResult) || !response ||
(response.statusCode + '' !== normalResponseCode)) {
finalError = new Error('problem in finding jobStatus, error: ' + error +
', responseCode: ' + (response?response.statusCode:'undefined'));
}
if (pollingCount === 1 && finalError) {
callback(finalError, {});
return;
}
// a special case of hpcs bug - in this case we must fail the machine
if (((!finalError) && finalResult.rawResult) &&
(finalResult.rawResult.server.status === 'SHUTOFF' ||
finalResult.rawResult.server.status.indexOf('ERROR') === 0)) {
callback(new Error('found a machine with shutoff status - hpcs cloud bug detected'));
return;
}
// statuses taken from:
// http://docs.hpcloud.com/api/v13/compute/ (look for NETWORKING string)
if ((!finalError) && finalResult.rawResult && (finalResult.rawResult.server.status === 'ACTIVE' ||
finalResult.rawResult.server.status === 'BUILD(block_device_mapping)' ||
finalResult.rawResult.server.status === 'BUILD(spawning)')) {
callback(undefined, finalResult);
return;
}
setTimeout(pollForStatusIp, 10000, settings, id, pollingCount - 1, callback);
});
});
}
function allocateAndBindIpAddress(settings, nodeId, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
var allocateIpSettings;
if (errorConnect) {
callback(errorConnect, {});
return;
}
allocateIpSettings = {
method: 'POST',
url: settings.regionContext.computeSettings.url + '/os-floating-ips',
headers: {
'X-Auth-Token': identityToken.accessToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
proxy: proxyURL
};
request(allocateIpSettings, function(error, response, bodyString) {
var normalResponseCode = '200',
finalResult = {},
addFloatingIpSettings;
try{
finalResult.rawResult = JSON.parse(bodyString);
}
catch(errorParse){
}
if (error === null && (finalResult.rawResult) && response &&
(response.statusCode + '' === normalResponseCode)) {
finalResult.ipAddress = finalResult.rawResult.floating_ip.ip;
addFloatingIpSettings = {
method: 'POST',
url: settings.regionContext.computeSettings.url + '/servers/' + nodeId +'/action',
headers: {
'X-Auth-Token': identityToken.accessToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
proxy: proxyURL,
body: JSON.stringify({addFloatingIp: {
address: finalResult.ipAddress
}})
};
request(addFloatingIpSettings, function(error, response, bodyString) {
var normalResponseCode = '202',
finalError;
if (error || !response || (response.statusCode + '' !== normalResponseCode)) {
finalError = new Error('can not add ip address ' + (response?response.statusCode:'undefined') + ' ' +
error);
}
callback(finalError, finalResult);
});
} else {
callback(new Error('can not allocate a floating ip'));
return;
}
});
});
}
function deallocateFloatingIpOfNode(settings, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
var keys = [],
ipsCounter = 0,
finalError;
function releaseIpCB(error, response, bodyString) {
var normalResponseCode = '202';
ipsCounter++;
if (error || !response || (response.statusCode + '' !== normalResponseCode)) {
finalError = new Error('can not release ip adress ' + (response?response.statusCode:'undefined') + ' ' +
error);
}
if (ipsCounter === keys.length) {
callback(finalError);
}
}
if (errorConnect) {
callback(errorConnect, {});
return;
}
if (settings.node && settings.node.releaseInfo && settings.node.releaseInfo.addresses) {
keys = underscore.keys(settings.node.releaseInfo.addresses);
}
if (keys.length === 0) {
callback(undefined);
return;
}
underscore.each(keys, function(key) {
var releaseIpSettings = {
method: 'DELETE',
url: settings.regionContext.computeSettings.url + '/os-floating-ips/' + settings.node.releaseInfo.addresses[key],
headers: {
'X-Auth-Token': identityToken.accessToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
proxy: proxyURL
};
request(releaseIpSettings, releaseIpCB);
});
});
}
// creates a map of ip address to ip id for making things efficient
function listIps(settings, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
var listIpsSettings;
if (errorConnect) {
callback(errorConnect, {});
return;
}
listIpsSettings = {
method: 'GET',
url: settings.regionContext.computeSettings.url + '/os-floating-ips',
headers: {
'X-Auth-Token': identityToken.accessToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
proxy: proxyURL
};
request(listIpsSettings, function(error, response, bodyString) {
var normalResponseCode = '200',
finalError,
rawResult,
ipsHash = {};
try{
rawResult = JSON.parse(bodyString);
}
catch(errorParse) {
}
if (error || !response || (response.statusCode + '' !== normalResponseCode) || (!rawResult)) {
finalError = new Error('can not find floating ips ' + (response?response.statusCode:'undefined') + ' ' +
error);
}
if (rawResult && rawResult.floating_ips) {
underscore.each(rawResult.floating_ips, function(floating) {
ipsHash[floating.ip] = floating.id;
});
}
callback(finalError, ipsHash);
});
});
}
var that = {
setProxy: function(proxyUrl) {
proxyURL = proxyUrl;
},
createPreparation: function (settings, callback) {
callback(null,null);
},
createRegionContext: function(authSettings, limits) {
return {
identitySettings: {
auth: {
identity: {
methods: ['accessKey'],
accessKey: {
"accessKey": authSettings.accessKey,
"secretKey": authSettings.secretKey
}
},
scope: {
project: {
id: authSettings.tenantId
}
}
},
url: 'https://' + authSettings.region + '.identity.hpcloudsvc.com:35357/v3/auth/tokens'
},
computeSettings: {
url: 'https://' +
authSettings.region + '.compute.hpcloudsvc.com/v2/' +
authSettings.tenantId
},
limits: limits,
providerName: 'hpcs_13_5',
pollingCount: 1
};
},
createNode: function(settings, cloudServicesTestSettings, nodeIndex, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
var createNodeSettings,
nodeParams = {
name: new Date().valueOf() + '-createdByStorm',
imageRef: settings.nodeParams.imageId,
flavorRef: settings.nodeParams.instanceType,
metadata: settings.nodeParams.tags,
key_name: settings.nodeParams.keyName,
availability_zone: 'az2'
},
securityGroups = settings.nodeParams.securityGroups,
userData = settings.nodeParams.userData;
if (errorConnect) {
callback(errorConnect, {});
return;
}
// adding securityGroups
if (securityGroups) {
nodeParams.security_groups = [];
underscore.each(securityGroups, function(securityGroup) {
nodeParams.security_groups.push({"name": securityGroup});
});
}
// adding user data
if (userData) {
nodeParams.user_data = new Buffer(JSON.stringify(userData)).toString('base64');
}
// vendor specific extension must be last
underscore.extend(nodeParams, settings.nodeParams.vendorSpecificParams);
createNodeSettings = {
method: 'POST',
url: settings.regionContext.computeSettings.url + '/servers',
headers: {
'X-Auth-Token': identityToken.accessToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
proxy: proxyURL,
body: JSON.stringify({server: nodeParams})
};
request(createNodeSettings, function(error, response, bodyString) {
var nodeData,
normalResponseCode = '202',
finalResult = {
rawResult: {},
node: {
tags: {logicName: settings.nodeParams.logicName}
}
},
errorCreate, nodeId, bodyJson;
try{
bodyJson = JSON.parse(bodyString);
}catch(errorParse) {
}
if (error !== null || !response || (response.statusCode + '' !== normalResponseCode) ||
!bodyJson) {
finalResult.node.status = 'ERROR_ALLOCATION';
errorCreate = new Error('can not createNode with parameters: ' + JSON.stringify(settings.nodeParams) +
'. statusCode: ' + (response?response.statusCode:'undefined') +
' ,body string' + bodyString,
'error: ' + error);
callback(errorCreate, finalResult);
return;
}
nodeId = bodyJson.server.id;
setTimeout(pollForStatusIp, 10000, settings, nodeId, 20, function(errorStatus, resultsNode) {
if (errorStatus) {
finalResult.node.status = 'ERROR_STATUS_POLLING';
callback(errorStatus, finalResult);
return;
}
finalResult.rawResult = resultsNode.rawResult;
//console.log(JSON.stringify(finalResult.rawResult, null, ' '));
finalResult.node = createSimpleNodeData(resultsNode.rawResult.server);
allocateAndBindIpAddress(settings, finalResult.rawResult.server.id, function(errorIp, ipResult) {
if (errorIp) {
finalResult.node.status = 'ERROR_IP';
errorCreate = new Error('can not allocate ip address with parameters: ' + JSON.stringify(settings.nodeParams) +
errorIp);
callback(errorIp, finalResult);
return;
}
finalResult.node.addresses[1] = ipResult.ipAddress;
finalResult.node.releaseInfo = {addresses: {}};
finalResult.node.releaseInfo.addresses[ipResult.ipAddress] =
ipResult.rawResult.floating_ip.id;
callback(undefined, finalResult);
});
});
});
});
},
listNodes: function(settings, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
if (errorConnect) {
callback(errorConnect, {});
return;
}
listIps(settings, function(errorConnect, ipsHash) {
var listRequestSettings;
listRequestSettings = {
method: 'GET',
url: settings.regionContext.computeSettings.url + '/servers/detail',
headers: {
'X-Auth-Token': identityToken.accessToken,
'Accept': 'application/json'
},
proxy: proxyURL
};
request(listRequestSettings, function(error, response, bodyString) {
var finalResults = {
nodes: []
},
normalResponseCode = '200',
errorList;
try {
finalResults.rawResult = JSON.parse(bodyString);
} catch (errorParse) {}
if (error === null && finalResults.rawResult && response &&
(response.statusCode + '' === normalResponseCode)) {
underscore.each(finalResults.rawResult.servers, function(server) {
var node = createSimpleNodeData(server);
node.releaseInfo = {addresses: {}};
node.releaseInfo.addresses[node.addresses[1]] =
ipsHash[node.addresses[1]];
finalResults.nodes.push(node);
});
} else {
errorList = new Error('cannot retrieve node machines list from hp cloud. reason: ' +
'. statusCode: ' + (response ? response.statusCode : 'undefined'));
}
callback(errorList, finalResults);
});
});
});
},
deleteNode: function(settings, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
if (errorConnect) {
callback(errorConnect, {});
return;
}
// we should ignore the errorDeleteIp
deallocateFloatingIpOfNode(settings, function(errorDeleteIp, result) {
var deleteRequestSettings;
deleteRequestSettings = {
method: 'DELETE',
headers: {
'X-Auth-Token': identityToken.accessToken,
'Accept': 'application/json'
},
url: settings.regionContext.computeSettings.url + '/servers/' + settings.node.id,
proxy: proxyURL
};
request(deleteRequestSettings, function(error, response, bodyString) {
var normalResponseCode = '204',
finalResult = {rawResult: undefined},
errorDelete;
if (error === null && response && (response.statusCode + '' === normalResponseCode)) {
finalResult.result = 'SUCCESS';
}
else {
finalResult.result = 'ERROR';
errorDelete = new Error('deleteNode failed for id: ' + JSON.stringify(settings.nodeParams) +
'. statusCode: ' + (response?response.statusCode:'undefined') +
', request settings: ' + JSON.stringify(deleteRequestSettings));
}
callback(errorDelete, finalResult);
});
});
});
},
createImage: function(settings, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
var createImageRequestSettings,
imageParams = {
name: new Date().valueOf() + '-createdByStorm',
serverId: settings.imageParams.nodeId,
metadata: settings.imageParams.tags
};
if (errorConnect) {
callback(errorConnect, {});
return;
}
underscore.extend(imageParams, settings.imageParams.vendorSpecificParams);
createImageRequestSettings = {
method: 'POST',
url: settings.regionContext.computeSettings.url + '/servers/' + settings.imageParams.nodeId + '/action',
headers: {
'X-Auth-Token': identityToken.accessToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
proxy: proxyURL,
body: JSON.stringify({createImage: imageParams})
};
request(createImageRequestSettings, function(errorRequest, response, bodyString) {
var normalResponseCode = '202',
location = '',
imageId = '',
finalResult = {},
errorCreate;
if (errorRequest !== null || !response ||
(response.statusCode + '' !== normalResponseCode)) {
errorCreate = new Error('cannot createImage with params: '+
JSON.stringify(settings.imageParams) +
', error: ' + JSON.stringify(errorRequest) +
'. statusCode: ' + (response?response.statusCode:'undefined'));
}
else {
location = response.headers.location;
imageId = location.slice(location.lastIndexOf('/') + 1);
finalResult.rawResult = location;
finalResult.imageId = imageId;
}
callback(errorCreate, finalResult);
});
});
},
listImages: function(settings, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
var requestSettings;
if (errorConnect) {
callback(errorConnect, {});
return;
}
requestSettings = {
method: 'GET',
url: settings.regionContext.computeSettings.url + '/images/detail',
headers: {
'X-Auth-Token': identityToken.accessToken,
'Accept': 'application/json'
},
proxy: proxyURL
};
request(requestSettings, function(error, response, bodyString) {
var normalResponseCode = '200',
errorListImage,
finalResult = {};
try {
finalResult.rawResult = JSON.parse(bodyString);
}
catch (errorParse) {
}
if (error !== null || (typeof (bodyString) !== 'string') || !response ||
(response.statusCode + '' !== normalResponseCode)) {
errorListImage = new Error('cannot retrieve images list from hp cloud. ' +
'. statusCode: ' + (response?response.statusCode:'undefined'));
}
else {
finalResult.images = [];
underscore.each(finalResult.rawResult.images, function(rawImage) {
var image = underscore.pick(rawImage, 'id', 'status', 'name');
image.tags = rawImage.metadata;
finalResult.images.push(image);
});
}
callback(errorListImage, finalResult);
});
});
},
deleteImage: function(settings, callback) {
connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
var requestSettings;
if (errorConnect) {
callback(errorConnect, {});
return;
}
requestSettings = {
url: settings.regionContext.computeSettings.url + '/images/' + settings.imageParams.imageId,
method: 'DELETE',
headers: {
'X-Auth-Token': identityToken.accessToken
},
proxy: proxyURL
};
request(requestSettings, function(error, response, bodyString) {
var normalResponseCode = '204',
errorDeleteImage,
finalResult = {rawResult: undefined};
if (error !== null || !response ||
(normalResponseCode !== response.statusCode + '')) {
errorDeleteImage = new Error('cannot deleteImage, error: ' + error + ', code: ' +
(response?response.statusCode:'undefined'));
finalResult.result = 'ERROR';
}
else {
finalResult.result = 'SUCCESS';
}
callback(errorDeleteImage, finalResult);
});
});
},
associateAddress: function(settings, callback){
var error = new Error('no implementation');
callback(error, null);
},
disassociateAddress: function(settings, callback){
var error = new Error('no implementation');
callback(error, null);
}
// getLimits: function(settings, callback) {
// connect(settings.regionContext.identitySettings, function(errorConnect, identityToken) {
// var requestSettings;
// if (errorConnect) {
// callback(errorConnect, {});
// return;
// }
// requestSettings = {
// method: 'GET',
// url: settings.regionContext.computeSettings.url + '/limits',
// headers: {
// 'X-Auth-Token': identityToken.accessToken,
// 'Accept': 'application/json'
// },
// proxy: proxyURL
// };
// request(requestSettings, function(error, response, bodyString) {
// var imagesData,
// normalResponseCode = '200',
// limits,
// finalError;
// if (error !== null || (typeof (bodyString) !== 'string') ||
// (normalResponseCode !== response.statusCode + '')
// ) {
// finalError = new Error('cannot retrieve images list from hp cloud. ' +
// '. statusCode: ' + (response?response.statusCode:'undefined'));
// }
// else {
// limits = JSON.parse(bodyString);
// }
// callback(finalError, limits);
// });
// });
// }
};
return that;
})();