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.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +32 -0
  3. data/.gitignore +0 -1
  4. data/.rubocop.yml +28 -10
  5. data/.rubocop_todo.yml +18 -121
  6. data/CHANGELOG.md +60 -2
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +149 -0
  9. data/README.md +34 -1
  10. data/Rakefile +2 -0
  11. data/bin/console +1 -0
  12. data/hcloud.gemspec +14 -9
  13. data/lib/hcloud/abstract_resource.rb +165 -60
  14. data/lib/hcloud/action.rb +8 -10
  15. data/lib/hcloud/action_resource.rb +3 -29
  16. data/lib/hcloud/client.rb +70 -29
  17. data/lib/hcloud/datacenter.rb +7 -7
  18. data/lib/hcloud/datacenter_resource.rb +6 -31
  19. data/lib/hcloud/entry_loader.rb +186 -20
  20. data/lib/hcloud/errors.rb +2 -0
  21. data/lib/hcloud/floating_ip.rb +18 -29
  22. data/lib/hcloud/floating_ip_resource.rb +15 -30
  23. data/lib/hcloud/image.rb +12 -32
  24. data/lib/hcloud/image_resource.rb +7 -38
  25. data/lib/hcloud/iso.rb +4 -1
  26. data/lib/hcloud/iso_resource.rb +7 -28
  27. data/lib/hcloud/location.rb +3 -9
  28. data/lib/hcloud/location_resource.rb +6 -27
  29. data/lib/hcloud/network.rb +33 -0
  30. data/lib/hcloud/network_resource.rb +25 -0
  31. data/lib/hcloud/pagination.rb +2 -9
  32. data/lib/hcloud/server.rb +37 -70
  33. data/lib/hcloud/server_resource.rb +17 -38
  34. data/lib/hcloud/server_type.rb +3 -10
  35. data/lib/hcloud/server_type_resource.rb +6 -28
  36. data/lib/hcloud/ssh_key.rb +6 -17
  37. data/lib/hcloud/ssh_key_resource.rb +13 -32
  38. data/lib/hcloud/typhoeus_ext.rb +110 -0
  39. data/lib/hcloud/version.rb +3 -1
  40. data/lib/hcloud/volume.rb +32 -0
  41. data/lib/hcloud/volume_resource.rb +29 -0
  42. data/lib/hcloud.rb +31 -5
  43. metadata +56 -22
  44. data/.travis.yml +0 -9
  45. data/lib/hcloud/multi_reply.rb +0 -21
@@ -1,36 +1,202 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/concern'
4
+ require 'active_model'
5
+
2
6
  module Hcloud
3
- module EntryLoader
7
+ module EntryLoader # rubocop:disable Metrics/ModuleLength
4
8
  extend ActiveSupport::Concern
5
9
 
10
+ module Collection
11
+ attr_accessor :response
12
+ end
13
+
6
14
  included do |klass|
7
- klass.send(:attr_reader, :parent, :client)
8
- klass.const_get(:Attributes).each do |attribute, _value|
9
- klass.send(:attr_accessor, attribute)
10
- end
15
+ klass.send(:attr_writer, :client)
16
+ klass.include(ActiveModel::Dirty)
11
17
  end
12
18
 
13
- def initialize(resource, parent, client)
14
- @parent = parent
15
- @client = client
16
- self.class.const_get(:Attributes).each do |attribute, value|
17
- case value
18
- when nil
19
- send("#{attribute}=", resource[attribute.to_s])
20
- when :time
21
- unless resource[attribute.to_s].nil?
22
- send("#{attribute}=", Time.parse(resource[attribute.to_s]))
19
+ class_methods do # rubocop:disable Metrics/BlockLength
20
+ attr_accessor :resource_url
21
+
22
+ def _callbacks
23
+ @_callbacks ||= { before: [], after: [] }
24
+ end
25
+
26
+ %i[before after].each do |method|
27
+ define_method(method) { |&block| _callbacks[method] << block }
28
+ end
29
+
30
+ def schema(**kwargs)
31
+ @schema ||= {}.with_indifferent_access
32
+ return @schema if kwargs.empty?
33
+
34
+ @schema.merge!(kwargs)
35
+ end
36
+
37
+ def updatable(*args)
38
+ define_attribute_methods(*args)
39
+ args.each do |updateable_attribute|
40
+ define_method(updateable_attribute) { _attributes[updateable_attribute] }
41
+ define_method("#{updateable_attribute}=") do |value|
42
+ if _attributes[updateable_attribute] != value
43
+ public_send("#{updateable_attribute}_will_change!")
44
+ end
45
+ _update_attribute(updateable_attribute, value)
23
46
  end
