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,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
|