diff --git a/app/models/chargeback.rb b/app/models/chargeback.rb index 8405b6762ad..d009b98677e 100644 --- a/app/models/chargeback.rb +++ b/app/models/chargeback.rb @@ -46,6 +46,9 @@ def self.report_row_key(consumption) "#{classification_id}_#{ts_key}" elsif @options[:groupby_label].present? "#{groupby_label_value(consumption, @options[:groupby_label])}_#{ts_key}" + elsif @options.group_by_tenant? + tenant = @options.tenant_for(consumption) + "#{tenant ? tenant.id : 'none'}_#{ts_key}" else default_key(consumption, ts_key) end @@ -75,6 +78,7 @@ def initialize(options, consumption) self.interval_name = options.interval self.chargeback_rates = '' self.entity ||= consumption.resource + self.tenant_name = consumption.resource.try(:tenant).try(:name) if options.group_by_tenant? end def showback_category @@ -164,6 +168,7 @@ def self.set_chargeback_report_options(rpt, group_by, header_for_tag, groupby_la static_cols = group_by == "project" ? report_static_cols - ["image_name"] : report_static_cols static_cols = group_by == "tag" ? [report_tag_field] : static_cols static_cols = group_by == "label" ? [report_label_field] : static_cols + static_cols = group_by == "tenant" ? ['tenant_name'] : static_cols rpt.cols += static_cols rpt.col_order = static_cols + ["display_range"] rpt.sortby = static_cols + ["start_date"] diff --git a/app/models/chargeback/consumption_history.rb b/app/models/chargeback/consumption_history.rb index ddc0ce9ac23..e7f3f19334d 100644 --- a/app/models/chargeback/consumption_history.rb +++ b/app/models/chargeback/consumption_history.rb @@ -22,7 +22,7 @@ def self.for_report(cb_class, options) # we are building hash with grouped calculated values # values are grouped by resource_id and timestamp (query_start_time...query_end_time) - records.group_by(&:resource_id).each do |_, metric_rollup_records| + options.group_with(records).each_value do |metric_rollup_records| consumption = ConsumptionWithRollups.new(metric_rollup_records, query_start_time, query_end_time) yield(consumption) unless consumption.consumed_hours_in_interval.zero? end diff --git a/app/models/chargeback/consumption_with_rollups.rb b/app/models/chargeback/consumption_with_rollups.rb index 66e7b7fa1c0..71d61993657 100644 --- a/app/models/chargeback/consumption_with_rollups.rb +++ b/app/models/chargeback/consumption_with_rollups.rb @@ -41,6 +41,13 @@ def max(metric, sub_metric = nil) values.present? ? values.max : 0 end + def sum_of_maxes_from_grouped_values(metric, sub_metric = nil) + return max(metric, sub_metric) if sub_metric + @grouped_values ||= {} + grouped_rollups = @rollups.group_by { |x| x.resource.id } + @grouped_values[metric] ||= grouped_rollups.map { |_, rollups| rollups.collect(&metric.to_sym).compact.max }.compact.sum + end + def avg(metric, sub_metric = nil) metric_sum = values(metric, sub_metric).sum metric_sum / consumed_hours_in_interval diff --git a/app/models/chargeback/consumption_without_rollups.rb b/app/models/chargeback/consumption_without_rollups.rb index 786f13fcebc..9b8882060ea 100644 --- a/app/models/chargeback/consumption_without_rollups.rb +++ b/app/models/chargeback/consumption_without_rollups.rb @@ -60,6 +60,7 @@ def current_value(metric, _sub_metric = nil) end alias avg current_value alias max current_value + alias sum_of_maxes_from_grouped_values current_value private :current_value end end diff --git a/app/models/chargeback/report_options.rb b/app/models/chargeback/report_options.rb index f1964b7d520..35146dc0fb9 100644 --- a/app/models/chargeback/report_options.rb +++ b/app/models/chargeback/report_options.rb @@ -17,7 +17,8 @@ class Chargeback :userid, :ext_options, :include_metrics, # enable charging allocated resources with C & U - :method_for_allocated_metrics + :method_for_allocated_metrics, + :group_by_tenant? ) do def self.new_from_h(hash) new(*hash.values_at(*members)) @@ -32,6 +33,7 @@ def method_for_allocated_metrics raise "Invalid method for allocated calculations #{method}" end + return :sum_of_maxes_from_grouped_values if method == :max && group_by_tenant? method end @@ -115,12 +117,24 @@ def duration_of_report_step end end + def tenant_for(consumption) + consumption.resource.tenant + end + + def group_with(records) + group_by_tenant? ? records.group_by { |x| x.resource.tenant.id } : records.group_by(&:resource_id) + end + def classification_for(consumption) tag = consumption.tag_names.find { |x| x.starts_with?(groupby_tag) } # 'department/*' tag = tag.split('/').second unless tag.blank? # 'department/finance' -> 'finance' tag_hash[tag] end + def group_by_tenant? + self[:groupby] == 'tenant' + end + private def tag_hash diff --git a/app/models/chargeback_vm.rb b/app/models/chargeback_vm.rb index d1220c34645..fd789972dfb 100644 --- a/app/models/chargeback_vm.rb +++ b/app/models/chargeback_vm.rb @@ -6,6 +6,7 @@ class ChargebackVm < Chargeback :vm_guid => :string, :owner_name => :string, :provider_name => :string, + :tenant_name => :string, :provider_uid => :string, :cpu_allocated_metric => :float, :cpu_allocated_cost => :float, diff --git a/spec/models/chargeback_vm_spec.rb b/spec/models/chargeback_vm_spec.rb index 660a48ec7ce..34eb82044c3 100644 --- a/spec/models/chargeback_vm_spec.rb +++ b/spec/models/chargeback_vm_spec.rb @@ -369,6 +369,196 @@ end end + context 'monthly report, group by tenants' do + let(:options) do + { + :interval => "monthly", + :interval_size => 12, + :end_interval_offset => 1, + :tenant_id => tenant_1.id, + :method_for_allocated_metrics => :max, + :include_metrics => true, + :groupby => "tenant", + } + end + + let(:monthly_used_rate) { hourly_rate * hours_in_month } + let(:monthly_allocated_rate) { count_hourly_rate * hours_in_month } + + # My Company + # \___Tenant 2 + # \___Tenant 3 + # \__Tenant 4 + # \__Tenant 5 + # + let(:tenant_1) { Tenant.root_tenant } + let(:vm_1_1) { FactoryGirl.create(:vm_vmware, :tenant => tenant_1, :miq_group => nil) } + let(:vm_2_1) { FactoryGirl.create(:vm_vmware, :tenant => tenant_1, :miq_group => nil) } + + let(:tenant_2) { FactoryGirl.create(:tenant, :name => 'Tenant 2', :parent => tenant_1) } + let(:vm_1_2) { FactoryGirl.create(:vm_vmware, :tenant => tenant_2, :miq_group => nil) } + let(:vm_2_2) { FactoryGirl.create(:vm_vmware, :tenant => tenant_2, :miq_group => nil) } + + let(:tenant_3) { FactoryGirl.create(:tenant, :name => 'Tenant 3', :parent => tenant_1) } + let(:vm_1_3) { FactoryGirl.create(:vm_vmware, :tenant => tenant_3, :miq_group => nil) } + let(:vm_2_3) { FactoryGirl.create(:vm_vmware, :tenant => tenant_3, :miq_group => nil) } + + let(:tenant_4) { FactoryGirl.create(:tenant, :name => 'Tenant 4', :divisible => false, :parent => tenant_3) } + let(:vm_1_4) { FactoryGirl.create(:vm_vmware, :tenant => tenant_4, :miq_group => nil) } + let(:vm_2_4) { FactoryGirl.create(:vm_vmware, :tenant => tenant_4, :miq_group => nil) } + + let(:tenant_5) { FactoryGirl.create(:tenant, :name => 'Tenant 5', :divisible => false, :parent => tenant_3) } + let(:vm_1_5) { FactoryGirl.create(:vm_vmware, :tenant => tenant_5, :miq_group => nil) } + let(:vm_2_5) { FactoryGirl.create(:vm_vmware, :tenant => tenant_5, :miq_group => nil) } + + subject { ChargebackVm.build_results_for_report_ChargebackVm(options).first } + + let(:derived_vm_numvcpus_tenant_5) { 1 } + let(:cpu_usagemhz_rate_average_tenant_5) { 50 } + + before(:each) do + add_metric_rollups_for([vm_1_1, vm_2_1], month_beginning...month_end, 8.hours, metric_rollup_params.merge!(:derived_vm_numvcpus => 1, :cpu_usagemhz_rate_average => 50)) + add_metric_rollups_for([vm_1_2, vm_2_2], month_beginning...month_end, 8.hours, metric_rollup_params.merge!(:derived_vm_numvcpus => 1, :cpu_usagemhz_rate_average => 50)) + add_metric_rollups_for([vm_1_3, vm_2_3], month_beginning...month_end, 8.hours, metric_rollup_params.merge!(:derived_vm_numvcpus => 1, :cpu_usagemhz_rate_average => 50)) + add_metric_rollups_for([vm_1_4, vm_2_4], month_beginning...month_end, 8.hours, metric_rollup_params.merge!(:derived_vm_numvcpus => 1, :cpu_usagemhz_rate_average => 50)) + add_metric_rollups_for([vm_1_5, vm_2_5], month_beginning...month_end, 8.hours, metric_rollup_params.merge!(:derived_vm_numvcpus => derived_vm_numvcpus_tenant_5, :cpu_usagemhz_rate_average => cpu_usagemhz_rate_average_tenant_5)) + end + + it 'reports each tenants' do + expect(subject.map(&:tenant_name)).to match_array([tenant_1, tenant_2, tenant_3, tenant_4, tenant_5].map(&:name)) + end + + def subject_row_for_tenant(tenant) + subject.detect { |x| x.tenant_name == tenant.name } + end + + let(:hourly_usage) { 30 * 3.0 / 720 } # count of metric rollups / hours in month + + it 'calculates allocated,used metric with using max,avg method with vcpus=1.0 and 50% usage' do + # sum of maxes from each VM: + # (max from first tenant_1's VM + max from second tenant_1's VM) * monthly_allocated_rate + expect(subject_row_for_tenant(tenant_1).cpu_allocated_metric).to eq(1 + 1) + expect(subject_row_for_tenant(tenant_1).cpu_allocated_cost).to eq((1 + 1) * monthly_allocated_rate) + + expect(subject_row_for_tenant(tenant_2).cpu_allocated_metric).to eq(1 + 1) + expect(subject_row_for_tenant(tenant_2).cpu_allocated_cost).to eq((1 + 1) * monthly_allocated_rate) + + expect(subject_row_for_tenant(tenant_3).cpu_allocated_metric).to eq(1 + 1) + expect(subject_row_for_tenant(tenant_3).cpu_allocated_cost).to eq((1 + 1) * monthly_allocated_rate) + + expect(subject_row_for_tenant(tenant_4).cpu_allocated_metric).to eq(1 + 1) + expect(subject_row_for_tenant(tenant_4).cpu_allocated_cost).to eq((1 + 1) * monthly_allocated_rate) + + expect(subject_row_for_tenant(tenant_5).cpu_allocated_metric).to eq(1 + 1) + expect(subject_row_for_tenant(tenant_5).cpu_allocated_cost).to eq((1 + 1) * monthly_allocated_rate) + + # each tenant has 2 VMs and each VM has 50 of cpu usage: + # 5 tenants(tenant_1 has 4 tenants and plus tenant_1 ) * 2 VMs * 50% of usage + expect(subject_row_for_tenant(tenant_1).cpu_used_metric).to eq(2 * 50 * hourly_usage) + # and cost - there is multiplication by monthly_used_rate + expect(subject_row_for_tenant(tenant_1).cpu_used_cost).to eq(2 * 50 * hourly_usage * monthly_used_rate) + + expect(subject_row_for_tenant(tenant_2).cpu_used_metric).to eq(2 * 50 * hourly_usage) + expect(subject_row_for_tenant(tenant_2).cpu_used_cost).to eq(2 * 50 * hourly_usage * monthly_used_rate) + + expect(subject_row_for_tenant(tenant_3).cpu_used_metric).to eq(2 * 50 * hourly_usage) + expect(subject_row_for_tenant(tenant_3).cpu_used_cost).to eq(2 * 50 * hourly_usage * monthly_used_rate) + + expect(subject_row_for_tenant(tenant_4).cpu_used_metric).to eq(2 * 50 * hourly_usage) + expect(subject_row_for_tenant(tenant_4).cpu_used_cost).to eq(2 * 50 * hourly_usage * monthly_used_rate) + + expect(subject_row_for_tenant(tenant_5).cpu_used_metric).to eq(2 * 50 * hourly_usage) + expect(subject_row_for_tenant(tenant_5).cpu_used_cost).to eq(2 * 50 * hourly_usage * monthly_used_rate) + end + + context 'vcpu=5 for VMs of tenant_5' do + let(:derived_vm_numvcpus_tenant_5) { 5 } + let(:cpu_usagemhz_rate_average_tenant_5) { 75 } + + it 'calculates allocated,used metric with using max,avg method with vcpus=1.0 and 50% usage' do + expect(subject_row_for_tenant(tenant_1).cpu_allocated_metric).to eq(1 + 1) + expect(subject_row_for_tenant(tenant_1).cpu_allocated_cost).to eq((1 + 1) * monthly_allocated_rate) + + expect(subject_row_for_tenant(tenant_2).cpu_allocated_metric).to eq(1 + 1) + expect(subject_row_for_tenant(tenant_2).cpu_allocated_cost).to eq((1 + 1) * monthly_allocated_rate) + + expect(subject_row_for_tenant(tenant_3).cpu_allocated_metric).to eq(1 + 1) + expect(subject_row_for_tenant(tenant_3).cpu_allocated_cost).to eq((1 + 1) * monthly_allocated_rate) + + expect(subject_row_for_tenant(tenant_4).cpu_allocated_metric).to eq(1 + 1) + expect(subject_row_for_tenant(tenant_4).cpu_allocated_cost).to eq((1 + 1) * monthly_allocated_rate) + + expect(subject_row_for_tenant(tenant_5).cpu_allocated_metric).to eq(5 + 5) + expect(subject_row_for_tenant(tenant_5).cpu_allocated_cost).to eq((5 + 5) * monthly_allocated_rate) + + # each tenant has 2 VMs and each VM has 50 of cpu usage: + # 5 tenants(tenant_1 has 4 tenants and plus tenant_1 ) * 2 VMs * 50% of usage + # but tenant_5 has 2 VMs and each VM has 75 of cpu usage + expect(subject_row_for_tenant(tenant_1).cpu_used_metric).to eq(hourly_usage * 2 * 50) + # and cost - there is multiplication by monthly_used_rate + expect(subject_row_for_tenant(tenant_1).cpu_used_cost).to eq(hourly_usage * 2 * 50 * monthly_used_rate) + + expect(subject_row_for_tenant(tenant_2).cpu_used_metric).to eq(hourly_usage * 2 * 50) + expect(subject_row_for_tenant(tenant_2).cpu_used_cost).to eq(hourly_usage * 2 * 50 * monthly_used_rate) + + expect(subject_row_for_tenant(tenant_3).cpu_used_metric).to eq(hourly_usage * 2 * 50) + expect(subject_row_for_tenant(tenant_3).cpu_used_cost).to eq(hourly_usage * 2 * 50 * monthly_used_rate) + + expect(subject_row_for_tenant(tenant_4).cpu_used_metric).to eq(hourly_usage * 2 * 50) + expect(subject_row_for_tenant(tenant_4).cpu_used_cost).to eq(hourly_usage * 2 * 50 * monthly_used_rate) + + expect(subject_row_for_tenant(tenant_5).cpu_used_metric).to eq(hourly_usage * 2 * 75) + expect(subject_row_for_tenant(tenant_5).cpu_used_cost).to eq(hourly_usage * 2 * 75 * monthly_used_rate) + end + + context 'test against group by vm report' do + let(:options_group_vm) do + { + :interval => "monthly", + :interval_size => 12, + :end_interval_offset => 1, + :tenant_id => tenant_1.id, + :method_for_allocated_metrics => :max, + :include_metrics => true, + :groupby => "vm" + } + end + + def result_row_for_vm(vm) + result_group_by_vm.detect { |x| x.vm_name == vm.name } + end + + let(:result_group_by_vm) { ChargebackVm.build_results_for_report_ChargebackVm(options_group_vm).first } + + it 'calculates used metric and cost same as report for each vm' do + # Tenant 1 VMs + all_vms_cpu_metric = [vm_1_1, vm_2_1].map { |vm| result_row_for_vm(vm).cpu_used_metric }.sum + all_vms_cpu_cost = [vm_1_1, vm_2_1].map { |vm| result_row_for_vm(vm).cpu_used_cost }.sum + + # Tenant 1 + expect(subject_row_for_tenant(tenant_1).cpu_used_metric).to eq(all_vms_cpu_metric) + expect(subject_row_for_tenant(tenant_1).cpu_used_cost).to eq(all_vms_cpu_cost) + + # Tenant 5 Vms + result_vm15 = result_row_for_vm(vm_1_5) + result_vm25 = result_row_for_vm(vm_2_5) + + expect(subject_row_for_tenant(tenant_5).cpu_used_metric).to eq(result_vm15.cpu_used_metric + result_vm25.cpu_used_metric) + expect(subject_row_for_tenant(tenant_5).cpu_used_cost).to eq(result_vm15.cpu_used_cost + result_vm25.cpu_used_cost) + end + + it 'calculated allocted metric and cost with using max(max is not summed up - it is taken maximum)' do + # Tenant 1 VMs + all_vms_cpu_metric = [vm_1_1, vm_2_1].map { |vm| result_row_for_vm(vm).cpu_allocated_metric }.sum + all_vms_cpu_cost = [vm_1_1, vm_2_1].map { |vm| result_row_for_vm(vm).cpu_allocated_cost }.sum + + expect(subject_row_for_tenant(tenant_1).cpu_allocated_metric).to eq(all_vms_cpu_metric) + expect(subject_row_for_tenant(tenant_1).cpu_allocated_cost).to eq(all_vms_cpu_cost) + end + end + end + end + context "Monthly" do let(:options) { base_options.merge(:interval => 'monthly') } before do diff --git a/spec/models/metering_vm_spec.rb b/spec/models/metering_vm_spec.rb index c79bd257937..c8ae09718ac 100644 --- a/spec/models/metering_vm_spec.rb +++ b/spec/models/metering_vm_spec.rb @@ -126,6 +126,7 @@ storage_used_metric metering_used_metric existence_hours_metric + tenant_name ) end