hcloud 0.1.2 → 1.0.3
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 +5 -5
- data/.github/workflows/ruby.yml +32 -0
- data/.gitignore +0 -1
- data/.rubocop.yml +28 -10
- data/.rubocop_todo.yml +18 -121
- data/CHANGELOG.md +60 -2
- data/Gemfile +3 -0
- data/Gemfile.lock +149 -0
- data/README.md +34 -1
- data/Rakefile +2 -0
- data/bin/console +1 -0
- data/hcloud.gemspec +14 -9
- data/lib/hcloud/abstract_resource.rb +165 -60
- data/lib/hcloud/action.rb +8 -10
- data/lib/hcloud/action_resource.rb +3 -29
- data/lib/hcloud/client.rb +70 -29
- data/lib/hcloud/datacenter.rb +7 -7
- data/lib/hcloud/datacenter_resource.rb +6 -31
- data/lib/hcloud/entry_loader.rb +186 -20
- data/lib/hcloud/errors.rb +2 -0
- data/lib/hcloud/floating_ip.rb +18 -29
- data/lib/hcloud/floating_ip_resource.rb +15 -30
- data/lib/hcloud/image.rb +12 -32
- data/lib/hcloud/image_resource.rb +7 -38
- data/lib/hcloud/iso.rb +4 -1
- data/lib/hcloud/iso_resource.rb +7 -28
- data/lib/hcloud/location.rb +3 -9
- data/lib/hcloud/location_resource.rb +6 -27
- data/lib/hcloud/network.rb +33 -0
- data/lib/hcloud/network_resource.rb +25 -0
- data/lib/hcloud/pagination.rb +2 -9
- data/lib/hcloud/server.rb +37 -70
- data/lib/hcloud/server_resource.rb +17 -38
- data/lib/hcloud/server_type.rb +3 -10
- data/lib/hcloud/server_type_resource.rb +6 -28
- data/lib/hcloud/ssh_key.rb +6 -17
- data/lib/hcloud/ssh_key_resource.rb +13 -32
- data/lib/hcloud/typhoeus_ext.rb +110 -0
- data/lib/hcloud/version.rb +3 -1
- data/lib/hcloud/volume.rb +32 -0
- data/lib/hcloud/volume_resource.rb +29 -0
- data/lib/hcloud.rb +31 -5
- metadata +56 -22
- data/.travis.yml +0 -9
- data/lib/hcloud/multi_reply.rb +0 -21
@@ -1,75 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
|
1
5
|
module Hcloud
|
2
6
|
class AbstractResource
|
3
7
|
include Enumerable
|
4
8
|
|
5
|
-
|
9
|
+
delegate :request, :prepare_request, to: :client
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def bind_to(klass)
|
13
|
+
resource = self
|
14
|
+
%w[find find_by where all [] page limit per_page order
|
15
|
+
to_a count pagnation each].each do |method|
|
16
|
+
klass.define_singleton_method(method) do |*args, &block|
|
17
|
+
resource.new(client: Client.connection).public_send(method, *args, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def filter_attributes(*keys)
|
23
|
+
return @filter_attributes if keys.to_a.empty?
|
24
|
+
|
25
|
+
@filter_attributes = keys
|
26
|
+
end
|
27
|
+
|
28
|
+
def resource_class
|
29
|
+
ancestors[ancestors.index(Hcloud::AbstractResource) - 1]
|
30
|
+
end
|
31
|
+
|
32
|
+
def resource_url(url = nil)
|
33
|
+
return (@resource_url = url) if url
|
34
|
+
|
35
|
+
@resource_url || resource_class.name.demodulize.gsub('Resource', '').tableize
|
36
|
+
end
|
37
|
+
|
38
|
+
def resource_path(path = nil)
|
39
|
+
return (@resource_path = path) if path
|
40
|
+
|
41
|
+
@resource_path || resource_url
|
42
|
+
end
|
43
|
+
|
44
|
+
def resource(res = nil)
|
45
|
+
return (@resource = res) if res
|
46
|
+
return @resource if @resource
|
47
|
+
|
48
|
+
auto_const = resource_class.name.demodulize.gsub('Resource', '').to_sym
|
49
|
+
return Hcloud.const_get(auto_const) if Hcloud.constants.include?(auto_const)
|
50
|
+
|
51
|
+
raise Error, "unable to lookup resource class for #{name}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :client
|
6
56
|
|
7
|
-
def initialize(client:,
|
57
|
+
def initialize(client:, base_path: '')
|
8
58
|
@client = client
|
9
|
-
@parent = parent
|
10
59
|
@page = 1
|
11
60
|
@per_page = 25
|
12
61
|
@order = []
|
13
62
|
@base_path = base_path
|
14
63
|
end
|
15
64
|
|
65
|
+
def all
|
66
|
+
where
|
67
|
+
end
|
68
|
+
|
69
|
+
def where(kwargs = {})
|
70
|
+
kwargs.each_key do |key|
|
71
|
+
keys = self.class.filter_attributes.map(&:to_s)
|
72
|
+
next if keys.include?(key.to_s)
|
73
|
+
|
74
|
+
raise ArgumentError, "unknown filter #{key}, allowed keys are #{keys}"
|
75
|
+
end
|
76
|
+
|
77
|
+
_dup :@query, @query.to_h.merge(kwargs)
|
78
|
+
end
|
79
|
+
|
80
|
+
def find(id)
|
81
|
+
prepare_request(
|
82
|
+
[self.class.resource_url, id].join('/'),
|
83
|
+
resource_path: resource_path.to_s.singularize,
|
84
|
+
resource_class: self.class.resource
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
def [](arg)
|
89
|
+
find_by(id: arg)
|
90
|
+
end
|
91
|
+
|
92
|
+
def find_by(**kwargs)
|
93
|
+
if id = kwargs.delete(:id)
|
94
|
+
return find(id)
|
95
|
+
end
|
96
|
+
|
97
|
+
per_page(1).where(**kwargs).first
|
98
|
+
rescue Error::NotFound
|
99
|
+
end
|
100
|
+
|
101
|
+
# def count
|
102
|
+
# per_page(1).first&.response&.pagination&.total_entries.to_i
|
103
|
+
# end
|
104
|
+
|
16
105
|
def page(page)
|
17
|
-
|
18
|
-
self
|
106
|
+
_dup :@page, page
|
19
107
|
end
|
20
108
|
|
21
109
|
def per_page(per_page)
|
22
|
-
|
23
|
-
self
|
110
|
+
_dup :@per_page, per_page
|
24
111
|
end
|
25
112
|
|
26
113
|
def limit(limit)
|
27
|
-
|
28
|
-
self
|
114
|
+
_dup :@limit, limit
|
29
115
|
end
|
30
116
|
|
31
117
|
def order(*sort)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
118
|
+
_dup :@order,
|
119
|
+
begin
|
120
|
+
sort.flat_map do |s|
|
121
|
+
case s
|
122
|
+
when Symbol, String then s.to_s
|
123
|
+
when Hash then s.map { |k, v| "#{k}:#{v}" }
|
124
|
+
else
|
125
|
+
raise ArgumentError,
|
126
|
+
"Unable to resolve type for given #{s.inspect} from #{sort}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
42
130
|
end
|
43
131
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
52
|
-
client.hydra.run
|
53
|
-
j = requests.map do |x|
|
54
|
-
Oj.load(x.response.body)
|
55
|
-
end
|
56
|
-
m = MultiReply.new(j: j, pagination: :auto)
|
57
|
-
m.cb = block
|
58
|
-
return m
|
59
|
-
end
|
60
|
-
m = MultiReply.new(j: [Oj.load(request(path, o.merge(ep: ep)).run.body)])
|
61
|
-
m.cb = block
|
62
|
-
m
|
132
|
+
def run
|
133
|
+
@run ||= multi_query(
|
134
|
+
resource_url,
|
135
|
+
q: @query,
|
136
|
+
resource_path: resource_path,
|
137
|
+
resource_class: self.class.resource
|
138
|
+
)
|
63
139
|
end
|
64
140
|
|
65
|
-
def each
|
66
|
-
|
67
|
-
|
68
|
-
|
141
|
+
def each(&block)
|
142
|
+
run.each(&block)
|
143
|
+
end
|
144
|
+
|
145
|
+
# this is just to keep the actual bevahior
|
146
|
+
def pagination
|
147
|
+
return :auto if client.auto_pagination
|
148
|
+
|
149
|
+
run.response.pagination
|
69
150
|
end
|
70
151
|
|
71
152
|
protected
|
72
153
|
|
154
|
+
def _dup(var, value)
|
155
|
+
dup.tap do |res|
|
156
|
+
res.instance_variable_set(var, value)
|
157
|
+
res.instance_variable_set(:@run, nil)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def resource_path
|
162
|
+
self.class.resource_path || self.class.resource_url
|
163
|
+
end
|
164
|
+
|
165
|
+
def resource_url
|
166
|
+
[@base_path.to_s, self.class.resource_url.to_s].reject(&:empty?).join('/')
|
167
|
+
end
|
168
|
+
|
169
|
+
def multi_query(path, **o)
|
170
|
+
return prepare_request(path, o.merge(ep: ep)) unless client&.auto_pagination
|
171
|
+
|
172
|
+
raise Error, 'unable to run auto paginate within concurrent excecution' if @concurrent
|
173
|
+
|
174
|
+
requests = __entries__(path, **o)
|
175
|
+
return requests.flat_map(&:resource) if requests.all? { |req| req.respond_to? :resource }
|
176
|
+
|
177
|
+
client.hydra.run
|
178
|
+
requests.flat_map { |req| req.response.resource }
|
179
|
+
end
|
180
|
+
|
73
181
|
def page_params(per_page: nil, page: nil)
|
74
182
|
{ per_page: per_page || @per_page, page: page || @page }.to_param
|
75
183
|
end
|
@@ -85,28 +193,25 @@ module Hcloud
|
|
85
193
|
r.compact.join('&')
|
86
194
|
end
|
87
195
|
|
88
|
-
|
89
|
-
client.request(*args)
|
90
|
-
end
|
91
|
-
|
196
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
92
197
|
def __entries__(path, **o)
|
93
|
-
|
94
|
-
|
95
|
-
return [
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
requests = r.times.map do |i|
|
198
|
+
first_page = request(path, o.merge(ep: ep(per_page: 1, page: 1))).run
|
199
|
+
total_entries = first_page.pagination.total_entries
|
200
|
+
return [first_page] if total_entries <= 1 || @limit == 1
|
201
|
+
|
202
|
+
total_entries = @limit if !@limit.nil? && (total_entries > @limit)
|
203
|
+
pages = total_entries / Client::MAX_ENTRIES_PER_PAGE
|
204
|
+
pages += 1 if (total_entries % Client::MAX_ENTRIES_PER_PAGE).positive?
|
205
|
+
pages.times.map do |page|
|
102
206
|
per_page = Client::MAX_ENTRIES_PER_PAGE
|
103
|
-
if !@limit.nil? && (
|
104
|
-
per_page =
|
207
|
+
if !@limit.nil? && (pages == (page + 1)) && (total_entries % per_page != 0)
|
208
|
+
per_page = total_entries % per_page
|
209
|
+
end
|
210
|
+
request(path, o.merge(ep: ep(per_page: per_page, page: page + 1))).tap do |req|
|
211
|
+
client.hydra.queue req
|
105
212
|
end
|
106
|
-
req = request(path, o.merge(ep: ep(per_page: per_page, page: i + 1)))
|
107
|
-
client.hydra.queue req
|
108
|
-
req
|
109
213
|
end
|
110
214
|
end
|
215
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
111
216
|
end
|
112
217
|
end
|
data/lib/hcloud/action.rb
CHANGED
@@ -1,16 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Hcloud
|
2
4
|
class Action
|
3
|
-
|
4
|
-
id: nil,
|
5
|
-
command: nil,
|
6
|
-
status: nil,
|
7
|
-
progress: nil,
|
8
|
-
started: :time,
|
9
|
-
finished: :time,
|
10
|
-
resources: nil,
|
11
|
-
error: nil
|
12
|
-
}.freeze
|
5
|
+
require 'hcloud/action_resource'
|
13
6
|
|
14
7
|
include EntryLoader
|
8
|
+
|
9
|
+
schema(
|
10
|
+
started: :time,
|
11
|
+
finished: :time
|
12
|
+
)
|
15
13
|
end
|
16
14
|
end
|
@@ -1,35 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
|
2
3
|
module Hcloud
|
3
4
|
class ActionResource < AbstractResource
|
4
|
-
|
5
|
-
mj(base_path('actions')) do |j|
|
6
|
-
j.flat_map { |x| x['actions'].map { |x| Action.new(x, self, client) } }
|
7
|
-
end
|
8
|
-
end
|
5
|
+
filter_attributes :status
|
9
6
|
|
10
|
-
|
11
|
-
Action.new(
|
12
|
-
Oj.load(request(base_path("actions/#{id.to_i}")).run.body)['action'],
|
13
|
-
self,
|
14
|
-
client
|
15
|
-
)
|
16
|
-
end
|
17
|
-
|
18
|
-
def [](arg)
|
19
|
-
find(arg)
|
20
|
-
rescue Error::NotFound
|
21
|
-
end
|
22
|
-
|
23
|
-
def where(status: nil)
|
24
|
-
mj(base_path('actions'), q: { status: status }) do |j|
|
25
|
-
j.flat_map { |x| x['actions'].map { |x| Action.new(x, self, client) } }
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def base_path(ext)
|
32
|
-
[@base_path, ext].reject(&:empty?).join('/')
|
33
|
-
end
|
7
|
+
bind_to Action
|
34
8
|
end
|
35
9
|
end
|
data/lib/hcloud/client.rb
CHANGED
@@ -1,18 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
autoload :Typhoeus, 'typhoeus'
|
2
4
|
autoload :Oj, 'oj'
|
3
5
|
|
6
|
+
require 'delegate'
|
7
|
+
|
4
8
|
module Hcloud
|
5
9
|
class Client
|
6
10
|
MAX_ENTRIES_PER_PAGE = 50
|
7
11
|
|
8
|
-
|
9
|
-
|
12
|
+
class << self
|
13
|
+
attr_writer :connection
|
14
|
+
|
15
|
+
def connection
|
16
|
+
return @connection if @connection.is_a? Hcloud::Client
|
17
|
+
|
18
|
+
raise ArgumentError, "client not correctly initialized, actually #{@client.inspect}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :token, :auto_pagination, :hydra, :user_agent
|
23
|
+
|
24
|
+
def initialize(token:, auto_pagination: false, concurrency: 20, user_agent: nil)
|
10
25
|
@token = token
|
26
|
+
@user_agent = user_agent || "hcloud-ruby v#{VERSION}"
|
11
27
|
@auto_pagination = auto_pagination
|
12
28
|
@concurrency = concurrency
|
13
29
|
@hydra = Typhoeus::Hydra.new(max_concurrency: concurrency)
|
14
30
|
end
|
15
31
|
|
32
|
+
def concurrent
|
33
|
+
@concurrent = true
|
34
|
+
ret = yield
|
35
|
+
ret.each do |element|
|
36
|
+
next unless element.is_a?(AbstractResource)
|
37
|
+
|
38
|
+
element.run
|
39
|
+
end
|
40
|
+
hydra.run
|
41
|
+
ret
|
42
|
+
ensure
|
43
|
+
@concurrent = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def concurrent?
|
47
|
+
!@concurrent.nil?
|
48
|
+
end
|
49
|
+
|
16
50
|
def authorized?
|
17
51
|
request('server_types').run
|
18
52
|
true
|
@@ -56,8 +90,34 @@ module Hcloud
|
|
56
90
|
FloatingIPResource.new(client: self)
|
57
91
|
end
|
58
92
|
|
59
|
-
def
|
60
|
-
|
93
|
+
def networks
|
94
|
+
NetworkResource.new(client: self)
|
95
|
+
end
|
96
|
+
|
97
|
+
def volumes
|
98
|
+
VolumeResource.new(client: self)
|
99
|
+
end
|
100
|
+
|
101
|
+
class ResourceFuture < Delegator
|
102
|
+
def initialize(request) # rubocop:disable Lint/MissingSuper
|
103
|
+
@request = request
|
104
|
+
end
|
105
|
+
|
106
|
+
def __getobj__
|
107
|
+
@__getobj__ ||= @request&.response&.resource
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def prepare_request(url, args = {}, &block)
|
112
|
+
req = request(url, **args.merge(block: block))
|
113
|
+
return req.run.resource unless concurrent?
|
114
|
+
|
115
|
+
hydra.queue req
|
116
|
+
ResourceFuture.new(req)
|
117
|
+
end
|
118
|
+
|
119
|
+
def request(path, options = {}) # rubocop:disable Metrics/MethodLength
|
120
|
+
hcloud_attributes = TyphoeusExt.collect_attributes(options)
|
61
121
|
if x = options.delete(:j)
|
62
122
|
options[:body] = Oj.dump(x, mode: :compat)
|
63
123
|
options[:method] ||= :post
|
@@ -68,41 +128,22 @@ module Hcloud
|
|
68
128
|
q << x.to_param
|
69
129
|
end
|
70
130
|
path = path.dup
|
71
|
-
path <<
|
131
|
+
path << "?#{q.join('&')}"
|
72
132
|
r = Typhoeus::Request.new(
|
73
133
|
"https://api.hetzner.cloud/v1/#{path}",
|
74
134
|
{
|
75
135
|
headers: {
|
76
136
|
'Authorization' => "Bearer #{token}",
|
137
|
+
'User-Agent' => user_agent,
|
77
138
|
'Content-Type' => 'application/json'
|
78
139
|
}
|
79
140
|
}.merge(options)
|
80
141
|
)
|
81
142
|
r.on_complete do |response|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
raise Error::Unauthorized
|
87
|
-
when 0
|
88
|
-
raise Error::ServerError, "Connection error: #{response.return_code}"
|
89
|
-
when 400...600
|
90
|
-
j = Oj.load(response.body)
|
91
|
-
code = j.dig('error', 'code')
|
92
|
-
error_class = case code
|
93
|
-
when 'invalid_input' then Error::InvalidInput
|
94
|
-
when 'forbidden' then Error::Forbidden
|
95
|
-
when 'locked' then Error::Locked
|
96
|
-
when 'not_found' then Error::NotFound
|
97
|
-
when 'rate_limit_exceeded' then Error::RateLimitExceeded
|
98
|
-
when 'resource_unavailable' then Error::ResourceUnavilable
|
99
|
-
when 'service_error' then Error::ServiceError
|
100
|
-
when 'uniqueness_error' then Error::UniquenessError
|
101
|
-
else
|
102
|
-
Error::ServerError
|
103
|
-
end
|
104
|
-
raise error_class, j.dig('error', 'message')
|
105
|
-
end
|
143
|
+
response.extend(TyphoeusExt)
|
144
|
+
response.attributes = hcloud_attributes
|
145
|
+
response.context.client = self
|
146
|
+
response.check_for_error unless response.request.hydra
|
106
147
|
end
|
107
148
|
r
|
108
149
|
end
|
data/lib/hcloud/datacenter.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Hcloud
|
2
4
|
class Datacenter
|
3
|
-
|
4
|
-
id: nil,
|
5
|
-
name: nil,
|
6
|
-
description: nil,
|
7
|
-
location: Location,
|
8
|
-
server_types: nil
|
9
|
-
}.freeze
|
5
|
+
require 'hcloud/datacenter_resource'
|
10
6
|
|
11
7
|
include EntryLoader
|
8
|
+
|
9
|
+
schema(
|
10
|
+
location: Location
|
11
|
+
)
|
12
12
|
end
|
13
13
|
end
|
@@ -1,44 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Hcloud
|
2
4
|
class DatacenterResource < AbstractResource
|
3
|
-
|
5
|
+
filter_attributes :name
|
4
6
|
|
5
|
-
|
6
|
-
j = Oj.load(request('datacenters').run.body)
|
7
|
-
dc = j['datacenters'].map { |x| Datacenter.new(x, self, client) }
|
8
|
-
dc.reject { |x| x.id == j['recommendation'] }.unshift(
|
9
|
-
dc.find { |x| x.id == j['recommendation'] }
|
10
|
-
)
|
11
|
-
end
|
7
|
+
bind_to Datacenter
|
12
8
|
|
13
9
|
def recommended
|
14
10
|
all.first
|
15
11
|
end
|
16
12
|
|
17
|
-
def find(id)
|
18
|
-
Datacenter.new(
|
19
|
-
Oj.load(request("datacenters/#{id}").run.body)['datacenter'],
|
20
|
-
self,
|
21
|
-
client
|
22
|
-
)
|
23
|
-
end
|
24
|
-
|
25
|
-
def find_by(name:)
|
26
|
-
x = Oj.load(request('datacenters', q: { name: name }).run.body)['datacenters']
|
27
|
-
return nil if x.none?
|
28
|
-
x.each do |s|
|
29
|
-
return Datacenter.new(s, self, client)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
13
|
def [](arg)
|
34
14
|
case arg
|
35
|
-
when Integer
|
36
|
-
|
37
|
-
find(arg)
|
38
|
-
rescue Error::NotFound
|
39
|
-
end
|
40
|
-
when String
|
41
|
-
find_by(name: arg)
|
15
|
+
when Integer then find_by(id: arg)
|
16
|
+
when String then find_by(name: arg)
|
42
17
|
end
|
43
18
|
end
|
44
19
|
end
|