24
- else
25
- if value.is_a?(Class) && value.include?(EntryLoader)
26
- send("#{attribute}=", value.new(resource[attribute.to_s], self, client))
47
+ end
48
+ end
49
+
50
+ def destructible
51
+ define_method(:destroy) { prepare_request(method: :delete) }
52
+ end
53
+
54
+ def protectable(*args)
55
+ define_method(:change_protection) do |**kwargs|
56
+ kwargs.each_key do |key|
57
+ next if args.map(&:to_s).include? key.to_s
58
+
59
+ raise ArgumentError, "#{key} not an allowed protection mode (allowed: #{args})"
27
60
  end
61
+ prepare_request('actions/change_protection', j: kwargs)
62
+ end
63
+ end
64
+
65
+ def has_actions # rubocop:disable Naming/PredicateName
66
+ define_method(:actions) do
67
+ ActionResource.new(client: client, base_path: resource_url)
68
+ end
69
+ end
70
+
71
+ def resource_class
72
+ ancestors.reverse.find { |const| const.include?(Hcloud::EntryLoader) }
73
+ end
74
+
75
+ def from_response(response, autoload_action: nil)
76
+ attributes = response.resource_attributes
77
+ action = response.parsed_json[:action] if autoload_action
78
+ client = response.context.client
79
+ if attributes.is_a?(Array)
80
+ results = attributes.map { |item| new(item).tap { |entity| entity.response = response } }
81
+ results.tap { |ary| ary.extend(Collection) }.response = response
82
+ return results
83
+ end
84
+
85
+ return Action.new(client, action) if attributes.nil? && action
86
+ return new(attributes).tap { |entity| entity.response = response } if action.nil?
87
+
88
+ [
89
+ Action.new(client, action),
90
+ new(attributes).tap { |entity| entity.response = response }
91
+ ]
92
+ end
93
+ end
94
+
95
+ attr_accessor :response
96
+
97
+ def initialize(client = nil, kwargs = {})
98
+ if client.is_a?(Hash)
99
+ kwargs = client
100
+ client = nil
101
+ end
102
+
103
+ @client = client
104
+ _load(kwargs)
105
+ end
106
+
107
+ def inspect
108
+ "#<#{self.class.name}:0x#{__id__.to_s(16)} #{_attributes.inspect}>"
109
+ end
110
+
111
+ def client
112
+ @client || response&.context&.client
113
+ end
114
+
115
+ def resource_url
116
+ if block = self.class.resource_url
117
+ return instance_exec(&block)
118
+ end
119
+
120
+ [self.class.resource_class.name.demodulize.tableize, id].compact.join('/')
121
+ end
122
+
123
+ def resource_path
124
+ self.class.resource_class.name.demodulize.underscore
125
+ end
126
+
127
+ def prepare_request(url_suffix = nil, **kwargs, &block)
128
+ kwargs[:resource_path] ||= resource_path
129
+ kwargs[:resource_class] ||= self.class
130
+ kwargs[:autoload_action] = true unless kwargs.key?(:autoload_action)
131
+
132
+ client.prepare_request(
133
+ [resource_url, url_suffix].compact.join('/'),
134
+ **kwargs,
135
+ &block
136
+ )
137
+ end
138
+
139
+ def _attributes
140
+ @_attributes ||= {}.with_indifferent_access
141
+ end
142
+
143
+ def method_missing(method, *args, &block)
144
+ _attributes.key?(method) ? _attributes[method] : super
145
+ end
146
+
147
+ def respond_to_missing?(method, *args, &block)
148
+ _attributes.key?(method) || super
149
+ end
150
+
151
+ def update(**kwargs)
152
+ context = self
153
+ _run_callbacks(:before)
154
+ prepare_request(j: kwargs, method: :put) do |response|
155
+ response.resource_class.from_response(
156
+ response,
157
+ autoload_action: response.autoload_action
158
+ ).tap do |*_args|
159
+ _run_callbacks(:after)
160
+ context.send(:changes_applied)
28
161
  end
29
162
  end
