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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b79b93bfa4a348fdc4d6990402400ebf709ff1cb9c8a105816f39beefc5209f4
4
- data.tar.gz: 281358b34a8c77704a074fa9399b6b9d0f1ffda9d7ca96a4d3b30f6ad40794a0
3
+ metadata.gz: 9d4a4e7ae052a00c793f664be431acc0b93c1e8a32e69dc7f1263efb6fd820cd
4
+ data.tar.gz: bc0001f2c9b44a19da53c9a5ea446b441de78236c787bc434820ffbb6504246b
5
5
  SHA512:
6
- metadata.gz: 0541d3c1e6904a79903b45980b4fd5de642f01da8df6bfd3e038f5f0f18e9bbb33d46cbf58f2898f249f9757297609be7e03ec5e5674b8a047bd1bb2840e4761
7
- data.tar.gz: 93562d90574c73387a5cd6d57637e36836cc480fd045c03ac3459331028cf6b0c16886c9642f403a6adacffdc77cf3021eb20995fa102ae664510dfc70b12f6e
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.1)
5
- excon (~> 0.82.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.82.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.0.0)
22
+ unicode-display_width (2.1.0)
23
23
 
24
24
  PLATFORMS
25
25
  ruby
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 Puzzle ITC
3
+ Copyright (c) 2021 Nik Wolfgramm
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 `--tag-by` option, you can summarize usage by tag:
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 your actual deployment"
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.82.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
@@ -6,6 +6,7 @@ require "json"
6
6
  require "cloudcost/error"
7
7
 
8
8
  module Cloudcost
9
+ # Support for cloudscale-cli style API-tokens and profiles
9
10
  class ApiToken
10
11
  attr_accessor :profile, :token
11
12
 
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
- output(servers, options) do |result|
42
- spinner.success("(done)") if 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 servers by tag", aliases: %w[-t]
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.success "(#{servers.size} found)" if spinner
144
+ spinner&.success "(#{servers.size} found)"
119
145
  servers
120
146
  end
121
147
 
122
- def output(servers, options)
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).grouped_cost_table
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
@@ -2,14 +2,15 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
- PRICING = YAML.load_file( File.join(
6
- File.expand_path("../..", __dir__), "data/pricing.yml")
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")
@@ -7,6 +7,7 @@ module Cloudcost
7
7
  tag_hash.map { |k, v| "#{k}=#{v}" }.join(" ")
8
8
  end
9
9
 
10
+ # Representation of cloudscale.ch server object
10
11
  class Server
11
12
  attr_accessor :data
12
13
 
@@ -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 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
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.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")
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
- 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
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cloudcost
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  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,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.2.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-17 00:00:00.000000000 Z
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.82.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.82.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 your actual deployment
84
- email: wolfgramm@puzzle.ch
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