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
data/lib/hcloud/server.rb CHANGED
@@ -1,109 +1,76 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class Server
3
- Attributes = {
4
- id: nil,
5
- name: nil,
6
- status: nil,
5
+ require 'hcloud/server_resource'
6
+
7
+ include EntryLoader
8
+
9
+ schema(
7
10
  created: :time,
8
- public_net: nil,
9
11
  server_type: ServerType,
10
12
  datacenter: Datacenter,
11
- image: Image,
12
- iso: nil,
13
- rescue_enabled: nil,
14
- locked: nil,
15
- backup_window: nil,
16
- outgoing_traffic: nil,
17
- ingoing_traffic: nil,
18
- included_traffic: nil
19
- }.freeze
20
-
21
- include EntryLoader
13
+ image: Image
14
+ )
22
15
 
23
- def update(name:)
24
- Server.new(
25
- Oj.load(request(base_path, j: { name: name }, method: :put).run.body)['server'],
26
- parent,
27
- client
28
- )
29
- end
16
+ protectable :delete, :rebuild
17
+ updatable :name
18
+ destructible
30
19
 
31
- def destroy
32
- action(request(base_path, method: :delete))[0]
33
- end
20
+ has_actions
34
21
 
35
22
  def enable_rescue(type: 'linux64', ssh_keys: [])
36
- query = {}
37
- method(:enable_rescue).parameters.inject(query) do |r, x|
38
- (var = eval(x.last.to_s)).nil? ? r : r.merge!(x.last => var)
39
- end
40
- a, j = action(request(base_path('actions/enable_rescue'), j: query))
41
- [a, j['root_password']]
23
+ query = COLLECT_ARGS.call(__method__, binding)
24
+ prepare_request('actions/enable_rescue', j: query) { |j| j[:root_password] }
42
25
  end
43
26
 
44
27
  def reset_password
45
- a, j = action(request(base_path('actions/reset_password'), method: :post))
46
- [a, j['root_password']]
28
+ prepare_request('actions/reset_password', method: :post) { |j| j[:root_password] }
47
29
  end
48
30
 
49
31
  def create_image(description: nil, type: nil)
50
- query = {}
51
- method(:create_image).parameters.inject(query) do |r, x|
52
- (var = eval(x.last.to_s)).nil? ? r : r.merge!(x.last => var)
53
- end
54
- a, j = action(request(base_path('actions/create_image'), j: query))
55
- [a, Image.new(j['image'], parent, client)]
32
+ query = COLLECT_ARGS.call(__method__, binding)
33
+ prepare_request('actions/create_image', j: query) { |j| Image.new(client, j[:image]) }
56
34
  end
57
35
 
58
36
  def rebuild(image:)
59
- a, j = action(request(base_path('actions/rebuild'), j: { image: image }))
60
- [a, j['root_password']]
37
+ prepare_request('actions/rebuild', j: { image: image }) { |j| j[:root_password] }
61
38
  end
62
39
 
63
40
  def change_type(server_type:, upgrade_disk: nil)
64
- query = {}
65
- method(:change_type).parameters.inject(query) do |r, x|
66
- (var = eval(x.last.to_s)).nil? ? r : r.merge!(x.last => var)
67
- end
68
- action(request(base_path('actions/change_type'), j: query))[0]
41
+ prepare_request('actions/change_type', j: COLLECT_ARGS.call(__method__, binding))
69
42
  end
70
43
 
71
- def enable_backup(backup_window:)
72
- action(request(base_path('actions/enable_backup'),
73
- j: { backup_window: backup_window }))[0]
44
+ # Specifying a backup window is not supported anymore. We keep this method
45
+ # to ensure backwards compatibility, but ignore the argument if provided.
46
+ def enable_backup(**_kwargs)
47
+ prepare_request('actions/enable_backup', method: :post)
74
48
  end
75
49
 
76
50
  def attach_iso(iso:)
