Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EUWE] Chargebacks for SCVMM (rollup-less) #13419

Merged
merged 73 commits into from
Jan 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
fc542ae
Set @options only once
isimluk Oct 19, 2016
dfee861
Drop support from start_time and end_time options
isimluk Oct 19, 2016
c10ab4e
Drop chargeback_type option documentation
isimluk Oct 19, 2016
7196894
Use Struct for storing Chargeback calculation options
isimluk Oct 19, 2016
e4ee6ec
Refactor: Extract method: tz
isimluk Oct 20, 2016
67a678f
Default interval should be property of ReportOptions
isimluk Oct 20, 2016
9257725
Refactor: move and rename method: report_time_range
isimluk Oct 20, 2016
46ab551
Refactor: move and rename method: duration_of_report_step
isimluk Oct 20, 2016
6595272
Do not pass interval variable along, use @options
isimluk Oct 20, 2016
24a6c15
Refactor: Extract method: start_of_report_step
isimluk Oct 20, 2016
6b2d5c3
Refactor: Extract method: tag_hash
isimluk Oct 20, 2016
0ad8578
Refactor: Extract method: report_step_range
isimluk Oct 20, 2016
6968a20
Set default :ext_options in Chargeback::ReportOptions
isimluk Oct 20, 2016
19417a7
Be explicit, when creating the report results
isimluk Oct 19, 2016
004f017
Do not instanciate Chargeback at the end of calculation
isimluk Oct 19, 2016
cbf9471
Avoid extra merge, compose hash in situ
isimluk Oct 20, 2016
603b3db
Move calculate_costs method to instance
isimluk Oct 19, 2016
69a26e8
Make reportable_metric_and_cost_fields private instance method
isimluk Oct 19, 2016
a2a41d3
Calculate key/values and report them in a single method
isimluk Oct 19, 2016
4775bfa
Rename method to better reflect what it does
isimluk Oct 19, 2016
419d9b5
Rename cost to hourly_cost
isimluk Oct 25, 2016
ca349ae
Make a part of cb_rate_detail private
isimluk Oct 25, 2016
ec7fc49
Refactor: Extract method: metric_and_cost_by
isimluk Oct 27, 2016
7c2d029
Refactor: Extract methods to tell what the RateDetail affects
isimluk Nov 9, 2016
5845a82
Refactor: Extract method: Relevant fields
isimluk Nov 14, 2016
5ceb42e
Refactor: move charging logic from cb to rate
isimluk Nov 14, 2016
6acb055
Charge only, when it is relevant
isimluk Nov 14, 2016
21951df
Fix fixed-rates calculation when missing cb fields
isimluk Nov 14, 2016
aec4086
Refactor: Move parents_determining_rate to MetricRollup record
isimluk Nov 8, 2016
8d4db4d
Refactor: Extract rates cache to separate class
isimluk Nov 8, 2016
0e052fe
Refactor: Extract consumption to a separate class
isimluk Nov 22, 2016
92bca79
Collect & memoize consumption values only once
isimluk Nov 22, 2016
d056ce9
Instantiate consumption object early, before charging
isimluk Nov 22, 2016
d7173b9
Refactor: Extract variable to memoized method
isimluk Nov 22, 2016
e075f56
Consumption should tell how many hours are in the interval
isimluk Nov 22, 2016
9e756c6
Instantiate Consumption object as early as possible
isimluk Nov 22, 2016
318efe1
If we round to seconds we can be more accurate in fact
isimluk Nov 22, 2016
69df278
Move hours_in_interval method to Consumption
isimluk Nov 22, 2016
b7d4c2b
Refactor: Extract method: classification_for_perf
isimluk Nov 29, 2016
710004f
Split key and extra keys methods
isimluk Nov 29, 2016
ad1f186
Put Report options to chargeback instance
isimluk Nov 29, 2016
c9d8bd2
Expand class method to constructor
isimluk Nov 29, 2016
0c4460a
Drop premature optimization
isimluk Nov 29, 2016
8bd5f7b
Make vms class variable
isimluk Nov 29, 2016
08aa9b2
Use directly the @vms variable to determine applicable rollups
isimluk Nov 29, 2016
5da2a59
Refactor: Extract method: vm_owner
isimluk Nov 29, 2016
d5563de
Refactor: Extract method: vms
isimluk Nov 29, 2016
7ce131e
Refactor: Extract methods: project & image.
isimluk Nov 29, 2016
ddaca81
Refactor: Expand date_fields class method to constructor
isimluk Nov 29, 2016
a1e465f
Refactor: expand method: get_tag_fields
isimluk Nov 29, 2016
9b9f26b
Refactor: this class method should be private instance method
isimluk Nov 30, 2016
68c689c
Replace inject by map
lpichler Nov 16, 2016
3355fda
Move tag prefix method to Metric::ChargebackHelper
lpichler Nov 16, 2016
2d1a365
Add prefixes to on one place
lpichler Nov 16, 2016
0dd2f0d
Use kind_of? for checking type and remove unnecessary var
lpichler Nov 16, 2016
edcdcc8
Refactor: Extract method: base_rollup_scope
isimluk Dec 16, 2016
c92740e
Refactor: Extract method: for_report
isimluk Dec 16, 2016
08ed2d9
Refactor: Extract method: key()
isimluk Dec 16, 2016
30e1d71
Extract method: first_metric_rollup_record
isimluk Dec 16, 2016
f1db045
Do not pass metric_rollup to main loop of chargeback
isimluk Dec 16, 2016
2251ed1
RatesCache should operate over Consumption not metric_rollup
isimluk Dec 16, 2016
73351ff
Initialize chargeback instances with consumption
isimluk Dec 16, 2016
41fed0b
init_extra_fields should depend only on consumption object
isimluk Dec 16, 2016
558f335
report_row_key should take just consumption class
isimluk Dec 16, 2016
cacd635
classification_for should not depend on metric_rollup
isimluk Dec 16, 2016
e9fd562
default_key should not depend on metric_rollup
isimluk Dec 16, 2016
6153c73
RatesCaches should not depend on metric_rollups
isimluk Dec 16, 2016
d5bfb46
Make first_metric_rollup_private
isimluk Dec 16, 2016
456486a
Extract child class: ConsumptionWithRollups
isimluk Dec 16, 2016
ab596a1
Chargebacks for Hyper-V without rollups
isimluk Dec 16, 2016
215a225
Spec for Hyper-V fixed charges.
isimluk Dec 19, 2016
57e4f1c
Charge allocation of hyperv instances without rollups
isimluk Dec 20, 2016
44aa15b
Unit test for ConsumptionWithoutRollups
isimluk Jan 2, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 47 additions & 233 deletions app/models/chargeback.rb
Original file line number Diff line number Diff line change
@@ -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 : _('<Empty>') }
[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 : _('<Empty>')
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)
Expand All @@ -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)

Expand Down Expand Up @@ -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
11 changes: 11 additions & 0 deletions app/models/chargeback/consumption.rb
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions app/models/chargeback/consumption_history.rb
Original file line number Diff line number Diff line change
@@ -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
Loading