hcloud 0.1.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|