cloud-blender
Version:
A high level library for cloud compute operations
1,341 lines (1,154 loc) • 60.9 kB
JavaScript
var request = require('request'),
underscore = require('underscore'),
crypto = require('crypto'),
parseString = require('xml2js').parseString,
async = require('async'),
AWSError = require('./aws-error'),
AWSSDK = require('aws-sdk'),
proxyagent = require('proxy-agent');
AWSSDK.config.ec2 = {
maxRetries: 5,
sslEnabled: true,
logger: process.stdout
};
module.exports = (function() {
var regionAzs = {
};
function getApiVersion(serviceType){
return serviceType === 'ec2' ? '2015-04-15' : '2010-05-08';
}
function parseBodyString(str, callback){
if(underscore.isString(str)){
parseString(str, callback);
}
else{
callback(null,null);
}
}
// 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 (settings.regionContext.computeSettings.subnetId){
// AZ is implicit on subnetId
// Use setImmediate so not to break async behaviour
async.setImmediate(function (){ callback(null, null); });
return;
}
if (region === 'sa-east-1') {
callback(new AWSError('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 AWSError('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, '');
}
// STEP 1
function canonicalRequest(settings, nowDate) {
var canonical = '';
canonical += settings.method + '\n'; // HTTPRequestMethod
canonical += settings.path + '\n';
canonical += ((settings.method === 'GET')?createURIString(settings.params):'') + '\n'; // CanonicalQueryString
canonical += 'host:' + settings.serviceType + '.' + settings.region + '.amazonaws.com\n' +
'x-amz-date:' + nowDate + '\n\n';
canonical += 'host;x-amz-date' + '\n';
if (settings.method === 'POST') {
canonical += hash(createURIString(settings.params));
}
else { // no body in GET
canonical += hash('');
}
//console.log('\ncanonical:\n$' + canonical + '$');
return canonical;
}
// STEP 2
function stringToSign(settings, nowDate) {
var str = '';
str += 'AWS4-HMAC-SHA256\n'; // Algorithm
str += nowDate + '\n'; // RequestDate
// CredentialScope
str += nowDate.substr(0,8) + '/';
str += settings.region + '/';
str += settings.serviceType + '/';
str += 'aws4_request\n';
// HashedCanonicalRequest
str += hash(canonicalRequest(settings, nowDate));
//console.log('\nstringToSign:\n$' + str+'$');
return str;
}
// STEP 3
function signature(settings, nowDate) {
var kDate = sign('AWS4' + settings.credentials.secretAccessKey, nowDate.substr(0,8)),
kRegion = sign(kDate, settings.region),
kService = sign(kRegion, settings.serviceType),
kCredentials = sign(kService, 'aws4_request');
return sign(kCredentials, stringToSign(settings, nowDate), 'hex');
}
// STEP 4
// This is the main of the signature process for POST method
function generateAuthorizationHeader(settings, nowDate) {
var authHeader = '';
authHeader = 'AWS4-HMAC-SHA256 ';
authHeader += 'Credential=' + settings.credentials.accessKeyId + '/';
authHeader += nowDate.substr(0,8) + '/';
authHeader += settings.region + '/';
authHeader += settings.serviceType + '/';
authHeader += 'aws4_request, ';
authHeader += 'SignedHeaders=host;x-amz-date, ';
authHeader += 'Signature=' + signature(settings, nowDate);
//console.log('\nauth header:\n' + authHeader);
return authHeader;
}
function generateEC2Request(settings, callback){
settings.serviceType = 'ec2';
generateAWSRequest(settings, callback);
}
function generateIAMRequest(settings, callback){
settings.serviceType = 'iam';
generateAWSRequest(settings, callback);
}
function generateAWSRequest(settings, callback) {
var body,
requestSettings,
uriStr,
authHeader,
domainSuffix = ((settings.region === 'cn-north-1')?'.amazonaws.com.cn':'.amazonaws.com'),
requestUrlBase = 'https://'+ settings.serviceType + (settings.serviceType === 'ec2' ? '.' + settings.region : '') + domainSuffix,
nowDate = createNowDate();
settings.params.Action = settings.action;
settings.params.Version = getApiVersion(settings.serviceType);
authHeader = generateAuthorizationHeader(settings, nowDate);
uriStr = createURIString(settings.params);
//console.log('uri str is ' + uriStr);
if (settings.method === 'POST') {
body = uriStr;
}
requestSettings = {
method: settings.method,
url: requestUrlBase + settings.path + ((settings.method === 'POST')?'':('?' + uriStr)),
headers: {
'Authorization': authHeader,
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Host': settings.serviceType + '.' + settings.region + '.amazonaws.com',
'x-amz-date': nowDate
},
body: body,
timeout: 120000
};
if (tunnelingProxyURL) {
requestSettings.proxy = tunnelingProxyURL;
}
//console.log(requestSettings);
request(requestSettings, function (error, response, bodyString) {
var errorRequest;
//we always try and parse the body string , even if we have an error. It might contains important information
//especially for StatusCode 400 errors . This information will be added to the providerError property
//if bodyString is null it will not return an error but jsonObj will be null
parseBodyString(bodyString, function(errParsing, jsonObj){
if(errParsing){
error = error || new AWSError();
error.message += 'bad parsing to XML of: ' + bodyString + ', parsing error is: ' + errParsing.message;
}
if(!jsonObj || error || response.statusCode>= 300){
errorRequest = new AWSError('problem in request: ' +
JSON.stringify(requestSettings) + ', description: ' + bodyString +
(response ? response.statusCode : 'request failed') + 'requestError: ' + error,
jsonObj);
callback(errorRequest);
}
else{
callback(null, jsonObj);
}
});
});
}
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++;
});
generateEC2Request(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);
}
});
}
function removeSecurityGroup(settings, groupInfo, callback){
var removeSecurityGroupSetting = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'POST',
params: {
GroupName: groupInfo.groupName
},
action: 'DeleteSecurityGroup'
};
generateEC2Request(removeSecurityGroupSetting, function (error) {
callback(error);
});
}
function createSecurityGroup(settings, groupInfo, callback) {
var createSecurityGroupSetting = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'POST',
params: {
GroupName: groupInfo.groupName,
GroupDescription: groupInfo.groupDescription
},
action: 'CreateSecurityGroup'
};
generateEC2Request(createSecurityGroupSetting, function (error) {
//what do we really want to do if we already have a duplicate group should we continue ? Do we want to check getEc2ErrorCode(error) !== 'InvalidGroup.Duplicate' ?
if (error) {
callback(error);
return;
}
var addIngressRules = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'POST',
params: {
GroupName: groupInfo.groupName
},
action: 'AuthorizeSecurityGroupIngress'
};
groupInfo.ingressRules.forEach(function (rule, index) {
index++;
//we can use either port to define a single port or fromPort toPort.
if(rule.port){
rule.fromPort = rule.toPort = rule.port;
}
addIngressRules.params['IpPermissions.' + index + '.' + 'IpProtocol'] = rule.protocol ? rule.protocol : 'tcp';
addIngressRules.params['IpPermissions.' + index + '.' + 'FromPort'] = rule.fromPort;
addIngressRules.params['IpPermissions.' + index + '.' + 'ToPort'] = rule.toPort;
addIngressRules.params['IpPermissions.' + index + '.' + 'IpRanges.1.CidrIp'] = rule.ipRange === 'all' ? '0.0.0.0/0' : rule.ipRange;
});
generateEC2Request(addIngressRules, function (error, result) {
if(error){
//We add a retry , because in some very rare cases , although we recieved a response that security group was created , it is still not ready , so we try again .
//This was confirmed by the AWS support team that it could happen and that we should use a timeout and try again.
setTimeout(function(){
generateEC2Request(addIngressRules, function(error, result){
console.log('Second call to AuthorizeSecurityGroupIngress for group: ' + groupInfo.groupName + ', region: ' + addIngressRules.region + ' ' + (error ? 'Failed.' : 'Succeeded.'));
callback(error, result);
});
}, 10000);
return;
}
callback(null, result);
});
});
}
function removeKeyPair(settings, keyPair, callback) {
var deleteKeyPair = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'POST',
params: {
KeyName: keyPair.name
},
action: 'DeleteKeyPair'
};
generateEC2Request(deleteKeyPair, function (error) {
callback(error);
});
}
function importKeyPair(settings, keyPair, callback) {
var importKeyPairSetting = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'POST',
params: {
KeyName: keyPair.name,
//API always sends base64 encoded public key. Input can be either raw or base64
PublicKeyMaterial: keyPair.publicKey ? new Buffer(keyPair.publicKey).toString("base64") : keyPair.publicKeyBase64
},
action: 'ImportKeyPair'
};
generateEC2Request(importKeyPairSetting, function (error) {
callback(error);
});
}
function createEc2Instance(settings){
var credentials = settings.regionContext.identitySettings.credentials;
return new AWSSDK.EC2({
region: settings.regionContext.computeSettings.region,
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey
});
}
function describeAddresses(settings, callback){
var ec2 = createEc2Instance(settings);
if (!settings.publicIps || settings.publicIps.length === 0){
callback("Error, you must provide addresses");
}
var params = {};
params.PublicIps = settings.publicIps;
ec2.describeAddresses(params, function (err, data) {
if (err) {
callback(err);
return;
}
callback(null, data.Addresses || []);
});
}
// the exported functions
var that = {
setProxy: function (proxyUrl) {
tunnelingProxyURL = proxyUrl;
if (!underscore.isEmpty(tunnelingProxyURL)) {
AWSSDK.config.update({
httpOptions: { agent: proxyagent(tunnelingProxyURL)}
});
}
},
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'
};
generateEC2Request(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: 'aws',
pollingCount: 180
};
},
listNodes: function (settings, callback) {
var generateSettings = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'GET',
params: {},
action: 'DescribeInstances'
};
generateEC2Request(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'
};
var finalResult = {};
var securityGroups = settings.nodeParams.securityGroups || [];
var userData = settings.nodeParams.userData;
var securityGroupsParamPrefix = "SecurityGroup.";
var subnetId = settings.regionContext.computeSettings.subnetId;
var isUsingVPC = settings.regionContext.computeSettings.isUsingVPC;
if (isUsingVPC && subnetId){
generateSettings.params["SubnetId"] = subnetId;
securityGroupsParamPrefix = "SecurityGroupId.";
}
underscore.each(securityGroups, function (group, i){
generateSettings.params[securityGroupsParamPrefix + i] = group;
});
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);
generateEC2Request(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 ec2, instanceId = settings.node.id;
function releaseElasticIps(done) {
function releaseEIP(association, cb) {
//disassociate is mandatory for Nondefault VPC
ec2.disassociateAddress({ AssociationId: association.AssociationId }, function(ignore) {
ec2.releaseAddress({ AllocationId: association.AllocationId }, function(err, data) {
cb(err);
});
});
}
try {
var credentials = settings.regionContext.identitySettings.credentials;
ec2 = new AWSSDK.EC2({
region: settings.regionContext.computeSettings.region,
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey
});
ec2.describeInstances({InstanceIds: [instanceId] }, function (err, data) {
if (err) {
done(err);
return;
}
var networkInterfaceId;
if (!underscore.isEmpty(data.Reservations) && !underscore.isEmpty(data.Reservations[0]) && !underscore.isEmpty(data.Reservations[0].Instances)) {
var instances = data.Reservations[0].Instances;
if (!underscore.isEmpty(instances[0]) && !underscore.isEmpty(instances[0].NetworkInterfaces)) {
if (instances[0].NetworkInterfaces[0] && instances[0].NetworkInterfaces[0].PrivateIpAddresses.length > 1) {
networkInterfaceId = instances[0].NetworkInterfaces[0].NetworkInterfaceId;
}
}
}
if (underscore.isEmpty(networkInterfaceId)) {
done();
return;
}
ec2.describeInstanceAttribute({ Attribute: 'userData', InstanceId: instanceId}, function (err, data) {
if (err || underscore.isEmpty(data) || underscore.isEmpty(data.UserData)) {
done();
return;
}
var decoded, userData;
try {
if (data.UserData && data.UserData.Value) {
decoded = new Buffer(data.UserData.Value, 'base64').toString();
userData = JSON.parse(decoded);
}
} catch(ex) {
console.log('decode failed: ' + ex.toString());
}
if (!underscore.isEmpty(userData) && userData.injectorUseMultipleIps && !userData.useIPWhiteList ) {
ec2.describeNetworkInterfaces({ NetworkInterfaceIds: [networkInterfaceId] }, function(err, data) {
if (err) {
done(err);
return;
}
var association = [];
if (!underscore.isEmpty(data.NetworkInterfaces) && !underscore.isEmpty(data.NetworkInterfaces[0])) {
var networkInterface = data.NetworkInterfaces[0];
underscore.each(networkInterface.PrivateIpAddresses, function(item) {
if (!item.Primary && item.Association) {
association.push(item.Association);
console.log('release Elastic IP. %s, %s ', item.Association.PublicIp, item.Association.AllocationId);
}
});
}
async.eachSeries(association, releaseEIP, function(err, results) {
if (err) {
console.log('failed to release Elastic IP. (%s)', err.toString());
}
done(err);
});
});
} else {
done();
}
});
});
} catch(ex) {
callback("deleteNode - got exception: " + ex.toString());
}
}
releaseElasticIps(function() {
var deleteSettings = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'POST',
params: {'InstanceId.1': settings.node.id},
action: 'TerminateInstances'
},
finalResult = {};
generateEC2Request(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'
},
action: 'CreateImage'
},
finalResult = {};
if (!settings.skipBlockDeviceMapping) {
underscore.extend(createImageSettings.params, {
'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
});
}
if (settings.imageParams.vendorSpecificParams) {
underscore.extend(createImageSettings.params, settings.imageParams.vendorSpecificParams);
}
generateEC2Request(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++;
});
generateEC2Request(tagsSettings, function (errorTags, result) {
callback(errorTags, finalResult);
});
}
else {
callback(error, finalResult);
}
});
},
listImages: function (settings, imageIdsArr, callback) {
var imagesObj = {}, imagesIndex = 0;
underscore.each(imageIdsArr, function (image) {
imagesIndex += 1;
imagesObj['ImageId.' + imagesIndex] = image;
});
var listSettings = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'GET',
params: {
'Owner.1': 'self'
},
action: 'DescribeImages'
}, snapSettings = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'GET',
params: {
'Owner.1': 'self'
},
action: 'DescribeSnapshots'
}, snapShots = {};
if (settings.vendorSpecificParams) {
underscore.extend(listSettings.params, settings.vendorSpecificParams);
}
if (imagesObj) {
underscore.extend(listSettings.params, imagesObj);
}
generateEC2Request(listSettings, function (error, result) {
generateEC2Request(snapSettings, function (errorSnap, resultSnap) {
underscore.each(resultSnap.DescribeSnapshotsResponse.snapshotSet[0].item, function (snapShot) {
snapShots[snapShot.snapshotId[0]] = snapShot.startTime[0];
});
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 = {}, creationTime;
var i,
loopIndex = 0,
ebsIndex = -1;
if (item.blockDeviceMapping && item.blockDeviceMapping[0] && item.blockDeviceMapping[0].item) {
loopIndex = item.blockDeviceMapping[0].item.length;
}
for (i = 0; i < loopIndex; i++) {
if (item.blockDeviceMapping[0].item[i].ebs) {
ebsIndex = i;
break;
}
}
if ((ebsIndex !== -1) &&
(item.imageState[0] === 'available')) { //only if the image is available we can be sure that we have the snapshotId
creationTime = snapShots[item.blockDeviceMapping[0].item[ebsIndex].ebs[0].snapshotId[0]];
}
else {
creationTime = Date.now();
}
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],
creationTime: creationTime,
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 = {};
generateEC2Request(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 = {};
generateEC2Request(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 = new AWSError(),
finalResult = {
rawResult: []
},
listSettings = {
regionContext: settings.regionContext
},
snapshotsToDelete = [],
i, length, snapshotCounter = 0;
function deleteSnapshotCB(errorDeleteSnapshot, resultDeleteSnapshot) {
snapshotCounter++;
if (errorDeleteSnapshot) {
errorDeleteImage.appendError(errorDeleteSnapshot);
finalResult.result = 'ERROR';
}
finalResult.rawResult.push(resultDeleteSnapshot.rawResult);
if (snapshotCounter === length) {
//we call the getCallbackError that returns null if there no errors in the errorDeleteImage.
//we do this because we are iterating and the errorDeleteImage can contain 0 or more errors.
//Instead of doing errorDeleteImage.length > 0 ? errorDeleteImage : null we do this logic in getCallbackError()
callback(errorDeleteImage.getCallbackError(), finalResult);
}
}
that.listImages(listSettings,settings.imageParams.imageId, function (errorList, resultList) {
var finalError;
if (errorList || (
resultList.images[0] && resultList.images[0].status !== 'ACTIVE')) {
finalResult.result = 'ERROR';
finalError = errorList ? errorList : new AWSError('problem in images state. Status not ACTIVE: ' + JSON.stringify(resultList.images[0]));
callback(finalError, 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.appendError(errorDeregister);
finalResult.result = 'ERROR';
callback(errorDeleteImage.getCallbackError(), finalResult);
return;
}
for (i = 0, length = snapshotsToDelete.length; i < length; i++) {
that.deleteSnapshot({
regionContext: settings.regionContext,
snapshotId: snapshotsToDelete[i]
}, deleteSnapshotCB);
}
});
});
},
associateAddress: function (settings, callback) {
var ec2 = createEc2Instance(settings);
var localSettings = {
publicIps: [settings.associatePairs.publicIp],
instanceId: settings.associatePairs.instanceId,
regionContext: settings.regionContext,
};
that.disassociateAddressesEx(localSettings, function (error) {
describeAddresses(localSettings, function (error, addresses){
if (!addresses || !addresses[0]){
callback("Error, could not find address");
return;
}
var associationInfo = {
InstanceId: localSettings.instanceId,
PublicIp: localSettings.publicIps[0]
};
if (addresses[0].Domain === "vpc"){
delete associationInfo.PublicIp;
associationInfo.AllocationId = addresses[0].AllocationId;
}
ec2.associateAddress(associationInfo, function (e, result){
callback(e, {
rawResult: result,
result: !error ? 'SUCCESS' : 'ERROR'
});
});
});
});
},
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 = {};
generateEC2Request(disassociateSettings, function (error, result) {
var confirmationString = 'SUCCESS';
if (error) {
confirmationString = 'ERROR';
}
finalResult.rawResult = result;
finalResult.result = confirmationString;
callback(error, finalResult);
});
},
disassociateAddressesEx: function (settings, callback) {
var ec2, addresses = [], params = {},
credentials = settings.regionContext.identitySettings.credentials;
function disassociateIp(item, done) {
var params = {};
if (item.Domain === 'vpc') {
if (!item.AssociationId) {
done();
return;
}
params.AssociationId = item.AssociationId;
} else {
params.PublicIp = item.PublicIp;
}
ec2.disassociateAddress(params, function (err) {
if (err) {
done(err);
return;
}
done();
});
}
ec2 = new AWSSDK.EC2({
region: settings.regionContext.computeSettings.region,
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey
});
params.PublicIps = settings.publicIps;
ec2.describeAddresses(params, function (err, data) {
if (err) {
callback(err);
return;
}
if (!underscore.isEmpty(data.Addresses)) {
underscore.each(data.Addresses, function(item) {
addresses.push(underscore.pick(item, ['PublicIp', 'AssociationId', 'Domain']));
});
}
async.eachSeries(addresses, disassociateIp, function(err) {
if (err) {
callback(err);
return;
}
callback();
});
});
},
allocateAddress: function (settings, callback) {
var finalResult = {};
var allocateSettings = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'POST',
params: {
Domain: settings.regionContext.computeSettings.isUsingVPC ? 'vpc' : 'standard'
},
action: 'AllocateAddress'
};
generateEC2Request(allocateSettings, function (error, result) {
if (result) {
finalResult.result = result.AllocateAddressResponse.publicIp[0];
finalResult.rawResult = result;
}
callback(error, finalResult);
});
},
releaseAddress: function (settings, callback) {
var finalResult = {},
paramsObj = (settings.requestInfo) ? {'AllocationId': settings.requestInfo} : {'PublicIp': settings.ip},
releaseSettings = {
credentials: settings.regionContext.identitySettings.credentials,
region: settings.regionContext.computeSettings.region,
path: '/',
method: 'POST',
params: paramsObj,
action: 'ReleaseAddress'
};
generateEC2Request(releaseSettings, function (error, result) {
if (result) {
finalResult.result = result.ReleaseAddressResponse.return[0];
finalResult.ip = settings.ip;
finalResult.rawResult = result;
}
callback(error, finalResult);
});
},
assignPublicAddresses: function (settings, callback) {
var ec2, publicIps = [], instanceInfo = {},
instanceId = settings.instanceId, numberOfPublicIps = settings.numberOfPublicIps;
function allocatePublicIp(done) {
ec2.allocateAddress({ Domain: 'vpc'}, function (err, data) {
if (err) {
done(err);
return;
}
publicIps.push( { PublicIp: data.PublicIp, AllocationId: data.AllocationId } );
done();
});
}
function describeInstance(done) {
ec2.describeInstances({InstanceIds: [instanceId] }, function (err, data) {
if (err) {
done(err);
return;
}
if (!underscore.isEmpty(data.Reservations) && !underscore.isEmpty(data.Reservations[0]) && !underscore.isEmpty(data.Reservations[0].Instances)) {
var instances = data.Reservations[0].Instances;
if (!underscore.isEmpty(instances[0]) && (instances[0].State.Code === 16) &&
!underscore.isEmpty(instances[0].NetworkInterfaces)) {
instanceInfo.NetworkInterfaceId = instances[0].NetworkInterfaces[0].NetworkInterfaceId;
instanceInfo.PrivateIpAddresses = instances[0].NetworkInterfaces[0].PrivateIpAddresses;
instanceInfo.PrivateIpAddresses = underscore.reject(instanceInfo.PrivateIpAddresses, function(addr) { return addr.Primary === true; } );
done();
return;
}
}
done('instance is not yet in running state or its network interface is not ready');
});
}
function associateIps(data, done) {
var params = {
AllocationId: data.AllocationId,
AllowReassociation: true,
NetworkInterfaceId: instanceInfo.NetworkInterfaceId
};
var privateIp = instanceInfo.PrivateIpAddresses.pop();
if (privateIp) {
params.PrivateIpAddress = privateIp.PrivateIpAddress;
}
ec2.associateAddress(params, function (err, data) {
if (err) {
done(err);
return;
}
done();
});
}
function allocatePublicIps(done) {
var tasks = [], params = {};
if (underscore.isEmpty(publicIps)) {
underscore.times(numberOfPublicIps - 1, function () { tasks.push(allocatePublicIp); });
async.series(tasks, function(err) {
done(err);
});
} else {
params.PublicIps = publicIps;
ec2.describeAddresses(params, function (err, data) {
if (err) {
done(err);
return;
}
publicIps = [];
if (!underscore.isEmpty(data.Addresses)) {
underscore.each(data.Addresses, function(item) {
publicIps.push(underscore.pick(item, ['PublicIp', 'AllocationId']));
});
}
done();
});
}
}
try {
if (underscore.isArray(settings.publicIps) && (settings.publicIps.length >= 1)) {
publicIps = underscore.clone(settings.publicIps);
numberOfPublicIps = publicIps.length + 1;
}
if (underscore.isEmpty(instanceId) || isNaN(numberOfPublicIps) || (numberOfPublicIps < 2)) {
callback(new Error('invalid parameter'));
return;
}
var credentials = settings.regionContext.identitySettings.credentials;
ec2 = new AWSSDK.EC2({
region: settings.regionContext.computeSettings.region,
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey
});
async.retry({times: 10, interval: 5000}, describeInstance, function(err) {
if (err) {
callback(err);
return;
}
var params = {
NetworkInterfaceId: instanceInfo.NetworkInterfaceId,
AllowReassignment: true,
SecondaryPrivateIpAddressCount: numberOfPublicIps - 1
};
ec2.assignPrivateIpAddresses(params, function (err, data) {
if (err) {
callback(err);
return;
}
describeInstance(function(err) {
if (err) {
callback(err);
return;
}
allocatePublicIps(function(err) {
if