csclient
Version:
An API client for the CloudStack
262 lines (231 loc) • 7.44 kB
JavaScript
'use strict';
const crypto = require('crypto');
const EventEmitter = require('events');
const qs = require('qs');
const url = require('url');
const APIError = require('./apierror');
/**
* Class CloudStackClient
*
* @class CloudStackClient
* @extends {EventEmitter}
*/
class CloudStackClient extends EventEmitter {
constructor(options) {
super();
this.baseUrl = options.baseUrl ? options.baseUrl : options.serverURL;
this.__apiKey = options.apiKey;
this.__secretKey = options.secretKey;
this.__http = options.http;
if (!this.__http) {
if (this.baseUrl && this.baseUrl.indexOf('https') === 0) {
this.__http = require('https');
} else {
this.__http = require('http');
}
}
// Single executor mode allows developers to use the execute method for both sync and async
// CloudStack API calls.
// If enabled, the client will make a listApis call to the server, and build a map of async
// commands. Once this map is complete, ready event is emitted and the execute method will be
// available.
// If disabled or not explicitly enabled, the execute method will work as an alias to executeSync
// The ready event is still immediately emitted in this case, and developers needn't handle it
if (options.singleExecutor === true) {
this.__buildAsyncMap();
this.execute = this.__execute;
} else {
setImmediate(() => {
this.emit('ready')
});
this.execute = this.executeSync;
}
// Default pollingTime will be 2000 (2 seconds)
this.__pollingTime = options.pollingTime || 2000;
// If a polling number is explicitly defined and is greater than 0, the executeAsync method
// will poll only upto pollingNumber times
if (!Number.isNaN(options.pollingNumber && options.pollingNumber > 0)) {
this.__pollingNumber = options.pollingNumber;
}
}
/**
* Private method that calls the listApis CloudStack API, and builds
* a map for async commands
*
*
* @memberOf CloudStackClient
*/
__buildAsyncMap() {
// making a call to listApis API with listall true and filtering only required attributes to save on time
this.executeSync('listApis', { listall: true , filter: 'name,isasync'}, (err, response) => {
if (err) {
this.emit('error', err);
} else {
this.__apiList = response['listapisresponse'].api;
this.__apiCount = response['listapisresponse'].count;
this.__asyncCommands = response['listapisresponse'].api.reduce((result, v, k) => {
if (v.isasync === true) {
result[v.name] = true;
} else {
result[v.name] = false;
}
return result;
}, {});
this.emit('ready');
}
});
}
/**
* Provides today's date in ISO form, ignoring zone timezone offset
*
* @returns {string} date
*
* @memberOf CloudStackClient
*/
__isoDate() {
let d = new Date();
d.setMinutes(d.getMinutes() + 5);
return d.toISOString().replace(/\.\d+Z/g, '+0000');
}
/**
* Public method to call async CloudStack APIs in separate executor mode
*
* @param {any} cmd
* @param {any} params
* @param {any} callback
*
* @memberOf CloudStackClient
*/
executeSync(cmd, params, callback) {
if (typeof params === 'function') {
callback = params;
params = {};
} else if (params === null || typeof params === 'undefined') {
params = {};
}
params['command'] = cmd;
params['response'] = 'json';
params['apiKey'] = this.__apiKey;
params['signatureversion'] = 3;
params['expires'] = this.__isoDate();
params['signature'] = this.__calculateSignature(params);
callback = callback || function () { };
let apiURL = url.parse(this.baseUrl)
apiURL.path += qs.stringify(params, { encode: true }).replace(/\%5B(\D*?)\%5D/g, '.$1');
let handleResponse = function (res) {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.once('end', () => {
res.removeAllListeners('close');
if (res.statusCode == 200) {
try {
return callback(null, JSON.parse(data));
} catch (err) {
return callback(err);
}
} else {
//CS 2.2 returns X-Description header
if (res.headers['X-Description']) {
return callback(new APIError(res.statusCode, res.headers['X-Description']));
}
let message = '';
let errorcode = 0;
//CS 4.2 uses JSON response.
if (data) {
try {
let json = JSON.parse(data);
let respName = cmd.toLowerCase() + 'response';
if (json[respName] && json[respName].errortext) {
errorcode = json[respName].errorcode;
message = json[respName].errortext;
}
}
catch (e) {
console.warn(e.message);
}
}
return callback(new APIError(errorcode, message));
}
});
res.once('close', (err) => {
return callback(err);
});
};
let req = this.__http.get(apiURL, handleResponse);
req.once('error', (err) => {
return callback(err);
});
}
/**
* Public method to call async CloudStack APIs in separate executor mode
*
* @param {any} cmd
* @param {any} params
* @param {any} callback
*
* @memberOf CloudStackClient
*/
executeAsync(cmd, params, callback) {
let pollingTime = this.__pollingTime;
this.executeSync(cmd, params, (err, response) => {
if (err) {
return callback(err);
}
let jobid = response[Object.keys(response)[0]].jobid;
let queryJobResult = (polled) => {
this.executeSync('queryAsyncJobResult', { jobid: jobid }, (error, queryResponse) => {
if (error) {
return callback(error);
}
queryResponse = queryResponse.queryasyncjobresultresponse;
// Checking for completion; 0 stands for pending; 1 for successful completion; 2 for error
if (queryResponse.jobstatus === 1 || queryResponse.jobstatus === 2) {
if (queryResponse.jobstatus === 1) {
return callback(null, queryResponse);
}
return callback(new APIError(queryResponse.jobresult.errorcode, queryResponse.jobresult.errortext));
}
// if polling number is not -1 or undefined, or if polling number is reached, respond with timeout
if (this.__pollingNumber !== -1 && !Number.isNaN(this.__pollingNumber) && polled >= this.__pollingNumber) {
return callback(new Error('Timeout'));
}
setTimeout(queryJobResult, this.__pollingTime, ++polled);
});
};
setTimeout(queryJobResult, this.__pollingTime, 0);
});
}
/**
* Private method that acts as a transparent relay agent for executeSync and executeAsync
* in single executor mode
*
* @param {any} cmd
* @param {any} params
* @param {any} callback
*
* @memberOf CloudStackClient
*/
__execute(cmd, params, callback) {
if (this.__asyncCommands[cmd] === true) {
this.executeAsync(cmd, params, callback);
} else {
this.executeSync(cmd, params, callback);
}
}
/**
* Private method that calculates the request signature for CloudStack API calls
* http://docs.cloudstack.apache.org/en/latest/dev.html#signing-api-requests
*
* @param {any} queryDict
* @returns
*
* @memberOf CloudStackClient
*/
__calculateSignature(queryDict) {
let hmac = crypto.createHmac('sha1', this.__secretKey);
let orderedQuery = qs.stringify(queryDict, { encode: true }).replace(/\%5B(\D*?)\%5D/g, '.$1').replace(/\%5B(\d*?)\%5D/g, '[$1]').split('&').sort().join('&').toLowerCase();
hmac.update(orderedQuery);
return hmac.digest('base64');
}
}
module.exports = CloudStackClient;