hetznercloud 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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile +6 -0
  4. data/LICENSE.md +21 -0
  5. data/README.md +123 -0
  6. data/config/inflections.rb +19 -0
  7. data/lib/core_ext/send_wrap.rb +15 -0
  8. data/lib/hcloud/action_collection.rb +37 -0
  9. data/lib/hcloud/client.rb +38 -0
  10. data/lib/hcloud/collection.rb +77 -0
  11. data/lib/hcloud/concerns/actionable.rb +29 -0
  12. data/lib/hcloud/concerns/concerns.rb +29 -0
  13. data/lib/hcloud/concerns/creatable.rb +46 -0
  14. data/lib/hcloud/concerns/deletable.rb +20 -0
  15. data/lib/hcloud/concerns/queryable.rb +41 -0
  16. data/lib/hcloud/concerns/updatable.rb +24 -0
  17. data/lib/hcloud/entities/amount.rb +8 -0
  18. data/lib/hcloud/entities/applied_to.rb +7 -0
  19. data/lib/hcloud/entities/applied_to_resource.rb +9 -0
  20. data/lib/hcloud/entities/apply_to.rb +15 -0
  21. data/lib/hcloud/entities/datacenter_server_type.rb +9 -0
  22. data/lib/hcloud/entities/dns_pointer.rb +8 -0
  23. data/lib/hcloud/entities/error.rb +8 -0
  24. data/lib/hcloud/entities/metrics.rb +12 -0
  25. data/lib/hcloud/entities/price.rb +9 -0
  26. data/lib/hcloud/entities/protection.rb +11 -0
  27. data/lib/hcloud/entities/rule.rb +15 -0
  28. data/lib/hcloud/entity.rb +23 -0
  29. data/lib/hcloud/errors.rb +36 -0
  30. data/lib/hcloud/http.rb +100 -0
  31. data/lib/hcloud/resource.rb +42 -0
  32. data/lib/hcloud/resource_type.rb +73 -0
  33. data/lib/hcloud/resources/action.rb +49 -0
  34. data/lib/hcloud/resources/datacenter.rb +43 -0
  35. data/lib/hcloud/resources/firewall.rb +80 -0
  36. data/lib/hcloud/resources/floating_ip.rb +144 -0
  37. data/lib/hcloud/resources/image.rb +140 -0
  38. data/lib/hcloud/resources/iso.rb +36 -0
  39. data/lib/hcloud/resources/location.rb +37 -0
  40. data/lib/hcloud/resources/placement_group.rb +75 -0
  41. data/lib/hcloud/resources/server.rb +179 -0
  42. data/lib/hcloud/resources/server_type.rb +42 -0
  43. data/lib/hcloud/resources/ssh_key.rb +77 -0
  44. data/lib/hcloud/resources/volume.rb +147 -0
  45. data/lib/hcloud/version.rb +17 -0
  46. data/lib/hcloud.rb +42 -0
  47. metadata +286 -0
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HCloud
4
+ class Entity
5
+ include ActiveModel::Attributes
6
+ include ActiveModel::AttributeAssignment
7
+
8
+ def initialize(attributes = {})
9
+ super()
10
+
11
+ assign_attributes(attributes) if attributes
12
+ end
13
+
14
+ def inspect
15
+ "#<#{self.class} #{attributes.filter_map { |name, value| "#{name}: #{value.inspect}" }.join(', ')}>"
16
+ end
17
+
18
+ def to_h
19
+ attributes
20
+ .transform_values { |v| v.try(:to_h) || v }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HCloud
4
+ module Errors
5
+ class ActionFailed < Error; end
6
+ class Conflict < Error; end
7
+ class Error < StandardError; end
8
+ class FirewallResourceNotFound < Error; end
9
+ class Forbidden < Error; end
10
+ class IPNotAvailable < Error; end
11
+ class IncompatibleNetworkType < Error; end
12
+ class InvalidInput < Error; end
13
+ class JSONError < Error; end
14
+ class Locked < Error; end
15
+ class Maintenance < Error; end
16
+ class NetworksOverlap < Error; end
17
+ class NoSpaceLeftInLocation < Error; end
18
+ class NoSubnetAvailable < Error; end
19
+ class NotFound < Error; end
20
+ class PlacementError < Error; end
21
+ class PlacementError < Error; end
22
+ class Protected < Error; end
23
+ class RateLimitExceeded < Error; end
24
+ class ResourceLimitExceeded < Error; end
25
+ class ResourceUnavailable < Error; end
26
+ class ResourceInUse < Error; end
27
+ class ServerAlreadyAdded < Error; end
28
+ class ServerAlreadyAttached < Error; end
29
+ class ServerError < Error; end
30
+ class ServiceError < Error; end
31
+ class TokenReadonly < Error; end
32
+ class Unauthorized < Error; end
33
+ class UniquenessError < Error; end
34
+ class UnsupportedError < Error; end
35
+ end
36
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http"
4
+
5
+ module HCloud
6
+ class HTTP
7
+ attr_reader :access_token, :endpoint, :logger, :timeout
8
+
9
+ def initialize(access_token, endpoint, logger, timeout = 10)
10
+ @access_token = access_token
11
+ @endpoint = endpoint
12
+ @logger = logger
13
+ @timeout = timeout
14
+ end
15
+
16
+ def get(path, params = {})
17
+ response = http
18
+ .get(url_for(path), params: transform_params(params))
19
+
20
+ data = response
21
+ .parse(:json)
22
+ .deep_symbolize_keys
23
+
24
+ return data if response.status.success?
25
+
26
+ raise Errors.const_get(data.dig(:error, :code).camelize), data.dig(:error, :message)
27
+ end
28
+
29
+ def put(path, body = {})
30
+ response = http
31
+ .put(url_for(path), json: body)
32
+
33
+ data = response
34
+ .parse(:json)
35
+ .deep_symbolize_keys
36
+
37
+ return data if response.status.success?
38
+
39
+ raise Errors.const_get(data.dig(:error, :code).camelize), data.dig(:error, :message)
40
+ end
41
+
42
+ def post(path, body = {})
43
+ response = http
44
+ .post(url_for(path), json: body)
45
+
46
+ data = response
47
+ .parse(:json)
48
+ .deep_symbolize_keys
49
+
50
+ return data if response.status.success?
51
+
52
+ raise Errors.const_get(data.dig(:error, :code).camelize), data.dig(:error, :message)
53
+ end
54
+
55
+ def delete(path)
56
+ response = http
57
+ .delete(url_for(path))
58
+
59
+ return if response.status.success?
60
+
61
+ data = response
62
+ .parse(:json)
63
+ .deep_symbolize_keys
64
+
65
+ raise Errors.const_get(data.dig(:error, :code).camelize), data.dig(:error, :message)
66
+ end
67
+
68
+ private
69
+
70
+ def http
71
+ @http ||= ::HTTP
72
+ .headers(accept: "application/json", user_agent: "#{HCloud::NAME}/#{HCloud::VERSION}")
73
+ .timeout(timeout)
74
+ .use(logging: { logger: logger })
75
+ .encoding("utf-8")
76
+ .auth("Bearer #{access_token}")
77
+ end
78
+
79
+ def url_for(path)
80
+ "#{endpoint}#{path}"
81
+ end
82
+
83
+ def transform_params(params)
84
+ params
85
+ .transform_values do |value|
86
+ # Don't transform if value is single argument: { sort: :id }
87
+ next value unless value.respond_to?(:each)
88
+
89
+ value.map do |element|
90
+ # Don't transform if element is single argument: { sort: [:id] }
91
+ next element unless element.respond_to?(:each)
92
+
93
+ # Join elements with : { sort: [id: :asc] }
94
+ element.to_a.join(":")
95
+ end
96
+ end
97
+ .compact
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HCloud
4
+ class Resource
5
+ include ActiveModel::Attributes
6
+ include ActiveModel::AttributeAssignment
7
+
8
+ include Concerns
9
+
10
+ def initialize(attributes = {})
11
+ super()
12
+
13
+ assign_attributes(attributes) if attributes
14
+ end
15
+
16
+ delegate :[], to: :attributes
17
+
18
+ def inspect
19
+ "#<#{self.class} #{attributes.filter_map { |name, value| "#{name}: #{value.inspect}" }.join(', ')}>"
20
+ end
21
+
22
+ def to_h
23
+ {
24
+ id: id,
25
+ }
26
+ end
27
+
28
+ def ==(other)
29
+ id && id == other.id
30
+ end
31
+
32
+ def self.resource_name
33
+ name.demodulize.underscore
34
+ end
35
+
36
+ def self.client
37
+ HCloud::Client.connection
38
+ end
39
+
40
+ delegate :client, :resource_name, to: :class
41
+ end
42
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HCloud
4
+ class ResourceType
5
+ class_attribute :resource_class_name
6
+
7
+ attr_reader :array
8
+
9
+ def initialize(array: false)
10
+ @array = array
11
+ end
12
+
13
+ # rubocop:disable Metrics/CyclomaticComplexity
14
+ def cast(value)
15
+ case value
16
+ when nil, []
17
+ array? ? [] : nil
18
+ when resource_class # Class
19
+ value
20
+ when Integer # ID
21
+ resource_class.new(id: value)
22
+ when String # Name
23
+ resource_class.new(name: value)
24
+ when Hash # Attribute hash
25
+ resource_class.new(value)
26
+ when Array # List
27
+ value.map { |v| cast(v) }
28
+ else
29
+ raise ArgumentError, "cannot cast value: #{value} for type #{resource_class_name}"
30
+ end
31
+ end
32
+ # rubocop:enable Metrics/CyclomaticComplexity
33
+
34
+ def resource_class
35
+ @resource_class ||= resource_class_name.constantize
36
+ end
37
+
38
+ alias array? array
39
+
40
+ def assert_valid_value(_); end
41
+
42
+ # rubocop:disable Naming/MethodName
43
+ def self.Type(class_name)
44
+ Class
45
+ .new(ResourceType) { self.resource_class_name = class_name }
46
+ .tap { |klass| HCloud.const_set(:"#{class_name.demodulize}ResourceType", klass) }
47
+ end
48
+ # rubocop:enable Naming/MethodName
49
+ end
50
+ end
51
+
52
+ ActiveModel::Type.register(:action, HCloud::ResourceType.Type("HCloud::Action"))
53
+ ActiveModel::Type.register(:amount, HCloud::ResourceType.Type("HCloud::Amount"))
54
+ ActiveModel::Type.register(:applied_to, HCloud::ResourceType.Type("HCloud::AppliedTo"))
55
+ ActiveModel::Type.register(:applied_to_resource, HCloud::ResourceType.Type("HCloud::AppliedToResource"))
56
+ ActiveModel::Type.register(:apply_to, HCloud::ResourceType.Type("HCloud::ApplyTo"))
57
+ ActiveModel::Type.register(:datacenter, HCloud::ResourceType.Type("HCloud::Datacenter"))
58
+ ActiveModel::Type.register(:datacenter_server_type, HCloud::ResourceType.Type("HCloud::DatacenterServerType"))
59
+ ActiveModel::Type.register(:dns_pointer, HCloud::ResourceType.Type("HCloud::DNSPointer"))
60
+ ActiveModel::Type.register(:error, HCloud::ResourceType.Type("HCloud::Error"))
61
+ ActiveModel::Type.register(:firewall, HCloud::ResourceType.Type("HCloud::Firewall"))
62
+ ActiveModel::Type.register(:floating_ip, HCloud::ResourceType.Type("HCloud::FloatingIP"))
63
+ ActiveModel::Type.register(:image, HCloud::ResourceType.Type("HCloud::Image"))
64
+ ActiveModel::Type.register(:iso, HCloud::ResourceType.Type("HCloud::ISO"))
65
+ ActiveModel::Type.register(:location, HCloud::ResourceType.Type("HCloud::Location"))
66
+ ActiveModel::Type.register(:placement_group, HCloud::ResourceType.Type("HCloud::PlacementGroup"))
67
+ ActiveModel::Type.register(:price, HCloud::ResourceType.Type("HCloud::Price"))
68
+ ActiveModel::Type.register(:protection, HCloud::ResourceType.Type("HCloud::Protection"))
69
+ ActiveModel::Type.register(:rule, HCloud::ResourceType.Type("HCloud::Rule"))
70
+ ActiveModel::Type.register(:server, HCloud::ResourceType.Type("HCloud::Server"))
71
+ ActiveModel::Type.register(:server_type, HCloud::ResourceType.Type("HCloud::ServerType"))
72
+ ActiveModel::Type.register(:ssh_key, HCloud::ResourceType.Type("HCloud::SSHKey"))
73
+ ActiveModel::Type.register(:volume, HCloud::ResourceType.Type("HCloud::Volume"))
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HCloud
4
+ ##
5
+ # Represents an action
6
+ #
7
+ # == List all actions
8
+ #
9
+ # HCloud::Action.all
10
+ # # => [#<HCloud::Action id: 1, ...>, ...]
11
+ #
12
+ # == Sort actions
13
+ #
14
+ # HCloud::Action.all.sort(command: :desc)
15
+ # # => [#<HCloud::Action id: 1, ...>, ...]
16
+ #
17
+ # HCloud::Action.all.sort(:id, command: :asc)
18
+ # # => [#<HCloud::Action id: 1, ...>, ...]
19
+ #
20
+ # == Search actions
21
+ #
22
+ # HCloud::Action.all.where(command: "my_action")
23
+ # # => #<HCloud::Action id: 1, ...>
24
+ #
25
+ # HCloud::Action.all.where(status: "success")
26
+ # # => #<HCloud::Action id: 1, ...>
27
+ #
28
+ # == Find action by ID
29
+ #
30
+ # HCloud::Action.find(1)
31
+ # # => #<HCloud::Action id: 1, ...>
32
+ #
33
+ class Action < Resource
34
+ queryable
35
+
36
+ attribute :id, :integer
37
+ attribute :command
38
+
39
+ attribute :started, :datetime
40
+ attribute :finished, :datetime
41
+ attribute :progress, :integer
42
+
43
+ attribute :status
44
+ attribute :error, :error
45
+
46
+ # TODO: return array of resources
47
+ attribute :resources, array: true, default: -> { [] }
48
+ end
49
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HCloud
4
+ ##
5
+ # Represents a datacenter
6
+ #
7
+ # == List all datacenters
8
+ #
9
+ # HCloud::Datacenter.all
10
+ # # => [#<HCloud::Datacenter id: 2, ...>, ...]
11
+ #
12
+ # == Search datacenters
13
+ #
14
+ # HCloud::Datacenter.all.where(name: "fsn1-dc8")
15
+ # # => #<HCloud::Datacenter id: 2, ...>
16
+ #
17
+ # == Find datacenter by ID
18
+ #
19
+ # HCloud::Datacenter.find(2)
20
+ # # => #<HCloud::Datacenter id: 2, ...>
21
+ #
22
+ # == Get datacenter recommendation
23
+ #
24
+ # HCloud::Datacenter.recommendation
25
+ # # => #<HCloud::Datacenter id: 2, ...>
26
+ #
27
+ class Datacenter < Resource
28
+ queryable
29
+
30
+ attribute :id, :integer
31
+ attribute :name
32
+ attribute :description
33
+
34
+ attribute :location, :location
35
+ attribute :server_types, :datacenter_server_type
36
+
37
+ def self.recommendation
38
+ find client
39
+ .get("/#{resource_name.pluralize}")
40
+ .fetch(:recommendation)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HCloud
4
+ ##
5
+ # Represents a firewall
6
+ #
7
+ # == List all firewalls
8
+ #
9
+ # HCloud::Firewall.all
10
+ # # => [#<HCloud::Firewall id: 1, ...>, ...]
11
+ #
12
+ # == Sort firewalls
13
+ #
14
+ # HCloud::Firewall.all.sort(name: :desc)
15
+ # # => [#<HCloud::Firewall id: 1, ...>, ...]
16
+ #
17
+ # HCloud::Firewall.all.sort(:id, name: :asc)
18
+ # # => [#<HCloud::Firewall id: 1, ...>, ...]
19
+ #
20
+ # == Search firewalls
21
+ #
22
+ # HCloud::Firewall.all.where(name: "my_irewall")
23
+ # # => #<HCloud::Firewall id: 1, ...>
24
+ #
25
+ # == Find firewall by ID
26
+ #
27
+ # HCloud::Firewall.find(1)
28
+ # # => #<HCloud::Firewall id: 1, ...>
29
+ #
30
+ # == Create firewall
31
+ #
32
+ # firewall = HCloud::Firewall.new(name: "my_firewall")
33
+ # firewall.create
34
+ # firewall.created?
35
+ # # => true
36
+ #
37
+ # firewall = HCloud::Firewall.create(name: "my_firewall")
38
+ # # => #<HCloud::Firewall id: 1, ...>
39
+ #
40
+ # == Update firewall
41
+ #
42
+ # firewall = HCloud::Firewall.find(1)
43
+ # firewall.name = "another_firewall"
44
+ # firewall.update
45
+ #
46
+ # == Delete firewall
47
+ #
48
+ # firewall = HCloud::Firewall.find(1)
49
+ # firewall.delete
50
+ # firewall.deleted?
51
+ # # => true
52
+ #
53
+ # TODO: firewall actions
54
+ #
55
+ class Firewall < Resource
56
+ queryable
57
+ creatable
58
+ updatable
59
+ deletable
60
+
61
+ attribute :id, :integer
62
+ attribute :name
63
+
64
+ attribute :applied_to, :applied_to, array: true, default: -> { [] }
65
+ # Only used for creation
66
+ attribute :apply_to, :apply_to, array: true, default: -> { [] }
67
+
68
+ attribute :rules, :rule, array: true, default: -> { [] }
69
+
70
+ attribute :labels, default: -> { {} }
71
+
72
+ def creatable_attributes
73
+ [:name, :labels, :apply_to, :rules]
74
+ end
75
+
76
+ def updatable_attributes
77
+ [:name, :labels]
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HCloud
4
+ ##
5
+ # Represents a floating IP
6
+ #
7
+ # == List all floating IPs
8
+ #
9
+ # HCloud::FloatingIP.all
10
+ # # => [#<HCloud::FloatingIP id: 1, ...>, ...]
11
+ #
12
+ # == Sort floating IPs
13
+ #
14
+ # HCloud::FloatingIP.all.sort(id: :asc)
15
+ # # => [#<HCloud::FloatingIP id: 1, ...>, ...]
16
+ #
17
+ # HCloud::FloatingIP.all.sort(:id, created: :asc)
18
+ # # => [#<HCloud::FloatingIP id: 1, ...>, ...]
19
+ #
20
+ # == Search floating IPs
21
+ #
22
+ # HCloud::FloatingIP.all.where(name: "my_floating_ip")
23
+ # # => #<HCloud::FloatingIP id: 1, ...>
24
+ #
25
+ # == Find floating IP by ID
26
+ #
27
+ # HCloud::FloatingIP.find(1)
28
+ # # => #<HCloud::FloatingIP id: 1, ...>
29
+ #
30
+ # == Create floating IP
31
+ #
32
+ # floating IP = HCloud::FloatingIP.new(name: "my_floating_ip", type: "ipv4", home_location: "fsn1")
33
+ # floating IP.create
34
+ # floating IP.created?
35
+ # # => true
36
+ #
37
+ # firewall = HCloud::FloatingIP.create(name: "my_floating_ip")
38
+ # # => #<HCloud::FloatingIP id: 1, ...>
39
+ #
40
+ # == Update floating IP
41
+ #
42
+ # floating IP = HCloud::FloatingIP.find(1)
43
+ # floating IP.name = "another_floating_ip"
44
+ # floating IP.update
45
+ #
46
+ # == Delete floating IP
47
+ #
48
+ # floating IP = HCloud::FloatingIP.find(1)
49
+ # floating IP.delete
50
+ # floating IP.deleted?
51
+ # # => true
52
+ #
53
+ # = Actions
54
+ # == List actions
55
+ #
56
+ # actions = HCloud::FloatingIP.find(1).actions
57
+ # # => [#<HCloud::Action id: 1, ...>, ...]
58
+ #
59
+ # == Sort actions
60
+ #
61
+ # HCloud::FloatingIP.find(1).actions.sort(finished: :desc)
62
+ # # => [#<HCloud::Action id: 1, ...>, ...]
63
+ #
64
+ # HCloud::FloatingIP.find(1).actions.sort(:command, finished: :asc)
65
+ # # => [#<HCloud::Actions id: 1, ...>, ...]
66
+ #
67
+ # == Search actions
68
+ #
69
+ # HCloud::FloatingIP.find(1).actions.where(command: "assign_floating_ip")
70
+ # # => #<HCloud::Action id: 1, ...>
71
+ #
72
+ # HCloud::FloatingIP.find(1).actions.where(status: "success")
73
+ # # => #<HCloud::Action id: 1, ...>
74
+ #
75
+ # == Find action by ID
76
+ #
77
+ # HCloud::FloatingIP.find(1).actions.find(1)
78
+ # # => #<HCloud::Action id: 1, ...>
79
+ #
80
+ # = Floating IP-specific actions
81
+ # == Assign a floating IP to a server
82
+ #
83
+ # HCloud::FloatingIP.find(1).assign(server: 1)
84
+ # # => #<HCloud::Action id: 1, ...>
85
+ #
86
+ # == Unassign a floating IP from a server
87
+ #
88
+ # HCloud::FloatingIP.find(1).unassign
89
+ # # => #<HCloud::Action id: 1, ...>
90
+ #
91
+ # == Change reverse DNS entry
92
+ #
93
+ # HCloud::FloatingIP.find(1).change_dns_ptr(dns_ptr: "server.example.com", ip: "1.2.3.4")
94
+ # # => #<HCloud::Action id: 1, ...>
95
+ #
96
+ # == Change protection
97
+ #
98
+ # HCloud::FloatingIP.find(1).change_protection(delete: true)
99
+ # # => #<HCloud::Action id: 1, ...>
100
+ #
101
+ class FloatingIP < Resource
102
+ actionable
103
+ queryable
104
+ creatable
105
+ updatable
106
+ deletable
107
+
108
+ attribute :id, :integer
109
+ attribute :name
110
+ attribute :description
111
+
112
+ attribute :type
113
+ attribute :ip
114
+ attribute :dns_ptr, :dns_pointer, array: true, default: -> { [] }
115
+
116
+ attribute :blocked, :boolean
117
+
118
+ attribute :home_location, :location
119
+
120
+ attribute :protection, :protection
121
+
122
+ # TODO: return Server object
123
+ attribute :server, :integer
124
+
125
+ attribute :labels, default: -> { {} }
126
+
127
+ alias blocked? blocked
128
+
129
+ action :assign
130
+ action :unassign
131
+
132
+ action :change_dns_ptr
133
+
134
+ action :change_protection
135
+
136
+ def creatable_attributes
137
+ [:name, :description, :type, :labels, home_location: [:id, :name], server: [:id, :name]]
138
+ end
139
+
140
+ def updatable_attributes
141
+ [:name, :labels]
142
+ end
143
+ end
144
+ end