UNPKG

pkgcloud

Version:

An infrastructure-as-a-service agnostic cloud library for node.js

699 lines (619 loc) 21.5 kB
/** * (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'); var Buffer = require('buffer').Buffer; var MANAGEMENT_API_VERSION = exports.MANAGEMENT_API_VERSION = '2012-03-01'; var MANAGEMENT_ENDPOINT = exports.MANAGEMENT_ENDPOINT = 'management.core.windows.net'; var STORAGE_ENDPOINT = exports.STORAGE_ENDPOINT = 'blob.core.windows.net'; var STORAGE_API_VERSION = exports.STORAGE_API_VERSION = HeaderConstants.TARGET_STORAGE_VERSION; var TABLES_ENDPOINT = exports.TABLES_ENDPOINT = 'table.core.windows.net'; var TABLES_API_VERSION = exports.TABLES_API_VERSION = '2012-02-12'; var MINIMUM_POLL_INTERVAL = exports.MINIMUM_POLL_INTERVAL = 3000; /** * 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. */ var createServer = 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.name, 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 = client.config.azure.ssh; if (!ssh) { next(null); return; } cert.getAzureCert(ssh.pem,function (err, info) { if (err) { next(err); } else { vmOptions.sshCertInfo = info; next(null); } }); }, 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); } } ); }; var 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; } }; var getMediaLinkUrl = function (storageAccount, fileName) { return 'http://' + storageAccount + '.' + STORAGE_ENDPOINT + '/vhd/' + fileName; }; var createEndpoints = function (ports) { var endPoints = '', template = templates.loadSync('endpoint.xml'); ports.forEach(function (port) { endPoints += templates.compileSync(template,port); }); return endPoints; }; var 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: client.config.azure.username, PASSWORD: client.config.azure.password, SSH_CERTIFICATE_FINGERPRINT: vmOptions.sshCertInfo.fingerprint, PORT: client.config.azure.ssh.port || '22', LOCAL_PORT: client.config.azure.ssh.localPort || '22', ROLESIZE: options.flavor, ENDPOINTS: createEndpoints(client.config.azure.ports), OS_SOURCE_IMAGE_NAME: vmOptions.image.Name, OS_IMAGE_MEDIALINK: mediaLink }; makeTemplateRequest(client, path, 'linuxDeployment.xml', configParams, callback); }; var 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: client.config.azure.computerName || options.name.slice(0, 15), LABEL_BASE64: label, PASSWORD: client.config.azure.password, ROLESIZE: options.flavor, ENDPOINTS: createEndpoints(client.config.azure.ports), OS_SOURCE_IMAGE_NAME: vmOptions.image.Name, OS_IMAGE_MEDIALINK: mediaLink }; makeTemplateRequest(client, path, 'windowsDeployment.xml', configParams, callback); }; var 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); }; var 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); }; var validateCreateOptions = function (options, config, callback) { if (typeof options === 'function') { options = {}; } options = options || {}; // no args // check required options values ['flavor', 'image', 'name'].forEach(function (member) { if (!options[member]) { errs.handle( errs.create({ message: 'options.' + member + ' is a required argument.' }), callback ); } }); ['username', 'password', 'location'].forEach(function (member) { if (!config.azure[member]) { errs.handle( errs.create({ message: 'config.azure.' + member + ' is a required azure config parameter.' }), callback ); } }); callback(null); }; /** * getServer */ var getServer = exports.getServer = function (client, serverName, callback) { getServersFromService(client,serverName, function (err, servers) { if (err) { callback(err); } else { callback(err, servers[0] ? servers[0] : null); } }); }; var 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); } ); }; var 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); //console.log(body); headers['content-length'] = body.length; headers['content-type'] = 'application/xml'; headers['accept'] = 'application/xml'; client.request({ method: 'POST', path: path, body:body, headers: headers }, next, function (body, res) { // poll azure for result of request pollRequestStatus(client, res.headers['x-ms-request-id'], MINIMUM_POLL_INTERVAL, next); }); }], function (err, result) { callback(err); } ); }; var createHostedService = exports.createHostedService = function (client, serviceName, callback) { var path = client.subscriptionId + '/services/hostedservices'; var params = { NAME: serviceName, LABEL_BASE64: new Buffer(serviceName).toString('base64'), LOCATION: client.config.azure.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? */ var 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? */ var 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); }; var 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); }; var 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 }, callback, function (body, res) { // poll azure for result of request pollRequestStatus(client, res.headers['x-ms-request-id'], MINIMUM_POLL_INTERVAL, callback); }); }; var getHostedServices = exports.getHostedServices = function (client, callback) { var path = client.subscriptionId + '/services/hostedservices', services = []; client.get(path, callback, function (body, res) { 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). */ var 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, result) { callback(err, true); } ); }; var deleteServer = function (client, serverName, callback) { var path = client.subscriptionId + '/services/hostedservices/' + serverName; path += '/deployments/' + serverName; client.request({ method: 'DELETE', path: path }, callback, function (body, res) { // poll azure for result of request pollRequestStatus(client, res.headers['x-ms-request-id'], MINIMUM_POLL_INTERVAL, callback); }); }; var 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, onError, function (body, res) { callback(null, body); }); }; var 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 }, callback, function (body, res) { // poll azure for result of request pollRequestStatus(client, res.headers['x-ms-request-id'], MINIMUM_POLL_INTERVAL, callback); }); }; var 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, result) { callback(err); }); } }); }; /** * getServersFromServices * Retrieves all servers (VMs) from the list of services */ var 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 */ var getServersFromService = function (client, serviceName, callback) { var servers = []; getHostedServiceProperties(client, serviceName,function (err, result) { if (err) { callback(err); } else { if (result && result.Deployments && result.Deployments.Deployment) { if (isVM(result.Deployments.Deployment)) { servers.push(result.Deployments.Deployment); } } callback(null, servers); } }); }; var 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). */ var getHostedServiceProperties = function (client, serviceName, callback) { var path = client.subscriptionId + '/services/hostedservices/' + serviceName + '?embed-detail=true'; var onError = function (err) { if (err.failCode === 'Item not found') { callback(null, null); } else { callback(err); } }; client.get(path, onError, function (body, res) { callback(null, body); }); }; /** * pollRequestStatus * uses Get Operation Status * GET https://management.core.windows.net/<subscription-id>/operations/<request-id> */ var pollRequestStatus = function (client, requestId, interval, callback) { var checkStatus = function () { var path = client.subscriptionId + '/operations/' + requestId; client.get(path, callback, function (body, res) { switch(body.Status) { case 'InProgress': setTimeout(checkStatus, interval); break; case 'Failed': callback(body.Error); break; case 'Succeeded': callback(null); break; } }); }; checkStatus(); }; var getStorageInfoFromUri = exports.getStorageInfoFromUri = function (uri, callback) { var u, tokens, path, info = {}; u = URL.parse(uri); if (!u.host || !u.path) { callback(errs.create({message: 'invalid Azure container or blob uri'})); return; } 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 */ var 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); } ); }; /** * destroyImage() * 1. get the requested image * 2. delete the image using its label */ var 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); } ); };