cloud_financial_officer 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 ADDED
@@ -0,0 +1,14 @@
1
+ *.gem
2
+ .bundle
3
+ config/accounts.yml
4
+ config/database.yml
5
+ config/policies
6
+ pkg/*
7
+ *~
8
+ .DS_Store
9
+ *.tmproj
10
+ db/*.sqlite3
11
+ log/*.log
12
+ tmp/**
13
+ doc/**
14
+ .yardoc
data/.powrc ADDED
@@ -0,0 +1 @@
1
+ export RACK_ENV=development
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private --protected lib/**/*.rb - API.md
data/API.md ADDED
@@ -0,0 +1,146 @@
1
+ Cloud Financial Officer REST API
2
+ ================
3
+ The REST API exposes 3 resources:
4
+
5
+ 1. Account - represents information about the accounts being tracked.
6
+ 2. Resource - represents an cloud computing asset, like a Server or S3 Bucket.
7
+ 3. Report - represents a group of billing records. Each billing record
8
+ represents a "line item" for a given Resource over a given time.
9
+
10
+
11
+ ----------------
12
+ Account
13
+ ----------------
14
+ Accounts are the "top-level" entity exposed by the API.
15
+ Each Account has a list of its Resources.
16
+
17
+ **RESOURCE LOCATION**: `/api/v1/accounts/[account-name]`
18
+
19
+ **DISCOVERABLE AT**: `api/v1/accounts`
20
+ **OR**: `api/v1`
21
+
22
+ **RESULT FIELDS**:
23
+
24
+ * id: the name of the account
25
+ * service: the name of the cloud service
26
+ * provider: the name of the cloud service provider
27
+ * url: link to an individual account
28
+ * report_url: link to a report for an individual account
29
+ * resources: an Array of links to an account's Resources
30
+ * billing_codes: an Array of String pairs representing the billing codes
31
+ for all the resources in this account
32
+
33
+
34
+ ----------------
35
+ Resource
36
+ ----------------
37
+ Resources represent individual cloud computing assets, and are always scoped
38
+ within an Account.
39
+
40
+ **RESOURCE LOCATION**: `/api/v1/accounts/[account-name]/[resource-type]/[resource-id]`
41
+
42
+ **DISCOVERABLE AT**: `/api/v1/accounts/[account-name]`
43
+
44
+ **RESULT FIELDS**:
45
+
46
+ * resource_id: the cloud provider's ID value for this resource
47
+ * resource_type: "Server", "Volume", "S3 Bucket", etc.
48
+ * account: this resource's account information
49
+ * report_url: link to a report for a specific resource
50
+ * billing_codes: an Array of String pairs representing the billing codes
51
+ for this resource
52
+
53
+
54
+ ----------------
55
+ Report
56
+ ----------------
57
+ Reports are a convenient way of summarizing costs for different combinations of
58
+ accounts, billing codes, resource types, etc..
59
+ In addition to the cost summary, each Report also exposes a list of billing
60
+ records that make up the total cost of the report.
61
+ Each billing record describes the costs incurred for one or more Resources
62
+ over a given duration.
63
+
64
+ **RESOURCE LOCATION**: `/api/v1/report/[for_[CONSTRAINT]]/[by_[GROUP]]`
65
+
66
+ **DISCOVERABLE AT**: `/api/v1/accounts/[account-name]`
67
+ **OR**: `/api/v1/accounts/[account-name]/[resource-type]/[resource-id]`
68
+
69
+
70
+ **CONSTRAINT EXPRESSIONS**: These limit the scope of the information in the report.
71
+ Constraints be chained together, irrespective of order.
72
+
73
+ * `for_account/[account1](/[account2]...)`
74
+ * `for_resource/[resourceID1](/[resourceID2]...)`
75
+ * `for_type/[resource_type1](/[resource_type2]...)`
76
+ * `for_provider/[provider1](/[provider2]...)`
77
+ * `for_service/[service1](/[service2]...)`
78
+ * `for_billing_type/[billing_type1](/[billing_type2]...)`
79
+ * `for_time/[start_datetime]/[stop_datetime]`
80
+ * `for_code/[key1]/[value1](/[key2]/[value2]...)`
81
+
82
+ **GROUPING EXPRESSIONS**: These limit the number of records in the report, but
83
+ not the total cost or duration of the report itself. Each grouping expression
84
+ causes all records that share the same value for the given grouping attribute
85
+ to be combined into one record (much like a SQL GROUP_BY clause).
86
+ Grouping expressions can be chained together, irrespective of order.
87
+
88
+ * `by_account`
89
+ * `by_resource`
90
+ * `by_type`
91
+ * `by_provider`
92
+ * `by_service`
93
+ * `by_billing_type`
94
+
95
+ **QUERY PARAMETERS**:
96
+
97
+ * content-type: text/json or text/csv
98
+
99
+ **REPORT SUMMARY FIELDS**:
100
+
101
+ * start_time: start time for the earliest billing record in this report
102
+ * stop_time: stop time for the latest billing record in this report
103
+ * total_cost: cost, in cents, for all the billing records in this report
104
+ * cost_per_hour: average hourly cost over all this report's records
105
+ * account: this resource's account information
106
+ * billing_codes: all the billing codes for all this report's records
107
+ * billing_records: Array of billing records that make up this report
108
+
109
+
110
+ ----------------
111
+ Report Generation Details
112
+ ----------------
113
+
114
+ Each Report represents a set of billing records. At the most granular level,
115
+ each billing record represents a period of time during which a specific cloud
116
+ resource incurred charges at a given hourly rate. And if the report was not
117
+ generated with a `by_[GROUP]` expression in the URL, then each billing record
118
+ in the report contains resource-specific data in all the following fields:
119
+
120
+ **BILLING RECORD FIELDS**:
121
+
122
+ * service: the name of the resource's cloud service
123
+ * provider: the provider for the resource's cloud service
124
+ * account: the name of the resource's account
125
+ * resource_id: the cloud provider's ID value for this resource
126
+ * resource_type: "Server", "Volume", "S3 Bucket", etc
127
+ * billing_type : Some resources, like RDS servers, are billed for both runtime
128
+ and storage. These costs are distinguished by billing_type.
129
+ * start_time: start time for this billing record
130
+ * stop_time: stop time for this billing record
131
+ * cost_per_hour: hourly cost for this resource over this record's duration
132
+ * total_cost: cost, in cents, for this resource over this record's duration
133
+
134
+ However, since that level of detail is needed only infrequently, most useful
135
+ reports group their records by one or more of the fields that make up each record.
136
+ In this case, all records that share the same value for this grouping field are
137
+ combined into a "composite record". The values for all the NON-grouping fields in
138
+ a composite record are set to "*" (which stands for "more than one value") for any
139
+ fields where the component records don't all share the same value.
140
+
141
+
142
+ ----------------
143
+ Report Generation Examples
144
+ ----------------
145
+
146
+ [TODO]
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec',
5
+ :version => 2,
6
+ :cli => "--color --format documentation -r ./spec/spec_helper.rb" do
7
+ watch(%r{^spec/.+_spec\.rb$})
8
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
9
+ watch('spec/spec_helper.rb') { "spec" }
10
+ end
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ Cloud Financial Officer
2
+ ================
3
+ Watches multiple cloud computing accounts, records expenses by resource,
4
+ and exposes a REST API to query the recorded costs.
5
+
6
+
7
+ ----------------
8
+ What is it?
9
+ ----------------
10
+ Cloud Financial Officer is an application and library for tracking and
11
+ reporting on the expenses generated by one or more cloud computing accounts.
12
+ It monitors all the accounts and computes costs for each cloud computing
13
+ "Resource", like a server (EC2 instance) or a storage container (S3 bucket).
14
+ It saves the resulting billing records into a database using ActiveRecord, and
15
+ generates JSON reports on this data.
16
+
17
+ The cloud computing Resources can be assigned "billing codes", which are
18
+ arbitrary pairs of Strings that can be used by an organization for internal
19
+ billing. Each Resource's billing codes are attached to all its billing records.
20
+
21
+ By default, a useful set of Resource coding policies is included. For example,
22
+ any Tags that are assigned to EC2 Resources are automatically converted to
23
+ billing codes. Billing codes from Security Groups are passed on to the Instances
24
+ that run within them, and from there to the EBS volumes attached to the
25
+ Instances. Custom policies can also be written (in Ruby) to assign billing codes
26
+ based on arbitrary logic.
27
+
28
+
29
+ ----------------
30
+ Why is it?
31
+ ----------------
32
+ The principal motivation behind this software is to provide "microbilling" for
33
+ Amazon Web Services, as explained in the README for the [Cloud Cost Tracker].
34
+
35
+ Though every effort is made to calculate costs accurately, the purpose of CFO
36
+ is not really to produce an exact number matching the bill from a given service
37
+ provider. It is more useful in determining the relative breakdown of costs
38
+ by provider, account, or resource type (which it does automatically), and more
39
+ useful still in reporting costs for internal billing units, like customer or
40
+ project. This can be achieved with EC2 tags or custom resource coding logic.
41
+
42
+
43
+ ----------------
44
+ Installation
45
+ ----------------
46
+ CFO can be used as a standalone application or as a Rack middleware library.
47
+
48
+ ### To install as an application ###
49
+
50
+ 1) Install project dependencies:
51
+
52
+ gem install rake bundler
53
+
54
+ 2) Fetch the code:
55
+
56
+ git clone https://github.com/benton/cloud_financial_officer.git
57
+ cd cloud_financial_officer
58
+
59
+ 3) Now edit the `Gemfile` to include your desired database driver.
60
+ Then bundle everything together.
61
+
62
+ bundle
63
+
64
+ ### To install as a library ###
65
+
66
+ 1) Install the gem, or add it as a Bundler dependency and `bundle`.
67
+
68
+ gem install cloud_financial_officer
69
+
70
+ 2) Require the middleware from your Rack application, then insert it
71
+ in the stack:
72
+
73
+ require 'cloud_financial_officer'
74
+ ...
75
+ config.middleware.use CFO::Service # (Rails application.rb)
76
+ # or
77
+ use CFO::Service # (Sinatra)
78
+
79
+ 3) Require the database migration tasks in your `Rakefile`:
80
+
81
+ require 'cloud_cost_tracker/tasks'
82
+
83
+
84
+ ### Initializing the database ###
85
+
86
+ 1) Create a Rails-style `config/database.yml` file (example included).
87
+
88
+ 2) Create the tables. *`ENV['RACK_ENV']` determines the configuration entry*
89
+
90
+ rake db:migrate:tracker
91
+
92
+
93
+ ----------------
94
+ Usage
95
+ ----------------
96
+ CFO consists of two parts:
97
+
98
+ * A rake task that gathers cost information.
99
+ * A web service that exposes a REST API for the billing records.
100
+
101
+ Both require information about which accounts to track and report on.
102
+ This is stored in `config/accounts.yml` (see the provided example).
103
+ Both also require a `config/database.yml`.
104
+
105
+ The tracking process is run with:
106
+
107
+ rake track
108
+
109
+ The service is run with:
110
+
111
+ rake service
112
+ or
113
+
114
+ rackup
115
+
116
+ The entry point for the REST API is `/api/v1/accounts`
117
+ (See the {file:API.md API documentation})
118
+
119
+
120
+ ----------------
121
+ Development
122
+ ----------------
123
+
124
+ *Getting started with development*
125
+
126
+ 1) Install project dependencies
127
+
128
+ gem install rake bundler
129
+
130
+ 2) Fetch the project code
131
+
132
+ git clone https://github.com/benton/cloud_financial_officer.git
133
+ cd cloud_financial_officer
134
+
135
+ 3) Bundle up and run the tests
136
+
137
+ bundle && rake
138
+
139
+
140
+
141
+ [Cloud Cost Tracker]: https://github.com/benton/cloud_cost_tracker
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ # Set up bundler
2
+ %w{rubygems bundler bundler/gem_tasks}.each {|dep| require dep}
3
+ bundles = [:default]
4
+ Bundler.setup(:default)
5
+ require 'cloud_financial_officer'
6
+ case CFO.env
7
+ when 'development' then Bundler.setup(:default, :development)
8
+ when 'test' then Bundler.setup(:default, :development, :test)
9
+ end
10
+
11
+ CFO.force_timezone # This does nothing by default - modify it if you need it
12
+
13
+ # Load all tasks from the cloud_cost_tracker gem
14
+ require 'cloud_cost_tracker/tasks'
15
+
16
+ # Load all tasks from 'lib/tasks'
17
+ Dir["#{File.dirname(__FILE__)}/lib/tasks/*.rake"].sort.each {|ext| load ext}
18
+
19
+ desc 'Default: runs all tests'
20
+ task :default => :spec
@@ -0,0 +1,43 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cfo/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cloud_financial_officer"
7
+ s.version = CFO::VERSION
8
+ s.authors = ["Benton Roberts"]
9
+ s.email = ["benton@bentonroberts.com"]
10
+ s.homepage = "http://github.com/benton/cloud_financial_officer"
11
+ s.summary = %q{Watches multiple cloud computing accounts, }+
12
+ %q{records expenses by resource, and exposes }+
13
+ %q{a REST API to query the recorded costs}
14
+ s.description = %q{Watches multiple cloud computing accounts, }+
15
+ %q{records expenses by resource, and exposes }+
16
+ %q{a REST API to query the recorded costs}
17
+ s.rubyforge_project = "cloud_financial_officer"
18
+
19
+ # This project is both a Gem and an Application,
20
+ # so the Gemfile.lock is included in the repo for application users,
21
+ # but excluded from the packaged Gem, for middleware use.
22
+ git_files = `git ls-files`.split("\n") # Read all files in the repo,
23
+ git_files.delete "Gemfile.lock" # remove Gemfile.lock, and
24
+ s.files = git_files # use the result in the Gem.
25
+
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
28
+ s.require_paths = ["lib"]
29
+
30
+ # Runtime dependencies
31
+ s.add_dependency "sinatra"
32
+ s.add_dependency "sinatra-contrib"
33
+ s.add_dependency "cloud_cost_tracker", ">=0.1.0"
34
+
35
+ # Development / Test dependencies
36
+ s.add_development_dependency "rake"
37
+ s.add_development_dependency "rspec"
38
+ s.add_development_dependency "factory_girl", "~> 2.1.0"
39
+ s.add_development_dependency "guard"
40
+ s.add_development_dependency "guard-rspec"
41
+ s.add_development_dependency "ruby_gntp"
42
+ s.add_development_dependency "sqlite3"
43
+ end
@@ -0,0 +1,45 @@
1
+ AWS EC2 production account:
2
+ :provider: AWS
3
+ :service: Compute
4
+ :credentials:
5
+ :aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
6
+ :aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
7
+ :delay: 120
8
+ :exclude_resources:
9
+ AWS S3 account:
10
+ :provider: AWS
11
+ :service: Storage
12
+ :credentials:
13
+ :aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
14
+ :aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
15
+ :delay: 120
16
+ :exclude_resources:
17
+ # - :directories # (buckets)
18
+ - :files # secondary entity - does not work yet
19
+ AWS RDS account:
20
+ :provider: AWS
21
+ :service: RDS
22
+ :credentials:
23
+ :aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
24
+ :aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
25
+ :delay: 60
26
+ :exclude_resources:
27
+ - :parameters # secondary entity - does not work yet
28
+ AWS Route 53 account:
29
+ :provider: AWS
30
+ :service: DNS
31
+ :credentials:
32
+ :aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
33
+ :aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
34
+ :delay: 60
35
+ :exclude_resources:
36
+ - :records # secondary entity - does not work yet
37
+ AWS Elasticache account:
38
+ :provider: AWS
39
+ :service: Elasticache
40
+ :credentials:
41
+ :aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
42
+ :aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
43
+ :delay: 60
44
+ :exclude_resources:
45
+
@@ -0,0 +1,18 @@
1
+ development:
2
+ username: root
3
+ pool: 10 # Best to use at lease one connection per tracked account
4
+ timeout: 10000 # Leave this nice and long
5
+ adapter: sqlite3
6
+ database: db/cfo_development.sqlite3
7
+ test:
8
+ username: root
9
+ pool: 10 # Best to use at lease one connection per tracked account
10
+ timeout: 10000 # Leave this nice and long
11
+ adapter: sqlite3
12
+ database: db/cfo_test.sqlite3
13
+ production:
14
+ username: root
15
+ pool: 10 # Best to use at lease one connection per tracked account
16
+ timeout: 10000 # Leave this nice and long
17
+ adapter: sqlite3
18
+ database: db/cfo_production.sqlite3
data/config.ru ADDED
@@ -0,0 +1,2 @@
1
+ require './lib/cfo/service'
2
+ run CFO::Service
data/lib/cfo/cfo.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'cloud_cost_tracker'
2
+ module CFO
3
+ require File.join(File.dirname(__FILE__), 'resources/account')
4
+
5
+ # Returns the current RACK_ENV, or 'development' if not set
6
+ # @return [String] ENV[RACK_ENV] || ENV[RAILS_ENV] || development
7
+ def self.env ; ::CloudCostTracker.env end
8
+
9
+ # Connects to the database defined in db_file.
10
+ # Uses the YAML section indexed by CloudCostTracker#env.
11
+ # @param db_file the path to a Rails-style YAML DB config file.
12
+ # @return [Hash] the current envinronment's database configuration.
13
+ def self.connect_to_database(db_file = ENV['DB_CONFIG_FILE'])
14
+ ::CloudCostTracker.connect_to_database(db_file)
15
+ end
16
+
17
+ # Loads account information defined in account_file.
18
+ # @param account_file the path to a YAML file (see accounts.yml.example).
19
+ # @return [Array<Account>] an Array of Account resources.
20
+ def self.read_accounts(account_file = ENV['ACCOUNT_FILE'])
21
+ account_hash = ::CloudCostTracker.read_accounts(account_file)
22
+ account_hash.map do |(account_name, account_info)|
23
+ account = Account.new(account_info.merge(:name => account_name))
24
+ end
25
+ end
26
+
27
+ PROJECT_DIR = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
28
+ require "#{PROJECT_DIR}/lib/cfo/version"
29
+ # Defines the root URI path of the CFO service.
30
+ # @return [String] the root URI path of the CFO service
31
+ def self.root
32
+ "/api/v#{CFO::API_VERSION}"
33
+ end
34
+
35
+ # Forces the ActiveRecord timezone settings,
36
+ # which normally defaults to :local.
37
+ # Uncomment the code in this function ONLY if you're using this CFO as a
38
+ # standalone application, and need to force the time zone.
39
+ # See: http://api.rubyonrails.org/classes/ActiveRecord/Timestamp.html
40
+ def self.force_timezone
41
+ #ActiveRecord::Base.default_timezone = :utc
42
+ end
43
+
44
+ end
@@ -0,0 +1,80 @@
1
+ module CFO
2
+ # Encapsulates sevral algorithms for for changing one set of BillingRecords
3
+ # into another.
4
+ module RecordFilter
5
+ include CloudCostTracker::Billing
6
+
7
+ # Truncates each record in records so that its start time >= start_time
8
+ # and its stop time <= stop_time.
9
+ # @param [Array<BillingRecord>] records the records to filter
10
+ # @param [DateTime] start_time the earliest time allowed in the records
11
+ # @param [DateTime] stop_time the latest time allowed in the records
12
+ # @return [Array<BillingRecord>] the modified records
13
+ def self.truncate_by_time(records, start_time, stop_time)
14
+ records.each do |record|
15
+ record.start_time = start_time if record.start_time < start_time
16
+ record.stop_time = stop_time if record.stop_time > stop_time
17
+ duration = Time.parse(record.stop_time.to_s) -
18
+ Time.parse(record.start_time.to_s)
19
+ record.total_cost = duration * record.cost_per_hour / SECONDS_PER_HOUR
20
+ end
21
+ records
22
+ end
23
+
24
+ # Filters an Array of BillingRecords by start and stop times.
25
+ # @param [Array<BillingRecord>] records the records to filter
26
+ # @param [Array<Hash>] constraints an Array of Hash constraints on the
27
+ # BillingRecords to be retrieved. See {#CFO::Record#initialize}.
28
+ # This method acts only on the 'for_time' constraint key, if it exists.
29
+ # @return [Array<BillingRecord>] records that match the time constraints
30
+ # in @constraint_array
31
+ def self.by_time(records, constraints)
32
+ time_constraint = constraints.find {|c| c['for_time']}
33
+ return records if time_constraint == nil
34
+ time_strings = time_constraint['for_time']
35
+ return records if time_strings.empty?
36
+ times = (time_strings.map do |time| # Convert values to DateTime
37
+ time.is_a?(Time) ? time : Time.parse(time.to_s)
38
+ end).sort
39
+ times << Time.now if times.size.odd?
40
+ results = records.select do |record|
41
+ (record.start_time < times.last) and
42
+ (record.stop_time > times.first)
43
+ end
44
+ RecordFilter.truncate_by_time(results, times.first, times.last)
45
+ end
46
+
47
+ # Filters an Array of BillingRecords by BillingCode keys and values
48
+ # @param [Array<BillingRecord>] records the records to filter
49
+ # @param [Array<Hash>] constraints an Array of Hash constraints on the
50
+ # BillingRecords to be retrieved. See {#CFO::Record#initialize}.
51
+ # This method acts only on the 'for_code' constraint key, if it exists.
52
+ # @return [Array<BillingRecord>] records that match the code constraints
53
+ # in @constraint_array
54
+ def self.by_code(records, constraints)
55
+ code_constraint = constraints.find {|c| c['for_code']}
56
+ return records if code_constraint == nil
57
+ code_strings = code_constraint['for_code']
58
+ return records if code_strings.empty?
59
+ # Make pairs from the array of code_strings
60
+ code_strings << '*' if code_strings.size.odd?
61
+ code_pairs = (0..(code_strings.size/2)-1).map do |index|
62
+ [code_strings[index*2], code_strings[(index*2)+1]]
63
+ end
64
+ matching_records = Array.new
65
+ records.each do |record|
66
+ record.billing_codes.map do |code|
67
+ code_pairs.map do |code_pair|
68
+ if ((code_pair[0] == '*') and (code_pair[1] == code.value)) ||
69
+ ((code_pair[0] == code.key) and (code_pair[1] == '*')) ||
70
+ ((code_pair[0] == code.key) and (code_pair[1] == code.value))
71
+ matching_records << record
72
+ end
73
+ end
74
+ end
75
+ end
76
+ matching_records
77
+ end
78
+
79
+ end
80
+ end