diff --git a/.gitignore b/.gitignore index a307754e3..fb1890114 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ azure_error coverage.html .coveralls.yml pkgcloud.lcov* -node-jscoverage \ No newline at end of file +node-jscoverage +*.private.* +*.vscode/launch.json \ No newline at end of file diff --git a/examples/compute/azure-v2.js b/examples/compute/azure-v2.js new file mode 100644 index 000000000..0150d9ea0 --- /dev/null +++ b/examples/compute/azure-v2.js @@ -0,0 +1,64 @@ +var pkgcloud = require('../../lib/pkgcloud'); +var client; +var options; + +// +// Create a pkgcloud compute instance +// +options = { + provider: 'azure-v2', + subscriptionId: '{subscriptionId}', + resourceGroup: '{resourceGroup}', + + servicePrincipal: { + clientId: '{spClientId}', + secret: '{spSecret}', + domain: '{spDomain}' + } +}; +client = pkgcloud.compute.createClient(options); + +// +// Create a server. +// This may take several minutes. +// +var createVMOfferOptions = { + name: 'ms-pkgc-vm-test', + flavor: 'Standard_D1', + username: 'pkgcloud', + password: 'Pkgcloud!!', + + storageOSDiskName: 'osdisk', + storageDataDiskNames: [ 'datadisk1' ], + + imagePublisher: 'Canonical', + imageOffer: 'UbuntuServer', + imageSku: '16.04.0-LTS', + imageVersion: 'latest' +}; + +console.log('creating server...'); + +client.createServer(createVMOfferOptions, function (err, server) { + + console.log('servre created successfully:'); + console.dir(server); + + if (err) { + console.log(err); + } else { + client.destroyServer(createVMOfferOptions, { + destroyNics: true, + destroyPublicIP: true, + destroyVnet: true, + destroyStorage: true + }, function (err, server) { + if (err) { + console.log(err); + } else { + console.log('deleted successfully'); + console.dir(server); + } + }); + } +}); diff --git a/examples/storage/azure-v2.js b/examples/storage/azure-v2.js new file mode 100644 index 000000000..99f774f3a --- /dev/null +++ b/examples/storage/azure-v2.js @@ -0,0 +1,76 @@ +var path = require('path'); +var pkgcloud = require('../../lib/pkgcloud'); + +var client = pkgcloud.storage.createClient({ + provider: 'azure-v2', + subscriptionId: '{subscriptionId}', + resourceGroup: '{resourceGroup}', + + servicePrincipal: { + clientId: '{spClientId}', + secret: '{spSecret}', + domain: '{spDomain}' + } +}); + +// client.getFiles('storageacountname', null, function (err, files) (function (err) { + +// var file = files[0]; +// client.getFile('storageacountname', file, null, function (err, file) (function (err) { +// console.dir(file); +// }) +// }); + +client.getFiles('storageacountname', { container: 'container-name' }, function (err, files) { + + if (err) { + return console.error(err); + } + + var file = files[0]; + console.dir(file); + + var download = client.download({ + container: 'storageacountname', + storage: { container: 'container-name' }, + remote: 'file.name.to.download.ext', + local: path.join(__dirname, 'file.name.to.download.ext') + }, function (err) { + return err ? console.dir(err) : null; + }); + + download.on('error', function(err) { + console.error(err); + }); + + download.on('end', function(file) { + console.log('file write has ended:'); + console.dir(file); + }); + + download.on('data', function(data) { + console.log(data && data.length); + }); +}); + +// client.createContainer('storageacountname', function (err, container) { +// console.log('created: ', container.toJSON()); + +// client.getContainer('storageacountname', function (err, container) { +// console.log('found: ', container.toJSON()); +// }); +// }); + +// client.getContainers(function (err, containers) { +// if (err) { +// console.error(err); +// } + +// client.getContainer(containers[0], function (err, container) { +// console.log('found: ', container.toJSON()); +// }); + +// containers.forEach(function (container) { +// console.log(container.toJSON()); +// }); +// }); diff --git a/lib/pkgcloud.js b/lib/pkgcloud.js index b70995991..927a7d693 100644 --- a/lib/pkgcloud.js +++ b/lib/pkgcloud.js @@ -21,6 +21,7 @@ var components = [ var providers = [ 'amazon', 'azure', + 'azure-v2', 'digitalocean', 'google', 'iriscouch', diff --git a/lib/pkgcloud/azure-v2/azure-api.js b/lib/pkgcloud/azure-v2/azure-api.js new file mode 100644 index 000000000..13522d951 --- /dev/null +++ b/lib/pkgcloud/azure-v2/azure-api.js @@ -0,0 +1,112 @@ +var errs = require('errs'); + +var msRestAzure = require('ms-rest-azure'); +var resourceManagement = require('azure-arm-resource'); + +var templates = require('./templates'); +var constants = require('./constants'); + +/** + * This callback type is called `requestCallback` and is displayed as a global symbol. + * + * @callback requestCallback + * @param {object} error + * @param {object} result + */ + +/** + * Request and save credentials for accessing azure ARM resources. + * @param {object} client object containing configuration. + * @param {boolean} setupLocation Should setup a location from the resource group. + * @param {requestCallback} callback to respond to when complete. + */ +function login(setupLocation, callback) { + + var self = this; + if (typeof setupLocation == 'function' && typeof callback === 'undefined') { + callback = setupLocation; + setupLocation = null; + } + + // Make sure credentials are refreshed by intervals + if (self.azure && self.azure.credentials && self.azure.lastRefresh) { + var now = new Date(); + if (now - self.azure.lastRefresh < constants.CREDENTIALS_LIFESPAN) { + return callback(null, self.azure.credentials); + } + } + + var config = self.config; + var servicePrincipal = config.servicePrincipal; + msRestAzure.loginWithServicePrincipalSecret( + servicePrincipal.clientId, + servicePrincipal.secret, + servicePrincipal.domain, + function (err, credentials) { + + if (err) { + errs.handle( + errs.create({ + message: 'There was a problem connecting to azure: ' + err + }), + callback + ); + } + + self.azure = self.azure || {}; + self.azure.credentials = credentials; + self.azure.lastRefresh = new Date(); + + if (setupLocation) { + return self.setupLocation(credentials, function (err) { + return callback(err, credentials); + }); + } else { + return callback(null, credentials); + } + }); +} + +/** + * Setting up an azure session including querying the default location from the resource group + * used by the current configuration + * @param {object} credentials object containing azure credentials + * @param {requestCallback} callback to respond to when complete. + */ +function setupLocation(credentials, callback) { + var self = this; + + self.azure = self.azure || {}; + self.azure.location = self.azure.location || self.config.location; + if (self.azure.location) { + return callback(); + } + + if (self.config.resourceGroup) { + var resourceClient = new resourceManagement.ResourceManagementClient(credentials, self.config.subscriptionId); + resourceClient.resourceGroups.get(self.config.resourceGroup, function (err, result) { + + if (err) { + return callback(err); + } + + self.azure.location = result.location; + return callback(); + }); + } +} + +/** + * Binding common methods to be available to all clients + * @param {object} credentials object containing azure credentials + * @param {requestCallback} callback to respond to when complete. + */ +function bind(client) { + client.login = login.bind(client); + client.setupLocation = setupLocation.bind(client); + client.deploy = templates.deploy.bind(client); +} + +module.exports = { + bind: bind +}; \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/client.js b/lib/pkgcloud/azure-v2/client.js new file mode 100644 index 000000000..f51efce8f --- /dev/null +++ b/lib/pkgcloud/azure-v2/client.js @@ -0,0 +1,56 @@ +/* + * client.js: Base client from which all Azure clients inherit from + * + * (C) Microsoft Open Technologies, Inc. All rights reserved. + * + */ + +var util = require('util'); + +var base = require('../core/base'); +var azureApi = require('./azure-api'); + +var Client = exports.Client = function (options) { + base.Client.call(this, options); + + options = options || {}; + + // Allow overriding serversUrl in child classes + this.provider = 'azure-v2'; + azureApi.bind(this); + + if (!this.before) { + this.before = []; + } +}; + +util.inherits(Client, base.Client); + +Client.prototype._toArray = function toArray(obj) { + if (typeof obj === 'undefined') { + return []; + } + + return Array.isArray(obj) ? obj : [obj]; +}; + +Client.prototype.failCodes = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Item not found', + 409: 'Already exists or in progress', + 412: 'Lease error', + 413: 'Request Entity Too Large', + 415: 'Bad Media Type', + 500: 'Fault', + 503: 'Service Unavailable' +}; + +Client.prototype.successCodes = { + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-authoritative information', + 204: 'No content' +}; diff --git a/lib/pkgcloud/azure-v2/compute/client/flavors.js b/lib/pkgcloud/azure-v2/compute/client/flavors.js new file mode 100644 index 000000000..1ebf961df --- /dev/null +++ b/lib/pkgcloud/azure-v2/compute/client/flavors.js @@ -0,0 +1,60 @@ +/* + * flavors.js: Implementation of Azure Flavors Client. + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +var _ = require('lodash'); +var ComputeManagementClient = require('azure-arm-compute'); + +/** + * Lists all flavors available to your account. + * @param {function} callback - cb(err, flavors). `flavors` is an array that + * represents the flavors that are available to your account + */ +function getFlavors(callback) { + var self = this; + + self.login(true, function (err) { + + if (err) { + return callback(err); + } + + var client = new ComputeManagementClient(self.azure.credentials, self.config.subscriptionId); + client.virtualMachineSizes.list(self.azure.location, function (err, results) { + return err + ? callback(err) + : callback(null, results.map(function (res) { + return new self.models.Flavor(self, res); + })); + }); + }); +} + +/** + * Gets a specified flavor of AWS DataSets using the provided details object. + * @param {Flavor|String} image - Flavor ID or an Flavor + * @param {function} callback cb(err, flavor). `flavor` is an object that + * represents the flavor that was retrieved. + */ +function getFlavor(flavor, callback) { + var self = this; + var flavorId = flavor instanceof self.models.Flavor ? flavor.id : flavor; + + if (flavor instanceof self.models.Flavor) { + return callback(null, flavor); + } + + self.getFlavors(function (err, flavors) { + return err ? + callback(err) : + callback(null, _.find(flavors, { id: flavorId })); + }); +} + +module.exports = { + getFlavors: getFlavors, + getFlavor: getFlavor +}; \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/compute/client/images.js b/lib/pkgcloud/azure-v2/compute/client/images.js new file mode 100644 index 000000000..5c6e404ab --- /dev/null +++ b/lib/pkgcloud/azure-v2/compute/client/images.js @@ -0,0 +1,144 @@ +/* + * images.js: Implementation of Azure Images Client. + * + * (C) Microsoft Open Technologies, Inc. + * + */ +var ComputeManagementClient = require('azure-arm-compute'); + +var constants = require('../../constants'); + +/** + * Lists all images available to your account. + * @param {object} options **Optional** + * @param {string} options.publisher + * @param {string} options.offer + * @param {string} options.sku + * @param {function} callback - cb(err, images). `images` is an array that + * represents the images that are available to your account + */ +exports.getImages = function getImages(options, callback) { + var self = this; + + if (!callback && typeof options === 'function') { + callback = options; + options = null; + } + + options = options || {}; + var publisher = options.publisher || constants.DEFAULT_VM_IMAGE.PUBLISHER; + var offer = options.offer || constants.DEFAULT_VM_IMAGE.OFFER; + var sku = options.sku || constants.DEFAULT_VM_IMAGE.SKU; + + self.login(true, function (err) { + + if (err) { + return callback(err); + } + + var client = new ComputeManagementClient(self.azure.credentials, self.config.subscriptionId); + client.virtualMachineImages.list(self.azure.location, publisher, offer, sku, function (err, results) { + return err + ? callback(err) + : callback(null, results.map(function (res) { + return new self.models.Image(self, res, publisher, offer, sku); + })); + }); + }); +}; + +/** + * Gets a specified image of Azure using the provided details object. + * @param {Image|String} image Image id or an Image + * @param {string} options.publisher + * @param {string} options.offer + * @param {string} options.sku + * @param {function} callback - cb(err, image). `image` is an object that + * represents the image that was retrieved. + */ +exports.getImage = function getImage(image , options, callback) { + var self = this; + var version = image instanceof self.models.Image ? image.name : image; + + if (!callback && typeof options === 'function') { + callback = options; + options = null; + } + + options = options || {}; + var publisher = options.publisher || constants.DEFAULT_VM_IMAGE.PUBLISHER; + var offer = options.offer || constants.DEFAULT_VM_IMAGE.OFFER; + var sku = options.sku || constants.DEFAULT_VM_IMAGE.SKU; + + self.login(true, function (err) { + + if (err) { + return callback(err); + } + + var client = new ComputeManagementClient(self.azure.credentials, self.config.subscriptionId); + client.virtualMachineImages.get(self.azure.location, publisher, offer, sku, version, function (err, result) { + return err + ? callback(err) + : callback(null, new self.models.Image(self, result, publisher, offer, sku, version)); + }); + }); +}; + +/** + * ### function createImage(options, callback) + * #### @id {Object} an object literal with options + * #### @name {String} String name of the image + * #### @server {Server} the server to use + * #### @callback {function} f(err, image). `image` is an object that + * represents the image that was created. + * + * Creates an image in Azure based on a server + */ +exports.createImage = function createImage(options, callback) { + options || (options = {}); + + if (!options.name) { + throw new TypeError('`name` is a required option'); + } + + if (!options.server) { + throw new TypeError('`server` is a required option'); + } + + var self = this; + var serverId = options.server instanceof self.models.Server + ? options.server.id + : options.server; + + console.log('creating image for server ', serverId); + return callback(new Error('method not implemented')); + // azureApi.createImage(this, serverId, options.name, function (err, result) { + // return !err + // ? self.getImage(result, callback) + // : callback(err); + // }); +}; + +/** + * ### function destroyImage(image, callback) + * #### @image {Image|String} Image id or an Image + * #### @callback {function} f(err, image). `image` is an object that + * represents the image that was deleted. + * + * Destroys an image in Azure + */ +exports.destroyImage = function destroyImage(image, callback) { + var self = this; + var imageId = image instanceof self.models.Image ? image.id : image; + var path = self.config.subscriptionId + '/services/images/' + imageId; + + self._xmlRequest({ + method: 'DELETE', + path: path + }, function (err, body, res) { + return err + ? callback(err) + : callback(null, { ok: imageId }, res); + }); +}; diff --git a/lib/pkgcloud/azure-v2/compute/client/index.js b/lib/pkgcloud/azure-v2/compute/client/index.js new file mode 100644 index 000000000..4457400bf --- /dev/null +++ b/lib/pkgcloud/azure-v2/compute/client/index.js @@ -0,0 +1,51 @@ +/* + * index.js: Compute client for Azure + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +var util = require('util'); +var https = require('https'); +var auth = require('../../../common/auth'); +var constants = require('../../constants'); +var azure = require('../../client'); +var _ = require('lodash'); + +var Client = exports.Client = function (options) { + azure.Client.call(this, options); + + this.models = this.models || {}; + this.models.Flavor = require('../flavor').Flavor; + this.models.Image = require('../image').Image; + this.models.Server = require('../server').Server; + + _.extend(this, require('./flavors')); + _.extend(this, require('./images')); + _.extend(this, require('./servers')); + + this.serversUrl = options.serversUrl || constants.MANAGEMENT_ENDPOINT; + this.subscriptionId = this.config.subscriptionId; + + this.azureKeys = { + key: this.config.key, + cert: this.config.cert + }; + + this.azureKeys.subscriptionId = this.config.subscriptionId; + + this.before.push(auth.azure.managementSignature); + + // The https agent is used by request for authenticating TLS/SSL https calls + if (this.protocol === 'https://') { + this.before.push(function (req) { + req.agent = new https.Agent({ + host: this.serversUrl, + key: options.key, + cert: options.cert + }); + }); + } +}; + +util.inherits(Client, azure.Client); \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/compute/client/servers.js b/lib/pkgcloud/azure-v2/compute/client/servers.js new file mode 100644 index 000000000..aa4b360b7 --- /dev/null +++ b/lib/pkgcloud/azure-v2/compute/client/servers.js @@ -0,0 +1,477 @@ +/* + * servers.js: Instance methods for working with servers from Azure Cloud + * + * (C) Microsoft Open Technologies, Inc. + * + */ +var async = require('async'); +var errs = require('errs'); +var _ = require('lodash'); + +var resourceManagement = require('azure-arm-resource'); +var ComputeManagementClient = require('azure-arm-compute'); + +var constants = require('../../constants'); + +/** + * Gets the current API version + * @param {function} callback cb(err, version). + */ +function getVersion(callback) { + callback(null, constants.MANAGEMENT_API_VERSION); +} + +/** + * Gets the current API limits + * @param {function} callback - cb(err, version). + */ +function getLimits(callback) { + return errs.handle( + errs.create({ + message: 'Azure\'s API is not rate limited' + }), + callback + ); +} + +/** + * Lists all servers available to your account. + * @param {function} callback - cb(err, servers). `servers` is an array that + * represents the servers that are available to your account + */ +function getServers(callback) { + var self = this; + + self.login(function (err) { + + if (err) { + return callback(err); + } + + var client = new ComputeManagementClient(self.azure.credentials, self.config.subscriptionId); + client.virtualMachines.list(self.config.resourceGroup, function (err, results) { + return err ? + callback(err) : + callback(null, results.map(function (res) { + return new self.models.Server(self, res); + })); + }); + }); +} + +/** + * Gets a server in Azure. + * @param {Server|String} server Server id or a server + * @param {Function} callback cb(err, serverId). + */ +function getServer(server, callback) { + var self = this; + var serverId = server instanceof self.models.Server ? server.name : server; + + self.login(function (err) { + + if (err) { + return callback(err); + } + + var client = new ComputeManagementClient(self.azure.credentials, self.config.subscriptionId); + + // This will ensure returning of instances running status + var options = { + expand: 'instanceView' + }; + client.virtualMachines.get(self.config.resourceGroup, serverId, options, function (err, result) { + + if (err) { + return callback(err); + } + + // Get public dns url + if (!result.networkProfile || + !result.networkProfile.networkInterfaces || + !result.networkProfile.networkInterfaces.length) { + return callback(null, new self.models.Server(self, result)); + } + + var networkInterfaceId = result.networkProfile.networkInterfaces[0].id; + var resourceClient = new resourceManagement.ResourceManagementClient(self.azure.credentials, self.config.subscriptionId); + + resourceClient.resources.getById(networkInterfaceId, constants.DEFAULT_API_VERSION, function (err, networkInterface) { + + if (err) { + return callback(err); + } + + if (!networkInterface.properties.ipConfigurations || + !networkInterface.properties.ipConfigurations.length || + !networkInterface.properties.ipConfigurations[0] || + !networkInterface.properties.ipConfigurations[0].properties || + !networkInterface.properties.ipConfigurations[0].properties.publicIPAddress || + !networkInterface.properties.ipConfigurations[0].properties.publicIPAddress.id) { + return callback(null, new self.models.Server(self, result)); + } + + var publicIPID = networkInterface.properties.ipConfigurations[0].properties.publicIPAddress.id; + resourceClient.resources.getById(publicIPID, constants.DEFAULT_API_VERSION, function (err, publicIP) { + if (err) { + return callback(err); + } + + if (!publicIP.properties.dnsSettings || !publicIP.properties.dnsSettings.fqdn) { + return callback(null, new self.models.Server(self, result)); + } + + result = result || {}; + result.hostname = publicIP.properties.dnsSettings.fqdn; + + return callback(null, new self.models.Server(self, result)); + }); + }); + }); + }); +} + +/** + * Creates a server with the specified options + * + * @description The flavor + * properties of the options can be instances of Flavor + * OR ids to those entities in Azure. + * + * @param {object} options - **Optional** options + * @param {string} options.name - **Optional** the name of server + * @param {function} callback cb(err, server). + */ +function createServer(options, callback) { + var self = this; + + if (!options.name || !options.username || !options.password) { + return errs.handle( + errs.create({ + message: 'Please provide a name for the vm, as well as the username and password for login' + }), + callback + ); + } + + if (!options.flavor) { + return errs.handle( + errs.create({ + message: 'When creating an azure server a flavor or an image need to be supplied' + }), + callback + ); + } + + var adjustVMTemplate = function (template) { + + var vmIndex = _.findIndex(template.resources, { + 'type': 'Microsoft.Compute/virtualMachines' + }); + + // Adding additional data disks + if (options.storageDataDiskNames && options.storageDataDiskNames.length) { + options.storageDataDiskNames.forEach(function (ddName, idx) { + template.resources[vmIndex].properties.storageProfile.dataDisks.push({ + 'name': 'datadisk' + idx.toString(), + 'diskSizeGB': '100', + 'lun': 0, + 'vhd': { + 'uri': '[concat(reference(concat(\'Microsoft.Storage/storageAccounts/\', variables(\'storageAccountName\')), \'2016-01-01\').primaryEndpoints.blob, parameters(\'storageContainerName\'),\'/\', \'' + ddName + '\', \'.vhd\')]' + }, + 'createOption': 'Empty' + }); + }); + } + + // If this is a windows machine, add an extension that enables ssh connection via Win32-OpenSSH + if (options.osType === 'Windows') { + template.resources[vmIndex].resources = [{ + 'type': 'Microsoft.Compute/virtualMachines/extensions', + 'name': '[concat(variables(\'vmName\'),\'/Win32sshExtension\')]', + 'apiVersion': constants.DEFAULT_API_VERSION, + 'location': '[resourceGroup().location]', + 'dependsOn': [ + '[concat(\'Microsoft.Compute/virtualMachines/\', variables(\'vmName\'))]' + ], + 'properties': { + 'publisher': 'Microsoft.Compute', + 'type': 'CustomScriptExtension', + 'typeHandlerVersion': '1.8', + 'settings': { + 'fileUris': ["https://raw.githubusercontent.com/CatalystCode/pkgcloud/master/lib/pkgcloud/azure-v2/scripts/ssh.ps1"], + 'commandToExecute': `powershell -File ssh.ps1 .\\${options.username} ${options.password}` + }, + } + }]; + } else { // linux - make sure the new user is in sudoers - so he can sudo with no password + template.resources[vmIndex].resources = [{ + 'type': 'Microsoft.Compute/virtualMachines/extensions', + 'name': '[concat(variables(\'vmName\'),\'/LinuxSudoExtension\')]', + 'apiVersion': constants.DEFAULT_API_VERSION, + 'location': '[resourceGroup().location]', + 'dependsOn': [ + '[concat(\'Microsoft.Compute/virtualMachines/\', variables(\'vmName\'))]' + ], + 'properties': { + 'publisher': 'Microsoft.OSTCExtensions', + 'type': 'CustomScriptForLinux', + 'typeHandlerVersion': '1.5', + 'settings': { + 'fileUris': ["https://raw.githubusercontent.com/CatalystCode/pkgcloud/master/lib/pkgcloud/azure-v2/scripts/sudo.sh"], + 'commandToExecute': 'bash sudo.sh ' + options.username + }, + } + }]; + } + + return template; + }; + + var templateName = 'compute' + (options.imageSourceUri ? '-from-image' : ''); + self.deploy(templateName, options, adjustVMTemplate, function (err) { + return err ? + callback(err) : + self.getServer(options.name, callback); + }); +} + +/** + * Destroy a server in Azure. + * @param {Server|string} server Server id or a server + * @param {object} options optional | options for deletion + * @param {boolean} options.destroyNics should destroy nics also + * @param {boolean} options.destroyPublicIP should destroy public ip also + * @param {boolean} options.destroyVnet should destroy vnet also + * @param {boolean} options.destroyStorage should destroy storage account also + * @param {function} callback cb(err, serverId). + */ +function destroyServer(server, options, callback) { + var self = this; + var serverId = server && server.name || server; + + if (typeof options === 'function' && typeof callback === 'undefined') { + callback = options; + options = {}; + } + + options = options || {}; + + var resourceClient; + var serverDetails; + var nicsIds; + var nicsDetails; + + var vnets; + var publicIPs; + + async.waterfall([ + function (next) { + self.login(next); + }, + function (credentials, next) { + self.getServer(serverId, next); + }, + function (_server, next) { + serverDetails = _server; + next(); + }, + function (next) { + // Deleting the vm + resourceClient = new resourceManagement.ResourceManagementClient(self.azure.credentials, self.config.subscriptionId); + var client = new ComputeManagementClient(self.azure.credentials, self.config.subscriptionId); + client.virtualMachines.deleteMethod(self.config.resourceGroup, serverId, next); + } + ], function (err) { + + if (err) { + return callback(err); + } + + if (!options.destroyNics && + !options.destroyPublicIP && + !options.destroyVnet && + !options.destroyStorage) { + return callback(); + } + + async.waterfall([ + function (next) { + // Deleting the nics + nicsIds = serverDetails && + serverDetails.azure && + serverDetails.azure.networkProfile && + serverDetails.azure.networkProfile.networkInterfaces || []; + + // Go over all nics, get their details and go on to delete them + async.eachSeries(nicsIds, function (nic, cb) { + + nicsDetails = []; + async.waterfall([ + function (nx) { + resourceClient.resources.getById(nic.id, constants.MANAGEMENT_API_VERSION, nx); + }, + function (nicDetails, request, response, nx) { + nicsDetails.push(nicDetails); + + if (options.destroyNics) { + resourceClient.resources.deleteById(nic.id, constants.MANAGEMENT_API_VERSION, nx); + } + } + ], cb); + + }, next); + }, + function (next) { + // Collecting public ips and vnet ids + publicIPs = []; + vnets = []; + nicsDetails.forEach(function (nic) { + + var configs = nic && nic.properties && nic.properties.ipConfigurations || []; + + // Collecting + configs.forEach(function (config) { + var props = config && config.properties || {}; + if (props.publicIPAddress && props.publicIPAddress.id) { + publicIPs.push(props.publicIPAddress.id); + } + + if (props.subnet && props.subnet.id && props.subnet.id.indexOf('/subnets/') >= 0) { + vnets.push(props.subnet.id.substr(0, props.subnet.id.indexOf('/subnets/'))); + } + }); + + }); + next(); + }, + function (next) { + + if (!options.destroyPublicIP) { + return next(); + } + + // Deleting public ips + async.eachSeries(publicIPs, function (publicIP, cb) { + resourceClient.resources.deleteById(publicIP, constants.MANAGEMENT_API_VERSION, cb); + }, next); + }, + function (next) { + + if (!options.destroyVnet) { + return next(); + } + + // Deleting vnets + async.eachSeries(vnets, function (vnet, cb) { + resourceClient.resources.deleteById(vnet, constants.MANAGEMENT_API_VERSION, cb); + }, next); + }, + function (next) { + // Deleting storage account + if (!options.destroyStorage) { + return next(); + } + + var storageUri = serverDetails && + serverDetails.azure && + serverDetails.azure.storageProfile && + serverDetails.azure.storageProfile.osDisk && + serverDetails.azure.storageProfile.osDisk.vhd && + serverDetails.azure.storageProfile.osDisk.vhd.uri || null; + + if (!storageUri || !storageUri.startsWith('https://')) { + return next(); + } + + var storageName = storageUri.substr('https://'.length); + storageName = storageName.substr(0, storageName.indexOf('.')); + + // Presuming the storage account is in the same resource group as the vm + resourceClient.resources.deleteMethod( + self.config.resourceGroup, + 'Microsoft.Storage', + 'storageAccounts', + storageName, + '', '2016-01-01', next); + } + ], function (error) { + callback(error, serverDetails); + }); + }); + +} + +/** + * Stop a server in Azure. + * @param {Server|string} server Server id or a server + * @param {function} callback cb(err, serverId). + */ +function stopServer(server, callback) { + var self = this; + var serverId = server instanceof self.models.Server ? server.id : server; + + self.login(function (err) { + + if (err) { + return callback(err); + } + + var client = new ComputeManagementClient(self.azure.credentials, self.config.subscriptionId); + client.virtualMachines.powerOff(self.config.resourceGroup, serverId, function (err) { + return err ? + callback(err) : + callback(null, serverId); + }); + }); +} + +/** + * Restart a server in Azure. + * @param {Server|string} server Server id or a server + * @param {function} callback cb(err, serverId). + */ +function rebootServer(server, callback) { + var self = this; + var serverId = server instanceof self.models.Server ? server.id : server; + + self.login(function (err) { + + if (err) { + return callback(err); + } + + var client = new ComputeManagementClient(self.azure.credentials, self.config.subscriptionId); + client.virtualMachines.restart(self.config.resourceGroup, serverId, function (err) { + return err ? + callback(err) : + callback(null, serverId); + }); + }); +} + +/** + * Rename a server in Azure. + * @param {Server|string} server Server id or a server + * @param {function} callback cb(err, serverId). + */ +function renameServer(server, callback) { + return errs.handle( + errs.create({ + message: 'Not supported by Azure.' + }), + callback + ); +} + +module.exports = { + getVersion: getVersion, + getLimits: getLimits, + getServers: getServers, + getServer: getServer, + createServer: createServer, + destroyServer: destroyServer, + stopServer: stopServer, + rebootServer: rebootServer, + renameServer: renameServer +}; \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/compute/flavor.js b/lib/pkgcloud/azure-v2/compute/flavor.js new file mode 100644 index 000000000..bd9d8cb71 --- /dev/null +++ b/lib/pkgcloud/azure-v2/compute/flavor.js @@ -0,0 +1,36 @@ +/* + * flavor.js: Azure Cloud Package flavors + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +var util = require('util'), + base = require('../../core/compute/flavor'); + +var Flavor = exports.Flavor = function Flavor(client, details) { + base.Flavor.call(this, client, details); +}; + +util.inherits(Flavor, base.Flavor); + +/** + * Assign parameters for size specifications according to azure API + * @param {object} details + * @param {number} details.maxDataDiskCount + * @param {number} details.memoryInMB + * @param {string} details.name + * @param {number} details.numberOfCores + * @param {number} details.osDiskSizeInMB + * @param {number} details.resourceDiskSizeInMB + * + * Todo: Make sure paramters are assigned correctly + */ +Flavor.prototype._setProperties = function (details) { + var id = details.name; + + this.id = id; + this.name = id; + this.ram = details.memoryInMB * 1024; + this.disk = details.maxDataDiskCount; +}; diff --git a/lib/pkgcloud/azure-v2/compute/image.js b/lib/pkgcloud/azure-v2/compute/image.js new file mode 100644 index 000000000..3398c8d4e --- /dev/null +++ b/lib/pkgcloud/azure-v2/compute/image.js @@ -0,0 +1,27 @@ +/* + * image.js: Azure OS Images + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +var util = require('util'), + base = require('../../core/compute/image'); + +var Image = exports.Image = function Image(client, details, publisher, offer, sku, version) { + base.Image.call(this, client, details, publisher, offer, sku, version); +}; + +util.inherits(Image, base.Image); + +Image.prototype._setProperties = function (details, publisher, offer, sku, version) { + this.id = details.id; + this.name = details.name; + this.location = details.location; + this.publisher = publisher; + this.offer = offer; + this.sku = sku; + this.version = version; + this.created = new Date(0); + this.details = this.azure = details; +}; diff --git a/lib/pkgcloud/azure-v2/compute/index.js b/lib/pkgcloud/azure-v2/compute/index.js new file mode 100644 index 000000000..ee51ee171 --- /dev/null +++ b/lib/pkgcloud/azure-v2/compute/index.js @@ -0,0 +1,15 @@ +/* + * index.js: Top-level include for the Azure compute module + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +exports.Client = require('./client').Client; +exports.Flavor = require('./flavor').Flavor; +exports.Image = require('./image').Image; +exports.Server = require('./server').Server; + +exports.createClient = function (options) { + return new exports.Client(options); +}; diff --git a/lib/pkgcloud/azure-v2/compute/server.js b/lib/pkgcloud/azure-v2/compute/server.js new file mode 100644 index 000000000..20d674502 --- /dev/null +++ b/lib/pkgcloud/azure-v2/compute/server.js @@ -0,0 +1,68 @@ +/* + * server.js: Azure Server + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +var util = require('util'); +var base = require('../../core/compute/server'); +var _ = require('lodash'); + +var Server = exports.Server = function Server(client, details) { + base.Server.call(this, client, details); + this.requestPending = false; +}; + +util.inherits(Server, base.Server); + +Server.prototype._setProperties = function (details) { + + details = details || {}; + this.id = details.id || ''; + this.name = details.name || ''; + this.location = details.location; + this.hostname = details.hostname || ''; + + //console.log('Status: ' + details.Status + ' RoleInstanceList: ' + roleInstance ? roleInstance.InstanceStatus : 'UNKNOWN'); + + // azure can return an inconsistent RoleInstance status (not in azure rest api docs) so we check everything. + // an azure vm has a complicated state machine. We need to check the status of both the deployment and the role. + // azure first starts a deployment and then starts a role. The role seems to go through STOPPEDVM, PROVISIONING and then + // READYROLE. + // Note: since azureAPI has to wait until azure responds to our createServer request, we most likely will miss all of the + // deployment states unless something goes wrong + // TODO: there doesn't seem to be an ERROR or FAIL status in pkgcloud + + var statuses = details.instanceView && details.instanceView.statuses || []; + var provisioningStatus = _.find(statuses, function (status) { + return status.code.startsWith('ProvisioningState/'); + }) || {}; + var powerStateStatus = _.find(statuses, function (status) { + return status.code.startsWith('PowerState/'); + }) || {}; + + // Azure ARM VMs are natively constructed out of a collection of roles. + if (provisioningStatus.code == 'ProvisioningState/succeeded' && powerStateStatus.code == 'PowerState/deallocated') { + this.status = this.STATUS.stopped; + } else if (provisioningStatus.code == 'ProvisioningState/succeeded' && powerStateStatus.code == 'PowerState/running') { + this.status = this.STATUS.running; + } else { + this.status = this.STATUS.unknown; + } + + var addresses = { private: [], public: [] }; + + // TODO: Need to clean up once I understand what is private ip? + this.addresses = details.addresses = addresses; + + if (details.RoleList && details.RoleList.Role) { + if (details.RoleList.Role.OSVirtualHardDisk) { + this.imageId = details.RoleList.Role.OSVirtualHardDisk.SourceImageName; + } + } + + this.serviceName = details.serviceName || details.Name; + + this.original = this.azure = details; +}; diff --git a/lib/pkgcloud/azure-v2/constants.js b/lib/pkgcloud/azure-v2/constants.js new file mode 100644 index 000000000..49ee5bd53 --- /dev/null +++ b/lib/pkgcloud/azure-v2/constants.js @@ -0,0 +1,39 @@ +/** +* Copyright (c) Microsoft. 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 Constants = { + /** Uri endpoint for accessing blob storage */ + MANAGEMENT_API_VERSION: '2016-03-30', + /** Endpoint for azure management */ + MANAGEMENT_ENDPOINT: 'management.core.windows.net', + /** Uri endpoint for accessing blob storage */ + STORAGE_URI_SUFFIX: 'blob.core.windows.net', + /** Azure credentials refresh rate in milliseconds */ + CREDENTIALS_LIFESPAN: 5000, + /** default api version when querying ARM resrouces */ + DEFAULT_API_VERSION: '2016-03-30', + /** Default size for new storage account */ + DEFAULT_STORAGE_SKU: 'Standard_LRS', + /** Default container to work with when none is specified */ + DEFAULT_STORAGE_CONTAINER: 'pkgcloud-container', + /** Default Image details when creating VM from image */ + DEFAULT_VM_IMAGE: { + PUBLISHER: 'MicrosoftWindowsServer', + OFFER: 'WindowsServer', + SKU: '2012-R2-Datacenter' + } +}; + +module.exports = Constants; \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/index.js b/lib/pkgcloud/azure-v2/index.js new file mode 100644 index 000000000..7da97c062 --- /dev/null +++ b/lib/pkgcloud/azure-v2/index.js @@ -0,0 +1,9 @@ +/* + * index.js: Top-level include for the Azure module. + * + * (C) Microsoft Open Technologies, Inc. All rights reserved. + * + */ + +exports.compute = require('./compute'); +exports.storage = require('./storage'); diff --git a/lib/pkgcloud/azure-v2/scripts/ssh.ps1 b/lib/pkgcloud/azure-v2/scripts/ssh.ps1 new file mode 100644 index 000000000..1c9e702cb --- /dev/null +++ b/lib/pkgcloud/azure-v2/scripts/ssh.ps1 @@ -0,0 +1,8 @@ +$username = $args[0] +$password = $args[1] +$securePassword = ConvertTo-SecureString $password -AsPlainText -Force +$credential = New-Object System.Management.Automation.PSCredential $username, $securePassword +Enable-PSRemoting -Force +$scriptPath = ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/DarwinJS/ChocoPackages/master/openssh/InstallChoco_and_win32-openssh_with_server.ps1')) +Invoke-Command -ScriptBlock ([scriptblock]::Create($scriptPath)) -Credential $credential -ComputerName localhost +Disable-PSRemoting -Force \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/scripts/sudo.sh b/lib/pkgcloud/azure-v2/scripts/sudo.sh new file mode 100644 index 000000000..430aee644 --- /dev/null +++ b/lib/pkgcloud/azure-v2/scripts/sudo.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Make sure you use a username that is lowercase. +USERNAME=$1 +echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/storage/client/containers.js b/lib/pkgcloud/azure-v2/storage/client/containers.js new file mode 100644 index 000000000..b04881415 --- /dev/null +++ b/lib/pkgcloud/azure-v2/storage/client/containers.js @@ -0,0 +1,182 @@ +/* + * containers.js: Instance methods for working with containers from Azure + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +var StorageManagementClient = require('azure-arm-storage'); +var azureStorage = require('azure-storage'); + +var constants = require('../../constants'); + +/** + * list a collection of storage accounts under a resource group + * @param {function} callback + */ +function getContainers(callback) { + var self = this; + + self.login(function (err) { + + if (err) { + return callback(err); + } + + var storageClient = new StorageManagementClient(self.azure.credentials, self.config.subscriptionId); + storageClient.storageAccounts.listByResourceGroup(self.config.resourceGroup, function (err, results) { + return err + ? callback(err) + : callback(null, results.map(function (res) { + return new self.models.Container(self, res); + })); + }); + }); +} + +/** + * Responds with the azure stoarge account with the given name + * @param {string|storage.Container} container - container name of container configuration + * @param {function} callback - Continuation to respond to when complete. + */ +function getContainer(container, callback) { + var self = this; + var containerName = container instanceof self.models.Container ? container.name : container; + + self.login(function (err) { + + if (err) { + return callback(err); + } + + var storageClient = new StorageManagementClient(self.azure.credentials, self.config.subscriptionId); + storageClient.storageAccounts.getProperties(self.config.resourceGroup, containerName, function (err, result) { + return err + ? callback(err) + : callback(null, new self.models.Container(self, result)); + }); + }); +} + +/** + * Create a new storage account + * @param {string|object} options - container name of container configuration + * @param {string} options.name - storage account name + * @param {string} options.type - storage account type + * @param {function} callback - Continuation to respond to when complete. + * + * From Azure docs: + * A container that was recently deleted cannot be recreated until all of + * its blobs are deleted. Depending on how much data was stored within the container, + * complete deletion can take seconds or minutes. If you try to create a container + * of the same name during this cleanup period, your call returns an error immediately. + */ +function createContainer(options, callback) { + var self = this; + var containerName = options instanceof self.models.Container ? options.name : options; + var parameters = typeof options == 'string' ? { name: options } : options; + + self.deploy('storage', parameters, function (err) { + return err ? + callback(err) : + self.getContainer(containerName, callback); + }); +} + +/** + * Destroy a new storage account + * @param {string|storage.Container} container - container name of container configuration + * @param {function} callback - Continuation to respond to when complete. + * + * From Azure docs: + * A container that was recently deleted cannot be recreated until all of + * its blobs are deleted. Depending on how much data was stored within the container, + * complete deletion can take seconds or minutes. If you try to create a container + * of the same name during this cleanup period, your call returns an error immediately. + */ +function destroyContainer(container, callback) { + var self = this; + var containerName = container instanceof self.models.Container ? container.name : container; + + self.login(function (err) { + + if (err) { + return callback(err); + } + + var storageClient = new StorageManagementClient(self.azure.credentials, self.config.subscriptionId); + storageClient.storageAccounts.deleteMethod(self.config.resourceGroup, containerName, callback); + }); +} + +function listContainerKeys(container, callback) { + var self = this; + var containerName = container instanceof self.models.Container ? container.name : container; + + self.login(function (err) { + + if (err) { + return callback(err); + } + + var storageClient = new StorageManagementClient(self.azure.credentials, self.config.subscriptionId); + storageClient.storageAccounts.listKeys(self.config.resourceGroup, containerName, function (err, result) { + return err + ? callback(err) + : callback(null, result); + }); + }); +} + +function getContainerKey (container, callback) { + var self = this; + var containerName = container instanceof self.models.Container ? container.name : container; + + self.azure = self.azure || {}; + self.azure.storageKeys = self.azure.storageKeys || {}; + + if (self.azure.storageKeys[containerName]) { + return callback(null, self.azure.storageKeys[containerName]); + } + + self.listContainerKeys(container, function (err, result) { + if (err) { + return callback(err); + } + + var key = result.keys[0].value; + self.azure.storageKeys[containerName] = key; + return callback(null, key); + }); +} + +function getBlobService(options, storageAccountName, callback) { + var self = this; + options = options || {}; + var azureContainer = typeof options == 'string' ? options : (options.container || constants.DEFAULT_STORAGE_CONTAINER); + + self.getContainerKey(storageAccountName, function (err, containerKey) { + if (err) { + return callback(err); + } + + var retryOperations = new azureStorage.ExponentialRetryPolicyFilter(); + var blobService = azureStorage.createBlobService(storageAccountName, containerKey).withFilter(retryOperations); + blobService.createContainerIfNotExists(azureContainer, null, function(error) { + return error ? + callback(error) : + callback(null, blobService); + }); + }); +} + + +module.exports = { + getBlobService: getBlobService, + getContainers: getContainers, + getContainer: getContainer, + createContainer: createContainer, + destroyContainer: destroyContainer, + listContainerKeys: listContainerKeys, + getContainerKey: getContainerKey +}; \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/storage/client/files.js b/lib/pkgcloud/azure-v2/storage/client/files.js new file mode 100644 index 000000000..d2eefef09 --- /dev/null +++ b/lib/pkgcloud/azure-v2/storage/client/files.js @@ -0,0 +1,304 @@ +/* + * files.js: Instance methods for working with files for Openstack Object Storage + * + * (C) 2013 Rackspace, Ken Perkins + * MIT LICENSE + * + */ + +var async = require('async'); +var filed = require('filed'); +var mime = require('mime'); +var through = require('through2'); +var _ = require('lodash'); +var urlJoin = require('url-join'); + +var constants = require('../../constants'); + +/** + * client.removeFile + * + * @description remove a file from a container + * + * @param {String|object} container the container or containerName + * @param {String|object} file the file or fileName to delete + * @param callback + */ +exports.removeFile = function (container, file, options, callback) { + options = options || {}; + var containerName = container instanceof this.models.Container ? container.name : container; + var fileName = file instanceof this.models.File ? file.name : file; + var azureContainerName = options.storage && options.storage.container || constants.DEFAULT_STORAGE_CONTAINER; + + this.getBlobService(azureContainerName, containerName, function (err, blobService) { + + if (err) { + return callback(err); + } + + blobService.deleteBlob(azureContainerName, fileName, function (err) { + return err ? + callback(err) : + callback(null, true); + }); + }); +}; + +/** + * client.bulkDelete + * + * @description remove a list of files from a container + * + * @param {String|object} container the container or containerName + * @param {array} files the files or fileNames to delete + * @param callback + */ +exports.TODO_bulkDelete = function(container, files, callback) { + var self = this, + containerName = container instanceof this.models.Container ? container.name : container; + this._request({ + method: 'DELETE', + body: files.map(function(file) { + return urlJoin(containerName, (file instanceof self.models.File ? file.name : file)); + }).join('\r\n'), + headers: { + 'Content-Type': 'text/plain' + }, + qs: { + 'bulk-delete': true + } + }, function(err, results) { + return err + ? callback(err) + : callback(null, results); + }); +}; + +/** + * client.upload + * + * @description upload a new file to a container. + * Returns the pipe interface so you can call: + * + * request('http://some.com/file.txt').pipe(client.upload(options)); + * + * @param {object} options + * @param {String|object} options.container the container to store the file in + * @param {String} options.remote the file name for the new file + * @param {String} [options.local] an optional local file path to upload + * @param {Stream} [options.stream] optionally explicitly provide the stream instead of pipe + * @param {object} [options.headers] optionally provide headers for the call + * @param {object} [options.metadata] optionally provide metadata for the object + * @param callback + * @returns {request|*} + */ +exports.TODO_upload = function (options) { + var self = this; + + // check for deprecated calling with a callback + if (typeof arguments[arguments.length - 1] === 'function') { + self.emit('log::warn', 'storage.upload no longer supports calling with a callback'); + } + + var container = options.container, + writableStream, + proxyStream = through(), + uploadOptions = { + method: 'PUT', + upload: true, + container: container, + path: options.remote, + headers: options.headers || {} + }; + + if (options.container instanceof this.models.Container) { + uploadOptions.container = options.container.name; + } + + if (options.contentType) { + uploadOptions.headers['content-type'] = options.contentType; + } + else { + uploadOptions.headers['content-type'] = mime.lookup(options.remote); + } + + if (options.metadata) { + uploadOptions.headers = _.extend(uploadOptions.headers, + self.serializeMetadata(self.OBJECT_META_PREFIX, options.metadata)); + } + + writableStream = this._request(uploadOptions); + + writableStream.on('complete', function(response) { + var err = self._parseError(response); + + if (err) { + proxyStream.emit('error', err); + return; + } + + // load the file metadata from the cloud, so we can return a proper model + self.getFile(uploadOptions.container, options.remote, function (err, file) { + if (err) { + proxyStream.emit('error', err); + return; + } + + proxyStream.emit('success', file); + }); + }); + + writableStream.on('error', function (err) { + proxyStream.emit('error', err); + }); + + writableStream.on('data', function (chunk) { + proxyStream.emit('data', chunk); + }); + + // we need a proxy stream so we can always return a file model + // via the 'success' event + proxyStream.pipe(writableStream); + + return proxyStream; +}; + + +/** + * client.getFiles + * + * @description get the list of files in a container. Returns at most 10,000 files if options.limit is unspecified. + * Abstracts the aggregation of files in the case that options.limit is >10,000. + * + * @param {String|object} container the container or containerName + * @param {object|Function} options + * @param {Number} [options.limit] the number of records to return + * @param {String} [options.marker] the id of the first record to return in the current query + * @param {Function} callback + */ +exports.getFiles = function (container, options, callback) { + + var self = this; + options = options || {}; + var containerName = container instanceof this.models.Container ? container.name : container; + var azureContainer = options.container || constants.DEFAULT_STORAGE_CONTAINER; + var blobs = []; + + self.getBlobService(azureContainer, containerName, function (err, blobService) { + + if (err) { + return callback(err); + } + + var aggregateBlobs = function (err, result, cb) { + if (err) { + cb(err); + } else { + blobs = blobs.concat(result.entries); + if (result.continuationToken !== null) { + blobService.listBlobsSegmented(azureContainer, result.continuationToken, aggregateBlobs); + } else { + cb(null, blobs); + } + } + }; + + blobService.listBlobsSegmented(azureContainer, null, function(err, result) { + aggregateBlobs(err, result, function (err, blobs) { + return err ? + callback(err) : + callback(null, blobs.map(function (blob) { + return new self.models.File(self, blob); + })); + }); + }); + + }); +}; + +/** + * client.getFile + * + * @description get the details for a specific file + * + * @param {String|object} container the container or containerName + * @param {String|object} file the file or fileName to get details for + * @param callback + */ +exports.getFile = function (container, file, options, callback) { + + var self = this; + options = options || {}; + var containerName = container instanceof this.models.Container ? container.name : container; + var azureContainerName = options.storage && options.storage.container || constants.DEFAULT_STORAGE_CONTAINER; + var fileName = file instanceof this.models.File ? file.name : file; + + self.getBlobService(azureContainerName, containerName, function (err, blobService) { + + if (err) { + return callback(err); + } + + blobService.getBlobProperties(azureContainerName, fileName, function (err, properties, status) { + return err ? + callback(err) : (!status || !status.isSuccessful) ? + callback(new Error('status is not successfull: ' + JSON.stringify(status || null))) : + callback(null, new self.models.File(self, properties)); + }); + }); +}; + +/** + * client.download + * + * @description download a file from a container + * Returns the pipe interface so you can call: + * + * client.download(options).pipe(fs.createWriteStream(options2)); + * + * @param {object} options + * @param {String|object} options.container the container to store the file in + * @param {String} options.remote the file name for the new file + * @param {String} [options.local] an optional local file path to download to + * @param {Stream} [options.stream] optionally explicitly provide the stream instead of pipe + * @param callback + * @returns {request|*} + */ +exports.download = function (options, callback) { + var self = this; + var container = options.container; + var containerName = container instanceof this.models.Container ? container.name : container; + var azureContainerName = options.storage && options.storage.container || constants.DEFAULT_STORAGE_CONTAINER; + var blobName = options.remote instanceof this.models.File ? options.remote.name : options.remote; + var inputStream; + + if (options.local) { + inputStream = filed(options.local); + } + else if (options.stream) { + inputStream = options.stream; + } + + var blobService; + async.waterfall([ + function (next) { + self.getBlobService(azureContainerName, containerName, next); + }, + function (_blobService, next) { + + blobService = _blobService; + blobService.getBlobProperties(azureContainerName, blobName, function (err, properties, status) { + return err ? + next(err) : (!status || !status.isSuccessful) ? + next(new Error('status is not successfull: ' + JSON.stringify(status || null))) : + next(null, properties); + }); + }, + function (properties, next) { + blobService.createReadStream(azureContainerName, blobName).pipe(inputStream); + return next(); + } + ], callback); + + return inputStream; +}; \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/storage/client/index.js b/lib/pkgcloud/azure-v2/storage/client/index.js new file mode 100644 index 000000000..5b66a7137 --- /dev/null +++ b/lib/pkgcloud/azure-v2/storage/client/index.js @@ -0,0 +1,23 @@ +/* + * client.js: Storage client for Azure + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +var util = require('util'); +var azure = require('../../client'); +var _ = require('lodash'); + +var Client = exports.Client = function (options) { + azure.Client.call(this, options); + + this.models = this.models || {}; + this.models.Container = require('../container').Container; + this.models.File = require('../file').File; + + _.extend(this, require('./containers')); + _.extend(this, require('./files')); +}; + +util.inherits(Client, azure.Client); \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/storage/container.js b/lib/pkgcloud/azure-v2/storage/container.js new file mode 100644 index 000000000..b20825035 --- /dev/null +++ b/lib/pkgcloud/azure-v2/storage/container.js @@ -0,0 +1,35 @@ +/* + * container.js: Azure container + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +var util = require('util'), + _ = require('lodash'), + base = require('../../core/storage/container'); + +var Container = exports.Container = function Container(client, details) { + base.Container.call(this, client, details); +}; + +util.inherits(Container, base.Container); + +Container.prototype._setProperties = function (details) { + if (typeof details === 'string') { + this.name = details; + return; + } + + this.name = details.name; + + // + // Azure specific + // + this.original = this.azure = details; + +}; + +Container.prototype.toJSON = function () { + return _.pick(this, ['name']); +}; diff --git a/lib/pkgcloud/azure-v2/storage/file.js b/lib/pkgcloud/azure-v2/storage/file.js new file mode 100644 index 000000000..212249537 --- /dev/null +++ b/lib/pkgcloud/azure-v2/storage/file.js @@ -0,0 +1,28 @@ +/* + * container.js: Azure File (Blob) + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +var util = require('util'), + _ = require('lodash'), + base = require('../../core/storage/file'); + +var File = exports.File = function File(client, details) { + base.File.call(this, client, details); +}; + +util.inherits(File, base.File); + +File.prototype._setProperties = function (details) { + + this.name = details.name; + this.size = details.contentLength ? parseInt(details.contentLength, 10) : 0; + this.lastModified = new Date(details.lastModified || null); + this.contentType = details.contentSettings && details.contentSettings.contentType; +}; + +File.prototype.toJSON = function () { + return _.pick(this, ['name', 'size', 'lastModified', 'contentType' ]); +}; \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/storage/index.js b/lib/pkgcloud/azure-v2/storage/index.js new file mode 100644 index 000000000..a55e536d9 --- /dev/null +++ b/lib/pkgcloud/azure-v2/storage/index.js @@ -0,0 +1,15 @@ +/* + * index.js: Top-level include for the Azure module + * + * (C) Microsoft Open Technologies, Inc. + * + */ + +exports.Client = require('./client').Client; +exports.Container = require('./container').Container; +exports.File = require('./file').File; +//exports.ChunkedStream = require('./utils').ChunkedStream; + +exports.createClient = function (options) { + return new exports.Client(options); +}; diff --git a/lib/pkgcloud/azure-v2/templates/arm-compute-from-image.json b/lib/pkgcloud/azure-v2/templates/arm-compute-from-image.json new file mode 100644 index 000000000..4c2ce5743 --- /dev/null +++ b/lib/pkgcloud/azure-v2/templates/arm-compute-from-image.json @@ -0,0 +1,181 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { "type": "string" }, + "flavor": { "type": "string", "defaultValue": "Standard_A1" }, + "username": { "type": "string" }, + "password": { "type": "securestring" }, + + "imageSourceUri": { "type": "string" }, + "imageOS": { "type": "string", "defaultValue": "Linux" }, + + "storageAccountName": { "type": "string", "defaultValue": "_NONE_" }, + "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS" }, + "storageContainerName": { "type": "string", "defaultValue": "vhds" }, + "storageOSDiskName": { "type": "string", "defaultValue": "osdisk" }, + + "publicIPAddressName": { "type": "string", "defaultValue": "_NONE_" }, + "publicIPAddressType": { "type": "string", "defaultValue": "Dynamic" }, + "dnsLabelPrefix": { "type": "string", "defaultValue": "_NONE_"}, + + "vnetName": { "type": "string", "defaultValue": "_NONE_" }, + "vnetAddressPrefix": { "type": "string", "defaultValue": "10.0.0.0/16" }, + "vnetSubnetName": { "type": "string", "defaultValue": "Subnet" }, + "vnetSubnetPrefix": { "type": "string", "defaultValue": "10.0.0.0/24" }, + + "nicName": { "type": "string", "defaultValue": "_NONE_" } + }, + "variables": { + "vmName": "[parameters('name')]", + "vmSize": "[parameters('flavor')]", + + "storageAccountName": "[replace(parameters('storageAccountName'), '_NONE_', concat(replace(variables('vmName'), '-', ''), 'store'))]", + "publicIPAddressName": "[replace(parameters('publicIPAddressName'), '_NONE_', concat(variables('vmName'), '-public-ip'))]", + "dnsLabelPrefix": "[replace(parameters('dnsLabelPrefix'), '_NONE_', concat(variables('vmName'), '-vmdns'))]", + "vnetName": "[replace(parameters('vnetName'), '_NONE_', concat(variables('vmName'), '-vnet'))]", + "nicName": "[replace(parameters('nicName'), '_NONE_', concat(variables('vmName'), '-nic'))]", + + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('vnetName'))]", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',parameters('vnetSubnetName'))]", + "hostDNSNameScriptArgument": "[concat('*.',resourceGroup().location,'.cloudapp.azure.com')]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "apiVersion": "2016-01-01", + "location": "[resourceGroup().location]", + "sku": { + "name": "[parameters('storageAccountType')]" + }, + "kind": "Storage", + "properties": {} + }, + { + "apiVersion": "2016-09-01", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[variables('publicIPAddressName')]", + "location": "[resourceGroup().location]", + "properties": { + "publicIPAllocationMethod": "[parameters('publicIPAddressType')]", + "dnsSettings": { + "domainNameLabel": "[variables('dnsLabelPrefix')]" + } + } + }, + { + "apiVersion": "2016-09-01", + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('vnetName')]", + "location": "[resourceGroup().location]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[parameters('vnetAddressPrefix')]" + ] + }, + "subnets": [ + { + "name": "[parameters('vnetSubnetName')]", + "properties": { + "addressPrefix": "[parameters('vnetSubnetPrefix')]" + } + } + ] + } + }, + { + "apiVersion": "2016-09-01", + "type": "Microsoft.Network/networkInterfaces", + "name": "[variables('nicName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]", + "[resourceId('Microsoft.Network/virtualNetworks/', variables('vnetName'))]" + ], + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + } + }, + { + "apiVersion": "2016-03-30", + "type": "Microsoft.Compute/virtualMachines", + "name": "[variables('vmName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", + "[resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[variables('vmSize')]" + }, + "osProfile": { + "computerName": "[variables('vmName')]", + "adminUsername": "[parameters('username')]", + "adminPassword": "[parameters('password')]" + }, + "storageProfile": { + "osDisk": { + "name": "osdisk", + "osType": "[parameters('imageOS')]", + "caching": "ReadWrite", + "createOption": "FromImage", + "image": { + "uri": "[parameters('imageSourceUri')]" + }, + "vhd": { + "uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2016-01-01').primaryEndpoints.blob, parameters('storageContainerName'),'/',parameters('storageOSDiskName'),'.vhd')]" + } + }, + "dataDisks": [] + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" + } + ] + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": "true", + "storageUri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2016-01-01').primaryEndpoints.blob)]" + } + } + } + } + ], + "outputs": { + "hostname": { + "type": "string", + "value": "[reference(variables('publicIPAddressName')).dnsSettings.fqdn]" + }, + "sshCommand": { + "type": "string", + "value": "[concat('ssh ', parameters('username'), '@', reference(variables('publicIPAddressName')).dnsSettings.fqdn)]" + }, + "location": { + "type": "string", + "value": "[resourceGroup().location]" + }, + "vmID": { + "type": "string", + "value": "[resourceId('Microsoft.Compute/virtualMachines/', variables('vmName'))]" + } + } +} \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/templates/arm-compute.json b/lib/pkgcloud/azure-v2/templates/arm-compute.json new file mode 100644 index 000000000..990ae8c9a --- /dev/null +++ b/lib/pkgcloud/azure-v2/templates/arm-compute.json @@ -0,0 +1,185 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { "type": "string" }, + "flavor": { "type": "string", "defaultValue": "Standard_A1" }, + "username": { "type": "string" }, + "password": { "type": "securestring" }, + + "storageAccountName": { "type": "string", "defaultValue": "_NONE_" }, + "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS" }, + "storageContainerName": { "type": "string", "defaultValue": "vhds" }, + "storageOSDiskName": { "type": "string", "defaultValue": "osdisk" }, + + "publicIPAddressName": { "type": "string", "defaultValue": "_NONE_" }, + "publicIPAddressType": { "type": "string", "defaultValue": "Dynamic" }, + "dnsLabelPrefix": { "type": "string", "defaultValue": "_NONE_"}, + + "vnetName": { "type": "string", "defaultValue": "_NONE_" }, + "vnetAddressPrefix": { "type": "string", "defaultValue": "10.0.0.0/16" }, + "vnetSubnetName": { "type": "string", "defaultValue": "Subnet" }, + "vnetSubnetPrefix": { "type": "string", "defaultValue": "10.0.0.0/24" }, + + "nicName": { "type": "string", "defaultValue": "_NONE_" }, + + "imagePublisher": { "type": "string", "defaultValue": "Canonical" }, + "imageOffer": { "type": "string", "defaultValue": "UbuntuServer" }, + "imageSku": { "type": "string", "defaultValue": "16.04.0-LTS" }, + "imageVersion": { "type": "string", "defaultValue": "latest" } + }, + "variables": { + "vmName": "[parameters('name')]", + "vmSize": "[parameters('flavor')]", + + "storageAccountName": "[replace(parameters('storageAccountName'), '_NONE_', concat(replace(variables('vmName'), '-', ''), 'store'))]", + "publicIPAddressName": "[replace(parameters('publicIPAddressName'), '_NONE_', concat(variables('vmName'), '-public-ip'))]", + "dnsLabelPrefix": "[replace(parameters('dnsLabelPrefix'), '_NONE_', concat(variables('vmName'), '-vmdns'))]", + "vnetName": "[replace(parameters('vnetName'), '_NONE_', concat(variables('vmName'), '-vnet'))]", + "nicName": "[replace(parameters('nicName'), '_NONE_', concat(variables('vmName'), '-nic'))]", + + "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('vnetName'))]", + "subnetRef": "[concat(variables('vnetID'),'/subnets/',parameters('vnetSubnetName'))]", + "hostDNSNameScriptArgument": "[concat('*.',resourceGroup().location,'.cloudapp.azure.com')]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "apiVersion": "2016-01-01", + "location": "[resourceGroup().location]", + "sku": { + "name": "[parameters('storageAccountType')]" + }, + "kind": "Storage", + "properties": {} + }, + { + "apiVersion": "2016-09-01", + "type": "Microsoft.Network/publicIPAddresses", + "name": "[variables('publicIPAddressName')]", + "location": "[resourceGroup().location]", + "properties": { + "publicIPAllocationMethod": "[parameters('publicIPAddressType')]", + "dnsSettings": { + "domainNameLabel": "[variables('dnsLabelPrefix')]" + } + } + }, + { + "apiVersion": "2016-09-01", + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('vnetName')]", + "location": "[resourceGroup().location]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[parameters('vnetAddressPrefix')]" + ] + }, + "subnets": [ + { + "name": "[parameters('vnetSubnetName')]", + "properties": { + "addressPrefix": "[parameters('vnetSubnetPrefix')]" + } + } + ] + } + }, + { + "apiVersion": "2016-09-01", + "type": "Microsoft.Network/networkInterfaces", + "name": "[variables('nicName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]", + "[resourceId('Microsoft.Network/virtualNetworks/', variables('vnetName'))]" + ], + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]" + }, + "subnet": { + "id": "[variables('subnetRef')]" + } + } + } + ] + } + }, + { + "apiVersion": "2016-03-30", + "type": "Microsoft.Compute/virtualMachines", + "name": "[variables('vmName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]", + "[resourceId('Microsoft.Network/networkInterfaces/', variables('nicName'))]" + ], + "properties": { + "hardwareProfile": { + "vmSize": "[variables('vmSize')]" + }, + "osProfile": { + "computerName": "[variables('vmName')]", + "adminUsername": "[parameters('username')]", + "adminPassword": "[parameters('password')]" + }, + "storageProfile": { + "imageReference": { + "publisher": "[parameters('imagePublisher')]", + "offer": "[parameters('imageOffer')]", + "sku": "[parameters('imageSku')]", + "version": "[parameters('imageVersion')]" + }, + "osDisk": { + "name": "osdisk", + "vhd": { + "uri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2016-01-01').primaryEndpoints.blob, parameters('storageContainerName'),'/',parameters('storageOSDiskName'),'.vhd')]" + }, + "caching": "ReadWrite", + "createOption": "FromImage" + }, + "dataDisks": [] + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]" + } + ] + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled": "true", + "storageUri": "[concat(reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2016-01-01').primaryEndpoints.blob)]" + } + } + } + } + ], + "outputs": { + "hostname": { + "type": "string", + "value": "[reference(variables('publicIPAddressName')).dnsSettings.fqdn]" + }, + "sshCommand": { + "type": "string", + "value": "[concat('ssh ', parameters('username'), '@', reference(variables('publicIPAddressName')).dnsSettings.fqdn)]" + }, + "location": { + "type": "string", + "value": "[resourceGroup().location]" + }, + "vmID": { + "type": "string", + "value": "[resourceId('Microsoft.Compute/virtualMachines/', variables('vmName'))]" + } + } +} \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/templates/arm-storage.json b/lib/pkgcloud/azure-v2/templates/arm-storage.json new file mode 100644 index 000000000..e05084559 --- /dev/null +++ b/lib/pkgcloud/azure-v2/templates/arm-storage.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { "type": "string" }, + "type": { "type": "string", "defaultValue": "Standard_LRS" } + }, + "variables": { }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[parameters('name')]", + "apiVersion": "2016-01-01", + "location": "[resourceGroup().location]", + "sku": { + "name": "[parameters('type')]" + }, + "kind": "Storage", + "properties": {} + } + ] +} \ No newline at end of file diff --git a/lib/pkgcloud/azure-v2/templates/index.js b/lib/pkgcloud/azure-v2/templates/index.js new file mode 100644 index 000000000..67e29ec38 --- /dev/null +++ b/lib/pkgcloud/azure-v2/templates/index.js @@ -0,0 +1,53 @@ +var path = require('path'); +var fs = require('fs'); + +var resourceManagement = require('azure-arm-resource'); + +function resolve(templateId) { + var templatePath = path.join(__dirname, '..', 'templates', 'arm-' + templateId + '.json'); + var contents = fs.readFileSync(templatePath); + return JSON.parse(contents); +} + +function deploy(templateName, options, templateProcess, callback) { + var self = this; + + if (templateProcess && !callback) { + callback = templateProcess; + templateProcess = function (template) { return template; }; + } + + self.login(function (err) { + + if (err) { + return callback(err); + } + + var template = resolve(templateName); + template = templateProcess(template); + var parameters = { + properties: { + template: template, + parameters: {}, + mode: 'Incremental' + } + }; + Object.keys(options).forEach(function (key) { + if (template.parameters[key]) { + parameters.properties.parameters[key] = { value: options[key] }; + } + }); + + var deploymentName = 'pkgc-' + (new Date()).toISOString().replace(/\:|Z|\.|\-/g, '').replace(/T/g, '-'); + var resourceClient = new resourceManagement.ResourceManagementClient(self.azure.credentials, self.config.subscriptionId); + resourceClient.deployments.createOrUpdate(self.config.resourceGroup, deploymentName, parameters, function (err, result) { + return err + ? callback(err) + : callback(null, result); + }); + }); +} + +module.exports = { + deploy: deploy +}; \ No newline at end of file diff --git a/lib/pkgcloud/azure/utils/azureApi.js b/lib/pkgcloud/azure/utils/azureApi.js index 8e3eb1adf..56285f9c7 100644 --- a/lib/pkgcloud/azure/utils/azureApi.js +++ b/lib/pkgcloud/azure/utils/azureApi.js @@ -20,7 +20,7 @@ var _ = require('lodash'); var errs = require('errs'); var URL = require('url'); var cert = require('../utils/cert'); -var pkgcloud = require('../../../../../pkgcloud'); +var pkgcloud = require('../../../../lib/pkgcloud'); exports.MANAGEMENT_API_VERSION = '2012-03-01'; exports.MANAGEMENT_ENDPOINT = 'management.core.windows.net'; diff --git a/package.json b/package.json index 87d9f7ac8..f3005ad10 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,10 @@ "dependencies": { "async": "0.9.x", "aws-sdk": "^2.2.43", + "azure-arm-compute": "^0.19.1", + "azure-arm-resource": "^1.6.1-preview", + "azure-arm-storage": "^0.14.0-preview", + "azure-storage": "^1.4.0", "errs": "0.3.x", "eventemitter2": "0.4.x", "fast-json-patch": "0.5.x", @@ -65,6 +69,7 @@ "ip": "0.3.x", "lodash": "^3.10.1", "mime": "1.2.x", + "ms-rest-azure": "^1.15.2", "qs": "1.2.x", "request": "2.40.x", "s3-upload-stream": "~1.0.7", @@ -79,6 +84,7 @@ "jshint": "~2.7.0", "mocha": "1.21.x", "mocha-lcov-reporter": "0.0.1", + "nock": "^9.0.2", "should": "4.0.x" }, "main": "./lib/pkgcloud", diff --git a/test/azure-v2/compute/client/test-servers.js b/test/azure-v2/compute/client/test-servers.js new file mode 100644 index 000000000..9b842b693 --- /dev/null +++ b/test/azure-v2/compute/client/test-servers.js @@ -0,0 +1,75 @@ +var mockRequests = require('../../mock-requests'); +var helpers = require('../../../helpers'); +var should = require('should'); + +var createParams = { + name: 'azure-vm-server', + flavor: 'DEFAULT', + username: 'username', + password: 'password', + + imagePublisher: 'Canonical', + imageOffer: 'UbuntuServer', + imageSku: '16.04.0-LTS', + imageVersion: 'latest' +}; +var client = helpers.createClient('azure-v2', 'compute'); + +describe('pkgcloud/azure-v2/servers', function () { + + it('Get multiple servers', function(done) { + + mockRequests.prepare(); + client.getServers(function (err, servers) { + should.not.exist(err); + should(servers).be.instanceOf(Array).and.have.lengthOf(1); + done(); + }); + + }); + + it('Get a single server with RUNNING state', function(done) { + + mockRequests.prepare(); + client.getServer('azure-vm-server', function (err, server) { + should.not.exist(err); + should.exist(server); + server.status.should.equal('RUNNING'); + done(); + }); + + }); + + + it('Creating a new server', function(done) { + + mockRequests.prepare(); + client.createServer(createParams, function (err, server) { + should.not.exist(err); + should.exist(server); + server.status.should.equal('RUNNING'); + done(); + }); + + }); + + it('Deleting a VM with dependencies', function (done) { + mockRequests.prepare(); + client.destroyServer(createParams, { + destroyNics: true, + destroyPublicIP: true, + destroyVnet: true, + destroyStorage: true + }, function (err) { + should.not.exist(err); + done(); + }); + }); + +}); + + + + + + diff --git a/test/azure-v2/mock-requests.js b/test/azure-v2/mock-requests.js new file mode 100644 index 000000000..871b5be7a --- /dev/null +++ b/test/azure-v2/mock-requests.js @@ -0,0 +1,96 @@ +var path = require('path'); +var nock = require('nock'); +var helpers = require('../helpers'); + +const azureAuthUri = 'https://login.microsoftonline.com'; +const azureManagementUri = 'https://management.azure.com'; +const apiVersion = '2016-03-30'; + +function loadFixture(name) { + return helpers.loadFixture(path.join('azure-v2', name)); +} + +function prepare() { + + var config = helpers.loadConfig('azure-v2'); + var sp = config.servicePrincipal; + + + // Nock authentication requests + nock(azureAuthUri) + .post('/' + sp.domain + '/oauth2/token?api-version=1.0') + .reply(200, loadFixture('authentication-certificate.json')); + + // Subscriptions + nock(azureManagementUri) + .get('/subscriptions?api-version=2015-11-01') + .reply(200, loadFixture('subscriptions.json')); + nock(azureManagementUri) + .get('/subscriptions?api-version=2016-06-01') + .reply(200, loadFixture('subscriptions.json')); + + // Resource group + nock(azureManagementUri + '/subscriptions/' + config.subscriptionId + '/resourcegroups') + .get('/' + config.resourceGroup + '?api-version=2016-09-01') + .reply(200, loadFixture('resourceGroup.json')); + + // Servers + nock(azureManagementUri + '/subscriptions/' + config.subscriptionId + '/resourceGroups/' + config.resourceGroup + '/providers/Microsoft.Compute') + .get('/virtualMachines?api-version=' + apiVersion) + .reply(200, loadFixture('servers.json')) + .get('/virtualMachines/azure-vm-server?$expand=instanceView&api-version=' + apiVersion) + .reply(200, loadFixture('server.json')) + .delete('/virtualMachines/azure-vm-server?api-version=' + apiVersion) + .reply(204, ''); + + // Images + nock(azureManagementUri + '/subscriptions/' + config.subscriptionId + '/providers/Microsoft.Compute/locations/location') + .get('/publishers/MicrosoftWindowsServer/artifacttypes/vmimage/offers/WindowsServer/skus/2012-R2-Datacenter/versions?api-version=2016-03-30') + .reply(200, loadFixture('servers.json')) + .get('/vmSizes?api-version=2016-03-30') + .reply(200, loadFixture('servers.json')); + + // Nicks + //https://management.azure.com//subscriptions/subscriptionId/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/nicName?api-version=2016-03-30 + nock(azureManagementUri + '/subscriptions/' + config.subscriptionId + '/resourceGroups/' + config.resourceGroup + '/providers/Microsoft.Network') + .get('/networkInterfaces/nicName?api-version=' + apiVersion) + .reply(200, loadFixture('nic.json')) + .delete('/networkInterfaces/nicName?api-version=' + apiVersion) + .reply(204, ''); + + // Public IPs + nock(azureManagementUri + '/subscriptions/' + config.subscriptionId + '/resourceGroups/' + config.resourceGroup + '/providers/Microsoft.Network') + .delete('/publicIPAddresses/publicIPName?api-version=' + apiVersion) + .reply(204, ''); + + // VNET + nock(azureManagementUri + '/subscriptions/' + config.subscriptionId + '/resourceGroups/' + config.resourceGroup + '/providers/Microsoft.Network') + .delete('/virtualNetworks/vnetName?api-version=' + apiVersion) + .reply(204, ''); + + // Storage + // https://management.azure.com/subscriptions/azure-account-subscription-id/resourceGroups/resource-group/providers/Microsoft.Storage/storageAccounts/test-storage?api-version=2016-05-01 + nock(azureManagementUri + '/subscriptions/' + config.subscriptionId + '/resourcegroups/' + config.resourceGroup + '/providers/microsoft.storage') + .filteringPath(function (path) { return path.toLowerCase(); }) + .get('/storageaccounts/azurestorage?api-version=2016-05-01') + .reply(200, loadFixture('container.json')) + .post('/storageaccounts/azurestorage/listkeys?api-version=2016-05-01') + .reply(200, loadFixture('container-keys.json')) + .delete('/storageaccounts/azurestorage/?api-version=2016-01-01') + .reply(204, ''); + + // url:"https://management.azure.com/subscriptions/73a4ea93-d914-424d-9e64-28adf397e8e3/resourceGroups/morshe-noobaa2/providers/Microsoft.Storage/storageAccounts/boobaavmstore3/listKeys?api-version=2016-05-01" + + // Deployments + nock(azureManagementUri + '/subscriptions/' + config.subscriptionId + '/resourcegroups/' + config.resourceGroup + '/providers/microsoft.resources/deployments') + .filteringPath(function (path) { + path = path.toLowerCase(); + return path.substr(0, path.indexOf('/deployments/pkgc-')) + '/deployments/pkgc-test'; + }) + .put('/pkgc-test') + .reply(200, {}); +} + +module.exports = { + prepare +}; \ No newline at end of file diff --git a/test/azure-v2/storage/client/test-storage.js b/test/azure-v2/storage/client/test-storage.js new file mode 100644 index 000000000..909254f9a --- /dev/null +++ b/test/azure-v2/storage/client/test-storage.js @@ -0,0 +1,52 @@ +//TODO: Make this a vows test + +var mockRequests = require('../../mock-requests'); +var helpers = require('../../../helpers'); +var should = require('should'); + +var client = helpers.createClient('azure-v2', 'storage'); + +describe('pkgcloud/azure-v2/storage', function () { + + it('Create container', function(done) { + + mockRequests.prepare(); + client.createContainer('azurestorage', function (err, container) { + should.not.exist(err); + should.exist(container); + should(container.name).be.exactly('azurestorage'); + done(); + }); + }); + + it('Get container', function(done) { + + mockRequests.prepare(); + client.getContainer('azurestorage', function (err, container) { + should.not.exist(err); + should.exist(container); + should(container.name).be.exactly('azurestorage'); + done(); + }); + }); + + // Todo: + // Find out if can test download file + // it('Get files in container', function(done) { + + // mockRequests.prepare(); + // client.getFiles('azurestorage', { container: 'container' }, function (err, files) { + // should.not.exist(err); + // should.exist(container); + // should(container.name).be.exactly('azurestorage'); + // done(); + // }); + // }); + +}); + + + + + + diff --git a/test/common/compute/server-test.js b/test/common/compute/server-test.js index 970a613f5..efbf63a72 100644 --- a/test/common/compute/server-test.js +++ b/test/common/compute/server-test.js @@ -14,6 +14,7 @@ var should = require('should'), providers = require('../../configs/providers.json'), Server = require('../../../lib/pkgcloud/core/compute/server').Server, azureApi = require('../../../lib/pkgcloud/azure/utils/azureApi'), + azureV2Mocks = require('../../azure-v2/mock-requests'), mock = !!process.env.MOCK; var azureOptions = require('../../fixtures/azure/azure-options.json'); @@ -46,7 +47,7 @@ providers.filter(function (provider) { // setup a filtering path for aws hockInstance.filteringPathRegEx(/https:\/\/ec2\.us-west-2\.amazonaws\.com([?\w\-\.\_0-9\/]*)/g, '$1'); - + server = http.createServer(hockInstance.handler); authServer = http.createServer(authHockInstance.handler); @@ -294,6 +295,10 @@ setupImagesMock = function (client, provider, servers) { .get('/azure-account-subscription-id/services/images') .replyWithFile(200, __dirname + '/../../fixtures/azure/images.xml'); } + else if (provider === 'azure-v2') { + + azureV2Mocks.prepare(); + } else if (provider === 'digitalocean') { servers.server .get('/v2/images?per_page=200&page=1') @@ -355,6 +360,9 @@ setupFlavorMock = function (client, provider, servers) { .get('/v2/5ACED3DC3AA740ABAA41711243CC6949/flavors/detail') .replyWithFile(200, __dirname + '/../../fixtures/hp/flavors.json'); } + else if (provider === 'azure-v2') { + azureV2Mocks.prepare(); + } }; setupServerMock = function (client, provider, servers) { @@ -471,6 +479,9 @@ setupServerMock = function (client, provider, servers) { .get('/v2/5ACED3DC3AA740ABAA41711243CC6949/servers/5a023de8-957b-4822-ad84-8c7a9ef83c07') .replyWithFile(200, __dirname + '/../../fixtures/openstack/serverCreated2.json'); } + else if (provider === 'azure-v2') { + azureV2Mocks.prepare(); + } }; setupGetServersMock = function (client, provider, servers) { diff --git a/test/configs/mock/azure-v2.json b/test/configs/mock/azure-v2.json new file mode 100644 index 000000000..ee401e17d --- /dev/null +++ b/test/configs/mock/azure-v2.json @@ -0,0 +1,9 @@ +{ + "subscriptionId": "azure-account-subscription-id", + "resourceGroup": "resource-group", + "servicePrincipal": { + "clientId": "sp-client-id", + "secret": "sp-secret", + "domain": "sp-domain" + } +} diff --git a/test/configs/providers.json b/test/configs/providers.json index 96c4a7971..378e4b04a 100644 --- a/test/configs/providers.json +++ b/test/configs/providers.json @@ -1 +1 @@ -["rackspace", "openstack", "joyent", "amazon", "azure", "digitalocean", "hp", "google"] \ No newline at end of file +["rackspace", "openstack", "joyent", "amazon", "azure", "digitalocean", "hp", "google", "azure-v2"] \ No newline at end of file diff --git a/test/fixtures/azure-v2/authentication-certificate.json b/test/fixtures/azure-v2/authentication-certificate.json new file mode 100644 index 000000000..365b7bf1a --- /dev/null +++ b/test/fixtures/azure-v2/authentication-certificate.json @@ -0,0 +1,9 @@ +{ + "token_type":"Bearer", + "expires_in":"3600", + "ext_expires_in":"10800", + "expires_on":"1483452282", + "not_before":"1483448382", + "resource":"https://management.core.windows.net/", + "access_token":"XXXXXXXX" +} \ No newline at end of file diff --git a/test/fixtures/azure-v2/container-keys.json b/test/fixtures/azure-v2/container-keys.json new file mode 100644 index 000000000..e2677ace7 --- /dev/null +++ b/test/fixtures/azure-v2/container-keys.json @@ -0,0 +1,14 @@ +{ + "keys": [ + { + "keyName": "key1", + "permissions":"Full", + "value":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==" + }, + { + "keyName": "key2", + "permissions":"Full", + "value":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/azure-v2/container.json b/test/fixtures/azure-v2/container.json new file mode 100644 index 000000000..bed276f20 --- /dev/null +++ b/test/fixtures/azure-v2/container.json @@ -0,0 +1,25 @@ +{ + "id": "/subscriptions/azure-account-subscription-id/resourceGroups/resource-group/providers/Microsoft.Storage/storageAccounts/azurestorage", + "kind": "Storage", + "location": "westeurope", + "name": "azurestorage", + "properties": { + "creationTime": "2017-01-08T16:29:06.2738884Z", + "primaryEndpoints": { + "blob": "https://azurestorage.blob.core.windows.net/", + "file": "https://azurestorage.file.core.windows.net/", + "queue": "https://azurestorage.queue.core.windows.net/", + "table": "https://azurestorage.table.core.windows.net/" + }, + "primaryLocation": "westeurope", + "provisioningState": "Succeeded", + "statusOfPrimary": "available" + }, + "sku": { + "name": "Standard_LRS", + "tier": "Standard" + }, + "tags": { + }, + "type": "Microsoft.Storage/storageAccounts" +} \ No newline at end of file diff --git a/test/fixtures/azure-v2/nic.json b/test/fixtures/azure-v2/nic.json new file mode 100644 index 000000000..3c2dbe0b5 --- /dev/null +++ b/test/fixtures/azure-v2/nic.json @@ -0,0 +1,37 @@ +{ + "name": "nickName", + "id": "/subscriptions/azure-account-subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/nickName", + "etag": "W/\"00000000-0000-0000-0000-000000000000\"", + "location": "westeurope", + "properties": { + "provisioningState": "Succeeded", + "resourceGuid": "00000000-0000-0000-0000-000000000000", + "ipConfigurations": [ + { + "name": "ipconfig1", + "id": "subscriptions/azure-account-subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/nickName/ipConfigurations/ipconfig1", + "etag": "W/\"00000000-0000-0000-0000-000000000000\"", + "properties": { + "provisioningState": "Succeeded", + "privateIPAddress": "10.0.0.4", + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "subscriptions/azure-account-subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/publicIPAddresses/publicIPName" + }, + "subnet": { + "id": "subscriptions/azure-account-subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/virtualNetworks/vnetName/subnets/Subnet" + }, + "primary": true, + "privateIPAddressVersion": "IPv4" + } + } + ], + "dnsSettings": { + "dnsServers": [], + "appliedDnsServers": [] + }, + "macAddress": "00-00-00-00-00-00", + "enableIPForwarding": false + }, + "type": "Microsoft.Network/networkInterfaces" +} \ No newline at end of file diff --git a/test/fixtures/azure-v2/resourceGroup.json b/test/fixtures/azure-v2/resourceGroup.json new file mode 100644 index 000000000..6db789d9c --- /dev/null +++ b/test/fixtures/azure-v2/resourceGroup.json @@ -0,0 +1,8 @@ +{ + "id": "/subscriptions/azure-account-subscription-id/resourcegroups/resource-group", + "name": "resource-group", + "location": "location", + "properties": { + "provisioningState": "Succeeded" + } +} \ No newline at end of file diff --git a/test/fixtures/azure-v2/server.json b/test/fixtures/azure-v2/server.json new file mode 100644 index 000000000..d03b15405 --- /dev/null +++ b/test/fixtures/azure-v2/server.json @@ -0,0 +1,104 @@ +{ + "id": "/subscriptions/azure-account-subscription-id/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/vmName", + "name":"vmName", + "type":"Microsoft.Compute/virtualMachines", + "location":"somelocation", + "properties" : { + "hardwareProfile": { + "vmSize":"Standard_SIZE" + }, + "storageProfile": { + "imageReference": { + "publisher":"PUBLISHER", + "offer":"OFFER", + "sku":"SKU", + "version":"VERSION" + }, + "osDisk": { + "osType":"SOME_OS", + "name":"osdisk", + "vhd": { "uri":"https://azurestorage.blob.core.windows.net/vhds/osdisk.vhd" }, + "caching":"ReadWrite", + "createOption":"FromImage" + }, + "dataDisks": [ + { + "lun":0, + "name":"datadisk1", + "vhd": { "uri":"https://azurestorage.blob.core.windows.net/vhds/datadisk1.vhd" }, + "caching":"None", + "createOption":"Empty", + "diskSizeGB":100 + } + ] + }, + "osProfile": { + "computerName":"vmName", + "adminUsername":"pkgcloud", + "linuxConfiguration": { "disablePasswordAuthentication":false }, + "secrets":[] + }, + "networkProfile": { + "networkInterfaces":[ + { + "id":"subscriptions/azure-account-subscription-id/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/nicName" + } + ] + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled":true, + "storageUri":"https://azurestorage.blob.core.windows.net/" + } + }, + "provisioningState": "Succeeded", + "instanceView": { + "vmAgent": { + "vmAgentVersion": "2.1.3", + "extensionHandlers": [], + "statuses": [{ + "code": "ProvisioningState/succeeded", + "level": "Info", + "displayStatus": "Ready", + "message": "Guest Agent is running", + "time": "2017-01-03T15:14:40.000Z" + }] + }, + "disks": [{ + "name": "osdisk", + "statuses": [{ + "code": "ProvisioningState/succeeded", + "level": "Info", + "displayStatus": "Provisioning succeeded", + "time": "2017-01-02T14:57:17.094Z" + }] + }, + { + "name": "datadisk1", + "statuses": [{ + "code": "ProvisioningState/succeeded", + "level": "Info", + "displayStatus": "Provisioning succeeded", + "time": "2017-01-02T14:57:17.094Z" + }] + }], + "bootDiagnostics": { + "consoleScreenshotBlobUri": "https://azurestorage.blob.core.windows.net/bootdiagnostics/vmName.screenshot.bmp", + "serialConsoleLogBlobUri": "https://azurestorage.blob.core.windows.net/bootdiagnostics/vmName.serialconsole.log" + }, + "statuses": [{ + "code": "ProvisioningState/succeeded", + "level": "Info", + "displayStatus": "Provisioning succeeded", + "time": "2017-01-02T14:57:55.844Z" + }, + { + "code": "PowerState/running", + "level": "Info", + "displayStatus": "VM running" + }] + }, + "vmId":"VM_ID_GUID" + } +} + diff --git a/test/fixtures/azure-v2/servers.json b/test/fixtures/azure-v2/servers.json new file mode 100644 index 000000000..c0b6a2564 --- /dev/null +++ b/test/fixtures/azure-v2/servers.json @@ -0,0 +1,60 @@ +{ + "value": [ + { + "id": "/subscriptions/subscriptionId/resourceGroups/resource-group/providers/Microsoft.Compute/virtualMachines/vmName", + "name":"vmName", + "type":"Microsoft.Compute/virtualMachines", + "location":"somelocation", + "hardwareProfile": { + "vmSize":"Standard_SIZE" + }, + "storageProfile": { + "imageReference": { + "publisher":"PUBLISHER", + "offer":"OFFER", + "sku":"SKU", + "version":"VERSION" + }, + "osDisk": { + "osType":"SOME_OS", + "name":"osdisk", + "vhd": { "uri":"https://azurestorage.blob.core.windows.net/vhds/osdisk.vhd" }, + "caching":"ReadWrite", + "createOption":"FromImage" + }, + "dataDisks": [ + { + "lun":0, + "name":"datadisk1", + "vhd": { "uri":"https://azurestorage.blob.core.windows.net/vhds/datadisk1.vhd" }, + "caching":"None", + "createOption":"Empty", + "diskSizeGB":100 + } + ] + }, + "osProfile": { + "computerName":"vmName", + "adminUsername":"pkgcloud", + "linuxConfiguration": { "disablePasswordAuthentication":false }, + "secrets":[] + }, + "networkProfile": { + "networkInterfaces":[ + { + "id":"/subscriptions/subscriptionId/resourceGroups/resource-group/providers/Microsoft.Network/networkInterfaces/nicName" + } + ] + }, + "diagnosticsProfile": { + "bootDiagnostics": { + "enabled":true, + "storageUri":"https://azurestorage.blob.core.windows.net/" + } + }, + "provisioningState": "Succeeded", + "vmId":"VM_ID_GUID" + } + ] +} + diff --git a/test/fixtures/azure-v2/subscriptions.json b/test/fixtures/azure-v2/subscriptions.json new file mode 100644 index 000000000..330059a9d --- /dev/null +++ b/test/fixtures/azure-v2/subscriptions.json @@ -0,0 +1,15 @@ +{ + "value": [ + { + "id":"/subscriptions/azure-account-subscription-id", + "subscriptionId":"azure-account-subscription-id", + "displayName":"Subscription Name", + "state":"Enabled", + "subscriptionPolicies": { + "locationPlacementId":"Internal_2014-09-01", + "quotaId":"Internal_2014-09-01", + "spendingLimit":"Off" + } + } + ] +} \ No newline at end of file diff --git a/test/helpers/index.js b/test/helpers/index.js index 830bb7975..b9dc2ae44 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -67,8 +67,8 @@ helpers.loadConfig = function loadConfig(provider) { return JSON.parse(content); }; -helpers.fixturePath = function fixturePath(path) { - return __dirname + '/../fixtures/' + path; +helpers.fixturePath = function fixturePath(fpath) { + return path.join(__dirname, '..', 'fixtures', fpath); }; helpers.loadFixture = function loadFixture(path, json) {