cloudcost 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1b5926bdf2873133e1d8b85b04c56210a6ee1a886a233693527462d5747d43ee
4
+ data.tar.gz: d9ac68dc18b82a7925134099d99bbf8abfa1f9cee540a4c2cc23932ed1c6ebab
5
+ SHA512:
6
+ metadata.gz: f466284d61657746d4e31f56e8a3c4f96bcf930460bcfb9f87a578c13c5edbd56ef302f3dd31608b0af99e580205416c8885e0c85edec1caf9de6abf6d682fba
7
+ data.tar.gz: f9941021d5ecc30c31b5453e72074eefc57c12c9570b9e3489ac45450d896d79136f343bc22235a1c6a3f1776ce2cd8d440436b5f69e4f6d2218324522222bd5
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+
2
+ vendor
3
+ pkg
4
+ .bundle
5
+ cloudscale.ini
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+ NewCops: enable
4
+
5
+ Style/StringLiterals:
6
+ Enabled: true
7
+ EnforcedStyle: double_quotes
8
+
9
+ Style/StringLiteralsInInterpolation:
10
+ Enabled: true
11
+ EnforcedStyle: double_quotes
12
+
13
+ Layout/LineLength:
14
+ Max: 120
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,31 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cloudcost (0.0.1)
5
+ excon (~> 0.82.0)
6
+ parseconfig (~> 1.1.0)
7
+ terminal-table (~> 3.0.1)
8
+ thor (~> 1.1.0)
9
+ tty-spinner (~> 0.9.3)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ excon (0.82.0)
15
+ parseconfig (1.1.0)
16
+ terminal-table (3.0.1)
17
+ unicode-display_width (>= 1.1.1, < 3)
18
+ thor (1.1.0)
19
+ tty-cursor (0.7.1)
20
+ tty-spinner (0.9.3)
21
+ tty-cursor (~> 0.7)
22
+ unicode-display_width (2.0.0)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ cloudcost!
29
+
30
+ BUNDLED WITH
31
+ 2.1.4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Puzzle ITC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # cloudcost - cloudscale.ch Cost Explorer
2
+
3
+ A CLI-tool which helps you explore costs on [cloudscale.ch](https://www.cloudscale.ch).
4
+
5
+ Resources are fetched from the [API](https://www.cloudscale.ch/en/api/v1) and costs calculated using prices defined in `data/pricing.yml`.
6
+
7
+ Please note that costs are always calculated based on the current usage.
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
+
10
+ ## Setup
11
+
12
+ Ruby is required, install dependencies using bundler:
13
+
14
+ ```sh
15
+ bundle install
16
+ ```
17
+
18
+ ## Configure API-Auth
19
+
20
+ cloudcost does support the same auth configuration options as [cloudscale-cli](https://cloudscale-ch.github.io/cloudscale-cli/).
21
+
22
+ You can manage multiple profiles using `cloudscale.ini` files ([see here](https://cloudscale-ch.github.io/cloudscale-cli/auth/) for instructions).
23
+
24
+
25
+ Otherwise you can export a `CLOUDSCALE_API_TOKEN` in your environment:
26
+
27
+ ```sh
28
+ export CLOUDSCALE_API_TOKEN=HELPIMTRAPPEDINATOKENGENERATOR
29
+ ```
30
+
31
+ or you can directly pass a token as a argument to the command: `--api-token HELPIMTRAPPEDINATOKENGENERATOR`
32
+
33
+ **NOTE:** The API_TOKEN does only require read access.
34
+
35
+ ## Usage
36
+
37
+ ### Help
38
+
39
+ Display help:
40
+
41
+ ```sh
42
+ cloudcost help
43
+ ```
44
+
45
+ Describe the server command:
46
+
47
+ ```sh
48
+ cloudcost help server
49
+ ```
50
+
51
+ ### Servers
52
+
53
+ #### Detailed List
54
+
55
+ List all servers from the given environment:
56
+
57
+ ```sh
58
+ cloudcost servers
59
+ ```
60
+
61
+ #### Summary
62
+
63
+ Only show summarized usage:
64
+
65
+ ```sh
66
+ cloudcost servers --summary
67
+ ```
68
+
69
+ #### Output CSV
70
+
71
+ Output in CSV format instead of a table:
72
+
73
+ ```sh
74
+ cloudcost servers --output csv
75
+ ```
76
+
77
+ #### Filter by name
78
+
79
+ Filter by servers by regex on name:
80
+
81
+ ```sh
82
+ # only show servers which names include a k8s or rancher:
83
+ cloudcost servers --name "k8s|rancher"
84
+
85
+ # exclude different name patterns
86
+ cloudcost servers --name "^[^ocp|^k8s|^rancher].*"
87
+ ```
88
+
89
+ #### Filter by tag
90
+
91
+ Filter servers by tag key:
92
+
93
+ ```sh
94
+ cloudcost servers --tag pitc_service
95
+ ```
96
+
97
+ Filter servers by tag value:
98
+
99
+ ```sh
100
+ cloudcost servers --tag pitc_service=ocp4
101
+ ```
102
+
103
+ ### Server Tags
104
+
105
+ #### Show tags
106
+
107
+ Display a list of servers and show theire tags:
108
+
109
+ ```sh
110
+ cloudcost server-tags
111
+ ```
112
+
113
+ Note thats the same filter options as with the `servers` command apply.
114
+
115
+ #### Show servers with missing tag
116
+
117
+ Only show servers which do NOT have a tag-key named "budget-group":
118
+
119
+ ```sh
120
+ cloudcost server-tags --missing-tag budget-group
121
+ ```
122
+
123
+ Note that this option can also be combined with `set-tags` or any other option.
124
+
125
+ #### Set tags
126
+
127
+ ```sh
128
+ cloudcost server-tags --name ldap --set-tags owner=sys budget-group=base-infrastructure
129
+ ```
130
+
131
+ #### Remove tags
132
+
133
+ ```sh
134
+ cloudcost server-tags --name ldap --remove-tags owner budget-group
135
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: %i[rubocop]
data/bin/cloudcost ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "cloudcost"
5
+
6
+ Cloudcost::CLI.start(ARGV)
data/cloudcost.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require_relative "lib/cloudcost/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "cloudcost"
8
+ s.version = Cloudcost::VERSION
9
+ s.homepage = "https://gitlab.puzzle.ch/nwolfgramm/cloudcost"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.authors = ["Nik Wolfgramm"]
13
+ s.description = "Calculate cloudscale.ch server costs from your actual deployment"
14
+ s.email = "wolfgramm@puzzle.ch"
15
+ s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
16
+ s.require_paths = ["lib"]
17
+ s.required_ruby_version = ">= 2.7"
18
+ s.summary = "cloudscale.ch cost explorer"
19
+ s.executables = %w[cloudcost]
20
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
21
+ s.license = "MIT"
22
+
23
+ s.add_dependency("excon", "~> 0.82.0")
24
+ s.add_dependency("parseconfig", "~> 1.1.0")
25
+ s.add_dependency("terminal-table", "~> 3.0.1")
26
+ s.add_dependency("thor", "~> 1.1.0")
27
+ s.add_dependency("tty-spinner", "~> 0.9.3")
28
+ end
data/data/pricing.yml ADDED
@@ -0,0 +1,29 @@
1
+ ---
2
+ # server pricing per day in CHF
3
+ servers:
4
+ flex-2: 1
5
+ flex-4: 1.5
6
+ flex-8: 3
7
+ flex-16: 6
8
+ flex-32: 12
9
+ flex-48: 18
10
+ flex-64: 24
11
+ flex-96: 36
12
+ plus-8: 4
13
+ plus-12: 6
14
+ plus-16: 8
15
+ plus-24: 12
16
+ plus-32: 16
17
+ plus-48: 24
18
+ plus-64: 32
19
+ plus-96: 48
20
+ plus-128: 64
21
+ plus-160: 80
22
+ plus-192: 96
23
+ plus-224: 112
24
+
25
+ # storage pricing per day and GB in CHF
26
+ storage:
27
+ ssd: 0.01
28
+ bulk: 0.0025
29
+ object: 0.003
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "json"
5
+
6
+ module Cloudcost
7
+ class ApiConnection
8
+ API_URL = "https://api.cloudscale.ch"
9
+
10
+ attr_accessor :connection
11
+
12
+ def initialize(api_token, options = {})
13
+ @api_url = options[:api_url] || API_URL
14
+ @api_token = api_token
15
+ @connection = new_connection
16
+ end
17
+
18
+ def get_resource(resource, options = {})
19
+ path = "v1/#{resource}"
20
+ path += "?tag:#{options[:tag]}" if options[:tag]
21
+ response = @connection.get(path: path, expects: [200])
22
+ JSON.parse(response.body, symbolize_names: true)
23
+ end
24
+
25
+ def get_servers(options = {})
26
+ servers = get_resource("servers", options)
27
+ servers = servers.reject { |server| server[:tags].key?(options[:missing_tag].to_sym) } if options[:missing_tag]
28
+ servers = servers.select { |server| /#{options[:name]}/.match? server[:name] } if options[:name]
29
+ servers
30
+ end
31
+
32
+ def set_server_tags(uuid, tags)
33
+ @connection.patch(
34
+ path: "v1/servers/#{uuid}",
35
+ body: { tags: tags }.to_json,
36
+ headers: { "Content-Type": "application/json" },
37
+ expects: [204]
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ def new_connection
44
+ Excon.new(
45
+ @api_url, headers: auth_header
46
+ )
47
+ end
48
+
49
+ def auth_header
50
+ { "Authorization" => "Bearer #{@api_token}" }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parseconfig"
4
+ require "excon"
5
+ require "json"
6
+ require "cloudcost/error"
7
+
8
+ module Cloudcost
9
+ class ApiToken
10
+ attr_accessor :profile, :token
11
+
12
+ def initialize(options = {})
13
+ @profile = options[:profile]
14
+ @token = load
15
+ end
16
+
17
+ def load
18
+ api_token = nil
19
+ if @profile
20
+ api_token = get_from_profile
21
+ raise ProfileError, "profile \"#{@profile}\" not found" unless api_token
22
+ else
23
+ api_token = ENV["CLOUDSCALE_API_TOKEN"]
24
+ raise TokenError, "no CLOUDSCALE_API_TOKEN found in environment" unless api_token
25
+ end
26
+ api_token
27
+ end
28
+
29
+ def get_from_profile
30
+ [
31
+ "#{ENV["XDG_CONFIG_HOME"] || "#{ENV["HOME"]}/.config"}/cloudscale/cloudscale.ini",
32
+ "#{ENV["HOME"]}/.cloudscale.ini",
33
+ "#{ENV["PWD"]}/cloudscale.ini"
34
+ ].each do |path|
35
+ if File.exist? path
36
+ config = ParseConfig.new(path)
37
+ return config[@profile]["api_token"] if config.groups.include? @profile
38
+ end
39
+ end
40
+ nil
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "tty-spinner"
5
+
6
+ module Cloudcost
7
+ class CLI < Thor
8
+ # Error raised by this runner
9
+ Error = Class.new(StandardError)
10
+
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ class_option :profile,
16
+ desc: "cloudscale.ini profile name",
17
+ aliases: %w[-p]
18
+
19
+ class_option :api_token,
20
+ desc: "cloudscale api token",
21
+ aliases: %w[-a]
22
+
23
+ desc "version", "app version"
24
+ def version
25
+ puts "cloudcost v#{Cloudcost::VERSION}"
26
+ end
27
+ map %w[--version -v] => :version
28
+
29
+ desc "servers", "explore servers"
30
+ option :name, desc: "filter name by regex", aliases: %w[-n]
31
+ option :tag, desc: "filter servers by tag", aliases: %w[-t]
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]
34
+ def servers
35
+ servers = load_servers(options)
36
+ spinner = TTY::Spinner.new("[:spinner] Calculating costs...", clear: options[:csv])
37
+ spinner.auto_spin
38
+ output(servers, options) do |result|
39
+ spinner.success "(done)"
40
+ puts
41
+ puts result
42
+ end
43
+ rescue Excon::Error, TokenError, ProfileError, PricingError => e
44
+ error_message = "ERROR: #{e.message}"
45
+ if spinner
46
+ spinner.error("(#{error_message})")
47
+ else
48
+ puts error_message
49
+ end
50
+ end
51
+
52
+ desc "server-tags", "show and assign tags of servers"
53
+ option :name, desc: "filter name by regex", aliases: %w[-n]
54
+ option :tag, desc: "filter servers by tag", aliases: %w[-t]
55
+ option :set_tags,
56
+ desc: "set tags",
57
+ aliases: %w[-T],
58
+ type: :array
59
+ option :remove_tags,
60
+ desc: "remove tags",
61
+ aliases: %w[-D],
62
+ type: :array
63
+ option :missing_tag,
64
+ desc: "show severs with missing tags",
65
+ aliases: %w[-M]
66
+ def server_tags
67
+ servers = load_servers(options)
68
+ servers.size.positive? ? puts(Cloudcost::ServerList.new(servers, options).tags_table) : exit
69
+ if (options[:set_tags] || options[:remove_tags]) && ask(
70
+ "Do you want to #{tag_option_to_s(options)}?",
71
+ default: "n"
72
+ ) == "y"
73
+ spinners = TTY::Spinner::Multi.new("[:spinner] Settings server tags")
74
+ servers.each do |server|
75
+ spinners.register("[:spinner] #{server.name}") do |spinner|
76
+ tags = server.tags.merge(options[:set_tags] ? tags_to_h(options[:set_tags]) : {})
77
+ (options[:remove_tags] || []).each do |tag|
78
+ tags.reject! { |k| k == tag.to_sym }
79
+ end
80
+ api_connection(options).set_server_tags(server.uuid, tags)
81
+ spinner.success
82
+ end
83
+ end
84
+ spinners.auto_spin
85
+ end
86
+ rescue Excon::Error, TokenError, ProfileError => e
87
+ error_message = "ERROR: #{e.message}"
88
+ if defined?(spinner)
89
+ spinner.error("(#{error_message})")
90
+ else
91
+ puts error_message
92
+ end
93
+ end
94
+
95
+ no_tasks do
96
+ def tags_to_h(tags_array)
97
+ tags_hash = {}
98
+ tags_array.each do |tag|
99
+ k_v = tag.split("=")
100
+ tags_hash[k_v[0].to_sym] = k_v[1]
101
+ end
102
+ tags_hash
103
+ end
104
+
105
+ def api_connection(options)
106
+ api_token = options[:api_token] || Cloudcost::ApiToken.new(options).token
107
+ Cloudcost::ApiConnection.new(api_token, options)
108
+ end
109
+
110
+ def load_servers(options)
111
+ spinner = TTY::Spinner.new("[:spinner] Loading servers...", clear: options[:csv])
112
+ spinner.auto_spin
113
+ servers = api_connection(options).get_servers(options).map { |server| Server.new(server) }
114
+ spinner.success "(#{servers.size} found)"
115
+ servers
116
+ end
117
+
118
+ def output(servers, options)
119
+ if options[:output] == "csv"
120
+ yield Cloudcost::ServerList.new(servers, options).to_csv
121
+ else
122
+ yield Cloudcost::ServerList.new(servers, options).cost_table
123
+ end
124
+ end
125
+
126
+ def tag_option_to_s(options)
127
+ messages = []
128
+ messages << "set tags \"#{options[:set_tags].join(", ")}\"" if options[:set_tags]
129
+ messages << "remove tags \"#{options[:remove_tags].join(", ")}\"" if options[:remove_tags]
130
+ messages.join(" and ")
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudcost
4
+ class Error < StandardError; end
5
+
6
+ class ProfileError < Error; end
7
+
8
+ class TokenError < Error; end
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ PRICING = YAML.load_file("data/pricing.yml")
6
+
7
+ module Cloudcost
8
+ class PricingError < StandardError
9
+ end
10
+
11
+ module Pricing
12
+ def self.server_costs_per_day(flavor)
13
+ PRICING["servers"][flavor] || raise(PricingError, "#{flavor} flavor not found in pricing.yml")
14
+ end
15
+
16
+ def self.storage_costs_per_day(type, size_in_gb)
17
+ raise PricingError, "#{type} storage type not found in pricing.yml" unless PRICING["storage"][type]
18
+
19
+ PRICING["storage"][type] * size_in_gb
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cloudcost/pricing"
4
+
5
+ module Cloudcost
6
+ def self.tags_to_s(tag_hash = [])
7
+ tag_hash.map { |k, v| "#{k}=#{v}" }.join(" ")
8
+ end
9
+
10
+ class Server
11
+ def initialize(data)
12
+ @data = data
13
+ @total_storage_per_type = sum_up_storage_per_type
14
+ end
15
+
16
+ def name
17
+ @data[:name]
18
+ end
19
+
20
+ def uuid
21
+ @data[:uuid]
22
+ end
23
+
24
+ def flavor
25
+ @data[:flavor][:slug]
26
+ end
27
+
28
+ def vcpu_count
29
+ @data[:flavor][:vcpu_count]
30
+ end
31
+
32
+ def memory_gb
33
+ @data[:flavor][:memory_gb]
34
+ end
35
+
36
+ def tags
37
+ @data[:tags]
38
+ end
39
+
40
+ def tags_to_s
41
+ Cloudcost.tags_to_s(tags)
42
+ end
43
+
44
+ def storage_size(type = :ssd)
45
+ @total_storage_per_type[type] || 0
46
+ end
47
+
48
+ def server_costs_per_day
49
+ Pricing.server_costs_per_day(@data[:flavor][:slug])
50
+ end
51
+
52
+ def storage_costs_per_day(type = :ssd)
53
+ Pricing.storage_costs_per_day(type.to_s, storage_size(type))
54
+ end
55
+
56
+ def total_costs_per_day
57
+ server_costs_per_day + storage_costs_per_day(:ssd) + storage_costs_per_day(:bulk)
58
+ end
59
+
60
+ def sum_up_storage_per_type
61
+ sum = {}
62
+ @data[:volumes].group_by { |volume| volume[:type].itself }.each do |group, vols|
63
+ sum.store(group.to_sym, 0)
64
+ vols.each { |volume| sum[volume[:type].to_sym] += volume[:size_gb] }
65
+ end
66
+ sum
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudcost
4
+ class ServerList
5
+ def initialize(servers, options = {})
6
+ @servers = servers
7
+ @options = options
8
+ end
9
+
10
+ def calculate_totals
11
+ totals = { vcpu: 0, memory: 0, ssd: 0, bulk: 0, cost: 0.0 }
12
+ @servers.each do |server|
13
+ totals[:vcpu] += server.vcpu_count
14
+ totals[:memory] += server.memory_gb
15
+ totals[:ssd] += server.storage_size(:ssd)
16
+ totals[:bulk] += server.storage_size(:bulk)
17
+ totals[:cost] += server.total_costs_per_day
18
+ end
19
+ totals
20
+ end
21
+
22
+ def headings
23
+ headings = @options[:summary] ? [""] : %w[Name UUID Flavor Tags]
24
+ headings.concat ["vCPU's", "Memory [GB]", "SSD [GB]", "Bulk [GB]", "CHF/day", "CHF/30-days"]
25
+ end
26
+
27
+ def rows
28
+ 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
44
+ end
45
+ rows
46
+ end
47
+
48
+ def totals
49
+ totals = calculate_totals
50
+ total_row = @options[:summary] ? %w[Total] : ["Total", "", "", ""]
51
+ total_row.concat [
52
+ totals[:vcpu],
53
+ totals[:memory],
54
+ totals[:ssd],
55
+ totals[:bulk],
56
+ format("%.2f", totals[:cost].round(2)),
57
+ format("%.2f", (totals[:cost] * 30).round(2))
58
+ ]
59
+ end
60
+
61
+ def tags_table
62
+ Terminal::Table.new do |t|
63
+ t.title = "cloudscale.ch server tags"
64
+ t.title += " (#{@options[:profile]})" if @options[:profile]
65
+ t.headings = %w[Name UUID Tags]
66
+ t.rows = @servers.sort_by(&:name).map do |server|
67
+ [
68
+ server.name,
69
+ server.uuid,
70
+ server.tags_to_s
71
+ ]
72
+ end
73
+ end
74
+ end
75
+
76
+ def cost_table
77
+ table = Terminal::Table.new do |t|
78
+ t.title = "cloudscale.ch server costs"
79
+ t.title += " (#{@options[:profile]})" if @options[:profile]
80
+ t.headings = headings
81
+ t.rows = rows unless @options[:summary]
82
+ end
83
+
84
+ table.add_separator unless @options[:summary]
85
+ table.add_row totals
86
+ first_number_row = @options[:summary] ? 1 : 2
87
+ (first_number_row..table.columns.size).each { |column| table.align_column(column, :right) }
88
+ table
89
+ end
90
+
91
+ def to_csv
92
+ CSV.generate do |csv|
93
+ csv << headings
94
+ if @options[:summary]
95
+ csv << totals
96
+ else
97
+ rows.each { |row| csv << row }
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloudcost
4
+ VERSION = "0.0.1"
5
+ end
data/lib/cloudcost.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "terminal-table"
4
+ require "csv"
5
+
6
+ require "cloudcost/version"
7
+ require "cloudcost/api_token"
8
+ require "cloudcost/api_connection"
9
+ require "cloudcost/server"
10
+ require "cloudcost/server_list"
11
+ require "cloudcost/cli"
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudcost
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Nik Wolfgramm
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: excon
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.82.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.82.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: parseconfig
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: terminal-table
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.0.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: thor
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.1.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.1.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: tty-spinner
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.9.3
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.9.3
83
+ description: Calculate cloudscale.ch server costs from your actual deployment
84
+ email: wolfgramm@puzzle.ch
85
+ executables:
86
+ - cloudcost
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rubocop.yml"
92
+ - Gemfile
93
+ - Gemfile.lock
94
+ - LICENSE
95
+ - README.md
96
+ - Rakefile
97
+ - bin/cloudcost
98
+ - cloudcost.gemspec
99
+ - data/pricing.yml
100
+ - lib/cloudcost.rb
101
+ - lib/cloudcost/api_connection.rb
102
+ - lib/cloudcost/api_token.rb
103
+ - lib/cloudcost/cli.rb
104
+ - lib/cloudcost/error.rb
105
+ - lib/cloudcost/pricing.rb
106
+ - lib/cloudcost/server.rb
107
+ - lib/cloudcost/server_list.rb
108
+ - lib/cloudcost/version.rb
109
+ homepage: https://gitlab.puzzle.ch/nwolfgramm/cloudcost
110
+ licenses:
111
+ - MIT
112
+ metadata: {}
113
+ post_install_message:
114
+ rdoc_options: []
115
+ require_paths:
116
+ - lib
117
+ required_ruby_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '2.7'
122
+ required_rubygems_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubygems_version: 3.1.2
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: cloudscale.ch cost explorer
132
+ test_files: []