cloudcost 0.1.1 → 0.4.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.
- checksums.yaml +4 -4
- data/Dockerfile +12 -0
- data/Gemfile.lock +5 -5
- data/LICENSE +1 -1
- data/README.md +49 -3
- data/Rakefile +14 -0
- data/cloudcost.gemspec +2 -3
- data/lib/cloudcost/api_connection.rb +14 -0
- data/lib/cloudcost/api_token.rb +1 -1
- data/lib/cloudcost/cli.rb +73 -21
- data/lib/cloudcost/commands/csv_output.rb +24 -0
- data/lib/cloudcost/{server.rb → commands/server/server.rb} +1 -2
- data/lib/cloudcost/commands/server/server_influxdb_output.rb +26 -0
- data/lib/cloudcost/commands/server/server_list.rb +71 -0
- data/lib/cloudcost/commands/server/server_tabular_output.rb +92 -0
- data/lib/cloudcost/commands/server.rb +7 -0
- data/lib/cloudcost/commands/volume/volume.rb +56 -0
- data/lib/cloudcost/commands/volume/volume_influxdb_output.rb +37 -0
- data/lib/cloudcost/commands/volume/volume_list.rb +85 -0
- data/lib/cloudcost/commands/volume.rb +6 -0
- data/lib/cloudcost/error.rb +2 -0
- data/lib/cloudcost/pricing.rb +4 -6
- data/lib/cloudcost/version.rb +1 -1
- data/lib/cloudcost.rb +4 -3
- metadata +17 -8
- data/lib/cloudcost/server_list.rb +0 -159
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e1f7c30aba80a09bbcd150df43e48d0d7dfa47bd14460634e6997d451957d5f9
|
4
|
+
data.tar.gz: f0f628f8ffbc0f7cdcc08e9882758b11392b159e5293671ca0f24cd3a507ec68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4bf7307e6baca20b673899a5729106114e6933e7a719022350ba9c2a14724b98b1b625503d1015d052279dfe5e2b029567dbfd7089207565ababe3cdd359ef67
|
7
|
+
data.tar.gz: d3abdac8948c4e93a9df63458018b390f1c1ea6d92a2365e1b99d93de973c862989f6dfca797fe3008e1796790a137c98b644bf9809fe1ed5c6f3b2535af8ae6
|
data/Dockerfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
FROM ruby:3.0-alpine
|
2
|
+
|
3
|
+
ARG INFLUX_RELEASE=influxdb2-client-2.1.0-linux-amd64.tar.gz
|
4
|
+
|
5
|
+
RUN apk --no-cache add wget \
|
6
|
+
&& gem install cloudcost \
|
7
|
+
&& wget https://dl.influxdata.com/influxdb/releases/$INFLUX_RELEASE \
|
8
|
+
&& tar xvfz $INFLUX_RELEASE -C /usr/local/bin --strip-components=1 \
|
9
|
+
&& rm -f $INFLUX_RELEASE
|
10
|
+
|
11
|
+
ENTRYPOINT [ "cloudcost" ]
|
12
|
+
CMD [ "help" ]
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cloudcost (0.0
|
5
|
-
excon (~> 0.
|
4
|
+
cloudcost (0.4.0)
|
5
|
+
excon (~> 0.85.0)
|
6
6
|
parseconfig (~> 1.1.0)
|
7
7
|
terminal-table (~> 3.0.1)
|
8
8
|
thor (~> 1.1.0)
|
@@ -11,15 +11,15 @@ PATH
|
|
11
11
|
GEM
|
12
12
|
remote: https://rubygems.org/
|
13
13
|
specs:
|
14
|
-
excon (0.
|
14
|
+
excon (0.85.0)
|
15
15
|
parseconfig (1.1.0)
|
16
|
-
terminal-table (3.0.
|
16
|
+
terminal-table (3.0.2)
|
17
17
|
unicode-display_width (>= 1.1.1, < 3)
|
18
18
|
thor (1.1.0)
|
19
19
|
tty-cursor (0.7.1)
|
20
20
|
tty-spinner (0.9.3)
|
21
21
|
tty-cursor (~> 0.7)
|
22
|
-
unicode-display_width (2.
|
22
|
+
unicode-display_width (2.1.0)
|
23
23
|
|
24
24
|
PLATFORMS
|
25
25
|
ruby
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -19,8 +19,7 @@ gem install cloudcost
|
|
19
19
|
|
20
20
|
cloudcost does support the same auth configuration options as [cloudscale-cli](https://cloudscale-ch.github.io/cloudscale-cli/).
|
21
21
|
|
22
|
-
You can manage multiple profiles using `cloudscale.ini` files ([see here](https://cloudscale-ch.github.io/cloudscale-cli/auth/) for instructions).
|
23
|
-
|
22
|
+
You can manage multiple profiles using `cloudscale.ini` files ([see here](https://cloudscale-ch.github.io/cloudscale-cli/auth/) for instructions).
|
24
23
|
|
25
24
|
Otherwise you can export a `CLOUDSCALE_API_TOKEN` in your environment:
|
26
25
|
|
@@ -66,7 +65,7 @@ cloudcost servers --summary
|
|
66
65
|
|
67
66
|
#### Group and summarize by tag
|
68
67
|
|
69
|
-
By using the `--
|
68
|
+
By using the `--group-by` option, you can summarize usage by tag:
|
70
69
|
|
71
70
|
```sh
|
72
71
|
cloudcost servers --group-by budget-group
|
@@ -87,6 +86,19 @@ cloudcost servers --group-by budget-group --profile prod | \
|
|
87
86
|
influx write --bucket my-bucket --org my-org --token my-super-secret-auth-token
|
88
87
|
```
|
89
88
|
|
89
|
+
Example Flux-Query for loading data from InfluxDB:
|
90
|
+
|
91
|
+
```sh
|
92
|
+
influx query --org my-org --token my-super-secret-auth-token \
|
93
|
+
'from(bucket:"my-bucket")
|
94
|
+
|> range(start: -1d)
|
95
|
+
|> filter(fn: (r) =>
|
96
|
+
r._measurement == "cloudscaleServerCosts" and
|
97
|
+
r._field == "cost_per_day") and
|
98
|
+
r.profile == "prod" and
|
99
|
+
r.group == "my-budget-group"'
|
100
|
+
```
|
101
|
+
|
90
102
|
#### CSV Output
|
91
103
|
|
92
104
|
Output in CSV format instead of a table:
|
@@ -154,3 +166,37 @@ cloudcost server-tags --name ldap --set-tags owner=sys budget-group=base-infrast
|
|
154
166
|
```sh
|
155
167
|
cloudcost server-tags --name ldap --remove-tags owner budget-group
|
156
168
|
```
|
169
|
+
|
170
|
+
### Volumes
|
171
|
+
|
172
|
+
List all volumes:
|
173
|
+
|
174
|
+
```sh
|
175
|
+
cloudcost volumes
|
176
|
+
```
|
177
|
+
|
178
|
+
Only list volumes of type `bulk`
|
179
|
+
|
180
|
+
```sh
|
181
|
+
cloudcost volumes --type bulk
|
182
|
+
```
|
183
|
+
|
184
|
+
List volumes which are not attached to a server:
|
185
|
+
|
186
|
+
```sh
|
187
|
+
cloudcost volumes --no-attached
|
188
|
+
```
|
189
|
+
|
190
|
+
Filter volumes by names:
|
191
|
+
|
192
|
+
```sh
|
193
|
+
cloudcost volumes --name "pvc"
|
194
|
+
```
|
195
|
+
|
196
|
+
Output as InfluxDB Line Protocol:
|
197
|
+
|
198
|
+
```sh
|
199
|
+
cloudcost volumes --output influx --profile prod --no-attached
|
200
|
+
```
|
201
|
+
|
202
|
+
NOTE: The Line Protocol output includes a tag `state` which is either "attached", "unattached" or "all".
|
data/Rakefile
CHANGED
@@ -6,3 +6,17 @@ require "rubocop/rake_task"
|
|
6
6
|
RuboCop::RakeTask.new
|
7
7
|
|
8
8
|
task default: %i[rubocop]
|
9
|
+
|
10
|
+
DOCKER_REGISTRY = "registry.puzzle.ch/puzzle/cloudcost"
|
11
|
+
|
12
|
+
desc "Build the docker image and tag it with the current version."
|
13
|
+
task :docker_build do
|
14
|
+
puts command = "docker build -t #{DOCKER_REGISTRY}:#{Cloudcost::VERSION} ."
|
15
|
+
puts `#{command}`
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Push the newest docker image."
|
19
|
+
task :docker_push do
|
20
|
+
puts command = "docker push #{DOCKER_REGISTRY}:#{Cloudcost::VERSION}"
|
21
|
+
puts `#{command}`
|
22
|
+
end
|
data/cloudcost.gemspec
CHANGED
@@ -10,8 +10,7 @@ Gem::Specification.new do |s|
|
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.authors = ["Nik Wolfgramm"]
|
13
|
-
s.description = "Calculate cloudscale.ch server costs from
|
14
|
-
s.email = "wolfgramm@puzzle.ch"
|
13
|
+
s.description = "Calculate cloudscale.ch server costs from the current deployment"
|
15
14
|
s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
16
15
|
s.require_paths = ["lib"]
|
17
16
|
s.required_ruby_version = ">= 2.7"
|
@@ -20,7 +19,7 @@ Gem::Specification.new do |s|
|
|
20
19
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
21
20
|
s.license = "MIT"
|
22
21
|
|
23
|
-
s.add_dependency("excon", "~> 0.
|
22
|
+
s.add_dependency("excon", "~> 0.85.0")
|
24
23
|
s.add_dependency("parseconfig", "~> 1.1.0")
|
25
24
|
s.add_dependency("terminal-table", "~> 3.0.1")
|
26
25
|
s.add_dependency("thor", "~> 1.1.0")
|
@@ -4,6 +4,7 @@ require "excon"
|
|
4
4
|
require "json"
|
5
5
|
|
6
6
|
module Cloudcost
|
7
|
+
# Connecting to and accessing the cloudscale.ch API
|
7
8
|
class ApiConnection
|
8
9
|
API_URL = "https://api.cloudscale.ch"
|
9
10
|
|
@@ -38,6 +39,19 @@ module Cloudcost
|
|
38
39
|
)
|
39
40
|
end
|
40
41
|
|
42
|
+
def get_volumes(options = {})
|
43
|
+
volumes = get_resource("volumes", options)
|
44
|
+
volumes = volumes.reject { |volume| volume[:tags].key?(options[:missing_tag].to_sym) } if options[:missing_tag]
|
45
|
+
volumes = volumes.select { |volume| /#{options[:name]}/.match? volume[:name] } if options[:name]
|
46
|
+
volumes = volumes.select { |volume| /#{options[:type]}/.match? volume[:type] } if options[:type]
|
47
|
+
unless options[:attached].nil?
|
48
|
+
volumes = volumes.select do |volume|
|
49
|
+
(volume[:servers].size.positive?) == options[:attached]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
volumes
|
53
|
+
end
|
54
|
+
|
41
55
|
private
|
42
56
|
|
43
57
|
def new_connection
|
data/lib/cloudcost/api_token.rb
CHANGED
data/lib/cloudcost/cli.rb
CHANGED
@@ -4,6 +4,7 @@ require "thor"
|
|
4
4
|
require "tty-spinner"
|
5
5
|
|
6
6
|
module Cloudcost
|
7
|
+
# Implementaion of CLI functionality
|
7
8
|
class CLI < Thor
|
8
9
|
# Error raised by this runner
|
9
10
|
Error = Class.new(StandardError)
|
@@ -38,22 +39,18 @@ module Cloudcost
|
|
38
39
|
spinner = TTY::Spinner.new("[:spinner] Calculating costs...", clear: options[:csv])
|
39
40
|
spinner.auto_spin
|
40
41
|
end
|
41
|
-
|
42
|
-
spinner
|
42
|
+
output_servers(servers, options) do |result|
|
43
|
+
spinner&.success("(done)")
|
43
44
|
puts result
|
44
45
|
end
|
45
46
|
rescue Excon::Error, TokenError, ProfileError, PricingError => e
|
46
47
|
error_message = "ERROR: #{e.message}"
|
47
|
-
|
48
|
-
spinner.error("(#{error_message})")
|
49
|
-
else
|
50
|
-
puts error_message
|
51
|
-
end
|
48
|
+
spinner ? spinner.error(error_message) : puts(error_message)
|
52
49
|
end
|
53
50
|
|
54
51
|
desc "server-tags", "show and assign tags of servers"
|
55
52
|
option :name, desc: "filter name by regex", aliases: %w[-n]
|
56
|
-
option :tag, desc: "filter
|
53
|
+
option :tag, desc: "filter by tag", aliases: %w[-t]
|
57
54
|
option :set_tags,
|
58
55
|
desc: "set tags",
|
59
56
|
aliases: %w[-T],
|
@@ -79,19 +76,40 @@ module Cloudcost
|
|
79
76
|
(options[:remove_tags] || []).each do |tag|
|
80
77
|
tags.reject! { |k| k == tag.to_sym }
|
81
78
|
end
|
82
|
-
|
83
|
-
|
79
|
+
begin
|
80
|
+
api_connection(options).set_server_tags(server.uuid, tags)
|
81
|
+
spinner.success
|
82
|
+
rescue Excon::Error => e
|
83
|
+
spinner.error "ERROR: #{e.message}"
|
84
|
+
end
|
84
85
|
end
|
85
86
|
end
|
86
87
|
spinners.auto_spin
|
87
88
|
end
|
88
|
-
rescue
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
89
|
+
rescue Cloudcost::TokenError, Cloudcost::ProfileError => e
|
90
|
+
puts "ERROR: #{e.message}"
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "volumes", "explore volumes"
|
94
|
+
option :name, desc: "filter name by regex", aliases: %w[-n]
|
95
|
+
option :tag, desc: "filter by tag", aliases: %w[-t]
|
96
|
+
option :summary, desc: "display totals only", type: :boolean, aliases: %w[-S]
|
97
|
+
option :type, enum: %w[ssd bulk], desc: "volume type"
|
98
|
+
option :attached, type: :boolean, desc: "volume attached to servers"
|
99
|
+
option :output, default: "table", enum: %w[table csv influx], desc: "output format", aliases: %w[-o]
|
100
|
+
def volumes
|
101
|
+
volumes = load_volumes(options)
|
102
|
+
if options[:output] == "table"
|
103
|
+
spinner = TTY::Spinner.new("[:spinner] Calculating costs...", clear: options[:csv])
|
104
|
+
spinner.auto_spin
|
105
|
+
end
|
106
|
+
output_volumes(volumes, options) do |result|
|
107
|
+
spinner&.success("(done)")
|
108
|
+
puts result
|
94
109
|
end
|
110
|
+
rescue Excon::Error, Cloudcost::TokenError, Cloudcost::ProfileError, Cloudcost::PricingError => e
|
111
|
+
error_message = "ERROR: #{e.message}"
|
112
|
+
spinner ? spinner.error(error_message) : puts(error_message)
|
95
113
|
end
|
96
114
|
|
97
115
|
no_tasks do
|
@@ -114,21 +132,55 @@ module Cloudcost
|
|
114
132
|
spinner = TTY::Spinner.new("[:spinner] Loading servers...", clear: options[:csv])
|
115
133
|
spinner.auto_spin
|
116
134
|
end
|
117
|
-
servers = api_connection(options).get_servers(options).map { |server| Server.new(server) }
|
118
|
-
spinner
|
135
|
+
servers = api_connection(options).get_servers(options).map { |server| Cloudcost::Server.new(server) }
|
136
|
+
spinner&.success "(#{servers.size} found)"
|
119
137
|
servers
|
138
|
+
rescue Excon::Error => e
|
139
|
+
spinner&.error "ERROR: #{e.message}"
|
140
|
+
[]
|
141
|
+
end
|
142
|
+
|
143
|
+
def load_volumes(options)
|
144
|
+
if options[:output] == "table"
|
145
|
+
spinner = TTY::Spinner.new("[:spinner] Loading volumes...", clear: options[:csv])
|
146
|
+
spinner.auto_spin
|
147
|
+
end
|
148
|
+
volumes = api_connection(options).get_volumes(options).map { |volume| Cloudcost::Volume.new(volume) }
|
149
|
+
spinner&.success "(#{volumes.size} found)"
|
150
|
+
volumes
|
151
|
+
rescue Excon::Error => e
|
152
|
+
spinner&.error "\ERROR: #{e.message}"
|
153
|
+
[]
|
120
154
|
end
|
121
155
|
|
122
|
-
def
|
123
|
-
if
|
124
|
-
yield
|
156
|
+
def output_servers(servers, options)
|
157
|
+
if servers.empty?
|
158
|
+
yield "WARNING: No servers found."
|
159
|
+
elsif options[:group_by]
|
160
|
+
yield Cloudcost::ServerList.new(servers, options).grouped_costs
|
125
161
|
elsif options[:output] == "csv"
|
126
162
|
yield Cloudcost::ServerList.new(servers, options).to_csv
|
127
163
|
else
|
164
|
+
if options[:output] == "influx"
|
165
|
+
puts "ERROR: group-by option required for influx output"
|
166
|
+
exit 1
|
167
|
+
end
|
128
168
|
yield Cloudcost::ServerList.new(servers, options).cost_table
|
129
169
|
end
|
130
170
|
end
|
131
171
|
|
172
|
+
def output_volumes(volumes, options)
|
173
|
+
if volumes.empty?
|
174
|
+
yield "WARNING: No volumes found."
|
175
|
+
elsif options[:output] == "csv"
|
176
|
+
yield Cloudcost::VolumeList.new(volumes, options).to_csv
|
177
|
+
elsif options[:output] == "influx"
|
178
|
+
yield Cloudcost::VolumeList.new(volumes, options).totals_influx_line_protocol
|
179
|
+
else
|
180
|
+
yield Cloudcost::VolumeList.new(volumes, options).cost_table
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
132
184
|
def tag_option_to_s(options)
|
133
185
|
messages = []
|
134
186
|
messages << "set tags \"#{options[:set_tags].join(", ")}\"" if options[:set_tags]
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudcost
|
4
|
+
# generic CSV output methods
|
5
|
+
module CsvOutput
|
6
|
+
def groups_to_csv(group_rows)
|
7
|
+
CSV.generate do |csv|
|
8
|
+
csv << headings
|
9
|
+
group_rows.each { |row| csv << row }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_csv
|
14
|
+
CSV.generate do |csv|
|
15
|
+
csv << headings
|
16
|
+
if @options[:summary]
|
17
|
+
csv << totals
|
18
|
+
else
|
19
|
+
rows.each { |row| csv << row }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,12 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "cloudcost/pricing"
|
4
|
-
|
5
3
|
module Cloudcost
|
6
4
|
def self.tags_to_s(tag_hash = [])
|
7
5
|
tag_hash.map { |k, v| "#{k}=#{v}" }.join(" ")
|
8
6
|
end
|
9
7
|
|
8
|
+
# Representation of cloudscale.ch server object
|
10
9
|
class Server
|
11
10
|
attr_accessor :data
|
12
11
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudcost
|
4
|
+
# InfluxDB output methods for the ServerList class
|
5
|
+
module ServerInfluxdbOutput
|
6
|
+
def grouped_influx_line_protocol(group_rows)
|
7
|
+
lines = []
|
8
|
+
group_rows.each do |row|
|
9
|
+
[
|
10
|
+
{ field: "server_count", position: 1, unit: "i" },
|
11
|
+
{ field: "vcpus", position: 2, unit: "i" },
|
12
|
+
{ field: "memory_gb", position: 3, unit: "i" },
|
13
|
+
{ field: "ssd_gb", position: 4, unit: "i" },
|
14
|
+
{ field: "bulk_gb", position: 5, unit: "i" },
|
15
|
+
{ field: "chf_per_day", position: 6, unit: "" }
|
16
|
+
].each do |field|
|
17
|
+
lines << %(
|
18
|
+
cloudscaleServerCosts,group=#{row[0]},profile=#{@options[:profile] || "?"}
|
19
|
+
#{field[:field]}=#{row[field[:position]]}#{field[:unit]}
|
20
|
+
).gsub(/\s+/, " ").strip
|
21
|
+
end
|
22
|
+
end
|
23
|
+
lines.join("\n")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudcost
|
4
|
+
# ServerList represents a list of servers and integrates several output methods
|
5
|
+
class ServerList
|
6
|
+
include Cloudcost::ServerTabularOutput
|
7
|
+
include Cloudcost::ServerInfluxdbOutput
|
8
|
+
include Cloudcost::CsvOutput
|
9
|
+
|
10
|
+
def initialize(servers, options = {})
|
11
|
+
@servers = servers
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def calculate_totals(servers = @servers)
|
16
|
+
totals = { vcpu: 0, memory: 0, ssd: 0, bulk: 0, cost: 0.0 }
|
17
|
+
servers.each do |server|
|
18
|
+
totals[:vcpu] += server.vcpu_count
|
19
|
+
totals[:memory] += server.memory_gb
|
20
|
+
totals[:ssd] += server.storage_size(:ssd)
|
21
|
+
totals[:bulk] += server.storage_size(:bulk)
|
22
|
+
totals[:cost] += server.total_costs_per_day
|
23
|
+
end
|
24
|
+
totals
|
25
|
+
end
|
26
|
+
|
27
|
+
def totals(servers = @servers)
|
28
|
+
totals = calculate_totals(servers)
|
29
|
+
total_row = @options[:summary] ? %w[Total] : ["Total", "", "", ""]
|
30
|
+
total_row.concat [
|
31
|
+
totals[:vcpu],
|
32
|
+
totals[:memory],
|
33
|
+
totals[:ssd],
|
34
|
+
totals[:bulk],
|
35
|
+
format("%.2f", totals[:cost].round(2)),
|
36
|
+
format("%.2f", (totals[:cost] * 30).round(2))
|
37
|
+
]
|
38
|
+
end
|
39
|
+
|
40
|
+
def grouped_costs
|
41
|
+
no_tag = "<no-tag>"
|
42
|
+
group_rows = @servers.group_by { |s| s.tags[@options[:group_by].to_sym] || no_tag }.map do |name, servers|
|
43
|
+
server_groups_data(name, servers).values.flatten
|
44
|
+
end
|
45
|
+
group_rows.sort! { |a, b| a[0] == no_tag ? 1 : a[0] <=> b[0] }
|
46
|
+
case @options[:output]
|
47
|
+
when "csv"
|
48
|
+
groups_to_csv(group_rows)
|
49
|
+
when "influx"
|
50
|
+
grouped_influx_line_protocol(group_rows)
|
51
|
+
else
|
52
|
+
grouped_cost_table(group_rows)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def server_groups_data(name, servers)
|
57
|
+
data = { name: name, count: 0, vcpu: 0, memory: 0, ssd: 0, bulk: 0, costs_daily: 0 }
|
58
|
+
servers.each do |server|
|
59
|
+
data[:count] += 1
|
60
|
+
data[:vcpu] += server.vcpu_count
|
61
|
+
data[:memory] += server.memory_gb
|
62
|
+
data[:ssd] += server.storage_size(:ssd)
|
63
|
+
data[:bulk] += server.storage_size(:bulk)
|
64
|
+
data[:costs_daily] += server.total_costs_per_day
|
65
|
+
end
|
66
|
+
data[:costs_monthly] = (data[:costs_daily] * 30).round(2)
|
67
|
+
data[:costs_daily] = data[:costs_daily].round(2)
|
68
|
+
data
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "terminal-table"
|
4
|
+
|
5
|
+
module Cloudcost
|
6
|
+
# Tabular output methods for the ServerList class
|
7
|
+
module ServerTabularOutput
|
8
|
+
def headings
|
9
|
+
headings = if @options[:summary]
|
10
|
+
[""]
|
11
|
+
elsif @options[:group_by]
|
12
|
+
%w[Group Servers]
|
13
|
+
else
|
14
|
+
%w[Name UUID Flavor Tags]
|
15
|
+
end
|
16
|
+
headings.concat ["vCPU's", "Memory [GB]", "SSD [GB]", "Bulk [GB]", "CHF/day", "CHF/30-days"]
|
17
|
+
end
|
18
|
+
|
19
|
+
def rows
|
20
|
+
rows = []
|
21
|
+
@servers.sort_by(&:name).map do |server|
|
22
|
+
rows << [
|
23
|
+
server.name,
|
24
|
+
server.uuid,
|
25
|
+
server.flavor,
|
26
|
+
server.tags_to_s,
|
27
|
+
server.vcpu_count,
|
28
|
+
server.memory_gb,
|
29
|
+
server.storage_size(:ssd),
|
30
|
+
server.storage_size(:bulk),
|
31
|
+
format("%.2f", server.total_costs_per_day.round(2)),
|
32
|
+
format("%.2f", (server.total_costs_per_day * 30).round(2))
|
33
|
+
]
|
34
|
+
end
|
35
|
+
rows
|
36
|
+
end
|
37
|
+
|
38
|
+
def totals(servers = @servers)
|
39
|
+
totals = calculate_totals(servers)
|
40
|
+
total_row = @options[:summary] ? %w[Total] : ["Total", "", "", ""]
|
41
|
+
total_row.concat [
|
42
|
+
totals[:vcpu],
|
43
|
+
totals[:memory],
|
44
|
+
totals[:ssd],
|
45
|
+
totals[:bulk],
|
46
|
+
format("%.2f", totals[:cost].round(2)),
|
47
|
+
format("%.2f", (totals[:cost] * 30).round(2))
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
def tags_table
|
52
|
+
Terminal::Table.new do |t|
|
53
|
+
t.title = "cloudscale.ch server tags"
|
54
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
55
|
+
t.headings = %w[Name UUID Tags]
|
56
|
+
t.rows = @servers.sort_by(&:name).map do |server|
|
57
|
+
[
|
58
|
+
server.name,
|
59
|
+
server.uuid,
|
60
|
+
server.tags_to_s
|
61
|
+
]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def cost_table
|
67
|
+
table = Terminal::Table.new do |t|
|
68
|
+
t.title = "cloudscale.ch server costs"
|
69
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
70
|
+
t.headings = headings
|
71
|
+
t.rows = rows unless @options[:summary]
|
72
|
+
end
|
73
|
+
|
74
|
+
table.add_separator unless @options[:summary]
|
75
|
+
table.add_row totals
|
76
|
+
first_number_row = @options[:summary] ? 1 : 2
|
77
|
+
(first_number_row..table.columns.size).each { |column| table.align_column(column, :right) }
|
78
|
+
table
|
79
|
+
end
|
80
|
+
|
81
|
+
def grouped_cost_table(group_rows)
|
82
|
+
table = Terminal::Table.new do |t|
|
83
|
+
t.title = "cloudscale.ch server costs grouped by tag \"#{@options[:group_by]}\""
|
84
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
85
|
+
t.headings = headings
|
86
|
+
end
|
87
|
+
table.rows = group_rows
|
88
|
+
(1..table.columns.size).each { |column| table.align_column(column, :right) }
|
89
|
+
table
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudcost
|
4
|
+
# Representation of cloudscale.ch volume object
|
5
|
+
class Volume
|
6
|
+
attr_accessor :data
|
7
|
+
|
8
|
+
def initialize(data)
|
9
|
+
@data = data
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
@data[:name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def uuid
|
17
|
+
@data[:uuid]
|
18
|
+
end
|
19
|
+
|
20
|
+
def type
|
21
|
+
@data[:type]
|
22
|
+
end
|
23
|
+
|
24
|
+
def servers
|
25
|
+
@data[:servers]
|
26
|
+
end
|
27
|
+
|
28
|
+
def server_name
|
29
|
+
servers.size.positive? ? servers.first[:name] : ""
|
30
|
+
end
|
31
|
+
|
32
|
+
def attached?
|
33
|
+
servers.size.positive?
|
34
|
+
end
|
35
|
+
|
36
|
+
def server_uuids
|
37
|
+
@data[:server_uuids]
|
38
|
+
end
|
39
|
+
|
40
|
+
def tags
|
41
|
+
@data[:tags]
|
42
|
+
end
|
43
|
+
|
44
|
+
def size_gb
|
45
|
+
@data[:size_gb]
|
46
|
+
end
|
47
|
+
|
48
|
+
def tags_to_s
|
49
|
+
Cloudcost.tags_to_s(tags)
|
50
|
+
end
|
51
|
+
|
52
|
+
def costs_per_day
|
53
|
+
Pricing.storage_costs_per_day(type, size_gb)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudcost
|
4
|
+
# InfluxDB output methods for the ServerList class
|
5
|
+
module VolumeInfluxdbOutput
|
6
|
+
def totals_influx_line_protocol
|
7
|
+
lines = []
|
8
|
+
tag_set = [
|
9
|
+
"profile=#{@options[:profile] || "?"}",
|
10
|
+
"state=#{volumes_attached_state}"
|
11
|
+
]
|
12
|
+
metrics = calculate_totals
|
13
|
+
[
|
14
|
+
{ field: "ssd_gb", key: :size_ssd, unit: "i" },
|
15
|
+
{ field: "bulk_gb", key: :size_bulk, unit: "i" },
|
16
|
+
{ field: "chf_per_day", key: :size, unit: "" }
|
17
|
+
].each do |field|
|
18
|
+
lines << %(
|
19
|
+
cloudscaleVolumeCosts,#{tag_set.join(",")}
|
20
|
+
#{field[:field]}=#{metrics[field[:key]]}#{field[:unit]}
|
21
|
+
).gsub(/\s+/, " ").strip
|
22
|
+
end
|
23
|
+
lines.join("\n")
|
24
|
+
end
|
25
|
+
|
26
|
+
def volumes_attached_state
|
27
|
+
case @options[:attached]
|
28
|
+
when true
|
29
|
+
"attached"
|
30
|
+
when false
|
31
|
+
"unattached"
|
32
|
+
else
|
33
|
+
"all"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "terminal-table"
|
4
|
+
|
5
|
+
module Cloudcost
|
6
|
+
# volumeList represents a list of volumes and integrates several output methods
|
7
|
+
class VolumeList
|
8
|
+
include Cloudcost::CsvOutput
|
9
|
+
include Cloudcost::VolumeInfluxdbOutput
|
10
|
+
|
11
|
+
def initialize(volumes, options = {})
|
12
|
+
@volumes = volumes
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def calculate_totals(volumes = @volumes)
|
17
|
+
total = { size: 0, size_ssd: 0, size_bulk: 0, cost: 0.0 }
|
18
|
+
volumes.each do |volume|
|
19
|
+
total[:size] += volume.size_gb
|
20
|
+
total["size_#{volume.type}".to_sym] += volume.size_gb if %w[ssd bulk].include? volume.type
|
21
|
+
total[:cost] += volume.costs_per_day
|
22
|
+
end
|
23
|
+
total
|
24
|
+
end
|
25
|
+
|
26
|
+
def totals(volumes = @volumes)
|
27
|
+
total = calculate_totals(volumes)
|
28
|
+
total_row = @options[:summary] ? %w[Total] : ["Total", "", "", "", ""]
|
29
|
+
if @options[:summary]
|
30
|
+
total_row.concat [
|
31
|
+
total[:size_ssd],
|
32
|
+
total[:size_bulk],
|
33
|
+
total[:size]
|
34
|
+
]
|
35
|
+
else
|
36
|
+
total_row.concat [total[:size]]
|
37
|
+
end
|
38
|
+
total_row.concat [
|
39
|
+
format("%.2f", total[:cost].round(2)),
|
40
|
+
format("%.2f", (total[:cost] * 30).round(2))
|
41
|
+
]
|
42
|
+
end
|
43
|
+
|
44
|
+
def headings
|
45
|
+
headings = if @options[:summary]
|
46
|
+
["", "SSD [GB]", "Bulk [GB]", "Total [GB]"]
|
47
|
+
else
|
48
|
+
["Name", "UUID", "Type", "Servers", "Tags", "Size [GB]"]
|
49
|
+
end
|
50
|
+
headings.concat ["CHF/day", "CHF/30-days"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def rows
|
54
|
+
rows = []
|
55
|
+
@volumes.sort_by(&:name).map do |volume|
|
56
|
+
rows << [
|
57
|
+
volume.name,
|
58
|
+
volume.uuid,
|
59
|
+
volume.type,
|
60
|
+
volume.server_name,
|
61
|
+
volume.tags_to_s,
|
62
|
+
volume.size_gb,
|
63
|
+
format("%.2f", volume.costs_per_day.round(2)),
|
64
|
+
format("%.2f", (volume.costs_per_day * 30).round(2))
|
65
|
+
]
|
66
|
+
end
|
67
|
+
rows
|
68
|
+
end
|
69
|
+
|
70
|
+
def cost_table
|
71
|
+
table = Terminal::Table.new do |t|
|
72
|
+
t.title = "cloudscale.ch volume costs"
|
73
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
74
|
+
t.headings = headings
|
75
|
+
t.rows = rows unless @options[:summary]
|
76
|
+
end
|
77
|
+
|
78
|
+
table.add_separator unless @options[:summary]
|
79
|
+
table.add_row totals
|
80
|
+
first_number_row = @options[:summary] ? 1 : 2
|
81
|
+
(first_number_row..table.columns.size).each { |column| table.align_column(column, :right) }
|
82
|
+
table
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/cloudcost/error.rb
CHANGED
data/lib/cloudcost/pricing.rb
CHANGED
@@ -2,14 +2,12 @@
|
|
2
2
|
|
3
3
|
require "yaml"
|
4
4
|
|
5
|
-
PRICING = YAML.load_file(
|
6
|
-
|
7
|
-
)
|
5
|
+
PRICING = YAML.load_file(File.join(
|
6
|
+
File.expand_path("../..", __dir__), "data/pricing.yml"
|
7
|
+
))
|
8
8
|
|
9
9
|
module Cloudcost
|
10
|
-
class
|
11
|
-
end
|
12
|
-
|
10
|
+
# pricing class which implements cost calculation methods for cloudscale.ch resources
|
13
11
|
module Pricing
|
14
12
|
def self.server_costs_per_day(flavor)
|
15
13
|
PRICING["servers"][flavor] || raise(PricingError, "#{flavor} flavor not found in pricing.yml")
|
data/lib/cloudcost/version.rb
CHANGED
data/lib/cloudcost.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "terminal-table"
|
4
3
|
require "csv"
|
5
4
|
|
6
5
|
require "cloudcost/version"
|
6
|
+
require "cloudcost/error"
|
7
7
|
require "cloudcost/api_token"
|
8
8
|
require "cloudcost/api_connection"
|
9
|
-
require "cloudcost/
|
10
|
-
require "cloudcost/
|
9
|
+
require "cloudcost/pricing"
|
10
|
+
require "cloudcost/commands/server"
|
11
|
+
require "cloudcost/commands/volume"
|
11
12
|
require "cloudcost/cli"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cloudcost
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nik Wolfgramm
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-09-
|
11
|
+
date: 2021-09-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: excon
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.85.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.85.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: parseconfig
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,8 +80,8 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.9.3
|
83
|
-
description: Calculate cloudscale.ch server costs from
|
84
|
-
email:
|
83
|
+
description: Calculate cloudscale.ch server costs from the current deployment
|
84
|
+
email:
|
85
85
|
executables:
|
86
86
|
- cloudcost
|
87
87
|
extensions: []
|
@@ -89,6 +89,7 @@ extra_rdoc_files: []
|
|
89
89
|
files:
|
90
90
|
- ".gitignore"
|
91
91
|
- ".rubocop.yml"
|
92
|
+
- Dockerfile
|
92
93
|
- Gemfile
|
93
94
|
- Gemfile.lock
|
94
95
|
- LICENSE
|
@@ -101,10 +102,18 @@ files:
|
|
101
102
|
- lib/cloudcost/api_connection.rb
|
102
103
|
- lib/cloudcost/api_token.rb
|
103
104
|
- lib/cloudcost/cli.rb
|
105
|
+
- lib/cloudcost/commands/csv_output.rb
|
106
|
+
- lib/cloudcost/commands/server.rb
|
107
|
+
- lib/cloudcost/commands/server/server.rb
|
108
|
+
- lib/cloudcost/commands/server/server_influxdb_output.rb
|
109
|
+
- lib/cloudcost/commands/server/server_list.rb
|
110
|
+
- lib/cloudcost/commands/server/server_tabular_output.rb
|
111
|
+
- lib/cloudcost/commands/volume.rb
|
112
|
+
- lib/cloudcost/commands/volume/volume.rb
|
113
|
+
- lib/cloudcost/commands/volume/volume_influxdb_output.rb
|
114
|
+
- lib/cloudcost/commands/volume/volume_list.rb
|
104
115
|
- lib/cloudcost/error.rb
|
105
116
|
- lib/cloudcost/pricing.rb
|
106
|
-
- lib/cloudcost/server.rb
|
107
|
-
- lib/cloudcost/server_list.rb
|
108
117
|
- lib/cloudcost/version.rb
|
109
118
|
homepage: https://gitlab.puzzle.ch/nwolfgramm/cloudcost
|
110
119
|
licenses:
|
@@ -1,159 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Cloudcost
|
4
|
-
class ServerList
|
5
|
-
def initialize(servers, options = {})
|
6
|
-
@servers = servers
|
7
|
-
@options = options
|
8
|
-
end
|
9
|
-
|
10
|
-
def calculate_totals(servers = @servers)
|
11
|
-
totals = { vcpu: 0, memory: 0, ssd: 0, bulk: 0, cost: 0.0 }
|
12
|
-
servers.each do |server|
|
13
|
-
totals[:vcpu] += server.vcpu_count
|
14
|
-
totals[:memory] += server.memory_gb
|
15
|
-
totals[:ssd] += server.storage_size(:ssd)
|
16
|
-
totals[:bulk] += server.storage_size(:bulk)
|
17
|
-
totals[:cost] += server.total_costs_per_day
|
18
|
-
end
|
19
|
-
totals
|
20
|
-
end
|
21
|
-
|
22
|
-
def headings
|
23
|
-
headings = if @options[:summary]
|
24
|
-
[""]
|
25
|
-
elsif @options[:group_by]
|
26
|
-
["Group", "Servers"]
|
27
|
-
else
|
28
|
-
%w[Name UUID Flavor Tags]
|
29
|
-
end
|
30
|
-
headings.concat ["vCPU's", "Memory [GB]", "SSD [GB]", "Bulk [GB]", "CHF/day", "CHF/30-days"]
|
31
|
-
end
|
32
|
-
|
33
|
-
def rows
|
34
|
-
rows = []
|
35
|
-
@servers.sort_by(&:name).map do |server|
|
36
|
-
rows << [
|
37
|
-
server.name,
|
38
|
-
server.uuid,
|
39
|
-
server.flavor,
|
40
|
-
server.tags_to_s,
|
41
|
-
server.vcpu_count,
|
42
|
-
server.memory_gb,
|
43
|
-
server.storage_size(:ssd),
|
44
|
-
server.storage_size(:bulk),
|
45
|
-
format("%.2f", server.total_costs_per_day.round(2)),
|
46
|
-
format("%.2f", (server.total_costs_per_day * 30).round(2))
|
47
|
-
]
|
48
|
-
end
|
49
|
-
rows
|
50
|
-
end
|
51
|
-
|
52
|
-
def totals(servers = @servers)
|
53
|
-
totals = calculate_totals(servers)
|
54
|
-
total_row = @options[:summary] ? %w[Total] : ["Total", "", "", ""]
|
55
|
-
total_row.concat [
|
56
|
-
totals[:vcpu],
|
57
|
-
totals[:memory],
|
58
|
-
totals[:ssd],
|
59
|
-
totals[:bulk],
|
60
|
-
format("%.2f", totals[:cost].round(2)),
|
61
|
-
format("%.2f", (totals[:cost] * 30).round(2))
|
62
|
-
]
|
63
|
-
end
|
64
|
-
|
65
|
-
def tags_table
|
66
|
-
Terminal::Table.new do |t|
|
67
|
-
t.title = "cloudscale.ch server tags"
|
68
|
-
t.title += " (#{@options[:profile]})" if @options[:profile]
|
69
|
-
t.headings = %w[Name UUID Tags]
|
70
|
-
t.rows = @servers.sort_by(&:name).map do |server|
|
71
|
-
[
|
72
|
-
server.name,
|
73
|
-
server.uuid,
|
74
|
-
server.tags_to_s
|
75
|
-
]
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def cost_table
|
81
|
-
table = Terminal::Table.new do |t|
|
82
|
-
t.title = "cloudscale.ch server costs"
|
83
|
-
t.title += " (#{@options[:profile]})" if @options[:profile]
|
84
|
-
t.headings = headings
|
85
|
-
t.rows = rows unless @options[:summary]
|
86
|
-
end
|
87
|
-
|
88
|
-
table.add_separator unless @options[:summary]
|
89
|
-
table.add_row totals
|
90
|
-
first_number_row = @options[:summary] ? 1 : 2
|
91
|
-
(first_number_row..table.columns.size).each { |column| table.align_column(column, :right) }
|
92
|
-
table
|
93
|
-
end
|
94
|
-
|
95
|
-
def grouped_cost_table
|
96
|
-
no_tag = "<no-tag>"
|
97
|
-
group_rows = @servers.group_by {|s| s.tags[@options[:group_by].to_sym] || no_tag }.map do |name, servers|
|
98
|
-
server_groups_data(name, servers).values.flatten
|
99
|
-
end.sort {|a, b| a[0] == no_tag ? 1 : a[0] <=> b[0] }
|
100
|
-
if @options[:output] == "csv"
|
101
|
-
CSV.generate do |csv|
|
102
|
-
csv << headings
|
103
|
-
group_rows.each { |row| csv << row }
|
104
|
-
end
|
105
|
-
elsif @options[:output] == "influx"
|
106
|
-
lines = []
|
107
|
-
group_rows.each do |row|
|
108
|
-
[
|
109
|
-
{ field: "server_count", position: 1, unit: "i" },
|
110
|
-
{ field: "vcpus", position: 2, unit: "i" },
|
111
|
-
{ field: "memory_gb", position: 3, unit: "i" },
|
112
|
-
{ field: "ssd_gb", position: 4, unit: "i" },
|
113
|
-
{ field: "bulk_gb", position: 5, unit: "i" },
|
114
|
-
{ field: "cost_per_day", position: 6, unit: "" },
|
115
|
-
].each do |field|
|
116
|
-
lines << "cloudscaleServerCosts,grouped_by=#{@options[:group_by]},group=#{row[0]},environment=#{@options[:profile] || "default"},currency=CHF #{field[:field]}=#{row[field[:position]]}#{field[:unit]}"
|
117
|
-
end
|
118
|
-
end
|
119
|
-
lines.join("\n")
|
120
|
-
else
|
121
|
-
table = Terminal::Table.new do |t|
|
122
|
-
t.title = "cloudscale.ch server costs grouped by tag \"#{@options[:group_by]}\""
|
123
|
-
t.title += " (#{@options[:profile]})" if @options[:profile]
|
124
|
-
t.headings = headings
|
125
|
-
end
|
126
|
-
table.rows = group_rows
|
127
|
-
(1..table.columns.size).each { |column| table.align_column(column, :right) }
|
128
|
-
table
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def server_groups_data(name, servers)
|
133
|
-
data = { name: name, count: 0, vcpu: 0, memory: 0, ssd: 0, bulk: 0, costs_daily: 0 }
|
134
|
-
servers.each do |server|
|
135
|
-
data[:count] += 1
|
136
|
-
data[:vcpu] += server.vcpu_count
|
137
|
-
data[:memory] += server.memory_gb
|
138
|
-
data[:ssd] += server.storage_size(:ssd)
|
139
|
-
data[:bulk] += server.storage_size(:bulk)
|
140
|
-
data[:costs_daily] += server.total_costs_per_day
|
141
|
-
end
|
142
|
-
data[:costs_monthly] = (data[:costs_daily] * 30).round(2)
|
143
|
-
data[:costs_daily] = data[:costs_daily].round(2)
|
144
|
-
data
|
145
|
-
end
|
146
|
-
|
147
|
-
def to_csv
|
148
|
-
CSV.generate do |csv|
|
149
|
-
csv << headings
|
150
|
-
if @options[:summary]
|
151
|
-
csv << totals
|
152
|
-
else
|
153
|
-
rows.each { |row| csv << row }
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
end
|
159
|
-
end
|