30
163
  end
31
164
 
32
- def request(*args)
33
- client.request(*args)
165
+ def save
166
+ update(changes.map { |key, _value| [key.to_sym, _attributes[key]] }.to_h)
167
+ end
168
+
169
+ def rollback
170
+ restore_attributes
171
+ end
172
+
173
+ def _run_callbacks(order)
174
+ self.class._callbacks[order].each { |block| instance_exec(&block) }
175
+ end
176
+
177
+ def _update_attribute(key, value)
178
+ _attributes[key] = value
179
+ instance_variable_set("@#{key}", value)
180
+ end
181
+
182
+ def _load(resource)
183
+ @_attributes = {}.with_indifferent_access
184
+
185
+ resource.each do |key, value|
186
+ definition = self.class.schema[key]
187
+
188
+ if definition == :time
189
+ _update_attribute(key, value ? Time.parse(value) : nil)
190
+ next
191
+ end
192
+
193
+ if definition.is_a?(Class) && definition.include?(EntryLoader)
194
+ _update_attribute(key, value ? definition.new(client, value) : nil)
195
+ next
196
+ end
197
+
198
+ _update_attribute(key, value.is_a?(Hash) ? value.with_indifferent_access : value)
199
+ end
34
200
  end
35
201
  end
36
202
  end
data/lib/hcloud/errors.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class Error < StandardError
3
5
  class Unauthorized < Error; end
@@ -1,43 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class FloatingIP
3
- Attributes = {
4
- id: nil,
5
- description: nil,
6
- ip: nil,
7
- type: nil,
8
- dns_ptr: nil,
9
- server: nil,
10
- home_location: Location,
11
- blocked: nil
12
- }.freeze
5
+ require 'hcloud/floating_ip_resource'
6
+
13
7
  include EntryLoader
14
8
 
15
- def update(description:)
16
- j = Oj.load(request("floating_ips/#{id.to_i}",
17
- j: { description: description },
18
- method: :put).run.body)
19
- FloatingIP.new(j['floating_ip'], self, client)
20
- end
9
+ schema(
10
+ home_location: Location,
11
+ created: :time
12
+ )
13
+
14
+ protectable :delete
15
+ updatable :description
16
+ destructible
17
+
18
+ has_actions
21
19
 
22
20
  def assign(server:)
23
- j = Oj.load(request("floating_ips/#{id.to_i}/actions/assign",
24
- j: { server: server }).run.body)
25
- Action.new(j['action'], self, client)
21
+ prepare_request('actions/assign', j: COLLECT_ARGS.call(__method__, binding))
26
22
  end
27
23
 
28
24
  def unassign
29
- j = Oj.load(request("floating_ips/#{id.to_i}/actions/unassign",
30
- method: :post).run.body)
31
- Action.new(j['action'], self, client)
32
- end
33
-
34
- def actions
35
- ActionResource.new(client: client, parent: self, base_path: "floating_ips/#{id.to_i}")
25
+ prepare_request('actions/unassign', method: :post)
36
26
  end
37
27
 
38
- def destroy
39
- request("floating_ips/#{id}", method: :delete).run.body
40
- true
28
+ def change_dns_ptr(ip:, dns_ptr:)
29
+ prepare_request('actions/change_dns_ptr', j: COLLECT_ARGS.call(__method__, binding))
41
30
  end
42
31
  end
43
32
  end
@@ -1,40 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class FloatingIPResource < AbstractResource
3
- include Enumerable
5
+ filter_attributes :name
4
6
 
5
- def all
6
- mj('floating_ips') do |j|
7
- j.flat_map { |x| x['floating_ips'].map { |x| FloatingIP.new(x, self, client) } }
8
- end
9
- end
7
+ bind_to FloatingIP
10
8
 
11
- def create(type:, server: nil, home_location: nil, description: nil)
12
- query = {}
13
- method(:create).parameters.inject(query) do |r, x|
14
- (var = eval(x.last.to_s)).nil? ? r : r.merge!(x.last => var)
9
+ def [](arg)
10
+ case arg
11
+ when Integer then find_by(id: arg)
12
+ when String then find_by(name: arg)
15
13
  end
16
- j = Oj.load(request('floating_ips', j: query, code: 200).run.body)
17
- [
18
- j.key?('action') ? Action.new(j['action'], self, client) : nil,
19
- FloatingIP.new(j['floating_ip'], self, client)
20
- ]
21
14
  end
