cloudcost 0.0.1
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 +7 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +14 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +31 -0
- data/LICENSE +21 -0
- data/README.md +135 -0
- data/Rakefile +8 -0
- data/bin/cloudcost +6 -0
- data/cloudcost.gemspec +28 -0
- data/data/pricing.yml +29 -0
- data/lib/cloudcost/api_connection.rb +53 -0
- data/lib/cloudcost/api_token.rb +43 -0
- data/lib/cloudcost/cli.rb +134 -0
- data/lib/cloudcost/error.rb +9 -0
- data/lib/cloudcost/pricing.rb +22 -0
- data/lib/cloudcost/server.rb +69 -0
- data/lib/cloudcost/server_list.rb +102 -0
- data/lib/cloudcost/version.rb +5 -0
- data/lib/cloudcost.rb +11 -0
- metadata +132 -0
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/.rubocop.yml
ADDED
data/Gemfile
ADDED
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
data/bin/cloudcost
ADDED
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,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
|
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: []
|