77
- action(request(base_path('actions/attach_iso'),
78
- j: { iso: iso }))[0]
51
+ prepare_request('actions/attach_iso', j: { iso: iso })
52
+ end
53
+
54
+ def attach_to_network(network:, ip: nil, alias_ips: nil)
55
+ prepare_request('actions/attach_to_network', j: COLLECT_ARGS.call(__method__, binding))
56
+ end
57
+
58
+ def detach_from_network(network:)
59
+ prepare_request('actions/detach_from_network', j: { network: network })
79
60
  end
80
61
 
81
62
  %w[
82
63
  poweron poweroff shutdown reboot reset
83
64
  disable_rescue disable_backup detach_iso
65
+ request_console
84
66
  ].each do |action|
85
67
  define_method(action) do
86
- action(request(base_path("actions/#{action}"), method: :post))[0]
68
+ prepare_request("actions/#{action}", method: :post)
87
69
  end
88
70
  end
89
71
 
90
- def actions
91
- ActionResource.new(client: client, parent: self, base_path: base_path)
92
- end
93
-
94
- private
95
-
96
- def action(request)
97
- j = Oj.load(request.run.body)
98
- [
99
- Action.new(j['action'], parent, client),
100
- j
101
- ]
102
- end
103
-
104
- def base_path(ext = nil)
105
- return ["servers/#{id}", ext].compact.join('/') unless id.nil?
106
- raise ResourcePathError, 'Unable to build resource path. Id is nil.'
72
+ def request_console
73
+ prepare_request('actions/request_console', method: :post) { |j| [j[:wss_url], j[:password]] }
107
74
  end
108
75
  end
109
76
  end
@@ -1,54 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class ServerResource < AbstractResource
5
+ filter_attributes :status, :name
6
+
7
+ bind_to Server
8
+
3
9
  def create(name:,
4
10
  server_type:,
5
- datacenter: nil,
11
+ image:, datacenter: nil,
6
12
  location: nil,
7
13
  start_after_create: nil,
8
- image:,
9
14
  ssh_keys: [],
15
+ networks: [],
10
16
  user_data: nil)
11
- query = {}
12
- method(:create).parameters.inject(query) do |r, x|
13
- (var = eval(x.last.to_s)).nil? ? r : r.merge!(x.last => var)
14
- end
15
- j = Oj.load(request('servers', j: query, code: 200).run.body)
16
- [
17
- Action.new(j['action'], self, client),
18
- Server.new(j['server'], self, client),
19
- j['root_password']
20
- ]
21
- end
22
-
23
- def all
24
- mj('servers') do |j|
25
- j.flat_map { |x| x['servers'].map { |x| Server.new(x, self, client) } }
17
+ prepare_request('servers', j: COLLECT_ARGS.call(__method__, binding),
18
+ expected_code: 201) do |response|
19
+ [
20
+ Action.new(client, response.parsed_json[:action]),
21
+ Server.new(client, response.parsed_json[:server]),
22
+ response.parsed_json[:root_password]
23
+ ]
26
24
  end
27
25
  end
28
26
 
29
- def find(id)
30
- Server.new(
31
- Oj.load(request("servers/#{id.to_i}").run.body)['server'], self, client
32
- )
33
- end
34
-
35
27
  def [](arg)
36
28
  case arg
37
- when Integer
38
- begin
39
- find(arg)
40
- rescue Error::NotFound
41
- end
42
- when String
43
- find_by(name: arg)
44
- end
45
- end
46
-
47
- def find_by(name:)
48
- x = Oj.load(request('servers', q: { name: name }).run.body)['servers']
49
- return nil if x.none?
50
- x.each do |s|
51
- return Server.new(s, self, client)
29
+ when Integer then find_by(id: arg)
30
+ when String then find_by(name: arg)
52
31
  end
53
32
  end
54
33
  end
@@ -1,15 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class ServerType
3
- Attributes = {
4
- id: nil,
5
- name: nil,
6
- description: nil,
7
- cores: nil,
8
- memory: nil,
9
- disk: nil,
10
- prices: nil,
11
- storage_type: nil
12
- }.freeze
5
+ require 'hcloud/server_type_resource'
13
6
 
