diff --git a/app/models/chargeback.rb b/app/models/chargeback.rb index e43ceda3652..1a80dbb758a 100644 --- a/app/models/chargeback.rb +++ b/app/models/chargeback.rb @@ -1,262 +1,74 @@ class Chargeback < ActsAsArModel - HOURS_IN_DAY = 24 - HOURS_IN_WEEK = 168 - - VIRTUAL_COL_USES = { - "v_derived_cpu_total_cores_used" => "cpu_usage_rate_average" - } - def self.build_results_for_report_chargeback(options) _log.info("Calculating chargeback costs...") + @options = options = ReportOptions.new_from_h(options) - tz = Metric::Helper.get_time_zone(options[:ext_options]) - # TODO: Support time profiles via options[:ext_options][:time_profile] - - interval = options[:interval] || "daily" - cb = new - - options[:ext_options] ||= {} - - if @options[:groupby_tag] - @tag_hash = Classification.hash_all_by_type_and_name[@options[:groupby_tag]][:entry] - end - - base_rollup = MetricRollup.includes( - :resource => [:hardware, :tenant, :tags, :vim_performance_states, :custom_attributes, {:container_image => :custom_attributes}], - :parent_host => :tags, - :parent_ems_cluster => :tags, - :parent_storage => :tags, - :parent_ems => :tags) - .select(*Metric::BASE_COLS).order("resource_id, timestamp") - perf_cols = MetricRollup.attribute_names - rate_cols = ChargebackRate.where(:default => true).flat_map do |rate| - rate.chargeback_rate_details.map(&:metric).select { |metric| perf_cols.include?(metric.to_s) } - end - - rate_cols.map! { |x| VIRTUAL_COL_USES.include?(x) ? VIRTUAL_COL_USES[x] : x }.flatten! - base_rollup = base_rollup.select(*rate_cols) - - timerange = get_report_time_range(options, interval, tz) data = {} + rates = RatesCache.new + ConsumptionHistory.for_report(self, options) do |consumption| + rates_to_apply = rates.get(consumption) - interval_duration = interval_to_duration(interval) - - timerange.step_value(interval_duration).each_cons(2) do |query_start_time, query_end_time| - records = base_rollup.where(:timestamp => query_start_time...query_end_time, :capture_interval_name => "hourly") - records = where_clause(records, options) - records = Metric::Helper.remove_duplicate_timestamps(records) - next if records.empty? - _log.info("Found #{records.length} records for time range #{[query_start_time, query_end_time].inspect}") - - hours_in_interval = hours_in_interval(query_start_time, query_end_time, interval) - - # 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| - metric_rollup_records = metric_rollup_records.select { |x| x.resource.present? } - next if metric_rollup_records.empty? - - # we need to select ChargebackRates for groups of MetricRollups records - # and rates are selected by first MetricRollup record - metric_rollup_record = metric_rollup_records.first - rates_to_apply = cb.get_rates(metric_rollup_record) - - # key contains resource_id and timestamp (query_start_time...query_end_time) - # extra_fields there some extra field like resource name and - # some of them are related to specific chargeback (ChargebackVm, ChargebackContainer,...) - key, extra_fields = key_and_fields(metric_rollup_record, interval, tz) - data[key] ||= extra_fields - - chargeback_rates = data[key]["chargeback_rates"].split(', ') + rates_to_apply.collect(&:description) - data[key]["chargeback_rates"] = chargeback_rates.uniq.join(', ') + key = report_row_key(consumption) + data[key] ||= new(options, consumption) - # we are getting hash with metrics and costs for metrics defined for chargeback - metrics_and_costs = calculate_costs(metric_rollup_records, rates_to_apply, hours_in_interval) + chargeback_rates = data[key]["chargeback_rates"].split(', ') + rates_to_apply.collect(&:description) + data[key]["chargeback_rates"] = chargeback_rates.uniq.join(', ') - data[key].merge!(metrics_and_costs) - end + # we are getting hash with metrics and costs for metrics defined for chargeback + data[key].calculate_costs(consumption, rates_to_apply) end - _log.info("Calculating chargeback costs...Complete") - [data.map { |r| new(r.last) }] - end - - def self.hours_in_interval(query_start_time, query_end_time, interval) - return HOURS_IN_DAY if interval == "daily" - return HOURS_IN_WEEK if interval == "weekly" - - (query_end_time - query_start_time) / 1.hour + [data.values] end - def self.interval_to_duration(interval) - case interval - when "daily" - 1.day - when "weekly" - 1.week - when "monthly" - 1.month + def self.report_row_key(consumption) + ts_key = @options.start_of_report_step(consumption.timestamp) + if @options[:groupby_tag].present? + classification = classification_for(consumption) + classification_id = classification.present? ? classification.id : 'none' + "#{classification_id}_#{ts_key}" + else + default_key(consumption, ts_key) end end - def self.key_and_fields(metric_rollup_record, interval, tz) - ts_key = get_group_key_ts(metric_rollup_record, interval, tz) - - key, extra_fields = if @options[:groupby_tag].present? - get_tag_keys_and_fields(metric_rollup_record, ts_key) - else - get_keys_and_extra_fields(metric_rollup_record, ts_key) - end - - [key, date_fields(metric_rollup_record, interval, tz).merge(extra_fields)] + def self.default_key(consumption, ts_key) + "#{consumption.resource_id}_#{ts_key}" end - def self.date_fields(metric_rollup_record, interval, tz) - start_ts, end_ts, display_range = get_time_range(metric_rollup_record, interval, tz) - - { - 'start_date' => start_ts, - 'end_date' => end_ts, - 'display_range' => display_range, - 'interval_name' => interval, - 'chargeback_rates' => '', - 'entity' => metric_rollup_record.resource - } - end - - def self.get_tag_keys_and_fields(perf, ts_key) - tag = perf.tag_names.split("|").select { |x| x.starts_with?(@options[:groupby_tag]) }.first # 'department/*' + def self.classification_for(consumption) + tag = consumption.tag_names.find { |x| x.starts_with?(@options[:groupby_tag]) } # 'department/*' tag = tag.split('/').second unless tag.blank? # 'department/finance' -> 'finance' - classification = @tag_hash[tag] - classification_id = classification.present? ? classification.id : 'none' - key = "#{classification_id}_#{ts_key}" - extra_fields = { "tag_name" => classification.present? ? classification.description : _('') } - [key, extra_fields] + @options.tag_hash[tag] end - def get_rates(perf) - @rates ||= {} - @rates[perf.hash_features_affecting_rate] ||= - begin - prefix = Chargeback.report_cb_model(self.class.name).underscore - ChargebackRate.get_assigned_for_target(perf.resource, - :tag_list => perf.tag_list_reconstruct.map! { |t| prefix + t }, - :parents => get_rate_parents(perf)) - end - if perf.resource_type == Container.name && @rates[perf.hash_features_affecting_rate].empty? - @rates[perf.hash_features_affecting_rate] = [ChargebackRate.find_by(:description => "Default Container Image Rate", :rate_type => "Compute")] + def initialize(options, consumption) + @options = options + super() + if @options[:groupby_tag].present? + classification = self.class.classification_for(consumption) + self.tag_name = classification.present? ? classification.description : _('') + else + init_extra_fields(consumption) end - @rates[perf.hash_features_affecting_rate] + self.start_date, self.end_date, self.display_range = options.report_step_range(consumption.timestamp) + self.interval_name = options.interval + self.chargeback_rates = '' + self.entity = consumption.resource end - def self.calculate_costs(metric_rollup_records, rates, hours_in_interval) - calculated_costs = {} - - chargeback_fields_present = metric_rollup_records.count(&:chargeback_fields_present?) - calculated_costs['fixed_compute_metric'] = chargeback_fields_present if chargeback_fields_present + def calculate_costs(consumption, rates) + self.fixed_compute_metric = consumption.chargeback_fields_present if consumption.chargeback_fields_present rates.each do |rate| rate.chargeback_rate_details.each do |r| - if !chargeback_fields_present && r.fixed? - cost = 0 - else - r.hours_in_interval = hours_in_interval - metric_value = r.metric_value_by(metric_rollup_records) - cost = r.cost(metric_value) * hours_in_interval - end - - # add values to hash and sum - reportable_metric_and_cost_fields(r.rate_name, r.group, metric_value, cost).each do |k, val| - next unless attribute_names.include?(k) - calculated_costs[k] ||= 0 - calculated_costs[k] += val + r.charge(relevant_fields, consumption).each do |field, value| + next unless self.class.attribute_names.include?(field) + self[field] = (self[field] || 0) + value end end end - - calculated_costs - end - - def self.reportable_metric_and_cost_fields(rate_name, rate_group, metric, cost) - cost_key = "#{rate_name}_cost" # metric cost value (e.g. Storage [Used|Allocated|Fixed] Cost) - metric_key = "#{rate_name}_metric" # metric value (e.g. Storage [Used|Allocated|Fixed]) - cost_group_key = "#{rate_group}_cost" # for total of metric's costs (e.g. Storage Total Cost) - metric_group_key = "#{rate_group}_metric" # for total of metrics (e.g. Storage Total) - - col_hash = {} - - defined_column_for_report = (report_col_options.keys & [metric_key, cost_key]).present? - - if defined_column_for_report - [metric_key, metric_group_key].each { |col| col_hash[col] = metric } - [cost_key, cost_group_key, 'total_cost'].each { |col| col_hash[col] = cost } - end - - col_hash - end - - def self.get_group_key_ts(perf, interval, tz) - ts = perf.timestamp.in_time_zone(tz) - case interval - when "daily" - ts = ts.beginning_of_day - when "weekly" - ts = ts.beginning_of_week - when "monthly" - ts = ts.beginning_of_month - else - raise _("interval '%{interval}' is not supported") % {:interval => interval} - end - - ts - end - - def self.get_time_range(perf, interval, tz) - ts = perf.timestamp.in_time_zone(tz) - case interval - when "daily" - [ts.beginning_of_day, ts.end_of_day, ts.strftime("%m/%d/%Y")] - when "weekly" - s_ts = ts.beginning_of_week - e_ts = ts.end_of_week - [s_ts, e_ts, "Week of #{s_ts.strftime("%m/%d/%Y")}"] - when "monthly" - s_ts = ts.beginning_of_month - e_ts = ts.end_of_month - [s_ts, e_ts, s_ts.strftime("%b %Y")] - else - raise _("interval '%{interval}' is not supported") % {:interval => interval} - end - end - - # @option options :start_time [DateTime] used with :end_time to create time range - # @option options :end_time [DateTime] - # @option options :interval_size [Fixednum] Used with :end_interval_offset to generate time range - # @option options :end_interval_offset - def self.get_report_time_range(options, interval, tz) - return options[:start_time]..options[:end_time] if options[:start_time] - raise _("Option 'interval_size' is required") if options[:interval_size].nil? - - end_interval_offset = options[:end_interval_offset] || 0 - start_interval_offset = (end_interval_offset + options[:interval_size] - 1) - - ts = Time.now.in_time_zone(tz) - case interval - when "daily" - start_time = (ts - start_interval_offset.days).beginning_of_day.utc - end_time = (ts - end_interval_offset.days).end_of_day.utc - when "weekly" - start_time = (ts - start_interval_offset.weeks).beginning_of_week.utc - end_time = (ts - end_interval_offset.weeks).end_of_week.utc - when "monthly" - start_time = (ts - start_interval_offset.months).beginning_of_month.utc - end_time = (ts - end_interval_offset.months).end_of_month.utc - else - raise _("interval '%{interval}' is not supported") % {:interval => interval} - end - - start_time..end_time end def self.report_cb_model(model) @@ -271,10 +83,6 @@ def self.report_tag_field "tag_name" end - def self.get_rate_parents - raise "Chargeback: get_rate_parents must be implemented in child class." - end - def self.set_chargeback_report_options(rpt, edit) rpt.cols = %w(start_date display_range) @@ -337,4 +145,10 @@ def self.load_custom_attribute(custom_attribute) entity.send(custom_attribute) end end + + private + + def relevant_fields + @relevant_fields ||= self.class.report_col_options.keys.to_set + end end # class Chargeback diff --git a/app/models/chargeback/consumption.rb b/app/models/chargeback/consumption.rb new file mode 100644 index 00000000000..ad4ac5d6d8b --- /dev/null +++ b/app/models/chargeback/consumption.rb @@ -0,0 +1,11 @@ +class Chargeback + class Consumption + def initialize(start_time, end_time) + @start_time, @end_time = start_time, end_time + end + + def hours_in_interval + @hours_in_interval ||= (@end_time - @start_time).round / 1.hour + end + end +end diff --git a/app/models/chargeback/consumption_history.rb b/app/models/chargeback/consumption_history.rb new file mode 100644 index 00000000000..c3a19326428 --- /dev/null +++ b/app/models/chargeback/consumption_history.rb @@ -0,0 +1,54 @@ +class Chargeback + class ConsumptionHistory + VIRTUAL_COL_USES = { + 'v_derived_cpu_total_cores_used' => 'cpu_usage_rate_average' + }.freeze + + def self.for_report(cb_class, options) + base_rollup = base_rollup_scope + timerange = options.report_time_range + interval_duration = options.duration_of_report_step + + extra_resources = cb_class.try(:extra_resources_without_rollups) || [] + timerange.step_value(interval_duration).each_cons(2) do |query_start_time, query_end_time| + extra_resources.each do |resource| + yield ConsumptionWithoutRollups.new(resource, query_start_time, query_end_time) + end + + records = base_rollup.where(:timestamp => query_start_time...query_end_time, :capture_interval_name => 'hourly') + records = cb_class.where_clause(records, options) + records = Metric::Helper.remove_duplicate_timestamps(records) + next if records.empty? + _log.info("Found #{records.length} records for time range #{[query_start_time, query_end_time].inspect}") + + # 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| + metric_rollup_records = metric_rollup_records.select { |x| x.resource.present? } + consumption = ConsumptionWithRollups.new(metric_rollup_records, query_start_time, query_end_time) + next if metric_rollup_records.empty? + yield(consumption) + end + end + end + + def self.base_rollup_scope + base_rollup = MetricRollup.includes( + :resource => [:hardware, :tenant, :tags, :vim_performance_states, :custom_attributes, + {:container_image => :custom_attributes}], + :parent_host => :tags, + :parent_ems_cluster => :tags, + :parent_storage => :tags, + :parent_ems => :tags) + .select(*Metric::BASE_COLS).order('resource_id, timestamp') + + perf_cols = MetricRollup.attribute_names + rate_cols = ChargebackRate.where(:default => true).flat_map do |rate| + rate.chargeback_rate_details.map(&:metric).select { |metric| perf_cols.include?(metric.to_s) } + end + rate_cols.map! { |x| VIRTUAL_COL_USES[x] || x }.flatten! + base_rollup.select(*rate_cols) + end + private_class_method :base_rollup_scope + end +end diff --git a/app/models/chargeback/consumption_with_rollups.rb b/app/models/chargeback/consumption_with_rollups.rb new file mode 100644 index 00000000000..85db3ed71e3 --- /dev/null +++ b/app/models/chargeback/consumption_with_rollups.rb @@ -0,0 +1,44 @@ +class Chargeback + class ConsumptionWithRollups < Consumption + delegate :timestamp, :resource, :resource_id, :resource_name, :resource_type, :parent_ems, + :hash_features_affecting_rate, :tag_list_with_prefix, :parents_determining_rate, + :to => :first_metric_rollup_record + + def initialize(metric_rollup_records, start_time, end_time) + super(start_time, end_time) + @rollups = metric_rollup_records + end + + def tag_names + first_metric_rollup_record.tag_names.split('|') + end + + def max(metric) + values(metric).max + end + + def avg(metric) + metric_sum = values(metric).sum + metric_sum / hours_in_interval + end + + def none?(metric) + values(metric).empty? + end + + def chargeback_fields_present + @chargeback_fields_present ||= @rollups.count(&:chargeback_fields_present?) + end + + private + + def values(metric) + @values ||= {} + @values[metric] ||= @rollups.collect(&metric.to_sym).compact + end + + def first_metric_rollup_record + @fmrr ||= @rollups.first + end + end +end diff --git a/app/models/chargeback/consumption_without_rollups.rb b/app/models/chargeback/consumption_without_rollups.rb new file mode 100644 index 00000000000..a0c618d4856 --- /dev/null +++ b/app/models/chargeback/consumption_without_rollups.rb @@ -0,0 +1,61 @@ +class Chargeback + class ConsumptionWithoutRollups < Consumption + delegate :id, :name, :type, :to => :resource, :prefix => :resource + attr_reader :resource + + def initialize(resource, start_time, end_time) + super(start_time, end_time) + @resource = resource + end + + def timestamp + @start_time + end + + def parent_ems + resource.ext_management_system + end + + def tag_names + resource.tags.collect(&:name) + end + + def hash_features_affecting_rate + resource.id + end + + def tag_list_with_prefix + tag_names.map { |t| "vm/tag#{t}" } + end + + def parents_determining_rate + [resource.host, resource.ems_cluster, resource.storage, parent_ems, resource.tenant, + MiqEnterprise.my_enterprise].compact + end + + def none?(metric) + current_value(metric).nil? + end + + def chargeback_fields_present + 1 # Yes, charge this interval as fixed_compute_*_* + end + + def current_value(metric) + # Return the last seen allocation for charging purposes. + @value ||= {} + @value[metric] ||= case metric + when 'derived_vm_numvcpus' # Allocated CPU count + resource.hardware.try(:cpu_total_cores) + when 'derived_memory_available' + resource.hardware.try(:memory_mb) + when 'derived_vm_allocated_disk_storage' + resource.allocated_disk_storage + end + @value[metric] + end + alias avg current_value + alias max current_value + private :current_value + end +end diff --git a/app/models/chargeback/rates_cache.rb b/app/models/chargeback/rates_cache.rb new file mode 100644 index 00000000000..4e07a84d620 --- /dev/null +++ b/app/models/chargeback/rates_cache.rb @@ -0,0 +1,18 @@ +class Chargeback + class RatesCache + def get(consumption) + # we need to select ChargebackRates for groups of MetricRollups records + # and rates are selected by first MetricRollup record + @rates ||= {} + @rates[consumption.hash_features_affecting_rate] ||= + ChargebackRate.get_assigned_for_target(consumption.resource, + :tag_list => consumption.tag_list_with_prefix, + :parents => consumption.parents_determining_rate) + if consumption.resource_type == Container.name && @rates[consumption.hash_features_affecting_rate].empty? + @rates[consumption.hash_features_affecting_rate] = [ChargebackRate.find_by( + :description => 'Default Container Image Rate', :rate_type => 'Compute')] + end + @rates[consumption.hash_features_affecting_rate] + end + end +end diff --git a/app/models/chargeback/report_options.rb b/app/models/chargeback/report_options.rb new file mode 100644 index 00000000000..aa39dd2e627 --- /dev/null +++ b/app/models/chargeback/report_options.rb @@ -0,0 +1,103 @@ +class Chargeback + # ReportOptions are usualy stored in MiqReport.db_options[:options] + ReportOptions = Struct.new( + :interval, # daily | weekly | monthly + :interval_size, + :end_interval_offset, + :owner, # userid + :tenant_id, + :tag, # like /managed/environment/prod (Mutually exclusive with :user) + :provide_id, + :entity_id, # 1/2/3.../all rails id of entity + :service_id, + :groupby, + :groupby_tag, + :userid, + :ext_options, + ) do + def self.new_from_h(hash) + new(*hash.values_at(*members)) + end + + def initialize(*) + super + self.interval ||= 'daily' + self.end_interval_offset ||= 0 + self.ext_options ||= {} + end + + def tz + # TODO: Support time profiles via options[:ext_options][:time_profile] + @tz ||= Metric::Helper.get_time_zone(ext_options) + end + + def report_time_range + raise _("Option 'interval_size' is required") if interval_size.nil? + + start_interval_offset = (end_interval_offset + interval_size - 1) + + ts = Time.now.in_time_zone(tz) + case interval + when 'daily' + start_time = (ts - start_interval_offset.days).beginning_of_day.utc + end_time = (ts - end_interval_offset.days).end_of_day.utc + when 'weekly' + start_time = (ts - start_interval_offset.weeks).beginning_of_week.utc + end_time = (ts - end_interval_offset.weeks).end_of_week.utc + when 'monthly' + start_time = (ts - start_interval_offset.months).beginning_of_month.utc + end_time = (ts - end_interval_offset.months).end_of_month.utc + else + raise _("interval '%{interval}' is not supported") % {:interval => interval} + end + + start_time..end_time + end + + def start_of_report_step(timestamp) + ts = timestamp.in_time_zone(tz) + case interval + when 'daily' + ts.beginning_of_day + when 'weekly' + ts.beginning_of_week + when 'monthly' + ts.beginning_of_month + else + raise _("interval '%{interval}' is not supported") % {:interval => interval} + end + end + + def report_step_range(timestamp) + ts = timestamp.in_time_zone(tz) + case interval + when 'daily' + [ts.beginning_of_day, ts.end_of_day, ts.strftime('%m/%d/%Y')] + when 'weekly' + s_ts = ts.beginning_of_week + e_ts = ts.end_of_week + [s_ts, e_ts, "Week of #{s_ts.strftime('%m/%d/%Y')}"] + when 'monthly' + s_ts = ts.beginning_of_month + e_ts = ts.end_of_month + [s_ts, e_ts, s_ts.strftime('%b %Y')] + else + raise _("interval '%{interval}' is not supported") % {:interval => interval} + end + end + + def duration_of_report_step + case interval + when 'daily' then 1.day + when 'weekly' then 1.week + when 'monthly' then 1.month + end + end + + def tag_hash + if groupby_tag + @tag_hash ||= Classification.hash_all_by_type_and_name[groupby_tag][:entry] + end + end + end +end diff --git a/app/models/chargeback_container_image.rb b/app/models/chargeback_container_image.rb index dacd260439e..4eb8128ba28 100644 --- a/app/models/chargeback_container_image.rb +++ b/app/models/chargeback_container_image.rb @@ -28,20 +28,9 @@ class ChargebackContainerImage < Chargeback ) def self.build_results_for_report_ChargebackContainerImage(options) - # Options: - # :rpt_type => chargeback - # :interval => daily | weekly | monthly - # :start_time - # :end_time - # :end_interval_offset - # :interval_size - # :owner => - # :tag => /managed/environment/prod (Mutually exclusive with :user) - # :chargeback_type => detail | summary - # :entity_id => 1/2/3.../all rails id of entity + # Options: a hash transformable to Chargeback::ReportOptions # Find Project by id or get all projects - @options = options provider_id = options[:provider_id] id = options[:entity_id] raise "must provide option :entity_id and provider_id" if id.nil? && provider_id.nil? @@ -66,23 +55,18 @@ def self.build_results_for_report_ChargebackContainerImage(options) build_results_for_report_chargeback(options) end - def self.get_keys_and_extra_fields(perf, ts_key) - project = @data_index.fetch_path(:container_project, :by_container_id, perf.resource_id) - image = @data_index.fetch_path(:container_image, :by_container_id, perf.resource_id) - - key = @options[:groupby] == 'project' ? "#{project.id}_#{ts_key}" : "#{project.id}_#{image.id}_#{ts_key}" + def self.default_key(metric_rollup_record, ts_key) + project = @data_index.fetch_path(:container_project, :by_container_id, metric_rollup_record.resource_id) + image = @data_index.fetch_path(:container_image, :by_container_id, metric_rollup_record.resource_id) + @options[:groupby] == 'project' ? "#{project.id}_#{ts_key}" : "#{project.id}_#{image.id}_#{ts_key}" + end - extra_fields = { - "project_name" => project.name, - "image_name" => image.try(:full_name) || _("Deleted"), # until image archiving is implemented - "project_uid" => project.ems_ref, - "provider_name" => perf.parent_ems.try(:name), - "provider_uid" => perf.parent_ems.try(:name), - "archived" => project.archived? ? _("Yes") : _("No"), - "entity" => image - } + def self.image(consumption) + @data_index.fetch_path(:container_image, :by_container_id, consumption.resource_id) + end - [key, extra_fields] + def self.project(consumption) + @data_index.fetch_path(:container_project, :by_container_id, consumption.resource_id) end def self.where_clause(records, _options) @@ -109,7 +93,16 @@ def self.report_col_options } end - def get_rate_parents(perf) - [perf.parent_ems] + private + + def init_extra_fields(consumption) + self.project_name = self.class.project(consumption).name + # until image archiving is implemented + self.image_name = self.class.image(consumption).try(:full_name) || _('Deleted') + self.project_uid = self.class.project(consumption).ems_ref + self.provider_name = consumption.parent_ems.try(:name) + self.provider_uid = consumption.parent_ems.try(:guid) + self.archived = self.class.project(consumption).archived? ? _('Yes') : _('No') + self.entity = self.class.image(consumption) end end # class ChargebackContainerImage diff --git a/app/models/chargeback_container_project.rb b/app/models/chargeback_container_project.rb index da0e4cadf64..eb6a3da90d1 100644 --- a/app/models/chargeback_container_project.rb +++ b/app/models/chargeback_container_project.rb @@ -27,20 +27,9 @@ class ChargebackContainerProject < Chargeback ) def self.build_results_for_report_ChargebackContainerProject(options) - # Options: - # :rpt_type => chargeback - # :interval => daily | weekly | monthly - # :start_time - # :end_time - # :end_interval_offset - # :interval_size - # :owner => - # :tag => /managed/environment/prod (Mutually exclusive with :user) - # :chargeback_type => detail | summary - # :entity_id => 1/2/3.../all rails id of entity + # Options: a hash transformable to Chargeback::ReportOptions # Find ContainerProjects according to any of these: - @options = options provider_id = options[:provider_id] project_id = options[:entity_id] filter_tag = options[:tag] @@ -63,19 +52,6 @@ def self.build_results_for_report_ChargebackContainerProject(options) build_results_for_report_chargeback(options) end - def self.get_keys_and_extra_fields(perf, ts_key) - key = "#{perf.resource_id}_#{ts_key}" - extra_fields = { - "project_name" => perf.resource_name, - "project_uid" => perf.resource.ems_ref, - "provider_name" => perf.parent_ems.try(:name), - "provider_uid" => perf.parent_ems.try(:guid), - "archived" => perf.resource.archived? ? _("Yes") : _("No") - } - - [key, extra_fields] - end - def self.where_clause(records, _options) records.where(:resource_type => ContainerProject.name, :resource_id => @projects.select(:id)) end @@ -100,8 +76,13 @@ def self.report_col_options } end - def get_rate_parents(perf) - # Get rate from assigned containers providers only - [perf.parent_ems].compact + private + + def init_extra_fields(consumption) + self.project_name = consumption.resource_name + self.project_uid = consumption.resource.ems_ref + self.provider_name = consumption.parent_ems.try(:name) + self.provider_uid = consumption.parent_ems.try(:guid) + self.archived = consumption.resource.archived? ? _('Yes') : _('No') end end # class Chargeback diff --git a/app/models/chargeback_rate_detail.rb b/app/models/chargeback_rate_detail.rb index fc8e7995725..34e43d7e76e 100644 --- a/app/models/chargeback_rate_detail.rb +++ b/app/models/chargeback_rate_detail.rb @@ -20,22 +20,25 @@ class ChargebackRateDetail < ApplicationRecord attr_accessor :hours_in_interval - def max_of_metric_from(metric_rollup_records) - metric_rollup_records.map(&metric.to_sym).max - end - - def avg_of_metric_from(metric_rollup_records) - metric_sum = metric_rollup_records.sum(&metric.to_sym) - metric_sum / @hours_in_interval + def charge(relevant_fields, consumption) + result = {} + if (relevant_fields & [metric_keys[0], cost_keys[0]]).present? + metric_value, cost = metric_and_cost_by(consumption) + if !consumption.chargeback_fields_present && fixed? + cost = 0 + end + metric_keys.each { |field| result[field] = metric_value } + cost_keys.each { |field| result[field] = cost } + end + result end - def metric_value_by(metric_rollup_records) + def metric_value_by(consumption) return 1.0 if fixed? - metric_rollups_without_nils = metric_rollup_records.select { |x| x.send(metric.to_sym).present? } - return 0 if metric_rollups_without_nils.empty? - return max_of_metric_from(metric_rollups_without_nils) if allocated? - return avg_of_metric_from(metric_rollups_without_nils) if used? + return 0 if consumption.none?(metric) + return consumption.max(metric) if allocated? + return consumption.avg(metric) if used? end def used? @@ -73,7 +76,7 @@ def find_rate(value) :yearly => "Year" } - def cost(value) + def hourly_cost(value) return 0.0 unless self.enabled? value = 1.0 if fixed? @@ -214,6 +217,25 @@ def contiguous_tiers? !error end + private + + def metric_keys + ["#{rate_name}_metric", # metric value (e.g. Storage [Used|Allocated|Fixed]) + "#{group}_metric"] # total of metric's group (e.g. Storage Total) + end + + def cost_keys + ["#{rate_name}_cost", # cost associated with metric (e.g. Storage [Used|Allocated|Fixed] Cost) + "#{group}_cost", # cost associated with metric's group (e.g. Storage Total Cost) + 'total_cost'] + end + + def metric_and_cost_by(consumption) + @hours_in_interval = consumption.hours_in_interval + metric_value = metric_value_by(consumption) + [metric_value, hourly_cost(metric_value) * consumption.hours_in_interval] + end + def first_tier?(tier,tiers) tier == tiers.first end diff --git a/app/models/chargeback_vm.rb b/app/models/chargeback_vm.rb index abcf6810337..4eb726e93fa 100644 --- a/app/models/chargeback_vm.rb +++ b/app/models/chargeback_vm.rb @@ -51,78 +51,30 @@ class ChargebackVm < Chargeback ) def self.build_results_for_report_ChargebackVm(options) - # Options: - # :rpt_type => chargeback - # :interval => daily | weekly | monthly - # :start_time - # :end_time - # :end_interval_offset - # :interval_size - # :owner => - # :tag => /managed/environment/prod (Mutually exclusive with :user) - # :chargeback_type => detail | summary + # Options: a hash transformable to Chargeback::ReportOptions @report_user = User.find_by(:userid => options[:userid]) - # Find Vms by user or by tag - if options[:owner] - user = User.find_by_userid(options[:owner]) - if user.nil? - _log.error("Unable to find user '#{options[:owner]}'. Calculating chargeback costs aborted.") - raise MiqException::Error, _("Unable to find user '%{name}'") % {:name => options[:owner]} - end - vms = user.vms - elsif options[:tag] - vms = Vm.find_tagged_with(:all => options[:tag], :ns => "*") - vms &= @report_user.accessible_vms if @report_user && @report_user.self_service? - elsif options[:tenant_id] - tenant = Tenant.find(options[:tenant_id]) - if tenant.nil? - _log.error("Unable to find tenant '#{options[:tenant_id]}'. Calculating chargeback costs aborted.") - raise MiqException::Error, "Unable to find tenant '#{options[:tenant_id]}'" - end - vms = tenant.vms - elsif options[:service_id] - service = Service.find(options[:service_id]) - if service.nil? - _log.error("Unable to find service '#{options[:service_id]}'. Calculating chargeback costs aborted.") - raise MiqException::Error, "Unable to find service '#{options[:service_id]}'" - end - vms = service.vms - else - raise _("must provide options :owner or :tag") - end - return [[]] if vms.empty? - - @options = options - @vm_owners = vms.inject({}) { |h, v| h[v.id] = v.evm_owner_name; h } - + @vm_owners = @vms = nil build_results_for_report_chargeback(options) end - def self.get_keys_and_extra_fields(perf, ts_key) - key = "#{perf.resource_id}_#{ts_key}" - @vm_owners[perf.resource_id] ||= perf.resource.evm_owner_name - - extra_fields = { - "vm_id" => perf.resource_id, - "vm_name" => perf.resource_name, - "vm_uid" => perf.resource.ems_ref, - "vm_guid" => perf.resource.try(:guid), - "owner_name" => @vm_owners[perf.resource_id], - "provider_name" => perf.parent_ems.try(:name), - "provider_uid" => perf.parent_ems.try(:guid) - } - - [key, extra_fields] - end - def self.where_clause(records, options) scope = records.where(:resource_type => "VmOrTemplate") if options[:tag] && (@report_user.nil? || !@report_user.self_service?) scope.where.not(:resource_id => nil).for_tag_names(options[:tag].split("/")[2..-1]) else - scope.where(:resource_id => @vm_owners.keys) + scope.where(:resource => vms) + end + end + + def self.extra_resources_without_rollups + # support hyper-v for which we do not collect metrics yet + scope = ManageIQ::Providers::Microsoft::InfraManager::Vm + if @options[:tag] && (@report_user.nil? || !@report_user.self_service?) + scope.find_tagged_with(:any => @options[:tag], :ns => '*') + else + scope.where(:id => vms) end end @@ -168,9 +120,55 @@ def self.report_col_options } end - def get_rate_parents(perf) - @enterprise ||= MiqEnterprise.my_enterprise - parents = perf.resource_parents + [@enterprise] - parents.compact + def self.vm_owner(consumption) + @vm_owners ||= vms.each_with_object({}) { |vm, res| res[vm.id] = vm.evm_owner_name } + @vm_owners[consumption.resource_id] ||= consumption.resource.evm_owner_name + end + + def self.vms + @vms ||= + begin + # Find Vms by user or by tag + if @options[:owner] + user = User.find_by_userid(@options[:owner]) + if user.nil? + _log.error("Unable to find user '#{@options[:owner]}'. Calculating chargeback costs aborted.") + raise MiqException::Error, _("Unable to find user '%{name}'") % {:name => @options[:owner]} + end + user.vms + elsif @options[:tag] + vms = Vm.find_tagged_with(:all => @options[:tag], :ns => '*') + vms &= @report_user.accessible_vms if @report_user && @report_user.self_service? + vms + elsif @options[:tenant_id] + tenant = Tenant.find(@options[:tenant_id]) + if tenant.nil? + _log.error("Unable to find tenant '#{@options[:tenant_id]}'. Calculating chargeback costs aborted.") + raise MiqException::Error, "Unable to find tenant '#{@options[:tenant_id]}'" + end + tenant.vms + elsif @options[:service_id] + service = Service.find(@options[:service_id]) + if service.nil? + _log.error("Unable to find service '#{@options[:service_id]}'. Calculating chargeback costs aborted.") + raise MiqException::Error, "Unable to find service '#{@options[:service_id]}'" + end + service.vms + else + raise _("must provide options :owner or :tag") + end + end + end + + private + + def init_extra_fields(consumption) + self.vm_id = consumption.resource_id + self.vm_name = consumption.resource_name + self.vm_uid = consumption.resource.ems_ref + self.vm_guid = consumption.resource.try(:guid) + self.owner_name = self.class.vm_owner(consumption) + self.provider_name = consumption.parent_ems.try(:name) + self.provider_uid = consumption.parent_ems.try(:guid) end end # class Chargeback diff --git a/app/models/metric/chargeback_helper.rb b/app/models/metric/chargeback_helper.rb index 638b3f36e90..6ebe59b4a09 100644 --- a/app/models/metric/chargeback_helper.rb +++ b/app/models/metric/chargeback_helper.rb @@ -1,4 +1,6 @@ module Metric::ChargebackHelper + TAG_MANAGED_PREFIX = "/tag/managed/".freeze + def hash_features_affecting_rate tags = tag_names.split('|').reject { |n| n.starts_with?('folder_path_') }.sort.join('|') keys = [tags] + resource_parents.map(&:id) @@ -6,15 +8,24 @@ def hash_features_affecting_rate keys.join('_') end - def tag_list_reconstruct - tag_list = tag_names.split("|").inject([]) { |arr, t| arr << "/tag/managed/#{t}" } + def tag_prefix + klass_prefix = case resource_type + when Container.name then 'container_image' + when VmOrTemplate.name then 'vm' + when ContainerProject.name then 'container_project' + end + + klass_prefix + TAG_MANAGED_PREFIX + end - if resource_type == Container.name + def tag_list_with_prefix + if resource.kind_of?(Container) state = resource.vim_performance_state_for_ts(timestamp.to_s) - tag_list += state.image_tag_names.split("|").inject([]) { |arr, t| arr << "/tag/managed/#{t}" } if state.present? - tag_list += resource.try(:container_image).try(:docker_labels).try(:collect) {|l| "/label/managed/#{l.name}/#{l.value}"} + image_tag_name = "#{state.image_tag_names}|" if state + labels = resource.try(:container_image).try(:docker_labels).try(:collect) {|l| "container_image/label/managed/#{l.name}/#{l.value}"} end - tag_list + + "#{image_tag_name}#{tag_names}".split("|").reject(&:empty?).map { |x| "#{tag_prefix}#{x}" } + (labels || []) end def resource_parents @@ -25,4 +36,15 @@ def resource_parents resource.try(:tenant) ].compact end + + def parents_determining_rate + case resource_type + when VmOrTemplate.name + (resource_parents + [MiqEnterprise.my_enterprise]).compact + when ContainerProject.name + [parent_ems].compact + when Container.name + [parent_ems] + end + end end diff --git a/spec/models/chargeback/consumption_without_rollups_spec.rb b/spec/models/chargeback/consumption_without_rollups_spec.rb new file mode 100644 index 00000000000..cf48475d417 --- /dev/null +++ b/spec/models/chargeback/consumption_without_rollups_spec.rb @@ -0,0 +1,21 @@ +describe Chargeback::ConsumptionWithoutRollups do + let(:cores) { 7 } + let(:mem_mb) { 1777 } + let(:disk_size) { 12_345 } + let(:hardware) do + FactoryGirl.build(:hardware, + :cpu_total_cores => cores, + :memory_mb => mem_mb, + :disks => [FactoryGirl.build(:disk, :size => disk_size)]) + end + let(:vm) { FactoryGirl.build(:vm_microsoft, :hardware => hardware) } + let(:consumption) { described_class.new(vm, nil, nil) } + + describe '#avg' do + it 'returns current values' do + expect(consumption.avg('derived_vm_numvcpus')).to eq(cores) + expect(consumption.avg('derived_memory_available')).to eq(mem_mb) + expect(consumption.avg('derived_vm_allocated_disk_storage')).to eq(disk_size) + end + end +end diff --git a/spec/models/chargeback_rate_detail_spec.rb b/spec/models/chargeback_rate_detail_spec.rb index 0822554f73f..b8f55a01fc2 100644 --- a/spec/models/chargeback_rate_detail_spec.rb +++ b/spec/models/chargeback_rate_detail_spec.rb @@ -26,7 +26,7 @@ let(:hours_in_month) { 720 } - it "#cost" do + it '#hourly_cost' do cvalue = 42.0 fixed_rate = 5.0 variable_rate = 8.26 @@ -46,13 +46,13 @@ :fixed_rate => fixed_rate, :variable_rate => variable_rate) cbd.update(:chargeback_tiers => [cbt]) - expect(cbd.cost(cvalue)).to eq(cvalue * cbd.hourly(variable_rate) + cbd.hourly(fixed_rate)) + expect(cbd.hourly_cost(cvalue)).to eq(cvalue * cbd.hourly(variable_rate) + cbd.hourly(fixed_rate)) cbd.group = 'fixed' - expect(cbd.cost(cvalue)).to eq(cbd.hourly(variable_rate) + cbd.hourly(fixed_rate)) + expect(cbd.hourly_cost(cvalue)).to eq(cbd.hourly(variable_rate) + cbd.hourly(fixed_rate)) cbd.enabled = false - expect(cbd.cost(cvalue)).to eq(0.0) + expect(cbd.hourly_cost(cvalue)).to eq(0.0) end it "#hourly" do @@ -194,7 +194,7 @@ :per_time => 'monthly', :chargeback_rate_detail_measure_id => cbdm.id, :hours_in_interval => hours_in_month) - expect(cbd_bytes.cost(100)).to eq(cbd_gigabytes.cost(100)) + expect(cbd_bytes.hourly_cost(100)).to eq(cbd_gigabytes.hourly_cost(100)) end it "#show_rates" do diff --git a/spec/models/chargeback_vm_spec.rb b/spec/models/chargeback_vm_spec.rb index 83525e92a55..032990d890f 100644 --- a/spec/models/chargeback_vm_spec.rb +++ b/spec/models/chargeback_vm_spec.rb @@ -50,79 +50,6 @@ Timecop.return end - describe "#get_rate_parents" do - let(:vm) do - FactoryGirl.create(:vm_vmware, :name => "test_vm", :evm_owner => @admin, :ems_ref => "ems_ref", - :ems_cluster => @ems_cluster, :storage => @storage, :host => @host1, - :ext_management_system => @ems - ) - end - - let(:metric_rollup_without_parents) do - FactoryGirl.create(:metric_rollup_vm_hr, - :cpu_usagemhz_rate_average => @cpu_usagemhz_rate, - :derived_vm_numvcpus => @cpu_count, - :derived_memory_available => @memory_available, - :derived_memory_used => @memory_used, - :disk_usage_rate_average => @disk_usage_rate, - :net_usage_rate_average => @net_usage_rate, - :derived_vm_used_disk_storage => @vm_used_disk_storage.gigabytes, - :derived_vm_allocated_disk_storage => @vm_allocated_disk_storage.gigabytes, - :tag_names => "environment/prod", - :resource => vm - ) - end - - let(:metric_rollup_with_parents) do - FactoryGirl.create(:metric_rollup_vm_hr, - :cpu_usagemhz_rate_average => @cpu_usagemhz_rate, - :derived_vm_numvcpus => @cpu_count, - :derived_memory_available => @memory_available, - :derived_memory_used => @memory_used, - :disk_usage_rate_average => @disk_usage_rate, - :net_usage_rate_average => @net_usage_rate, - :derived_vm_used_disk_storage => @vm_used_disk_storage.gigabytes, - :derived_vm_allocated_disk_storage => @vm_allocated_disk_storage.gigabytes, - :tag_names => "environment/prod", - :resource => vm, - :parent_host_id => @host1.id, - :parent_ems_cluster_id => @ems_cluster.id, - :parent_ems_id => @ems.id, - :parent_storage_id => @storage.id, - ) - end - - it "uses resource's host, cluster, storage and ems" do - parents = described_class.new.send(:get_rate_parents, metric_rollup_without_parents) - - expected_array = [ - vm.host, - vm.ems_cluster, - vm.storage, - vm.ext_management_system, - vm.tenant, - MiqEnterprise.my_enterprise - ].compact - - expect(parents).to match_array(expected_array) - end - - it "uses host, cluster, storage ems from MetricRollup record" do - parents = described_class.new.send(:get_rate_parents, metric_rollup_with_parents) - - expected_array = [ - metric_rollup_with_parents.parent_host, - metric_rollup_with_parents.parent_ems_cluster, - metric_rollup_with_parents.parent_storage, - metric_rollup_with_parents.parent_ems, - vm.tenant, - MiqEnterprise.my_enterprise - ].compact - - expect(parents).to match_array(expected_array) - end - end - let(:report_static_fields) { %w(vm_name) } it "uses static fields" do @@ -871,10 +798,11 @@ def used_average_for(metric, hours_in_interval) :parent_ems_id => @ems.id, :parent_storage_id => @storage.id, :resource => @vm1) end + let(:consumption) { Chargeback::ConsumptionWithRollups.new([metric_rollup], nil, nil) } before do ChargebackRate.set_assignments(:compute, [rate_assignment_options]) - @rate = chargeback_vm.get_rates(metric_rollup).first + @rate = Chargeback::RatesCache.new.get(consumption).first @assigned_rate = ChargebackRate.get_assignments("Compute").first end @@ -884,45 +812,58 @@ def used_average_for(metric, hours_in_interval) end end - describe ".get_keys_and_extra_fields" do - let(:timestamp_key) { "2012-08-31T07:00:00Z" } - let(:vm_owners) { {@vm1.id => @vm1.evm_owner_name} } - let(:metric_rollup) do - FactoryGirl.create(:metric_rollup_vm_hr, :timestamp => timestamp_key, :tag_names => "environment/prod", - :parent_host_id => @host1.id, :parent_ems_cluster_id => @ems_cluster.id, - :parent_ems_id => @ems.id, :parent_storage_id => @storage.id, - :resource => @vm1, :resource_name => @vm1.name) + describe '.report_row_key' do + let(:report_options) { Chargeback::ReportOptions.new } + let(:timestamp_key) { 'Fri, 13 May 2016 10:40:00 UTC +00:00' } + let(:beginning_of_day) { timestamp_key.in_time_zone.beginning_of_day } + let(:metric_rollup) { FactoryGirl.build(:metric_rollup_vm_hr, :timestamp => timestamp_key, :resource => @vm1) } + let(:consumption) { Chargeback::ConsumptionWithRollups.new([metric_rollup], nil, nil) } + subject { described_class.report_row_key(consumption) } + before do + described_class.instance_variable_set(:@options, report_options) end - it "returns extra fields" do - ChargebackVm.instance_variable_set(:@vm_owners, vm_owners) - - extra_fields = ChargebackVm.get_keys_and_extra_fields(metric_rollup, timestamp_key) - expected_fields = {"vm_name" => @vm1.name, "owner_name" => @admin.name, "provider_name" => @ems.name, - "provider_uid" => @ems.guid, "vm_uid" => "ems_ref", "vm_guid" => @vm1.guid, - "vm_id" => @vm1.id} + it { is_expected.to eq("#{metric_rollup.resource_id}_#{beginning_of_day}") } + end - expect("#{metric_rollup.resource_id}_#{timestamp_key}").to eq(extra_fields.first) - expect(extra_fields.second).to eq(expected_fields) + describe '#initialize' do + let(:report_options) { Chargeback::ReportOptions.new } + let(:vm_owners) { {@vm1.id => @vm1.evm_owner_name} } + let(:consumption) { Chargeback::ConsumptionWithRollups.new([metric_rollup], nil, nil) } + let(:shared_extra_fields) do + {'vm_name' => @vm1.name, 'owner_name' => @admin.name, 'vm_uid' => 'ems_ref', 'vm_guid' => @vm1.guid, + 'vm_id' => @vm1.id} end + subject { ChargebackVm.new(report_options, consumption).attributes } - let(:metric_rollup_without_ems) do - FactoryGirl.create(:metric_rollup_vm_hr, :timestamp => timestamp_key, :tag_names => "environment/prod", - :parent_host_id => @host1.id, :parent_ems_cluster_id => @ems_cluster.id, - :parent_storage_id => @storage.id, - :resource => @vm1, :resource_name => @vm1.name) + before do + ChargebackVm.instance_variable_set(:@vm_owners, vm_owners) end - it "return extra fields when parent ems is missing" do - ChargebackVm.instance_variable_set(:@vm_owners, vm_owners) + context 'with parent ems' do + let(:metric_rollup) do + FactoryGirl.build(:metric_rollup_vm_hr, :tag_names => 'environment/prod', + :parent_host_id => @host1.id, :parent_ems_cluster_id => @ems_cluster.id, + :parent_ems_id => @ems.id, :parent_storage_id => @storage.id, + :resource => @vm1, :resource_name => @vm1.name) + end - extra_fields = ChargebackVm.get_keys_and_extra_fields(metric_rollup_without_ems, timestamp_key) - expected_fields = {"vm_name" => @vm1.name, "owner_name" => @admin.name, "provider_name" => nil, - "provider_uid" => nil, "vm_uid" => "ems_ref", "vm_guid" => @vm1.guid, - "vm_id" => @vm1.id} + it 'sets extra fields' do + is_expected.to include(shared_extra_fields.merge('provider_name' => @ems.name, 'provider_uid' => @ems.guid)) + end + end - expect("#{metric_rollup.resource_id}_#{timestamp_key}").to eq(extra_fields.first) - expect(extra_fields.second).to eq(expected_fields) + context 'when parent ems is missing' do + let(:metric_rollup) do + FactoryGirl.build(:metric_rollup_vm_hr, :tag_names => 'environment/prod', + :parent_host_id => @host1.id, :parent_ems_cluster_id => @ems_cluster.id, + :parent_storage_id => @storage.id, + :resource => @vm1, :resource_name => @vm1.name) + end + + it 'sets extra fields when parent ems is missing' do + is_expected.to include(shared_extra_fields.merge('provider_name' => nil, 'provider_uid' => nil)) + end end end @@ -996,4 +937,38 @@ def used_average_for(metric, hours_in_interval) expect(subject.tag_name).to eq('Production') end end + + context 'for SCVMM (hyper-v)' do + let(:base_options) do + {:interval_size => 1, :end_interval_offset => 0, :tag => '/managed/environment/prod', + :ext_options => {:tz => 'UTC'}, :userid => @admin.userid} + end + let!(:vm1) do + vm = FactoryGirl.create(:vm_microsoft) + vm.tag_with(@tag.name, :ns => '*') + vm + end + let(:options) { base_options.merge(:interval => 'daily') } + let(:tier) do + FactoryGirl.create(:chargeback_tier, :start => 0, + :finish => Float::INFINITY, + :fixed_rate => @hourly_rate.to_s, + :variable_rate => 0.0) + end + let!(:rate_detail) do + FactoryGirl.create(:chargeback_rate_detail_fixed_compute_cost, + :chargeback_rate_id => @cbr.id, + :chargeback_tiers => [tier], + :per_time => 'hourly') + end + + subject { ChargebackVm.build_results_for_report_ChargebackVm(options).first.first } + + it 'works' do + expect(subject.chargeback_rates).to eq(@cbr.description) + expect(subject.fixed_compute_metric).to eq(1) # One day of fixed compute metric + expect(subject.fixed_compute_1_cost).to eq(@hourly_rate * 24) + expect(subject.total_cost).to eq(@hourly_rate * 24) + end + end end diff --git a/spec/models/metric_rollup/chargeback_helper_spec.rb b/spec/models/metric_rollup/chargeback_helper_spec.rb new file mode 100644 index 00000000000..6da6ad18af6 --- /dev/null +++ b/spec/models/metric_rollup/chargeback_helper_spec.rb @@ -0,0 +1,62 @@ +describe MetricRollup do + describe '#parents_determining_rate' do + before do + MiqRegion.seed + MiqEnterprise.seed + end + + context 'VmOrTemplate' do + let(:ems) { FactoryGirl.build(:ems_vmware) } + let(:ems_cluster) { FactoryGirl.build(:ems_cluster, :ext_management_system => ems) } + let(:storage) { FactoryGirl.build(:storage_target_vmware) } + let(:host) { FactoryGirl.build(:host) } + let(:vm) do + FactoryGirl.create(:vm_vmware, :name => 'test_vm', :ems_ref => 'ems_ref', + :ems_cluster => ems_cluster, :storage => storage, :host => host, + :ext_management_system => ems + ) + end + + subject { metric_rollup.parents_determining_rate } + + context 'metric_rollup record with parents not nil' do + let(:metric_rollup) do + FactoryGirl.build(:metric_rollup_vm_hr, + :resource => vm, + :parent_host => host, + :parent_ems_cluster => ems_cluster, + :parent_ems => ems, + :parent_storage => storage, + ) + end + + let(:parents_from_rollup) do + [ + metric_rollup.parent_host, + metric_rollup.parent_ems_cluster, + metric_rollup.parent_storage, + metric_rollup.parent_ems, + MiqEnterprise.my_enterprise + ] + end + + it { is_expected.to match_array(parents_from_rollup) } + end + + context 'metric_rollup record with parents nil' do + let(:metric_rollup) { FactoryGirl.build(:metric_rollup_vm_hr, :resource => vm) } + let(:parents_from_vm) do + [ + vm.host, + vm.ems_cluster, + vm.storage, + vm.ext_management_system, + MiqEnterprise.my_enterprise + ] + end + + it { is_expected.to match_array(parents_from_vm) } + end + end + end +end