hcloud 0.1.2 → 1.0.3

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