cloud_cost_tracker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +16 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/Guardfile +10 -0
- data/LICENSE.TXT +10 -0
- data/README.md +123 -0
- data/Rakefile +15 -0
- data/billing issues.md +4 -0
- data/bin/cloud_cost_tracker +94 -0
- data/cloud_cost_tracker.gemspec +36 -0
- data/config/accounts.example.yml +45 -0
- data/config/billing/compute-aws-servers/us-east-on_demand.yml +44 -0
- data/config/billing/compute-aws-snapshots.yml +5 -0
- data/config/billing/compute-aws-volumes.yml +5 -0
- data/config/billing/elasticache-aws.yml +15 -0
- data/config/billing/rds-aws-servers/us-east-on_demand-mysql.yml +26 -0
- data/config/billing/rds-aws-servers/us-east-on_demand-storage.yml +10 -0
- data/config/billing/storage-aws-directories.yml +5 -0
- data/config/database.example.yml +18 -0
- data/db/migrate/20120118000000_create_billing_records.rb +22 -0
- data/db/migrate/20120119000000_create_billing_codes.rb +20 -0
- data/lib/cloud_cost_tracker/billing/account_billing_policy.rb +106 -0
- data/lib/cloud_cost_tracker/billing/compute/aws/servers.rb +30 -0
- data/lib/cloud_cost_tracker/billing/compute/aws/snapshots.rb +43 -0
- data/lib/cloud_cost_tracker/billing/compute/aws/volumes.rb +30 -0
- data/lib/cloud_cost_tracker/billing/elasticache/clusters.rb +33 -0
- data/lib/cloud_cost_tracker/billing/rds/server_storage.rb +30 -0
- data/lib/cloud_cost_tracker/billing/rds/servers.rb +31 -0
- data/lib/cloud_cost_tracker/billing/resource_billing_policy.rb +119 -0
- data/lib/cloud_cost_tracker/billing/storage/aws/directories.rb +47 -0
- data/lib/cloud_cost_tracker/coding/account_coding_policy.rb +73 -0
- data/lib/cloud_cost_tracker/coding/compute/aws/account_coding_policy.rb +38 -0
- data/lib/cloud_cost_tracker/coding/compute/aws/servers.rb +26 -0
- data/lib/cloud_cost_tracker/coding/compute/aws/volumes.rb +26 -0
- data/lib/cloud_cost_tracker/coding/rds/account_coding_policy.rb +25 -0
- data/lib/cloud_cost_tracker/coding/rds/servers.rb +59 -0
- data/lib/cloud_cost_tracker/coding/resource_coding_policy.rb +25 -0
- data/lib/cloud_cost_tracker/extensions/fog_model.rb +41 -0
- data/lib/cloud_cost_tracker/models/billing_code.rb +14 -0
- data/lib/cloud_cost_tracker/models/billing_record.rb +88 -0
- data/lib/cloud_cost_tracker/tasks.rb +3 -0
- data/lib/cloud_cost_tracker/tracker.rb +86 -0
- data/lib/cloud_cost_tracker/util/module_instrospection.rb +13 -0
- data/lib/cloud_cost_tracker/version.rb +3 -0
- data/lib/cloud_cost_tracker.rb +113 -0
- data/lib/tasks/database.rake +25 -0
- data/lib/tasks/rspec.rake +6 -0
- data/lib/tasks/track.rake +19 -0
- data/spec/lib/cloud_cost_tracker/billing/account_billing_policy_spec.rb +33 -0
- data/spec/lib/cloud_cost_tracker/billing/resource_billing_policy_spec.rb +102 -0
- data/spec/lib/cloud_cost_tracker/cloud_cost_tracker_spec.rb +57 -0
- data/spec/lib/cloud_cost_tracker/coding/account_coding_policy_spec.rb +32 -0
- data/spec/lib/cloud_cost_tracker/coding/resource_coding_policy_spec.rb +23 -0
- data/spec/lib/cloud_cost_tracker/models/billing_code_spec.rb +35 -0
- data/spec/lib/cloud_cost_tracker/models/billing_record_spec.rb +206 -0
- data/spec/lib/cloud_cost_tracker/tracker_spec.rb +37 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/support/_configure_logging.rb +6 -0
- data/writing-billing-policies.md +8 -0
- data/writing-coding-policies.md +7 -0
- metadata +224 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Extensions
|
3
|
+
# Adds convenience methods to Fog::Model instances for tracking billing info
|
4
|
+
module FogModel
|
5
|
+
|
6
|
+
# Adds a billing code to this cloud computing resource
|
7
|
+
# (Has no effect if the identical key is alreay set)
|
8
|
+
# @param [String] key the key for the desired billing code
|
9
|
+
# @param [String] value the value for the desired billing code
|
10
|
+
def code(key, value)
|
11
|
+
@_billng_codes ||= Array.new
|
12
|
+
if not @_billng_codes.include?([key, value])
|
13
|
+
@_billng_codes.push [key, value]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Adds a billing code to this cloud computing resource
|
18
|
+
# (Has no effect if the identical key is alreay set)
|
19
|
+
# @param [String] key the key for the desired billing code
|
20
|
+
# @param [String] value the value for the desired billing code
|
21
|
+
def remove_code(key, value)
|
22
|
+
@_billng_codes ||= Array.new
|
23
|
+
@_billng_codes.delete [key, value]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns this resource's billing codes
|
27
|
+
# @return [Array [<String>, <String>]]
|
28
|
+
def billing_codes
|
29
|
+
@_billng_codes ||= Array.new
|
30
|
+
@_billng_codes
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module Fog
|
38
|
+
class Model
|
39
|
+
include CloudCostTracker::Extensions::FogModel
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
class BillingCode < ActiveRecord::Base
|
4
|
+
|
5
|
+
has_and_belongs_to_many :billing_records,
|
6
|
+
:join_table => 'billing_records_codes'
|
7
|
+
|
8
|
+
# Validations
|
9
|
+
validates :key, :presence => true
|
10
|
+
#validates :value, :uniqueness => {:scope => :key}
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
class BillingRecord < ActiveRecord::Base
|
4
|
+
|
5
|
+
has_and_belongs_to_many :billing_codes,
|
6
|
+
:join_table => 'billing_records_codes'
|
7
|
+
|
8
|
+
# Validations
|
9
|
+
validates_presence_of :provider, :service, :account,
|
10
|
+
:resource_id, :resource_type, :billing_type,
|
11
|
+
:start_time, :stop_time, :cost_per_hour, :total_cost
|
12
|
+
validates_associated :billing_codes
|
13
|
+
|
14
|
+
# Returns the most recent BillingRecord for the same resource as
|
15
|
+
# the billing_record, or nil of there are none.
|
16
|
+
# Note that if billing_record is already in the database, this
|
17
|
+
# method can return billing_record itself!
|
18
|
+
# @param [BillingRecord] other_billing_record the record to match
|
19
|
+
# @return [BillingRecord,nil] the latest "matching" BillingRecord
|
20
|
+
def self.most_recent_like(billing_record)
|
21
|
+
results = BillingRecord.where(
|
22
|
+
:provider => billing_record.provider,
|
23
|
+
:service => billing_record.service,
|
24
|
+
:account => billing_record.account,
|
25
|
+
:resource_id => billing_record.resource_id,
|
26
|
+
:resource_type => billing_record.resource_type,
|
27
|
+
:billing_type => billing_record.billing_type,
|
28
|
+
).order(:stop_time).reverse_order.limit(1)
|
29
|
+
results.empty? ? nil : results.first
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns true if this BillingRecord's duration overlaps (inclusively)
|
33
|
+
# with that of other_billing_record. The minimum amount of time that can be
|
34
|
+
# between the records and still allow them to "overlap" can be increased
|
35
|
+
def overlaps_with(other_billing_record, min_proximity = 0)
|
36
|
+
return true if start_time == other_billing_record.start_time
|
37
|
+
first, second = nil, nil # Which record started first?
|
38
|
+
if start_time < other_billing_record.start_time
|
39
|
+
first, second = self, other_billing_record
|
40
|
+
else
|
41
|
+
first, second = other_billing_record, self
|
42
|
+
end
|
43
|
+
second.start_time - first.stop_time <= min_proximity
|
44
|
+
end
|
45
|
+
|
46
|
+
# Changes the stop time of this BillingRecord to be that of the
|
47
|
+
# other_billing_record, then recalculates the total
|
48
|
+
def merge_with(other_billing_record)
|
49
|
+
self.stop_time = other_billing_record.stop_time
|
50
|
+
total = ((stop_time - start_time) * cost_per_hour) / 3600
|
51
|
+
self.total_cost = total
|
52
|
+
ActiveRecord::Base.connection_pool.with_connection {save!}
|
53
|
+
end
|
54
|
+
|
55
|
+
# Creates BillngCodes as necessary from codes, an Array of String pairs,
|
56
|
+
# and associates this BillingRecord with those BillingCodes.
|
57
|
+
# @param [Array <Array <String>>] codes the String pairs to use as codes
|
58
|
+
def set_codes(codes)
|
59
|
+
codes.each do |key, value|
|
60
|
+
self.billing_codes << ( # Find the matching BillingCode or make a new one
|
61
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
62
|
+
BillingCode.where(:key => key, :value => value).first_or_create!
|
63
|
+
end
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Hash] a Hash representation of this BillingRecord.
|
69
|
+
def to_hash
|
70
|
+
{
|
71
|
+
'id' => id,
|
72
|
+
'provider' => provider,
|
73
|
+
'service' => service,
|
74
|
+
'account' => account,
|
75
|
+
'resource_id' => resource_id,
|
76
|
+
'resource_type' => resource_type,
|
77
|
+
'billing_type' => billing_type,
|
78
|
+
'start_time' => start_time,
|
79
|
+
'stop_time' => stop_time,
|
80
|
+
'cost_per_hour' => cost_per_hour,
|
81
|
+
'total_cost' => total_cost,
|
82
|
+
'billing_codes' => billing_codes.map {|c| [c.key, c.value]}
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
|
3
|
+
# Tracks the resources in one or more Fog accounts in an ActiveRecord database
|
4
|
+
class Tracker
|
5
|
+
|
6
|
+
# each CloudCostTracker is a thin wrapper around it's @tracker,
|
7
|
+
require 'fog_tracker' # which is a FogTracker:Tracker
|
8
|
+
extend Forwardable # most public methods are delegated
|
9
|
+
def_delegators :@tracker,:start,:stop,:query,:[],:update,
|
10
|
+
:running?,:types_for_account,:logger
|
11
|
+
|
12
|
+
attr_reader :accounts
|
13
|
+
|
14
|
+
# Creates an object for tracking Fog accounts in an ActiveRecord database
|
15
|
+
# @param [Hash] accounts a Hash of account information
|
16
|
+
# (see accounts.yml.example)
|
17
|
+
# @param [Hash] options optional additional parameters:
|
18
|
+
# - :delay (Integer) - Override time between polling of accounts
|
19
|
+
# (should take a single Exception as its only required parameter)
|
20
|
+
# - :logger - a Ruby Logger-compatible object
|
21
|
+
def initialize(accounts = {}, options={})
|
22
|
+
@delay = options[:delay]
|
23
|
+
@log = options[:logger] || FogTracker.default_logger
|
24
|
+
@running = false
|
25
|
+
@accounts = setup_fog_tracker(accounts)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Creates a FogTracker::Tracker that calls bill_for_account
|
31
|
+
# each time an account is refreshed
|
32
|
+
def setup_fog_tracker(accounts)
|
33
|
+
@tracker = FogTracker::Tracker.new(accounts,
|
34
|
+
{:delay => @delay, :logger => @log,
|
35
|
+
:callback => Proc.new do |resources|
|
36
|
+
checked_resources = sanity_check(resources)
|
37
|
+
code_resources(checked_resources)
|
38
|
+
bill_for_resources(checked_resources)
|
39
|
+
end
|
40
|
+
}
|
41
|
+
)
|
42
|
+
@tracker.accounts
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# Make sure returned resources are billable and codeable
|
47
|
+
# @param [Array<Fog::Model>] resources the resources to check
|
48
|
+
# @return [Array<Fog::Model>] the resources deemed OK
|
49
|
+
def sanity_check(resources)
|
50
|
+
resources.select do |resource|
|
51
|
+
if resource.identity == nil
|
52
|
+
@log.warn "Found a Fog::Model with no identity. Removing it..."
|
53
|
+
@log.debug "BAD Fog::Model instance: #{resource.inspect}"
|
54
|
+
false # return false from select to indicate resource is bad
|
55
|
+
else
|
56
|
+
true # return true from select to indicate resource is OK
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Adds billing codes for all resources
|
62
|
+
def code_resources(resources)
|
63
|
+
return if resources.empty?
|
64
|
+
account = resources.first.tracker_account
|
65
|
+
@log.info "Generating billing codes for account #{account[:name]}"
|
66
|
+
coding_class = CloudCostTracker::account_coding_class(
|
67
|
+
account[:service], account[:provider])
|
68
|
+
coding_agent = coding_class.new(resources, {:logger => @log})
|
69
|
+
coding_agent.setup(resources)
|
70
|
+
coding_agent.code(resources)
|
71
|
+
@log.info "Generated billing codes for account #{account[:name]}"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Generates BillingRecords for all Resources in account named +account_name+
|
75
|
+
def bill_for_resources(resources)
|
76
|
+
return if resources.empty?
|
77
|
+
account = resources.first.tracker_account
|
78
|
+
@log.info "Computing costs for account #{account[:name]}"
|
79
|
+
billing_agent = Billing::AccountBillingPolicy.new(
|
80
|
+
resources, {:logger => @log})
|
81
|
+
billing_agent.setup(resources)
|
82
|
+
billing_agent.bill_for(resources)
|
83
|
+
@log.info "Wrote billing records for account #{account[:name]}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Makes the Module heirarchy needs to be inspectable
|
2
|
+
class Module
|
3
|
+
def submodules
|
4
|
+
constants.collect {|const_name| const_get(const_name)}.select do |const|
|
5
|
+
const.class == Module
|
6
|
+
end
|
7
|
+
end
|
8
|
+
def classes
|
9
|
+
constants.collect {|const_name| const_get(const_name)}.select do |const|
|
10
|
+
const.class == Class
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
# Load generic ResourceBillingPolicy and Coding classes
|
5
|
+
require 'cloud_cost_tracker/billing/resource_billing_policy'
|
6
|
+
require 'cloud_cost_tracker/coding/resource_coding_policy'
|
7
|
+
# Load all ruby files from 'cloud_cost_tracker' directory - except the tasks
|
8
|
+
libs = Dir[File.join(File.dirname(__FILE__), "cloud_cost_tracker/**/*.rb")]
|
9
|
+
libs.delete File.join(File.dirname(__FILE__), "cloud_cost_tracker/tasks.rb")
|
10
|
+
libs.each {|f| require f}
|
11
|
+
|
12
|
+
# Top-level module namespace - defines some static methods for mapping providers,
|
13
|
+
# services and resources to their various Billing and Coding Policies
|
14
|
+
module CloudCostTracker
|
15
|
+
# Returns the current RACK_ENV or RAILS_ENV, or 'development' if not set.
|
16
|
+
# @return [String] ENV[RACK_ENV] || ENV[RAILS_ENV] || development
|
17
|
+
def self.env
|
18
|
+
(ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development').downcase
|
19
|
+
end
|
20
|
+
|
21
|
+
# Connects to the database defined in db_file.
|
22
|
+
# Uses the YAML section indexed by {CloudCostTracker#env}.
|
23
|
+
# @param db_file the path to a Rails-style YAML DB config file.
|
24
|
+
# @return [Hash] the current envinronment's database configuration.
|
25
|
+
def self.connect_to_database(db_file = ENV['DB_CONFIG_FILE'])
|
26
|
+
db_file ||= "./config/database.yml"
|
27
|
+
all_dbs = YAML::load(File.read db_file)
|
28
|
+
if not ::ActiveRecord::Base.connected?
|
29
|
+
::ActiveRecord::Base.establish_connection(all_dbs[CloudCostTracker.env])
|
30
|
+
end
|
31
|
+
all_dbs[CloudCostTracker.env]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Loads account information defined in account_file.
|
35
|
+
# @param account_file the path to a YAML file (see accounts.yml.example).
|
36
|
+
# @return [Hash] the cleaned, validated Hash of account info.
|
37
|
+
def self.read_accounts(account_file = ENV['ACCOUNT_FILE'])
|
38
|
+
FogTracker.read_accounts(account_file)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Creates and returns an Array of ResourceBillingPolicy (subclass) instances
|
42
|
+
# for billing the given +resource+, or an empty Array of none are found
|
43
|
+
# @param [Class] resource_class the Class object for the billed resource
|
44
|
+
# @param [Hash] options optional additional parameters:
|
45
|
+
# - :logger - a Ruby Logger-compatible object
|
46
|
+
# @return [Array <ResourceBillingPolicy>] billing policy objects for resource
|
47
|
+
def self.create_billing_agents(resource_class, options = {})
|
48
|
+
agents = Array.new
|
49
|
+
# Safely descend through the Billing module Heirarchy
|
50
|
+
if matches = resource_class.name.match(%r{^Fog::(\w+)::(\w+)::(\w+)})
|
51
|
+
fog_svc, provider, model_name = matches[1], matches[2], matches[3]
|
52
|
+
if CloudCostTracker::Billing.const_defined? fog_svc
|
53
|
+
service_module = CloudCostTracker::Billing::const_get fog_svc
|
54
|
+
if service_module.const_defined? provider
|
55
|
+
provider_module = service_module.const_get provider
|
56
|
+
# Search through the classes in the module for all matches
|
57
|
+
classes = provider_module.classes.each do |policy_class|
|
58
|
+
if policy_class.name =~ /#{model_name}.*BillingPolicy/
|
59
|
+
agents << policy_class.new(:logger => options[:logger])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
agents.sort {|a,b| a.class.name <=> b.class.name} # sort by class name
|
66
|
+
end
|
67
|
+
|
68
|
+
# Creates and returns an Array of ResourceCodingPolicy (subclass) instances
|
69
|
+
# for coding the given +resource+, or an empty Array of none are found
|
70
|
+
# @param [Class] resource_class the Class object for the billed resource
|
71
|
+
# @param [Hash] options optional additional parameters:
|
72
|
+
# - :logger - a Ruby Logger-compatible object
|
73
|
+
# @return [Array <ResourceCodingPolicy>] coding policy objects for resource
|
74
|
+
def self.create_coding_agents(resource_class, options = {})
|
75
|
+
agents = Array.new
|
76
|
+
# Safely descend through the Coding module Heirarchy
|
77
|
+
if matches = resource_class.name.match(%r{^Fog::(\w+)::(\w+)::(\w+)})
|
78
|
+
fog_svc, provider, model_name = matches[1], matches[2], matches[3]
|
79
|
+
if CloudCostTracker::Coding.const_defined? fog_svc
|
80
|
+
service_module = CloudCostTracker::Coding::const_get fog_svc
|
81
|
+
if service_module.const_defined? provider
|
82
|
+
provider_module = service_module.const_get provider
|
83
|
+
# Search through the classes in the module for all matches
|
84
|
+
classes = provider_module.classes.each do |policy_class|
|
85
|
+
if policy_class.name =~ /#{model_name}.*CodingPolicy/
|
86
|
+
agents << policy_class.new(:logger => options[:logger])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
agents.sort {|a,b| a.class.name <=> b.class.name} # sort by class name
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns a Class object, of the appropriate subclass of
|
96
|
+
# AccountCodingPolicy, given a Fog service and provider name.
|
97
|
+
# If none exists, returns CloudCostTracker::Coding::AccountCodingPolicy.
|
98
|
+
def self.account_coding_class(fog_service, provider)
|
99
|
+
agent_class = CloudCostTracker::Coding::AccountCodingPolicy
|
100
|
+
if CloudCostTracker::Coding.const_defined? fog_service
|
101
|
+
service_module = CloudCostTracker::Coding::const_get fog_service
|
102
|
+
if service_module.const_defined? provider
|
103
|
+
provider_module = service_module.const_get provider
|
104
|
+
if provider_module.const_defined? 'AccountCodingPolicy'
|
105
|
+
agent_class = provider_module.const_get 'AccountCodingPolicy'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
agent_class
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'cloud_cost_tracker'
|
3
|
+
|
4
|
+
namespace :db do
|
5
|
+
namespace :migrate do
|
6
|
+
|
7
|
+
desc "Conforms the schema of the tracker database. "+
|
8
|
+
"VERSION=[20120119000000] RACK_ENV=[development]"
|
9
|
+
task :tracker => :db_connect do
|
10
|
+
ActiveRecord::Migrator.migrate(
|
11
|
+
File.join(Gem.loaded_specs['cloud_cost_tracker'].full_gem_path,
|
12
|
+
'db', 'migrate'), ENV["VERSION"] ? ENV["VERSION"].to_i : nil
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
task :db_connect do
|
17
|
+
config_file = ENV['DB_CONFIG_FILE'] || './config/database.yml'
|
18
|
+
puts "Using database config file #{config_file}..."
|
19
|
+
config = YAML::load(File.open(config_file))[CloudCostTracker.env]
|
20
|
+
puts "Using database #{config['database']}..."
|
21
|
+
ActiveRecord::Base.establish_connection(config)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
desc "Runs the tracker [LOG_LEVEL=[INFO]]"
|
2
|
+
task :track do
|
3
|
+
Rake::Task["db:migrate:db_connect"].invoke
|
4
|
+
require File.join(File.dirname(__FILE__), '..', 'cloud_cost_tracker')
|
5
|
+
|
6
|
+
# Setup logging
|
7
|
+
log = FogTracker.default_logger(STDOUT)
|
8
|
+
log.level = ::Logger.const_get((ENV['LOG_LEVEL'] || 'INFO').to_sym)
|
9
|
+
|
10
|
+
log.info "Loading account information..."
|
11
|
+
accounts = FogTracker.read_accounts './config/accounts.yml'
|
12
|
+
|
13
|
+
log.info "Loading custom coding policies..."
|
14
|
+
Dir["./config/policies/*.rb"].each {|f| require f}
|
15
|
+
|
16
|
+
CloudCostTracker::Tracker.new(accounts, {:logger => log}).start
|
17
|
+
while true do ; sleep 60 end # Loop forever
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
describe AccountBillingPolicy do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@server = FAKE_AWS.servers.new
|
7
|
+
@db = FAKE_RDS.servers.new
|
8
|
+
@default_policy = AccountBillingPolicy.new([@server, @db])
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#setup' do
|
12
|
+
it "does nothing in the default implementation" do
|
13
|
+
(Proc.new {@default_policy.setup(nil)}).should_not raise_error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#bill_for' do
|
18
|
+
it 'calls set, get_cost_for_duration, and write_billing_record_for'+
|
19
|
+
' on each resource billing policy' do
|
20
|
+
resource_policy = double "fake ResourceBillingPolicy"
|
21
|
+
resource_policy.stub(:get_cost_for_duration).and_return 1.0
|
22
|
+
CloudCostTracker.stub(:create_billing_agents).and_return([resource_policy])
|
23
|
+
resource_policy.should_receive(:setup).with([@server, @db]).exactly(4).times
|
24
|
+
account_policy = AccountBillingPolicy.new([@server, @db])
|
25
|
+
resource_policy.should_receive(:get_cost_for_duration)
|
26
|
+
resource_policy.should_receive(:write_billing_record_for).exactly(2).times
|
27
|
+
account_policy.bill_for([@server, @db])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
describe ResourceBillingPolicy do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
BillingRecord.delete_all
|
7
|
+
@resource = FAKE_AWS.servers.new
|
8
|
+
@default_policy = ResourceBillingPolicy.new(:logger => LOG)
|
9
|
+
@end_time = Time.now
|
10
|
+
@start_time = @end_time - 3600 # one hour time span
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:each) do
|
14
|
+
BillingRecord.delete_all
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should expose a (null-impementation) setup method" do
|
18
|
+
(Proc.new {@default_policy.setup nil}).should_not raise_error
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#get_cost_for_duration' do
|
22
|
+
it 'should always return a cost of 1, even with nil arguments' do
|
23
|
+
@default_policy.get_cost_for_duration(nil, nil).should == 1.0
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#write_billing_record_for' do
|
28
|
+
before(:each) do
|
29
|
+
@resource.stub(:tracker_account).and_return(FAKE_ACCOUNT)
|
30
|
+
@resource.stub(:identity).and_return "fake server ID"
|
31
|
+
@resource.stub(:status).and_return "running"
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when it calculates a zero cost for a resource" do
|
35
|
+
it "writes no billing record for the resource" do
|
36
|
+
BillingRecord.all.count.should == 0
|
37
|
+
@resource.stub(:status).and_return "stopped"
|
38
|
+
# This next line is not *really* stubbing the tested behavior --
|
39
|
+
# the default implementation of :get_cost_for_duration in
|
40
|
+
# the subject class must be overriden to test its effects in
|
41
|
+
# any implemented subclasses
|
42
|
+
@default_policy.stub(:get_cost_for_duration).and_return 0
|
43
|
+
@default_policy.write_billing_record_for(
|
44
|
+
@resource, 0.0, 0.0, @start_time, @end_time)
|
45
|
+
BillingRecord.all.count.should == 0
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "when a record exists with the same resource and billing type" do
|
50
|
+
before(:each) do
|
51
|
+
@existing_record = BillingRecord.new(
|
52
|
+
:provider => FAKE_ACCOUNT[:provider],
|
53
|
+
:service => FAKE_ACCOUNT[:service],
|
54
|
+
:account => FAKE_ACCOUNT[:name],
|
55
|
+
:resource_id => @resource.identity,
|
56
|
+
:resource_type => 'Server',
|
57
|
+
:billing_type => "ResourceBillingPolicy",
|
58
|
+
:start_time => @start_time,
|
59
|
+
:stop_time => @end_time,
|
60
|
+
:cost_per_hour => 34.0,
|
61
|
+
:total_cost => 34.0
|
62
|
+
)
|
63
|
+
BillingRecord.stub(:most_recent_like).and_return @existing_record
|
64
|
+
end
|
65
|
+
context "when the records match in hourly cost and billing codes" do
|
66
|
+
context "and they overlap in time" do
|
67
|
+
it "invokes merge_with on the existing record" do
|
68
|
+
@existing_record.should_receive(:merge_with)
|
69
|
+
@default_policy.write_billing_record_for(
|
70
|
+
@resource, 34.0, 34.0, @end_time, @end_time + 60)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
context "when the new record differs in hourly rate or billing codes" do
|
75
|
+
it "writes a new record starting at the existing record's stop time" do
|
76
|
+
@existing_record.should_not_receive(:merge_with)
|
77
|
+
@default_policy.write_billing_record_for(
|
78
|
+
@resource, 5000000.0, 34.0, @end_time - 1, @end_time + 99)
|
79
|
+
result = BillingRecord.where(:resource_id => @resource.identity).last
|
80
|
+
result.start_time.to_s.should == @existing_record.stop_time.to_s
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'with no existing record for the same resource and billing type' do
|
86
|
+
it "writes a new record" do
|
87
|
+
BillingRecord.stub(:find_last_matching_record).and_return(nil)
|
88
|
+
@default_policy.write_billing_record_for(
|
89
|
+
@resource, 0.0, 1.0, @start_time, @end_time)
|
90
|
+
results = BillingRecord.where(:resource_id => @resource.identity)
|
91
|
+
results.should_not be_empty
|
92
|
+
results.first.account.should == FAKE_ACCOUNT_NAME
|
93
|
+
results.first.provider.should == 'AWS'
|
94
|
+
results.first.service.should == 'Compute'
|
95
|
+
results.first.billing_type.should == 'ResourceBillingPolicy'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
|
3
|
+
describe '#create_billing_agents' do
|
4
|
+
context "for a resource with only a single BillingPolicy defined" do
|
5
|
+
it "returns an Array with the correct ResourceBillingPolicy subclass" do
|
6
|
+
CloudCostTracker::create_billing_agents(FAKE_AWS.servers.new.class).
|
7
|
+
first.class.should == Billing::Compute::AWS::ServerBillingPolicy
|
8
|
+
end
|
9
|
+
end
|
10
|
+
context "for a resource with several BillingPolicy classes" do
|
11
|
+
it "returns an Array with the correct ResourceBillingPolicy subclasses" do
|
12
|
+
agents = CloudCostTracker::create_billing_agents(FAKE_RDS.servers.new.class)
|
13
|
+
agent_class_names = agents.map {|agent| agent.class.name.split('::').last}
|
14
|
+
agent_class_names.should ==
|
15
|
+
['ServerBillingPolicy', 'ServerStorageBillingPolicy']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
context "for a resource whose BillingPolicy class is not defined" do
|
19
|
+
it "returns an empty Array" do
|
20
|
+
CloudCostTracker::create_billing_agents(double("x").class).should == []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#create_coding_agents' do
|
26
|
+
context "for a resource with only a single CodingPolicy defined" do
|
27
|
+
it "returns an Array with the correct ResourceCodingPolicy subclass" do
|
28
|
+
CloudCostTracker::create_coding_agents(FAKE_AWS.volumes.new.class).
|
29
|
+
first.class.should == Coding::Compute::AWS::VolumeCodingPolicy
|
30
|
+
end
|
31
|
+
end
|
32
|
+
context "for a resource with several CodingPolicy classes" do
|
33
|
+
#it "returns an Array with the correct ResourceCodingPolicy subclass" do
|
34
|
+
# pending("need for multiple coding policies per resource")
|
35
|
+
# agents = CloudCostTracker::create_coding_agents(FAKE_RDS.servers.new.class)
|
36
|
+
# agent_classes = agents.map {|agent| agent.class.name.split('::').last}
|
37
|
+
# agent_classes.should include 'ServerCodingPolicy'
|
38
|
+
# agent_classes.should include 'ServerStorageCodingPolicy'
|
39
|
+
#end
|
40
|
+
end
|
41
|
+
context "for a resource whose CodingPolicy class is not defined" do
|
42
|
+
it "returns an empty Array" do
|
43
|
+
CloudCostTracker::create_coding_agents(double("x").class).should == []
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#account_coding_class' do
|
49
|
+
context "for a Fog service with an AccountCodingPolicy subclass" do
|
50
|
+
it "returns the correct AccountCodingPolicy subclass" do
|
51
|
+
CloudCostTracker::account_coding_class('Compute', 'AWS').
|
52
|
+
should == Coding::Compute::AWS::AccountCodingPolicy
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Coding
|
3
|
+
describe AccountCodingPolicy do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@server = FAKE_AWS.servers.new
|
7
|
+
@db = FAKE_RDS.servers.new
|
8
|
+
@default_policy = AccountCodingPolicy.new([@server, @db])
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#setup' do
|
12
|
+
it "does nothing in the default implementation" do
|
13
|
+
(Proc.new {@default_policy.setup(nil)}).should_not raise_error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#priority_classes' do
|
18
|
+
it "returns an empty Array in the default implementation" do
|
19
|
+
@default_policy.priority_classes.should == Array.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '#code' do
|
24
|
+
it 'calls attach_account_codes on each resource' do
|
25
|
+
@default_policy.should_receive(:attach_account_codes).exactly(2).times
|
26
|
+
@default_policy.code([@server, @db])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Coding
|
3
|
+
describe ResourceCodingPolicy do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@resource = FAKE_AWS.servers.new
|
7
|
+
@default_policy = ResourceCodingPolicy.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should expose a (null-impementation) setup method" do
|
11
|
+
(Proc.new {@default_policy.setup(nil)}).should_not raise_error
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#code' do
|
15
|
+
it 'should clear all billing codes the resource' do
|
16
|
+
@default_policy.code(@resource)
|
17
|
+
@resource.billing_codes.should == []
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|