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
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