X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=skyquake%2Fframework%2Futils%2Frw.js;fp=skyquake%2Fframework%2Futils%2Frw.js;h=1cca8f2c4a4fba672a321970645cfd063a93d056;hb=e29efc315df33d546237e270470916e26df391d6;hp=0000000000000000000000000000000000000000;hpb=9c5e457509ba5a1822c316635c6308874e61b4b9;p=osm%2FUI.git diff --git a/skyquake/framework/utils/rw.js b/skyquake/framework/utils/rw.js new file mode 100644 index 000000000..1cca8f2c4 --- /dev/null +++ b/skyquake/framework/utils/rw.js @@ -0,0 +1,924 @@ + +/* + * + * 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;