hcloud 0.1.2 → 1.0.0

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 (44) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -1
  3. data/.rubocop.yml +24 -9
  4. data/.rubocop_todo.yml +18 -121
  5. data/.travis.yml +3 -3
  6. data/CHANGELOG.md +13 -0
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +143 -0
  9. data/README.md +34 -1
  10. data/Rakefile +2 -0
  11. data/bin/console +1 -0
  12. data/hcloud.gemspec +5 -3
  13. data/lib/hcloud.rb +31 -5
  14. data/lib/hcloud/abstract_resource.rb +162 -55
  15. data/lib/hcloud/action.rb +8 -10
  16. data/lib/hcloud/action_resource.rb +3 -29
  17. data/lib/hcloud/client.rb +66 -28
  18. data/lib/hcloud/datacenter.rb +7 -7
  19. data/lib/hcloud/datacenter_resource.rb +6 -31
  20. data/lib/hcloud/entry_loader.rb +181 -20
  21. data/lib/hcloud/errors.rb +2 -0
  22. data/lib/hcloud/floating_ip.rb +18 -29
  23. data/lib/hcloud/floating_ip_resource.rb +15 -30
  24. data/lib/hcloud/image.rb +12 -32
  25. data/lib/hcloud/image_resource.rb +7 -38
  26. data/lib/hcloud/iso.rb +4 -1
  27. data/lib/hcloud/iso_resource.rb +7 -28
  28. data/lib/hcloud/location.rb +3 -9
  29. data/lib/hcloud/location_resource.rb +6 -27
  30. data/lib/hcloud/network.rb +33 -0
  31. data/lib/hcloud/network_resource.rb +25 -0
  32. data/lib/hcloud/pagination.rb +2 -9
  33. data/lib/hcloud/server.rb +37 -70
  34. data/lib/hcloud/server_resource.rb +16 -36
  35. data/lib/hcloud/server_type.rb +3 -10
  36. data/lib/hcloud/server_type_resource.rb +6 -28
  37. data/lib/hcloud/ssh_key.rb +6 -17
  38. data/lib/hcloud/ssh_key_resource.rb +13 -32
  39. data/lib/hcloud/typhoeus_ext.rb +112 -0
  40. data/lib/hcloud/version.rb +3 -1
  41. data/lib/hcloud/volume.rb +32 -0
  42. data/lib/hcloud/volume_resource.rb +29 -0
  43. metadata +31 -13
  44. data/lib/hcloud/multi_reply.rb +0 -21
@@ -1,13 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class Datacenter
3
- Attributes = {
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
- include Enumerable
5
+ filter_attributes :name
4
6
 
5
- def all
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
- begin
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
@@ -1,36 +1,197 @@
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.keys.each 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
+ @client = client
99
+ _load(kwargs)
100
+ end
101
+
102
+ def inspect
103
+ "#<#{self.class.name}:0x#{__id__.to_s(16)} #{_attributes.inspect}>"
104
+ end
105
+
106
+ def client
107
+ @client || response&.context&.client
108
+ end
109
+
110
+ def resource_url
111
+ if block = self.class.resource_url
112
+ return instance_exec(&block)
113
+ end
114
+
115
+ [self.class.resource_class.name.demodulize.tableize, id].compact.join('/')
116
+ end
117
+
118
+ def resource_path
119
+ self.class.resource_class.name.demodulize.underscore
120
+ end
121
+
122
+ def prepare_request(url_suffix = nil, **kwargs, &block)
123
+ kwargs[:resource_path] ||= resource_path
124
+ kwargs[:resource_class] ||= self.class
125
+ kwargs[:autoload_action] = true unless kwargs.key?(:autoload_action)
126
+
127
+ client.prepare_request(
128
+ [resource_url, url_suffix].compact.join('/'),
129
+ **kwargs,
130
+ &block
131
+ )
132
+ end
133
+
134
+ def _attributes
135
+ @_attributes ||= {}.with_indifferent_access
136
+ end
137
+
138
+ def method_missing(method, *args, &block)
139
+ _attributes.key?(method) ? _attributes[method] : super
140
+ end
141
+
142
+ def respond_to_missing?(method, *args, &block)
143
+ _attributes.key?(method) || super
144
+ end
145
+
146
+ def update(**kwargs)
147
+ context = self
148
+ _run_callbacks(:before)
149
+ prepare_request(j: kwargs, method: :put) do |response|
150
+ response.resource_class.from_response(
151
+ response,
152
+ autoload_action: response.autoload_action
153
+ ).tap do |*_args|
154
+ _run_callbacks(:after)
155
+ context.send(:changes_applied)
28
156
  end
29
157
  end
30
158
  end
31
159
 
32
- def request(*args)
33
- client.request(*args)
160
+ def save
161
+ update(changes.map { |key, _value| [key.to_sym, _attributes[key]] }.to_h)
162
+ end
163
+
164
+ def rollback
165
+ restore_attributes
166
+ end
167
+
168
+ def _run_callbacks(order)
169
+ self.class._callbacks[order].each { |block| instance_exec(&block) }
170
+ end
171
+
172
+ def _update_attribute(key, value)
173
+ _attributes[key] = value
174
+ instance_variable_set("@#{key}", value)
175
+ end
176
+
177
+ def _load(resource)
178
+ @_attributes = {}.with_indifferent_access
179
+
180
+ resource.each do |key, value|
181
+ definition = self.class.schema[key]
182
+
183
+ if definition == :time
184
+ _update_attribute(key, value ? Time.parse(value) : nil)
185
+ next
186
+ end
187
+
188
+ if definition.is_a?(Class) && definition.include?(EntryLoader)
189
+ _update_attribute(key, value ? definition.new(client, value) : nil)
190
+ next
191
+ end
192
+
193
+ _update_attribute(key, value.is_a?(Hash) ? value.with_indifferent_access : value)
194
+ end
34
195
  end
35
196
  end
36
197
  end
@@ -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
@@ -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