14
7
  include EntryLoader
15
8
  end
@@ -1,37 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class ServerTypeResource < AbstractResource
3
- include Enumerable
4
-
5
- def all
6
- j = Oj.load(request('server_types').run.body)
7
- j['server_types'].map { |x| ServerType.new(x, self, client) }
8
- end
5
+ filter_attributes :name
9
6
 
10
- def find(id)
11
- ServerType.new(
12
- Oj.load(request("server_types/#{id}").run.body)['server_type'],
13
- self,
14
- client
15
- )
16
- end
17
-
18
- def find_by(name:)
19
- x = Oj.load(request('server_types', q: { name: name }).run.body)['server_types']
20
- return nil if x.none?
21
- x.each do |s|
22
- return ServerType.new(s, self, client)
23
- end
24
- end
7
+ bind_to ServerType
25
8
 
26
9
  def [](arg)
27
10
  case arg
28
- when Integer
29
- begin
30
- find(arg)
31
- rescue Error::NotFound
32
- end
33
- when String
34
- find_by(name: arg)
11
+ when Integer then find_by(id: arg)
12
+ when String then find_by(name: arg)
35
13
  end
36
14
  end
37
15
  end
@@ -1,23 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class SSHKey
3
- Attributes = {
4
- id: nil,
5
- name: nil,
6
- fingerprint: nil,
7
- public_key: nil
8
- }.freeze
9
- include EntryLoader
5
+ require 'hcloud/ssh_key_resource'
10
6
 
11
- def update(name:)
12
- j = Oj.load(request("ssh_keys/#{id.to_i}",
13
- j: { name: name },
14
- method: :put).run.body)
15
- SSHKey.new(j['ssh_key'], self, client)
16
- end
7
+ include EntryLoader
17
8
 
18
- def destroy
19
- request("ssh_keys/#{id}", method: :delete).run.body
20
- true
21
- end
9
+ updatable :name
10
+ destructible
22
11
  end
23
12
  end
@@ -1,41 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
4
  class SSHKeyResource < AbstractResource
3
- def all
4
- mj('ssh_keys') do |j|
5
- j.flat_map { |x| x['ssh_keys'].map { |x| SSHKey.new(x, self, client) } }
6
- end
7
- end
5
+ filter_attributes :name
8
6
 
9
- def create(name:, public_key:)
10
- j = Oj.load(request('ssh_keys', j: { name: name, public_key: public_key }).run.body)
11
- SSHKey.new(j['ssh_key'], self, client)
12
- end
13
-
14
- def find(id)
15
- SSHKey.new(
16
- Oj.load(request("ssh_keys/#{id}").run.body)['ssh_key'],
17
- self,
18
- client
19
- )
20
- end
21
-
22
- def find_by(name:)
23
- x = Oj.load(request('ssh_keys', q: { name: name }).run.body)['ssh_keys']
24
- return nil if x.none?
25
- x.each do |s|
26
- return SSHKey.new(s, self, client)
7
+ def [](arg)
8
+ case arg
9
+ when Integer then find_by(id: arg)
10
+ when String then find_by(name: arg)
27
11
  end
28
12
  end
29
13
 
30
- def [](arg)
31
- case arg
32
- when Integer
33
- begin
34
- find(arg)
35
- rescue Error::NotFound
36
- end
37
- when String
38
- find_by(name: arg)
14
+ def create(name:, public_key:)
15
+ prepare_request(
16
+ 'ssh_keys', j: COLLECT_ARGS.call(__method__, binding),
17
+ expected_code: 201
18
+ ) do |response|
19
+ SSHKey.new(client, response.parsed_json[:ssh_key])
39
20
  end
40
21
  end
41
22
  end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+
