UNPKG

cloud-blender

Version:

A high level library for cloud compute operations

696 lines (605 loc) 27.2 kB
var request = require('request'), underscore = require('underscore'), crypto = require('crypto'), errorHandler = require('./error_handler.js'), parseString = require('xml2js').parseString, API_VERSION = '2013-02-01'; module.exports = (function() { var regionAzs = { }; // this function build the regionAzs data structure. // if it cant find a record - it queries the cloud // if it fails - it stops querying the cloud // This must be called dynamically the first time the lib is being loaded // since ec2 has a lot of mess with AZs function getAvailabiltyZone(settings, callback) { var region = settings.regionContext.computeSettings.region, regionAz = regionAzs[region], zones, index, selectedRegion; if (region === 'sa-east-1') { callback(new Error('Sau Paulo region has bug in az-c - disabling az usage for it')); return; } if (!regionAz) { that.describeAZs(settings, function(error, azs) { if (error) { regionAzs[region] = {counter: -1};// indicating error happened and not to try again callback(error); return; } regionAzs[region] = { counter: 1, // 1 is for incrementing the counter since we use 0 in this call zones: azs }; callback(undefined, azs[0]); }); return; } zones = regionAz.zones; index = regionAz.counter; if (index === -1) { // we failed in a previous call callback(new Error ('failed to find azs')); return; } selectedRegion = zones[index%(zones.length)]; regionAz.counter ++; callback(undefined, selectedRegion); } // internal members // ---------------- var tunnelingProxyURL; // internal functions // ------------------ function uriEscape(string) { var output = encodeURIComponent(string); output = output.replace(/[^A-Za-z0-9_.~\-%]+/g, escape); output = output.replace(/[*]/g, function(ch) { return '%' + ch.charCodeAt(0).toString(16).toUpperCase(); }); return output; } function createURIString(params) { var sortedKeysArr, i, length, uriString = ''; sortedKeysArr = underscore.keys(params).sort(); for (i = 0, length = sortedKeysArr.length; i < length; i++) { uriString += uriEscape(sortedKeysArr[i]) + '=' + uriEscape(params[sortedKeysArr[i]]); if (i !== length -1) { uriString += '&'; } } return uriString; } function hash(data) { return crypto.createHash('sha256').update(new Buffer(data)).digest('hex'); } function sign(key, data, digest) { return crypto.createHmac('sha256', key).update(data).digest(digest); } function createNowDate() { return (new Date()).toISOString().replace(/[:\-]|\.\d{3}/g, ''); } function localCloudRequest(settings, callback) { var body, requestSettings, uriStr, nowDate = createNowDate(); settings.params.Action = settings.action; settings.params.Version = API_VERSION; uriStr = createURIString(settings.params); //console.log('uri str is ' + uriStr); if (settings.method === 'POST') { //body = uriStr; body = JSON.stringify(settings.params); } requestSettings = { method: settings.method, url: (process.env.STORM_LOCALCLOUD_HOST || 'http://localhost:3001') + settings.path + ((settings.method === 'POST')?'':('?' + uriStr)),// TODO: make it configurable - http://localhost:3001 headers: { /*'Authorization': authHeader,*/ //'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Content-Type': 'application/json', /*'Host': 'ec2.' + settings.region + '.amazonaws.com',*/ 'x-amz-date': nowDate }, body: body, timeout: 120000 }; // if (tunnelingProxyURL !== undefined) { // requestSettings.proxy = tunnelingProxyURL; // } console.log('******* '+JSON.stringify(requestSettings)); request(requestSettings, function (error, response, bodyString) { var errorRequest, result; if (!error && bodyString && response.statusCode < 300) { // parseString(bodyString, function (errorParsing, jsonObj) { // // if (errorParsing) { // errorRequest = new Error('bad parsing to XML of: ' + bodyString + // ', parsing error is: ' + errorParsing.message); // } // else { // result = jsonObj; // } // callback(errorRequest, result); // }); callback(errorRequest, JSON.parse(bodyString)); } else { errorRequest = new Error('problem in request: ' + JSON.stringify(requestSettings) + ', description: ' + bodyString + (response ? response.statusCode : 'request failed') + ', requestError: ' + error); callback(errorRequest, result); } }); } function createSimpleNodeData(rawNode) { var node = { id: rawNode.instanceId[0], status: rawNode.instanceState[0].name[0].toUpperCase(), // libCloud addresses: [null, null], releaseInfo: {} }; if (node.status === 'RUNNING') { node.status = 'ACTIVE';// make it aligned with libCloud } if (rawNode.privateIpAddress) { node.addresses[0] = (rawNode.privateIpAddress[0]); } if (rawNode.ipAddress) { node.addresses[1] = (rawNode.ipAddress[0]); } if (rawNode.tagSet) { node.tags = {}; underscore.each(rawNode.tagSet, function(tagSet) { underscore.each(tagSet.item, function(item) { node.tags[item.key[0]] = item.value[0]; }); }); } return node; } function createTagsPolling(settings, nodeId, pollingCount, interval, callback) { var tagsSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'POST', params: {'ResourceId.1': nodeId}, action: 'CreateTags' }, tagCount = 0; underscore.each(settings.nodeParams.tags, function(value, key) { tagsSettings.params['Tag.' + tagCount + '.Key'] = key; tagsSettings.params['Tag.' + tagCount + '.Value'] = value; tagCount++; }); localCloudRequest(tagsSettings, function(errorTags, result) { if (errorTags && pollingCount > 1){ // console.log('tags creation failed, polling count: ' + pollingCount + ' ' +errorTags); setTimeout(createTagsPolling, interval, settings, nodeId, pollingCount - 1, interval, callback); } else { callback(errorTags, result); } }); } // the exported functions var that = { setProxy: function (proxyUrl) { tunnelingProxyURL = proxyUrl; }, createPreparation: function (settings, callback) { callback(null,null); }, describeAZs: function(settings, callback) { var generateSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'GET', params: {}, action: 'DescribeAvailabilityZones' }; localCloudRequest(generateSettings, function(error, result) { var regionAZs = []; if (error) { callback(error); return; } underscore.each(result.DescribeAvailabilityZonesResponse.availabilityZoneInfo[0].item, function(zone) { if (zone.zoneState[0] === 'available') { regionAZs.push(zone.zoneName[0]); } }); callback(error, regionAZs); }); }, createRegionContext: function(regionSettings, limits) { return { identitySettings: { credentials: { accessKeyId: regionSettings.accessKey, secretAccessKey: regionSettings.secretKey } }, computeSettings: { region: regionSettings.region }, limits: limits, providerName: 'onprem', pollingCount: 180 }; }, listNodes: function(settings, callback) { var generateSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'GET', params: {}, action: 'DescribeInstances' }; localCloudRequest(generateSettings, function(error, result) { var finalResults = { nodes: [], rawResult: result }; if (result) { //console.log(JSON.stringify(result, null, ' ')); underscore.each(result.DescribeInstancesResponse.reservationSet, function(group) { underscore.each(group.item, function(item){ underscore.each(item.instancesSet, function(instanceSet) { underscore.each(instanceSet.item, function(item) { var node = createSimpleNodeData(item); // terminated machines can confuse us // other cloud vendors may not return them so // we skip them - they can be obtained using rawNodes if (node.status !== 'TERMINATED'){ finalResults.nodes.push(node); } }); }); }); }); } callback(error, finalResults); }); }, //listNodes createNode: function(settings, cloudServicesTestSettings, nodeIndex, callback) { getAvailabiltyZone(settings, function(errorAZ, az) { var generateSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'POST', params: { ImageId: settings.nodeParams.imageId, InstanceType: settings.nodeParams.instanceType, KeyName: settings.nodeParams.keyName, MinCount: 1, MaxCount: 1, 'BlockDeviceMapping.1.DeviceName': '/dev/sdb', 'BlockDeviceMapping.1.VirtualName': 'ephemeral0' }, action: 'RunInstances' }, finalResult = { }, i, length, securityGroups = settings.nodeParams.securityGroups, userData = settings.nodeParams.userData; // adding securityGroups if (securityGroups) { for (i = 0, length = securityGroups.length; i < length; i++) { generateSettings.params['SecurityGroup.' + i] = securityGroups[i]; } } // adding user data if (userData) { generateSettings.params.UserData = new Buffer(JSON.stringify(userData)).toString('base64'); } if (az) { generateSettings.params['Placement.AvailabilityZone'] = az; } // adding (and possibly overriding) vendor specific params underscore.extend(generateSettings.params, settings.nodeParams.vendorSpecificParams); localCloudRequest(generateSettings, function(error, result) { // we add tags synthetically since adding tags is not supported // in the RunInstance command like in hpcloud var node = { tags: settings.nodeParams.tags }, rawNode; if (error) { node.status = 'ERROR'; } else { rawNode = result.RunInstancesResponse.instancesSet[0].item[0]; node = underscore.extend(node,createSimpleNodeData(rawNode)); } finalResult.rawResult = result; finalResult.node = node; if (!error && settings.nodeParams.tags) { createTagsPolling(settings, node.id, 3, 10000, function(errorTags, result) { if (errorTags) { node.status = 'ERROR_TAGS'; } callback(errorTags, finalResult); }); } else { callback(error, finalResult); } }); }); },//createNode deleteNode: function(settings, callback) { var deleteSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'POST', params: {'InstanceId.1': settings.node.id}, action: 'TerminateInstances' }, finalResult = {}; localCloudRequest(deleteSettings, function(error, result) { var confirmationString = 'SUCCESS'; if (error) { confirmationString = 'ERROR'; } finalResult.rawResult = result; finalResult.result = confirmationString; callback(error, finalResult); }); },// delete node createImage: function(settings, callback) { var createImageSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'POST', params: { InstanceId: settings.imageParams.nodeId, Name: new Date().valueOf() + '-createdByStorm', 'BlockDeviceMapping.1.DeviceName': '/dev/sda1', 'BlockDeviceMapping.1.Ebs.VolumeType': 'gp2' //'BlockDeviceMapping.1.Ebs.VolumeType': 'io1', //'BlockDeviceMapping.1.Ebs.Iops': 300, //'BlockDeviceMapping.1.Ebs.VolumeSize': 10 }, action: 'CreateImage' }, finalResult = {}; if (settings.imageParams.vendorSpecificParams) { underscore.extend(createImageSettings.params, settings.imageParams.vendorSpecificParams); } localCloudRequest(createImageSettings, function(error, result) { var tagCount = 0, tagsSettings; finalResult.rawResult = result; if (result) { finalResult.imageId = result.CreateImageResponse.imageId[0]; } if (!error && settings.imageParams.tags) { tagsSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'POST', params: {'ResourceId.1': finalResult.imageId}, action: 'CreateTags' }; underscore.each(settings.imageParams.tags, function(value, key) { tagsSettings.params['Tag.' + tagCount + '.Key'] = key; tagsSettings.params['Tag.' + tagCount + '.Value'] = value; tagCount++; }); localCloudRequest(tagsSettings, function(errorTags, result) { callback(errorTags, finalResult); }); } else { callback(error, finalResult); } }); }, listImages: function(settings, callback) { var listSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'GET', params: { 'Owner.1': 'self' }, action: 'DescribeImages' }; if (settings.vendorSpecificParams) { underscore.extend(listSettings.params, settings.vendorSpecificParams); } localCloudRequest(listSettings, function(error, result) { var finalResults = { images: [], rawResult: result }; if (result) { finalResults.images = []; underscore.each(result.DescribeImagesResponse.imagesSet, function(imageSet) { underscore.each(imageSet.item, function(item) { var image = underscore.pick(item, 'imageId', 'name', 'imageState', 'tagSet'), tags = {}; underscore.each(image.tagSet, function(tagSet) { underscore.each(tagSet.item, function(tagItem) { tags[tagItem.key[0]] = tagItem.value[0]; }); }); finalResults.images.push({ id: image.imageId[0], status: (image.imageState[0]==='available')?'ACTIVE':image.imageState[0].toUpperCase(), name: image.name[0], tags: tags }); }); }); } callback(error, finalResults); }); }, deleteSnapshot: function(settings, callback) { var deleteSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'POST', params: {'SnapshotId': settings.snapshotId}, action: 'DeleteSnapshot' }, finalResult = {}; localCloudRequest(deleteSettings, function(error, result) { var confirmationString = 'SUCCESS'; if (error) { confirmationString = 'ERROR'; } finalResult.rawResult = result; finalResult.result = confirmationString; callback(error, finalResult); }); }, deregisterImage: function(settings, callback) { var deregisterSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'POST', params: {'ImageId': settings.imageParams.imageId}, action: 'DeregisterImage' }, finalResult = {}; localCloudRequest(deregisterSettings, function(error, result) { var confirmationString = 'SUCCESS'; if (error) { confirmationString = 'ERROR'; } finalResult.rawResult = result; finalResult.result = confirmationString; callback(error, finalResult); }); }, deleteImage: function(settings, callback) { var errorDeleteImage, finalResult = { rawResult: [] }, listSettings = { regionContext: settings.regionContext, vendorSpecificParams: { 'ImageId.1': settings.imageParams.imageId } }, snapshotsToDelete = [], i, length, snapshotCounter = 0; function deleteSnapshotCB(errorDeleteSnapshot, resultDeleteSnapshot) { snapshotCounter++; if (errorDeleteSnapshot) { errorDeleteImage = errorHandler.concatError(errorDeleteImage, errorDeleteSnapshot); finalResult.result = 'ERROR'; } finalResult.rawResult.push(resultDeleteSnapshot.rawResult); if (snapshotCounter === length) { callback(errorDeleteImage, finalResult); } } that.listImages(listSettings, function(errorList, resultList) { if (errorList || ( resultList.images[0] && resultList.images[0].status !== 'ACTIVE')) { finalResult.result = 'ERROR'; callback(new Error('problem in images state: ' + JSON.stringify(resultList.images[0]) + ', error: '+ errorList), finalResult); return; } // @@@@@@@ should make sure all these loops are necessary underscore.each(resultList.rawResult.DescribeImagesResponse.imagesSet, function(imageSet) { underscore.each(imageSet.item, function(item) { underscore.each(item.blockDeviceMapping, function(block) { underscore.each(block.item, function(blockItem) { underscore.each(blockItem.ebs, function(ebsItem) { snapshotsToDelete.push(ebsItem.snapshotId[0]); }); }); }); }); }); that.deregisterImage(settings, function(errorDeregister, resultDeregister) { finalResult.result = 'SUCCESS'; finalResult.rawResult.push(resultDeregister.rawResult); if (errorDeregister) { errorDeleteImage = errorHandler.concatError(errorDeleteImage, errorDeregister); finalResult.result = 'ERROR'; callback(errorDeleteImage, finalResult); return; } for (i = 0, length = snapshotsToDelete.length; i < length; i++) { that.deleteSnapshot({ regionContext: settings.regionContext, snapshotId: snapshotsToDelete[i] }, deleteSnapshotCB); } }); }); }, associateAddress: function(settings, callback){ settings.publicIp = settings.associatePairs.publicIp; that.disassociateAddress(settings, function(error) { var associateSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'POST', params: {'InstanceId': settings.associatePairs.instanceId, 'PublicIp': settings.associatePairs.publicIp}, action: 'AssociateAddress' }, finalResult = {}; localCloudRequest(associateSettings, function(error, result) { var confirmationString = 'SUCCESS'; if (error) { confirmationString = 'ERROR'; } finalResult.rawResult = result; finalResult.result = confirmationString; callback(error, finalResult); }); }); }, disassociateAddress: function(settings, callback){ var disassociateSettings = { credentials: settings.regionContext.identitySettings.credentials, region: settings.regionContext.computeSettings.region, path: '/', method: 'POST', params: {'PublicIp': settings.publicIp}, action: 'DisassociateAddress' }, finalResult = {}; localCloudRequest(disassociateSettings, function(error, result) { var confirmationString = 'SUCCESS'; if (error) { confirmationString = 'ERROR'; } finalResult.rawResult = result; finalResult.result = confirmationString; callback(error, finalResult); }); } }; return that; })();