cloudcost 0.0.1 → 0.0.2

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: 1b5926bdf2873133e1d8b85b04c56210a6ee1a886a233693527462d5747d43ee
4
- data.tar.gz: d9ac68dc18b82a7925134099d99bbf8abfa1f9cee540a4c2cc23932ed1c6ebab
3
+ metadata.gz: f3d45a2bcff555d702572b129ad9031b4b5b62224715243d10a39185d044df5e
4
+ data.tar.gz: 14f89c7052221999e1781b7f7658137902542df9d0012ac903e7b9403fb448f6
5
5
  SHA512:
6
- metadata.gz: f466284d61657746d4e31f56e8a3c4f96bcf930460bcfb9f87a578c13c5edbd56ef302f3dd31608b0af99e580205416c8885e0c85edec1caf9de6abf6d682fba
7
- data.tar.gz: f9941021d5ecc30c31b5453e72074eefc57c12c9570b9e3489ac45450d896d79136f343bc22235a1c6a3f1776ce2cd8d440436b5f69e4f6d2218324522222bd5
6
+ metadata.gz: df8b5eeae07ab0bd56f0ab0616467b8e2a4dcf11b9bd2c5da4dae9cab1fffb82b05c6c4ec19c499566ccfa2af4d7763ea988ee5b8c2c9120a3d6f2bf5438e996
7
+ data.tar.gz: 92e260d8d279a4b4b7c5863565579233794e30a4a602408237252f7b627f7b1c04d0779282b8618d64c14411a52d7565e9e7963c7c07a430574c56614ea6c6d2
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
 
2
2
  vendor
3
3
  pkg
4
+ data
4
5
  .bundle
5
6
  cloudscale.ini
