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 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