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.
Files changed (61) hide show
  1. data/.gitignore +16 -0
  2. data/.yardopts +1 -0
  3. data/Gemfile +4 -0
  4. data/Guardfile +10 -0
  5. data/LICENSE.TXT +10 -0
  6. data/README.md +123 -0
  7. data/Rakefile +15 -0
  8. data/billing issues.md +4 -0
  9. data/bin/cloud_cost_tracker +94 -0
  10. data/cloud_cost_tracker.gemspec +36 -0
  11. data/config/accounts.example.yml +45 -0
  12. data/config/billing/compute-aws-servers/us-east-on_demand.yml +44 -0
  13. data/config/billing/compute-aws-snapshots.yml +5 -0
  14. data/config/billing/compute-aws-volumes.yml +5 -0
  15. data/config/billing/elasticache-aws.yml +15 -0
  16. data/config/billing/rds-aws-servers/us-east-on_demand-mysql.yml +26 -0
  17. data/config/billing/rds-aws-servers/us-east-on_demand-storage.yml +10 -0
  18. data/config/billing/storage-aws-directories.yml +5 -0
  19. data/config/database.example.yml +18 -0
  20. data/db/migrate/20120118000000_create_billing_records.rb +22 -0
  21. data/db/migrate/20120119000000_create_billing_codes.rb +20 -0
  22. data/lib/cloud_cost_tracker/billing/account_billing_policy.rb +106 -0
  23. data/lib/cloud_cost_tracker/billing/compute/aws/servers.rb +30 -0
  24. data/lib/cloud_cost_tracker/billing/compute/aws/snapshots.rb +43 -0
  25. data/lib/cloud_cost_tracker/billing/compute/aws/volumes.rb +30 -0
  26. data/lib/cloud_cost_tracker/billing/elasticache/clusters.rb +33 -0
  27. data/lib/cloud_cost_tracker/billing/rds/server_storage.rb +30 -0
  28. data/lib/cloud_cost_tracker/billing/rds/servers.rb +31 -0
  29. data/lib/cloud_cost_tracker/billing/resource_billing_policy.rb +119 -0
  30. data/lib/cloud_cost_tracker/billing/storage/aws/directories.rb +47 -0
  31. data/lib/cloud_cost_tracker/coding/account_coding_policy.rb +73 -0
  32. data/lib/cloud_cost_tracker/coding/compute/aws/account_coding_policy.rb +38 -0
  33. data/lib/cloud_cost_tracker/coding/compute/aws/servers.rb +26 -0
  34. data/lib/cloud_cost_tracker/coding/compute/aws/volumes.rb +26 -0
  35. data/lib/cloud_cost_tracker/coding/rds/account_coding_policy.rb +25 -0
  36. data/lib/cloud_cost_tracker/coding/rds/servers.rb +59 -0
  37. data/lib/cloud_cost_tracker/coding/resource_coding_policy.rb +25 -0
  38. data/lib/cloud_cost_tracker/extensions/fog_model.rb +41 -0
  39. data/lib/cloud_cost_tracker/models/billing_code.rb +14 -0
  40. data/lib/cloud_cost_tracker/models/billing_record.rb +88 -0
  41. data/lib/cloud_cost_tracker/tasks.rb +3 -0
  42. data/lib/cloud_cost_tracker/tracker.rb +86 -0
  43. data/lib/cloud_cost_tracker/util/module_instrospection.rb +13 -0
  44. data/lib/cloud_cost_tracker/version.rb +3 -0
  45. data/lib/cloud_cost_tracker.rb +113 -0
  46. data/lib/tasks/database.rake +25 -0
  47. data/lib/tasks/rspec.rake +6 -0
  48. data/lib/tasks/track.rake +19 -0
  49. data/spec/lib/cloud_cost_tracker/billing/account_billing_policy_spec.rb +33 -0
  50. data/spec/lib/cloud_cost_tracker/billing/resource_billing_policy_spec.rb +102 -0
  51. data/spec/lib/cloud_cost_tracker/cloud_cost_tracker_spec.rb +57 -0
  52. data/spec/lib/cloud_cost_tracker/coding/account_coding_policy_spec.rb +32 -0
  53. data/spec/lib/cloud_cost_tracker/coding/resource_coding_policy_spec.rb +23 -0
  54. data/spec/lib/cloud_cost_tracker/models/billing_code_spec.rb +35 -0
  55. data/spec/lib/cloud_cost_tracker/models/billing_record_spec.rb +206 -0
  56. data/spec/lib/cloud_cost_tracker/tracker_spec.rb +37 -0
  57. data/spec/spec_helper.rb +58 -0
  58. data/spec/support/_configure_logging.rb +6 -0
  59. data/writing-billing-policies.md +8 -0
  60. data/writing-coding-policies.md +7 -0
  61. 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,3 @@
1
+ # Load the tasks which are exported from this gem
2
+ load "#{File.dirname(__FILE__)}/../../lib/tasks/database.rake"
3
+ load "#{File.dirname(__FILE__)}/../../lib/tasks/track.rake"
@@ -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,3 @@
1
+ module CloudCostTracker
2
+ VERSION = "0.1.0"
3
+ 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,6 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ desc "Run specs"
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.rspec_opts = ['--color', '--format documentation', '-r ./spec/spec_helper.rb']
6
+ 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