data/README.md CHANGED
@@ -7,12 +7,12 @@ Resources are fetched from the [API](https://www.cloudscale.ch/en/api/v1) and co
7
7
  Please note that costs are always calculated based on the current usage.
8
8
  Your actual bills are based on the effective usage over time and may include additional service fees, i.e. for data transfer or discounts.
9
9
 
10
- ## Setup
10
+ ## Installation
11
11
 
12
- Ruby is required, install dependencies using bundler:
12
+ Install the gem:
13
13
 
14
14
  ```sh
15
- bundle install
15
+ gem install cloudcost
16
16
  ```
17
17
 
18
18
  ## Configure API-Auth
@@ -30,8 +30,6 @@ export CLOUDSCALE_API_TOKEN=HELPIMTRAPPEDINATOKENGENERATOR
30
30
 
31
31
  or you can directly pass a token as a argument to the command: `--api-token HELPIMTRAPPEDINATOKENGENERATOR`
32
32
 
33
- **NOTE:** The API_TOKEN does only require read access.
34
-
35
33
  ## Usage
36
34
 
37
35
  ### Help
@@ -66,7 +64,30 @@ Only show summarized usage:
66
64
  cloudcost servers --summary
67
65
  ```
68
66
 
69
- #### Output CSV
67
+ #### Group and summarize by tag
68
+
69
+ By using the `--tag-by` option, you can summarize usage by tag:
70
+
71
+ ```sh
72
+ cloudcost servers --group-by budget-group
73
+ ```
74
+
75
+ #### Influx Line Protocol output
76
+
77
+ The CLI also supports output in Influxdb Line Protocol format when using the `--group-by` option:
78
+
79
+ ```sh
80
+ cloudcost servers --group-by budget-group --output influx
81
+ ```
82
+
83
+ The output can directly ba pipped to the influxdb CLI:
84
+
85
+ ```sh
86
+ cloudcost servers --group-by budget-group --profile prod | \
87
+ influx write --bucket my-bucket --org my-org --token my-super-secret-auth-token
88
+ ```
89
+
90
+ #### CSV Output
70
91
 
71
92
  Output in CSV format instead of a table:
72
93
 
@@ -91,13 +112,13 @@ cloudcost servers --name "^[^ocp|^k8s|^rancher].*"
91
112
  Filter servers by tag key:
92
113
 
93
114
  ```sh
94
- cloudcost servers --tag pitc_service
115
+ cloudcost servers --tag service
95
116
  ```
96
117
 
97
118
  Filter servers by tag value:
98
119
 
99
120
  ```sh
100
- cloudcost servers --tag pitc_service=ocp4
121
+ cloudcost servers --tag service=ocp4
101
122
  ```
102
123
 
103
124
  ### Server Tags
data/lib/cloudcost/cli.rb CHANGED
@@ -30,14 +30,16 @@ module Cloudcost
30
30
  option :name, desc: "filter name by regex", aliases: %w[-n]
31
31
  option :tag, desc: "filter servers by tag", aliases: %w[-t]
32
32
  option :summary, desc: "display totals only", type: :boolean, aliases: %w[-S]
33
- option :output, default: "table", enum: %w[table csv], desc: "output format", aliases: %w[-o]
33
+ option :group_by, desc: "group by tag", aliases: %w[-G]
34
+ option :output, default: "table", enum: %w[table csv influx], desc: "output format", aliases: %w[-o]
34
35
  def servers
35
36
  servers = load_servers(options)
36
- spinner = TTY::Spinner.new("[:spinner] Calculating costs...", clear: options[:csv])
37
- spinner.auto_spin
37
+ if options[:output] == "table"
38
+ spinner = TTY::Spinner.new("[:spinner] Calculating costs...", clear: options[:csv])
39
+ spinner.auto_spin
40
+ end
38
41
  output(servers, options) do |result|
39
- spinner.success "(done)"
40
- puts
42
+ spinner.success("(done)") if spinner
41
43
  puts result
42
44
  end
43
45
  rescue Excon::Error, TokenError, ProfileError, PricingError => e
@@ -108,15 +110,19 @@ module Cloudcost
108
110
  end
109
111
 
110
112
  def load_servers(options)
111
- spinner = TTY::Spinner.new("[:spinner] Loading servers...", clear: options[:csv])
112
- spinner.auto_spin
113
+ if options[:output] == "table"
114
+ spinner = TTY::Spinner.new("[:spinner] Loading servers...", clear: options[:csv])
115
+ spinner.auto_spin
116
+ end
113
117
  servers = api_connection(options).get_servers(options).map { |server| Server.new(server) }
114
- spinner.success "(#{servers.size} found)"
118
+ spinner.success "(#{servers.size} found)" if spinner
115
119
  servers
116
120
  end
117
121
 
118
122
  def output(servers, options)
119
- if options[:output] == "csv"
123
+ if options[:group_by]
124
+ yield Cloudcost::ServerList.new(servers, options).grouped_cost_table
125
+ elsif options[:output] == "csv"
120
126
  yield Cloudcost::ServerList.new(servers, options).to_csv
121
127
  else
122
128
  yield Cloudcost::ServerList.new(servers, options).cost_table
@@ -2,7 +2,9 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
- PRICING = YAML.load_file("data/pricing.yml")
5
+ PRICING = YAML.load_file( File.join(
6
+ File.expand_path("../..", __dir__), "data/pricing.yml")
7
+ )
6
8
 
7
9
  module Cloudcost
8
10
  class PricingError < StandardError
@@ -8,6 +8,8 @@ module Cloudcost
8
8
  end
9
9
 
10
10
  class Server
11
+ attr_accessor :data
12
+
11
13
  def initialize(data)
12
14
  @data = data
13
15
  @total_storage_per_type = sum_up_storage_per_type
@@ -33,6 +35,10 @@ module Cloudcost
33
35
  @data[:flavor][:memory_gb]
34
36
  end
35
37
 
38
+ def volumes
39
+ @data[:volumes]
40
+ end
41
+
36
42
  def tags
37
43
  @data[:tags]
38
44
  end
@@ -46,7 +52,7 @@ module Cloudcost
46
52
  end
47
53
 
48
54
  def server_costs_per_day
49
- Pricing.server_costs_per_day(@data[:flavor][:slug])
55
+ Pricing.server_costs_per_day(flavor)
50
56
  end
51
57
 
52
58
  def storage_costs_per_day(type = :ssd)
@@ -59,7 +65,7 @@ module Cloudcost
59
65
 
60
66
  def sum_up_storage_per_type
61
67
  sum = {}
62
- @data[:volumes].group_by { |volume| volume[:type].itself }.each do |group, vols|
68
+ volumes.group_by { |volume| volume[:type].itself }.each do |group, vols|
63
69
  sum.store(group.to_sym, 0)
64
70
  vols.each { |volume| sum[volume[:type].to_sym] += volume[:size_gb] }
65
71
  end
@@ -7,9 +7,9 @@ module Cloudcost
7
7
  @options = options
8
8
  end
9
9
 
10
- def calculate_totals
10
+ def calculate_totals(servers = @servers)
11
11
  totals = { vcpu: 0, memory: 0, ssd: 0, bulk: 0, cost: 0.0 }
12
- @servers.each do |server|
12
+ servers.each do |server|
13
13
  totals[:vcpu] += server.vcpu_count
14
14
  totals[:memory] += server.memory_gb
15
15
  totals[:ssd] += server.storage_size(:ssd)
@@ -20,33 +20,37 @@ module Cloudcost
20
20
  end
21
21
 
22
22
  def headings
23
- headings = @options[:summary] ? [""] : %w[Name UUID Flavor Tags]
23
+ headings = if @options[:summary]
24
+ [""]
25
+ elsif @options[:group_by]
26
+ ["Group", "Servers"]
27
+ else
28
+ %w[Name UUID Flavor Tags]
29
+ end
24
30
  headings.concat ["vCPU's", "Memory [GB]", "SSD [GB]", "Bulk [GB]", "CHF/day", "CHF/30-days"]
25
31
  end
26
32
 
27
33
  def rows
28
34
  rows = []
29
- unless @options[:summary]
30
- @servers.sort_by(&:name).map do |server|
31
- rows << [
32
- server.name,
33
- server.uuid,
34
- server.flavor,
35
- server.tags_to_s,
36
- server.vcpu_count,
37
- server.memory_gb,
38
- server.storage_size(:ssd),
39
- server.storage_size(:bulk),
40
- format("%.2f", server.total_costs_per_day.round(2)),
41
- format("%.2f", (server.total_costs_per_day * 30).round(2))
42
- ]
43
- end
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
+ ]
44
48
  end
45
49
  rows
46
50
  end
47
51
 
48
- def totals
49
- totals = calculate_totals
52
+ def totals(servers = @servers)
53
+ totals = calculate_totals(servers)
50
54
  total_row = @options[:summary] ? %w[Total] : ["Total", "", "", ""]
51
55
  total_row.concat [
52
56
  totals[:vcpu],
@@ -88,6 +92,58 @@ module Cloudcost
88
92
  table
89
93
  end
90
94
 
95
+ def grouped_cost_table
96
+ no_tag = "<no-tag>"
97
+ group_rows = @servers.group_by {|s| s.tags[@options[:group_by].to_sym] || no_tag }.map do |name, servers|
98
+ server_groups_data(name, servers).values.flatten
99
+ end.sort {|a, b| a[0] == no_tag ? 1 : a[0] <=> b[0] }
100
+ if @options[:output] == "csv"
101
+ CSV.generate do |csv|
102
+ csv << headings
103
+ group_rows.each { |row| csv << row }
104
+ end
105
+ elsif @options[:output] == "influx"
106
+ lines = []
107
+ group_rows.each do |row|
108
+ [
109
+ { field: "server_count", position: 1, unit: "i" },
110
+ { field: "vcpus", position: 2, unit: "i" },
111
+ { field: "memory_gb", position: 3, unit: "i" },
112
+ { field: "ssd_gb", position: 4, unit: "i" },
113
+ { field: "bulk_gb", position: 5, unit: "i" },
114
+ { field: "cost_per_day", position: 6, unit: "" },
115
+ ].each do |field|
116
+ lines << "cloudscaleServerCosts,grouped_by=#{@options[:group_by]},group=#{row[0]},environment=#{@options[:profile] || "default"},currency=CHF #{field[:field]}=#{row[field[:position]]}#{field[:unit]}"
117
+ end
118
+ end
119
+ lines.join("\n")
120
+ else
121
+ table = Terminal::Table.new do |t|
122
+ t.title = "cloudscale.ch server costs grouped by tag \"#{@options[:group_by]}\""
123
+ t.title += " (#{@options[:profile]})" if @options[:profile]
124
+ t.headings = headings
125
+ end
126
+ table.rows = group_rows
127
+ (1..table.columns.size).each { |column| table.align_column(column, :right) }
128
+ table
129
+ end
130
+ end
131
+
132
+ def server_groups_data(name, servers)
133
+ data = { name: name, count: 0, vcpu: 0, memory: 0, ssd: 0, bulk: 0, costs_daily: 0 }
134
+ servers.each do |server|
135
+ data[:count] += 1
136
+ data[:vcpu] += server.vcpu_count
137
+ data[:memory] += server.memory_gb
138
+ data[:ssd] += server.storage_size(:ssd)
139
+ data[:bulk] += server.storage_size(:bulk)
140
+ data[:costs_daily] += server.total_costs_per_day
141
+ end
142
+ data[:costs_monthly] = (data[:costs_daily] * 30).round(2)
143
+ data[:costs_daily] = data[:costs_daily].round(2)
144
+ data
145
+ end
146
+
91
147
  def to_csv
92
148
  CSV.generate do |csv|
93
149
  csv << headings
@@ -98,5 +154,6 @@ module Cloudcost
98
154
  end
99
155
  end
100
156
  end
157
+
101
158
  end
102
159
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cloudcost
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
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.0.1
4
+ version: 0.0.2
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-07 00:00:00.000000000 Z
11
+ date: 2021-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon