hetznercloud 1.0.0

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