22
15
 
23
- def find(id)
24
- FloatingIP.new(
25
- Oj.load(request("floating_ips/#{id}").run.body)['floating_ip'],
26
- self,
27
- client
28
- )
29
- end
30
-
31
- def [](arg)
32
- case arg
33
- when Integer
34
- begin
35
- find(arg)
36
- rescue Error::NotFound
37
- end
16
+ def create(type:, server: nil, home_location: nil, description: nil)
17
+ prepare_request(
18
+ 'floating_ips', j: COLLECT_ARGS.call(__method__, binding),
19
+ expected_code: 201
20
+ ) do |response|
21
+ action = Action.new(client, response[:action]) if response[:action]
22
+ [action, FloatingIP.new(client, response[:floating_ip])]
38
23
  end
39
24
  end
40
25
  end
data/lib/hcloud/image.rb CHANGED
@@ -1,42 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class Image
3
- Attributes = {
4
- id: nil,
5
- type: nil,
6
- status: nil,
7
- name: nil,
8
- description: nil,
9
- image_size: nil,
10
- disk_size: nil,
11
- created: :time,
12
- created_from: nil,
13
- bound_to: nil,
14
- os_flavor: nil,
15
- os_version: nil,
16
- rapid_deploy: nil
17
- }.freeze
5
+ require 'hcloud/image_resource'
18
6
 
19
7
  include EntryLoader
20
8
 
21
- def to_snapshot
22
- update(type: 'snapshot')
23
- end
9
+ schema(
10
+ created: :time,
11
+ deprecated: :time
12
+ )
24
13
 
25
- def update(description: nil, type: nil)
26
- query = {}
27
- method(:update).parameters.inject(query) do |r, x|
28
- (var = eval(x.last.to_s)).nil? ? r : r.merge!(x.last => var)
29
- end
30
- Image.new(
31
- Oj.load(request("images/#{id.to_i}", j: query, method: :put).run.body)['image'],
32
- parent,
33
- client
34
- )
35
- end
14
+ protectable :delete
15
+ updatable :description, :type
16
+ destructible
36
17
 
37
- def destroy
38
- request("images/#{id.to_i}", method: :delete).run
39
- true
18
+ def to_snapshot
19
+ update(type: 'snapshot')
40
20
  end
41
21
  end
42
22
  end
@@ -1,46 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class ImageResource < AbstractResource
3
- def all
4
- mj('images') do |j|
5
- j.flat_map { |x| x['images'].map { |x| Image.new(x, self, client) } }
6
- end
7
- end
5
+ filter_attributes :type, :bound_to, :name
6
+
7
+ bind_to Image
8
8
 
9
9
  def [](arg)
10
10
  case arg
11
- when Integer
12
- begin
13
- find(arg)
14
- rescue Error::NotFound
15
- end
16
- when String
17
- find_by(name: arg)
18
- end
19
- end
20
-
21
- def find(id)
22
- Image.new(
23
- Oj.load(request("images/#{id.to_i}").run.body)['image'],
24
- self,
25
- client
26
- )
27
- end
28
-
29
- def where(sort: nil, type: nil, bound_to: nil, name: nil)
30
- query = {}
31
- method(:where).parameters.inject(query) do |r, x|
32
- (var = eval(x.last.to_s)).nil? ? r : r.merge!(x.last => var)
33
- end
34
- mj('images', q: query) do |j|
35
- j.flat_map { |x| x['images'].map { |x| Image.new(x, self, client) } }
36
- end
37
- end
38
-
39
- def find_by(name:)
40
- j = Oj.load(request('images', q: { name: name }).run.body)['images']
41
- return if j.none?
42
- j.each do |x|
43
- return Image.new(x, self, client)
11
+ when Integer then find_by(id: arg)
12
+ when String then find_by(name: arg)
44
13
  end
45
14
  end
46
15
  end
data/lib/hcloud/iso.rb CHANGED
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class Iso
3
5
  Attributes = {
4
6
  id: nil,
5
7
  name: nil,
6
8
  description: nil,
7
- type: nil
9
+ type: nil,
10
+ deprecated: :time
8
11
  }.freeze
9
12
 
10
13
  include EntryLoader
