cloudcost 0.1.0 → 0.3.1
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 +41 -3
- data/cloudcost.gemspec +2 -3
- data/lib/cloudcost/api_connection.rb +10 -0
- data/lib/cloudcost/api_token.rb +1 -1
- data/lib/cloudcost/cli.rb +71 -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 +6 -0
- data/lib/cloudcost/commands/volume/volume.rb +58 -0
- data/lib/cloudcost/commands/volume/volume_list.rb +79 -0
- data/lib/cloudcost/commands/volume.rb +4 -0
- data/lib/cloudcost/error.rb +2 -0
- data/lib/cloudcost/pricing.rb +4 -4
- 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: 1b58764fd578b202e42b613003289f95b778b9c409f6bcb4c9ce6d565c90016d
|
4
|
+
data.tar.gz: f8137acc0b1c0529550b5a1c135fcb808ca3e1596ea6d22b018aa117c6f44311
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28a277fafc87edfc66ea733900e62bb801879cc8434974901b8e1c34532bc4e5fddbc3b3b3ba04a14c478e6045de999aaf3bbfa72297b8f69a718da5461f9751
|
7
|
+
data.tar.gz: 74f8f13db587ccc0bfd0ca94ac5444a6238cde48744c23957432235bb7ed2e7d06d91a6ab0ba38f7911bb2de8abe0993ccd59539c51deef20edd2ef72c5c9327
|
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.
|
5
|
-
excon (~> 0.
|
4
|
+
cloudcost (0.3.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,29 @@ 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
|
+
```
|
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,15 @@ 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
|
+
volumes = volumes.select { |volume| (volume[:servers].size > 0) == options[:attached] } if options[:attached] != nil
|
48
|
+
volumes
|
49
|
+
end
|
50
|
+
|
41
51
|
private
|
42
52
|
|
43
53
|
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], 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,53 @@ 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.size < 1
|
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.size < 1
|
174
|
+
yield "WARNING: No volumes found."
|
175
|
+
elsif options[:output] == "csv"
|
176
|
+
yield Cloudcost::VolumeList.new(volumes, options).to_csv
|
177
|
+
else
|
178
|
+
yield Cloudcost::VolumeList.new(volumes, options).cost_table
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
132
182
|
def tag_option_to_s(options)
|
133
183
|
messages = []
|
134
184
|
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,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudcost
|
4
|
+
|
5
|
+
# Representation of cloudscale.ch volume object
|
6
|
+
class Volume
|
7
|
+
attr_accessor :data
|
8
|
+
|
9
|
+
def initialize(data)
|
10
|
+
@data = data
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
@data[:name]
|
15
|
+
end
|
16
|
+
|
17
|
+
def uuid
|
18
|
+
@data[:uuid]
|
19
|
+
end
|
20
|
+
|
21
|
+
def type
|
22
|
+
@data[:type]
|
23
|
+
end
|
24
|
+
|
25
|
+
def servers
|
26
|
+
@data[:servers]
|
27
|
+
end
|
28
|
+
|
29
|
+
def server_name
|
30
|
+
servers.size > 0 ? servers.first[:name] : ""
|
31
|
+
end
|
32
|
+
|
33
|
+
def attached?
|
34
|
+
servers.size > 0 ? true : false
|
35
|
+
end
|
36
|
+
|
37
|
+
def server_uuids
|
38
|
+
@data[:server_uuids]
|
39
|
+
end
|
40
|
+
|
41
|
+
def tags
|
42
|
+
@data[:tags]
|
43
|
+
end
|
44
|
+
|
45
|
+
def size_gb
|
46
|
+
@data[:size_gb]
|
47
|
+
end
|
48
|
+
|
49
|
+
def tags_to_s
|
50
|
+
Cloudcost.tags_to_s(tags)
|
51
|
+
end
|
52
|
+
|
53
|
+
def costs_per_day
|
54
|
+
Pricing.storage_costs_per_day(type, size_gb)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,79 @@
|
|
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
|
+
|
9
|
+
include Cloudcost::CsvOutput
|
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, cost: 0.0 }
|
18
|
+
volumes.each do |volume|
|
19
|
+
total[:size] += volume.size_gb
|
20
|
+
total[:cost] += volume.costs_per_day
|
21
|
+
end
|
22
|
+
total
|
23
|
+
end
|
24
|
+
|
25
|
+
def totals(volumes = @volumes)
|
26
|
+
total = calculate_totals(volumes)
|
27
|
+
total_row = @options[:summary] ? %w[Total] : ["Total", "", "", "", ""]
|
28
|
+
total_row.concat [
|
29
|
+
total[:size],
|
30
|
+
format("%.2f", total[:cost].round(2)),
|
31
|
+
format("%.2f", (total[:cost] * 30).round(2))
|
32
|
+
]
|
33
|
+
end
|
34
|
+
|
35
|
+
def headings
|
36
|
+
headings = if @options[:summary]
|
37
|
+
[""]
|
38
|
+
elsif @options[:group_by]
|
39
|
+
%w[Group Volumes]
|
40
|
+
else
|
41
|
+
%w[Name UUID Type Servers Tags]
|
42
|
+
end
|
43
|
+
headings.concat ["Size [GB]", "CHF/day", "CHF/30-days"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def rows
|
47
|
+
rows = []
|
48
|
+
@volumes.sort_by(&:name).map do |volume|
|
49
|
+
rows << [
|
50
|
+
volume.name,
|
51
|
+
volume.uuid,
|
52
|
+
volume.type,
|
53
|
+
volume.server_name,
|
54
|
+
volume.tags_to_s,
|
55
|
+
volume.size_gb,
|
56
|
+
format("%.2f", volume.costs_per_day.round(2)),
|
57
|
+
format("%.2f", (volume.costs_per_day * 30).round(2))
|
58
|
+
]
|
59
|
+
end
|
60
|
+
rows
|
61
|
+
end
|
62
|
+
|
63
|
+
def cost_table
|
64
|
+
table = Terminal::Table.new do |t|
|
65
|
+
t.title = "cloudscale.ch volume costs"
|
66
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
67
|
+
t.headings = headings
|
68
|
+
t.rows = rows unless @options[:summary]
|
69
|
+
end
|
70
|
+
|
71
|
+
table.add_separator unless @options[:summary]
|
72
|
+
table.add_row totals
|
73
|
+
first_number_row = @options[:summary] ? 1 : 2
|
74
|
+
(first_number_row..table.columns.size).each { |column| table.align_column(column, :right) }
|
75
|
+
table
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
data/lib/cloudcost/error.rb
CHANGED
data/lib/cloudcost/pricing.rb
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require "yaml"
|
4
4
|
|
5
|
-
PRICING = YAML.load_file(
|
5
|
+
PRICING = YAML.load_file(File.join(
|
6
|
+
File.expand_path("../..", __dir__), "data/pricing.yml"
|
7
|
+
))
|
6
8
|
|
7
9
|
module Cloudcost
|
8
|
-
class
|
9
|
-
end
|
10
|
-
|
10
|
+
# pricing class which implements cost calculation methods for cloudscale.ch resources
|
11
11
|
module Pricing
|
12
12
|
def self.server_costs_per_day(flavor)
|
13
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.1
|
4
|
+
version: 0.3.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-20 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,17 @@ 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_list.rb
|
104
114
|
- lib/cloudcost/error.rb
|
105
115
|
- lib/cloudcost/pricing.rb
|
106
|
-
- lib/cloudcost/server.rb
|
107
|
-
- lib/cloudcost/server_list.rb
|
108
116
|
- lib/cloudcost/version.rb
|
109
117
|
homepage: https://gitlab.puzzle.ch/nwolfgramm/cloudcost
|
110
118
|
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
|