cloud_financial_officer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,102 @@
1
+ module CFO
2
+ # Encapsulates sevral algorithms for for changing one set of BillingRecords
3
+ # into another.
4
+ module RecordGrouper
5
+ include CloudCostTracker::Billing
6
+
7
+ # Maps the grouping Strings to BillingRecord attributes.
8
+ # Any strings not found as keys in this Hash are interpreted as
9
+ # BillingCode keys.
10
+ ATTR_4_GROUPING = {
11
+ 'account' => :account, 'service' => :service, 'provider' => :provider,
12
+ 'type' => :resource_type, 'resource_id' => :resource_id,
13
+ 'billing_type' => :billing_type,
14
+ }
15
+
16
+ # Defines the value put assigned to composite record's attibute when all
17
+ # its commponent records don't share the same value.
18
+ MULTIPLE_VALUES_VALUE = '*'
19
+
20
+ # Returns a set of "composite" BillingRecords based on an Array of groupings.
21
+ # If a grouping is one of ['account', 'provider', 'service', 'resource_id',
22
+ # 'resource_type', 'billing_type'], it is evaluated as a Record's attribute.
23
+ # Otherwise, its treated as a BillingCode key, and evaluated to it's value.
24
+ # @param [Array<BillingRecord>] input_records the records to group together
25
+ # @param [Array<String>] groupings the paramters to group records by
26
+ # @return [Array<BillingRecord>] the composite BillingRecords
27
+ def self.by(groupings, input_records)
28
+ # index the results by a "composite key", made by evaluating the groupings
29
+ results = Hash.new
30
+ input_records.each do |record|
31
+ # create the composite key for this record
32
+ comp_key = composite_key_for(record, groupings)
33
+ # if there is already composite record for this key,
34
+ if comp_record = results[comp_key]
35
+ # merge this record's data into the composite record
36
+ results[comp_key] = combine_records(comp_record, record)
37
+ else # otherwise, create a new composite record
38
+ # index the record into the results with the composite key
39
+ results[comp_key] = record
40
+ end
41
+ end
42
+ # Now output the results in composite key order
43
+ results.keys.sort.map {|comp_key| results[comp_key]}
44
+ end
45
+
46
+ private
47
+
48
+ # Generates a composite key for a given record based on a list of groupings
49
+ def self.composite_key_for(record, groupings)
50
+ groupings.inject("") do |comp_key, grouping|
51
+ if ATTR_4_GROUPING[grouping] # this grouping represents an attribute
52
+ comp_key += record.send(ATTR_4_GROUPING[grouping])
53
+ else # this grouping is a BillingCode key -- search for the BillingCode
54
+ new_partial_key = '*UNDEFINED_OR_EMPTY*'
55
+ if code = record.billing_codes.find {|c| c.key == grouping}
56
+ new_partial_key = code.value if (code.value && (code.value != ''))
57
+ end
58
+ comp_key += new_partial_key
59
+ end
60
+ end
61
+ end
62
+
63
+ # Merges some information from record over a composite record comp_record,
64
+ # based on the provided groupings.
65
+ def self.combine_records(comp_record, record)
66
+ comp_record.total_cost += record.total_cost
67
+ comp_record.start_time = [comp_record.start_time, record.start_time].min
68
+ comp_record.stop_time = [comp_record.stop_time, record.stop_time].max
69
+ # recalculate the hourly rate for the composite record
70
+ duration = comp_record.stop_time - comp_record.start_time
71
+ comp_record.cost_per_hour = duration != 0 ?
72
+ comp_record.total_cost * SECONDS_PER_HOUR / duration : 0
73
+ comp_record.billing_codes =
74
+ (comp_record.billing_codes + record.billing_codes).uniq
75
+ ATTR_4_GROUPING.values.each do |attribute|
76
+ existing_value = comp_record.send(attribute)
77
+ new_record_value = record.send(attribute)
78
+ if existing_value != new_record_value
79
+ comp_record.send("#{attribute.to_s}=", MULTIPLE_VALUES_VALUE)
80
+ end
81
+ end
82
+ comp_record
83
+ end
84
+
85
+ # Creates a new composite BillingRecord with only the attributes matching the
86
+ # grouping fields.
87
+ def self.create_composite_record(record, groupings)
88
+ record_attribs = groupings.inject({}) do |attrs, grouping|
89
+ if ATTR_4_GROUPING[grouping] # this grouping String represents an attribute
90
+ attrs[ATTR_4_GROUPING[grouping]] = record.send(ATTR_4_GROUPING[grouping])
91
+ end
92
+ attrs
93
+ end
94
+ BillingRecord.new(record_attribs.merge({
95
+ :total_cost => record.total_cost, :cost_per_hour => record.cost_per_hour,
96
+ :start_time => record.start_time, :stop_time => record.stop_time,
97
+ :billing_codes => record.billing_codes
98
+ }))
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,53 @@
1
+ module CFO
2
+ # Represents an account for a specific service with a cloud provider.
3
+ class Account
4
+ # A human-readable ID for the account - best to make this URL-safe.
5
+ attr_accessor :name
6
+
7
+ # The (Fog) name of the cloud computing service provider.
8
+ attr_accessor :provider
9
+
10
+ # The (Fog) name of the cloud computing service.
11
+ attr_accessor :service
12
+
13
+ # An Array of String pairs representing all the BillingCodes associated
14
+ # with all this Acccount's BillingRecords.
15
+ attr_accessor :billing_codes
16
+
17
+ # Creates a object representing a REST Resource for an account.
18
+ # @param attributes [Hash] - :name, :provider, :service, or :billing_codes
19
+ def initialize(attributes = {})
20
+ attributes.each {|k,v| self.send("#{k}=",v) if self.respond_to?("#{k}=")}
21
+ end
22
+
23
+ # @return [Hash] a RESTful Hash representation of this object.
24
+ # @param [Booolean] verbose whether to include biling codes and resources.
25
+ def to_hash(verbose = true)
26
+ extra_info = {
27
+ 'resources' => CloudResource.hash_list(:account => @name),
28
+ 'billing_codes' => @billing_codes,
29
+ }
30
+ {
31
+ 'id' => @name,
32
+ 'service' => @service,
33
+ 'provider' => @provider,
34
+ 'url' => uri,
35
+ 'report_url' => report_uri,
36
+ }.merge(verbose ? extra_info : {})
37
+ end
38
+
39
+ # @return [String] a URI path to this object's JSON representation
40
+ def uri
41
+ "#{CFO.root}/accounts/#{URI.escape(@name)}"
42
+ end
43
+
44
+ # @return [String] a URI path to this object's cost report.
45
+ def report_uri
46
+ "#{CFO.root}/report/for_account/#{URI.escape(@name)}"
47
+ end
48
+
49
+ # @return [String] a RESTful JSON representation of this object.
50
+ def to_json ; to_hash.to_json end
51
+
52
+ end
53
+ end
@@ -0,0 +1,74 @@
1
+ module CFO
2
+ # Represents a billable asset on a cloud computing service.
3
+ class CloudResource
4
+ include CloudCostTracker::Billing
5
+
6
+ # A CFO::Account object.
7
+ attr_accessor :account
8
+
9
+ # The resource type, unique this account.
10
+ attr_accessor :resource_type
11
+
12
+ # The resource ID, unique to this account and resource type.
13
+ attr_accessor :resource_id
14
+
15
+ # An Array of String pairs representing all the BillingCodes associated
16
+ # with all this Resource's BillingRecords.
17
+ attr_accessor :billing_codes
18
+
19
+ # Creates a object representing a REST Resource for a billable resource.
20
+ # @param attributes [Hash] - :account, :resource_type,
21
+ # :resource_id, or :billing_codes
22
+ def initialize(attributes = {})
23
+ attributes.each {|k,v| self.send("#{k}=",v) if self.respond_to?("#{k}=")}
24
+ end
25
+
26
+ # @return [Hash] a RESTful Hash representation of this object.
27
+ # @param [Booolean] verbose whether to include a full representation.
28
+ def to_hash(verbose = true)
29
+ extra_data = {
30
+ 'account' => {
31
+ 'id' => @account.name,
32
+ 'service' => @account.service,
33
+ 'provider' => @account.provider,
34
+ 'url' => @account.uri,
35
+ },
36
+ 'report_url' => report_uri,
37
+ 'billing_codes' => @billing_codes,
38
+ }
39
+ { # Return the resource Hash, perhaps with the account merged in
40
+ 'resource_id' => @resource_id,
41
+ 'resource_type' => @resource_type,
42
+ 'url' => uri,
43
+ }.merge(verbose ? extra_data : {})
44
+ end
45
+
46
+ # @return [String] a URI path to this object's JSON representation
47
+ def uri
48
+ "#{CFO.root}/accounts/#{URI.escape(@account.name)}/"+
49
+ "#{URI.escape(@resource_type)}/#{URI.escape(@resource_id)}"
50
+ end
51
+
52
+ # @return [String] a URI path to this object's cost report.
53
+ def report_uri
54
+ "#{CFO.root}/report/for_account/#{URI.escape(@account.name)}/for_type/"+
55
+ "#{URI.escape(@resource_type)}/for_resource/#{URI.escape(@resource_id)}"
56
+ end
57
+
58
+ # @return [String] a RESTful JSON representation of this object.
59
+ def to_json ; to_hash.to_json end
60
+
61
+ # @return [Array<Resource>] a 'links-only' list of matcing Resources,
62
+ # loaded from the database.
63
+ def self.hash_list(conditions = {})
64
+ BillingRecord.select('*').group(:resource_id).where(conditions).
65
+ collect do |rec|
66
+ CloudResource.new(
67
+ rec.attributes.merge('account' =>
68
+ CFO::Account.new(rec.attributes.merge(:name => rec.account)))
69
+ ).to_hash(false) # Don't include the full account info
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,147 @@
1
+ module CFO
2
+ # Represents a JSON report, based on an Array of (Hash) constraints.
3
+ class Report
4
+ include CloudCostTracker::Billing
5
+
6
+ # Creates a object representing a REST Resource for an account.
7
+ # @param [Array<Hash>] constraint_array a set of Hash constraints on the
8
+ # BillingRecords to be retrieved.
9
+ # Each hash key is a type of constraint on the type of BillingRecord
10
+ # information that is returned. The value is an array of Strings, which
11
+ # are generally just text to search for in some BillingRecord column, but
12
+ # are interpreted differently depending on the type of constraint.
13
+ # @param [Array<String>] groupings a set of Strings for grouping the results.
14
+ # Strings that match BillingRecord attributes will group all records that
15
+ # share the same value for that attribute into one record.
16
+ # Other Strings will can match BillingCode keys, for grouping by Code.
17
+ # @param [Hash] options optional additional parameters:
18
+ # - :logger - a Ruby Logger-compatible object
19
+ def initialize(constraint_array = [], groupings = [], options = {})
20
+ @constraints = constraint_array
21
+ @groupings = groupings
22
+ @log = options[:logger] || FogTracker.default_logger(nil)
23
+ end
24
+
25
+ # @return [Hash] a Hash representation of the desired cost report.
26
+ def to_hash
27
+ update_records # Update @records from the database
28
+ update_summary_attributes # Update summary attributes
29
+ {
30
+ 'start_time' => @start_time,
31
+ 'stop_time' => @stop_time,
32
+ 'total_cost' => @total_cost,
33
+ 'cost_per_hour' => @hourly_rate.to_f,
34
+ 'billing_codes' => @bill_codes,
35
+ 'billing_records' => @records.map do |r|
36
+ r.to_hash.merge(
37
+ :cost_per_hour => r.cost_per_hour.to_f,
38
+ :total_cost => r.total_cost.to_f,
39
+ )
40
+ end,
41
+ }
42
+ end
43
+
44
+ # @return [String] a CSV representation of the desired cost report.
45
+ def to_csv
46
+ update_records # Update @records from the database
47
+ code_count = @records.inject({}) do |count, record|
48
+ record.billing_codes.each do |code|
49
+ count[code.key] = count[code.key] ? count[code.key] + 1 : 1
50
+ end
51
+ count
52
+ end
53
+ sorted_codes = code_count.keys.sort {|a,b| code_count[b] <=> code_count[a]}
54
+ headers = {
55
+ :start_time => 'Start Time',
56
+ :stop_time => 'Stop Time',
57
+ :account => 'Account Name',
58
+ :provider => 'Provder',
59
+ :service => 'Service',
60
+ :billing_type => 'Billing Type',
61
+ :total_cost => 'Total Cost',
62
+ :cost_per_hour => 'Hourly Rate',
63
+ :resource_id => 'Resource ID',
64
+ :resource_type => 'Resource Type',
65
+ }
66
+ @log.info "Using headers: #{headers.values + sorted_codes}"
67
+ csv = ((headers.values + sorted_codes).map{|h| "\"#{h}\""}).join(',') +"\n"
68
+ @records.each do |record|
69
+ record_data = headers.keys.map {|h| record.send h}
70
+ record_data += sorted_codes.map do |code|
71
+ billing_codes = record.billing_codes.select {|c| c.key == code}
72
+ case billing_codes.count
73
+ when 0 then ''
74
+ when 1 then billing_codes.first.value
75
+ else CFO::RecordGrouper::MULTIPLE_VALUES_VALUE
76
+ end
77
+ end
78
+ csv += (record_data.map{|value| "\"#{value}\""}).join(',') +"\n"
79
+ end
80
+ csv
81
+ end
82
+
83
+ # @return [String] a JSON report on the desired costs.
84
+ def to_json ; to_hash.to_json end
85
+
86
+ # Sets a constraint based on resource IDs.
87
+ # @param [Array<String>] resource_ids the values to match in the records.
88
+ def for_resource(resource_ids) ; {:resource_id => resource_ids} end
89
+
90
+ # Sets a constraint based on resource type.
91
+ # @param [Array<String>] resource_types the values to match in the records.
92
+ def for_type(resource_types) ; {:resource_type => resource_types} end
93
+
94
+ # Sets a constraint based on account name.
95
+ # @param [Array<String>] account_names the values to match in the records.
96
+ def for_account(account_names) ; {:account => account_names} end
97
+
98
+ # Sets a constraint based on provider name.
99
+ # @param [Array<String>] providers the values to match in the records.
100
+ def for_provider(providers) ; {:provider => providers} end
101
+
102
+ # Sets a constraint based on cloud computing service name.
103
+ # @param [Array<String>] services the values to match in the records.
104
+ def for_service(services) ; {:service => services} end
105
+
106
+ # Sets a constraint based on billing type.
107
+ # @param [Array<String>] billing_types the values to match in the records
108
+ def for_billing_type(billing_types) ; {:billing_type => billing_types} end
109
+
110
+ private
111
+
112
+ # Updates @records from the database.
113
+ def update_records
114
+ conditions = Hash.new
115
+ @constraints.each do |constraint_hash|
116
+ constraint_hash.each do |constraint_key, values|
117
+ if self.respond_to? constraint_key
118
+ conditions.merge!(self.send(constraint_key, values))
119
+ end
120
+ end
121
+ end
122
+ # Query for BillingRecords, but freeze the resulting records
123
+ @records = BillingRecord.find(
124
+ :all, :conditions => conditions, :readonly => true
125
+ )
126
+ @log.debug "Got #{@records.count} records for constraints #{conditions}."
127
+ @records = RecordFilter.by_time(@records, @constraints)
128
+ @records = RecordFilter.by_code(@records, @constraints)
129
+ @records = RecordGrouper.by(@groupings, @records) if not @groupings.empty?
130
+ end
131
+
132
+ # Updates summary attributes for time range, cost and billing codes.
133
+ def update_summary_attributes
134
+ @start_time = (@records.map{|r| r.start_time}).sort.first
135
+ @stop_time = (@records.map{|r| r.stop_time}).sort.last
136
+ @total_cost = @records.inject(0.0) {|total, r| total += r.total_cost}
137
+ duration = (@start_time && @stop_time) ? (
138
+ Time.parse(@stop_time.to_s) - Time.parse(@start_time.to_s)
139
+ ) : 0
140
+ @hourly_rate = duration != 0 ? @total_cost * SECONDS_PER_HOUR / duration : 0
141
+ @bill_codes = (@records.map {|r| r.billing_codes}).flatten.uniq.map do |c|
142
+ [c.key, c.value]
143
+ end
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env ruby
2
+ require File.join(File.dirname(__FILE__), '..', 'cloud_financial_officer')
3
+ require 'sinatra/base'
4
+ CFO.force_timezone # This does nothing by default - modify it if you need it
5
+
6
+ module CFO
7
+ # A RESTful web service for reporting on a CloudCostTracker database.
8
+ class Service < Sinatra::Base
9
+ include ::CloudCostTracker::Billing
10
+
11
+ # Load account information
12
+ configure do ; set :accounts, CFO.read_accounts end
13
+
14
+ configure :development do
15
+ require 'sinatra/reloader'
16
+ register Sinatra::Reloader
17
+ end
18
+ configure :production, :development do
19
+ enable :logging
20
+ ActiveRecord::Base.logger ||= FogTracker.default_logger(STDOUT)
21
+ #ActiveRecord::Base.logger.level = ::Logger::DEBUG
22
+ end
23
+ before do
24
+ if not ActiveRecord::Base.connected? # Connect to the database as needed
25
+ logger.info "Using database #{(CFO.connect_to_database)[:database]}..."
26
+ end
27
+ logger.info "Got params #{params}"
28
+ if params['content-type'] && (params['content-type'] =~ %r{(text/)?csv})
29
+ content_type 'text/csv' # serve only JSON by default
30
+ else
31
+ content_type 'application/json' # serve only JSON by default
32
+ end
33
+ end
34
+
35
+
36
+ ######## ACCOUNTS ########
37
+
38
+ # Returns a list of accounts from / or /accounts.
39
+ get %r{^#{CFO.root}(/accounts)?/?$} do
40
+ (settings.accounts.map{|account| account.to_hash(false)}).to_json
41
+ end
42
+
43
+ # Returns an individial account.
44
+ get "#{CFO.root}/accounts/:account_name" do
45
+ if account = settings.accounts.find {|a| a.name == params[:account_name]}
46
+ account.billing_codes = BillingCode.joins(:billing_records).where(
47
+ :billing_records => {:account => params[:account_name]}
48
+ ).uniq.map {|c| [c.key, c.value]}
49
+ account.to_json
50
+ else
51
+ [404, {'error' => "Account #{params[:account_name]} not found"}.to_json]
52
+ end
53
+ end
54
+
55
+ ######## RESOURCES ########
56
+
57
+ # Returns a list of CloudResources.
58
+ get %r{^#{CFO.root}/resources/?$} do
59
+ CFO::CloudResource.hash_list.to_json
60
+ end
61
+
62
+ # Returns an individial CloudResource.
63
+ get "#{CFO.root}/accounts/:account_name/:type/:resource_id" do
64
+ all_records = BillingRecord.where(
65
+ :account => params[:account_name],
66
+ :resource_type => params[:type],
67
+ :resource_id => params[:resource_id],
68
+ )
69
+ if record = all_records.first
70
+ codes = BillingCode.joins(:billing_records).where(
71
+ :billing_records => {:resource_id => params[:resource_id]}
72
+ )
73
+ CFO::CloudResource.new(
74
+ :resource_id => record.resource_id,
75
+ :resource_type => record.resource_type,
76
+ :account => CFO::Account.new(
77
+ :name => record.account,
78
+ :service => record.service,
79
+ :provider => record.provider,
80
+ ),
81
+ :billing_codes => codes.map {|c| [c.key, c.value]}
82
+ ).to_json
83
+ else
84
+ [404, {'error' => "Resource #{params[:resource_id]}"+
85
+ " in account #{params[:account_name]} not found"}.to_json
86
+ ]
87
+ end
88
+ end
89
+
90
+ ######## REPORTS ########
91
+
92
+ # Returns a cost report based on constraints in the request path.
93
+ get %r{^#{CFO.root}/report.*} do
94
+ constraints = constraints_from_path(request.path_info)
95
+ logger.info "Generating report for #{constraints.inspect}."
96
+ groupings = groupings_from_path(request.path_info)
97
+ logger.info "Using groupings #{groupings.inspect}."
98
+ report = CFO::Report.new(constraints, groupings, :logger => logger)
99
+ if params['content-type'] && (params['content-type'] =~ %r{(text/)?csv})
100
+ report_file = "report_"+
101
+ ((constraints.map {|c| c.flatten.flatten}).flatten).join('_')+
102
+ ".#{Time.now.strftime('%Y-%m-%d-%H%M%S')}.csv"
103
+ response['Content-Disposition'] = "attachment; filename=#{report_file}"
104
+ report.to_csv
105
+ else
106
+ report.to_json
107
+ end
108
+ end
109
+
110
+ CONSTRAINT_PATTERN = /(for_\w+)/
111
+ GROUPING_PATTERN = /by_(\w+)/
112
+
113
+ private
114
+
115
+ # Returns a Hash of report constraints (see Report.new) from a path String
116
+ def constraints_from_path(path)
117
+ constraints = []
118
+ tokens = (path.split('/')).drop(2)
119
+ while not tokens.empty?
120
+ if key_match = tokens.first.match(CONSTRAINT_PATTERN)
121
+ constraint_key, constraint_values = key_match[1], Array.new
122
+ constraint_values = tokens.drop(1).take_while do |t|
123
+ (t !~ CONSTRAINT_PATTERN) && (t !~ GROUPING_PATTERN)
124
+ end
125
+ constraints << {constraint_key => constraint_values}
126
+ tokens = tokens.drop(constraint_values.size + 1)
127
+ else
128
+ tokens = tokens.drop 1
129
+ end
130
+ end
131
+ constraints
132
+ end
133
+
134
+ # Returns a Hash of report constraints (see Report.new) from a path String
135
+ def groupings_from_path(path)
136
+ tokens = (path.split('/')).drop(2)
137
+ tokens.inject([]) do |groupings, token|
138
+ if not token.scan(GROUPING_PATTERN).empty?
139
+ groupings << token.scan(GROUPING_PATTERN).first.first
140
+ end
141
+ groupings
142
+ end
143
+ end
144
+
145
+ # start the server if ruby file executed directly
146
+ run! if app_file == $0
147
+ end
148
+ end
@@ -0,0 +1,7 @@
1
+ module CFO
2
+ # The version of this CFO Gem / Library.
3
+ VERSION = "0.1.0"
4
+
5
+ # This library's version of the CFO REST API.
6
+ API_VERSION = "1"
7
+ end
@@ -0,0 +1,8 @@
1
+ %w{rubygems bundler}.each {|lib| require lib}
2
+ require File.join(File.dirname(__FILE__), "cfo/cfo")
3
+ Bundler.require(:default, CFO.env.to_sym)
4
+ Bundler.require(:development) if CFO.env == 'test'
5
+ require 'cloud_cost_tracker'
6
+
7
+ # Load all ruby files from 'cfo' directory
8
+ Dir[File.join(File.dirname(__FILE__), "cfo/**/*.rb")].each {|f| require f}
@@ -0,0 +1,6 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ desc "Runs all tests"
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.rspec_opts = ['--color', '--format documentation', '-r ./spec/spec_helper.rb']
6
+ end
@@ -0,0 +1,5 @@
1
+ desc "Runs the service"
2
+ task :service do
3
+ require './lib/cfo/service'
4
+ CFO::Service.run!
5
+ end
data/spec/factories.rb ADDED
@@ -0,0 +1,35 @@
1
+ ACCOUNT_NAME = "fake-account-name"
2
+ SERVICE_NAME = "fake-service"
3
+ PROVIDER_NAME = "fake-provider"
4
+ RESOURCE_ID = "fake-resource-ID"
5
+ RESOURCE_TYPE = "fake-resource-type"
6
+ BILLING_TYPE = "fake-billing-type"
7
+ RECORD_TOTAL = 1.0
8
+ RECORD_HOURLY = 60.0
9
+ RECORD_DURATION = 60
10
+
11
+ FactoryGirl.define do
12
+ factory :account, :class => CFO::Account do
13
+ name ACCOUNT_NAME
14
+ service SERVICE_NAME
15
+ provider PROVIDER_NAME
16
+ end
17
+
18
+ factory :resource, :class => CFO::CloudResource do
19
+ resource_id RESOURCE_ID
20
+ resource_type RESOURCE_TYPE
21
+ end
22
+
23
+ factory :record, :class => CloudCostTracker::Billing::BillingRecord do
24
+ provider PROVIDER_NAME
25
+ service SERVICE_NAME
26
+ account ACCOUNT_NAME
27
+ resource_id RESOURCE_ID
28
+ resource_type RESOURCE_TYPE
29
+ billing_type BILLING_TYPE
30
+ start_time Time.now - RECORD_DURATION
31
+ stop_time Time.now
32
+ cost_per_hour RECORD_HOURLY
33
+ total_cost RECORD_TOTAL
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ module CFO
2
+ include CloudCostTracker::Billing
3
+
4
+ describe RecordFilter do
5
+
6
+ # Number of BillingRecords to report on
7
+ NUM_RECORDS = 50 # Each has its own unique provider, service, etc.
8
+
9
+ before(:all) do
10
+ CloudCostTracker::Billing::BillingRecord.delete_all
11
+ @billing_start = Time.now
12
+ # Create many BillingRecords, each with its own unique attributes
13
+ (1..NUM_RECORDS).each do |i|
14
+ create(:record, {
15
+ :provider => "#{PROVIDER_NAME}#{i}",
16
+ :service => "#{SERVICE_NAME}#{i}",
17
+ :account => "#{ACCOUNT_NAME}#{i}",
18
+ :resource_id => "#{RESOURCE_ID}#{i}",
19
+ :resource_type => "#{RESOURCE_TYPE}#{i}",
20
+ :billing_type => "#{BILLING_TYPE}#{i}",
21
+ })
22
+ end
23
+ @billing_stop = Time.now + 1
24
+ end
25
+
26
+ after(:all) do
27
+ BillingRecord.delete_all
28
+ end
29
+
30
+ describe '#truncate_by_time' do
31
+ it "limits all records' start and stop times to within the given range" do
32
+ # make sure some records are out-of the start and stop range
33
+ first_record = BillingRecord.first
34
+ last_record = BillingRecord.last
35
+ first_record.start_time = @billing_start - SECONDS_PER_HOUR
36
+ last_record.stop_time = @billing_stop + SECONDS_PER_HOUR
37
+ [first_record, last_record].each {|r| r.save!}
38
+ # Now filter the records by time
39
+ results = RecordFilter.truncate_by_time(BillingRecord.all,
40
+ @billing_start, @billing_stop)
41
+ results.each do |truncated_record|
42
+ truncated_record.start_time.should >= @billing_start
43
+ truncated_record.stop_time.should <= @billing_stop
44
+ end
45
+ end
46
+ end
47
+
48
+ end
49
+ end