cloud_financial_officer 0.1.0

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