Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

First commit of rackspace-openstack

  • Loading branch information...
commit 19eb2322ed778947d84c94eb1faa731dd4ec54bc 0 parents
Ken Perkins authored
4 .gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.testing/
+config.json
+test/manual.js
13 Makefile
@@ -0,0 +1,13 @@
+MOCHA_OPTS=-t 120000 test/*-tests.js
+REPORTER = spec
+
+check: test
+
+test: test-unit
+
+test-unit:
+ @NODE_ENV=test ./node_modules/.bin/mocha \
+ --reporter $(REPORTER) \
+ $(MOCHA_OPTS)
+
+.PHONY: test
53 README.md
@@ -0,0 +1,53 @@
+# rackspace-openstack
+
+A client implementation for the Rackspace Openstack API (v2)
+
+## Usage
+
+The rackspace-openstack module is compliant with the [Rackspace Openstack API][0]. rackspace-openstack
+is a nearly feature complete wrapper for the Rackspace Openstack APIs and should work in most scenarios.
+
+### Getting Started
+Creating and authenticating your client against the Rackspace API is simple:
+
+```Javascript
+var openstack = require('openstack'),
+ config = {
+ auth : {
+ username: 'your-username',
+ apiKey: 'your-api-key'
+ }
+};
+
+var client = openstack.createClient(config);
+
+client.authorize(function(err) {
+ if (err) {
+ process.exit(1);
+ return;
+ }
+
+ // Do stuff here
+}
+```
+
+### Creating a Server
+```Javascript
+client.createServer({
+ image: '5cebb13a-f783-4f8c-8058-c4182c724ccd',
+ flavor: 2,
+ name: 'My Server'
+}, function(err, server) {
+
+ // Do stuff with your new server
+
+});
+
+```
+
+## Run Tests
+All rackspace-openstack tests are available by running `make test`
+
+#### Author: [Ken Perkins](http://github.com/kenperkins)
+
+[0]: http://docs.rackspace.com/servers/api/v2/cs-devguide/content/ch_preface.html
22 lib/openstack.js
@@ -0,0 +1,22 @@
+/*
+ * openstack.js: Wrapper for rackspace-openstack object
+ *
+ * (C) 2012 Clipboard, Inc.
+ * Inspired by node-cloudservers from Nodejitsu
+ * MIT LICENSE
+ *
+ */
+
+var openstack = exports;
+
+// Expose version through `pkginfo`.
+require('pkginfo')(module, 'version');
+
+// Core functionality
+openstack.createClient = require('./openstack/core').createClient;
+
+// Type Definitions
+openstack.Client = require('./openstack/core').Client;
+openstack.Server = require('./openstack/server').Server;
+openstack.Flavor = require('./openstack/flavor').Flavor;
+openstack.Image = require('./openstack/image').Image;
502 lib/openstack/core.js
@@ -0,0 +1,502 @@
+/*
+ * core.js: Core functions for accessing rackspace openstack servers
+ *
+ * (C) 2012 Clipboard, Inc.
+ * Inspired by node-cloudservers from Nodejitsu
+ * MIT LICENSE
+ *
+ */
+
+var request = require('request'),
+ openstack = require('../openstack'),
+ _ = require('underscore');//,
+ //utils = require('./utils');
+
+var usAuthUrl = 'https://identity.api.rackspacecloud.com/v2.0',
+ ukAuthUrl = 'https://lon.identity.api.rackspacecloud.com/v2.0';
+
+exports.createClient = function(options) {
+ return new Client(options);
+};
+
+var Client = exports.Client = function(options) {
+ if (!options || !options.auth) throw new Error('options.auth is required to create Config');
+
+ this.config = {};
+
+ this.config.auth = options.auth;
+
+ if (options.location && options.location === 'UK') {
+ this.config.authUrl = ukAuthUrl;
+ }
+ else {
+ this.config.authUrl = usAuthUrl;
+ }
+
+ this.authorized = false;
+};
+
+/**
+ * @name Client.authorize
+ * @description authorize talks to the rackspace API, and upon validation,
+ * populates your client with auth tokens and service endpoints
+ * @param {Function} callback handles the callback for validating your Auth
+ */
+Client.prototype.authorize = function(callback) {
+ var self = this;
+ var authRequestOptions = {
+ uri: this.config.authUrl + '/tokens',
+ json: {
+ auth: {
+ 'RAX-KSKEY:apiKeyCredentials':{
+ 'username': this.config.auth.username,
+ 'apiKey' : this.config.auth.apiKey
+ }
+ }
+ }
+ };
+
+ request(authRequestOptions, function(err, res, body) {
+ if (err) {
+ callback(err);
+ return;
+ }
+ else if (body && body.unauthorized) {
+ callback(body.unauthorized);
+ return;
+ }
+
+ self.authorized = true;
+
+ self.config.token = body.access.token;
+ self.config.serviceCatalog = body.access.serviceCatalog;
+ self.config.defaultRegion = body.access.user['RAX-AUTH:defaultRegion'];
+
+ callback(err, self.config);
+ });
+};
+
+/**
+ * @name Client.authorizedRequest
+ * @description Global handler for creating a new authorized request to the
+ * Rackspace API endpoint
+ * @param {Object} options provides required values for the request
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.authorizedRequest = function(options, callback) {
+ var self = this;
+
+ if (!options || !callback) {
+ throw new Error('Options and Callback are required');
+ }
+
+ var endpoint = getEndpoint({
+ type: 'compute',
+ name: 'cloudServersOpenStack',
+ region: self.config.defaultRegion
+ }, self.config.serviceCatalog);
+
+ var requestOptions = {
+ uri: endpoint + options.uri,
+ method: options.method || 'GET',
+ json: options.data ? options.data : true,
+ headers: {
+ 'X-AUTH-TOKEN': self.config.token.id
+ }
+ };
+
+ request(requestOptions, callback);
+};
+
+/**
+ * @name getServers
+ * @description getServers retrieves your list of servers
+ * @param {Object|Function} details provides filters on your servers request NOT USED
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.getServers = function(details, callback) {
+ var self = this;
+
+ if (typeof(details) === 'function') {
+ callback = details;
+ details = {};
+ }
+
+ var requestOptions = {
+ uri: '/servers/detail'
+ };
+
+ requestOptions.qs = _.pick(details,
+ 'image',
+ 'flavor',
+ 'name',
+ 'status',
+ 'marker',
+ 'limit',
+ 'changes-since');
+
+ self.authorizedRequest(requestOptions, function(err, res, body) {
+
+ if (err || !body.servers) {
+ callback(err);
+ return;
+ }
+
+ var servers = [];
+
+ for (var i = 0; i < body.servers.length; i++) {
+ servers.push(new openstack.Server(self, body.servers[i]));
+ }
+
+ callback(err, servers);
+ });
+};
+
+/**
+ * @name Client.getServer
+ * @description getServer retrieves the specified server
+ * @param {String} id of the server to get
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.getServer = function(id, callback) {
+ var self = this;
+
+ var requestOptions = {
+ uri: '/servers/' + id
+ };
+
+ self.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err || !body.server) {
+ callback(err);
+ return;
+ }
+
+ callback(err, new openstack.Server(self, body.server));
+ });
+};
+
+/**
+ * @name Client.createServer
+ * @description Creates a server with the specified options. The flavor / image
+ * properties of the options can be instances of rackspace-openstack's objects
+ * (Flavor, Image) OR ids to those entities in Rackspace.
+ * @param {Object} options the server options to use in building your server
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.createServer = function(options, callback) {
+ var self = this, flavorId, imageId;
+
+ ['flavor', 'image', 'name'].forEach(function(required) {
+ if (!options[required]) throw new Error('options.' +
+ required + ' is a required argument.');
+ });
+
+ flavorId = options['flavor'] instanceof openstack.Flavor ?
+ options['flavor'].id : parseInt(options['flavor'], 10);
+
+ imageId = options['image'] instanceof openstack.Image ?
+ options['image'].id : options['image'];
+
+ var requestOptions = {
+ uri: '/servers',
+ method: 'POST',
+ data: {
+ server: {
+ name: options['name'],
+ imageRef: imageId,
+ flavorRef: flavorId,
+ metadata: options['metadata'],
+ personality: options['personality'] || []
+ }
+ }
+ };
+
+ // Don't set the adminPass on the request unless we've got one
+ if (options.adminPass) {
+ requestOptions.data.server.adminPass = options.adminPass;
+ }
+
+ self.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err || !body.server) {
+ callback(err);
+ return;
+ }
+
+ callback(err, new openstack.Server(self, _.extend({}, body.server, options)));
+ });
+};
+
+/**
+ * @name Client.destroyServer
+ * @description deletes the specified server
+ * @param {Object|String} server or server id to delete
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.destroyServer = function(server, callback) {
+ var self = this, serverId;
+
+ serverId = server instanceof openstack.Server ? server.id : server;
+
+ if (!server) {
+ throw new Error('Server is a required argument.');
+ }
+
+ var requestOptions = {
+ uri: '/servers/' + serverId,
+ method: 'DELETE'
+ };
+
+ self.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ callback(err, res.statusCode === 204);
+ });
+};
+
+/**
+ * @name Client.getFlavors
+ * @description getFlavors retrieves your list of image flavors
+ * @param {Object|Function} details provides filters on your flavors request NOT USED
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.getFlavors = function(details, callback) {
+ var self = this;
+
+ if (typeof(details) === 'function') {
+ callback = details;
+ details = {};
+ }
+
+ var requestOptions = {
+ uri: '/flavors/detail'
+ };
+
+ requestOptions.qs = _.pick(details,
+ 'minDisk',
+ 'minRam',
+ 'marker',
+ 'limit');
+
+ self.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err || !body.flavors) {
+ callback(err);
+ return;
+ }
+
+ var flavors = [];
+
+ for (var i = 0; i < body.flavors.length; i++) {
+ flavors.push(new openstack.Flavor(self, body.flavors[i]));
+ }
+
+ callback(err, flavors);
+ });
+};
+
+/**
+ * @name Client.getFlavor
+ * @description getFlavor retrieves the specified flavor
+ * @param {String} id of the flavor to get
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.getFlavor = function(id, callback) {
+ var self = this;
+
+ var requestOptions = {
+ uri: '/flavors/' + id
+ };
+
+ self.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err || !body.flavor) {
+ callback(err);
+ return;
+ }
+
+ callback(err, new openstack.Flavor(self, body.flavor));
+ });
+};
+
+
+/**
+ * @name Client.getImages
+ * @description getImages retrieves your list of server images
+ * @param {Object|Function} details provides filters on your server images request NOT USED
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.getImages = function(details, callback) {
+ var self = this;
+
+ if (typeof(details) === 'function') {
+ callback = details;
+ details = {};
+ }
+
+ var requestOptions = {
+ uri: '/images/detail'
+ };
+
+ requestOptions.qs = _.pick(details,
+ 'server',
+ 'name',
+ 'status',
+ 'marker',
+ 'limit',
+ 'changes-since',
+ 'type');
+
+ self.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err || !body.images) {
+ callback(err);
+ return;
+ }
+
+ var images = [];
+
+ for (var i = 0; i < body.images.length; i++) {
+ images.push(new openstack.Image(self, body.images[i]));
+ }
+
+ callback(err, images);
+ });
+};
+
+/**
+ * @name Client.getImage
+ * @description Gets the details for a specified image id
+ *
+ * @param {String} id the image id of the requested image
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.getImage = function(id, callback) {
+ var self = this;
+
+ var requestOptions = {
+ uri: '/images/' + id
+ };
+
+ self.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err || !body.image) {
+ callback(err);
+ return;
+ }
+
+ callback(err, new openstack.Image(self, body.image));
+ });
+};
+
+/**
+ * @name Client.createServerImage
+ * @description This operation creates a new image for a specified server.
+ * Once complete, a new image is available that you can use to rebuild or
+ * create servers.
+ *
+ * @param {Object} options handles the callback of your api call
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.createServerImage = function(options, callback) {
+ var self = this;
+
+ ['name', 'server'].forEach(function(required) {
+ if (!options[required]) throw new Error('options.' +
+ required + ' is a required argument.');
+ });
+
+ var serverId = options.server instanceof openstack.Server ?
+ options.server.id : options.server;
+
+ var createImageData = {
+ name: options.name
+ };
+
+ if (options.metadata) {
+ createImageData.metadata = options.metadata;
+ }
+
+ var requestOptions = {
+ uri: '/servers/' + serverId + '/action',
+ method: 'POST',
+ data: {
+ createImage: createImageData
+ }
+ };
+
+ self.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err || res.statusCode !== 202) {
+ callback(err);
+ return;
+ }
+
+ // HACK
+ // Rackspace returns a URL to a non-existant End point, so instead
+ // we strip the guid off the end of the request and return it as the
+ // image Id
+ var re = new RegExp("images/([abcdef0-9]{8}-[abcdef0-9]{4}-[abcedf0-9]{4}-[abcdef0-9]{4}-[abcdef0-9]{12})$");
+ callback(err, res.headers['location'].match(re)[1]);
+ });
+};
+
+/**
+ * @name Client.destroyImage
+ * @description This operation deletes the specified image from the system.
+ *
+ * @param {Object} image the image object or id to delete
+ * @param {Function} callback handles the callback of your api call
+ */
+Client.prototype.destroyImage = function(image, callback) {
+ var self = this,
+ imageId = image instanceof openstack.Image ? image.id : image;
+
+ var destroyOptions = {
+ method: 'DELETE',
+ uri: '/images/' + imageId
+ };
+
+ self.authorizedRequest(destroyOptions, function(err, res, body) {
+ if (err || res.statusCode !== 204) {
+ callback(err);
+ return;
+ }
+
+ callback(err, true);
+ });
+};
+
+function getEndpoint(options, catalog) {
+
+ ['type', 'name', 'region'].forEach(function(required) {
+ if (!options[required]) throw new Error('options.' +
+ required + ' is a required argument.');
+ });
+
+ if (!catalog) {
+ throw new Error('Catalog is a required argument.');
+ }
+
+ var endpointUrl = '';
+
+ for (var i = 0; i < catalog.length; i++) {
+ var service = catalog[i];
+
+ if (service.type === options.type && service.name === options.name) {
+ if (service.endpoints.length === 1) {
+ endpointUrl = service.endpoints[0].publicURL;
+ break;
+ }
+
+ for (var j = 0; j < service.endpoints.length; j++) {
+
+ var endpoint = service.endpoints[j];
+
+ if (endpoint.region === options.region) {
+ endpointUrl = endpoint.publicURL;
+ break;
+ }
+ }
+ }
+ }
+
+ return endpointUrl;
+}
52 lib/openstack/flavor.js
@@ -0,0 +1,52 @@
+/*
+ * server.js: Instance of a single rackspace openstack flavor
+ *
+ * (C) 2012 Clipboard, Inc.
+ * Inspired by node-cloudservers from Nodejitsu
+ * MIT LICENSE
+ *
+ */
+var Flavor = function(client, details) {
+ if (!details) {
+ throw new Error("Flavor must be constructed with at-least basic details.")
+ }
+
+ this.client = client;
+ this._setProperties(details);
+};
+
+Flavor.prototype = {
+ /**
+ * @name Flavor.getDetails
+ * @description Update the flavor details for this instance
+ * @param {Function} callback handles the callback of your api call
+ */
+ getDetails: function(callback) {
+ var self = this;
+ this.client.getFlavor(this.id, function(err, flavor) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ self._setProperties(flavor);
+ callback(null, self);
+ });
+ },
+
+ /**
+ * @name Flavor._setProperties
+ * @description Loads the properties of an object into this instance
+ * @param {Object} details the details to load
+ */
+ _setProperties: function(details) {
+ this.id = details.id;
+ this.name = details.name;
+ this.ram = details.ram;
+ this.swap = details.swap;
+ this.vcpus = details.vcpus;
+ this.disk = details.disk;
+ }
+};
+
+exports.Flavor = Flavor;
108 lib/openstack/image.js
@@ -0,0 +1,108 @@
+/*
+ * server.js: Instance of a single rackspace openstack server image
+ *
+ * (C) 2012 Clipboard, Inc.
+ * Inspired by node-cloudservers from Nodejitsu
+ * MIT LICENSE
+ *
+ */
+
+var Image = function(client, details) {
+ if (!details) {
+ throw new Error("Image must be constructed with at least basic details.")
+ }
+
+ this.client = client;
+ this._setProperties(details);
+}
+
+Image.prototype = {
+ /**
+ * @name Image.getDetails
+ * @description Update the image details for this instance
+ * @param {Function} callback handles the callback of your api call
+ */
+ getDetails: function(callback) {
+ var self = this;
+ self.client.getImage(this.id, function(err, image) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ self._setProperties(image);
+ callback(null, self);
+ });
+ },
+
+ /**
+ * @name Image.destroy
+ * @description This operation deletes the specified image from the system.
+ * @param {Function} callback handles the callback of your api call
+ */
+ destroy: function(callback) {
+ var self = this;
+
+ self.client.destroyImage(self, callback);
+ },
+
+ /**
+ * @name Image.setWait
+ * @description Continually polls Rackspace and checks the
+ * results against the attributes parameter. When the attributes match
+ * the callback will be fired.
+ *
+ * @param {Object} attributes the value to check for during the interval
+ * @param {Number} interval timeout in ms
+ * @param {Function} callback handles the callback of your api call
+ */
+ setWait: function(attributes, interval, callback) {
+ var self = this;
+ var equalCheckId = setInterval(function() {
+
+ self.getDetails(function(err, server) {
+ if (err) return; // Ignore errors
+
+ var equal = true, keys = Object.keys(attributes);
+ for (var index in keys) {
+ if (attributes[keys[index]] !== server[keys[index]]) {
+ equal = false;
+ break;
+ }
+ }
+
+ if (equal) {
+ clearInterval(equalCheckId);
+ callback(null, self);
+ }
+ });
+ }, interval);
+
+ return equalCheckId;
+ },
+
+ /**
+ * @name Image.clearWait
+ * @description Clears a previously setWait for this instance
+ * @param {Number} intervalId the interval to clear
+ */
+ clearWait: function(intervalId) {
+ clearInterval(intervalId);
+ },
+
+ /**
+ * @name Image._setProperties
+ * @description Loads the properties of an object into this instance
+ * @param {Object} details the details to load
+ */
+ _setProperties: function(details) {
+ this.id = details.id;
+ this.name = details.name;
+ this.updated = details.updated;
+ this.created = details.created;
+ this.status = details.status;
+ this.progress = details.progress;
+ }
+};
+
+exports.Image = Image;
408 lib/openstack/server.js
@@ -0,0 +1,408 @@
+/*
+ * server.js: Instance of a single rackspace openstack server
+ *
+ * (C) 2012 Clipboard, Inc.
+ * Inspired by node-cloudservers from Nodejitsu
+ * MIT LICENSE
+ *
+ */
+
+var openstack = require('../openstack');
+
+var Server = exports.Server = function(client, details) {
+ if (!details) {
+ throw new Error("Server must be constructed with at least basic details.")
+ }
+
+ this.client = client;
+ this._setProperties(details);
+};
+
+Server.prototype = {
+
+ /**
+ * @name Server.doServerAction
+ * @description Wrapper for a series of server action api calls,
+ * including resize, rebuild confirmResize, revertResize, among others
+ * @param {Object} options provides the data and optional expected
+ * status for the response
+ * @param {Function} callback handles the callback of your api call
+ */
+ doServerAction: function(options, callback) {
+ var self = this, action, expectedStatus;
+
+ if (!options.action) throw new Error('options.action is a required argument.');
+
+ expectedStatus = options.expectedStatus ? options.expectedStatus : 202;
+ action = options.action;
+
+ var requestOptions = {
+ uri: '/servers/' + self.id + '/action',
+ method: 'POST',
+ data: action
+ };
+
+ self.client.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ callback(err, res.statusCode === expectedStatus);
+ });
+ },
+
+ /**
+ * @name Server.confirmResize
+ * @description makes a Server.doServerAction call to sign off on the
+ * server resize. Removes the previous server at Rackspace and it
+ * cannot be rolled back to.
+ * @param {Function} callback handles the callback of your api call
+ */
+ confirmResize: function(callback) {
+ this.doServerAction(
+ {
+ action: { 'confirmResize': null },
+ expectedStatus: 204
+ }, callback);
+ },
+
+ /**
+ * @name Server.destroy
+ * @description Deletes this instance from Rackspace
+ * @param {Function} callback handles the callback of your api call
+ */
+ destroy: function(callback) {
+ this.client.destroyServer(this, callback);
+ },
+
+ /**
+ * @name Server.getAddresses
+ * @description Gets the network addresses for the server, optionally
+ * specifying a specific network ID
+ * @param {String|Function} network id if provided, otherwise is all
+ * @param {Function} callback handles the callback of your api call
+ */
+ getAddresses: function(network, callback) {
+ var self = this,
+ uri = '/servers/' + self.id + '/ips';
+
+ if (typeof(network) !== 'function') {
+ callback = network;
+ }
+ else {
+ uri += '/' + network;
+ }
+
+ var requestOptions = {
+ uri: uri
+ };
+
+ self.client.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ callback(err, body.addresses ? body.addresses : body.network);
+ });
+ },
+
+ /**
+ * @name Server.getDetails
+ * @description Update the server details for this instance
+ * @param {Function} callback handles the callback of your api call
+ */
+ getDetails: function(callback) {
+ var self = this;
+ this.client.getServer(this.id, function(err, server) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ self._setProperties(server);
+ callback(null, self);
+ });
+ },
+
+ /**
+ * @name Server.reboot
+ * @description reboots the server, optionally providing type of reboot.
+ * @param {String|Function} type An optional string (soft|hard) for the
+ * reboot. Soft is default if not provided.
+ * @param {Function} callback handles the callback of your api call
+ */
+ reboot: function(type, callback) {
+
+ if (typeof(type) === 'function') {
+ callback = type;
+ type = 'soft';
+ }
+
+ this.doServerAction(
+ {
+ action: {
+ 'reboot': { 'type': type.toUpperCase() }
+ }
+ }, callback);
+ },
+
+ /**
+ * @name Server.rebuild
+ * @description Rebuilds this instance with the specified image. This
+ * will delete all data on the server instance. The 'image' can
+ * be an instance of a rackspace-openstack Image or an image id.
+ * @param {Object|Function} options Optionally provide a new set of server
+ * options to be used while rebuilding.
+ * @param {Function} callback handles the callback of your api call
+ */
+ rebuild: function(options, callback) {
+
+ if (typeof(options) === 'function') {
+ callback = options;
+ options = {};
+ }
+
+ var serverOptions = {};
+
+ serverOptions.name = options.name || this.name;
+ serverOptions.imageRef = options.imageRef || this.image.id;
+ serverOptions.flavorRef = options.flavorRef || this.flavor.id;
+ serverOptions.metadata = options.metadata || this.metadata;
+ serverOptions.personality = options.personality || this.personality;
+
+ // Don't specify the admin pass unless it's explicit
+ if (options.adminPass) {
+ serverOptions.adminPass = options.adminPass;
+ }
+
+ this.doServerAction(
+ {
+ action: {
+ 'rebuild': serverOptions
+ }
+ }, callback);
+ },
+
+ /**
+ * @name Server.resize
+ * @description Resizes this instance to another flavor. In essence scaling
+ * the server up or down. The original server is saved for a period of time
+ * to rollback if there is a problem. The 'flavor' can be an instance of a
+ * rackspace-openstack Flavor or a flavor id.
+ * @param {Object|String} flavor Provide the Flavor or flavor id to be used
+ * when resizing this server
+ * @param {Function} callback handles the callback of your api call
+ */
+ resize: function(flavor, callback) {
+ var flavorId = flavor instanceof openstack.Flavor ? flavor.id : flavor;
+
+ this.doServerAction(
+ {
+ action: {
+ 'resize': { 'flavorRef': flavorId }
+ }
+ }, callback);
+ },
+
+ /**
+ * @name Server.revertResize
+ * @description Rollback this server the saved image from before the resize
+ * @param {Function} callback handles the callback of your api call
+ */
+ revertResize: function(callback) {
+ this.doServerAction(
+ {
+ action: { 'revertResize': null }
+ }, callback);
+ },
+
+ /**
+ * @name Server.changeName
+ * @description Change the name of this server. Does not change the hostname
+ * @param {String} name The new name of the server
+ * @param {Function} callback handles the callback of your api call
+ */
+ changeName: function(name, callback) {
+
+ var self = this,
+ updateOptions = {
+ method: 'PUT',
+ uri: '/servers' + this.id,
+ data: {
+ 'server': {
+ 'name': name
+ }
+ }
+ };
+
+ this.client.authorizedRequest(updateOptions, function(err, res, body) {
+ if (err || !body.server) {
+ callback(err);
+ return;
+ }
+
+ self._setProperties(body.server);
+ callback(err, res.statusCode === 200);
+ });
+ },
+
+ /**
+ * @name Server.changeAdminPassword
+ * @description Changes the administrator password for a VM
+ * @param {String} newPassword The new password for the server administrator
+ * @param {Function} callback handles the callback of your api call
+ */
+ changeAdminPassword: function(newPassword, callback) {
+ this.doServerAction(
+ {
+ action: {
+ changePassword: {
+ adminPass: newPassword
+ }
+ }
+ }, callback);
+ },
+
+ /**
+ * @name Server.rescue
+ * @description Enter rescue mode to reboot a virtual machine (VM) in rescue
+ * mode so that you can access the VM with a new root password and fix any
+ * file system and configuration errors.
+ *
+ * Doesn't use Server.doServerAction as we need to handle the custom response
+ *
+ * @param {Function} callback handles the callback of your api call
+ */
+ rescue: function(callback) {
+ var self = this,
+ requestOptions = {
+ uri: '/servers/' + this.id + '/action',
+ method: 'POST',
+ data: {
+ action: {
+ rescue: 'none'
+ }
+ }
+ };
+
+ self.client.authorizedRequest(requestOptions, function(err, res, body) {
+ if (err || res.statusCode !== 200 || !body.adminPass) {
+ callback(err);
+ return;
+ }
+
+ callback(err, body);
+ });
+ },
+
+ /**
+ * @name Server.unrescue
+ * @description After you resolve any problems and reboot a rescued server,
+ * you can unrescue the server. When you unrescue the server, the repaired
+ * image is restored to its running state with your original password.
+ *
+ * @param {Function} callback handles the callback of your api call
+ */
+ unrescue: function(callback) {
+ this.doServerAction(
+ {
+ action: {
+ unrescue: null
+ }
+ }, callback);
+ },
+
+ /**
+ * @name Server.createImage
+ * @description This operation creates a new image for a specified server.
+ * Once complete, a new image is available that you can use to rebuild or
+ * create servers.
+ *
+ * Doesn't use Server.doServerAction as we need to handle the custom response
+ *
+ * @param {Object} options handles the callback of your api call
+ * @param {Function} callback handles the callback of your api call
+ */
+ createImage: function(options, callback) {
+ this.client.createServerImage({
+ name: options.name,
+ server: this
+ }, callback);
+ },
+
+ /**
+ * @name Server.setWait
+ * @description Continually polls Rackspace CloudServers and checks the
+ * results against the attributes parameter. When the attributes match
+ * the callback will be fired.
+ *
+ * @param {Object} attributes the value to check for during the interval
+ * @param {Number} interval timeout in ms
+ * @param {Function} callback handles the callback of your api call
+ */
+ setWait: function(attributes, interval, callback) {
+ var self = this;
+ var equalCheckId = setInterval(function() {
+
+ self.getDetails(function(err, server) {
+ if (err) return; // Ignore errors
+
+ var equal = true, keys = Object.keys(attributes);
+ for (var index in keys) {
+ if (attributes[keys[index]] !== server[keys[index]]) {
+ equal = false;
+ break;
+ }
+ }
+
+ if (equal) {
+ clearInterval(equalCheckId);
+ callback(null, self);
+ }
+ });
+ }, interval);
+
+ return equalCheckId;
+ },
+
+ /**
+ * @name Server.clearWait
+ * @description Clears a previously setWait for this instance
+ * @param {Number} intervalId the interval to clear
+ */
+ clearWait: function(intervalId) {
+ clearInterval(intervalId);
+ },
+
+ /**
+ * @name Server._setProperties
+ * @description Loads the properties of an object into this instance
+ * @param {Object} details the details to load
+ */
+ _setProperties: function(details) {
+ // Set core properties
+ this.id = details.id;
+ this.name = details.name;
+
+ // Only set these if present
+ if (details.flavor && typeof(details.flavor) === 'object') {
+ this.flavor = details.flavor;
+ }
+
+ if (details.image && typeof(details.image) === 'object') {
+ this.image = details.image;
+ }
+
+ // Additional Properties
+ this.progress = details.progress || this.progress;
+ this.adminPass = details.adminPass || this.adminPass;
+ this.status = details.status || this.status;
+ this.hostId = details.hostId || this.hostId;
+ this.addresses = details.addresses || {};
+ this.metadata = details.metadata || {};
+ this.accessIPv4 = details.accessIPv4;
+ this.accessIPv6 = details.accessIPv6;
+ }
+};
33 package.json
@@ -0,0 +1,33 @@
+{
+ "name": "rackspace-openstack",
+ "description": "A client implementation for Rackspace Openstack in node.js",
+ "version": "0.0.1",
+ "author": "Clipboard, Inc. <support@clipboard.com>",
+ "maintainers": [
+ {
+ "name": "Ken Perkins",
+ "email": "ken@clipboard.com"
+ }
+ ],
+ "keywords": [
+ "cloud computing",
+ "api",
+ "rackspace cloud",
+ "cloudservers",
+ "openstack"
+ ],
+ "dependencies": {
+ "pkginfo": "0.2.x",
+ "request": "2.9.x",
+ "underscore": "1.4.x"
+ },
+ "devDependencies": {
+ "mocha": "1.6.x",
+ "should": "*"
+ },
+ "main": "./lib/openstack",
+ "engines": {
+ "node": ">= 0.8"
+ }
+}
+
48 test/authenticate-tests.js
@@ -0,0 +1,48 @@
+var os = require('../lib/openstack');
+var config = require('../config.json');
+var should = require('should');
+
+describe('Authentication Tests', function() {
+
+ it('Should fail because of missing credentials', function(done) {
+ try {
+ var client = os.createClient();
+ }
+ catch (e) {
+ should.exist(e);
+ e.should.be.an.instanceof(Error);
+ e.should.have.property('message', 'options.auth is required to create Config')
+ done();
+ }
+ });
+
+ it('Should connect and authenticate against Rackspace', function(done) {
+ var client = os.createClient({
+ auth: config.auth
+ });
+
+ client.authorize(function(err, config) {
+ should.not.exist(err);
+ should.exist(config);
+ should.exist(config.token);
+
+ done();
+ });
+ });
+
+ it('Should fail connecting with invalid password', function(done) {
+ var client = os.createClient({
+ auth: {
+ username: config.auth.username,
+ apiKey: 'thisisnotreal'
+ }
+ });
+
+ client.authorize(function(err, config) {
+ should.exist(err);
+ should.not.exist(config);
+
+ done();
+ });
+ });
+});
44 test/flavors-tests.js
@@ -0,0 +1,44 @@
+var os = require('../lib/openstack');
+var config = require('../config.json');
+var should = require('should'),
+ util = require('util');
+
+describe('Flavors Tests', function() {
+
+ var client;
+
+ before(function(done) {
+ client = os.createClient({
+ auth: config.auth
+ });
+
+ client.authorize(function(err, config) {
+ should.not.exist(err);
+ should.exist(config);
+
+ done();
+ });
+ });
+
+ it('Get Flavors', function(done) {
+ client.getFlavors(function(err, flavors) {
+ should.not.exist(err);
+ should.exist(flavors);
+
+ flavors.should.be.an.instanceof(Array);
+
+ done();
+ });
+ });
+
+ it('Get Flavor by id', function(done) {
+ client.getFlavor(2, function(err, flavor) {
+ should.not.exist(err);
+ should.exist(flavor);
+
+ flavor.should.be.an.instanceof(os.Flavor);
+
+ done();
+ });
+ });
+});
44 test/images-tests.js
@@ -0,0 +1,44 @@
+var os = require('../lib/openstack');
+var config = require('../config.json');
+var should = require('should'),
+ util = require('util');
+
+describe('Images Tests', function() {
+
+ var client;
+
+ before(function(done) {
+ client = os.createClient({
+ auth: config.auth
+ });
+
+ client.authorize(function(err, config) {
+ should.not.exist(err);
+ should.exist(config);
+
+ done();
+ });
+ });
+
+ it('Get Images', function(done) {
+ client.getImages(function(err, images) {
+ should.not.exist(err);
+ should.exist(images);
+
+ images.should.be.an.instanceof(Array);
+
+ done();
+ });
+ });
+
+ it('Get Image by id', function(done) {
+ client.getImage('5cebb13a-f783-4f8c-8058-c4182c724ccd', function(err, image) {
+ should.not.exist(err);
+ should.exist(image);
+
+ image.should.be.an.instanceof(os.Image);
+
+ done();
+ });
+ });
+});
48 test/servers-tests.js
@@ -0,0 +1,48 @@
+var os = require('../lib/openstack');
+var config = require('../config.json');
+var should = require('should'),
+ util = require('util');
+
+describe('Servers tests', function() {
+
+ var client;
+
+ before(function(done) {
+ client = os.createClient({
+ auth: config.auth
+ });
+
+ client.authorize(function(err, config) {
+ should.not.exist(err);
+ should.exist(config);
+
+ done();
+ });
+ });
+
+ it('get servers', function(done) {
+ client.getServers(function(err, servers) {
+ should.not.exist(err);
+ should.exist(servers);
+
+ //console.log(util.inspect(servers, false, null));
+ done();
+ });
+ });
+
+// it('create a server', function(done) {
+// client.createServer({
+// image: '5cebb13a-f783-4f8c-8058-c4182c724ccd',
+// flavor: 2,
+// name: 'test-from-mocha'
+// }, function(err, server) {
+// should.not.exist(err);
+// should.exist(server);
+//
+// server.setWait({ status: 'ACTIVE' }, 5000, function() {
+// console.dir(server);
+// done();
+// });
+// });
+// });
+});
Please sign in to comment.
Something went wrong with that request. Please try again.