5
+ module Hcloud
6
+ module TyphoeusExt
7
+ Context = Struct.new(:client)
8
+
9
+ ATTRIBUTES = %i[
10
+ block autoload_action resource_path
11
+ resource_class expected_code
12
+ ].freeze
13
+
14
+ attr_accessor(*ATTRIBUTES)
15
+
16
+ def self.collect_attributes(kwargs)
17
+ hash = {}
18
+ ATTRIBUTES.each do |key|
19
+ hash[key] = kwargs.delete(key) if kwargs.key?(key)
20
+ end
21
+ hash
22
+ end
23
+
24
+ def attributes=(kwargs)
25
+ kwargs.each do |key, value|
26
+ public_send("#{key}=", value)
27
+ end
28
+ end
29
+
30
+ def parsed_json
31
+ return {} if code == 204
32
+
33
+ @parsed_json ||= Oj.load(body, symbol_keys: true).tap do |json|
34
+ next unless request.hydra
35
+
36
+ check_for_error(
37
+ e_code: json.to_h.dig(:error, :code),
38
+ e_message: json.to_h.dig(:error, :message)
39
+ )
40
+ end
41
+ rescue StandardError
42
+ raise Error::UnexpectedError, "unable to load body: #{body}"
43
+ end
44
+
45
+ def context
46
+ @context ||= Context.new
47
+ end
48
+
49
+ def check_for_error(e_code: nil, e_message: nil)
50
+ case code
51
+ when 401 then raise(Error::Unauthorized)
52
+ when 0 then raise(Error::ServerError, "Connection error: #{return_code}")
53
+ when 400...600
54
+ raise _error_class(e_code || error_code), e_message || error_message
55
+ end
56
+
57
+ raise Error::UnexpectedError, body if expected_code && expected_code != code
58
+ end
59
+
60
+ def pagination
61
+ @pagination ||= Pagination.new(parsed_json[:meta].to_h[:pagination])
62
+ end
63
+
64
+ def resource_attributes
65
+ _resource = [@resource_path].flatten.compact.map(&:to_s).map(&:to_sym)
66
+ return parsed_json if _resource.empty?
67
+
68
+ parsed_json.dig(*_resource)
69
+ end
70
+
71
+ def [](arg)
72
+ parsed_json[arg]
73
+ end
74
+
75
+ def resource
76
+ action = parsed_json[:action] if autoload_action
77
+ return [Action.new(self, action), block.call(self)].flatten(1) if block && action
78
+ return block.call(self) if block
79
+
80
+ @resource_class.from_response(self, autoload_action: autoload_action)
81
+ end
82
+
83
+ def error_code
84
+ error[:code]
85
+ end
86
+
87
+ def error_message
88
+ error[:message]
89
+ end
90
+
91
+ def error
92
+ parsed_json[:error].to_h
93
+ end
94
+
95
+ def _error_class(code)
96
+ case code
97
+ when 'invalid_input' then Error::InvalidInput
98
+ when 'forbidden' then Error::Forbidden
99
+ when 'locked' then Error::Locked
100
+ when 'not_found' then Error::NotFound
101
+ when 'rate_limit_exceeded' then Error::RateLimitExceeded
102
+ when 'resource_unavailable' then Error::ResourceUnavailable
103
+ when 'service_error' then Error::ServiceError
104
+ when 'uniqueness_error' then Error::UniquenessError
105
+ else
106
+ Error::ServerError
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hcloud
2
- VERSION = '0.1.2'.freeze
4
+ VERSION = '1.0.3'
3
5
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hcloud
4
+ class Volume
5
+ require 'hcloud/volume_resource'
6
+
7
+ include EntryLoader
8
+
9
+ schema(
10
+ created: :time,
11
+ location: Location
12
+ )
13
+
14
+ protectable :delete
15
+ updatable :name
16
+ destructible
17
+
18
+ has_actions
19
+
20
+ def attach(server:, automount:)
21
+ prepare_request('actions/attach', j: COLLECT_ARGS.call(__method__, binding))
22
+ end
23
+
24
+ def detach
25
+ prepare_request('actions/detach', method: :post)
26
+ end
27
+
28
+ def resize(size:)
29
+ prepare_request('actions/resize', j: COLLECT_ARGS.call(__method__, binding))
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hcloud
4
+ class VolumeResource < AbstractResource
5
+ filter_attributes :name
6
+
7
+ def [](arg)
8
+ case arg
9
+ when Integer then find_by(id: arg)
10
+ when String then find_by(name: arg)
11
+ end
12
+ end
13
+
14
+ def create(size:, name:, automount: nil, format: nil, location: nil, server: nil)
15
+ prepare_request(
16
+ 'volumes', j: COLLECT_ARGS.call(__method__, binding),
17
+ expected_code: 201
18
+ ) do |response|
19
+ [
20
+ Action.new(client, response.parsed_json[:action]),
21
+ Volume.new(client, response.parsed_json[:volume]),
22
+ response.parsed_json[:next_actions].map do |action|
23
+ Action.new(client, action)
24
+ end
25
+ ]
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/hcloud.rb CHANGED
@@ -1,29 +1,55 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hcloud/version'
2
4
  require 'active_support/core_ext/object/to_query'
