nvoi 0.1.5 → 0.1.7

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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/todo/refactor/00-overview.md +171 -0
  3. data/.claude/todo/refactor/01-objects.md +96 -0
  4. data/.claude/todo/refactor/02-utils.md +143 -0
  5. data/.claude/todo/refactor/03-external-cloud.md +164 -0
  6. data/.claude/todo/refactor/04-external-dns.md +104 -0
  7. data/.claude/todo/refactor/05-external.md +133 -0
  8. data/.claude/todo/refactor/06-cli.md +123 -0
  9. data/.claude/todo/refactor/07-cli-deploy-command.md +177 -0
  10. data/.claude/todo/refactor/08-cli-deploy-steps.md +201 -0
  11. data/.claude/todo/refactor/09-cli-delete-command.md +169 -0
  12. data/.claude/todo/refactor/10-cli-exec-command.md +157 -0
  13. data/.claude/todo/refactor/11-cli-credentials-command.md +190 -0
  14. data/.claude/todo/refactor/12-cli-db-command.md +128 -0
  15. data/.claude/todo/refactor/_target.md +79 -0
  16. data/.claude/todo/refactor-execution/00-entrypoint.md +49 -0
  17. data/.claude/todo/refactor-execution/01-objects.md +42 -0
  18. data/.claude/todo/refactor-execution/02-utils.md +41 -0
  19. data/.claude/todo/refactor-execution/03-external-cloud.md +38 -0
  20. data/.claude/todo/refactor-execution/04-external-dns.md +35 -0
  21. data/.claude/todo/refactor-execution/05-external-other.md +46 -0
  22. data/.claude/todo/refactor-execution/06-cli-deploy.md +45 -0
  23. data/.claude/todo/refactor-execution/07-cli-delete.md +43 -0
  24. data/.claude/todo/refactor-execution/08-cli-exec.md +30 -0
  25. data/.claude/todo/refactor-execution/09-cli-credentials.md +34 -0
  26. data/.claude/todo/refactor-execution/10-cli-db.md +31 -0
  27. data/.claude/todo/refactor-execution/11-cli-router.md +44 -0
  28. data/.claude/todo/refactor-execution/12-cleanup.md +120 -0
  29. data/.claude/todo/refactor-execution/_monitoring-strategy.md +126 -0
  30. data/.claude/todo/scaleway.impl.md +644 -0
  31. data/.claude/todo/scaleway.reference.md +520 -0
  32. data/.claude/todos.md +550 -0
  33. data/Gemfile +6 -0
  34. data/Gemfile.lock +46 -5
  35. data/Rakefile +1 -1
  36. data/doc/config-schema.yaml +44 -11
  37. data/examples/golang/deploy.enc +0 -0
  38. data/examples/golang/main.go +18 -0
  39. data/exe/nvoi +3 -1
  40. data/ingest +0 -0
  41. data/lib/nvoi/cli/config/command.rb +219 -0
  42. data/lib/nvoi/cli/credentials/edit/command.rb +384 -0
  43. data/lib/nvoi/cli/credentials/show/command.rb +35 -0
  44. data/lib/nvoi/cli/db/command.rb +308 -0
  45. data/lib/nvoi/cli/delete/command.rb +75 -0
  46. data/lib/nvoi/cli/delete/steps/detach_volumes.rb +98 -0
  47. data/lib/nvoi/cli/delete/steps/teardown_dns.rb +50 -0
  48. data/lib/nvoi/cli/delete/steps/teardown_firewall.rb +46 -0
  49. data/lib/nvoi/cli/delete/steps/teardown_network.rb +30 -0
  50. data/lib/nvoi/cli/delete/steps/teardown_server.rb +50 -0
  51. data/lib/nvoi/cli/delete/steps/teardown_tunnel.rb +44 -0
  52. data/lib/nvoi/cli/delete/steps/teardown_volume.rb +61 -0
  53. data/lib/nvoi/cli/deploy/command.rb +184 -0
  54. data/lib/nvoi/cli/deploy/steps/build_image.rb +27 -0
  55. data/lib/nvoi/cli/deploy/steps/cleanup_images.rb +42 -0
  56. data/lib/nvoi/cli/deploy/steps/configure_tunnel.rb +102 -0
  57. data/lib/nvoi/cli/deploy/steps/deploy_service.rb +399 -0
  58. data/lib/nvoi/cli/deploy/steps/provision_network.rb +44 -0
  59. data/lib/nvoi/cli/deploy/steps/provision_server.rb +143 -0
  60. data/lib/nvoi/cli/deploy/steps/provision_volume.rb +171 -0
  61. data/lib/nvoi/cli/deploy/steps/setup_k3s.rb +490 -0
  62. data/lib/nvoi/cli/exec/command.rb +173 -0
  63. data/lib/nvoi/cli/logs/command.rb +66 -0
  64. data/lib/nvoi/cli/onboard/command.rb +761 -0
  65. data/lib/nvoi/cli/unlock/command.rb +72 -0
  66. data/lib/nvoi/cli.rb +339 -141
  67. data/lib/nvoi/config_api/actions/app.rb +53 -0
  68. data/lib/nvoi/config_api/actions/compute_provider.rb +55 -0
  69. data/lib/nvoi/config_api/actions/database.rb +70 -0
  70. data/lib/nvoi/config_api/actions/domain_provider.rb +40 -0
  71. data/lib/nvoi/config_api/actions/env.rb +32 -0
  72. data/lib/nvoi/config_api/actions/init.rb +67 -0
  73. data/lib/nvoi/config_api/actions/secret.rb +32 -0
  74. data/lib/nvoi/config_api/actions/server.rb +66 -0
  75. data/lib/nvoi/config_api/actions/service.rb +52 -0
  76. data/lib/nvoi/config_api/actions/volume.rb +40 -0
  77. data/lib/nvoi/config_api/base.rb +38 -0
  78. data/lib/nvoi/config_api/result.rb +26 -0
  79. data/lib/nvoi/config_api.rb +93 -0
  80. data/lib/nvoi/errors.rb +68 -50
  81. data/lib/nvoi/external/cloud/aws.rb +450 -0
  82. data/lib/nvoi/external/cloud/base.rb +99 -0
  83. data/lib/nvoi/external/cloud/factory.rb +48 -0
  84. data/lib/nvoi/external/cloud/hetzner.rb +402 -0
  85. data/lib/nvoi/external/cloud/scaleway.rb +559 -0
  86. data/lib/nvoi/external/cloud.rb +15 -0
  87. data/lib/nvoi/external/containerd.rb +86 -0
  88. data/lib/nvoi/external/database/mysql.rb +84 -0
  89. data/lib/nvoi/external/database/postgres.rb +82 -0
  90. data/lib/nvoi/external/database/provider.rb +65 -0
  91. data/lib/nvoi/external/database/sqlite.rb +72 -0
  92. data/lib/nvoi/external/database.rb +22 -0
  93. data/lib/nvoi/external/dns/cloudflare.rb +310 -0
  94. data/lib/nvoi/external/kubectl.rb +65 -0
  95. data/lib/nvoi/external/ssh.rb +106 -0
  96. data/lib/nvoi/objects/config_override.rb +60 -0
  97. data/lib/nvoi/objects/configuration.rb +483 -0
  98. data/lib/nvoi/objects/database.rb +56 -0
  99. data/lib/nvoi/objects/dns.rb +14 -0
  100. data/lib/nvoi/objects/firewall.rb +11 -0
  101. data/lib/nvoi/objects/network.rb +11 -0
  102. data/lib/nvoi/objects/server.rb +14 -0
  103. data/lib/nvoi/objects/service_spec.rb +26 -0
  104. data/lib/nvoi/objects/tunnel.rb +14 -0
  105. data/lib/nvoi/objects/volume.rb +17 -0
  106. data/lib/nvoi/utils/config_loader.rb +172 -0
  107. data/lib/nvoi/utils/constants.rb +61 -0
  108. data/lib/nvoi/{credentials/manager.rb → utils/credential_store.rb} +16 -16
  109. data/lib/nvoi/{credentials → utils}/crypto.rb +8 -5
  110. data/lib/nvoi/{config → utils}/env_resolver.rb +10 -2
  111. data/lib/nvoi/utils/logger.rb +84 -0
  112. data/lib/nvoi/{config/naming.rb → utils/namer.rb} +37 -25
  113. data/lib/nvoi/{deployer → utils}/retry.rb +23 -3
  114. data/lib/nvoi/utils/templates.rb +62 -0
  115. data/lib/nvoi/version.rb +1 -1
  116. data/lib/nvoi.rb +27 -55
  117. data/templates/app-ingress.yaml.erb +3 -1
  118. data/templates/error-backend.yaml.erb +134 -0
  119. metadata +121 -44
  120. data/examples/golang/deploy.yml +0 -54
  121. data/lib/nvoi/cloudflare/client.rb +0 -287
  122. data/lib/nvoi/config/config.rb +0 -248
  123. data/lib/nvoi/config/loader.rb +0 -102
  124. data/lib/nvoi/config/ssh_keys.rb +0 -82
  125. data/lib/nvoi/config/types.rb +0 -274
  126. data/lib/nvoi/constants.rb +0 -59
  127. data/lib/nvoi/credentials/editor.rb +0 -272
  128. data/lib/nvoi/deployer/cleaner.rb +0 -36
  129. data/lib/nvoi/deployer/image_builder.rb +0 -23
  130. data/lib/nvoi/deployer/infrastructure.rb +0 -126
  131. data/lib/nvoi/deployer/orchestrator.rb +0 -146
  132. data/lib/nvoi/deployer/service_deployer.rb +0 -311
  133. data/lib/nvoi/deployer/tunnel_manager.rb +0 -57
  134. data/lib/nvoi/deployer/types.rb +0 -8
  135. data/lib/nvoi/k8s/renderer.rb +0 -44
  136. data/lib/nvoi/k8s/templates.rb +0 -29
  137. data/lib/nvoi/logger.rb +0 -72
  138. data/lib/nvoi/providers/aws.rb +0 -403
  139. data/lib/nvoi/providers/base.rb +0 -111
  140. data/lib/nvoi/providers/hetzner.rb +0 -288
  141. data/lib/nvoi/providers/hetzner_client.rb +0 -170
  142. data/lib/nvoi/remote/docker_manager.rb +0 -203
  143. data/lib/nvoi/remote/ssh_executor.rb +0 -72
  144. data/lib/nvoi/remote/volume_manager.rb +0 -103
  145. data/lib/nvoi/service/delete.rb +0 -234
  146. data/lib/nvoi/service/deploy.rb +0 -80
  147. data/lib/nvoi/service/exec.rb +0 -144
  148. data/lib/nvoi/service/provider.rb +0 -36
  149. data/lib/nvoi/steps/application_deployer.rb +0 -26
  150. data/lib/nvoi/steps/database_provisioner.rb +0 -60
  151. data/lib/nvoi/steps/k3s_cluster_setup.rb +0 -105
  152. data/lib/nvoi/steps/k3s_provisioner.rb +0 -351
  153. data/lib/nvoi/steps/server_provisioner.rb +0 -43
  154. data/lib/nvoi/steps/services_provisioner.rb +0 -29
  155. data/lib/nvoi/steps/tunnel_configurator.rb +0 -66
  156. data/lib/nvoi/steps/volume_provisioner.rb +0 -154
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvoi
4
+ module External
5
+ module Cloud
6
+ # Factory creates cloud providers from config
7
+ module Factory
8
+ class << self
9
+ def for(config)
10
+ case config.provider_name
11
+ when "hetzner"
12
+ h = config.hetzner
13
+ Hetzner.new(h.api_token)
14
+ when "aws"
15
+ a = config.aws
16
+ Aws.new(a.access_key_id, a.secret_access_key, a.region)
17
+ when "scaleway"
18
+ s = config.scaleway
19
+ Scaleway.new(s.secret_key, s.project_id, zone: s.zone)
20
+ else
21
+ raise Errors::ProviderError, "unknown provider: #{config.provider_name}"
22
+ end
23
+ end
24
+
25
+ def validate(config, provider)
26
+ case config.provider_name
27
+ when "hetzner"
28
+ h = config.hetzner
29
+ provider.validate_credentials
30
+ provider.validate_instance_type(h.server_type)
31
+ provider.validate_region(h.server_location)
32
+ when "aws"
33
+ a = config.aws
34
+ provider.validate_credentials
35
+ provider.validate_instance_type(a.instance_type)
36
+ provider.validate_region(a.region)
37
+ when "scaleway"
38
+ s = config.scaleway
39
+ provider.validate_credentials
40
+ provider.validate_instance_type(s.server_type)
41
+ provider.validate_region(s.zone)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,402 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module Nvoi
7
+ module External
8
+ module Cloud
9
+ # Hetzner provider implements the compute provider interface for Hetzner Cloud
10
+ class Hetzner < Base
11
+ BASE_URL = "https://api.hetzner.cloud/v1"
12
+
13
+ def initialize(token)
14
+ @token = token
15
+ @conn = Faraday.new do |f|
16
+ f.request :json
17
+ f.response :json
18
+ f.headers["Authorization"] = "Bearer #{token}"
19
+ end
20
+ end
21
+
22
+ # Network operations
23
+
24
+ def find_or_create_network(name)
25
+ network = find_network_by_name(name)
26
+ return to_network(network) if network
27
+
28
+ network = create_network_api(
29
+ name:,
30
+ ip_range: Utils::Constants::NETWORK_CIDR,
31
+ subnets: [{
32
+ type: "cloud",
33
+ ip_range: Utils::Constants::SUBNET_CIDR,
34
+ network_zone: "eu-central"
35
+ }]
36
+ )
37
+
38
+ to_network(network)
39
+ end
40
+
41
+ def get_network_by_name(name)
42
+ network = find_network_by_name(name)
43
+ raise Errors::NetworkError, "network not found: #{name}" unless network
44
+
45
+ to_network(network)
46
+ end
47
+
48
+ def delete_network(id)
49
+ delete("/networks/#{id.to_i}")
50
+ end
51
+
52
+ # Firewall operations
53
+
54
+ def find_or_create_firewall(name)
55
+ firewall = find_firewall_by_name(name)
56
+ return to_firewall(firewall) if firewall
57
+
58
+ firewall = create_firewall_api(
59
+ name:,
60
+ rules: [{
61
+ direction: "in",
62
+ protocol: "tcp",
63
+ port: "22",
64
+ source_ips: ["0.0.0.0/0", "::/0"]
65
+ }]
66
+ )
67
+
68
+ to_firewall(firewall)
69
+ end
70
+
71
+ def get_firewall_by_name(name)
72
+ firewall = find_firewall_by_name(name)
73
+ raise Errors::FirewallError, "firewall not found: #{name}" unless firewall
74
+
75
+ to_firewall(firewall)
76
+ end
77
+
78
+ def delete_firewall(id)
79
+ delete("/firewalls/#{id.to_i}")
80
+ end
81
+
82
+ # Server operations
83
+
84
+ def find_server(name)
85
+ server = find_server_by_name(name)
86
+ return nil unless server
87
+
88
+ to_server(server)
89
+ end
90
+
91
+ def find_server_by_id(id)
92
+ server = get("/servers/#{id.to_i}")["server"]
93
+ return nil unless server
94
+
95
+ to_server(server)
96
+ rescue Errors::NotFoundError
97
+ nil
98
+ end
99
+
100
+ def list_servers
101
+ get("/servers")["servers"].map { |s| to_server(s) }
102
+ end
103
+
104
+ def create_server(opts)
105
+ # Resolve IDs
106
+ server_type = find_server_type(opts.type)
107
+ raise Errors::ValidationError, "invalid server type: #{opts.type}" unless server_type
108
+
109
+ image = find_image(opts.image)
110
+ raise Errors::ValidationError, "invalid image: #{opts.image}" unless image
111
+
112
+ location = find_location(opts.location)
113
+ raise Errors::ValidationError, "invalid location: #{opts.location}" unless location
114
+
115
+ create_opts = {
116
+ name: opts.name,
117
+ server_type: server_type["name"],
118
+ image: image["name"],
119
+ location: location["name"],
120
+ user_data: opts.user_data,
121
+ start_after_create: true
122
+ }
123
+
124
+ # Add network if provided
125
+ if opts.network_id && !opts.network_id.empty?
126
+ create_opts[:networks] = [opts.network_id.to_i]
127
+ end
128
+
129
+ # Add firewall if provided
130
+ if opts.firewall_id && !opts.firewall_id.empty?
131
+ create_opts[:firewalls] = [{ firewall: opts.firewall_id.to_i }]
132
+ end
133
+
134
+ server = post("/servers", create_opts)["server"]
135
+ to_server(server)
136
+ end
137
+
138
+ def wait_for_server(server_id, max_attempts)
139
+ server = Utils::Retry.poll(max_attempts:, interval: Utils::Constants::SERVER_READY_INTERVAL) do
140
+ s = get("/servers/#{server_id.to_i}")["server"]
141
+ to_server(s) if s["status"] == "running"
142
+ end
143
+
144
+ raise Errors::ServerCreationError, "server did not become running after #{max_attempts} attempts" unless server
145
+
146
+ server
147
+ end
148
+
149
+ def delete_server(id)
150
+ server = get("/servers/#{id.to_i}")["server"]
151
+
152
+ # Remove from firewalls
153
+ get("/firewalls")["firewalls"].each do |fw|
154
+ fw["applied_to"]&.each do |applied|
155
+ next unless applied["type"] == "server" && applied.dig("server", "id") == id.to_i
156
+
157
+ remove_firewall_from_server(fw["id"], id.to_i)
158
+ rescue StandardError
159
+ # Ignore cleanup errors
160
+ end
161
+ end
162
+
163
+ # Detach from networks
164
+ server["private_net"]&.each do |pn|
165
+ detach_server_from_network(id.to_i, pn["network"])
166
+ rescue StandardError
167
+ # Ignore cleanup errors
168
+ end
169
+
170
+ delete("/servers/#{id.to_i}")
171
+ end
172
+
173
+ # Volume operations
174
+
175
+ def create_volume(opts)
176
+ server = get("/servers/#{opts.server_id.to_i}")["server"]
177
+ raise Errors::VolumeError, "server not found: #{opts.server_id}" unless server
178
+
179
+ volume = post("/volumes", {
180
+ name: opts.name,
181
+ size: opts.size,
182
+ location: server.dig("datacenter", "location", "name"),
183
+ format: "xfs"
184
+ })["volume"]
185
+
186
+ to_volume(volume)
187
+ end
188
+
189
+ def get_volume(id)
190
+ volume = get("/volumes/#{id.to_i}")["volume"]
191
+ return nil unless volume
192
+
193
+ to_volume(volume)
194
+ end
195
+
196
+ def get_volume_by_name(name)
197
+ volume = get("/volumes")["volumes"].find { |v| v["name"] == name }
198
+ return nil unless volume
199
+
200
+ to_volume(volume)
201
+ end
202
+
203
+ def delete_volume(id)
204
+ delete("/volumes/#{id.to_i}")
205
+ end
206
+
207
+ def attach_volume(volume_id, server_id)
208
+ post("/volumes/#{volume_id.to_i}/actions/attach", { server: server_id.to_i })
209
+ end
210
+
211
+ def detach_volume(volume_id)
212
+ post("/volumes/#{volume_id.to_i}/actions/detach", {})
213
+ end
214
+
215
+ def wait_for_device_path(volume_id, _ssh)
216
+ # Hetzner provides device_path in API response
217
+ Utils::Retry.poll(max_attempts: 30, interval: 2) do
218
+ volume = get("/volumes/#{volume_id.to_i}")["volume"]
219
+ volume["linux_device"] if volume && volume["linux_device"] && !volume["linux_device"].empty?
220
+ end
221
+ end
222
+
223
+ # Validation operations
224
+
225
+ def validate_instance_type(instance_type)
226
+ server_type = find_server_type(instance_type)
227
+ raise Errors::ValidationError, "invalid hetzner server type: #{instance_type}" unless server_type
228
+
229
+ true
230
+ end
231
+
232
+ def validate_region(region)
233
+ location = find_location(region)
234
+ raise Errors::ValidationError, "invalid hetzner location: #{region}" unless location
235
+
236
+ true
237
+ end
238
+
239
+ def validate_credentials
240
+ get("/server_types")
241
+ true
242
+ rescue Errors::AuthenticationError => e
243
+ raise Errors::ValidationError, "hetzner credentials invalid: #{e.message}"
244
+ end
245
+
246
+ # List available server types for onboarding
247
+ def list_server_types
248
+ get("/server_types")["server_types"].map do |t|
249
+ {
250
+ name: t["name"],
251
+ description: t["description"],
252
+ cores: t["cores"],
253
+ memory: t["memory"],
254
+ disk: t["disk"],
255
+ price: t.dig("prices", 0, "price_monthly", "gross")
256
+ }
257
+ end
258
+ end
259
+
260
+ # List available locations for onboarding
261
+ def list_locations
262
+ get("/locations")["locations"].map do |l|
263
+ {
264
+ name: l["name"],
265
+ city: l["city"],
266
+ country: l["country"],
267
+ description: l["description"]
268
+ }
269
+ end
270
+ end
271
+
272
+ private
273
+
274
+ def get(path)
275
+ response = @conn.get("#{BASE_URL}#{path}")
276
+ handle_response(response)
277
+ end
278
+
279
+ def post(path, payload = {})
280
+ response = @conn.post("#{BASE_URL}#{path}", payload)
281
+ handle_response(response)
282
+ end
283
+
284
+ def delete(path)
285
+ response = @conn.delete("#{BASE_URL}#{path}")
286
+ return nil if response.status == 204
287
+ handle_response(response)
288
+ end
289
+
290
+ def handle_response(response)
291
+ case response.status
292
+ when 200..299
293
+ response.body
294
+ when 401
295
+ raise Errors::AuthenticationError, "Invalid Hetzner API token"
296
+ when 404
297
+ raise Errors::NotFoundError, parse_error(response)
298
+ when 422
299
+ raise Errors::ValidationError, parse_error(response)
300
+ else
301
+ raise Errors::ApiError, parse_error(response)
302
+ end
303
+ end
304
+
305
+ def parse_error(response)
306
+ if response.body.is_a?(Hash) && response.body["error"]
307
+ response.body["error"]["message"]
308
+ else
309
+ "HTTP #{response.status}: #{response.body}"
310
+ end
311
+ end
312
+
313
+ def find_network_by_name(name)
314
+ get("/networks")["networks"].find { |n| n["name"] == name }
315
+ end
316
+
317
+ def find_firewall_by_name(name)
318
+ get("/firewalls")["firewalls"].find { |f| f["name"] == name }
319
+ end
320
+
321
+ def find_server_by_name(name)
322
+ get("/servers")["servers"].find { |s| s["name"] == name }
323
+ end
324
+
325
+ def find_server_type(name)
326
+ get("/server_types")["server_types"].find { |t| t["name"] == name }
327
+ end
328
+
329
+ def find_image(name)
330
+ response = get("/images?name=#{name}")
331
+ response["images"]&.first
332
+ end
333
+
334
+ def find_location(name)
335
+ get("/locations")["locations"].find { |l| l["name"] == name }
336
+ end
337
+
338
+ def create_network_api(payload)
339
+ post("/networks", payload)["network"]
340
+ end
341
+
342
+ def create_firewall_api(payload)
343
+ post("/firewalls", payload)["firewall"]
344
+ end
345
+
346
+ def remove_firewall_from_server(firewall_id, server_id)
347
+ payload = {
348
+ remove_from: [{
349
+ type: "server",
350
+ server: { id: server_id }
351
+ }]
352
+ }
353
+ post("/firewalls/#{firewall_id}/actions/remove_from_resources", payload)
354
+ end
355
+
356
+ def detach_server_from_network(server_id, network_id)
357
+ post("/servers/#{server_id}/actions/detach_from_network", { network: network_id })
358
+ end
359
+
360
+ def to_network(data)
361
+ Objects::Network::Record.new(
362
+ id: data["id"].to_s,
363
+ name: data["name"],
364
+ ip_range: data["ip_range"]
365
+ )
366
+ end
367
+
368
+ def to_firewall(data)
369
+ Objects::Firewall::Record.new(
370
+ id: data["id"].to_s,
371
+ name: data["name"]
372
+ )
373
+ end
374
+
375
+ def to_server(data)
376
+ # Get private IP from private_net array
377
+ private_ip = data["private_net"]&.first&.dig("ip")
378
+
379
+ Objects::Server::Record.new(
380
+ id: data["id"].to_s,
381
+ name: data["name"],
382
+ status: data["status"],
383
+ public_ipv4: data.dig("public_net", "ipv4", "ip"),
384
+ private_ipv4: private_ip
385
+ )
386
+ end
387
+
388
+ def to_volume(data)
389
+ Objects::Volume::Record.new(
390
+ id: data["id"].to_s,
391
+ name: data["name"],
392
+ size: data["size"],
393
+ location: data.dig("location", "name"),
394
+ status: data["status"],
395
+ server_id: data["server"]&.to_s,
396
+ device_path: data["linux_device"]
397
+ )
398
+ end
399
+ end
400
+ end
401
+ end
402
+ end