cloudcost 0.2.0 → 0.3.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/Gemfile.lock +4 -4
- data/LICENSE +1 -1
- data/README.md +40 -1
- data/cloudcost.gemspec +2 -3
- data/lib/cloudcost/api_connection.rb +10 -0
- data/lib/cloudcost/api_token.rb +1 -0
- data/lib/cloudcost/cli.rb +50 -6
- data/lib/cloudcost/csv_output.rb +24 -0
- data/lib/cloudcost/influxdb_output.rb +26 -0
- data/lib/cloudcost/pricing.rb +4 -3
- data/lib/cloudcost/server.rb +1 -0
- data/lib/cloudcost/server_list.rb +15 -103
- data/lib/cloudcost/tabular_output.rb +90 -0
- data/lib/cloudcost/version.rb +1 -1
- data/lib/cloudcost/volume.rb +58 -0
- data/lib/cloudcost/volume_list.rb +86 -0
- data/lib/cloudcost.rb +5 -0
- metadata +11 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d4a4e7ae052a00c793f664be431acc0b93c1e8a32e69dc7f1263efb6fd820cd
|
4
|
+
data.tar.gz: bc0001f2c9b44a19da53c9a5ea446b441de78236c787bc434820ffbb6504246b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d9d615b734291387c9adbb5ac28a619a65d359fbf584af406175496abe1450f6a8ce5a7365a16f42319744959fbbdf7eb6ae556b49e47929b7dc46521f15a79
|
7
|
+
data.tar.gz: 8672668910d5c4e6de0115bb4c64466283afcb43baeb791914e3a62cd399d0b7285d8d86ad45a21692d3f447ad0249ea66463a02ef4cc47b931a291a8b0d0024
|
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.3.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,7 +11,7 @@ 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
16
|
terminal-table (3.0.1)
|
17
17
|
unicode-display_width (>= 1.1.1, < 3)
|
@@ -19,7 +19,7 @@ GEM
|
|
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
@@ -66,7 +66,7 @@ cloudcost servers --summary
|
|
66
66
|
|
67
67
|
#### Group and summarize by tag
|
68
68
|
|
69
|
-
By using the `--
|
69
|
+
By using the `--group-by` option, you can summarize usage by tag:
|
70
70
|
|
71
71
|
```sh
|
72
72
|
cloudcost servers --group-by budget-group
|
@@ -87,6 +87,19 @@ cloudcost servers --group-by budget-group --profile prod | \
|
|
87
87
|
influx write --bucket my-bucket --org my-org --token my-super-secret-auth-token
|
88
88
|
```
|
89
89
|
|
90
|
+
Example Flux-Query for loading data from InfluxDB:
|
91
|
+
|
92
|
+
```sh
|
93
|
+
influx query --org my-org --token my-super-secret-auth-token \
|
94
|
+
'from(bucket:"my-bucket")
|
95
|
+
|> range(start: -1d)
|
96
|
+
|> filter(fn: (r) =>
|
97
|
+
r._measurement == "cloudscaleServerCosts" and
|
98
|
+
r._field == "cost_per_day") and
|
99
|
+
r.profile == "prod" and
|
100
|
+
r.group == "my-budget-group"'
|
101
|
+
```
|
102
|
+
|
90
103
|
#### CSV Output
|
91
104
|
|
92
105
|
Output in CSV format instead of a table:
|
@@ -154,3 +167,29 @@ cloudcost server-tags --name ldap --set-tags owner=sys budget-group=base-infrast
|
|
154
167
|
```sh
|
155
168
|
cloudcost server-tags --name ldap --remove-tags owner budget-group
|
156
169
|
```
|
170
|
+
|
171
|
+
### Volumes
|
172
|
+
|
173
|
+
List all volumes:
|
174
|
+
|
175
|
+
```sh
|
176
|
+
cloudcost volumes
|
177
|
+
```
|
178
|
+
|
179
|
+
Only list volumes of type `bulk`
|
180
|
+
|
181
|
+
```sh
|
182
|
+
cloudcost volumes --type bulk
|
183
|
+
```
|
184
|
+
|
185
|
+
List volumes which are not attached to a server:
|
186
|
+
|
187
|
+
```sh
|
188
|
+
cloudcost volumes --no-attached
|
189
|
+
```
|
190
|
+
|
191
|
+
Filter volumes by names:
|
192
|
+
|
193
|
+
```sh
|
194
|
+
cloudcost volumes --name "pvc"
|
195
|
+
```
|
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,8 +39,8 @@ 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
|
@@ -53,7 +54,7 @@ module Cloudcost
|
|
53
54
|
|
54
55
|
desc "server-tags", "show and assign tags of servers"
|
55
56
|
option :name, desc: "filter name by regex", aliases: %w[-n]
|
56
|
-
option :tag, desc: "filter
|
57
|
+
option :tag, desc: "filter by tag", aliases: %w[-t]
|
57
58
|
option :set_tags,
|
58
59
|
desc: "set tags",
|
59
60
|
aliases: %w[-T],
|
@@ -94,6 +95,31 @@ module Cloudcost
|
|
94
95
|
end
|
95
96
|
end
|
96
97
|
|
98
|
+
desc "volumes", "explore volumes"
|
99
|
+
option :name, desc: "filter name by regex", aliases: %w[-n]
|
100
|
+
option :tag, desc: "filter by tag", aliases: %w[-t]
|
101
|
+
option :summary, desc: "display totals only", type: :boolean, aliases: %w[-S]
|
102
|
+
option :type, enum: %w[ssd bulk], desc: "volume type"
|
103
|
+
option :attached, type: :boolean, desc: "volume attached to servers"
|
104
|
+
def volumes
|
105
|
+
volumes = load_volumes(options)
|
106
|
+
if options[:output] == "table"
|
107
|
+
spinner = TTY::Spinner.new("[:spinner] Calculating costs...", clear: options[:csv])
|
108
|
+
spinner.auto_spin
|
109
|
+
end
|
110
|
+
output_volumes(volumes, options) do |result|
|
111
|
+
spinner&.success("(done)")
|
112
|
+
puts result
|
113
|
+
end
|
114
|
+
rescue Excon::Error, TokenError, ProfileError, PricingError => e
|
115
|
+
error_message = "ERROR: #{e.message}"
|
116
|
+
if spinner
|
117
|
+
spinner.error("(#{error_message})")
|
118
|
+
else
|
119
|
+
puts error_message
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
97
123
|
no_tasks do
|
98
124
|
def tags_to_h(tags_array)
|
99
125
|
tags_hash = {}
|
@@ -115,20 +141,38 @@ module Cloudcost
|
|
115
141
|
spinner.auto_spin
|
116
142
|
end
|
117
143
|
servers = api_connection(options).get_servers(options).map { |server| Server.new(server) }
|
118
|
-
spinner
|
144
|
+
spinner&.success "(#{servers.size} found)"
|
119
145
|
servers
|
120
146
|
end
|
121
147
|
|
122
|
-
def
|
148
|
+
def load_volumes(options)
|
149
|
+
if options[:output] == "table"
|
150
|
+
spinner = TTY::Spinner.new("[:spinner] Loading volumes...", clear: options[:csv])
|
151
|
+
spinner.auto_spin
|
152
|
+
end
|
153
|
+
volumes = api_connection(options).get_volumes(options).map { |volume| Volume.new(volume) }
|
154
|
+
spinner&.success "(#{volumes.size} found)"
|
155
|
+
volumes
|
156
|
+
end
|
157
|
+
|
158
|
+
def output_servers(servers, options)
|
123
159
|
if options[:group_by]
|
124
|
-
yield Cloudcost::ServerList.new(servers, options).
|
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
|
+
yield Cloudcost::VolumeList.new(volumes, options).cost_table
|
174
|
+
end
|
175
|
+
|
132
176
|
def tag_option_to_s(options)
|
133
177
|
messages = []
|
134
178
|
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
|
+
# CSV output methods for the ServerList class
|
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
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudcost
|
4
|
+
# InfluxDB output methods for the ServerList class
|
5
|
+
module InfluxdbOutput
|
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
|
data/lib/cloudcost/pricing.rb
CHANGED
@@ -2,14 +2,15 @@
|
|
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
10
|
class PricingError < StandardError
|
11
11
|
end
|
12
12
|
|
13
|
+
# pricing class which implements cost calculation methods for cloudscale.ch resources
|
13
14
|
module Pricing
|
14
15
|
def self.server_costs_per_day(flavor)
|
15
16
|
PRICING["servers"][flavor] || raise(PricingError, "#{flavor} flavor not found in pricing.yml")
|
data/lib/cloudcost/server.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Cloudcost
|
4
|
+
# ServerList represents a list of servers and integrates several output methods
|
4
5
|
class ServerList
|
6
|
+
include Cloudcost::TabularOutput
|
7
|
+
include Cloudcost::CsvOutput
|
8
|
+
include Cloudcost::InfluxdbOutput
|
9
|
+
|
5
10
|
def initialize(servers, options = {})
|
6
11
|
@servers = servers
|
7
12
|
@options = options
|
@@ -19,36 +24,6 @@ module Cloudcost
|
|
19
24
|
totals
|
20
25
|
end
|
21
26
|
|
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
27
|
def totals(servers = @servers)
|
53
28
|
totals = calculate_totals(servers)
|
54
29
|
total_row = @options[:summary] ? %w[Total] : ["Total", "", "", ""]
|
@@ -62,70 +37,19 @@ module Cloudcost
|
|
62
37
|
]
|
63
38
|
end
|
64
39
|
|
65
|
-
def
|
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
|
40
|
+
def grouped_costs
|
96
41
|
no_tag = "<no-tag>"
|
97
|
-
group_rows = @servers.group_by {|s| s.tags[@options[:group_by].to_sym] || no_tag }.map do |name, servers|
|
42
|
+
group_rows = @servers.group_by { |s| s.tags[@options[:group_by].to_sym] || no_tag }.map do |name, servers|
|
98
43
|
server_groups_data(name, servers).values.flatten
|
99
|
-
end
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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")
|
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)
|
120
51
|
else
|
121
|
-
|
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
|
52
|
+
grouped_cost_table(group_rows)
|
129
53
|
end
|
130
54
|
end
|
131
55
|
|
@@ -143,17 +67,5 @@ module Cloudcost
|
|
143
67
|
data[:costs_daily] = data[:costs_daily].round(2)
|
144
68
|
data
|
145
69
|
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
70
|
end
|
159
71
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudcost
|
4
|
+
# Tabular output methods for the ServerList class
|
5
|
+
module TabularOutput
|
6
|
+
def headings
|
7
|
+
headings = if @options[:summary]
|
8
|
+
[""]
|
9
|
+
elsif @options[:group_by]
|
10
|
+
%w[Group Servers]
|
11
|
+
else
|
12
|
+
%w[Name UUID Flavor Tags]
|
13
|
+
end
|
14
|
+
headings.concat ["vCPU's", "Memory [GB]", "SSD [GB]", "Bulk [GB]", "CHF/day", "CHF/30-days"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def rows
|
18
|
+
rows = []
|
19
|
+
@servers.sort_by(&:name).map do |server|
|
20
|
+
rows << [
|
21
|
+
server.name,
|
22
|
+
server.uuid,
|
23
|
+
server.flavor,
|
24
|
+
server.tags_to_s,
|
25
|
+
server.vcpu_count,
|
26
|
+
server.memory_gb,
|
27
|
+
server.storage_size(:ssd),
|
28
|
+
server.storage_size(:bulk),
|
29
|
+
format("%.2f", server.total_costs_per_day.round(2)),
|
30
|
+
format("%.2f", (server.total_costs_per_day * 30).round(2))
|
31
|
+
]
|
32
|
+
end
|
33
|
+
rows
|
34
|
+
end
|
35
|
+
|
36
|
+
def totals(servers = @servers)
|
37
|
+
totals = calculate_totals(servers)
|
38
|
+
total_row = @options[:summary] ? %w[Total] : ["Total", "", "", ""]
|
39
|
+
total_row.concat [
|
40
|
+
totals[:vcpu],
|
41
|
+
totals[:memory],
|
42
|
+
totals[:ssd],
|
43
|
+
totals[:bulk],
|
44
|
+
format("%.2f", totals[:cost].round(2)),
|
45
|
+
format("%.2f", (totals[:cost] * 30).round(2))
|
46
|
+
]
|
47
|
+
end
|
48
|
+
|
49
|
+
def tags_table
|
50
|
+
Terminal::Table.new do |t|
|
51
|
+
t.title = "cloudscale.ch server tags"
|
52
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
53
|
+
t.headings = %w[Name UUID Tags]
|
54
|
+
t.rows = @servers.sort_by(&:name).map do |server|
|
55
|
+
[
|
56
|
+
server.name,
|
57
|
+
server.uuid,
|
58
|
+
server.tags_to_s
|
59
|
+
]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def cost_table
|
65
|
+
table = Terminal::Table.new do |t|
|
66
|
+
t.title = "cloudscale.ch server costs"
|
67
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
68
|
+
t.headings = headings
|
69
|
+
t.rows = rows unless @options[:summary]
|
70
|
+
end
|
71
|
+
|
72
|
+
table.add_separator unless @options[:summary]
|
73
|
+
table.add_row totals
|
74
|
+
first_number_row = @options[:summary] ? 1 : 2
|
75
|
+
(first_number_row..table.columns.size).each { |column| table.align_column(column, :right) }
|
76
|
+
table
|
77
|
+
end
|
78
|
+
|
79
|
+
def grouped_cost_table(group_rows)
|
80
|
+
table = Terminal::Table.new do |t|
|
81
|
+
t.title = "cloudscale.ch server costs grouped by tag \"#{@options[:group_by]}\""
|
82
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
83
|
+
t.headings = headings
|
84
|
+
end
|
85
|
+
table.rows = group_rows
|
86
|
+
(1..table.columns.size).each { |column| table.align_column(column, :right) }
|
87
|
+
table
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/cloudcost/version.rb
CHANGED
@@ -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,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cloudcost
|
4
|
+
# volumeList represents a list of volumes and integrates several output methods
|
5
|
+
class VolumeList
|
6
|
+
|
7
|
+
def initialize(volumes, options = {})
|
8
|
+
@volumes = volumes
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def calculate_totals(volumes = @volumes)
|
13
|
+
total = { size: 0, cost: 0.0 }
|
14
|
+
volumes.each do |volume|
|
15
|
+
total[:size] += volume.size_gb
|
16
|
+
total[:cost] += volume.costs_per_day
|
17
|
+
end
|
18
|
+
total
|
19
|
+
end
|
20
|
+
|
21
|
+
def totals(volumes = @volumes)
|
22
|
+
total = calculate_totals(volumes)
|
23
|
+
total_row = @options[:summary] ? %w[Total] : ["Total", "", "", "", ""]
|
24
|
+
total_row.concat [
|
25
|
+
total[:size],
|
26
|
+
format("%.2f", total[:cost].round(2)),
|
27
|
+
format("%.2f", (total[:cost] * 30).round(2))
|
28
|
+
]
|
29
|
+
end
|
30
|
+
|
31
|
+
def headings
|
32
|
+
headings = if @options[:summary]
|
33
|
+
[""]
|
34
|
+
elsif @options[:group_by]
|
35
|
+
%w[Group Volumes]
|
36
|
+
else
|
37
|
+
%w[Name UUID Type Servers Tags]
|
38
|
+
end
|
39
|
+
headings.concat ["Size [GB]", "CHF/day", "CHF/30-days"]
|
40
|
+
end
|
41
|
+
|
42
|
+
def rows
|
43
|
+
rows = []
|
44
|
+
@volumes.sort_by(&:name).map do |volume|
|
45
|
+
rows << [
|
46
|
+
volume.name,
|
47
|
+
volume.uuid,
|
48
|
+
volume.type,
|
49
|
+
volume.server_name,
|
50
|
+
volume.tags_to_s,
|
51
|
+
volume.size_gb,
|
52
|
+
format("%.2f", volume.costs_per_day.round(2)),
|
53
|
+
format("%.2f", (volume.costs_per_day * 30).round(2))
|
54
|
+
]
|
55
|
+
end
|
56
|
+
rows
|
57
|
+
end
|
58
|
+
|
59
|
+
def cost_table
|
60
|
+
table = Terminal::Table.new do |t|
|
61
|
+
t.title = "cloudscale.ch volume costs"
|
62
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
63
|
+
t.headings = headings
|
64
|
+
t.rows = rows unless @options[:summary]
|
65
|
+
end
|
66
|
+
|
67
|
+
table.add_separator unless @options[:summary]
|
68
|
+
table.add_row totals
|
69
|
+
first_number_row = @options[:summary] ? 1 : 2
|
70
|
+
(first_number_row..table.columns.size).each { |column| table.align_column(column, :right) }
|
71
|
+
table
|
72
|
+
end
|
73
|
+
|
74
|
+
def grouped_cost_table(group_rows)
|
75
|
+
table = Terminal::Table.new do |t|
|
76
|
+
t.title = "cloudscale.ch volume costs grouped by tag \"#{@options[:group_by]}\""
|
77
|
+
t.title += " (#{@options[:profile]})" if @options[:profile]
|
78
|
+
t.headings = headings
|
79
|
+
end
|
80
|
+
table.rows = group_rows
|
81
|
+
(1..table.columns.size).each { |column| table.align_column(column, :right) }
|
82
|
+
table
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
data/lib/cloudcost.rb
CHANGED
@@ -7,5 +7,10 @@ require "cloudcost/version"
|
|
7
7
|
require "cloudcost/api_token"
|
8
8
|
require "cloudcost/api_connection"
|
9
9
|
require "cloudcost/server"
|
10
|
+
require "cloudcost/tabular_output"
|
11
|
+
require "cloudcost/influxdb_output"
|
12
|
+
require "cloudcost/csv_output"
|
10
13
|
require "cloudcost/server_list"
|
14
|
+
require "cloudcost/volume"
|
15
|
+
require "cloudcost/volume_list"
|
11
16
|
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.3.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-19 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,11 +102,16 @@ files:
|
|
102
102
|
- lib/cloudcost/api_connection.rb
|
103
103
|
- lib/cloudcost/api_token.rb
|
104
104
|
- lib/cloudcost/cli.rb
|
105
|
+
- lib/cloudcost/csv_output.rb
|
105
106
|
- lib/cloudcost/error.rb
|
107
|
+
- lib/cloudcost/influxdb_output.rb
|
106
108
|
- lib/cloudcost/pricing.rb
|
107
109
|
- lib/cloudcost/server.rb
|
108
110
|
- lib/cloudcost/server_list.rb
|
111
|
+
- lib/cloudcost/tabular_output.rb
|
109
112
|
- lib/cloudcost/version.rb
|
113
|
+
- lib/cloudcost/volume.rb
|
114
|
+
- lib/cloudcost/volume_list.rb
|
110
115
|
homepage: https://gitlab.puzzle.ch/nwolfgramm/cloudcost
|
111
116
|
licenses:
|
112
117
|
- MIT
|