5
+ require 'active_support/core_ext/hash/indifferent_access'
3
6
 
4
7
  module Hcloud
5
8
  autoload :Error, 'hcloud/errors'
6
9
  autoload :Client, 'hcloud/client'
10
+ autoload :TyphoeusExt, 'hcloud/typhoeus_ext'
7
11
  autoload :AbstractResource, 'hcloud/abstract_resource'
8
- autoload :MultiReply, 'hcloud/multi_reply'
9
- autoload :ServerResource, 'hcloud/server_resource'
10
12
  autoload :EntryLoader, 'hcloud/entry_loader'
13
+
14
+ autoload :Server, 'hcloud/server'
15
+ autoload :ServerResource, 'hcloud/server_resource'
16
+
17
+ autoload :ServerType, 'hcloud/server_type'
18
+ autoload :ServerTypeResource, 'hcloud/server_type_resource'
19
+
11
20
  autoload :FloatingIP, 'hcloud/floating_ip'
12
21
  autoload :FloatingIPResource, 'hcloud/floating_ip_resource'
22
+
13
23
  autoload :SSHKey, 'hcloud/ssh_key'
14
24
  autoload :SSHKeyResource, 'hcloud/ssh_key_resource'
15
- autoload :Server, 'hcloud/server'
16
- autoload :ServerType, 'hcloud/server_type'
17
- autoload :ServerTypeResource, 'hcloud/server_type_resource'
25
+
18
26
  autoload :Datacenter, 'hcloud/datacenter'
19
27
  autoload :DatacenterResource, 'hcloud/datacenter_resource'
28
+
20
29
  autoload :Location, 'hcloud/location'
21
30
  autoload :LocationResource, 'hcloud/location_resource'
31
+
22
32
  autoload :Image, 'hcloud/image'
23
33
  autoload :ImageResource, 'hcloud/image_resource'
34
+
35
+ autoload :Network, 'hcloud/network'
36
+ autoload :NetworkResource, 'hcloud/network_resource'
37
+
38
+ autoload :Volume, 'hcloud/volume'
39
+ autoload :VolumeResource, 'hcloud/volume_resource'
40
+
24
41
  autoload :Action, 'hcloud/action'
25
42
  autoload :ActionResource, 'hcloud/action_resource'
43
+
26
44
  autoload :Iso, 'hcloud/iso'
27
45
  autoload :IsoResource, 'hcloud/iso_resource'
46
+
28
47
  autoload :Pagination, 'hcloud/pagination'
48
+
49
+ COLLECT_ARGS = proc do |method_name, bind|
50
+ query = bind.receiver.method(method_name).parameters.inject({}) do |hash, (_type, name)|
51
+ hash.merge(name => bind.local_variable_get(name))
52
+ end
53
+ query.delete_if { |_, v| v.nil? }
54
+ end
29
55
  end