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,30 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
module Compute
|
4
|
+
module AWS
|
5
|
+
# The default billing policy for Amazon EC2 server instances
|
6
|
+
class ServerBillingPolicy < ResourceBillingPolicy
|
7
|
+
# The YAML pricing data is read from config/billing
|
8
|
+
CENTS_PER_HOUR = YAML.load(File.read File.join(
|
9
|
+
CONSTANTS_DIR, 'compute-aws-servers', 'us-east-on_demand.yml'))
|
10
|
+
|
11
|
+
# Returns the storage cost for a given EC2 server
|
12
|
+
# over some duration (in seconds)
|
13
|
+
def get_cost_for_duration(ec2_server, duration)
|
14
|
+
return 0.0 if ec2_server.state =~ /(stopped|terminated)/
|
15
|
+
hourly_cost = CENTS_PER_HOUR[platform_for(ec2_server)][ec2_server.flavor_id]
|
16
|
+
(hourly_cost * duration) / SECONDS_PER_HOUR
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns either 'windows' or 'unix', based on this instance's platform
|
20
|
+
def platform_for(resource)
|
21
|
+
('windows' == resource.platform) ? 'windows' : 'unix'
|
22
|
+
end
|
23
|
+
|
24
|
+
def billing_type ; "EC2 Instance runtime" end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
module Compute
|
4
|
+
module AWS
|
5
|
+
# The default billing policy for Amazon EBS Snapshots
|
6
|
+
#
|
7
|
+
# *NOT REALLY WORKING* - AWS does not report the size
|
8
|
+
# of snapshots, so there's no way to accurately verify the cost! :(
|
9
|
+
class SnapshotBillingPolicy < ResourceBillingPolicy
|
10
|
+
# The YAML pricing data is read from config/billing
|
11
|
+
CENTS_PER_GB_PER_MONTH = YAML.load(File.read File.join(
|
12
|
+
CONSTANTS_DIR, 'compute-aws-snapshots.yml'))
|
13
|
+
|
14
|
+
# Returns the storage cost for a given EBS Snapshot
|
15
|
+
# over some duration (in seconds)
|
16
|
+
# TODO - Make an estimate based on lineage and volume size
|
17
|
+
# This code is only accurate if the snapshot is the first for its volume
|
18
|
+
def get_cost_for_duration(snapshot, duration)
|
19
|
+
return 0.0 # TEMPORARILY DISABLED UNTIL WORKAROUND IS FOUND
|
20
|
+
CENTS_PER_GB_PER_MONTH[zone(snapshot)] * snapshot.volume_size *
|
21
|
+
duration / SECONDS_PER_MONTH
|
22
|
+
end
|
23
|
+
|
24
|
+
# Follows the snapshot's volume and returns its region, and
|
25
|
+
# chops the availability zone letter from the region
|
26
|
+
def zone(resource)
|
27
|
+
volume = resource.account_resources('volumes').find do |v|
|
28
|
+
v.identity == resource.volume_id
|
29
|
+
end
|
30
|
+
if volume && volume.availability_zone
|
31
|
+
volume.availability_zone.chop
|
32
|
+
else
|
33
|
+
"us-east-1"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def billing_type ; "EBS snapshot storage" end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
module Compute
|
4
|
+
module AWS
|
5
|
+
# The default billing policy for Amazon EBS Volumes
|
6
|
+
class VolumeBillingPolicy < ResourceBillingPolicy
|
7
|
+
# The YAML pricing data is read from config/billing
|
8
|
+
CENTS_PER_GB_PER_MONTH = YAML.load(File.read File.join(
|
9
|
+
CONSTANTS_DIR, 'compute-aws-volumes.yml'))
|
10
|
+
|
11
|
+
# Returns the storage cost for a given EBS Volume
|
12
|
+
# over some duration (in seconds)
|
13
|
+
def get_cost_for_duration(volume, duration)
|
14
|
+
return 0.0 if volume.state =~ /(deleting|deleted)/
|
15
|
+
CENTS_PER_GB_PER_MONTH[zone(volume)] * volume.size *
|
16
|
+
duration / SECONDS_PER_MONTH
|
17
|
+
end
|
18
|
+
|
19
|
+
# Chops the availability zone letter from the region
|
20
|
+
def zone(resource)
|
21
|
+
resource.availability_zone.chop
|
22
|
+
end
|
23
|
+
|
24
|
+
def billing_type ; "EBS volume storage" end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
module AWS
|
4
|
+
module Elasticache
|
5
|
+
# The default billing policy for Amazon Elasticache Clusters
|
6
|
+
class ClusterBillingPolicy < ResourceBillingPolicy
|
7
|
+
# The YAML pricing data is read from config/billing
|
8
|
+
CENTS_PER_HOUR = YAML.load(File.read File.join(
|
9
|
+
CONSTANTS_DIR, 'elasticache-aws.yml'))
|
10
|
+
|
11
|
+
def billing_type ; "Elasticache Cluster runtime" end
|
12
|
+
|
13
|
+
# Returns the storage cost for a given Elasticache Cluster
|
14
|
+
# over some duration (in seconds)
|
15
|
+
def get_cost_for_duration(cluster, duration)
|
16
|
+
#@log.warn "Calculating cost for #{cluster.tracker_description}"
|
17
|
+
return 0.0 if cluster.status =~ /(stopped|terminated)/
|
18
|
+
hourly_cost = cluster.num_nodes *
|
19
|
+
CENTS_PER_HOUR[region(cluster)][cluster.node_type]
|
20
|
+
(hourly_cost * duration) / SECONDS_PER_HOUR
|
21
|
+
end
|
22
|
+
|
23
|
+
# Chops the availability zone letter from the availability zone
|
24
|
+
# and returns it as a string. e.g., "us-east-XXXXX" => "us-east"
|
25
|
+
def region(cluster)
|
26
|
+
cluster.zone.split('-')[0..1].join('-')
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
module AWS
|
4
|
+
module RDS
|
5
|
+
# The default billing policy for Amazon RDS server storage costs
|
6
|
+
class ServerStorageBillingPolicy < ResourceBillingPolicy
|
7
|
+
# The YAML pricing data is read from config/billing
|
8
|
+
CENTS_PER_GB_PER_MONTH = YAML.load(File.read File.join(
|
9
|
+
CONSTANTS_DIR, 'rds-aws-servers', 'us-east-on_demand-storage.yml'))
|
10
|
+
|
11
|
+
# Returns the storage cost for a given RDS server
|
12
|
+
# over some duration (in seconds)
|
13
|
+
def get_cost_for_duration(rds_server, duration)
|
14
|
+
CENTS_PER_GB_PER_MONTH[zone_setting(rds_server)] *
|
15
|
+
rds_server.allocated_storage * duration / SECONDS_PER_MONTH
|
16
|
+
end
|
17
|
+
|
18
|
+
# returns either 'multi_az' or 'standard',
|
19
|
+
# depending on whether this RDS server is multi-AZ
|
20
|
+
def zone_setting(resource)
|
21
|
+
resource.multi_az ? 'multi_az' : 'standard'
|
22
|
+
end
|
23
|
+
|
24
|
+
def billing_type ; "RDS Instance storage" end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
module AWS
|
4
|
+
module RDS
|
5
|
+
# The default billing policy for Amazon RDS server runtime costs
|
6
|
+
class ServerBillingPolicy < ResourceBillingPolicy
|
7
|
+
# The YAML pricing data is read from config/billing
|
8
|
+
CENTS_PER_HOUR = YAML.load(File.read File.join(
|
9
|
+
CONSTANTS_DIR, 'rds-aws-servers', 'us-east-on_demand-mysql.yml'))
|
10
|
+
|
11
|
+
# Returns the runtime cost for a given RDS server
|
12
|
+
# over some duration (in seconds)
|
13
|
+
def get_cost_for_duration(rds_server, duration)
|
14
|
+
hourly_cost =
|
15
|
+
CENTS_PER_HOUR[zone_setting(rds_server)][rds_server.flavor_id]
|
16
|
+
(hourly_cost * duration) / SECONDS_PER_HOUR
|
17
|
+
end
|
18
|
+
|
19
|
+
# returns either 'multi_az' or 'standard',
|
20
|
+
# depending on whether this RDS server is multi-AZ
|
21
|
+
def zone_setting(resource)
|
22
|
+
resource.multi_az ? 'multi_az' : 'standard'
|
23
|
+
end
|
24
|
+
|
25
|
+
def billing_type ; "RDS Instance runtime" end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# Abstract class, defines a generic Billing Policy that always returns 0.0 cost
|
2
|
+
module CloudCostTracker
|
3
|
+
module Billing
|
4
|
+
|
5
|
+
PRECISION = 10 # (Should match database migration precision)
|
6
|
+
# Defines a directory for holding YML pricing constants
|
7
|
+
CONSTANTS_DIR = File.join(File.dirname(__FILE__),'../../../config/billing')
|
8
|
+
|
9
|
+
# Some time and size constants
|
10
|
+
|
11
|
+
SECONDS_PER_MINUTE = 60
|
12
|
+
SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60
|
13
|
+
SECONDS_PER_DAY = SECONDS_PER_HOUR * 24
|
14
|
+
SECONDS_PER_YEAR = SECONDS_PER_DAY * 365
|
15
|
+
SECONDS_PER_MONTH = SECONDS_PER_YEAR / 12
|
16
|
+
BYTES_PER_KB = 1024
|
17
|
+
BYTES_PER_MB = BYTES_PER_KB * 1024
|
18
|
+
BYTES_PER_GB = BYTES_PER_MB * 1024
|
19
|
+
|
20
|
+
# Implements the logic for billing a single resource.
|
21
|
+
# All Billing Policies should inherit from this class, and define
|
22
|
+
# {#get_cost_for_duration}
|
23
|
+
class ResourceBillingPolicy
|
24
|
+
include CloudCostTracker
|
25
|
+
|
26
|
+
# Don't override this method - use {#setup} instead for
|
27
|
+
# one-time behavior
|
28
|
+
# @param [Hash] options optional parameters:
|
29
|
+
# - :logger - a Ruby Logger-compatible object
|
30
|
+
def initialize(options={})
|
31
|
+
@log = options[:logger] || FogTracker.default_logger
|
32
|
+
end
|
33
|
+
|
34
|
+
# An initializer called by the framework once per billling cycle.
|
35
|
+
# Override this method if you need to perform high-latency operations,
|
36
|
+
# like network transactions, that should not be performed per-resource.
|
37
|
+
def setup(resources) ; end
|
38
|
+
|
39
|
+
# Returns the cost for a particular resource over some duration in seconds.
|
40
|
+
# ALL BILLING POLICY SUBCLASSES SHOULD OVERRIDE THIS METHOD
|
41
|
+
# @param [Fog::Model] resource the resource to be billed
|
42
|
+
# @param [Integer] duration the number of seconds for this billing period.
|
43
|
+
# @return [Float] The amount the resource cost over duration seconds.
|
44
|
+
def get_cost_for_duration(resource, duration) ; 1.0 end
|
45
|
+
|
46
|
+
# Returns the default billing type for this policy.
|
47
|
+
# Override this to set a human-readable name for the policy.
|
48
|
+
# Defaults to the last part of the subclass name.
|
49
|
+
# @return [String] a description of the costs incurred under this policy.
|
50
|
+
def billing_type
|
51
|
+
self.class.name.split('::').last #(defaluts to class name)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates or Updates a BillingRecord for this BillingPolicy's resource.
|
55
|
+
# Don't override this -- it's called once for each resource by the
|
56
|
+
# {CloudCostTracker::Billing::AccountBillingPolicy}.
|
57
|
+
# @param [Fog::Model] resource the resource for the record to be written
|
58
|
+
# @param [Float] hourly_rate the resource's hourly rate for this period
|
59
|
+
# @param [Float] total the resource's total cost for this period
|
60
|
+
# @param [Time] start_time the start time for any new BillingRecords
|
61
|
+
# @param [Time] end_time the start time for any new BillingRecords
|
62
|
+
def write_billing_record_for(resource, hourly_rate, total,
|
63
|
+
start_time, end_time)
|
64
|
+
account = resource.tracker_account
|
65
|
+
resource_type = resource.class.name.split('::').last
|
66
|
+
return if total == 0.0 # Write no record if the cost is zero
|
67
|
+
new_record = BillingRecord.new(
|
68
|
+
:provider => account[:provider],
|
69
|
+
:service => account[:service],
|
70
|
+
:account => account[:name],
|
71
|
+
:resource_id => resource.identity,
|
72
|
+
:resource_type => resource_type,
|
73
|
+
:billing_type => billing_type,
|
74
|
+
:start_time => start_time,
|
75
|
+
:stop_time => end_time,
|
76
|
+
:cost_per_hour => hourly_rate,
|
77
|
+
:total_cost => total
|
78
|
+
)
|
79
|
+
new_record.set_codes(resource.billing_codes)
|
80
|
+
# Combine BillingRecords within maximim_gap of one another
|
81
|
+
write_or_merge_with_existing(new_record, account[:delay].to_i)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# Writes 'record' into the database if:
|
87
|
+
# 1. there's no previous record for this resource + billing type; or
|
88
|
+
# 2. the previous such record differs in hourly cost or billing codes; or
|
89
|
+
# 3. the records are separated by more than maximum_time_gap
|
90
|
+
# Otherwise, `record` is merged with the previous record that matches its
|
91
|
+
# resource and billing type
|
92
|
+
def write_or_merge_with_existing(new_record, maximum_time_gap)
|
93
|
+
write_new_record = true
|
94
|
+
last_record = BillingRecord.most_recent_like(new_record)
|
95
|
+
# If the previous record for this resource/billing type has the same
|
96
|
+
# hourly rate and billing codes, just update the previous record
|
97
|
+
if last_record && last_record.overlaps_with(new_record, maximum_time_gap)
|
98
|
+
if (last_record.cost_per_hour.round(PRECISION) ==
|
99
|
+
new_record.cost_per_hour.round(PRECISION)) &&
|
100
|
+
(last_record.billing_codes == new_record.billing_codes)
|
101
|
+
@log.debug "Updating record #{last_record.id} for "+
|
102
|
+
" #{new_record.resource_type} #{new_record.resource_id}"+
|
103
|
+
" in account #{new_record.account}"
|
104
|
+
last_record.merge_with new_record
|
105
|
+
write_new_record = false
|
106
|
+
else # If the previous record has different rate or codes...
|
107
|
+
# Make the new record begin where the previous one leaves off
|
108
|
+
new_record.start_time = last_record.stop_time
|
109
|
+
end
|
110
|
+
end
|
111
|
+
if write_new_record
|
112
|
+
@log.debug "Creating new record for for #{new_record.resource_type}"+
|
113
|
+
" #{new_record.resource_id} in account #{new_record.account}"
|
114
|
+
ActiveRecord::Base.connection_pool.with_connection {new_record.save!}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Billing
|
3
|
+
module Storage
|
4
|
+
module AWS
|
5
|
+
# The default billing policy for Amazon S3 buckets
|
6
|
+
class DirectoryBillingPolicy < ResourceBillingPolicy
|
7
|
+
# The YAML pricing data is read from config/billing
|
8
|
+
CENTS_PER_GB_PER_MONTH = YAML.load(File.read File.join(
|
9
|
+
CONSTANTS_DIR, 'storage-aws-directories.yml'))
|
10
|
+
|
11
|
+
# Returns the runtime cost for a given S3 bucket
|
12
|
+
# over some duration (in seconds)
|
13
|
+
def get_cost_for_duration(s3_bucket, duration)
|
14
|
+
CENTS_PER_GB_PER_MONTH[zone(s3_bucket)] * total_size(s3_bucket) *
|
15
|
+
duration / SECONDS_PER_MONTH
|
16
|
+
end
|
17
|
+
|
18
|
+
# Chops the availability zone letter from the region
|
19
|
+
def zone(resource)
|
20
|
+
'us-east-1'
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the total size of all a bucket's objecs, in GB
|
24
|
+
def total_size(bucket)
|
25
|
+
# check for saved value
|
26
|
+
return @bucket_size[bucket] if @bucket_size[bucket]
|
27
|
+
@log.debug "Computing size for #{bucket.tracker_description}"
|
28
|
+
total_bytes = 0
|
29
|
+
bucket.files.each {|object| total_bytes += object.content_length}
|
30
|
+
@log.debug "total bytes = #{total_bytes}"
|
31
|
+
# save the total size for later
|
32
|
+
@bucket_size[bucket] = total_bytes / BYTES_PER_GB.to_f
|
33
|
+
end
|
34
|
+
|
35
|
+
# Remembers each bucket size, because iterating over S3 objects is
|
36
|
+
# slow, and get_cost_for_duration is called twice
|
37
|
+
def setup(resources)
|
38
|
+
@bucket_size = Hash.new
|
39
|
+
end
|
40
|
+
|
41
|
+
def billing_type ; "S3 Bucket storage" end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Coding
|
3
|
+
# Implements the generic logic for attaching billing codes to all resources
|
4
|
+
# in a single account.
|
5
|
+
# Initializes the necessary ResourceCodingPolicy objects, sorts the
|
6
|
+
# resources by policy, and calls {#code} once on each resource's policy,
|
7
|
+
# to compute the billing codes as pairs of Strings.
|
8
|
+
class AccountCodingPolicy
|
9
|
+
|
10
|
+
# Creates an object that implements a coding policy
|
11
|
+
# that attaches no billing codes
|
12
|
+
# @param [Array <Fog::Model>] resources the resources to code
|
13
|
+
# @param [Hash] options optional parameters:
|
14
|
+
# - :logger - a Ruby Logger-compatible object
|
15
|
+
def initialize(resources, options={})
|
16
|
+
@log = options[:logger] || FogTracker.default_logger
|
17
|
+
setup_resource_coding_agents(resources)
|
18
|
+
end
|
19
|
+
|
20
|
+
# An initializer called by the framework once per bill coding cycle.
|
21
|
+
# Override this method if you need to perform high-latency operations,
|
22
|
+
# like network transactions, that should not be performed per-resource.
|
23
|
+
def setup(resources) ; end
|
24
|
+
|
25
|
+
# Defines the order in which resource collections are coded.
|
26
|
+
# Override this method if you need to code the resource collections
|
27
|
+
# in a particular order. Return an Array of the Fog::Model subclasses.
|
28
|
+
# @return [Array <Class>] the class names, in preferred coding order
|
29
|
+
def priority_classes ; Array.new end
|
30
|
+
|
31
|
+
# Defines an acount-wide coding strategy for coding each resource.
|
32
|
+
# Override this method if you need to write logic for attaching billing
|
33
|
+
# codes to all resources in an account, regardless of collection / type.
|
34
|
+
def attach_account_codes(resource) ; end
|
35
|
+
|
36
|
+
# Defines the default method for coding all resources.
|
37
|
+
# Attaches Billing Codes (String pairs) to resources, as @billing_codes.
|
38
|
+
# Resources whose class is in {#priority_classes} are coded first.
|
39
|
+
def code(resources)
|
40
|
+
return if resources.empty?
|
41
|
+
account = resources.first.tracker_account
|
42
|
+
@log.info "Coding account #{account[:name]}"
|
43
|
+
resources.each {|resource| attach_account_codes(resource)}
|
44
|
+
classes_to_code = priority_classes + (@agents.keys - priority_classes)
|
45
|
+
classes_to_code.delete_if {|res_class| @agents[res_class] == nil}
|
46
|
+
classes_to_code.each do |fog_model_class|
|
47
|
+
@log.info "Coding #{fog_model_class} in account #{account[:name]}"
|
48
|
+
collection = resources.select {|r| r.class == fog_model_class}
|
49
|
+
collection.each do |resource|
|
50
|
+
@agents[fog_model_class].each do |agent|
|
51
|
+
@log.debug "Coding #{resource.tracker_description}"
|
52
|
+
agent.code(resource)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Builds a Hash of CodingPolicy agents, indexed by resource Class name
|
61
|
+
def setup_resource_coding_agents(resources)
|
62
|
+
@agents = Hash.new
|
63
|
+
((resources.collect {|r| r.class}).uniq).each do |resource_class|
|
64
|
+
@agents[resource_class] = CloudCostTracker::create_coding_agents(
|
65
|
+
resource_class, {:logger => @log})
|
66
|
+
@agents[resource_class].each {|agent| agent.setup(resources)}
|
67
|
+
@agents.delete(resource_class) if @agents[resource_class].empty?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'cloud_cost_tracker/coding/account_coding_policy'
|
2
|
+
module CloudCostTracker
|
3
|
+
module Coding
|
4
|
+
module Compute
|
5
|
+
module AWS
|
6
|
+
# Implements the logic for attaching billing codes to all resources
|
7
|
+
# in a single AWS EC2 account, and defines the default order in which
|
8
|
+
# EC2 resources get coded.
|
9
|
+
class AccountCodingPolicy < CloudCostTracker::Coding::AccountCodingPolicy
|
10
|
+
|
11
|
+
# Defines the order in which EC2 resources are coded.
|
12
|
+
# @return [Array <Class>] the class names, in preferred coding order
|
13
|
+
def priority_classes
|
14
|
+
[
|
15
|
+
Fog::Compute::AWS::Tag,
|
16
|
+
Fog::Compute::AWS::SecurityGroup,
|
17
|
+
Fog::Compute::AWS::KeyPair,
|
18
|
+
Fog::Compute::AWS::Server, # pulls codes from security group
|
19
|
+
Fog::Compute::AWS::Volume, # pulls codes from server
|
20
|
+
Fog::Compute::AWS::Snapshot, # pulls codes from volume
|
21
|
+
Fog::Compute::AWS::Address,
|
22
|
+
]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Translates all AWS Tags into Billing Codes
|
26
|
+
def attach_account_codes(resource)
|
27
|
+
if resource.respond_to?(:tags) and resource.tags
|
28
|
+
resource.tags.each do |key, value|
|
29
|
+
resource.code(key, value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Coding
|
3
|
+
module Compute
|
4
|
+
module AWS
|
5
|
+
# Implements the logic for attaching billing codes to EC2 servers
|
6
|
+
class ServerCodingPolicy < ResourceCodingPolicy
|
7
|
+
|
8
|
+
# Copies all billing codes from this server's Security Group
|
9
|
+
def code(ec2_server)
|
10
|
+
ec2_server.groups.each do |group_name|
|
11
|
+
group = ec2_server.account_resources('security_groups').find do |g|
|
12
|
+
g.identity == group_name
|
13
|
+
end
|
14
|
+
if group
|
15
|
+
group.billing_codes.each do |billing_code|
|
16
|
+
ec2_server.code(billing_code[0], billing_code[1])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Coding
|
3
|
+
module Compute
|
4
|
+
module AWS
|
5
|
+
# Implements the logic for attaching billing codes to EBS Volumes
|
6
|
+
class VolumeCodingPolicy < ResourceCodingPolicy
|
7
|
+
|
8
|
+
# Copies all billing codes from any attached instance to this volume
|
9
|
+
def code(ebs_volume)
|
10
|
+
if (ebs_volume.state == "in-use") && (ebs_volume.server_id != "")
|
11
|
+
server = ebs_volume.account_resources('servers').find do |server|
|
12
|
+
server.identity == ebs_volume.server_id
|
13
|
+
end
|
14
|
+
if server
|
15
|
+
server.billing_codes.each do |server_code|
|
16
|
+
ebs_volume.code(server_code[0], server_code[1])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Coding
|
3
|
+
module RDS
|
4
|
+
module AWS
|
5
|
+
# Implements the logic for attaching billing codes to all resources
|
6
|
+
# in a single AWS RDS account, and defines the default order in which
|
7
|
+
# RDS resources get coded.
|
8
|
+
class AccountCodingPolicy < CloudCostTracker::Coding::AccountCodingPolicy
|
9
|
+
|
10
|
+
# Defines the order in which EC2 resources are coded.
|
11
|
+
# @return [Array <Class>] the class names, in preferred coding order
|
12
|
+
def priority_classes
|
13
|
+
[
|
14
|
+
Fog::AWS::RDS::SecurityGroup,
|
15
|
+
Fog::AWS::RDS::ParameterGroup,
|
16
|
+
Fog::AWS::RDS::Server, # pulls codes from security group
|
17
|
+
Fog::AWS::RDS::Snapshot, # pulls codes from server
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module CloudCostTracker
|
2
|
+
module Coding
|
3
|
+
module AWS
|
4
|
+
module RDS
|
5
|
+
# Implements the logic for attaching billing codes to RDS instances
|
6
|
+
class ServerCodingPolicy < ResourceCodingPolicy
|
7
|
+
|
8
|
+
# Indexes the security and parameter groups, so that BillingCodes
|
9
|
+
# can be pulled from them quickly when code() is called
|
10
|
+
def setup(resources)
|
11
|
+
@acc_sec_groups = Hash.new # Index the security groups
|
12
|
+
resources.first.account_resources('security_groups').each do |group|
|
13
|
+
@acc_sec_groups[group.identity] = group
|
14
|
+
end
|
15
|
+
@acc_param_groups = Hash.new # Index the parameter groups
|
16
|
+
resources.first.account_resources('parameter_groups').each do |group|
|
17
|
+
@acc_param_groups[group.identity] = group
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Copies all billing codes from this server's Groups
|
22
|
+
def code(rds_server)
|
23
|
+
code_from_security_group(rds_server)
|
24
|
+
code_from_parameter_group(rds_server)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Copies all billing codes from this server's Security Group
|
28
|
+
def code_from_security_group(rds_server)
|
29
|
+
if rds_server.db_security_groups
|
30
|
+
server_groups = rds_server.db_security_groups.map do |group|
|
31
|
+
@acc_sec_groups[group['DBSecurityGroupName']]
|
32
|
+
end
|
33
|
+
server_groups.each do |group|
|
34
|
+
group.billing_codes.each do |billing_code|
|
35
|
+
rds_server.code(billing_code[0], billing_code[1])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Copies all billing codes from this server's Parameter Group
|
42
|
+
def code_from_parameter_group(rds_server)
|
43
|
+
if rds_server.db_parameter_groups
|
44
|
+
server_groups = rds_server.db_parameter_groups.map do |group|
|
45
|
+
@acc_param_groups[group['DBParameterGroupName']]
|
46
|
+
end
|
47
|
+
server_groups.each do |group|
|
48
|
+
group.billing_codes.each do |billing_code|
|
49
|
+
rds_server.code(billing_code[0], billing_code[1])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Abstract class, defines a generic resource Coding Policy that attaches no codes
|
2
|
+
module CloudCostTracker
|
3
|
+
module Coding
|
4
|
+
class ResourceCodingPolicy
|
5
|
+
|
6
|
+
# Creates an object that implements a billing policy
|
7
|
+
# that attaches no billing codes
|
8
|
+
# @param [Hash] options optional parameters:
|
9
|
+
# - :logger - a Ruby Logger-compatible object
|
10
|
+
def initialize(options={})
|
11
|
+
@log = options[:logger] || FogTracker.default_logger
|
12
|
+
end
|
13
|
+
|
14
|
+
# Used by subclasses to perform setup each time an account's
|
15
|
+
# resources are coded
|
16
|
+
# High-latency operations like network transactions that are not
|
17
|
+
# per-resource should be performed here
|
18
|
+
def setup(resources) ; end
|
19
|
+
|
20
|
+
# Attaches Billing Codes (String pairs) to resource, as billing_codes
|
21
|
+
def code(resource) ; end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|