Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • opentrafficshaper/opentrafficshaper
  • eezysol/opentrafficshaper
  • sudesh/opentrafficshaper
  • Yuriy/opentrafficshaper
  • nkukard/opentrafficshaper
  • fcardoso/opentrafficshaper
6 results
Show changes
Showing
with 2185 additions and 618 deletions
/* Flot plugin for stacking data sets rather than overlyaing them.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin assumes the data is sorted on x (or y if stacking horizontally).
For line charts, it is assumed that if a line has an undefined gap (from a
null point), then the line above it should have the same gap - insert zeros
instead of "null" if you want another behaviour. This also holds for the start
and end of the chart. Note that stacking a mix of positive and negative values
in most instances doesn't make sense (so it looks weird).
Two or more series are stacked when their "stack" attribute is set to the same
key (which can be any number or string or just "true"). To specify the default
stack, you can set the stack option like this:
series: {
stack: null/false, true, or a key (number/string)
}
You can also specify it for a single series, like this:
$.plot( $("#placeholder"), [{
data: [ ... ],
stack: true
}])
The stacking order is determined by the order of the data series in the array
(later series end up on top of the previous).
Internally, the plugin modifies the datapoints in each series, adding an
offset to the y value. For line series, extra data points are inserted through
interpolation. If there's a second y value, it's also adjusted (e.g for bar
charts or filled areas).
*/(function(e){function n(e){function t(e,t){var n=null;for(var r=0;r<t.length;++r){if(e==t[r])break;t[r].stack==e.stack&&(n=t[r])}return n}function n(e,n,r){if(n.stack==null||n.stack===!1)return;var i=t(n,e.getData());if(!i)return;var s=r.pointsize,o=r.points,u=i.datapoints.pointsize,a=i.datapoints.points,f=[],l,c,h,p,d,v,m=n.lines.show,g=n.bars.horizontal,y=s>2&&(g?r.format[2].x:r.format[2].y),b=m&&n.lines.steps,w=!0,E=g?1:0,S=g?0:1,x=0,T=0,N,C;for(;;){if(x>=o.length)break;N=f.length;if(o[x]==null){for(C=0;C<s;++C)f.push(o[x+C]);x+=s}else if(T>=a.length){if(!m)for(C=0;C<s;++C)f.push(o[x+C]);x+=s}else if(a[T]==null){for(C=0;C<s;++C)f.push(null);w=!0,T+=u}else{l=o[x+E],c=o[x+S],p=a[T+E],d=a[T+S],v=0;if(l==p){for(C=0;C<s;++C)f.push(o[x+C]);f[N+S]+=d,v=d,x+=s,T+=u}else if(l>p){if(m&&x>0&&o[x-s]!=null){h=c+(o[x-s+S]-c)*(p-l)/(o[x-s+E]-l),f.push(p),f.push(h+d);for(C=2;C<s;++C)f.push(o[x+C]);v=d}T+=u}else{if(w&&m){x+=s;continue}for(C=0;C<s;++C)f.push(o[x+C]);m&&T>0&&a[T-u]!=null&&(v=d+(a[T-u+S]-d)*(l-p)/(a[T-u+E]-p)),f[N+S]+=v,x+=s}w=!1,N!=f.length&&y&&(f[N+2]+=v)}if(b&&N!=f.length&&N>0&&f[N]!=null&&f[N]!=f[N-s]&&f[N+1]!=f[N-s+1]){for(C=0;C<s;++C)f[N+s+C]=f[N+C];f[N+1]=f[N-s+1]}}r.points=f}e.hooks.processDatapoints.push(n)}var t={series:{stack:null}};e.plot.plugins.push({init:n,options:t,name:"stack",version:"1.2"})})(jQuery);
\ No newline at end of file
(function($){var options={series:{stack:null}};function init(plot){function findMatchingSeries(s,allseries){var res=null;for(var i=0;i<allseries.length;++i){if(s==allseries[i])break;if(allseries[i].stack==s.stack)res=allseries[i]}return res}function stackData(plot,s,datapoints){if(s.stack==null||s.stack===false)return;var other=findMatchingSeries(s,plot.getData());if(!other)return;var ps=datapoints.pointsize,points=datapoints.points,otherps=other.datapoints.pointsize,otherpoints=other.datapoints.points,newpoints=[],px,py,intery,qx,qy,bottom,withlines=s.lines.show,horizontal=s.bars.horizontal,withbottom=ps>2&&(horizontal?datapoints.format[2].x:datapoints.format[2].y),withsteps=withlines&&s.lines.steps,fromgap=true,keyOffset=horizontal?1:0,accumulateOffset=horizontal?0:1,i=0,j=0,l,m;while(true){if(i>=points.length)break;l=newpoints.length;if(points[i]==null){for(m=0;m<ps;++m)newpoints.push(points[i+m]);i+=ps}else if(j>=otherpoints.length){if(!withlines){for(m=0;m<ps;++m)newpoints.push(points[i+m])}i+=ps}else if(otherpoints[j]==null){for(m=0;m<ps;++m)newpoints.push(null);fromgap=true;j+=otherps}else{px=points[i+keyOffset];py=points[i+accumulateOffset];qx=otherpoints[j+keyOffset];qy=otherpoints[j+accumulateOffset];bottom=0;if(px==qx){for(m=0;m<ps;++m)newpoints.push(points[i+m]);newpoints[l+accumulateOffset]+=qy;bottom=qy;i+=ps;j+=otherps}else if(px>qx){if(withlines&&i>0&&points[i-ps]!=null){intery=py+(points[i-ps+accumulateOffset]-py)*(qx-px)/(points[i-ps+keyOffset]-px);newpoints.push(qx);newpoints.push(intery+qy);for(m=2;m<ps;++m)newpoints.push(points[i+m]);bottom=qy}j+=otherps}else{if(fromgap&&withlines){i+=ps;continue}for(m=0;m<ps;++m)newpoints.push(points[i+m]);if(withlines&&j>0&&otherpoints[j-otherps]!=null)bottom=qy+(otherpoints[j-otherps+accumulateOffset]-qy)*(px-qx)/(otherpoints[j-otherps+keyOffset]-qx);newpoints[l+accumulateOffset]+=bottom;i+=ps}fromgap=false;if(l!=newpoints.length&&withbottom)newpoints[l+2]+=bottom}if(withsteps&&l!=newpoints.length&&l>0&&newpoints[l]!=null&&newpoints[l]!=newpoints[l-ps]&&newpoints[l+1]!=newpoints[l-ps+1]){for(m=0;m<ps;++m)newpoints[l+ps+m]=newpoints[l+m];newpoints[l+1]=newpoints[l-ps+1]}}datapoints.points=newpoints}plot.hooks.processDatapoints.push(stackData)}$.plot.plugins.push({init:init,options:options,name:"stack",version:"1.2"})})(jQuery);
\ No newline at end of file
/* Flot plugin that adds some extra symbols for plotting points.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
The symbols are accessed as strings through the standard symbol options:
series: {
points: {
symbol: "square" // or "diamond", "triangle", "cross"
}
}
*/(function(e){function t(e,t,n){var r={square:function(e,t,n,r,i){var s=r*Math.sqrt(Math.PI)/2;e.rect(t-s,n-s,s+s,s+s)},diamond:function(e,t,n,r,i){var s=r*Math.sqrt(Math.PI/2);e.moveTo(t-s,n),e.lineTo(t,n-s),e.lineTo(t+s,n),e.lineTo(t,n+s),e.lineTo(t-s,n)},triangle:function(e,t,n,r,i){var s=r*Math.sqrt(2*Math.PI/Math.sin(Math.PI/3)),o=s*Math.sin(Math.PI/3);e.moveTo(t-s/2,n+o/2),e.lineTo(t+s/2,n+o/2),i||(e.lineTo(t,n-o/2),e.lineTo(t-s/2,n+o/2))},cross:function(e,t,n,r,i){var s=r*Math.sqrt(Math.PI)/2;e.moveTo(t-s,n-s),e.lineTo(t+s,n+s),e.moveTo(t-s,n+s),e.lineTo(t+s,n-s)}},i=t.points.symbol;r[i]&&(t.points.symbol=r[i])}function n(e){e.hooks.processDatapoints.push(t)}e.plot.plugins.push({init:n,name:"symbols",version:"1.0"})})(jQuery);
\ No newline at end of file
(function($){function processRawData(plot,series,datapoints){var handlers={square:function(ctx,x,y,radius,shadow){var size=radius*Math.sqrt(Math.PI)/2;ctx.rect(x-size,y-size,size+size,size+size)},diamond:function(ctx,x,y,radius,shadow){var size=radius*Math.sqrt(Math.PI/2);ctx.moveTo(x-size,y);ctx.lineTo(x,y-size);ctx.lineTo(x+size,y);ctx.lineTo(x,y+size);ctx.lineTo(x-size,y)},triangle:function(ctx,x,y,radius,shadow){var size=radius*Math.sqrt(2*Math.PI/Math.sin(Math.PI/3));var height=size*Math.sin(Math.PI/3);ctx.moveTo(x-size/2,y+height/2);ctx.lineTo(x+size/2,y+height/2);if(!shadow){ctx.lineTo(x,y-height/2);ctx.lineTo(x-size/2,y+height/2)}},cross:function(ctx,x,y,radius,shadow){var size=radius*Math.sqrt(Math.PI)/2;ctx.moveTo(x-size,y-size);ctx.lineTo(x+size,y+size);ctx.moveTo(x-size,y+size);ctx.lineTo(x+size,y-size)}};var s=series.points.symbol;if(handlers[s])series.points.symbol=handlers[s]}function init(plot){plot.hooks.processDatapoints.push(processRawData)}$.plot.plugins.push({init:init,name:"symbols",version:"1.0"})})(jQuery);
\ No newline at end of file
/* Flot plugin for thresholding data.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin supports these options:
series: {
threshold: {
below: number
color: colorspec
}
}
It can also be applied to a single series, like this:
$.plot( $("#placeholder"), [{
data: [ ... ],
threshold: { ... }
}])
An array can be passed for multiple thresholding, like this:
threshold: [{
below: number1
color: color1
},{
below: number2
color: color2
}]
These multiple threshold objects can be passed in any order since they are
sorted by the processing function.
The data points below "below" are drawn with the specified color. This makes
it easy to mark points below 0, e.g. for budget data.
Internally, the plugin works by splitting the data into two series, above and
below the threshold. The extra series below the threshold will have its label
cleared and the special "originSeries" attribute set to the original series.
You may need to check for this in hover events.
*/(function(e){function n(t){function n(t,n,r,i,s){var o=r.pointsize,u,a,f,l,c,h=e.extend({},n);h.datapoints={points:[],pointsize:o,format:r.format},h.label=null,h.color=s,h.threshold=null,h.originSeries=n,h.data=[];var p=r.points,d=n.lines.show,v=[],m=[],g;for(u=0;u<p.length;u+=o){a=p[u],f=p[u+1],c=l,f<i?l=v:l=m;if(d&&c!=l&&a!=null&&u>0&&p[u-o]!=null){var y=a+(i-f)*(a-p[u-o])/(f-p[u-o+1]);c.push(y),c.push(i);for(g=2;g<o;++g)c.push(p[u+g]);l.push(null),l.push(null);for(g=2;g<o;++g)l.push(p[u+g]);l.push(y),l.push(i);for(g=2;g<o;++g)l.push(p[u+g])}l.push(a),l.push(f);for(g=2;g<o;++g)l.push(p[u+g])}r.points=m,h.datapoints.points=v;if(h.datapoints.points.length>0){var b=e.inArray(n,t.getData());t.getData().splice(b+1,0,h)}}function r(t,r,i){if(!r.threshold)return;r.threshold instanceof Array?(r.threshold.sort(function(e,t){return e.below-t.below}),e(r.threshold).each(function(e,o){n(t,r,i,o.below,o.color)})):n(t,r,i,r.threshold.below,r.threshold.color)}t.hooks.processDatapoints.push(r)}var t={series:{threshold:null}};e.plot.plugins.push({init:n,options:t,name:"threshold",version:"1.2"})})(jQuery);
\ No newline at end of file
(function($){var options={series:{threshold:null}};function init(plot){function thresholdData(plot,s,datapoints,below,color){var ps=datapoints.pointsize,i,x,y,p,prevp,thresholded=$.extend({},s);thresholded.datapoints={points:[],pointsize:ps,format:datapoints.format};thresholded.label=null;thresholded.color=color;thresholded.threshold=null;thresholded.originSeries=s;thresholded.data=[];var origpoints=datapoints.points,addCrossingPoints=s.lines.show;var threspoints=[];var newpoints=[];var m;for(i=0;i<origpoints.length;i+=ps){x=origpoints[i];y=origpoints[i+1];prevp=p;if(y<below)p=threspoints;else p=newpoints;if(addCrossingPoints&&prevp!=p&&x!=null&&i>0&&origpoints[i-ps]!=null){var interx=x+(below-y)*(x-origpoints[i-ps])/(y-origpoints[i-ps+1]);prevp.push(interx);prevp.push(below);for(m=2;m<ps;++m)prevp.push(origpoints[i+m]);p.push(null);p.push(null);for(m=2;m<ps;++m)p.push(origpoints[i+m]);p.push(interx);p.push(below);for(m=2;m<ps;++m)p.push(origpoints[i+m])}p.push(x);p.push(y);for(m=2;m<ps;++m)p.push(origpoints[i+m])}datapoints.points=newpoints;thresholded.datapoints.points=threspoints;if(thresholded.datapoints.points.length>0){var origIndex=$.inArray(s,plot.getData());plot.getData().splice(origIndex+1,0,thresholded)}}function processThresholds(plot,s,datapoints){if(!s.threshold)return;if(s.threshold instanceof Array){s.threshold.sort(function(a,b){return a.below-b.below});$(s.threshold).each(function(i,th){thresholdData(plot,s,datapoints,th.below,th.color)})}else{thresholdData(plot,s,datapoints,s.threshold.below,s.threshold.color)}}plot.hooks.processDatapoints.push(processThresholds)}$.plot.plugins.push({init:init,options:options,name:"threshold",version:"1.2"})})(jQuery);
\ No newline at end of file
/* Pretty handling of time axes.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
Set axis.mode to "time" to enable. See the section "Time series data" in
API.txt for details.
*/(function(e){function n(e,t){return t*Math.floor(e/t)}function r(e,t,n,r){if(typeof e.strftime=="function")return e.strftime(t);var i=function(e,t){return e=""+e,t=""+(t==null?"0":t),e.length==1?t+e:e},s=[],o=!1,u=e.getHours(),a=u<12;n==null&&(n=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]),r==null&&(r=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]);var f;u>12?f=u-12:u==0?f=12:f=u;for(var l=0;l<t.length;++l){var c=t.charAt(l);if(o){switch(c){case"a":c=""+r[e.getDay()];break;case"b":c=""+n[e.getMonth()];break;case"d":c=i(e.getDate());break;case"e":c=i(e.getDate()," ");break;case"h":case"H":c=i(u);break;case"I":c=i(f);break;case"l":c=i(f," ");break;case"m":c=i(e.getMonth()+1);break;case"M":c=i(e.getMinutes());break;case"q":c=""+(Math.floor(e.getMonth()/3)+1);break;case"S":c=i(e.getSeconds());break;case"y":c=i(e.getFullYear()%100);break;case"Y":c=""+e.getFullYear();break;case"p":c=a?"am":"pm";break;case"P":c=a?"AM":"PM";break;case"w":c=""+e.getDay()}s.push(c),o=!1}else c=="%"?o=!0:s.push(c)}return s.join("")}function i(e){function t(e,t,n,r){e[t]=function(){return n[r].apply(n,arguments)}}var n={date:e};e.strftime!=undefined&&t(n,"strftime",e,"strftime"),t(n,"getTime",e,"getTime"),t(n,"setTime",e,"setTime");var r=["Date","Day","FullYear","Hours","Milliseconds","Minutes","Month","Seconds"];for(var i=0;i<r.length;i++)t(n,"get"+r[i],e,"getUTC"+r[i]),t(n,"set"+r[i],e,"setUTC"+r[i]);return n}function s(e,t){if(t.timezone=="browser")return new Date(e);if(!t.timezone||t.timezone=="utc")return i(new Date(e));if(typeof timezoneJS!="undefined"&&typeof timezoneJS.Date!="undefined"){var n=new timezoneJS.Date;return n.setTimezone(t.timezone),n.setTime(e),n}return i(new Date(e))}function l(t){t.hooks.processOptions.push(function(t,i){e.each(t.getAxes(),function(e,t){var i=t.options;i.mode=="time"&&(t.tickGenerator=function(e){var t=[],r=s(e.min,i),u=0,l=i.tickSize&&i.tickSize[1]==="quarter"||i.minTickSize&&i.minTickSize[1]==="quarter"?f:a;i.minTickSize!=null&&(typeof i.tickSize=="number"?u=i.tickSize:u=i.minTickSize[0]*o[i.minTickSize[1]]);for(var c=0;c<l.length-1;++c)if(e.delta<(l[c][0]*o[l[c][1]]+l[c+1][0]*o[l[c+1][1]])/2&&l[c][0]*o[l[c][1]]>=u)break;var h=l[c][0],p=l[c][1];if(p=="year"){if(i.minTickSize!=null&&i.minTickSize[1]=="year")h=Math.floor(i.minTickSize[0]);else{var d=Math.pow(10,Math.floor(Math.log(e.delta/o.year)/Math.LN10)),v=e.delta/o.year/d;v<1.5?h=1:v<3?h=2:v<7.5?h=5:h=10,h*=d}h<1&&(h=1)}e.tickSize=i.tickSize||[h,p];var m=e.tickSize[0];p=e.tickSize[1];var g=m*o[p];p=="second"?r.setSeconds(n(r.getSeconds(),m)):p=="minute"?r.setMinutes(n(r.getMinutes(),m)):p=="hour"?r.setHours(n(r.getHours(),m)):p=="month"?r.setMonth(n(r.getMonth(),m)):p=="quarter"?r.setMonth(3*n(r.getMonth()/3,m)):p=="year"&&r.setFullYear(n(r.getFullYear(),m)),r.setMilliseconds(0),g>=o.minute&&r.setSeconds(0),g>=o.hour&&r.setMinutes(0),g>=o.day&&r.setHours(0),g>=o.day*4&&r.setDate(1),g>=o.month*2&&r.setMonth(n(r.getMonth(),3)),g>=o.quarter*2&&r.setMonth(n(r.getMonth(),6)),g>=o.year&&r.setMonth(0);var y=0,b=Number.NaN,w;do{w=b,b=r.getTime(),t.push(b);if(p=="month"||p=="quarter")if(m<1){r.setDate(1);var E=r.getTime();r.setMonth(r.getMonth()+(p=="quarter"?3:1));var S=r.getTime();r.setTime(b+y*o.hour+(S-E)*m),y=r.getHours(),r.setHours(0)}else r.setMonth(r.getMonth()+m*(p=="quarter"?3:1));else p=="year"?r.setFullYear(r.getFullYear()+m):r.setTime(b+g)}while(b<e.max&&b!=w);return t},t.tickFormatter=function(e,t){var n=s(e,t.options);if(i.timeformat!=null)return r(n,i.timeformat,i.monthNames,i.dayNames);var u=t.options.tickSize&&t.options.tickSize[1]=="quarter"||t.options.minTickSize&&t.options.minTickSize[1]=="quarter",a=t.tickSize[0]*o[t.tickSize[1]],f=t.max-t.min,l=i.twelveHourClock?" %p":"",c=i.twelveHourClock?"%I":"%H",h;a<o.minute?h=c+":%M:%S"+l:a<o.day?f<2*o.day?h=c+":%M"+l:h="%b %d "+c+":%M"+l:a<o.month?h="%b %d":u&&a<o.quarter||!u&&a<o.year?f<o.year?h="%b":h="%b %Y":u&&a<o.year?f<o.year?h="Q%q":h="Q%q %Y":h="%Y";var p=r(n,h,i.monthNames,i.dayNames);return p})})})}var t={xaxis:{timezone:null,timeformat:null,twelveHourClock:!1,monthNames:null}},o={second:1e3,minute:6e4,hour:36e5,day:864e5,month:2592e6,quarter:7776e6,year:525949.2*60*1e3},u=[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[.25,"month"],[.5,"month"],[1,"month"],[2,"month"]],a=u.concat([[3,"month"],[6,"month"],[1,"year"]]),f=u.concat([[1,"quarter"],[2,"quarter"],[1,"year"]]);e.plot.plugins.push({init:l,options:t,name:"time",version:"1.0"}),e.plot.formatDate=r})(jQuery);
\ No newline at end of file
(function($){var options={xaxis:{timezone:null,timeformat:null,twelveHourClock:false,monthNames:null}};function floorInBase(n,base){return base*Math.floor(n/base)}function formatDate(d,fmt,monthNames,dayNames){if(typeof d.strftime=="function"){return d.strftime(fmt)}var leftPad=function(n,pad){n=""+n;pad=""+(pad==null?"0":pad);return n.length==1?pad+n:n};var r=[];var escape=false;var hours=d.getHours();var isAM=hours<12;if(monthNames==null){monthNames=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}if(dayNames==null){dayNames=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]}var hours12;if(hours>12){hours12=hours-12}else if(hours==0){hours12=12}else{hours12=hours}for(var i=0;i<fmt.length;++i){var c=fmt.charAt(i);if(escape){switch(c){case"a":c=""+dayNames[d.getDay()];break;case"b":c=""+monthNames[d.getMonth()];break;case"d":c=leftPad(d.getDate());break;case"e":c=leftPad(d.getDate()," ");break;case"h":case"H":c=leftPad(hours);break;case"I":c=leftPad(hours12);break;case"l":c=leftPad(hours12," ");break;case"m":c=leftPad(d.getMonth()+1);break;case"M":c=leftPad(d.getMinutes());break;case"q":c=""+(Math.floor(d.getMonth()/3)+1);break;case"S":c=leftPad(d.getSeconds());break;case"y":c=leftPad(d.getFullYear()%100);break;case"Y":c=""+d.getFullYear();break;case"p":c=isAM?""+"am":""+"pm";break;case"P":c=isAM?""+"AM":""+"PM";break;case"w":c=""+d.getDay();break}r.push(c);escape=false}else{if(c=="%"){escape=true}else{r.push(c)}}}return r.join("")}function makeUtcWrapper(d){function addProxyMethod(sourceObj,sourceMethod,targetObj,targetMethod){sourceObj[sourceMethod]=function(){return targetObj[targetMethod].apply(targetObj,arguments)}}var utc={date:d};if(d.strftime!=undefined){addProxyMethod(utc,"strftime",d,"strftime")}addProxyMethod(utc,"getTime",d,"getTime");addProxyMethod(utc,"setTime",d,"setTime");var props=["Date","Day","FullYear","Hours","Milliseconds","Minutes","Month","Seconds"];for(var p=0;p<props.length;p++){addProxyMethod(utc,"get"+props[p],d,"getUTC"+props[p]);addProxyMethod(utc,"set"+props[p],d,"setUTC"+props[p])}return utc}function dateGenerator(ts,opts){if(opts.timezone=="browser"){return new Date(ts)}else if(!opts.timezone||opts.timezone=="utc"){return makeUtcWrapper(new Date(ts))}else if(typeof timezoneJS!="undefined"&&typeof timezoneJS.Date!="undefined"){var d=new timezoneJS.Date;d.setTimezone(opts.timezone);d.setTime(ts);return d}else{return makeUtcWrapper(new Date(ts))}}var timeUnitSize={second:1e3,minute:60*1e3,hour:60*60*1e3,day:24*60*60*1e3,month:30*24*60*60*1e3,quarter:3*30*24*60*60*1e3,year:365.2425*24*60*60*1e3};var baseSpec=[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[.25,"month"],[.5,"month"],[1,"month"],[2,"month"]];var specMonths=baseSpec.concat([[3,"month"],[6,"month"],[1,"year"]]);var specQuarters=baseSpec.concat([[1,"quarter"],[2,"quarter"],[1,"year"]]);function init(plot){plot.hooks.processOptions.push(function(plot,options){$.each(plot.getAxes(),function(axisName,axis){var opts=axis.options;if(opts.mode=="time"){axis.tickGenerator=function(axis){var ticks=[];var d=dateGenerator(axis.min,opts);var minSize=0;var spec=opts.tickSize&&opts.tickSize[1]==="quarter"||opts.minTickSize&&opts.minTickSize[1]==="quarter"?specQuarters:specMonths;if(opts.minTickSize!=null){if(typeof opts.tickSize=="number"){minSize=opts.tickSize}else{minSize=opts.minTickSize[0]*timeUnitSize[opts.minTickSize[1]]}}for(var i=0;i<spec.length-1;++i){if(axis.delta<(spec[i][0]*timeUnitSize[spec[i][1]]+spec[i+1][0]*timeUnitSize[spec[i+1][1]])/2&&spec[i][0]*timeUnitSize[spec[i][1]]>=minSize){break}}var size=spec[i][0];var unit=spec[i][1];if(unit=="year"){if(opts.minTickSize!=null&&opts.minTickSize[1]=="year"){size=Math.floor(opts.minTickSize[0])}else{var magn=Math.pow(10,Math.floor(Math.log(axis.delta/timeUnitSize.year)/Math.LN10));var norm=axis.delta/timeUnitSize.year/magn;if(norm<1.5){size=1}else if(norm<3){size=2}else if(norm<7.5){size=5}else{size=10}size*=magn}if(size<1){size=1}}axis.tickSize=opts.tickSize||[size,unit];var tickSize=axis.tickSize[0];unit=axis.tickSize[1];var step=tickSize*timeUnitSize[unit];if(unit=="second"){d.setSeconds(floorInBase(d.getSeconds(),tickSize))}else if(unit=="minute"){d.setMinutes(floorInBase(d.getMinutes(),tickSize))}else if(unit=="hour"){d.setHours(floorInBase(d.getHours(),tickSize))}else if(unit=="month"){d.setMonth(floorInBase(d.getMonth(),tickSize))}else if(unit=="quarter"){d.setMonth(3*floorInBase(d.getMonth()/3,tickSize))}else if(unit=="year"){d.setFullYear(floorInBase(d.getFullYear(),tickSize))}d.setMilliseconds(0);if(step>=timeUnitSize.minute){d.setSeconds(0)}if(step>=timeUnitSize.hour){d.setMinutes(0)}if(step>=timeUnitSize.day){d.setHours(0)}if(step>=timeUnitSize.day*4){d.setDate(1)}if(step>=timeUnitSize.month*2){d.setMonth(floorInBase(d.getMonth(),3))}if(step>=timeUnitSize.quarter*2){d.setMonth(floorInBase(d.getMonth(),6))}if(step>=timeUnitSize.year){d.setMonth(0)}var carry=0;var v=Number.NaN;var prev;do{prev=v;v=d.getTime();ticks.push(v);if(unit=="month"||unit=="quarter"){if(tickSize<1){d.setDate(1);var start=d.getTime();d.setMonth(d.getMonth()+(unit=="quarter"?3:1));var end=d.getTime();d.setTime(v+carry*timeUnitSize.hour+(end-start)*tickSize);carry=d.getHours();d.setHours(0)}else{d.setMonth(d.getMonth()+tickSize*(unit=="quarter"?3:1))}}else if(unit=="year"){d.setFullYear(d.getFullYear()+tickSize)}else{d.setTime(v+step)}}while(v<axis.max&&v!=prev);return ticks};axis.tickFormatter=function(v,axis){var d=dateGenerator(v,axis.options);if(opts.timeformat!=null){return formatDate(d,opts.timeformat,opts.monthNames,opts.dayNames)}var useQuarters=axis.options.tickSize&&axis.options.tickSize[1]=="quarter"||axis.options.minTickSize&&axis.options.minTickSize[1]=="quarter";var t=axis.tickSize[0]*timeUnitSize[axis.tickSize[1]];var span=axis.max-axis.min;var suffix=opts.twelveHourClock?" %p":"";var hourCode=opts.twelveHourClock?"%I":"%H";var fmt;if(t<timeUnitSize.minute){fmt=hourCode+":%M:%S"+suffix}else if(t<timeUnitSize.day){if(span<2*timeUnitSize.day){fmt=hourCode+":%M"+suffix}else{fmt="%b %d "+hourCode+":%M"+suffix}}else if(t<timeUnitSize.month){fmt="%b %d"}else if(useQuarters&&t<timeUnitSize.quarter||!useQuarters&&t<timeUnitSize.year){if(span<timeUnitSize.year){fmt="%b"}else{fmt="%b %Y"}}else if(useQuarters&&t<timeUnitSize.year){if(span<timeUnitSize.year){fmt="Q%q"}else{fmt="Q%q %Y"}}else{fmt="%Y"}var rt=formatDate(d,fmt,opts.monthNames,opts.dayNames);return rt}}})})}$.plot.plugins.push({init:init,options:options,name:"time",version:"1.0"});$.plot.formatDate=formatDate})(jQuery);
\ No newline at end of file
/*
* Functions to help create flot graphs
* Copyright (c) 2013-2014, AllWorldIT
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*/
// Function draw a graph
function awit_flot_draw_graph(options) {
// Setup default graph options
var defaults = {
series: {
lines: {
show: true,
lineWidth: 1,
fill: true,
fillColor: {
colors: [
{ opacity: 0.1 },
{ opacity: 0.13 }
]
}
},
points: {
show: false,
lineWidth: 2,
radius: 3
},
shadowSize: 0,
stack: true
},
grid: {
hoverable: true,
clickable: false,
tickColor: "#F9F9F9",
borderWidth: 1
},
legend: {
labelBoxBorderColor: "#AAAAAA"
},
xaxes: [
{
mode: "time",
tickLength: 10
}
],
yaxes: [
{
position: 'left',
min: 0,
tickFormatter: _flot_format_bandwidth
},
{
position: 'right',
min: 0,
tickDecimals: false
}
]
}
// Merge our options ontop of our defaults
var plotOptions = jQuery.extend({},defaults,options);
// Grab the placeholder
var placeholder = jQuery('#'+plotOptions.id);
// Plot the graph, the [] signifies an empty dataset, the data is populated by awitds
var plot = jQuery.plot(placeholder, [ ], plotOptions);
return plot;
}
// Function draw pie graph
function awit_flot_draw_pie(options) {
// Setup default graph options
var defaults = {
series: {
pie: {
show: true,
radius: 1,
label: {
show: true,
radius: 3/4,
formatter: _flot_format_pie_label,
background: {
opacity: 0.5,
color: '#000000'
}
}
}
}
}
// Merge our options ontop of our defaults
var plotOptions = jQuery.extend({},defaults,options);
// Set count to 1 and override timestamp to 1 for all identifiers
if (typeof(plotOptions.awitds) !== 'undefined') {
for (var tag in plotOptions.awitds.xaxis) {
for (var identifier in plotOptions.awitds.xaxis[tag]) {
plotOptions.awitds.xaxis[tag][identifier].maxCount = 1;
plotOptions.awitds.xaxis[tag][identifier].overrideTimestamp = 1;
}
}
}
// Grab the placeholder
var placeholder = jQuery('#'+plotOptions.id);
// Plot the graph, the [] signifies an empty dataset, the data is populated by awitds
var plot = jQuery.plot(placeholder, [ ], plotOptions);
return plot;
}
// Function to format thousands with , and add Kbps
function _flot_format_bandwidth(value, axis) {
return _flot_format_thousands(value,axis) + ' Kbps';
}
// Function to format thousands
function _flot_format_thousands(value, axis) {
// Convert number to string
value = value.toString();
// Match on 3 digits
var R = new RegExp('(-?[0-9]+)([0-9]{3})');
while(R.test(value)) {
// Replace market with ,
value = value.replace(R, '$1,$2');
}
return value;
}
function _flot_format_pie_label(label, series) {
return "<div style='font-size:8pt; text-align:center; padding:2px; color:white;'>" + label + "<br/>" + Math.round(series.percent) + "%</div>";
}
// vim: ts=4
opentrafficshaper/plugins/webserver/pages/static/logo-inverted-short.png

