blob: 1cca8f2c4a4fba672a321970645cfd063a93d056 [file] [log] [blame]
/*
*
* Copyright 2016 RIFT.IO Inc
*
* 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.
*
*/
// rw.js will no longer be necessary when Angular dependency no longer exists
/**
* reset values in an array, useful when an array instance is
* being observed for changes and simply setting array reference
* to a new array instance is not a great option.
*
* Example:
* x = ['a', 'b']
* setValues(x, ['c', 'd'])
* x
* ['c', 'd']
*/
var rw = rw || {
// Angular specific for now but can be modified in one location if need ever be
BaseController: function() {
var self = this;
// handles factories with detach/cancel methods and listeners with cancel method
if (this.$scope) {
this.$scope.$on('$stateChangeStart', function() {
var properties = Object.getOwnPropertyNames(self);
properties.forEach(function(key) {
var propertyValue = self[key];
if (propertyValue) {
if (Array.isArray(propertyValue)) {
propertyValue.forEach(function(item) {
if (item.off && typeof item.off == 'function') {
item.off(null, null, self);
}
});
propertyValue.length = 0;
} else {
if (propertyValue.detached && typeof propertyValue.detached == 'function') {
propertyValue.detached();
} else if (propertyValue.cancel && typeof propertyValue.cancel == 'function') {
propertyValue.cancel();
} else if (propertyValue.off && typeof propertyValue.off == 'function') {
propertyValue.off(null, null, self);
}
}
}
});
});
};
// call in to do additional cleanup
if (self.doCleanup && typeof self.doCleanup == 'function') {
self.doCleanup();
};
},
getSearchParams: function (url) {
var a = document.createElement('a');
a.href = url;
var params = {};
var items = a.search.replace('?', '').split('&');
for (var i = 0; i < items.length; i++) {
if (items[i].length > 0) {
var key_value = items[i].split('=');
params[key_value[0]] = key_value[1];
}
}
return params;
},
inplaceUpdate : function(ary, values) {
var args = [0, ary.length];
Array.prototype.splice.apply(ary, args.concat(values));
}
};
// explore making this configurable somehow
// api_server = 'http://localhost:5050';
rw.search_params = rw.getSearchParams(window.location.href);
// MONKEY PATCHING
if (Element.prototype == null) {
Element.prototype.uniqueId = 0;
}
Element.prototype.generateUniqueId = function() {
Element.prototype.uniqueId++;
return 'uid' + Element.prototype.uniqueId;
};
Element.prototype.empty = Element.prototype.empty || function() {
while(this.firstChild) {
this.removeChild(this.firstChild);
}
};
/**
* Merge one object into another. No circular reference checking so if there
* is there might be infinite recursion.
*/
rw.merge = function(obj1, obj2) {
for (prop in obj2) {
if (typeof(obj2[prop]) == 'object') {
if (prop in obj1) {
this.merge(obj1[prop], obj2[prop]);
} else {
obj1[prop] = obj2[prop];
}
} else {
obj1[prop] = obj2[prop];
}
}
}
Element.prototype.getElementByTagName = function(tagName) {
for (var i = this.children.length - 1; i >= 0; i--) {
if (this.children[i].localName == tagName) {
return this.children[i];
}
}
};
rw.ui = {
computedWidth : function(elem, defaultValue) {
var s = window.getComputedStyle(elem);
var w = s['width'];
if (w && w != 'auto') {
// I've never seen this case, but here anyway
return w;
}
w = s['min-width'];
if (w) {
return w;
}
return defaultValue;
},
computedHeight : function(elem, defaultValue) {
var s = window.getComputedStyle(elem);
var w = s['height'];
if (w && w != 'auto') {
// I've never seen this case, but here anyway
return w;
}
w = s['min-height'];
if (w) {
return w;
}
return defaultValue;
},
computedStyle : function(elem, property, defaultValue) {
var s = window.getComputedStyle(elem);
if (s[property]) {
return s[property];
}
return defaultValue;
},
odd : function(n) {
return Math.abs(n) % 2 == 1 ? 'odd' : '';
},
status : function(s) {
return s == 'OK' ? 'yes' : 'no';
},
capitalize: function(s) {
return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
},
fmt: function(n, fmtStr) {
return numeral(n).format(fmtStr);
},
// assumes values are in megabytes!
bytes: function(n, capacity) {
if (n === undefined || isNaN(n)) {
return '';
}
var units = false;
if (capacity === undefined) {
capacity = n;
units = true;
}
var suffixes = [
['KB' , 1000],
['MB' , 1000000],
['GB' , 1000000000],
['TB' , 1000000000000],
['PB' , 1000000000000000]
];
for (var i = 0; i < suffixes.length; i++) {
if (capacity < suffixes[i][1]) {
return (numeral((n * 1000) / suffixes[i][1]).format('0,0') + (units ? suffixes[i][0] : ''));
}
}
return n + (units ? 'B' : '');
},
// assumes values are already in megabits!
bits: function(n, capacity) {
if (n === undefined || isNaN(n)) {
return '';
}
var units = false;
if (capacity === undefined) {
capacity = n;
units = true;
}
var suffixes = [
['Mbps' , 1000],
['Gbps' , 1000000],
['Tbps' , 1000000000],
['Pbps' , 1000000000000]
];
for (var i = 0; i < suffixes.length; i++) {
if (capacity < suffixes[i][1]) {
return (numeral((n * 1000) / suffixes[i][1]).format('0,0') + (units ? suffixes[i][0] : ''));
}
}
return n + (units ? 'Bps' : '');
},
ppsUtilization: function(pps) {
return pps ? numeral(pps / 1000000).format('0.0') : '';
},
ppsUtilizationMax: function(item) {
var rate = item.rate / 10000;
var max = item.max * 0.0015;
return rate/max;
},
bpsAsPps: function(speed) {
return parseInt(speed) * 0.0015;
},
upperCase: function(s) {
return s.toUpperCase()
},
mbpsAsPps: function(mbps) {
var n = parseInt(mbps);
return isNaN(n) ? 0 : rw.ui.fmt(rw.ui.bpsAsPps(n * 1000000), '0a').toUpperCase();
},
k: function(n) {
return rw.ui.fmt(rw.ui.noNaN(n), '0a');
},
noNaN: function(n) {
return isNaN(n) ? 0 : n;
},
// Labels used in system
l10n : {
vnf: {
'trafsimclient': 'Traf Sim Client',
'trafsimserver': 'Traf Sim Server',
'ltemmesim': 'MME',
'ltegwsim': 'SAE Gateway',
'trafgen': 'Traf Gen Client',
'trafsink': 'Traf Gen Server',
'loadbal': 'Load Balancer',
'slbalancer': 'Scriptable Load Balancer'
}
}
};
rw.math = {
editXml : function(xmlTemplate, domEditor) {
var str2dom = new DOMParser();
var dom = str2dom.parseFromString(xmlTemplate, 'text/xml');
if (domEditor) {
domEditor(dom);
}
var dom2str = new XMLSerializer();
return dom2str.serializeToString(dom);
},
num : function(el, tag) {
return parseInt(this.str(el, tag), 10);
},
str : function(el, tag) {
var tags = el.getElementsByTagName(tag);
return tags.length > 0 ? tags[0].textContent.trim() : '';
},
sum : function(total, i, key, value) {
if (_.isNumber(value)) {
total[key] = (i === 0 ? value : (total[key] + value));
}
},
sum2 : function(key) {
return function(prev, cur, i) {
var value = cur[key];
if (_.isNumber(value)) {
if (typeof(prev) === 'undefined') {
return value;
} else {
return prev + value;
}
}
return prev;
};
},
max : function(key) {
return function(prev, cur, i) {
var value = cur[key];
if (_.isNumber(value)) {
if (typeof(prev) === 'undefined') {
return value;
} else if (prev < value) {
return value;
}
}
return prev;
};
},
avg2 : function(key) {
var sum = rw.math.sum2(key);
return function(prev, cur, i, ary) {
var s = sum(prev, cur, i);
if (i === ary.length - 1) {
return s / ary.length;
}
return s;
};
},
avg : function(rows, key) {
var total = XmlMath.total(rows, key);
return total / rows.length;
},
total : function(rows, key) {
var total = 0;
for (var i = rows.length - 1; i >= 0; i--) {
var n = parseInt(rows[i][key]);
if (!isNaN(n)) {
total += n;
}
}
return total;
},
run : function(total, rows, operation) {
var i;
var f = function(value, key) {
operation(total, i, key, value);
};
for (i = 0; i < rows.length; i++) {
_.each(rows[i], f);
}
}
};
rw.db = {
open: function (name, onInit, onOpen) {
var self = this;
var open = window.indexedDB.open(name, 2);
open.onerror = function (e) {
console.log('Could not open database', name, e.target.error.message);
};
open.onsuccess = function (e) {
var db = e.target.result;
onOpen(db);
};
open.onupgradeneeded = function (e) {
var db = e.target.result;
onInit(db);
};
}
};
rw.db.Offline = function(name) {
this.name = name;
this.datastore = 'offline';
};
rw.db.Offline.prototype = {
open : function(onOpen) {
rw.db.open(this.name, this.init.bind(this), onOpen);
},
getItem : function(url, onData) {
var self = this;
this.open(function(db) {
var query = db.transaction(self.datastore)
.objectStore(self.datastore)
.get(url);
query.onsuccess = function(e) {
if (e.target.result) {
onData(e.target.result.data);
} else {
console.log('No data found for ' + url + '. You may need to rebuild your offline database');
}
}
});
},
init : function(db) {
var self = this;
if (!db.objectStoreNames.contains(this.datastore)) {
var create = db.createObjectStore(this.datastore, {keyPath: 'url'});
create.onerror = function(e) {
console.log('Could not create object store ' + this.datastore);
}
}
},
saveStore : function(store) {
var self = this;
this.open(function(db) {
for (var i = 0; i < store.length; i++) {
var save = db.transaction(self.datastore, "readwrite")
.objectStore(self.datastore)
.put(store[i]);
}
});
}
};
rw.api = {
nRequests : 0,
tRequestsMs: 0,
tRequestMaxMs: 0,
nRequestsByUrl: {},
statsId: null,
resetStats: function() {
rw.api.nRequests = 0;
rw.api.tRequestsMs = 0;
rw.api.tRequestMaxMs = 0;
rw.api.nRequestsByUrl = {};
},
handleAjaxError : function (req, status, err) {
console.log('failed', req, status, err);
},
json: function(url) {
return this.get(url, 'application/json')
},
get: function(url, accept) {
var deferred = jQuery.Deferred();
if (rw.api.offline) {
rw.api.offline.getItem(url, function (data) {
deferred.resolve(data);
});
} else {
var startTime = new Date().getTime();
jQuery.ajax(rw.api.server + url, {
type: 'GET',
dataType: 'json',
error: rw.api.handleAjaxError,
headers: rw.api.headers(accept),
success: function (data) {
rw.api.recordUrl(url, startTime);
deferred.resolve(data);
},
error: function(e) {
deferred.reject(e);
}
});
}
return deferred.promise();
},
headers: function(accept, contentType) {
var h = {
Accept: accept
};
if (rw.api.statsId != null) {
h['x-stats'] = rw.api.statsId;
}
if (contentType) {
h['Content-Type'] = contentType;
}
return h;
},
recordUrl:function(url, startTime) {
var elapsed = new Date().getTime() - startTime;
rw.api.tRequestsMs += elapsed;
rw.api.nRequests += 1;
rw.api.tRequestMaxMs = Math.max(rw.api.tRequestMaxMs, elapsed);
if (url in rw.api.nRequestsByUrl) {
var metric = rw.api.nRequestsByUrl[url];
metric.url = url;
metric.n += 1;
metric.max = Math.max(metric.max, elapsed);
} else {
rw.api.nRequestsByUrl[url] = {url: url, n: 1, max: elapsed};
}
},
put: function(url, data, contentType) {
return this.push('PUT', url, data, contentType);
},
post: function(url, data, contentType) {
return this.push('POST', url, data, contentType);
},
rpc: function(url, data, error) {
if(error === undefined){
error = function(a,b,c){
}
}
return this.push('POST', url, data, 'application/vnd.yang.data+json', error);
},
push: function(method, url, data, contentType, errorFn) {
var deferred = jQuery.Deferred();
if (rw.api.offline) {
// eating offline put request
if(contentType == 'application/vnd.yang.data+json'){
var payload = data;
rw.api.offline.getItem(url, function (data) {
deferred.resolve(data);
});
}
deferred.resolve({});
} else {
var startTime = new Date().getTime();
jQuery.ajax(rw.api.server + url, {
type: method,
error: rw.api.handleAjaxError,
dataType: 'json',
headers: rw.api.headers('application/json', contentType),
data: JSON.stringify(data),
success: function (response) {
rw.api.recordUrl(url, startTime);
deferred.resolve(response);
},
error: errorFn
});
}
return deferred.promise();
},
setOffline: function(name) {
if (name) {
rw.api.offline = new rw.db.Offline(name);
} else {
rw.api.offline = false;
}
},
// When passing things to ConfD ('/api/...') then '/' needs to be
// %252F(browser) --> %2F(Rest) --> / ConfD
encodeUrlParam: function(value) {
var once = rw.api.singleEncodeUrlParam(value);
return once.replace(/%/g, '%25');
},
// UrlParam cannot have '/' and encoding it using %2F gets double-encoded in flask
singleEncodeUrlParam: function(value) {
return value.replace(/\//g, '%2F');
}
};
rw.api.SocketSubscriber = function(url) {
this.url = url;
this.id = ++rw.api.SocketSubscriber.uniqueId;
this.subscribed = false;
this.offlineRateMs = 2000;
},
rw.api.SocketSubscriber.uniqueId = 0;
rw.api.SocketSubscriber.prototype = {
// does not support PUT/PORT with payloads requests yet
websubscribe : function(webUrl, onload, offline) {
this.subscribeMeta(onload, {
url: webUrl
}, offline);
},
subscribeMeta : function(onload, meta, offline) {
var self = this;
if (this.subscribed) {
this.unsubscribe();
}
var m = meta || {};
if (rw.api.offline) {
this.subscribeOffline(onload, m, offline);
} else {
this.subscribeOnline(onload, m);
}
},
subscribeOffline: function(onload, meta, offline) {
var self = this;
rw.api.json(meta.url).done(function(data) {
var _update = function() {
if (offline) {
offline(data);
} else {
onload(data);
}
};
this.offlineTimer = setInterval(_update, self.offlineRateMs);
});
},
subscribeOnline: function(onload, meta) {
var self = this;
var _subscribe = function() {
meta.widgetId = self.id;
meta.accept = meta.accept || 'application/json';
document.websocket().emit(self.url, meta);
self.subscribed = true;
};
var _register = function() {
document.websocket().on(self.url + '/' + self.id, function(dataString) {
var data = dataString;
// auto convert to object to make backward compatible
if (meta.accept.match(/[+\/]json$/) && dataString != '') {
data = JSON.parse(dataString);
}
onload(data);
});
_subscribe();
};
document.websocket().on('error',function(d){
console.log('socket error', d)
});
document.websocket().on('close',function(d){
console.log('socket close', d)
})
document.websocket().on('connect', _register);
// it's possible this call is not nec. and will always be called
// as part of connect statement above
_register();
},
unsubscribe : function() {
if (rw.api.offline) {
if (this.offlineTimer) {
clearInterval(this.offlineTimer);
this.offlineTimer = null;
}
} else {
var unsubscribe = { widgetId: this.id, enable: false };
document.websocket().emit(this.url, unsubscribe);
this.subscribed = false;
}
}
};
rw.api.server = rw.search_params['api_server'] || '';
document.websocket = function() {
if ( ! this.socket ) {
//io.reconnection = true;
var wsServer = rw.api.server || 'http://' + document.domain + ':' + location.port;
var wsUrl = wsServer + '/rwapp';
this.socket = io.connect(wsUrl, {reconnection:true});
}
return this.socket;
};
rw.api.setOffline(rw.search_params['offline']);
rw.vnf = {
ports: function(service) {
return _.flatten(jsonPath.eval(service, '$.connector[*].interface[*].port'));
},
fabricPorts: function(service) {
return _.flatten(jsonPath.eval(service, '$.vm[*].fabric.port'));
}
};
rw.VcsVisitor = function(enter, leave) {
this.enter = enter;
this.leave = leave;
}
rw.VcsVisitor.prototype = {
visit: function(node, parent, i, listType) {
var hasChildren = this.enter(parent, node, i, listType);
if (hasChildren) {
switch (rw.vcs.nodeType(node)) {
case 'rwsector':
this.visitChildren(node, node.collection, 'collection');
break;
case 'rwcolony':
this.visitChildren(node, node.collection, 'collection');
this.visitChildren(node, node.vm, 'vm');
break;
case 'rwcluster':
this.visitChildren(node, node.vm, 'vm');
break;
case 'RWVM':
this.visitChildren(node, node.process, 'process');
break;
case 'RWPROC':
this.visitChildren(node, node.tasklet, 'tasklet');
break;
}
}
if (this.leave) {
this.leave(parent, node, obj, i, listType);
}
},
visitChildren : function(parent, children, listType) {
if (!children) {
return;
}
var i = 0;
var self = this;
_.each(children, function(child) {
self.visit.call(self, child, parent, i, listType);
i += 1;
});
}
};
rw.vcs = {
allVms : function() {
return _.flatten([this.jpath('$.collection[*].vm'), this.jpath('$.collection[*].collection[*].vm')], true);
},
vms: function(n) {
if (n == undefined || n === null || n === this) {
return this.allVms();
}
switch (rw.vcs.nodeType(n)) {
case 'rwcolony':
return this.jpath('$.collection[*].vm[*]', n);
case 'rwcluster':
return this.jpath('$.vm[*]', n);
case 'RWVM':
return [n];
default:
return null;
}
},
nodeType: function(node) {
if (node.component_type === 'RWCOLLECTION') {
return node.collection_info['collection-type'];
}
return node.component_type;
},
allClusters : function() {
return this.jpath('$.collection[*].collection');
},
allColonies: function() {
return this.jpath('$.collection');
},
allPorts:function(n) {
switch (rw.vcs.nodeType(n)) {
case 'rwsector':
return this.jpath('$.collection[*].collection[*].vm[*].port[*]', n);
case 'rwcolony':
return this.jpath('$.collection[*].vm[*].port[*]', n);
case 'rwcluster':
return this.jpath('$.vm[*].port[*]', n);
case 'RWVM':
return this.jpath('$.port[*]', n);
default:
return null;
}
},
allFabricPorts:function(n) {
switch (rw.vcs.nodeType(n)) {
case 'rwsector':
return this.jpath('$.collection[*].collection[*].vm[*].fabric.port[*]', n);
case 'rwcolony':
return this.jpath('$.collection[*].vm[*].fabric.port[*]', n);
case 'rwcluster':
return this.jpath('$.vm[*].fabric.port[*]', n);
case 'RWVM':
return this.jpath('$.fabric.port[*]', n);
default:
return null;
}
},
getChildren: function(n) {
switch (rw.vcs.nodeType(n)) {
case 'rwcolony':
return 'vm' in n ? _.union(n.collection, n.vm) : n.collection;
case 'rwcluster':
return n.vm;
case 'RWVM':
return n.process;
case 'RWPROC':
return n.tasklet;
}
return [];
},
jpath : function(jpath, n) {
return _.flatten(jsonPath.eval(n || this, jpath), true);
}
};
rw.trafgen = {
startedActual : null, // true or false once server-side state is loaded
startedPerceived : false,
ratePerceived : 25,
rateActual : null, // non-null once server-side state is loaded
packetSizePerceived : 1024,
packetSizeActual: null
};
rw.trafsim = {
startedActual : null, // true or false once server-side state is loaded
startedPerceived : false,
ratePerceived : 50000,
rateActual : null, // non-null once server-side state is loaded
maxRate: 200000
};
rw.aggregateControlPanel = null;
rw.theme = {
// purple-2 and blue-2
txBps : 'hsla(212, 57%, 50%, 1)',
txBpsTranslucent : 'hsla(212, 57%, 50%, 0.7)',
rxBps : 'hsla(212, 57%, 50%, 1)',
rxBpsTranslucent : 'hsla(212, 57%, 50%, 0.7)',
txPps : 'hsla(260, 35%, 50%, 1)',
txPpsTranslucent : 'hsla(260, 35%, 50%, 0.7)',
rxPps : 'hsla(260, 35%, 50%, 1)',
rxPpsTranslucent : 'hsla(260, 35%, 50%, 0.7)',
memory : 'hsla(27, 98%, 57%, 1)',
memoryTranslucent : 'hsla(27, 98%, 57%, 0.7)',
cpu : 'hsla(123, 45%, 50%, 1)',
cpuTranslucent : 'hsla(123, 45%, 50%, 0.7)',
storage : 'hsla(180, 78%, 25%, 1)',
storageTranslucent : 'hsla(180, 78%, 25%, 0.7)'
};
rw.RateTimer = function(onChange, onStop) {
this.rate = 0;
this.onChange = onChange;
this.onStop = onStop;
this.testFrequency = 500;
this.testWaveLength = 5000;
this.timer = null;
return this;
};
rw.RateTimer.prototype = {
start: function() {
this.rate = 0;
if (!this.timer) {
this.n = 0;
var strategy = this.smoothRateStrategy.bind(this);
this.testingStartTime = new Date().getTime();
this.timer = setInterval(strategy, this.testFrequency);
}
},
stop: function() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
this.onStop();
}
},
smoothRateStrategy: function() {
var x = (new Date().getTime() - this.testingStartTime) / this.testWaveLength;
this.rate = Math.round(100 * 0.5 * (1 - Math.cos(x)));
// in theory you could use wavelength and frequency to determine stop but this
// is guaranteed to stop at zero.
this.onChange(this.rate);
this.n += 1;
if (this.n >= 10 && this.rate < 1) {
this.stop();
}
}
};
module.exports = rw;