cloud_cost_tracker 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|