4.85 KiB

opentrafficshaper/plugins/webserver/pages/static/logo-inverted.png

5.59 KiB

opentrafficshaper/plugins/webserver/pages/static/logo.png

5.54 KiB

# OpenTrafficShaper webserver module: users page
# Copyright (C) 2007-2013, AllWorldIT
#
# OpenTrafficShaper webserver module: statistics page
# Copyright (C) 2007-2023, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
......@@ -32,169 +32,1282 @@ our (@ISA,@EXPORT,@EXPORT_OK);
use DateTime;
use HTML::Entities;
use HTTP::Status qw( :constants );
use HTTP::Status qw(
:constants
);
use JSON;
use URI;
use URI::Escape qw(
uri_escape
);
use awitpt::util qw(
parseURIQuery
isNumber
);
use opentrafficshaper::logger;
use opentrafficshaper::utils;
use opentrafficshaper::plugins;
use opentrafficshaper::plugins::configmanager qw(
getPoolByName
getInterfaceGroup
getInterfaceGroups
getInterface
getTrafficClass
getAllTrafficClasses
isTrafficClassIDValid
);
use opentrafficshaper::plugins::statistics::statistics;
# Default page/action
sub default
# Graphs to display on pools stat page
sub POOL_GRAPHS {
{
3600 => 'Last Hour',
86400 => 'Last 24 Hours',
604800 => 'Last 7 Days',
2419200 => 'Last Month'
}
};
# Graphs by pool
sub byPool
{
my ($kernel,$globals,$client_session_id,$request) = @_;
my ($kernel,$system,$client_session_id,$request) = @_;
# Header
my $content = "";
# Check request
if ($request->method ne "GET") {
$content .=<<EOF;
<p class="info text-center">Invalid Method</p>
EOF
goto END;
}
# Parse GET data
my $queryParams = $request->uri->query_form_hash;
# We need our PID
if (!defined($queryParams->{'pool'})) {
$content .=<<EOF;
<p class="info text-center">No "pool" in Query String</p>
EOF
goto END;
}
# Check we have an interface group ID and pool name
my ($interfaceGroupID,$poolName) = split(/:/,$queryParams->{'pool'});
if (!defined($interfaceGroupID) || !defined($poolName)) {
$content .=<<EOF;
<p class="info text-center">Format of "pool" option is invalid, use InterfaceGroupID:PoolName</p>
EOF
goto END;
}
# Check if we get some data back when pulling in the pool from the backend
my $pool;
if (!defined($pool = getPoolByName($interfaceGroupID,$poolName))) {
$content .=<<EOF;
<p class="info text-center">Pool Not Found</p>
EOF
goto END;
}
# Header for the page
$content .= <<EOF;
<legend>
<a href="/limits/pool-list"><span class="glyphicon glyphicon-circle-arrow-left"></span></a>
Pool Stats View
</legend>
EOF
my $timespan = 900;
my $now = time();
my $startTimestamp = $now - $timespan;
# Menu setup
my $menu = [
{
'name' => 'Graphs',
'items' => [
{
'name' => 'Live',
'link' => sprintf("by-pool?pool=%s",uri_escape("$interfaceGroupID:$poolName"))
},
{
'name' => 'Historical',
'link' => sprintf("by-pool?pool=%s&static=1",uri_escape("$interfaceGroupID:$poolName"))
}
]
}
];
my $name = (defined($pool->{'FriendlyName'}) && $pool->{'FriendlyName'} ne "") ? $pool->{'FriendlyName'} :
$pool->{'Name'};
my $nameEncoded = encode_entities($name);
# Build content
$content .= <<EOF;
EOF
# Graphs to display
my @graphs = ();
# Check if we doing a static display or not
if (defined($queryParams->{'static'}) && $queryParams->{'static'}) {
# Loop with periods to display on this page
foreach my $period (sort { $a <=> $b } keys %{POOL_GRAPHS()}) {
my $canvasName = "flotCanvas$period";
my $graphName = POOL_GRAPHS()->{$period};
my $now = time();
my $startTimestamp = $now - $period;
$content .= <<EOF;
<h4 style="color: #8F8F8F;">Statistics: $nameEncoded - $graphName</h4>
<div id="$canvasName" class="flotCanvas poolCanvas" style="width: 800px; height: 240px" ></div>
EOF
# Static graphs
push(@graphs,{
'Type' => 'graph',
'Tag' => $canvasName,
'Title' => sprintf("Pool: %s",$pool->{'Name'}),
'Datasources' => [
{
'Type' => 'ajax',
'Subscriptions' => [
{
'Type' => 'pool',
'Data' => sprintf('%s:%s',$pool->{'InterfaceGroupID'},$pool->{'Name'}),
'StartTimestamp' => $startTimestamp,
'EndTimestamp' => $now
}
]
}
],
'XIdentifiers' => [
{ 'Name' => 'tx.cir', 'Label' => "TX Cir", 'Timespan' => $period },
{ 'Name' => 'tx.limit', 'Label' => "TX Limit", 'Timespan' => $period },
{ 'Name' => 'tx.rate', 'Label' => "TX Rate", 'Timespan' => $period },
{ 'Name' => 'rx.cir', 'Label' => "RX Cir", 'Timespan' => $period },
{ 'Name' => 'rx.limit', 'Label' => "RX Limit", 'Timespan' => $period },
{ 'Name' => 'rx.rate', 'Label' => "RX Rate", 'Timespan' => $period }
]
});
}
# Display live graph
} else {
my $canvasName = "flotCanvas";
$content .= <<EOF;
<h4 style="color: #8F8F8F;">Live Statistics: $nameEncoded</h4>
<div id="$canvasName" class="flotCanvas poolCanvas" style="width: 1000px; height: 400px" />
EOF
# Live graph
push(@graphs,{
'Type' => 'graph',
'Tag' => $canvasName,
'Title' => sprintf("Pool: %s",$pool->{'Name'}),
'Datasources' => [
{
'Type' => 'websocket',
'Subscriptions' => [
sprintf('pool=%s:%s',$pool->{'InterfaceGroupID'},$pool->{'Name'})
]
},
{
'Type' => 'ajax',
'Subscriptions' => [
{
'Type' => 'pool',
'Data' => sprintf('%s:%s',$pool->{'InterfaceGroupID'},$pool->{'Name'}),
'StartTimestamp' => $startTimestamp,
'EndTimestamp' => $now
}
]
}
],
'XIdentifiers' => [
{ 'Name' => 'tx.cir', 'Label' => "TX Cir", 'Timespan' => $timespan },
{ 'Name' => 'tx.limit', 'Label' => "TX Limit", 'Timespan' => $timespan },
{ 'Name' => 'tx.rate', 'Label' => "TX Rate", 'Timespan' => $timespan },
{ 'Name' => 'rx.cir', 'Label' => "RX Cir", 'Timespan' => $timespan },
{ 'Name' => 'rx.limit', 'Label' => "RX Limit", 'Timespan' => $timespan },
{ 'Name' => 'rx.rate', 'Label' => "RX Rate", 'Timespan' => $timespan }
]
});
}
# Build graphs
my $javascript = _buildGraphJavascript(\@graphs);
# Files loaded at end of HTML document
my @javascripts = (
'/static/flot/jquery.flot.min.js',
'/static/flot/jquery.flot.time.min.js',
'/static/flot/jquery.flot.pie.min.js',
'/static/flot/jquery.flot.resize.min.js',
'/static/js/flot-functions.js',
'/static/awit-flot-toolkit/js/jquery.flot.awitds.js',
'/static/awit-flot-toolkit/js/resize.js'
);
my @stylesheets = (
'/static/awit-flot-toolkit/css/awit-flot-toolkit.css'
);
END:
return (HTTP_OK,$content,{ 'menu' => $menu, 'javascripts' => \@javascripts, 'javascript' => $javascript });
}
# Graphs by class
sub byClass
{
my ($kernel,$system,$client_session_id,$request) = @_;
# Header
my $content = "";
# Check request
if ($request->method ne "GET") {
$content .=<<EOF;
<p class="info text-center">Invalid Method</p>
EOF
goto END;
}
# Parse GET data
my $queryParams = $request->uri->query_form_hash;
# We need our class definition
if (!defined($queryParams->{'class'})) {
$content .=<<EOF;
<p class="info text-center">No "class" in Query String</p>
EOF
goto END;
}
# Check we have an interface group ID and traffic class id
my ($interfaceGroupID,$trafficClassID) = split(/:/,$queryParams->{'class'});
if (!defined($interfaceGroupID) || !defined($trafficClassID)) {
$content .=<<EOF;
<p class="info text-center">Format of "class" option is invalid, use InterfaceGroupID:ClassID</p>
EOF
goto END;
}
# Check if we get some data back when pulling in the interface group from the backend
my $interfaceGroup;
if (!defined($interfaceGroup = getInterfaceGroup($interfaceGroupID))) {
$content .=<<EOF;
<p class="info text-center">Interface Group Not Found</p>
EOF
goto END;
}
# Check if we get some data back when pulling in the traffic class from the backend
my $trafficClass;
if (!defined($trafficClass = getTrafficClass($trafficClassID))) {
$content .=<<EOF;
<p class="info text-center">Traffic Class Not Found</p>
EOF
goto END;
}
# Header for the page
$content .= <<EOF;
<legend>
<a href="/statistics/igdashboard?interface-group=$interfaceGroupID">
<span class="glyphicon glyphicon-circle-arrow-left"></span>
</a>
Class Stats View
</legend>
EOF
# Menu setup
my $menu = [
{
'name' => 'Graphs',
'items' => [
{
'name' => 'Live',
'link' => sprintf("by-class?class=%s","$interfaceGroupID:$trafficClassID")
},
{
'name' => 'Historical',
'link' => sprintf("by-class?class=%s&static=1","$interfaceGroupID:$trafficClassID")
}
]
}
];
my $nameEncoded = encode_entities($trafficClass->{'Name'});
# Build content
$content .= <<EOF;
EOF
# Graphs to display
my @graphs = ();
# Check if we doing a static display or not
if (defined($queryParams->{'static'}) && $queryParams->{'static'}) {
# Loop with periods to display on this page
foreach my $period (sort { $a <=> $b } keys %{POOL_GRAPHS()}) {
my $canvasName = "flotCanvas$period";
my $graphName = POOL_GRAPHS()->{$period};
my $now = time();
my $startTimestamp = $now - $period;
$content .= <<EOF;
<h4 style="color: #8F8F8F;">Class Statistics: $nameEncoded - $graphName</h4>
<div id="$canvasName" class="flotCanvas poolCanvas" style="width: 800px; height: 240px" ></div>
EOF
# Static graphs
push(@graphs,{
'Type' => 'graph',
'Tag' => $canvasName,
'Title' => sprintf("Class: %s",$trafficClass->{'Name'}),
'Datasources' => [
{
'Type' => 'ajax',
'Subscriptions' => [
{
'Type' => 'class',
'Data' => sprintf('%s:%s',$interfaceGroupID,$trafficClassID),
'StartTimestamp' => $startTimestamp,
'EndTimestamp' => $now
}
]
}
],
'XIdentifiers' => [
{ 'Name' => 'tx.cir', 'Label' => "TX Cir", 'Timespan' => $period },
{ 'Name' => 'tx.limit', 'Label' => "TX Limit", 'Timespan' => $period },
{ 'Name' => 'tx.rate', 'Label' => "TX Rate", 'Timespan' => $period },
{ 'Name' => 'rx.cir', 'Label' => "RX Cir", 'Timespan' => $period },
{ 'Name' => 'rx.limit', 'Label' => "RX Limit", 'Timespan' => $period },
{ 'Name' => 'rx.rate', 'Label' => "RX Rate", 'Timespan' => $period }
]
});
}
# Display live graph
} else {
my $canvasName = "flotCanvas";
$content .= <<EOF;
<h4 style="color: #8F8F8F;">Live Statistics: $nameEncoded</h4>
<div id="$canvasName" class="flotCanvas poolCanvas" style="width: 1000px; height: 400px" />
EOF
# Live graph
push(@graphs,{
'Type' => 'graph',
'Tag' => $canvasName,
'Title' => sprintf("Class: %s",$trafficClass->{'Name'}),
'Datasources' => [
{
'Type' => 'websocket',
'Subscriptions' => [
sprintf('class=%s:%s',$interfaceGroupID,$trafficClassID)
]
}
],
'XIdentifiers' => [
{ 'Name' => 'tx.cir', 'Label' => "TX Cir", 'Timespan' => 900 },
{ 'Name' => 'tx.limit', 'Label' => "TX Limit", 'Timespan' => 900 },
{ 'Name' => 'tx.rate', 'Label' => "TX Rate", 'Timespan' => 900 },
{ 'Name' => 'rx.cir', 'Label' => "RX Cir", 'Timespan' => 900 },
{ 'Name' => 'rx.limit', 'Label' => "RX Limit", 'Timespan' => 900 },
{ 'Name' => 'rx.rate', 'Label' => "RX Rate", 'Timespan' => 900 }
]
});
}
# Build graphs
my $javascript = _buildGraphJavascript(\@graphs);
# Files loaded at end of HTML document
my @javascripts = (
'/static/flot/jquery.flot.min.js',
'/static/flot/jquery.flot.time.min.js',
'/static/flot/jquery.flot.pie.min.js',
'/static/flot/jquery.flot.resize.min.js',
'/static/js/flot-functions.js',
'/static/awit-flot-toolkit/js/jquery.flot.awitds.js',
'/static/awit-flot-toolkit/js/resize.js'
);
my @stylesheets = (
'/static/awit-flot-toolkit/css/awit-flot-toolkit.css'
);
END:
return (HTTP_OK,$content,{ 'menu' => $menu, 'javascripts' => \@javascripts, 'javascript' => $javascript });
}
# Dashboard display
sub dashboard
{
my ($kernel,$system,$client_session_id,$request) = @_;
# Header
my $content = <<EOF;
<legend>
Dashboard View
</legend>
EOF
# Build list of graphs for the left hand side
my @interfaceGroups = sort(getInterfaceGroups());
my @trafficClasses = sort(getAllTrafficClasses());
my $timespan = 900;
my $now = time();
my $startTimestamp = $now - $timespan;
my @graphs;
my $graphCounter = 0;
foreach my $interfaceGroupID (@interfaceGroups) {
my $interfaceGroup = getInterfaceGroup($interfaceGroupID);
push(@graphs,{
'.InterfaceGroup' => $interfaceGroup->{'ID'},
'Tag' => sprintf('tag%s',$graphCounter++),
'Type' => 'graph',
'Title' => sprintf("%s: Main",$interfaceGroup->{'Name'}),
'Datasources' => [
{
'Type' => 'websocket',
'Subscriptions' => [
sprintf('interface-group=%s',$interfaceGroupID),
]
},
{
'Type' => 'ajax',
'Subscriptions' => [
{
'Type' => 'interface-group',
'Data' => $interfaceGroupID,
'StartTimestamp' => $startTimestamp,
'EndTimestamp' => $now
}
]
}
],
'XIdentifiers' => [
{ 'Name' => 'tx.rate', 'Label' => "TX Rate", 'Timespan' => $timespan },
{ 'Name' => 'rx.rate', 'Label' => "RX Rate", 'Timespan' => $timespan }
]
});
}
foreach my $graph (@graphs) {
my $interfaceGroupEscaped = uri_escape($graph->{'.InterfaceGroup'});
$content .= <<EOF;
<div class="row">
<h4 class="text-center" style="color:#8f8f8f;">$graph->{'Title'}</h4>
<a href="/statistics/igdashboard?interface-group=$interfaceGroupEscaped">
<div id="$graph->{'Tag'}" class="flotCanvas poolCanvas"
style="width: 1000px; height: 250px; margin-left: auto; margin-right: auto;">
</div>
</a>
</div>
EOF
}
# Build graphs
my $javascript = _buildGraphJavascript(\@graphs);
# Files loaded at end of HTML document
my @javascripts = (
'/static/flot/jquery.flot.min.js',
'/static/flot/jquery.flot.time.min.js',
'/static/flot/jquery.flot.pie.min.js',
'/static/flot/jquery.flot.resize.min.js',
'/static/js/flot-functions.js',
'/static/awit-flot-toolkit/js/jquery.flot.awitds.js',
'/static/awit-flot-toolkit/js/resize.js'
);
my @stylesheets = (
'/static/awit-flot-toolkit/css/awit-flot-toolkit.css'
);
return (HTTP_OK,$content,{
'javascripts' => \@javascripts,
'javascript' => $javascript,
'stylesheets' => \@stylesheets
});
}
# Dashboard display for interface groups
sub igdashboard
{
my ($kernel,$system,$client_session_id,$request) = @_;
# Header
my $content = <<EOF;
<div id="header">
<h2>Traffic Shaper Stats</h2>
</div>
<legend>
<a href="/statistics/dashboard"><span class="glyphicon glyphicon-circle-arrow-left"></span></a>
Interface Dashboard View
</legend>
EOF
<div id="content" style="float:left">
# Get query params
my $queryParams = $request->uri->query_form_hash;
<div style="position: relative; top:50px;">
<h4 style="color:#8f8f8f;">Json Data (loads from /static/stats.json.js)</h4>
<br/>
# We need our PID
if (!defined($queryParams->{'interface-group'})) {
$content .=<<EOF;
<p class="info text-center">No "interface-group" in Query String</p>
EOF
return (HTTP_TEMPORARY_REDIRECT,"/statistics");
}
my $interfaceGroup = getInterfaceGroup($queryParams->{'interface-group'});
if (!defined($interfaceGroup)) {
$content .=<<EOF;
<p class="info text-center">Invalid "interface-group" in Query String</p>
EOF
return (HTTP_TEMPORARY_REDIRECT,"/statistics");
}
# Left and right graphs are added to the main graph list
my @graphs = ();
# Left and right graphs
my @leftGraphs = ();
my @rightGraphs = ();
# Build list of graphs for the left hand side
my @trafficClasses = sort(getAllTrafficClasses());
my $timespan = 900;
my $now = time();
my $startTimestamp = $now - $timespan;
foreach my $trafficClassID (@trafficClasses) {
my $trafficClass = getTrafficClass($trafficClassID);
<div id="ajaxData" class="ajaxData" style="float:left; width:1024px; height: 560px"></div>
</div>
push(@leftGraphs,{
'Type' => 'graph',
'Title' => sprintf("%s: %s",$interfaceGroup->{'Name'},$trafficClass->{'Name'}),
'.URIData' => sprintf('%s:%s',$interfaceGroup->{'ID'},$trafficClassID),
'Datasources' => [
{
'Type' => 'websocket',
'Subscriptions' => [
sprintf('class=%s:%s',$interfaceGroup->{'ID'},$trafficClassID),
sprintf('counter=configmanager.classpoolmembers.%s',$trafficClassID)
]
},
{
'Type' => 'ajax',
'Subscriptions' => [
{
'Type' => 'class',
'Data' => sprintf('%s:%s',$interfaceGroup->{'ID'},$trafficClassID),
'StartTimestamp' => $startTimestamp,
'EndTimestamp' => $now
}
]
},
{
'Type' => 'ajax',
'Subscriptions' => [
{
'Type' => 'counter',
'Data' => sprintf('configmanager.classpoolmembers.%s',$trafficClassID),
'StartTimestamp' => $startTimestamp,
'EndTimestamp' => $now
}
]
}
],
'XIdentifiers' => [
{ 'Name' => 'tx.cir', 'Label' => "TX Cir", 'Timespan' => $timespan },
{ 'Name' => 'tx.limit', 'Label' => "TX Limit", 'Timespan' => $timespan },
{ 'Name' => 'tx.rate', 'Label' => "TX Rate", 'Timespan' => $timespan },
{ 'Name' => 'rx.cir', 'Label' => "RX Cir", 'Timespan' => $timespan },
{ 'Name' => 'rx.limit', 'Label' => "RX Limit", 'Timespan' => $timespan },
{ 'Name' => 'rx.rate', 'Label' => "RX Rate", 'Timespan' => $timespan }
],
'YIdentifiers' => [
{
'Name' => sprintf('configmanager.classpoolmembers.%s',$trafficClassID),
'Label' => "Pool Count",
'Timespan' => $timespan
},
]
});
}
# Pool distribution
my @dataSubscriptions = ();
my @xidentifiers = ();
foreach my $trafficClassID (@trafficClasses) {
my $trafficClass = getTrafficClass($trafficClassID);
push(@dataSubscriptions,sprintf('counter=configmanager.classpools.%s',$trafficClassID));
push(@xidentifiers,{
'Name' => sprintf('configmanager.classpools.%s',$trafficClassID),
'Label' => $trafficClass->{'Name'}
});
}
push(@rightGraphs,{
'Type' => 'pie',
'Title' => "Pool Distribution",
'Datasources' => [
{
'Type' => 'websocket',
'Subscriptions' => \@dataSubscriptions
}
],
'XIdentifiers' => \@xidentifiers
});
</div>
# Loop while we have graphs to output
my $graphCounter = 0;
while (@leftGraphs || @rightGraphs) {
# Layout Begin
$content .= <<EOF;
<div class="row">
EOF
# LHS - Begin
$content .= <<EOF;
<div class="col-md-8">
EOF
# Loop with 2 sets of normal graphs per row
for (my $row = 0; $row < 2; $row++) {
# LHS - Begin Row
$content .= <<EOF;
<div class="row">
<div class="col-xs-6">
EOF
# Graph 1
if (defined(my $graph = shift(@leftGraphs))) {
my $uriData = $graph->{'.URIData'};
# FIXME - Dynamic script inclusion required
# Assign this graph a tag
$graph->{'Tag'} = "tag".$graphCounter++;
$content .= <<EOF;
<h4 style="color:#8f8f8f;">$graph->{'Title'}</h4>
<a href="/statistics/by-class?class=$uriData">
<div id="$graph->{'Tag'}" class="flotCanvas dashboardCanvas"
style="width: 600px; height: 250px"></div>
</a>
EOF
push(@graphs,$graph);
}
# LHS - Spacer
$content .= <<EOF;
</div>
<div class="col-xs-6">
EOF
# Graph 2
if (defined(my $graph = shift(@leftGraphs))) {
my $uriData = $graph->{'.URIData'};
# Assign this graph a tag
$graph->{'Tag'} = "tag".$graphCounter++;
$content .= <<EOF;
<h4 style="color:#8f8f8f;">$graph->{'Title'}</h4>
<a href="/statistics/by-class?class=$uriData">
<div id="$graph->{'Tag'}" class="flotCanvas dashboardCanvas"
style="width: 600px; height: 250px"></div>
</a>
EOF
push(@graphs,$graph);
}
# LHS - End Row
$content .= <<EOF;
</div>
</div>
EOF
}
# LHS - End
$content .= <<EOF;
</div>
EOF
# RHS - Begin Row
$content .= <<EOF;
<div class="col-md-4">
EOF
# Graph
if (defined(my $graph = shift(@rightGraphs))) {
# Assign this graph a tag
$graph->{'Tag'} = "tag".$graphCounter++;
$content .= <<EOF;
<h4 style="color:#8f8f8f;">$graph->{'Title'}</h4>
<div id="$graph->{'Tag'}" class="flotCanvas dashboardCanvas" style="width: 600px; height: 340px"></div>
EOF
push(@graphs,$graph);
}
# RHS - End Row
$content .= <<EOF;
</div>
EOF
# Layout End
$content .= <<EOF;
</div>
EOF
}
# Build graphs
my $javascript = _buildGraphJavascript(\@graphs);
#$content .= statistics::do_test();
# $content .= opentrafficshaper::plugins::statistics::do_test();
# Files loaded at end of HTML document
my @javascripts = (
'/static/awit-flot/jquery.flot.min.js',
'/static/awit-flot/jquery.flot.time.min.js',
# '/static/awit-flot/jquery.flot.websockets.js'
'/static/flot/jquery.flot.min.js',
'/static/flot/jquery.flot.time.min.js',
'/static/flot/jquery.flot.pie.min.js',
'/static/flot/jquery.flot.resize.min.js',
'/static/js/flot-functions.js',
'/static/awit-flot-toolkit/js/jquery.flot.awitds.js',
'/static/awit-flot-toolkit/js/resize.js'
);
my @stylesheets = (
'/static/awit-flot-toolkit/css/awit-flot-toolkit.css'
);
END:
# String put in <script> </script> tags after the above files are loaded
my $javascript =<<EOF;
//
// Tooltip - Displays detailed information regarding the data point
//
function showTooltip(x, y, contents) {
jQuery('<div id="tooltip">' + contents + '</div>').css( {
position: 'absolute',
display: 'none',
top: y - 30,
left: x - 50,
color: "#fff",
padding: '2px 5px',
'border-radius': '6px',
'background-color': '#000',
opacity: 0.80
}).appendTo("body").fadeIn(200);
}
var previousPoint = null;
jQuery("#ajaxData").bind("plothover", function (event, pos, item) {
if (item) {
if (previousPoint != item.dataIndex) {
previousPoint = item.dataIndex;
jQuery("#tooltip").remove();
var x = item.datapoint[0].toFixed(0),
y = item.datapoint[1].toFixed(0);
showTooltip(item.pageX, item.pageY,
item.series.label + ' date: ' + month);
}
}
else {
jQuery("#tooltip").remove();
previousPoint = null;
}
});
// Setting up the graph here
options = {
series: {
lines: { show: true,
lineWidth: 1,
fill: true,
fillColor: { colors: [ { opacity: 0.1 }, { opacity: 0.13 } ] }
},
points: { show: true,
lineWidth: 2,
radius: 3
},
shadowSize: 0,
stack: true
},
grid: {
hoverable: true,
clickable: false,
tickColor: "#f9f9f9",
borderWidth: 0
},
legend: {
// show: false
labelBoxBorderColor: "#fff"
//,container: '#legend-container'
},
xaxis: {
mode: "time",
tickSize: [2, "second"],
tickFormatter: function (v, axis) {
var date = new Date(v);
if (date.getSeconds() % 1 == 0) {
var hours = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
var minutes = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
var seconds = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
return hours + ":" + minutes + ":" + seconds;
} else {
return "";
}
},
},
yaxis: {
min: 0,
max: 4000,
tickFormatter: function (v, axis) {
if (v % 10 == 0) {
return v;
} else {
return "";
}
},
}
}
//*/
// loading the stats.json.js file as data and drawing the graph on successful response.
jQuery.ajax({
url: '/static/stats.json.js',
dataType: "json",
success: function(statsData){
plot = null;
if (statsData.length > 0) {
plot = jQuery.plot(jQuery("#ajaxData"), statsData, options);
}
}
return (HTTP_OK,$content,{
'javascripts' => \@javascripts,
'javascript' => $javascript,
'stylesheets' => \@stylesheets
});
}
# Return data in json format
#
# Supported URLs:
#
# pool=<tag>:<interface-group-id>:<pool-name>
#
# class=<tag>:<interface-group-id>:<class-id>,...
#
# interface-group=<tag>:<interface-group>,...
#
# counter=<tag>:<counter>,...
#
# max=<max number of entries to return, default 100>
#
# key=<data key to return>
sub jsondata
{
my ($kernel,$system,$client_session_id,$request) = @_;
# Parse GET data
my $queryParams = $request->uri->query_form_hash;
my $rawData = { };
# Check for some things that apply to all types of data
my $startTimestamp;
my $endTimestamp;
if (defined($queryParams->{'start'})) {
$startTimestamp = isNumber($queryParams->{'start'});
}
if (defined($queryParams->{'end'})) {
$endTimestamp = isNumber($queryParams->{'end'});
}
# Process pools
if (defined($queryParams->{'pool'})) {
# Simple de-dupication
my %poolSpecs;
foreach my $poolSpec ($request->uri->query_param('pool')) {
$poolSpecs{$poolSpec} = 1;
}
# Then loop through the unique keys
foreach my $poolSpec (keys %poolSpecs) {
# Check we have a tag, interface group ID and pool name
my ($tag,$rawInterfaceGroupID,$rawPoolName) = split(/:/,$poolSpec);
if (!defined($tag)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Invalid format for pool specification '%s'",
$poolSpec
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Invalid format for pool specification '$poolSpec'"},
{ 'type' => 'json' });
}
if (!defined($rawInterfaceGroupID)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has an invalid interface group ID '%s'",
$tag,
$poolSpec
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has invalid interface group ID '$rawPoolName'"},
{ 'type' => 'json' });
}
# Check if we can grab the interface group
my $interfaceGroup = getInterfaceGroup($rawInterfaceGroupID);
if (!defined($interfaceGroup)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has an invalid interface group ID '%s'",
$tag,
$rawInterfaceGroupID
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has an invalid interface group ID ".
"'$rawInterfaceGroupID'"},
{ 'type' => 'json' });
}
if (!defined($rawPoolName)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has an invalid pool name '%s'",
$tag,
$poolSpec
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has an invalid pool name '$rawPoolName'"},
{ 'type' => 'json' });
}
# Grab pool
my $pool = getPoolByName($rawInterfaceGroupID,$rawPoolName);
if (!defined($pool)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has an invalid pool name '%s'",
$tag,
$rawPoolName
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has an invalid pool name '$rawPoolName'"},
{ 'type' => 'json' });
}
# Grab SID
my $sid = opentrafficshaper::plugins::statistics::getSIDFromPID($pool->{'ID'});
if (!defined($sid)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' stats data cannot be found for pool ".
"'%s'",
$tag,
$rawPoolName
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' stats data cannot be found for pool '$rawPoolName'"},
{ 'type' => 'json' });
}
# Pull in stats data
my $statsData = opentrafficshaper::plugins::statistics::getStatsBySID($sid,undef,$startTimestamp,$endTimestamp);
# Loop with timestamps
foreach my $timestamp (sort keys %{$statsData}) {
# Grab the stat
my $tstat = $statsData->{$timestamp};
# Loop with its keys
foreach my $item (keys %{$tstat}) {
# Add the keys to the data to return
push(@{$rawData->{$tag}->{$item}->{'data'}},[
$timestamp,
$tstat->{$item}
]);
}
}
}
}
# Process classes
if (defined($queryParams->{'class'})) {
# Simple de-dupication
my %trafficClassIDSpecs;
foreach my $rawClassID ($request->uri->query_param('class')) {
$trafficClassIDSpecs{$rawClassID} = 1;
}
# Then loop through the unique keys
foreach my $trafficClassIDSpec (keys %trafficClassIDSpecs) {
# Check we have a tag, interface group ID and class
my ($tag,$rawInterfaceGroupID,$rawClassID) = split(/:/,$trafficClassIDSpec);
if (!defined($tag)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Invalid format for class ID specification '%s'",
$trafficClassIDSpec
);
return (HTTP_OK,{
'status' => 'fail',
'message' => "Invalid format for class ID specification '$trafficClassIDSpec'"
},
{ 'type' => 'json' }
);
}
if (!defined($rawInterfaceGroupID)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has an invalid interface group ID '%s'",
$tag,
$trafficClassIDSpec
);
return (HTTP_OK,{
'status' => 'fail',
'message' => "Tag '$tag' has an invalid interface group ID '$trafficClassIDSpec'"
},
{ 'type' => 'json' }
);
}
if (!defined($rawClassID)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has an invalid class ID '%s'",
$tag,
$trafficClassIDSpec
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has an invalid class ID '$trafficClassIDSpec'"},
{ 'type' => 'json' });
}
# Get more sane values...
my $interfaceGroup = getInterfaceGroup($rawInterfaceGroupID);
if (!defined($interfaceGroup)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has a non-existent interface group ID ".
"'%s'",
$tag,
$rawInterfaceGroupID
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has a non-existent interface group ID ".
"'$rawInterfaceGroupID'"},
{ 'type' => 'json' });
}
my $trafficClassID = isTrafficClassIDValid($rawClassID);
if (!defined($trafficClassID)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has a non-existent class ID '%s'",
$tag,
$rawClassID
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has a non-existent class ID '$rawClassID'"},
{ 'type' => 'json' });
}
# Grab data for each direction associated with a class ID on an inteface group
foreach my $direction ('Tx','Rx') {
# Grab stats ID
my $sid = opentrafficshaper::plugins::statistics::getSIDFromCID($interfaceGroup->{"${direction}Interface"},
$trafficClassID);
if (!defined($sid)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' stats data cannot be found for ".
"class ID '%s'",
$tag,
$trafficClassID
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' stats data cannot be found for class ID "
."'$trafficClassID'"},
{ 'type' => 'json' });
}
# Pull in stats data, override direction used
my $statsData = opentrafficshaper::plugins::statistics::getStatsBySID(
$sid,
{ 'Direction' => lc($direction) },
$startTimestamp,
$endTimestamp
);
# Loop with timestamps
foreach my $timestamp (sort keys %{$statsData}) {
# Grab the stat
my $tstat = $statsData->{$timestamp};
# Loop with its keys
foreach my $item (keys %{$tstat}) {
# Add the keys to the data to return
push(@{$rawData->{$tag}->{$item}->{'data'}},[
$timestamp,
$tstat->{$item}
]);
}
}
}
}
}
# Process interface groups
if (defined($queryParams->{'interface-group'})) {
# Simple de-dupication
my %interfaceGroupIDSpecs;
foreach my $rawInterfaceGroupID ($request->uri->query_param('interface-group')) {
$interfaceGroupIDSpecs{$rawInterfaceGroupID} = 1;
}
# Then loop through the unique keys
foreach my $interfaceGroupIDSpec (keys %interfaceGroupIDSpecs) {
# Check we have a tag, interface group ID and class
my ($tag,$rawInterfaceGroupID) = split(/:/,$interfaceGroupIDSpec);
if (!defined($tag)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Invalid format for interface group ".
"specification '%s'",
$interfaceGroupIDSpec
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Invalid format for interface group specification ".
"'$interfaceGroupIDSpec'"},
{ 'type' => 'json' });
}
if (!defined($rawInterfaceGroupID)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has an invalid interface group ID '%s'",
$tag,
$interfaceGroupIDSpec
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has an invalid interface group ID ".
"'$interfaceGroupIDSpec'"},
{ 'type' => 'json' });
}
my $interfaceGroupID = getInterfaceGroup($rawInterfaceGroupID);
if (!defined($interfaceGroupID)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has a non-existent interface group ID ".
"'%s'",
$tag,
$rawInterfaceGroupID
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has a non-existent interface group ID ".
"'$rawInterfaceGroupID'"},
{ 'type' => 'json' });
}
# Loop with both directions
foreach my $direction ('Tx','Rx') {
# Grab stats ID
my $sid = opentrafficshaper::plugins::statistics::getSIDFromCID($interfaceGroupID->{"${direction}Interface"},0);
if (!defined($sid)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' stats data cannot be found for ".
"interface group ID '%s'",
$tag,
$rawInterfaceGroupID
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' stats data cannot be found for interface group ".
"ID '$rawInterfaceGroupID'"},
{ 'type' => 'json' });
}
# Pull in stats data, override direction used
my $statsData = opentrafficshaper::plugins::statistics::getStatsBySID(
$sid,
{ 'Direction' => lc($direction) },
$startTimestamp,
$endTimestamp
);
# Loop with timestamps
foreach my $timestamp (sort keys %{$statsData}) {
# Grab the stat
my $tstat = $statsData->{$timestamp};
# Loop with its keys
foreach my $item (keys %{$tstat}) {
# Add the keys to the data to return
push(@{$rawData->{$tag}->{$item}->{'data'}},[
$timestamp,
$tstat->{$item}
]);
}
}
}
}
}
# If we need to return a counter, lets see what there is we can return...
if (defined($queryParams->{'counter'})) {
# Lets get unique counters as keys
my %counterSpecs;
foreach my $rawCounterSpec (@{$queryParams->{'counter'}->{'values'}}) {
$counterSpecs{$rawCounterSpec} = 1;
}
# Then loop through the unique keys
foreach my $counterSpec (keys %counterSpecs) {
# Check we have a tag and counter
my ($tag,$rawCounter) = split(/:/,$counterSpec);
if (!defined($tag)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Invalid format for counter specification '%s'",
$counterSpec
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Invalid format for counter specification '$counterSpec'"},
{ 'type' => 'json' });
}
if (!defined($rawCounter)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has an invalid interface group ID '%s'",
$tag,
$counterSpec
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has an invalid interface group ID ".
"'$counterSpec'"},
{ 'type' => 'json' });
}
# Grab the SID
my $sid = opentrafficshaper::plugins::statistics::getSIDFromCounter($rawCounter);
if (!defined($sid)) {
$system->{'logger'}->log(LOG_INFO,"[WEBSERVER/PAGES/STATISTICS] Tag '%s' has a non-existent counter '%s'",
$tag,
$counterSpec
);
return (HTTP_OK,{'status' => 'fail', 'message' => "Tag '$tag' has a non-existent counter ".
"'$counterSpec'"},
{ 'type' => 'json' });
}
# Pull in stats data
my $statsData = opentrafficshaper::plugins::statistics::getStatsBasicBySID($sid,{ 'Name' => $rawCounter });
# Loop with timestamps
foreach my $timestamp (sort keys %{$statsData}) {
# Grab the stat
my $tstat = $statsData->{$timestamp};
# Loop with its keys
foreach my $item (keys %{$tstat}) {
# Add the keys to the data to return
push(@{$rawData->{$tag}->{$item}->{'data'}},[
$timestamp,
$tstat->{$item}
]);
}
}
}
}
return (HTTP_OK,{'status' => 'success', 'data' => $rawData},{ 'type' => 'json' });
}
# Function to build the javascript we need to display graphs in a canvas
sub _buildGraphJavascript
{
my $graphs = shift;
my $javascript = "";
foreach my $graph (@{$graphs}) {
my $encodedCanvasName = encode_entities($graph->{'Tag'});
# Items we going to need...
my @datasources = ();
my $axesIdentifiers = { 'X' => [ ], 'Y' => [ ] };
my @axesStrList;
# Loop with and build the JS for our datasources
foreach my $datasource (@{$graph->{'Datasources'}}) {
# Websocket based data
if ($datasource->{'Type'} eq "websocket") {
# Create Subscriptions
my @subscriptions;
foreach my $subscription (@{$datasource->{'Subscriptions'}}) {
my $encodedSubscription = encode_entities($subscription);
push(@subscriptions,"{ 'function': 'subscribe', args: ['$encodedCanvasName','$encodedSubscription'] }");
}
# Create subscription string
my $subscriptionStr = join(',',@subscriptions);
# Add datasource
push(@datasources,<<EOF);
{
type: 'websocket',
uri: 'ws://'+window.location.host+'/statistics/graphdata',
shared: true,
// Websocket specific
onconnect: [
$subscriptionStr
]
}
EOF
# JSON based data
} elsif ($datasource->{'Type'} eq "ajax") {
# Create Subscriptions
my @subscriptions;
foreach my $subscription (@{$datasource->{'Subscriptions'}}) {
my $encodedType = encode_entities($subscription->{'Type'});
my $encodedData = encode_entities($subscription->{'Data'});
# Data we nee dto pull
push(@subscriptions,sprintf(
"%s=%s:%s",
$encodedType,
$encodedCanvasName,
$encodedData
));
# Check if we have a start period
if (defined($subscription->{'StartTimestamp'})) {
push(@subscriptions,sprintf("start=%s",$subscription->{'StartTimestamp'}));
}
# Check if we have an end period
if (defined($subscription->{'EndTimestamp'})) {
push(@subscriptions,sprintf("end=%s",$subscription->{'EndTimestamp'}));
}
}
# Create subscription string
my $subscriptionStr = join('&',@subscriptions);
# Add datasource
push(@datasources,<<EOF);
{
type: 'ajax',
url: '///'+window.location.host+'/statistics/jsondata?$subscriptionStr'
}
EOF
}
}
# Loop with axes and build our axes structure
foreach my $axis (keys %{$axesIdentifiers}) {
foreach my $identifier (@{$graph->{"${axis}Identifiers"}}) {
# Our first identifier option is the label
my @options = (
sprintf("label: '%s'", encode_entities($identifier->{'Label'}))
);
# Set limiting factors if there are any
if (defined($identifier->{'Timespan'})) {
push(@options,sprintf("maxTimespan: %s",$identifier->{'Timespan'}));
}
if (defined($identifier->{'Count'})) {
push(@options,sprintf("maxCount: %s",$identifier->{'Count'}));
}
# Join everything up
my $optionsStr = join(' ,',@options);
# Add to axes
push(@{$axesIdentifiers->{$axis}},sprintf("'%s': { %s }",encode_entities($identifier->{'Name'}),$optionsStr));
}
push(@axesStrList,
sprintf("%saxis: { '%s': { %s } }",lc($axis),$encodedCanvasName,join(',',@{$axesIdentifiers->{$axis}}))
);
}
# Build final JS
my $datasourceStr = join(',',@datasources);
my $axesStr = join(',',@axesStrList);
$javascript .=<<EOF;
awit_flot_draw_$graph->{'Type'}({
id: '$encodedCanvasName',
awitds: {
sources: [
$datasourceStr
],
$axesStr
}
});
EOF
}
return (HTTP_OK,$content,{ 'javascripts' => \@javascripts, 'javascript' => $javascript });
return $javascript;
}
1;
# vim: ts=4
# OpenTrafficShaper webserver module: statistics websockets
# Copyright (C) 2007-2013, AllWorldIT
#
# Copyright (C) 2007-2023, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
......@@ -31,16 +31,24 @@ our (@ISA,@EXPORT,@EXPORT_OK);
use DateTime;
use HTML::Entities;
use HTTP::Status qw( :constants );
use JSON;
use POE;
use URI;
use awitpt::util qw(
parseKeyPairString
);
use opentrafficshaper::logger;
use opentrafficshaper::utils;
use opentrafficshaper::plugins::configmanager qw(
getPoolByName
getInterfaceGroup
isTrafficClassIDValid
);
use constant {
VERSION => '0.0.1'
VERSION => '1.0.0'
};
......@@ -48,54 +56,56 @@ use constant {
our $pluginInfo = {
Name => "Webserver/WebSockets/Statistics",
Version => VERSION,
Requires => ["webserver","statistics"],
Init => \&plugin_init,
};
# Copy of system globals
my $globals;
# Logger
my $logger;
# Stats subscriptsions
my $cSubscriptions = {}; # Clients
my $uSubscriptions = {}; # Users
my $subscribers = {}; # Connections subscribed, indexed by client_session_id then ssid
my $subscriberMap = {}; # Index of connections by ssid
# Initialize plugin
sub plugin_init
{
$globals = shift;
my $system = shift;
# Setup our environment
$logger = $globals->{'logger'};
$logger = $system->{'logger'};
$logger->log(LOG_NOTICE,"[WEBSERVER] OpenTrafficShaper Snapin [WebSockets/Statistics] Module v".VERSION." - Copyright (c) 2013, AllWorldIT");
$logger->log(LOG_NOTICE,"[WEBSERVER] OpenTrafficShaper Snapin [WebSockets/Statistics] Module v%s - Copyright (c) 2013-2014".
", AllWorldIT",
VERSION
);
# Protocol conversion
opentrafficshaper::plugins::webserver::snapin_register('HTTP=>WebSocket','statistics','graphdata', {
'requires' => [ 'statistics' ],
'on_request' => \&graphdata_http2websocket,
'on_disconnect' => \&websocket_disconnect,
}
);
# Live graphdata feed
opentrafficshaper::plugins::webserver::snapin_register('WebSocket','statistics','graphdata', {
'requires' => [ 'statistics' ],
'on_request' => \&graphdata_websocket,
'on_request' => \&graphdata_websocket_onrequest,
'on_disconnect' => \&graphdata_websocket_disconnect
}
);
# This is our session handling sending data to connections
POE::Session->create(
inline_states => {
_start => \&session_start,
_stop => \&session_stop,
_start => \&_session_start,
_stop => \&_session_stop,
'websocket.send' => \&do_send,
'websocket.send' => \&_session_websocket_send,
}
);
......@@ -104,10 +114,11 @@ sub plugin_init
# Session initialization
sub session_start
sub _session_start
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# Set our alias
$kernel->alias_set("plugin.webserver.websockets.statistics");
......@@ -116,17 +127,16 @@ sub session_start
# Session stop
sub session_stop
sub _session_stop
{
my ($kernel,$heap) = @_[KERNEL,HEAP];
# Remove our alias
$kernel->alias_remove("plugin.webserver.websockets.statistics");
# Destroy data
$globals = undef;
$cSubscriptions = {};
$uSubscriptions = {};
$subscribers = {};
$subscriberMap = {};
$logger->log(LOG_DEBUG,"[WEBSERVER] Snapin/WebSockets/Statistics - Shutdown");
......@@ -135,45 +145,63 @@ sub session_stop
# Send data to client
sub do_send
sub _session_websocket_send
{
my ($kernel,$heap,$uid,$data) = @_[KERNEL, HEAP, ARG0, ARG1];
my ($kernel,$heap,$statsData) = @_[KERNEL, HEAP, ARG0, ARG1];
# Loop through subscriptions
foreach my $client_session_id (keys %{$uSubscriptions->{$uid}}) {
my $socket = $uSubscriptions->{$uid}->{$client_session_id};
# Loop with stats data
foreach my $ssid (keys %{$statsData}) {
my $ssidStat = $statsData->{$ssid};
use Data::Dumper; print STDERR "Got request to send client '$client_session_id': ".Dumper($data);
# Check if we know about this SSID
if (!defined($subscriberMap->{$ssid})) {
$logger->log(LOG_ERR,"[WEBSERVER] Snapin/WebSockets/Statistics - Subscription inconsidency with SSID '$ssid'");
next;
}
# my $json = sprintf('{"label": "%s", data: [%s] }', );
# First stage, pull in the data items we want
my $rawData;
# Loop with timestamps
foreach my $timestamp (sort keys %{$ssidStat}) {
# Grab the stat
my $tstat = $ssidStat->{$timestamp};
# Loop with its keys
foreach my $item (keys %{$tstat}) {
# Add the keys to the data to return
push(@{$rawData->{$item}->{'data'}},[
$timestamp,
$tstat->{$item}
]);
}
}
my $socket = $subscriberMap->{$ssid}->{'Socket'};
my $tag = $subscriberMap->{$ssid}->{'Tag'};
$socket->put("hello there");
$socket->put(_json_data({ $tag => $rawData }));
}
}
}
# HTTP to WebSocket
sub websocket_disconnect
sub graphdata_websocket_disconnect
{
my ($kernel,$globals,$client_session_id) = @_;
my $logger = $globals->{'logger'};
$logger->log(LOG_INFO,"[WEBSERVER] Snapin/WebSockets/Statistics - Client '$client_session_id' disconnected");
# Loop with our UID's
foreach my $uid (keys %{$cSubscriptions->{$client_session_id}}) {
# Remove tracking info
delete($uSubscriptions->{$uid}->{$client_session_id});
delete($cSubscriptions->{$client_session_id}->{$uid});
# If there are no more clients for this uid, then unsubscribe it
if (keys %{$uSubscriptions->{$uid}} < 1) {
$kernel->post('statistics' => 'unsubscribe' => 'plugin.webserver.websockets.statistics' => 'websocket.send' => $uid);
}
# Loop with our clients' subscriber ID's
foreach my $ssid (keys %{$subscribers->{$client_session_id}}) {
# And unsubscribe them
opentrafficshaper::plugins::statistics::unsubscribe($ssid);
# Remove the ssid map
delete($subscriberMap->{$ssid});
}
# Remove the client
delete($subscribers->{$client_session_id});
}
......@@ -181,33 +209,282 @@ sub websocket_disconnect
sub graphdata_http2websocket
{
my ($kernel,$globals,$client_session_id,$request,$socket) = @_;
my $logger = $globals->{'logger'};
# Grab the query string
my %queryForm = $request->uri->query_form();
# my %queryForm = $request->uri->query_form();
# Check we have a user ID
if (!defined($queryForm{'uid'})) {
return (HTTP_BAD_REQUEST,"Request not valid","Request does not contain a 'uid' parameter");
}
my $uid = $queryForm{'uid'};
# if (!defined($queryForm{'uid'})) {
# return (HTTP_BAD_REQUEST,"Request not valid","Request does not contain a 'uid' parameter");
# }
# my $uid = $queryForm{'uid'};
$logger->log(LOG_INFO,"[WEBSERVER] Snapin/WebSockets/Statistics - Accepting upgrade of HTTP to WebSocket");
# Subscribe to the stats for this user
if (!defined($uSubscriptions->{$uid}) || !(keys %{$uSubscriptions->{$uid}})) {
$logger->log(LOG_INFO,"[WEBSERVER] Snapin/WebSockets/Statistics - Subscribing to '$uid'");
$kernel->post('statistics' => 'subscribe' => 'plugin.webserver.websockets.statistics' => 'websocket.send' => $uid);
}
# if (!defined($uSubscriptions->{$uid}) || !(keys %{$uSubscriptions->{$uid}})) {
# $logger->log(LOG_INFO,"[WEBSERVER] Snapin/WebSockets/Statistics - Subscribing to '$uid'");
# $kernel->post('statistics' => 'subscribe' => 'plugin.webserver.websockets.statistics' => 'websocket.send' => $uid);
# }
# Setup tracking of our client & user subscriptions
$cSubscriptions->{$client_session_id}->{$uid} = $socket;
$uSubscriptions->{$uid}->{$client_session_id} = $socket;
# $cSubscriptions->{$client_session_id}->{$uid} = $socket;
# $uSubscriptions->{$uid}->{$client_session_id} = $socket;
# And return...
return;
}
# Websocket data handler
sub graphdata_websocket_onrequest
{
my ($kernel,$globals,$client_session_id,$request,$socket) = @_;
my $logger = $globals->{'logger'};
# Parse the command we got...
if ($request->{'function'} eq "subscribe") {
# Grab the first parameter as our tag
my $tag = shift(@{$request->{'args'}});
if (!defined($tag) || ref($tag) ne "") {
return (
opentrafficshaper::plugins::webserver::WS_ERROR,
"The first parameter of 'subscribe' must be a text based tag"
);
}
# Pull off our datasets
my $datasets = { };
foreach my $arg (@{$request->{'args'}}) {
my ($item,$params) = split(/=/,$arg);
push(@{$datasets->{$item}},$params);
}
# Parse dataset data
my @sidList;
if (defined($datasets->{'pool'})) {
# Loop with pool specifications
foreach my $poolSpec (@{$datasets->{'pool'}}) {
# Split interface group id and pool name
my ($rawInterfaceGroupID,$rawPoolName) = split(/:/,$poolSpec);
if (!defined($rawInterfaceGroupID)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource for pool has invalid format '$poolSpec'"
);
}
if (!defined($rawPoolName)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource for pool has invalid format '$poolSpec'"
);
}
# Check if we can grab the interface group
my $interfaceGroup = getInterfaceGroup($rawInterfaceGroupID);
if (!defined($interfaceGroup)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource has invalid interface group '$rawInterfaceGroupID'"
);
}
# Check if the pool name exists
my $pool = getPoolByName($rawInterfaceGroupID,$rawPoolName);
if (!defined($pool)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource has invalid pool '$rawPoolName'"
);
}
# Check if we have a stats ID
my $sid = opentrafficshaper::plugins::statistics::getSIDFromPID($pool->{'ID'});
if (!defined($sid)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource stats for pool not found '$rawPoolName'"
);
}
# Add SID to SID list that we need to subscribe to
push(@sidList,{'SID' => $sid});
}
}
if (defined($datasets->{'class'})) {
# Loop with class specifications
foreach my $classIDSpec (@{$datasets->{'class'}}) {
# Check we have a tag, interface group ID and class ID
my ($rawInterfaceGroupID,$rawClassID) = split(/:/,$classIDSpec);
if (!defined($rawInterfaceGroupID)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource for class has invalid format '$classIDSpec'"
);
}
if (!defined($rawClassID)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource for class has invalid format '$classIDSpec'"
);
}
# Get more sane values...
my $interfaceGroup = getInterfaceGroup($rawInterfaceGroupID);
if (!defined($interfaceGroup)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource has invalid interface group '$rawInterfaceGroupID'"
);
}
my $classID = isTrafficClassIDValid($rawClassID);
if (!defined($classID)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource has invalid class '$rawClassID'"
);
}
# Loop with both directions
foreach my $direction ('Tx','Rx') {
my $interfaceDevice = $interfaceGroup->{"${direction}Interface"};
# Grab stats ID
my $sid = opentrafficshaper::plugins::statistics::getSIDFromCID($interfaceDevice,$classID);
if (!defined($sid)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource stats for class '$classID' on interface '$interfaceDevice' not found"
);
}
# Add SID to SID list that we need to subscribe to
push(@sidList,{'SID' => $sid, 'Conversions' => { 'Direction' => lc($direction) }});
}
}
}
if (defined($datasets->{'interface-group'})) {
# Loop with interface-group specifications
foreach my $interfaceGroupSpec (@{$datasets->{'interface-group'}}) {
# Check we have a tag, interface group ID and class ID
my ($rawInterfaceGroupID) = split(/:/,$interfaceGroupSpec);
if (!defined($rawInterfaceGroupID)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource for interface group has invalid format '$interfaceGroupSpec'"
);
}
# Get more sane values...
my $interfaceGroup = getInterfaceGroup($rawInterfaceGroupID);
if (!defined($interfaceGroup)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource has invalid interface group '$rawInterfaceGroupID'"
);
}
# Loop with both directions
foreach my $direction ('Tx','Rx') {
my $interfaceDevice = $interfaceGroup->{"${direction}Interface"};
# Grab stats ID using a small direction hack
my $sid = opentrafficshaper::plugins::statistics::getSIDFromCID($interfaceDevice,0);
if (!defined($sid)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource stats for interface '$interfaceDevice' not found"
);
}
# Add SID to SID list that we need to subscribe to
push(@sidList,{'SID' => $sid, 'Conversions' => { 'Direction' => lc($direction) }});
}
}
}
if (defined($datasets->{'counter'})) {
# Loop with counter specifications
foreach my $counterSpec (@{$datasets->{'counter'}}) {
# Check we have a tag and counter
my ($rawCounter) = split(/:/,$counterSpec);
if (!defined($rawCounter)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource for counter has invalid format '$counterSpec'"
);
}
# Grab the SID
my $sid = opentrafficshaper::plugins::statistics::getSIDFromCounter($rawCounter);
if (!defined($sid)) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Datasource stats for counter not found '$rawCounter'"
);
}
# Add SID to SID list that we need to subscribe to
push(@sidList,{'SID' => $sid, 'Conversions' => { 'Name' => $rawCounter }});
}
}
# No datasources?
if (!@sidList) {
return (
opentrafficshaper::plugins::webserver::WS_FAIL,
"Failed to identify any subscription requests"
);
}
# Loop wiht subscription list
foreach my $item (@sidList) {
my $ssid = opentrafficshaper::plugins::statistics::subscribe(
$item->{'SID'},
$item->{'Conversions'},
'plugin.webserver.websockets.statistics',
'websocket.send'
);
# Save this client and the streaming id (ssid) we got back
$subscriberMap->{$ssid} = $subscribers->{$client_session_id}->{$ssid} = {
'Tag' => $tag,
'SID' => $item->{'SID'},
'Socket' => $socket
};
}
return (opentrafficshaper::plugins::webserver::WS_OK,"Subscribed");
# No command at all
} else {
return (opentrafficshaper::plugins::webserver::WS_ERROR,"Function '$request->{'function'}' does not exist");
}
return (opentrafficshaper::plugins::webserver::WS_OK,"Function not found");
}
#
# Internal functions
#
# Return a json dataset
sub _json_data
{
my $data = shift;
# Build the structure we're going to encode
my $res;
$res->{'status'} = "success";
$res->{'data'} = $data;
# Use ID of 0 to signify out of band data
$res->{'id'} = -1;
return encode_json($res);
}
1;
# vim: ts=4
# OpenTrafficShaper webserver module
# Copyright (C) 2007-2013, AllWorldIT
#
# Copyright (C) 2007-2023, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
......@@ -21,23 +21,24 @@ package opentrafficshaper::plugins::webserver;
use strict;
use warnings;
use bytes;
use HTML::Entities;
use HTTP::Response;
use HTTP::Status qw( :constants :is );
use JSON;
use POE qw( Component::Server::TCP );
use POE::Filter::HybridHTTP;
use URI;
use opentrafficshaper::logger;
use opentrafficshaper::plugins;
use opentrafficshaper::POE::Filter::HybridHTTP;
# Pages (this is used a little below)
use opentrafficshaper::plugins::webserver::pages::static;
use opentrafficshaper::plugins::webserver::pages::index;
use opentrafficshaper::plugins::webserver::pages::limits;
use opentrafficshaper::plugins::webserver::pages::statistics;
use opentrafficshaper::plugins::webserver::pages::configmanager;
......@@ -52,7 +53,12 @@ our (@ISA,@EXPORT,@EXPORT_OK);
);
use constant {
VERSION => '0.0.1'
VERSION => '1.0.0',
# WebSocket return status
WS_OK => 0,
WS_ERROR => -1,
WS_FAIL => -2
};
......@@ -60,19 +66,21 @@ use constant {
our $pluginInfo = {
Name => "Webserver",
Version => VERSION,
Init => \&plugin_init,
Start => \&plugin_start,
};
# Copy of system globals
# Our globals
my $globals;
# Copy of system logger
my $logger;
# Client connections open
my $connections = { };
#
# $globals->{'Connections'}
# Web resources
my $resources = {
......@@ -85,16 +93,31 @@ my $resources = {
'_catchall' => \&opentrafficshaper::plugins::webserver::pages::static::_catchall,
},
'limits' => {
'default' => \&opentrafficshaper::plugins::webserver::pages::limits::default,
'add' => \&opentrafficshaper::plugins::webserver::pages::limits::add,
},
'statistics' => {
'by-username' => \&opentrafficshaper::plugins::webserver::pages::statistics::byusername,
'data-by-username' => \&opentrafficshaper::plugins::webserver::pages::statistics::databyusername,
'default' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_list,
'pool-list' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_list,
'pool-override-add' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_override_addedit,
'pool-override-list' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_override_list,
'pool-override-remove' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_override_remove,
'pool-override-edit' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_override_addedit,
'pool-add' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_addedit,
'pool-list' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_list,
'pool-remove' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_remove,
'pool-edit' => \&opentrafficshaper::plugins::webserver::pages::limits::pool_addedit,
'poolmember-add' => \&opentrafficshaper::plugins::webserver::pages::limits::poolmember_addedit,
'poolmember-list' => \&opentrafficshaper::plugins::webserver::pages::limits::poolmember_list,
'poolmember-remove' => \&opentrafficshaper::plugins::webserver::pages::limits::poolmember_remove,
'poolmember-edit' => \&opentrafficshaper::plugins::webserver::pages::limits::poolmember_addedit,
'limit-add' => \&opentrafficshaper::plugins::webserver::pages::limits::limit_add
},
'configmanager' => {
'default' => \&opentrafficshaper::plugins::webserver::pages::configmanager::default,
'add' => \&opentrafficshaper::plugins::webserver::pages::configmanager::add,
'admin-config' => \&opentrafficshaper::plugins::webserver::pages::configmanager::admin_config,
},
},
};
......@@ -107,7 +130,7 @@ sub snapin_register
my ($protocol,$module,$action,$data) = @_;
$logger->log(LOG_INFO,"[WEBSERVER] Registered snapin: protocol = $protocol, module = $module, action = $action");
$logger->log(LOG_INFO,"[WEBSERVER] Registered snapin: protocol = %s, module = %s, action = %s",$protocol,$module,$action);
# Load resource
$resources->{$protocol}->{$module}->{$action} = $data;
......@@ -115,26 +138,28 @@ sub snapin_register
# Initialize plugin
sub plugin_init
{
$globals = shift;
my $system = shift;
# Setup our environment
$logger = $globals->{'logger'};
$logger = $system->{'logger'};
$logger->log(LOG_NOTICE,"[WEBSERVER] OpenTrafficShaper Webserver Module v".VERSION." - Copyright (c) 2013, AllWorldIT");
$logger->log(LOG_NOTICE,"[WEBSERVER] OpenTrafficShaper Webserver Module v%s - Copyright (c) 2007-2023, AllWorldIT",VERSION);
# Initialize
$globals->{'Connections'} = { };
# Spawn a web server on port 8088 of all interfaces.
POE::Component::Server::TCP->new(
Port => 8088,
Port => 8080,
# Handle session connections & disconnections
ClientConnected => \&server_client_connected,
ClientDisconnected => \&server_client_disconnected,
# Filter to handle HTTP
ClientFilter => 'POE::Filter::HybridHTTP',
ClientFilter => 'opentrafficshaper::POE::Filter::HybridHTTP',
# Function to handle HTTP requests (as we passing through a filter)
ClientInput => \&server_request,
# Setup the sever
......@@ -142,11 +167,34 @@ sub plugin_init
Stopped => \&server_session_stop,
);
# Load statistics pages if the statistics module is enabled
if (isPluginLoaded('statistics')) {
# Check if we can actually load the pages
eval("use opentrafficshaper::plugins::webserver::pages::statistics");
if ($@) {
$logger->log(LOG_INFO,"[WEBSERVER] Failed to load statistics pages: %s",$@);
exit;
} else {
# Load resources
$resources->{'HTTP'}->{'statistics'} = {
'by-pool' => \&opentrafficshaper::plugins::webserver::pages::statistics::byPool,
'by-class' => \&opentrafficshaper::plugins::webserver::pages::statistics::byClass,
'jsondata' => \&opentrafficshaper::plugins::webserver::pages::statistics::jsondata,
'dashboard' => \&opentrafficshaper::plugins::webserver::pages::statistics::dashboard,
'igdashboard' => \&opentrafficshaper::plugins::webserver::pages::statistics::igdashboard
};
$logger->log(LOG_INFO,"[WEBSERVER] Loaded statistics pages as statistics module is loaded");
}
}
return 1;
}
# Start the plugin
sub plugin_start
{
......@@ -164,15 +212,20 @@ sub server_session_start
}
# Server session stopped
sub server_session_stop
{
my $kernel = $_[KERNEL];
# Tear down data
$globals = undef;
$logger->log(LOG_DEBUG,"[WEBSERVER] Shutdown");
}
# Signal that the client has connected
sub server_client_connected
{
......@@ -181,40 +234,47 @@ sub server_client_connected
# Save our socket on the client
$connections->{$client_session_id}->{'socket'} = $heap->{'client'};
$connections->{$client_session_id}->{'protocol'} = 'HTTP';
$globals->{'Connections'}->{$client_session_id}->{'Socket'} = $heap->{'client'};
$globals->{'Connections'}->{$client_session_id}->{'Protocol'} = 'HTTP';
}
# Signal that the client has disconnected
# Signal that the client has disconnected
sub server_client_disconnected
{
my ($kernel,$heap,$session,$request) = @_[KERNEL, HEAP, SESSION, ARG0];
my $client_session_id = $session->ID;
my $client_session_id = $session->ID;
my $client = $globals->{'Connections'}->{$client_session_id};
# Check if we have a disconnection function to call
if (
defined($connections->{$client_session_id}->{'resource'}) &&
defined($connections->{$client_session_id}->{'resource'}->{'handler'}) &&
ref($connections->{$client_session_id}->{'resource'}->{'handler'}) eq 'HASH' &&
defined($connections->{$client_session_id}->{'resource'}->{'handler'}->{'on_disconnect'})
defined($client->{'Resource'}) &&
defined($client->{'Resource'}->{'Handler'}) &&
ref($client->{'Resource'}->{'Handler'}) eq 'HASH' &&
defined($client->{'Resource'}->{'Handler'}->{'on_disconnect'})
) {
# Call disconnection function
$connections->{$client_session_id}->{'resource'}->{'handler'}->{'on_disconnect'}->($kernel,$globals,$client_session_id);
$client->{'Resource'}->{'Handler'}->{'on_disconnect'}
->($kernel,$globals,$client_session_id);
}
# Remove client session
delete($connections->{$client_session_id});
delete($globals->{'Connections'}->{$client_session_id});
}
# Handle the HTTP request
sub server_request
{
my ($kernel,$heap,$session,$request) = @_[KERNEL, HEAP, SESSION, ARG0];
my $client_session_id = $session->ID;
my $conn = $connections->{$client_session_id};
my $client = $globals->{'Connections'}->{$client_session_id};
# Our response back if one, and if we should just close the connection or not
......@@ -222,7 +282,7 @@ sub server_request
my $closeWhenDone = 0;
# Its HTTP
if ($conn->{'protocol'} eq "HTTP") {
if ($client->{'Protocol'} eq "HTTP") {
# Check the protocol we're currently handling
# We may have a response from the filter indicating an error
if (ref($request) eq "HTTP::Response") {
......@@ -234,18 +294,19 @@ sub server_request
}
# Support for HTTP/1.1 "Connection: close" header...
if ($request->header('Connection') eq "close") {
my $connection = $request->header('Connection');
if (defined($connection) && $connection eq "close") {
$closeWhenDone = 1;
}
# Its a websocket
} elsif ($conn->{'protocol'} eq "WebSocket") {
# XXX - this should call the callback
} elsif ($client->{'Protocol'} eq "WebSocket") {
$response = _server_request_websocket($kernel,$client_session_id,$request);
}
# If there is a response send it
if (defined($response)) {
$conn->{'socket'}->put($response);
$client->{'Socket'}->put($response);
}
# Check if connection must be closed
......@@ -255,6 +316,7 @@ sub server_request
}
# Display fault
sub httpDisplayFault
{
......@@ -285,6 +347,7 @@ EOF
}
# Do a redirect
sub httpRedirect
{
......@@ -294,6 +357,7 @@ sub httpRedirect
}
# Create a response object
sub httpCreateResponse
{
......@@ -314,43 +378,77 @@ sub httpCreateResponse
# Set header
$headers->content_type("text/html");
# Bootstrap stuff
my $mainCols = 12;
# Check if we have a menu structure, if we do, display the sidebar
my $styleStr = "";
my $menuStr = "";
my $stylesheetsStr = "";
my $stylesheetStr = "";
my $javascriptStr = "";
if (defined($options)) {
# Check if style snippet exists
if (defined(my $style = $options->{'style'})) {
$styleStr .= $style;
}
my $versionStr = VERSION;
if (defined($options)) {
# Check if menu exists
if (my $menu = $options->{'menu'}) {
$menuStr =<<EOF;
<ul class="nav nav-pills nav-stacked">
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
EOF
# Loop with sub menu sections
foreach my $section (keys %{$menu}) {
$menuStr .=<<EOF;
<li class="nav-header">$section</li>
my @menuItems = ();
foreach my $section (@{$menu}) {
my $menuItem = "";
my $sectionName = encode_entities($section->{'name'});
$menuItem .=<<EOF;
<li class="active text-center"><a href="#">$sectionName</a></li>
EOF
# Loop with menu items
foreach my $item (keys %{$menu->{$section}}) {
my $link = "/" . $module . "/" . $menu->{$section}->{$item};
# Sanitize slightly
$link =~ s,/+$,,;
foreach my $item (@{$section->{'items'}}) {
my $itemName = encode_entities($item->{'name'});
# Sanitize link
my $itemLink = "/" . $module . "/" . $item->{'link'};
$itemLink =~ s,/+$,,;
# Build sections
$menuStr .=<<EOF;
<li><a href="$link">$item</a></li>
$menuItem .=<<EOF;
<li class="text-center"><a href="$itemLink">$itemName</a></li>
EOF
}
# Add menu item
push(@menuItems,$menuItem);
}
# Join menu items
$menuStr .= join(<<EOF,@menuItems);
<hr />
EOF
$menuStr .=<<EOF;
</ul>
</ul>
</div>
EOF
# Reduce number of main cols to make way for menu
$mainCols = 10;
}
# Check if we have a list of style assets
if (defined(my $stylesheets = $options->{'stylesheets'})) {
foreach my $script (@{$stylesheets}) {
$stylesheetsStr .=<<EOF;
<link href="$script" rel="stylesheet"></script>
EOF
}
}
# Check if stylesheet snippet exists
if (defined(my $stylesheet = $options->{'stylesheet'})) {
$stylesheetStr .=<<EOF;
$stylesheet
EOF
}
# Check if we have a list of javascript assets
if (defined(my $javascripts = $options->{'javascripts'})) {
foreach my $script (@{$javascripts}) {
......@@ -372,30 +470,33 @@ EOF
# Create the payload we returning
$payload = <<EOF;
<!DOCTYPE html>
<html>
<head>
<title>OpenTrafficShaper - Enterprise Traffic Shaper</title>
<!-- Meta -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Assets -->
<link href="/static/favicon.ico" rel="icon" />
<link href="/static/jquery-ui/css/ui-lightness/jquery-ui.min.css" rel="stylesheet">
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/jquery-ui/css/ui-lightness/jquery-ui.min.css" rel="stylesheet" />
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
$stylesheetsStr
<style type="text/css">
body {
padding-top: 50px;
}
.main-area {
padding-top: 15px;
padding-bottom: 15px;
}
$styleStr
$stylesheetStr
</style>
<!-- End Assets -->
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<a class="navbar-brand" href="/">
<img src="/static/logo-inverted-short.png" alt="Open Traffic Shaper" width="100%" height="auto" />
</a>
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
......@@ -403,11 +504,10 @@ $styleStr
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">OpenTrafficShaper</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li class="active"><a href="/">Home</a></li>
<li><a href="/limits">Limits</a></li>
<li><a href="/configmanager">ConfigManager</a></li>
</ul>
......@@ -415,20 +515,22 @@ $styleStr
</div>
</div>
<div class="main-area container">
<div class="col-md-2">
<div style="padding: 15px 15px">
<div class="row">
$menuStr
</div>
<div class="col-md-10">
<div class="col-md-$mainCols">
$content
</div>
</div>
<hr>
</div>
<div style="padding: 0 15px">
<hr />
<footer>
<p class="muted">v$globals->{'version'} - Copyright &copy; 2013, <a href="http://www.allworldit.com">AllWorldIT</a></p>
<p class="muted">
v$versionStr - Copyright &copy; 2007-2023, <a href="http://www.allworldit.com">AllWorldIT</a>
</p>
</footer>
</div>
</body>
......@@ -467,9 +569,10 @@ EOF
sub _server_request_http
{
my ($kernel,$client_session_id,$request) = @_;
my $conn = $connections->{$client_session_id};
my $conn = $globals->{'Connections'}->{$client_session_id};
my $protocol = "HTTP"; # By default we're HTTP
# Pull off connection attributes
......@@ -522,19 +625,23 @@ sub _server_request_http
return httpDisplayFault(HTTP_INTERNAL_SERVER_ERROR,"Internal server error","Server configuration error");
}
$logger->log(LOG_DEBUG,"[WEBSERVER] Parsed HTTP request into: module='$module', action='$action'");
$logger->log(LOG_DEBUG,"[WEBSERVER] Parsed HTTP request into: module='%s', action='%s'",$module,$action);
# Save what resource we just accessed
$connections->{$client_session_id}->{'resource'} = {
'module' => $module,
'action' => $action,
'handler' => $handler,
$globals->{'Connections'}->{$client_session_id}->{'Resource'} = {
'Module' => $module,
'Action' => $action,
'Handler' => $handler,
};
my $system = {
'logger' => $logger
};
# This is normal HTTP request
if ($protocol eq "HTTP") {
# Do the function call now
my ($res,$content,$extra) = $function->($kernel,$globals,$client_session_id,$request);
my ($res,$content,$extra) = $function->($kernel,$system,$client_session_id,$request);
# Module return undef if they don't want to handle the request
......@@ -549,7 +656,7 @@ sub _server_request_http
# The content in a redirect is the URL
} elsif ($res == HTTP_TEMPORARY_REDIRECT) {
$response = httpRedirect("//".$request->header('host')."/" . $content);
# Extra in this case is the error description
# Extra in this case is the error description
} else {
$response = httpDisplayFault($res,$content,$extra);
}
......@@ -557,29 +664,134 @@ sub _server_request_http
# Its a websocket upgrade request
} elsif ($protocol eq "HTTP=>WebSocket") {
# Do the function call now
my ($res,$ret1,$ret2) = $function->($kernel,$globals,$client_session_id,$request,$conn->{'socket'});
# Make sure we have an upgrade path to WebSocket
my ($newHandler) = _parse_http_resource($request,"WebSocket");
if (!defined($newHandler)) {
return httpDisplayFault(HTTP_INTERNAL_SERVER_ERROR,"Internal server error","Cannot upgrade to websocket");
}
# Do the function call now
my ($res,$ret1,$ret2) = $function->($kernel,$globals,$client_session_id,$request,$conn->{'Socket'});
# If we have a response defined, we rejected the upgrade
if (defined($res)) {
$response = httpDisplayFault($res,$ret1,$ret2);
} else {
# Return our upgrade response
$response = _server_request_http_wsupgrade($request,$module,$action);
# Upgrade the protocol
$connections->{$client_session_id}->{'protocol'} = 'WebSocket';
# Upgrade the protocol & handler
$globals->{'Connections'}->{$client_session_id}->{'Protocol'} = 'WebSocket';
$globals->{'Connections'}->{$client_session_id}->{'Resource'}->{'Handler'} = $newHandler;
}
}
END:
$logger->log(LOG_INFO,"[WEBSERVER] $protocol Request: ".$response->code." [$module/$action] - ".encode_entities($request->method)." ".
encode_entities($request->uri)." ".encode_entities($request->protocol));
$logger->log(LOG_INFO,"[WEBSERVER] %s Request: %s [%s/%s] - %s %s %s",
$protocol,
$response->code,
$module,
$action,
encode_entities($request->method),
encode_entities($request->uri),
encode_entities($request->protocol)
);
return $response;
}
# Handle the websocket request
sub _server_request_websocket
{
my ($kernel,$client_session_id,$rawRequest) = @_;
my $conn = $globals->{'Connections'}->{$client_session_id};
my $handler = $conn->{'Resource'}->{'Handler'};
# Function we need to call
my $function = $handler;
# Check if we're a hash... override if we are
if (ref($handler) eq "HASH") {
$function = $handler->{'on_request'};
}
# If its something else, blow up
if (ref($function) ne "CODE") {
$logger->log(LOG_ERR,"[WEBSERVER] No 'on_request' handler for websocket");
return encode_json({'status' => "error", 'message' => "Internal server error"});
}
# Safely decode JSON
my $request;
eval { $request = decode_json($rawRequest); };
if ($@) {
# FIXME: NOTICE: Failed to decode JSON request '&#3;&#xFFFD; , may be disconnect from websocket?
$logger->log(LOG_NOTICE,"[WEBSERVER] Failed to decode JSON request '%s'",encode_entities($rawRequest));
return encode_json({'status' => "error", 'message' => "Failed to decode JSON"});
}
# Save the command id
my $rCode = $request->{'id'};
# Check the function and args are present
if (!defined($request->{'function'}) || !defined($request->{'args'})) {
return encode_json({
'status' => "error",
'message' => "Invalid format for JSON request, must have 'function' and 'args' properties"
});
}
# Check function is a string
if (!(ref($request->{'function'}) eq "" && length($request->{'function'}) > 0)) {
return encode_json({
'status' => "error",
'message' => "The 'function' property must contain a function name"
});
}
# And the args is an array ref
if (!ref($request->{'args'}) eq "ARRAY") {
return encode_json({
'status' => "error",
'message' => "The 'args' property must contain an array of arguments"
});
}
# Do the function call now
my ($res,$content,$extra) = $function->($kernel,$globals,$client_session_id,$request,$conn->{'Socket'});
$logger->log(LOG_INFO,"[WEBSERVER] %s Request [%s/%s]",
$conn->{'Protocol'},
$conn->{'Resource'}->{'Module'},
$conn->{'Resource'}->{'Action'},
);
# Check if its a success or failure
my $ret;
if ($res == WS_OK) {
$ret->{'status'} = "success";
$ret->{'data'} = $content;
} elsif ($res == WS_ERROR) {
$ret->{'status'} = "error";
$ret->{'message'} = $content;
} elsif ($res == WS_FAIL) {
$ret->{'status'} = "fail";
$ret->{'message'} = $content;
} else {
$ret->{'status'} = "error";
$ret->{'message'} = "Internal server error";
}
# Add ID if we have one
if (defined($request->{'id'})) {
$ret->{'id'} = $request->{'id'};
}
return encode_json($ret);
}
# Function to parse a HTTP resource
sub _parse_http_resource
{
......@@ -598,8 +810,8 @@ sub _parse_http_resource
$dmodule = "index" if (!defined($dmodule) || $dmodule eq "");
$daction = "default" if (!defined($daction) || $daction eq "");
# Sanitize
(my $module = $dmodule) =~ s/[^A-Za-z0-9]//g;
(my $action = $daction) =~ s/[^A-Za-z0-9\-]//g;
(my $module = $dmodule) =~ s/[^A-Za-z0-9]//g;
(my $action = $daction) =~ s/[^A-Za-z0-9\-]//g;
# If module name is sneaky? then just block it
if ($module ne $dmodule) {
......@@ -629,9 +841,10 @@ sub _parse_http_resource
# If we have a list of requires, check them
if (defined($handler->{'requires'})) {
foreach my $require (@{$handler->{'requires'}}) {
if (!plugin_is_loaded($require)) {
return httpDisplayFault(HTTP_NOT_IMPLEMENTED,"Method Not Available","The requested method '$action' in '$module' is not currently available");
}
if (!isPluginLoaded($require)) {
return httpDisplayFault(HTTP_NOT_IMPLEMENTED,"Method Not Available","The requested method '$action' in ".
"'$module' is not currently available");
}
}
}
}
......@@ -640,6 +853,7 @@ sub _parse_http_resource
}
# Handle Websocket
sub _server_request_http_wsupgrade
{
......@@ -660,8 +874,14 @@ sub _server_request_http_wsupgrade
$headers
);
$logger->log(LOG_INFO,"[WEBSERVER] WebSocket Upgrade: ".$response->code." [$module/$action] - ".encode_entities($request->method)." ".
encode_entities($request->uri)." ".encode_entities($request->protocol));
$logger->log(LOG_INFO,"[WEBSERVER] WebSocket Upgrade: %s [%s/%s] - %s %s %s",
$response->code,
$module,
$action,
encode_entities($request->method),
encode_entities($request->uri),
encode_entities($request->protocol)
);
return $response;
}
......
# OpenTrafficShaper utility functions
# Copyright (C) 2007-2023, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
package opentrafficshaper::util;
use strict;
use warnings;
# Exporter stuff
require Exporter;
our (@ISA,@EXPORT,@EXPORT_OK);
@ISA = qw(Exporter);
@EXPORT = qw(
isIPv46
isIPv46CIDR
);
@EXPORT_OK = qw(
);
# Check if this is a valid IPv4 or IPv6 address
sub isIPv46
{
my ($rawIP) = @_;
my $ip = NetAddr::IP->new($rawIP);
if (!defined($ip)) {
return;
}
if ($ip->version == 4) {
if ($ip->masklen != 32) {
return;
}
return $ip->addr;
} elsif ($ip->version == 6) {
if ($ip->masklen != 128) {
return;
}
return $ip->short;
}
return;
}
# Check if this is a valid IPv4 or IPv6 CIDR
sub isIPv46CIDR
{
my ($rawIP) = @_;
my $ip = NetAddr::IP->new($rawIP);
if (!defined($ip)) {
return;
}
if ($ip->version() == 4) {
return $ip->network()->addr() . "/" . $ip->masklen();
} elsif ($ip->version == 6) {
return $ip->network()->short() . "/" . $ip->masklen();
}
return;
}
1;
\ No newline at end of file
# Utility functions
# Copyright (C) 2013, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
package opentrafficshaper::utils;
use strict;
use warnings;
# Exporter stuff
require Exporter;
our (@ISA,@EXPORT,@EXPORT_OK);
@ISA = qw(Exporter);
@EXPORT = qw(
prettyUndef
toHex
parseFormContent
isVariable
isUsername
isIP
isNumber
booleanize
);
@EXPORT_OK = qw(
);
# Print a undef in a pretty fashion
sub prettyUndef
{
my $var = shift;
if (!defined($var)) {
return "-undef-";
} else {
return $var;
}
}
# Return hex representation of a decimal
sub toHex
{
my $decimal = shift;
return sprintf('%x',$decimal);
}
# Parse form post data from HTTP content
sub parseFormContent
{
my $data = shift;
my %res;
# Split information into name/value pairs
my @pairs = split(/&/, $data);
foreach my $pair (@pairs)
{
my ($name, $value) = split(/=/, $pair);
$value =~ tr/+/ /;
$value =~ s/%(..)/pack("C", hex($1))/eg;
$res{$name} = $value;
}
return \%res;
}
# Check if variable is normal
sub isVariable
{
my $var = shift;
# A variable cannot be undef?
if (!defined($var)) {
return undef;
}
return (ref($var) eq "");
}
# Check if variable is a username
sub isUsername
{
my $var = shift;
# Make sure we're not a ref
if (!isVariable($var)) {
return undef;
}
# Lowercase it
$var = lc($var);
# Normal username
if ($var =~ /^[a-z0-9_\-\.]+$/) {
return $var;
}
# Username with domain
if ($var =~ /^[a-z0-9_\-\.]+\@[a-z0-9\-\.]+$/) {
return $var;
}
return undef;
}
# Check if variable is an IP
sub isIP
{
my $var = shift;
# Make sure we're not a ref
if (!isVariable($var)) {
return undef;
}
# Lowercase it
$var = lc($var);
# Normal IP
if ($var =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
return $var;
}
return undef;
}
# Check if variable is a number
sub isNumber
{
my $var = shift;
# Make sure we're not a ref
if (!isVariable($var)) {
return undef;
}
# Strip leading 0's
if ($var =~ /^0*([0-9]+)$/) {
my $val = int($1);
# Check we not 0 or negative
if ($val > 0) {
return $val;
}
# Check if we allow 0's
if ($val == 0) {
return $val;
}
}
return undef;
}
# Booleanize the variable depending on its contents
sub booleanize
{
my $var = shift;
# Check if we're defined
if (!isVariable($var)) {
return undef;
}
# If we're a number
if (my $val = isNumber($var)) {
if ($val == 0) {
return 0;
} else {
return 1;
}
}
# Nuke whitespaces
$var =~ s/\s//g;
# Allow true, on, set, enabled, 1
if ($var =~ /^(?:true|on|set|enabled|1|yes)$/i) {
return 1;
}
# Invalid or unknown
return 0;
}
1;
# vim: ts=4
# OpenTrafficShaper version package
# Copyright (C) 2007-2013, AllWorldIT
#
# Copyright (C) 2007-2023, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
......@@ -30,7 +30,7 @@ our (@ISA,@EXPORT,@EXPORT_OK);
use constant {
VERSION => "1.0.0",
VERSION => "2.0.4",
};
......
#!/usr/bin/perl
# Main OpenTrafficShaper program
# Copyright (C) 2007-2013, AllWorldIT
#
# Copyright (C) 2007-2023, AllWorldIT
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
......@@ -18,10 +18,10 @@
use strict;
use warnings;
use File::Basename qw(dirname);
# Set the dirs we look for library files in
use lib('/usr/local/lib/opentrafficshaper-1.0','/usr/lib/opentrafficshaper-1.0',
'/usr/lib64/opentrafficshaper-1.0','opentrafficshaper','awitpt', 'lib');
use lib(dirname(__FILE__),dirname(__FILE__)."/awitpt/lib");
# Enable assertions
BEGIN {
......@@ -76,7 +76,7 @@ exit;
# Function to display banner
sub displayBanner
{
$logger->log(LOG_NOTICE,"[MAIN] OpenTrafficShaper v".VERSION." - Copyright (c) 2007-2013, AllWorldIT");
$logger->log(LOG_NOTICE,"[MAIN] OpenTrafficShaper v%s - Copyright (c) 2007-2023, AllWorldIT",VERSION);
}
......@@ -89,7 +89,7 @@ sub parseCfgCmdLine
$cfg->{'config_file'} = "/etc/opentrafficshaper.conf";
$cfg->{'background'} = "yes";
$cfg->{'pid_file'} = "/var/run/opentrafficshaper/opentrafficshaperd.pid";
$cfg->{'pid_file'} = "/run/opentrafficshaper/opentrafficshaperd.pid";
$cfg->{'log_level'} = 2;
$cfg->{'log_file'} = "/var/log/opentrafficshaper/opentrafficshaperd.log";
......@@ -123,6 +123,7 @@ sub parseCfgCmdLine
-file => $cfg->{'config_file'},
-nocase => 1
) or die "Failed to open config file '".$cfg->{'config_file'}."': $!";
my $inifileHandle = tied( %inifile );
# Copy config
my %config = %inifile;
......@@ -155,8 +156,8 @@ sub parseCfgCmdLine
if (ref($config{'plugins'}{'load'}) eq "ARRAY") {
foreach my $plugin (@{$config{'plugins'}{'load'}}) {
$plugin =~ s/\s+//g;
# Skip comments
next if ($plugin =~ /^#/);
# Skip comments
next if ($plugin =~ /^#/);
push(@{$cfg->{'plugin_list'}},$plugin);
}
} elsif (defined($config{'plugins'}{'load'})) {
......@@ -168,6 +169,17 @@ sub parseCfgCmdLine
}
}
# We may have config file groups we want to remember for other plugins
foreach my $group ($inifileHandle->Groups()) {
# Loop with group members
foreach my $member ($inifileHandle->GroupMembers($group)) {
# Chop off group name and just get the member
my $cleanMember = substr($member,length($group)+1);
# Link the config...
$config{$group}->{$cleanMember} = $config{$member};
}
}
$globals->{'file.config'} = \%config;
$globals->{'config'} = $cfg;
}
......@@ -180,9 +192,9 @@ sub displayHelp {
print(STDERR<<EOF);
Usage: $0 [args]
--config=<file> Configuration file
--debug Put into debug mode
--fg Don't go into background
--config=<file> Configuration file
--debug Put into debug mode
--fg Don't go into background
EOF
}
......@@ -220,7 +232,7 @@ sub init
plugin_register($pluginName,0);
}
$logger->log(LOG_INFO,"[MAIN] Plugins initialized.");
$logger->log(LOG_INFO,"[MAIN] Plugins initialized");
}
# Function to start things up
......@@ -254,7 +266,7 @@ sub daemonize {
open STDOUT, '> /dev/null'
or die "Can't open stdout log: $!";
defined(my $pid = fork)
defined(my $pid = fork)
or die "Can't fork: $!";
exit if $pid;
......@@ -265,7 +277,7 @@ sub daemonize {
print(FH $$);
close(FH);
} else {
$logger->log(LOG_WARN,"[MAIN] Unable to write PID to '".$globals->{'config'}->{'pid_file'}."': $!");
$logger->log(LOG_WARN,"[MAIN] Unable to write PID to '%s': %s",$globals->{'config'}->{'pid_file'},$!);
}
}
......