@@ -1,36 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class IsoResource < AbstractResource
3
- def all
4
- mj('isos') do |j|
5
- j.flat_map { |x| x['isos'].map { |x| Iso.new(x, self, client) } }
6
- end
7
- end
5
+ filter_attributes :name
6
+
7
+ bind_to Iso
8
8
 
9
9
  def [](arg)
10
10
  case arg
11
- when Integer
12
- begin
13
- find(arg)
14
- rescue Error::NotFound
15
- end
16
- when String
17
- find_by(name: arg)
18
- end
19
- end
20
-
21
- def find(id)
22
- Iso.new(
23
- Oj.load(request("isos/#{id.to_i}").run.body)['iso'],
24
- self,
25
- client
26
- )
27
- end
28
-
29
- def find_by(name:)
30
- j = Oj.load(request('isos', q: { name: name }).run.body)['isos']
31
- return if j.none?
32
- j.each do |x|
33
- return Iso.new(x, self, client)
11
+ when Integer then find_by(id: arg)
12
+ when String then find_by(name: arg)
34
13
  end
35
14
  end
36
15
  end
@@ -1,14 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class Location
3
- Attributes = {
4
- id: nil,
5
- name: nil,
6
- description: nil,
7
- country: nil,
8
- city: nil,
9
- longitude: nil,
10
- latitude: nil
11
- }.freeze
5
+ require 'hcloud/location_resource'
12
6
 
13
7
  include EntryLoader
14
8
  end
@@ -1,36 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class LocationResource < AbstractResource
3
- def all
4
- mj('locations') do |j|
5
- j.flat_map { |x| x['locations'].map { |x| Location.new(x, self, client) } }
6
- end
7
- end
5
+ filter_attributes :name
8
6
 
9
- def find(id)
10
- Location.new(
11
- Oj.load(request("locations/#{id}").run.body)['location'],
12
- self,
13
- client
14
- )
15
- end
16
-
17
- def find_by(name:)
18
- x = Oj.load(request('locations', q: { name: name }).run.body)['locations']
19
- return nil if x.none?
20
- x.each do |s|
21
- return Location.new(s, self, client)
22
- end
23
- end
7
+ bind_to Location
24
8
 
25
9
  def [](arg)
26
10
  case arg
27
- when Integer
28
- begin
29
- find(arg)
30
- rescue Error::NotFound
31
- end
32
- when String
33
- find_by(name: arg)
11
+ when Integer then find_by(id: arg)
12
+ when String then find_by(name: arg)
34
13
  end
35
14
  end
36
15
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hcloud
4
+ class Network
5
+ include EntryLoader
6
+
7
+ schema(
8
+ created: :time
9
+ )
10
+
11
+ protectable :delete
12
+ updatable :name
13
+ destructible
14
+
15
+ has_actions
16
+
17
+ def add_subnet(type:, network_zone:, ip_range: nil)
18
+ prepare_request('actions/add_subnet', j: COLLECT_ARGS.call(__method__, binding))
19
+ end
20
+
21
+ def del_subnet(ip_range:)
22
+ prepare_request('actions/del_subnet', j: COLLECT_ARGS.call(__method__, binding))
23
+ end
24
+
25
+ def add_route(destination:, gateway:)
26
+ prepare_request('actions/add_route', j: COLLECT_ARGS.call(__method__, binding))
27
+ end
28
+
29
+ def del_route(destination:, gateway:)
30
+ prepare_request('actions/del_route', j: COLLECT_ARGS.call(__method__, binding))
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hcloud
4
+ class NetworkResource < AbstractResource
5
+ filter_attributes :name
6
+
7
+ bind_to Network
8
+
9
+ def [](arg)
10
+ case arg
11
+ when Integer then find_by(id: arg)
12
+ when String then find_by(name: arg)
13
+ end
14
+ end
15
+
16
+ def create(name:, ip_range:, subnets: nil, routes: nil)
17
+ prepare_request(
18
+ 'networks', j: COLLECT_ARGS.call(__method__, binding),
19
+ expected_code: 201
20
+ ) do |response|
21
+ Network.new(client, response.parsed_json[:network])
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,14 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class Pagination
3
- Attributes = {
4
- page: nil,
5
- per_page: nil,
6
- previous_page: nil,
7
- next_page: nil,
8
- last_page: nil,
9
- total_entries: nil
10
- }.freeze
11
-
12
5
  include EntryLoader
13
6
  end
14
7
  end