cloudcost 0.2.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- 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 +16 -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: e9d9dcfa0be1af8bdca48829a644e2537b44f74ccc67a39a96c1b6995287fe9a
|
4
|
+
data.tar.gz: c886680ef321bb68f8241f8e1b146dc6e76070cc15e950e7a8724a55dec9cdf5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b68e3c49dac3c5eae508aa31a0eb1fdd124e314a1c90fb3d0b5e03c56e57266c37f7c9dd2b20e1f74700e6d4853c0b7986fdf798a38b3d89e714b463b88530a
|
7
|
+
data.tar.gz: 33c6d53ddb662a3cdcb7ebace5a569853ab49439dc2f2cf5a49122eb40cae1433ffdce765f97d73613415dfcde9b32fced9f0b57056a7196e22b1dc289304248
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cloudcost (0.
|
5
|
-
excon (~> 0.
|
4
|
+
cloudcost (0.4.1)
|
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: :cost, 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.1
|
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: []
|
@@ -102,10 +102,18 @@ files:
|
|
102
102
|
- lib/cloudcost/api_connection.rb
|
103
103
|
- lib/cloudcost/api_token.rb
|
104
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
|
105
115
|
- lib/cloudcost/error.rb
|
106
116
|
- lib/cloudcost/pricing.rb
|
107
|
-
- lib/cloudcost/server.rb
|
108
|
-
- lib/cloudcost/server_list.rb
|
109
117
|
- lib/cloudcost/version.rb
|
110
118
|
homepage: https://gitlab.puzzle.ch/nwolfgramm/cloudcost
|
111
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: "chf_per_day", position: 6, unit: "" },
|
115
|
-
].each do |field|
|
116
|
-
lines << "cloudscaleServerCosts,group=#{row[0]},profile=#{@options[:profile] || "?"} #{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
|