diff --git a/install.sh b/install.sh
index 681c236228..6fbec3466f 100755
--- a/install.sh
+++ b/install.sh
@@ -1497,6 +1497,7 @@ src/sunstone/public/vendor/flot/jquery.flot.navigate.min.js \
src/sunstone/public/vendor/flot/jquery.flot.pie.min.js \
src/sunstone/public/vendor/flot/jquery.flot.resize.min.js \
src/sunstone/public/vendor/flot/jquery.flot.stack.min.js \
+src/sunstone/public/vendor/flot/jquery.flot.tooltip.min.js \
src/sunstone/public/vendor/flot/LICENSE.txt \
src/sunstone/public/vendor/flot/NOTICE"
diff --git a/src/sunstone/public/js/plugins/groups-tab.js b/src/sunstone/public/js/plugins/groups-tab.js
index 9c35733922..b35598601f 100644
--- a/src/sunstone/public/js/plugins/groups-tab.js
+++ b/src/sunstone/public/js/plugins/groups-tab.js
@@ -703,9 +703,16 @@ function updateGroupInfo(request,group){
'
};
+ var accounting_tab = {
+ title: tr("Accounting"),
+ icon: "fa-bar-chart-o",
+ content: '
'
+ };
+
Sunstone.updateInfoPanelTab("group_info_panel","group_info_tab",info_tab);
Sunstone.updateInfoPanelTab("group_info_panel","group_quotas_tab",quotas_tab);
Sunstone.updateInfoPanelTab("group_info_panel","group_providers_tab",providers_tab);
+ Sunstone.updateInfoPanelTab("group_info_panel","group_accouning_tab",accounting_tab);
Sunstone.popUpInfoPanel("group_info_panel", 'groups-tab');
$("#add_rp_button", $("#group_info_panel")).click(function(){
@@ -715,6 +722,11 @@ function updateGroupInfo(request,group){
return false;
});
+
+ accountingGraphs(
+ $("#group_accounting","#group_info_panel"),
+ { fixed_group: info.ID,
+ init_group_by: "user" });
}
function setup_group_resource_tab_content(zone_id, zone_section, str_zone_tab_id, str_datatable_id, selected_group_clusters, group) {
diff --git a/src/sunstone/public/js/plugins/users-tab.js b/src/sunstone/public/js/plugins/users-tab.js
index 98c98aeebe..f835e49dcb 100644
--- a/src/sunstone/public/js/plugins/users-tab.js
+++ b/src/sunstone/public/js/plugins/users-tab.js
@@ -715,10 +715,23 @@ function updateUserInfo(request,user){
content : quotas_html
};
+ var accounting_tab = {
+ title: tr("Accounting"),
+ icon: "fa-bar-chart-o",
+ content: ''
+ };
+
Sunstone.updateInfoPanelTab("user_info_panel","user_info_tab",info_tab);
Sunstone.updateInfoPanelTab("user_info_panel","user_quotas_tab",quotas_tab);
+ Sunstone.updateInfoPanelTab("user_info_panel","user_accouning_tab",accounting_tab);
//Sunstone.updateInfoPanelTab("user_info_panel","user_acct_tab",acct_tab);
Sunstone.popUpInfoPanel("user_info_panel", 'users-tab');
+
+ accountingGraphs(
+ $("#user_accounting","#user_info_panel"),
+ { fixed_user: info.ID,
+ init_group_by: "vm" });
+
};
// Used also from groups-tabs.js
diff --git a/src/sunstone/public/js/sunstone.js b/src/sunstone/public/js/sunstone.js
index 21790d112e..f3104c4575 100644
--- a/src/sunstone/public/js/sunstone.js
+++ b/src/sunstone/public/js/sunstone.js
@@ -3902,3 +3902,478 @@ $(document).ready(function(){
}
})
});
+
+// div is a jQuery selector
+// The following options can be set:
+// fixed_user fix an owner user ID
+// fixed_group fix an owner group ID
+// init_group_by "user", "group", "vm". init the group-by selector
+function accountingGraphs(div, opt){
+ div.append(
+ '\
+
\
+ '+tr("Time range")+'\
+
\
+
\
+ \
+
\
+
\
+ \
+
\
+
\
+ \
+ \
+
\
+ \
+ \
+
\
+
\
+ \
+ \
+
\
+ \
+ \
+ ');
+
+ if (opt == undefined){
+ opt = {};
+ }
+
+ //--------------------------------------------------------------------------
+ // Init start time to 1st of last month
+ //--------------------------------------------------------------------------
+ var d = new Date();
+
+ d.setDate(1);
+ d.setMonth(d.getMonth() - 1);
+
+ $("#acct_start_time", div).val(d.getFullYear() + '/' + (d.getMonth()+1) + '/' + d.getDate());
+
+ $("#acct_start_time", div).jdPicker();
+ $("#acct_end_time", div).jdPicker();
+
+ //--------------------------------------------------------------------------
+ // VM owner: all, group, user
+ //--------------------------------------------------------------------------
+
+ if (opt.fixed_user != undefined || opt.fixed_group != undefined){
+ $("input[name='acct_owner']", div).attr("disabled", "disabled");
+
+ $("#acct_owner_select", div).show();
+
+ if(opt.fixed_user != undefined){
+ var text = tr("User") +" " + opt.fixed_user;
+ $("input[value='acct_owner_user']", div).attr("checked", "checked");
+ } else {
+ var text = tr("Group") + " " + opt.fixed_group;
+ $("input[value='acct_owner_group']", div).attr("checked", "checked");
+ }
+
+ $("#acct_owner_select", div).text(text);
+ } else {
+ $("input[name='acct_owner']", div).change(function(){
+ var value = $(this).val();
+
+ switch (value){
+ case "acct_owner_all":
+ $("#acct_owner_select", div).hide();
+ break;
+
+ case "acct_owner_group":
+ $("#acct_owner_select", div).show();
+ insertSelectOptions("#acct_owner_select", div, "Group");
+ break;
+
+ case "acct_owner_user":
+ $("#acct_owner_select", div).show();
+ insertSelectOptions("#acct_owner_select", div, "User", -1, false,
+ '');
+ break;
+ }
+ });
+ }
+
+ //--------------------------------------------------------------------------
+ // Init group by select
+ //--------------------------------------------------------------------------
+
+ if(opt.init_group_by != undefined){
+ $("#acct_group_by", div).val(opt.init_group_by);
+ }
+
+ //--------------------------------------------------------------------------
+ // Submit request
+ //--------------------------------------------------------------------------
+
+ // TODO: make start_time mandatory
+
+ $("#acct_submit", div).on("click", function(){
+ var start_time = -1;
+ var end_time = -1;
+
+ var v = $("#acct_start_time", div).val();
+ if (v != ""){
+ start_time = new Date(v).getTime() / 1000;
+ }
+
+ var v = $("#acct_end_time", div).val();
+ if (v != ""){
+ end_time = new Date(v).getTime() / 1000;
+ }
+
+ var options = {
+ "start_time": start_time,
+ "end_time": end_time
+ };
+
+ if (opt.fixed_user != undefined){
+ options.userfilter = opt.fixed_user;
+ } else if (opt.fixed_group != undefined){
+ options.group = opt.fixed_group;
+ } else {
+ var select_val = $("#acct_owner_select .resource_list_select", div).val();
+
+ switch ($("input[name='acct_owner']:checked", div).val()){
+ case "acct_owner_all":
+ break;
+
+ case "acct_owner_group":
+ if(select_val != ""){
+ options.group = select_val;
+ }
+ break;
+
+ case "acct_owner_user":
+ if(select_val != ""){
+ options.userfilter = select_val;
+ }
+ break;
+ }
+ }
+
+ OpenNebula.VM.accounting({
+ // timeout: true,
+ success: function(req, response){
+ fillAccounting(div, req, response);
+ },
+ error: onError,
+ data: options
+ });
+ });
+}
+
+function fillAccounting(div, req, response) {
+/*
+ console.log(req);
+ console.log(response);
+*/
+
+
+/*
+TODO More options:
+
+Granularity: month, day, hour
+Time range
+Group by: user, group, vm, [flow]
+Filter by: user, group, vm, [flow]
+
+Download csv
+*/
+
+ var options = req.request.data[0];
+
+ //--------------------------------------------------------------------------
+ // Time slots
+ //--------------------------------------------------------------------------
+
+ // start_time is mandatory
+ var start = new Date(options.start_time * 1000);
+
+ var end = new Date();
+
+ if(options.end_time != undefined && options.end_time != -1){
+ var end = new Date(options.end_time * 1000)
+ }
+
+ // granularity of 1 day
+ var times = [];
+
+ var tmp_time = start;
+
+ // End time is the start of the last time slot. We use <=, to
+ // add one extra time step
+ while (tmp_time <= end) {
+ times.push(tmp_time.getTime());
+
+ // day += 1
+ tmp_time.setDate( tmp_time.getDate() + 1 );
+ }
+
+ //--------------------------------------------------------------------------
+ // Flot options
+ //--------------------------------------------------------------------------
+
+ var options = {
+// colors: [ "#2ba6cb", "#707D85", "#AC5A62" ],
+
+ xaxis : {
+ mode: "time",
+ timeformat: "%y/%m/%d",
+ min: times[0],
+ max: end.getTime(),
+ color: "#999",
+ size: 8
+ },
+ yaxis : { labelWidth: 50,
+ min: 0,
+ color: "#999",
+ size: 8
+ },
+ series: {
+ bars: {
+ show: true,
+ lineWidth: 1,
+ fill: true,
+ barWidth: 24*60*60*1000 * 0.8,
+ align: "center"
+ },
+ stack: true
+ },
+ legend : {
+ show : true,
+ noColumns: 6,
+ container: $("#acct_legend", div)
+ },
+ grid: {
+ borderWidth: 1,
+ borderColor: "#cfcfcf",
+ hoverable: true
+ },
+ tooltip: true,
+ tooltipOpts: {
+ content: "%x | %s | %y" //"%s | X: %x | Y: %y"
+/*
+ xDateFormat: string //null
+ yDateFormat: string //null
+ monthNames: string // null
+ dayNames: string // null
+ shifts: {
+ x: int //10
+ y: int //20
+ },
+ defaultTheme: boolean //true
+ onHover: function(flotItem, $tooltipEl)
+*/
+ }
+ };
+
+ //--------------------------------------------------------------------------
+ // Group by
+ //--------------------------------------------------------------------------
+
+ // TODO: Allow to change group by dynamically, instead of calling oned again
+
+ switch ($("#acct_group_by", div).val()){
+ case "user":
+ var group_by_fn = function(history){
+ return history.VM.UID;
+ }
+
+ var group_by_prefix = tr("User")+" ";
+
+ break;
+
+ case "group":
+ var group_by_fn = function(history){
+ return history.VM.GID;
+ }
+
+ var group_by_prefix = tr("Group")+" ";
+
+ break;
+
+ case "vm":
+ var group_by_fn = function(history){
+ return history.OID;
+ }
+
+ var group_by_prefix = tr("VM")+" ";
+
+ break;
+ }
+
+ //--------------------------------------------------------------------------
+ // Filter history entries
+ //--------------------------------------------------------------------------
+
+ // TODO filter
+ // True to proccess, false to discard
+ var filter_by_fn = function(history){
+// return history.OID == 3605 || history.OID == 2673;
+ return true;
+ }
+
+ //--------------------------------------------------------------------------
+ // Process data series for flot
+ //--------------------------------------------------------------------------
+
+ var series = {};
+ series.CPU_HOURS = {};
+ series.MEM_HOURS = {};
+
+ // TODO: response can be an empty object
+
+ $.each(response.HISTORY_RECORDS.HISTORY, function(index, history){
+
+ if(!filter_by_fn(history)){
+ return true; //continue
+ }
+
+ var group_by = group_by_fn(history);
+
+ if (series.CPU_HOURS[group_by] == undefined){
+ series.CPU_HOURS[group_by] = {};
+ }
+
+ if (series.MEM_HOURS[group_by] == undefined){
+ series.MEM_HOURS[group_by] = {};
+ }
+
+// TODO Optimize getting here?
+// var serie = series.CPU_HOURS[history.VM.UID];
+
+ for (var i = 0; i t || history.ETIME == 0) &&
+ (history.STIME != 0 && history.STIME*1000 <= t_next) ) {
+
+ var stime = t;
+ if(history.STIME != 0){
+ stime = Math.max(t, history.STIME*1000);
+ }
+
+ var etime = t_next;
+ if(history.ETIME != 0){
+ etime = Math.min(t_next, history.ETIME*1000);
+ }
+
+ var n_hours = (etime - stime) / 1000 / 60 / 60;
+
+ // --- cpu ---
+
+ // TODO cpu may not exist
+ var val = parseFloat(history.VM.TEMPLATE.CPU) * n_hours;
+
+ var serie = series.CPU_HOURS[group_by];
+
+ if(serie[t] == undefined){
+ serie[t] = 0;
+ }
+
+ serie[t] += val;
+
+ // --- mem ---
+
+ // TODO memory may not exist. In GB
+ var val = parseInt(history.VM.TEMPLATE.MEMORY)/1024 * n_hours;
+
+ var serie = series.MEM_HOURS[group_by];
+
+ if(serie[t] == undefined){
+ serie[t] = 0;
+ }
+
+ serie[t] += val;
+ }
+ }
+ });
+
+// console.log(series);
+
+ //--------------------------------------------------------------------------
+ // Create series, draw plots
+ //--------------------------------------------------------------------------
+
+ // --- cpu ---
+
+ var plot_series = [];
+
+ $.each(series.CPU_HOURS, function(key, val){
+ var data = [];
+
+ $.each(val, function(time,num){
+ data.push([parseInt(time),num]);
+ });
+
+ plot_series.push(
+ {
+ label: group_by_prefix+key,
+ data: data
+ });
+ });
+
+ $.plot($("#acct_cpu_graph", div), plot_series, options);
+
+ // --- mem ---
+
+ var plot_series = [];
+
+ $.each(series.MEM_HOURS, function(key, val){
+ var data = [];
+
+ $.each(val, function(time,num){
+ data.push([parseInt(time),num]);
+ });
+
+ plot_series.push(
+ {
+ label: group_by_prefix+key,
+ data: data
+ });
+ });
+
+ $.plot($("#acct_mem_graph", div), plot_series, options);
+}
\ No newline at end of file
diff --git a/src/sunstone/public/vendor/flot/jquery.flot.tooltip.min.js b/src/sunstone/public/vendor/flot/jquery.flot.tooltip.min.js
new file mode 100644
index 0000000000..8fdce4e4cb
--- /dev/null
+++ b/src/sunstone/public/vendor/flot/jquery.flot.tooltip.min.js
@@ -0,0 +1,12 @@
+/*
+ * jquery.flot.tooltip
+ *
+ * description: easy-to-use tooltips for Flot charts
+ * version: 0.6.7
+ * author: Krzysztof Urbas @krzysu [myviews.pl]
+ * website: https://github.com/krzysu/flot.tooltip
+ *
+ * build on 2014-03-26
+ * released under MIT License, 2012
+*/
+Array.prototype.indexOf||(Array.prototype.indexOf=function(t,i){if(void 0===this||null===this)throw new TypeError('"this" is null or not defined');var e=this.length>>>0;for(i=+i||0,1/0===Math.abs(i)&&(i=0),0>i&&(i+=e,0>i&&(i=0));e>i;i++)if(this[i]===t)return i;return-1}),function(t){var i={tooltip:!1,tooltipOpts:{content:"%s | X: %x | Y: %y",xDateFormat:null,yDateFormat:null,monthNames:null,dayNames:null,shifts:{x:10,y:20},defaultTheme:!0,onHover:function(){}}},e=function(t){this.tipPosition={x:0,y:0},this.init(t)};e.prototype.init=function(i){function e(t){var i={};i.x=t.pageX,i.y=t.pageY,s.updateTooltipPosition(i)}function o(t,i,e){var o=s.getDomElement();if(e){var n;n=s.stringFormat(s.tooltipOptions.content,e),o.html(n),s.updateTooltipPosition({x:i.pageX,y:i.pageY}),o.css({left:s.tipPosition.x+s.tooltipOptions.shifts.x,top:s.tipPosition.y+s.tooltipOptions.shifts.y}).show(),"function"==typeof s.tooltipOptions.onHover&&s.tooltipOptions.onHover(e,o)}else o.hide().html("")}var s=this,n=t.plot.plugins.length;if(this.plotPlugins=[],n)for(var r=0;n>r;r++)this.plotPlugins.push(t.plot.plugins[r].name);i.hooks.bindEvents.push(function(i,n){s.plotOptions=i.getOptions(),s.plotOptions.tooltip!==!1&&void 0!==s.plotOptions.tooltip&&(s.tooltipOptions=s.plotOptions.tooltipOpts,s.getDomElement(),t(i.getPlaceholder()).bind("plothover",o),t(n).bind("mousemove",e))}),i.hooks.shutdown.push(function(i,s){t(i.getPlaceholder()).unbind("plothover",o),t(s).unbind("mousemove",e)})},e.prototype.getDomElement=function(){var i;return t("#flotTip").length>0?i=t("#flotTip"):(i=t("").attr("id","flotTip"),i.appendTo("body").hide().css({position:"absolute"}),this.tooltipOptions.defaultTheme&&i.css({background:"#fff","z-index":"1040",padding:"0.4em 0.6em","border-radius":"0.5em","font-size":"0.8em",border:"1px solid #111",display:"none","white-space":"nowrap"})),i},e.prototype.updateTooltipPosition=function(i){var e=t("#flotTip").outerWidth()+this.tooltipOptions.shifts.x,o=t("#flotTip").outerHeight()+this.tooltipOptions.shifts.y;i.x-t(window).scrollLeft()>t(window).innerWidth()-e&&(i.x-=e),i.y-t(window).scrollTop()>t(window).innerHeight()-o&&(i.y-=o),this.tipPosition.x=i.x,this.tipPosition.y=i.y},e.prototype.stringFormat=function(t,i){var e,o,s=/%p\.{0,1}(\d{0,})/,n=/%s/,r=/%lx/,a=/%ly/,p=/%x\.{0,1}(\d{0,})/,l=/%y\.{0,1}(\d{0,})/,d="%x",h="%y";if(i.series.threshold!==void 0?(e=i.datapoint[0],o=i.datapoint[1]):(e=i.series.data[i.dataIndex][0],o=i.series.data[i.dataIndex][1]),null===i.series.label&&i.series.originSeries&&(i.series.label=i.series.originSeries.label),"function"==typeof t&&(t=t(i.series.label,e,o,i)),i.series.percent!==void 0&&(t=this.adjustValPrecision(s,t,i.series.percent)),t=i.series.label!==void 0?t.replace(n,i.series.label):t.replace(n,""),t=this.hasAxisLabel("xaxis",i)?t.replace(r,i.series.xaxis.options.axisLabel):t.replace(r,""),t=this.hasAxisLabel("yaxis",i)?t.replace(a,i.series.yaxis.options.axisLabel):t.replace(a,""),this.isTimeMode("xaxis",i)&&this.isXDateFormat(i)&&(t=t.replace(p,this.timestampToDate(e,this.tooltipOptions.xDateFormat))),this.isTimeMode("yaxis",i)&&this.isYDateFormat(i)&&(t=t.replace(l,this.timestampToDate(o,this.tooltipOptions.yDateFormat))),"number"==typeof e&&(t=this.adjustValPrecision(p,t,e)),"number"==typeof o&&(t=this.adjustValPrecision(l,t,o)),i.series.xaxis.ticks!==void 0){var u;u=this.hasRotatedXAxisTicks(i)?"rotatedTicks":"ticks";var x=i.dataIndex+i.seriesIndex;i.series.xaxis[u].length>x&&!this.isTimeMode("xaxis",i)&&(t=t.replace(p,i.series.xaxis[u][x].label))}if(i.series.yaxis.ticks!==void 0)for(var c in i.series.yaxis.ticks)if(i.series.yaxis.ticks.hasOwnProperty(c)){var m=this.isCategoriesMode("yaxis",i)?i.series.yaxis.ticks[c].label:i.series.yaxis.ticks[c].v;m===o&&(t=t.replace(l,i.series.yaxis.ticks[c].label))}return i.series.xaxis.tickFormatter!==void 0&&(t=t.replace(d,i.series.xaxis.tickFormatter(e,i.series.xaxis).replace(/\$/g,"$$"))),i.series.yaxis.tickFormatter!==void 0&&(t=t.replace(h,i.series.yaxis.tickFormatter(o,i.series.yaxis).replace(/\$/g,"$$"))),t},e.prototype.isTimeMode=function(t,i){return i.series[t].options.mode!==void 0&&"time"===i.series[t].options.mode},e.prototype.isXDateFormat=function(){return this.tooltipOptions.xDateFormat!==void 0&&null!==this.tooltipOptions.xDateFormat},e.prototype.isYDateFormat=function(){return this.tooltipOptions.yDateFormat!==void 0&&null!==this.tooltipOptions.yDateFormat},e.prototype.isCategoriesMode=function(t,i){return i.series[t].options.mode!==void 0&&"categories"===i.series[t].options.mode},e.prototype.timestampToDate=function(i,e){var o=new Date(1*i);return t.plot.formatDate(o,e,this.tooltipOptions.monthNames,this.tooltipOptions.dayNames)},e.prototype.adjustValPrecision=function(t,i,e){var o,s=i.match(t);return null!==s&&""!==RegExp.$1&&(o=RegExp.$1,e=e.toFixed(o),i=i.replace(t,e)),i},e.prototype.hasAxisLabel=function(t,i){return-1!==this.plotPlugins.indexOf("axisLabels")&&i.series[t].options.axisLabel!==void 0&&i.series[t].options.axisLabel.length>0},e.prototype.hasRotatedXAxisTicks=function(i){return 1===t.grep(t.plot.plugins,function(t){return"tickRotor"===t.name}).length&&i.series.xaxis.rotatedTicks!==void 0};var o=function(t){new e(t)};t.plot.plugins.push({init:o,options:i,name:"tooltip",version:"0.6.7"})}(jQuery);
\ No newline at end of file
diff --git a/src/sunstone/views/index.erb b/src/sunstone/views/index.erb
index 315026cf98..6f084a53bf 100644
--- a/src/sunstone/views/index.erb
+++ b/src/sunstone/views/index.erb
@@ -13,6 +13,7 @@
+