@yawetse/pkgcloud
Version:
An infrastructure-as-a-service agnostic cloud library for node.js
709 lines (628 loc) • 21.6 kB
JavaScript
/**
* (C) Microsoft Open Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var HeaderConstants = require('./constants').HeaderConstants;
var async = require('async');
var templates = require('../compute/templates/templates');
var _ = require('underscore');
var errs = require('errs');
var URL = require('url');
var cert = require('../utils/cert');
var pkgcloud = require('../../../../../pkgcloud');
exports.MANAGEMENT_API_VERSION = '2012-03-01';
exports.MANAGEMENT_ENDPOINT = 'management.core.windows.net';
var STORAGE_ENDPOINT = exports.STORAGE_ENDPOINT = 'blob.core.windows.net';
exports.STORAGE_API_VERSION = HeaderConstants.TARGET_STORAGE_VERSION;
exports.TABLES_ENDPOINT = 'table.core.windows.net';
exports.TABLES_API_VERSION = '2012-02-12';
var MINIMUM_POLL_INTERVAL = exports.MINIMUM_POLL_INTERVAL = 3000;
// Declaring variables for helper functions defined later
var createVM, createLinuxVM, createWindowsVM, validateCreateOptions, getServer,
getServers, rebootServer, stopServer, deleteHostedService, destroyServer, createImage,
captureServer, deleteImage, destroyImage, getMediaLinkUrl, createEndpoints,
makeTemplateRequest, createHostedService, addCertificate, getHostedServices,
deleteServer, getOSImage, deleteOSDisk, deleteOSBlob, getServersFromServices,
getServersFromService, isVM, getHostedServiceProperties, pollRequestStatus,
getStorageInfoFromUri;
/**
* createServer()
*
* In order to deploy a vm, Azure requires us to do the following
* before we can actually try to create the vm.
* 1. get or create a Hosted Service (we use the same name as the vm)
* 2. resolve the OSImage url to a container on the user's account
* 3. upload SSH certificate (if necessary)
* 4. create the VM
*
* Note: creating a VM on Azure will fail if one of the following is true
* 1. The VM (with the same name) already exists
* 2. The blob storage (with the same name) for the OSImage already exists
* 3. The VM disk (with the same name) for the OSImage already exists
* 4. The storage account is in a different azure location than the vm
* (East US, West US...)
*
* Note: createServer() must wait for Azure to respond if the createDeployment (vm)
* request succeeded. createServer() asynchronously polls Azure to get
* the result. Once the result is received, the callback function will be called
* with the server information or error. The state of returned server will most likely
* be PROVISIONING or STOPPED. Use server.setWait() to continue polling the server until
* its status is RUNNING. This entire process may take several minutes.
*/
exports.createServer = function (client, options, callback) {
var vmOptions = {},
ssh;
// async execute the following tasks one by one and bail if there is an error
async.waterfall([
function (next) {
// validate createServer options
validateCreateOptions(options, client.config, next);
},
function (next) {
getHostedServiceProperties(client, options.name, next);
},
function (service, next) {
// if the HostedService does not exist, create it
vmOptions.hostedService = service;
if (vmOptions.hostedService === null) {
createHostedService(client, options, function (err, service) {
if (err) {
next(err);
} else {
vmOptions.hostedService = service;
next(null);
}
});
} else {
next(null);
}
},
function (next) {
// get the server's OSImage info
getOSImage(client, options.image, function (err, res) {
if (err) {
next(err);
} else {
vmOptions.image = res;
next(null);
}
});
},
function (next) {
ssh = options.ssh;
if (ssh) {
vmOptions.sshCertInfo = cert.getAzureCertInfo(ssh.cert);
}
next();
},
function (next) {
// add the ssh certificate to the service
if (vmOptions.sshCertInfo) {
addCertificate(client, options.name, vmOptions.sshCertInfo.cert, ssh.pemPassword, function (err) {
next(err);
});
} else {
next(null);
}
},
function (next) {
// create the VM and wait for response
createVM(client, options, vmOptions, next);
},
function (next) {
// now get the actual server info
getServer(client, options.name, next);
}],
function (err, result) {
if (err) {
callback(err);
} else {
// return the server info
callback(null, result);
}
}
);
};
/**
* getServer
*/
getServer = exports.getServer = function (client, serverName, callback) {
getServersFromService(client, serverName, function (err, servers) {
return !err
? callback(err, servers[0] ? servers[0] : null)
: callback(err);
});
};
getServers = exports.getServers = function (client, callback) {
// async execute the following tasks one by one and bail if there is an error
async.waterfall([
function (next) {
// get the list of Hosted Services
getHostedServices(client, next);
},
function (hostedServices, next) {
// get the list of Servers from the Hosted Services
getServersFromServices(client, hostedServices, next);
}],
function (err, servers) {
callback(err, servers);
}
);
};
makeTemplateRequest = function (client, path, templateName, params, callback) {
var headers = {},
body;
// async execute the following tasks one by one and bail if there is an error
async.waterfall([
function (next) {
templates.load(templateName, next);
},
function (template, next) {
// compile template with params
body = _.template(template, params);
headers['content-length'] = body.length;
headers['content-type'] = 'application/xml';
headers['accept'] = 'application/xml';
client._request({
method: 'POST',
path: path,
body: body,
headers: headers
}, function (err, body, res) {
if (err) {
return next(err);
}
// poll azure for result of request
pollRequestStatus(client, res.headers['x-ms-request-id'], MINIMUM_POLL_INTERVAL, next);
});
}],
function (err) {
callback(err);
}
);
};
createHostedService = exports.createHostedService = function (client, options, callback) {
var path = client.subscriptionId + '/services/hostedservices';
var params = {
NAME: options.name,
LABEL_BASE64: new Buffer(options.name).toString('base64'),
LOCATION: options.location
};
makeTemplateRequest(client, path, 'createHostedService.xml', params, callback);
};
/**
* rebootServer
* uses Restart Role
* POST https://management.core.windows.net/<subscription-id>/services/hostedservices/<service-name>/deployments/<deployment-name>/roleinstances/<role-name>/operations
* A successful operation returns status code 201 (Created). Need to poll for success?
*/
rebootServer = exports.rebootServer = function (client, serviceName, callback) {
var path = client.subscriptionId + '/services/hostedservices/' +
serviceName + '/deployments/' +
serviceName + '/roleInstances/' +
serviceName + '/Operations';
makeTemplateRequest(client, path, 'restartRole.xml', {}, callback);
};
/**
* stopServer
* uses Shutdown Role
* POST https://management.core.windows.net/<subscription-id>/services/hostedservices/<service-name>/deployments/<deployment-name>/roleinstances/<role-name>/operations
* A successful operation returns status code 201 (Created). Need to poll for success?
*/
stopServer = exports.stopServer = function (client, serviceName, callback) {
var path = client.subscriptionId + '/services/hostedservices/' +
serviceName + '/deployments/' +
serviceName + '/roleInstances/' +
serviceName + '/Operations';
makeTemplateRequest(client, path, 'shutdownRole.xml', {}, callback);
};
addCertificate = function (client, serviceName, cert, password, callback) {
var path = client.subscriptionId + '/services/hostedservices/' +
serviceName + '/certificates';
var params = {
CERT_BASE64: new Buffer(cert, 'utf8').toString('base64'),
PASSWORD: password
};
makeTemplateRequest(client, path, 'addCertificate.xml', params, callback);
};
deleteHostedService = exports.deleteHostedService = function (client, serviceName, callback) {
// DELETE https://management.core.windows.net/<subscription-id>/services/hostedservices/<service-name>
var path = client.subscriptionId + '/services/hostedservices/' + serviceName;
client._request({
method: 'DELETE',
path: path
}, function (err, body, res) {
if (err) {
return callback(err);
}
// poll azure for result of request
pollRequestStatus(client, res.headers['x-ms-request-id'], MINIMUM_POLL_INTERVAL, callback);
});
};
getHostedServices = exports.getHostedServices = function (client, callback) {
var path = client.subscriptionId + '/services/hostedservices',
services = [];
client.get(path, function (err, body) {
if (err) {
return callback(err);
}
if (body.HostedService) {
// need to check if azure returned an array or single object
if (Array.isArray(body.HostedService)) {
body.HostedService.forEach(function (service) {
services.push(service);
});
} else {
services.push(body.HostedService);
}
}
callback(null, services);
});
};
/**
* destroyServer
* uses Delete Deployment
* DELETE https://management.core.windows.net/<subscription-id>/services/hostedservices/<service-name>/deployments/<deployment-name>
* Because Delete Deployment is an asynchronous operation, it always returns status code 202 (Accept).
* To determine the status code for the operation once it is complete, call Get Operation Status.
* Because Delete Deployment is an asynchronous operation, it always returns status code 202 (Accept).
*/
destroyServer = exports.destroyServer = function (client, serverName, callback) {
var server = null;
// async execute the following tasks one by one and bail if there is an error
async.waterfall([
function (next) {
// get the list of Hosted Services
getServer(client, serverName, next);
},
function (result, next) {
server = result;
// get the list of Hosted Services
stopServer(client, serverName, next);
},
function (next) {
deleteServer(client, serverName, next);
},
function (next) {
deleteOSDisk(client, server, next);
},
function (next) {
deleteOSBlob(client, server, next);
},
function (next) {
deleteHostedService(client, serverName, next);
}],
function (err) {
callback(err, true);
}
);
};
deleteServer = function (client, serverName, callback) {
var path = client.subscriptionId + '/services/hostedservices/' + serverName;
path += '/deployments/' + serverName;
client._request({
method: 'DELETE',
path: path
}, function (err, body, res) {
if (err) {
return callback(err);
}
// poll azure for result of request
pollRequestStatus(client, res.headers['x-ms-request-id'], MINIMUM_POLL_INTERVAL, callback);
});
};
getOSImage = exports.getOSImage = function (client, imageName, callback) {
var path = '/' + client.subscriptionId + '/services/images/' + imageName;
var onError = function (err) {
if (err.failCode === 'Item not found') {
callback(null, null);
} else {
callback(err);
}
};
client.get(path, function (err, body) {
return err
? onError(err)
: callback(null, body);
});
};
deleteOSDisk = function (client, server, callback) {
var diskName = null,
path;
if (server && server.RoleList && server.RoleList.Role) {
if (server.RoleList.Role.OSVirtualHardDisk) {
diskName = server.RoleList.Role.OSVirtualHardDisk.DiskName;
}
}
if (diskName === null) {
callback(null);
return;
}
// https://management.core.windows.net/<subscription-id>/services/disks/<disk-name>
path = client.subscriptionId + '/services/disks/' + diskName;
client._request({
method: 'DELETE',
path: path
}, function (err, body, res) {
if (err) {
return callback(err);
}
// poll azure for result of request
pollRequestStatus(client, res.headers['x-ms-request-id'], MINIMUM_POLL_INTERVAL, callback);
});
};
deleteOSBlob = function (client, server, callback) {
var blob = null;
if (server && server.RoleList && server.RoleList.Role) {
if (server.RoleList.Role.OSVirtualHardDisk) {
blob = server.RoleList.Role.OSVirtualHardDisk.MediaLink;
}
}
if (blob === null) {
callback(null);
return;
}
getStorageInfoFromUri(blob, function (err, info) {
if (err) {
callback(err);
} else {
var storage = pkgcloud.storage.createClient(client.config);
storage.removeFile(info.container, info.file, function (err) {
callback(err);
});
}
});
};
/**
* getServersFromServices
* Retrieves all servers (VMs) from the list of services
*/
getServersFromServices = function (client, services, callback) {
var task = function (service, next) {
getServersFromService(client, service.ServiceName, function (err, servers) {
next(err, servers);
});
};
// Check each service for deployed VMs.
async.concat(services, task, function (err, servers) {
callback(err, servers);
});
};
/**
* getServersFromServices
* Retrieves all servers (VMs) from a Hosted Service
*/
getServersFromService = function (client, serviceName, callback) {
var servers = [];
getHostedServiceProperties(client, serviceName, function (err, result) {
if (err) {
return callback(err);
}
if (result && result.Deployments && result.Deployments.Deployment) {
if (isVM(result.Deployments.Deployment)) {
servers.push(result.Deployments.Deployment);
}
}
callback(null, servers);
});
};
isVM = function (deployment) {
if (deployment.RoleList && deployment.RoleList.Role) {
if (deployment.RoleList.Role.RoleType === 'PersistentVMRole') {
return true;
}
}
return false;
};
/**
Get Hosted Service Properties
GET https://management.core.windows.net/<subscription-id>/services/hostedservices/<service-name>?embed-detail=true
A successful operation returns status code 200 (OK).
*/
getHostedServiceProperties = function (client, serviceName, callback) {
var path = client.subscriptionId + '/services/hostedservices/' + serviceName + '?embed-detail=true';
var onError = function (err) {
return err.failCode === 'Item not found'
? callback(null, null)
: callback(err);
};
client.get(path, function (err, body) {
return err
? onError(err)
: callback(null, body);
});
};
/**
* pollRequestStatus
* uses Get Operation Status
* GET https://management.core.windows.net/<subscription-id>/operations/<request-id>
*/
pollRequestStatus = function (client, requestId, interval, callback) {
var checkStatus = function () {
var path = client.subscriptionId + '/operations/' + requestId;
client.get(path, function (err, body) {
if (err) {
return callback(err);
}
switch (body.Status) {
case 'InProgress':
setTimeout(checkStatus, interval);
break;
case 'Failed':
callback(body.Error);
break;
case 'Succeeded':
callback(null);
break;
}
});
};
checkStatus();
};
getStorageInfoFromUri = exports.getStorageInfoFromUri = function (uri, callback) {
var u, tokens, path,
info = {};
u = URL.parse(uri);
if (!u.host || !u.path) {
return callback(errs.create({message: 'invalid Azure container or blob uri'}));
}
tokens = u.host.split('.');
info.storage = tokens[0];
path = u.path;
// if necessary, remove leading '/' from path
if (path.charAt(0) === '/') {
path = path.substr(1);
}
tokens = path.split('/');
info.container = tokens.shift();
info.file = tokens.join('/');
callback(null, info);
};
/**
* createImage()
* 1. Check if the server exists
* 2. stop server if it is running
* 3. capture server image
*/
createImage = exports.createImage = function (client, serverName, targetImageName, callback) {
async.waterfall([
function (next) {
// stop the server
stopServer(client, serverName, next);
},
function (next) {
// capture the server image
captureServer(client, serverName, targetImageName, next);
}],
function (err) {
callback(err, targetImageName);
}
);
};
deleteImage = function (client, image, callback) {
// https://management.core.windows.net/<subscription-id>/services/images/<image-name>
var path = client.subscriptionId + '/services/images/' + image.Name;
var configParams = {
LABEL: image.LABEL
};
makeTemplateRequest(client, path, 'deleteImage.xml', configParams, callback);
};
/**
* destroyImage()
* 1. get the requested image
* 2. delete the image using its label
*/
destroyImage = exports.destroyImage = function (client, imageName, callback) {
async.waterfall([
function (next) {
// stop the server
client.getImage(client, imageName, next);
},
function (image, next) {
deleteImage(client, image, next);
}],
function (err) {
callback(err, imageName);
}
);
};
createVM = function (client, options, vmOptions, callback) {
// check OS type of image to determine if we are creating a linux or windows VM
switch (vmOptions.image.OS.toLowerCase()) {
case 'linux':
createLinuxVM(client, options, vmOptions, callback);
break;
case 'windows':
createWindowsVM(client, options, vmOptions, callback);
break;
default:
callback(errs.create({message: 'Unknown Image OS: ' + vmOptions.image.OS}));
break;
}
};
getMediaLinkUrl = function (storageAccount, fileName) {
return 'http://' + storageAccount + '.' + STORAGE_ENDPOINT + '/vhd/' + fileName;
};
createEndpoints = function (ports) {
var endPoints = '',
template = templates.loadSync('endpoint.xml');
(ports || []).forEach(function (port) {
endPoints += templates.compileSync(template, port);
});
return endPoints;
};
createLinuxVM = function (client, options, vmOptions, callback) {
var path = client.subscriptionId + '/services/hostedservices/' + options.name + '/deployments';
var mediaLink = getMediaLinkUrl(client.config.storageAccount, options.name + '.vhd');
var label = new Buffer(options.name).toString('base64');
var configParams = {
NAME: options.name,
LABEL_BASE64: label,
USERNAME: options.username,
PASSWORD: options.password,
SSH_CERTIFICATE_FINGERPRINT: vmOptions.sshCertInfo.fingerprint,
PORT: options.ssh.port || '22',
LOCAL_PORT: options.ssh.localPort || '22',
ROLESIZE: options.flavor,
ENDPOINTS: createEndpoints(options.ports),
OS_SOURCE_IMAGE_NAME: vmOptions.image.Name,
OS_IMAGE_MEDIALINK: mediaLink
};
makeTemplateRequest(client, path, 'linuxDeployment.xml', configParams, callback);
};
createWindowsVM = function (client, options, vmOptions, callback) {
var path = client.subscriptionId + '/services/hostedservices/' + options.name + '/deployments';
var mediaLink = getMediaLinkUrl(client.config.storageAccount, options.name + '.vhd');
var label = new Buffer(options.name).toString('base64');
var configParams = {
NAME: options.name,
COMPUTER_NAME: options.computerName || options.name.slice(0, 15),
LABEL_BASE64: label,
PASSWORD: options.password,
ROLESIZE: options.flavor,
ENDPOINTS: createEndpoints(options.ports),
OS_SOURCE_IMAGE_NAME: vmOptions.image.Name,
OS_IMAGE_MEDIALINK: mediaLink
};
makeTemplateRequest(client, path, 'windowsDeployment.xml', configParams, callback);
};
captureServer = function (client, serverName, targetImageName, callback) {
// <subscription-id>/services/hostedservices/<service-name>/deployments/<deployment-name>/roleinstances/<role-name>/operations
var path = client.subscriptionId + '/services/hostedservices/' +
serverName + '/deployments/' +
serverName + '/roleInstances/' +
serverName + '/Operations';
var configParams = {
NAME: targetImageName
};
makeTemplateRequest(client, path, 'captureRole.xml', configParams, callback);
};
validateCreateOptions = function (options, config, callback) {
if (typeof options === 'function') {
options = {};
}
options = options || {}; // no args
// check required options values
['flavor', 'image', 'name', 'username', 'password', 'location'].forEach(function (member) {
if (!options[member]) {
errs.handle(
errs.create({ message: 'options.' + member + ' is a required argument.' }),
callback
);
}
});
callback();
};
exports._updateMinimumPollInterval = function(interval) {
MINIMUM_POLL_INTERVAL = interval;
};