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