cloud_cost_tracker 0.1.0

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