lib/services/lookup.js
// Copyright 2015, EMC, Inc.
'use strict';
module.exports = lookupServiceFactory;
lookupServiceFactory.$provide = 'Services.Lookup';
lookupServiceFactory.$inject = [
'Promise',
'Services.Waterline',
'Services.Configuration',
'Assert',
'Errors',
'_',
'lru-cache',
'ARPCache',
'ChildProcess'
];
/**
* Provides a LookupService singleton.
* @private
* @param {Promise} Promise
* @param {Waterline} waterline
* @param {Assert} assert
* @param {Errors} errors
* @param {lodash} _
* @param {lru-cache} LRU Cache Package
*
* @return {LookupService} An instance of the LookupService.
*/
function lookupServiceFactory(
Promise,
waterline,
configuration,
assert,
Errors,
_,
LRU,
arpCache,
ChildProcess
) {
// TODO: stolen from http-service could stand to be a re-usable helper.
/**
* Get remote address of the client.
* @private
* @param {req} req from express
* @returns {String|Undefined} either the ip of requester or undefined
* if unavailable
*/
function remoteAddress(req) {
assert.ok(req, 'req');
if(req.get("X-Real-IP")) {
return req.get("X-Real-IP");
}
if (req.ip) {
return req.ip;
}
if (req._remoteAddress) {
return req._remoteAddress;
}
if (req.connection) {
return req.connection.remoteAddress;
}
// TODO: This doesn't appear to ever get hit and causes an assertion
// in the IP lookup due to undefined not being an IP so we need to investigate
// what to do in this particular case (which again doesn't seem to have ever
// actually occurred)
return undefined;
}
/**
* Provides functionality to correlate various identifiers
* to their appropriate counterparts.
* @constructor
*/
function LookupService () {
this.resetNodeIdCache();
this.resetMacRequests();
}
/**
* Reset all cached Node IDs.
*/
LookupService.prototype.resetNodeIdCache = function () {
if (this.nodeIdCache) {
this.nodeIdCache.reset();
return;
}
this.nodeIdCache = LRU({
max: 500,
maxAge: 30000
});
};
LookupService.prototype.resetMacRequests = function () {
if (configuration.get('externalLookupHelper', null)) {
this.macRequests = {};
} else {
delete this.macRequests;
}
};
/**
* Look up an entry in the lookups collection by IP. Optionally fall back
* to other mechanisms if we can't find an entry.
* @private
* @param {ip} IP address to look for
* @returns lookups record
*/
LookupService.prototype.lookupByIP = function (ip) {
return this.validateArpCache()
.then(function() {
return waterline.lookups.findOneByTerm(ip);
});
};
/**
* Look up an entry in the lookups collection by MAC. Optionally fall back
* to other mechanisms if we can't find an entry.
* @private
* @param {mac} MAC address to look for
* @returns lookups record
*/
LookupService.prototype.lookupByMAC = function (mac) {
var self = this;
return this.validateArpCache()
.then(function() {
return waterline.lookups.findOneByTerm(mac);
})
.catch(function (error) {
var helper = configuration.get('externalLookupHelper', null);
if (helper !== null) {
return self.runExternalHelper(helper, mac).then(function () {
return waterline.lookups.findOneByTerm(mac);
});
}
throw error;
});
};
/**
* Run an external helper script to fill in missing lookup entries.
* Only one instance of the helper script is run at any given time for
* any given MAC address. All other requests will be resolved when the
* running instance returns.
* @private
* @param {helperPath} Path to the lookup script.
* @param {mac} The MAC address we need information on.
*/
LookupService.prototype.runExternalHelper = function (helperPath, mac) {
var self = this;
if (mac in this.macRequests) {
return new Promise(function (resolve, reject) {
var responder = {resolve: resolve, reject: reject};
self.macRequests[mac].push(responder);
});
}
var helper = new ChildProcess(helperPath, [mac]);
this.macRequests[mac] = [];
return this.processHelperResults(helper).then(function () {
self.handleHelperPromises(mac);
});
};
/**
* Process the output from an external lookup helper script.
* The expected output is one or more lines of text of the form:
* <MAC address> <IP address>
* Each entry will be turned into arguments for this.setIpAddress.
* @private
* @param {helper} The ChildProcess object representing the helper script.
*/
LookupService.prototype.processHelperResults = function (helper) {
var self = this;
return helper.run().then(function (ret) {
return Promise.map(ret.stdout.split('\n'), function (line) {
if (!line) {
return;
}
var items = line.split(' ');
return self.setIpAddress(items[1], items[0]);
});
});
};
/**
* Clean up remaining helper script promises if any are waiting on the
* completion of a lookup script.
* @private
* @param {mac} The MAC address that was being updated.
*/
LookupService.prototype.handleHelperPromises = function (mac) {
var requests = this.macRequests[mac] || [];
delete this.macRequests[mac];
requests.forEach(function (responder) {
responder.resolve();
});
};
/**
* Reset the cache for a specific IP or MAC address.
* @param {String} cache key - IP or MAC address.
*/
LookupService.prototype.clearNodeIdCache = function (key) {
this.assignNodeIdCache(key, null);
};
LookupService.prototype.clearNodeIdCacheAndThrow = function (key, error) {
this.clearNodeIdCache(key);
throw error;
};
/**
* Check the cache for a Node ID by IP or MAC addres.
* @param {String} cache key - IP or MAC address.
* @return {Promise|null} Returns a promise if the Node ID is cached,
otherwise null.
*/
LookupService.prototype.checkNodeIdCache = function (key) {
var current = this.nodeIdCache.get(key);
if (current === null || typeof current === 'string') {
return new Promise(function (resolve, reject) {
var responder = {resolve: resolve, reject: reject};
this.handleCachePromise(responder, current);
}.bind(this));
}
if (Array.isArray(current)) {
return new Promise(function (resolve, reject) {
var buffer = this.nodeIdCache.get(key),
responder = {resolve: resolve, reject: reject};
if (Array.isArray(buffer)) {
buffer.push(responder);
}
else {
this.handleCachePromise(responder);
}
}.bind(this));
}
this.nodeIdCache.set(key, []);
return null;
};
/**
* Cache Node ID by either an IP or MAC address.
* @param {String} cache key - IP or MAC address.
* @param {String} cache value - Node ID.
* @return {String} value converted to a string.
*/
LookupService.prototype.assignNodeIdCache = function(key, value) {
value = value && value.toString();
var buffer = this.nodeIdCache.peek(key);
this.nodeIdCache.set(key, value);
if (Array.isArray(buffer)) {
buffer.forEach(function (responder) {
this.handleCachePromise(responder, value);
}.bind(this));
}
return value;
};
LookupService.prototype.handleCachePromise = function (responder, value) {
if (!value) {
return responder.reject(
new Errors.NotFoundError(
'Lookup Record Not Found (handleCachePromise)',
null
)
);
}
responder.resolve(value);
};
/**
* Provides a middleware function suitable for
* looking up the request IP via express and adding a req.macaddress and
* req.macAddress value representing the MAC address used for the HTTP
* request.
* @return {function} An express middleware function.
*/
LookupService.prototype.ipAddressToMacAddressMiddleware = function () {
var self = this;
return function(req, res, next) {
self.ipAddressToMacAddress(remoteAddress(req)).then(function (macAddress) {
// Provide both lower case and camel case versions.
req.macaddress = macAddress;
req.macAddress = macAddress;
}).finally(function() {
next();
});
};
};
/**
* Converts the given IP address into a MAC address
* via a lookup in the leaseCache.
* @param {String} ip IP Address to correlate to a MAC Address.
* @return {Promise.<String>} A promise fulfilled to the MAC address which
* correlates to the provided IP.
*/
LookupService.prototype.ipAddressToMacAddress = function (ip) {
assert.isIP(ip);
return this.lookupByIP(ip).then(function (record) {
return record.macAddress;
});
};
/**
* macAddressToNode converts the given MAC address into a Node document via
* a lookup in the DomainService.
* @param {String} macAddress MAC Address to correlate to a Node.
* @return {Promise.<NodeDocument>} A promise fulfilled to the Node which
* correlates to the provided MAC Address.
*/
LookupService.prototype.macAddressToNode = function (macAddress) {
return this.lookupByMAC(macAddress).then(function (record) {
if (record.node) {
return waterline.nodes.needOneById(record.node);
} else {
throw new Errors.NotFoundError(
'Lookup Record Not Found (macAddressToNode)',
record
);
}
});
};
/**
* Returns the IP Address associated with the given MAC address.
* @param {String} macAddress MAC Address to correlate with an IP.
* @return {Promise.<String>} A promise fulfilled with the IP Address which
* correlates to the provided MAC Address.
*/
LookupService.prototype.macAddressToIp = function (macAddress) {
return this.lookupByMAC(macAddress).then(function (record) {
if (record.ipAddress) {
return record.ipAddress;
} else {
throw new Errors.NotFoundError(
'Lookup Record Not Found (macAddressToIp)',
record
);
}
});
};
/**
* Converts the given MAC address into the BSON ID of the node document via
* a lookup in the DomainService. This function will cache values to avoid
* excessive round trips to the DB, so its use is recommended when only the
* BSON ID is needed.
* @param {String} macAddress MAC Address to correlate to a Node.
* @return {Promise.<ObjectId>} A promise fulfilled to the Node ID which
* correlates to the provided MAC Address.
*/
LookupService.prototype.macAddressToNodeId = function (macAddress) {
var self = this;
var cachePromise = self.checkNodeIdCache(macAddress);
if (cachePromise) {
return cachePromise;
}
return this.lookupByMAC(macAddress)
.then(function (record) {
if (record.node) {
return self.assignNodeIdCache(macAddress, record.node);
}
else {
self.clearNodeIdCache(macAddress);
throw new Errors.NotFoundError(
'Lookup Record Not Found (macAddressToNodeId)',
record
);
}
})
.catch(self.clearNodeIdCacheAndThrow.bind(self, macAddress));
};
/**
* Converts the given IP Address into a Node document via
* a lookup using the leaseCache & DomainService.
* @param {String} ip IP Address to correlate to a Node.
* @return {Promise.<NodeDocument>} A promise fulfilled to the Node which
* correlates to the provided IP Address.
*/
LookupService.prototype.ipAddressToNode = function (ip) {
assert.isIP(ip);
return this.lookupByIP(ip).then(function (record) {
if (record.node) {
return waterline.nodes.needOneById(record.node);
} else {
throw new Errors.NotFoundError(
'Lookup Record Not Found (ipAddressToNode)',
record
);
}
});
};
/**
* Converts the given IP Address into the BSON ID of the Node via
* a lookup using the leaseCache & DomainService. This function will cache
* values to avoid excessive round trips to the DB, so its use is
* recommended when only the BSON ID is needed.
* @param {String} ip IP Address to correlate to a Node.
* @return {Promise.<ObjectId>} A promise fulfilled to the Node ID which
* correlates to the provided IP Address.
*/
LookupService.prototype.ipAddressToNodeId = function (ip) {
assert.isIP(ip);
var self = this;
var cachePromise = self.checkNodeIdCache(ip);
if (cachePromise) {
return cachePromise;
}
return this.lookupByIP(ip).then(function (record) {
if (record.node) {
return self.assignNodeIdCache(ip, record.node);
}
else {
self.clearNodeIdCache(ip);
throw new Errors.NotFoundError(
'Lookup Record Not Found (ipAddressToNodeId)',
record
);
}
})
.catch(self.clearNodeIdCacheAndThrow.bind(self, ip));
};
/**
* Returns a list of IP Addresses for the given Node ID.
* @param {String} id Node ID
* @return {Promise.<Array>} A promise fulfilled with an array of
* IP Addresses associated with the Node.
*/
LookupService.prototype.nodeIdToIpAddresses = function (id) {
var self = this;
assert.isMongoId(id);
return self.validateArpCache()
.then(function() {
return waterline.lookups.findByTerm(id).then(function (records) {
return _.reduce(records, function (ipAddresses, record) {
if (record.ipAddress) {
ipAddresses.push(record.ipAddress);
}
return ipAddresses;
}, []);
});
});
};
/**
* Returns a list of object containing IP and Mac Addresses for the given Node ID.
* @param {String} id Node ID
* @return {Promise.<Array>} A promise fulfilled with an array of
* objects containing IP Addresses and Mac Addresses associated with the Node.
*/
LookupService.prototype.findIpMacAddresses = function (nodeId) {
var self = this;
return self.validateArpCache()
.then(function() {
return waterline.lookups.find({node: nodeId}).then(function (records) {
return _.reduce(records, function (ipMacAddresses, record) {
ipMacAddresses.push(_.pick(record, ['ipAddress', 'macAddress']));
return ipMacAddresses;
}, []);
});
});
};
/**
* Returns the proxy server for the given Node ID.
* @param {String} id Node ID
* @return {Promise.<String>} A promise fulfilled to the Proxy server URL
* that corresponds to this node
*/
LookupService.prototype.nodeIdToProxy = function (id) {
return waterline.lookups.findOneByTerm(id).then(function (record) {
if (record && record.proxy) {
return record.proxy;
} else {
return undefined;
}
});
};
LookupService.prototype.setIpAddress = function (ip, mac) {
return waterline.lookups.setIp(ip, mac);
};
LookupService.prototype.validateArpCache = function () {
var self = this;
return Promise.resolve().then(function() {
if(configuration.get('arpCacheEnabled', true)) {
return arpCache.getCurrent()
.map(function(entry) {
return self.setIpAddress(entry.ip, entry.mac);
});
}
});
};
LookupService.prototype.start = function () {
return Promise.resolve();
};
LookupService.prototype.stop = function () {
return Promise.resolve();
};
return new LookupService();
}