cloudcost 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|