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 +14 -0
- data/.powrc +1 -0
- data/.yardopts +1 -0
- data/API.md +146 -0
- data/Gemfile +2 -0
- data/Guardfile +10 -0
- data/README.md +141 -0
- data/Rakefile +20 -0
- data/cloud_financial_officer.gemspec +43 -0
- data/config/accounts.example.yml +45 -0
- data/config/database.example.yml +18 -0
- data/config.ru +2 -0
- data/lib/cfo/cfo.rb +44 -0
- data/lib/cfo/record_filter.rb +80 -0
- data/lib/cfo/record_grouper.rb +102 -0
- data/lib/cfo/resources/account.rb +53 -0
- data/lib/cfo/resources/cloud_resource.rb +74 -0
- data/lib/cfo/resources/report.rb +147 -0
- data/lib/cfo/service.rb +148 -0
- data/lib/cfo/version.rb +7 -0
- data/lib/cloud_financial_officer.rb +8 -0
- data/lib/tasks/rspec.rake +6 -0
- data/lib/tasks/service.rake +5 -0
- data/spec/factories.rb +35 -0
- data/spec/lib/cfo/record_filter_spec.rb +49 -0
- data/spec/lib/cfo/record_grouper_spec.rb +110 -0
- data/spec/lib/cfo/resources/account_spec.rb +46 -0
- data/spec/lib/cfo/resources/cloud_resource_spec.rb +90 -0
- data/spec/lib/cfo/resources/report_spec.rb +264 -0
- data/spec/lib/cfo/service_spec.rb +103 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/support/_configure_logging.rb +6 -0
- metadata +204 -0
data/.gitignore
ADDED
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
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
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
|