cloudcost 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|