durable-proxmox-ruby 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE +21 -0
- data/README.md +572 -0
- data/durable-proxmox-ruby.gemspec +47 -0
- data/exe/proxmox +218 -0
- data/lib/durable_proxmox/client.rb +262 -0
- data/lib/durable_proxmox/dsl.rb +374 -0
- data/lib/durable_proxmox/errors.rb +44 -0
- data/lib/durable_proxmox/ip_address_group.rb +99 -0
- data/lib/durable_proxmox/node.rb +126 -0
- data/lib/durable_proxmox/version.rb +5 -0
- data/lib/durable_proxmox/vm.rb +200 -0
- data/lib/durable_proxmox.rb +31 -0
- metadata +201 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DurableProxmox
|
|
4
|
+
# DSL for declaratively defining Proxmox VMs.
|
|
5
|
+
#
|
|
6
|
+
# Example:
|
|
7
|
+
# DurableProxmox::DSL.configure do
|
|
8
|
+
# vm 'web-server-1' do
|
|
9
|
+
# ip '192.168.2.100'
|
|
10
|
+
# template_id 911
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
class DSL
|
|
14
|
+
# @return [Array<VMDefinition>] List of defined VMs
|
|
15
|
+
attr_reader :vms
|
|
16
|
+
|
|
17
|
+
# @return [DurableProxmox::Client] The Proxmox client
|
|
18
|
+
attr_reader :client
|
|
19
|
+
|
|
20
|
+
# Executes a DSL configuration block.
|
|
21
|
+
#
|
|
22
|
+
# @param client [DurableProxmox::Client, nil] The Proxmox client (creates one if nil)
|
|
23
|
+
# @param insecure [Boolean] Skip SSL certificate validation (default: false)
|
|
24
|
+
# @yield Block with VM definitions
|
|
25
|
+
# @return [DSL] The DSL instance
|
|
26
|
+
def self.configure(client = nil, insecure: false, &block)
|
|
27
|
+
dsl = new(client, insecure: insecure)
|
|
28
|
+
dsl.instance_eval(&block)
|
|
29
|
+
dsl
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Creates a new DSL instance.
|
|
33
|
+
#
|
|
34
|
+
# @param client [DurableProxmox::Client, nil] The Proxmox client
|
|
35
|
+
# @param insecure [Boolean] Skip SSL certificate validation (default: false)
|
|
36
|
+
def initialize(client = nil, insecure: false)
|
|
37
|
+
@client = client || Client.new(nil, insecure: insecure)
|
|
38
|
+
@vms = []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Defines a VM in the DSL.
|
|
42
|
+
#
|
|
43
|
+
# @param name [String] The VM hostname
|
|
44
|
+
# @yield Block with VM configuration
|
|
45
|
+
# @return [VMDefinition] The VM definition
|
|
46
|
+
def vm(name, &block)
|
|
47
|
+
vm_def = VMDefinition.new(name, @client)
|
|
48
|
+
vm_def.instance_eval(&block) if block_given?
|
|
49
|
+
@vms << vm_def
|
|
50
|
+
vm_def
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Applies all VM definitions (create or update VMs).
|
|
54
|
+
#
|
|
55
|
+
# @param node [DurableProxmox::Node, nil] The target node (uses first node if nil)
|
|
56
|
+
# @return [Array<Hash>] Results for each VM
|
|
57
|
+
def apply(node = nil)
|
|
58
|
+
target_node = node || @client.nodes.first
|
|
59
|
+
@vms.map { |vm_def| vm_def.apply(target_node) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Plans all VM definitions (shows what would be done without executing).
|
|
63
|
+
#
|
|
64
|
+
# @param node [DurableProxmox::Node, nil] The target node (uses first node if nil)
|
|
65
|
+
# @return [Array<Hash>] Plan for each VM
|
|
66
|
+
def plan(node = nil)
|
|
67
|
+
target_node = node || @client.nodes.first
|
|
68
|
+
@vms.map { |vm_def| vm_def.plan(target_node) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Represents a VM definition in the DSL.
|
|
73
|
+
class VMDefinition
|
|
74
|
+
# @return [String] VM hostname
|
|
75
|
+
attr_reader :name
|
|
76
|
+
|
|
77
|
+
# @return [String, nil] IP address
|
|
78
|
+
attr_reader :ip
|
|
79
|
+
|
|
80
|
+
# @return [String, nil] Gateway address
|
|
81
|
+
attr_reader :gateway
|
|
82
|
+
|
|
83
|
+
# @return [Integer, nil] Template VM ID for cloning
|
|
84
|
+
attr_reader :template_id
|
|
85
|
+
|
|
86
|
+
# @return [Integer, nil] Number of CPU cores
|
|
87
|
+
attr_reader :cores
|
|
88
|
+
|
|
89
|
+
# @return [Integer, nil] Memory in MB
|
|
90
|
+
attr_reader :memory
|
|
91
|
+
|
|
92
|
+
# @return [Integer, nil] Specific VMID to use
|
|
93
|
+
attr_reader :vmid
|
|
94
|
+
|
|
95
|
+
# @return [Array<String>, nil] DNS server addresses
|
|
96
|
+
attr_reader :dns_servers
|
|
97
|
+
|
|
98
|
+
# @return [DurableProxmox::Client] The Proxmox client
|
|
99
|
+
attr_reader :client
|
|
100
|
+
|
|
101
|
+
# Creates a new VM definition.
|
|
102
|
+
#
|
|
103
|
+
# @param name [String] The VM hostname
|
|
104
|
+
# @param client [DurableProxmox::Client] The Proxmox client
|
|
105
|
+
def initialize(name, client)
|
|
106
|
+
@name = name
|
|
107
|
+
@client = client
|
|
108
|
+
@template_id = (ENV["SOURCE_VMID"] || 911).to_i
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Sets the IP address for the VM.
|
|
112
|
+
#
|
|
113
|
+
# @param value [String, nil] The IP address (returns current value if nil)
|
|
114
|
+
# @return [String, nil] The IP address
|
|
115
|
+
def ip(value = nil)
|
|
116
|
+
return @ip if value.nil?
|
|
117
|
+
@ip = value
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Sets the gateway address for the VM.
|
|
121
|
+
#
|
|
122
|
+
# @param value [String, nil] The gateway address (returns current value if nil)
|
|
123
|
+
# @return [String, nil] The gateway address
|
|
124
|
+
def gateway(value = nil)
|
|
125
|
+
return @gateway if value.nil?
|
|
126
|
+
@gateway = value
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Sets the template ID for cloning.
|
|
130
|
+
#
|
|
131
|
+
# @param value [Integer, nil] The template VM ID (returns current value if nil)
|
|
132
|
+
# @return [Integer, nil] The template ID
|
|
133
|
+
def template_id(value = nil)
|
|
134
|
+
return @template_id if value.nil?
|
|
135
|
+
@template_id = value
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Sets the number of CPU cores.
|
|
139
|
+
#
|
|
140
|
+
# @param value [Integer, nil] Number of cores (returns current value if nil)
|
|
141
|
+
# @return [Integer, nil] The number of cores
|
|
142
|
+
def cores(value = nil)
|
|
143
|
+
return @cores if value.nil?
|
|
144
|
+
@cores = value
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Sets the memory size.
|
|
148
|
+
#
|
|
149
|
+
# @param value [Integer, nil] Memory in MB (returns current value if nil)
|
|
150
|
+
# @return [Integer, nil] The memory size
|
|
151
|
+
def memory(value = nil)
|
|
152
|
+
return @memory if value.nil?
|
|
153
|
+
@memory = value
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Sets a specific VMID.
|
|
157
|
+
#
|
|
158
|
+
# @param value [Integer, nil] The VMID (returns current value if nil)
|
|
159
|
+
# @return [Integer, nil] The VMID
|
|
160
|
+
def vmid(value = nil)
|
|
161
|
+
return @vmid if value.nil?
|
|
162
|
+
@vmid = value
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Sets DNS server addresses.
|
|
166
|
+
#
|
|
167
|
+
# @param servers [Array<String>, String] DNS server IP address(es) (returns current value if empty)
|
|
168
|
+
# @return [Array<String>, nil] The DNS servers
|
|
169
|
+
def dns_servers(*servers)
|
|
170
|
+
return @dns_servers if servers.empty?
|
|
171
|
+
@dns_servers = servers.flatten
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Applies this VM definition (creates or updates the VM).
|
|
175
|
+
#
|
|
176
|
+
# @param node [DurableProxmox::Node] The target node
|
|
177
|
+
# @return [Hash] Result with VM details
|
|
178
|
+
def apply(node)
|
|
179
|
+
# Check if VM already exists
|
|
180
|
+
existing_vm = node.vms.find { |vm| vm.name == @name }
|
|
181
|
+
|
|
182
|
+
if existing_vm
|
|
183
|
+
update_vm(existing_vm, node)
|
|
184
|
+
else
|
|
185
|
+
create_vm(node)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Plans this VM definition (shows what would be done without executing).
|
|
190
|
+
#
|
|
191
|
+
# @param node [DurableProxmox::Node] The target node
|
|
192
|
+
# @return [Hash] Plan with VM details
|
|
193
|
+
def plan(node)
|
|
194
|
+
# Check if VM already exists
|
|
195
|
+
existing_vm = node.vms.find { |vm| vm.name == @name }
|
|
196
|
+
|
|
197
|
+
if existing_vm
|
|
198
|
+
plan_update(existing_vm, node)
|
|
199
|
+
else
|
|
200
|
+
plan_create(node)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
# Creates a new VM from this definition.
|
|
207
|
+
#
|
|
208
|
+
# @param node [DurableProxmox::Node] The target node
|
|
209
|
+
# @return [Hash] Result with VM details
|
|
210
|
+
def create_vm(node)
|
|
211
|
+
# Clone from template
|
|
212
|
+
new_vmid = @vmid || node.get_next_vmid
|
|
213
|
+
puts "Creating VM '#{@name}' (vmid: #{new_vmid}) from template #{@template_id}..."
|
|
214
|
+
|
|
215
|
+
params = {
|
|
216
|
+
newid: new_vmid,
|
|
217
|
+
target: node.node_id,
|
|
218
|
+
name: @name,
|
|
219
|
+
full: 1
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
response = @client.post("nodes/#{node.node_id}/qemu/#{@template_id}/clone", params)
|
|
223
|
+
resp = response.body.dig("data")
|
|
224
|
+
|
|
225
|
+
if resp
|
|
226
|
+
print "Cloning VM"
|
|
227
|
+
@client.wait_for_upid(node.node_id, resp)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Update configuration
|
|
231
|
+
node.clear_cache
|
|
232
|
+
vm = node.vms.find { |v| v.vm_id == new_vmid }
|
|
233
|
+
|
|
234
|
+
if vm
|
|
235
|
+
puts "VM '#{@name}' created successfully (vmid: #{new_vmid})"
|
|
236
|
+
apply_configuration(vm, node)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
{
|
|
240
|
+
action: "created",
|
|
241
|
+
name: @name,
|
|
242
|
+
vmid: new_vmid,
|
|
243
|
+
ip: @ip
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Updates an existing VM with this definition.
|
|
248
|
+
#
|
|
249
|
+
# @param vm [DurableProxmox::VM] The existing VM
|
|
250
|
+
# @param node [DurableProxmox::Node] The node
|
|
251
|
+
# @return [Hash] Result with VM details
|
|
252
|
+
def update_vm(vm, node)
|
|
253
|
+
puts "Updating VM '#{@name}' (vmid: #{vm.vm_id})..."
|
|
254
|
+
apply_configuration(vm, node)
|
|
255
|
+
puts "VM '#{@name}' updated successfully"
|
|
256
|
+
|
|
257
|
+
{
|
|
258
|
+
action: "updated",
|
|
259
|
+
name: @name,
|
|
260
|
+
vmid: vm.vm_id,
|
|
261
|
+
ip: @ip
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Applies configuration to a VM.
|
|
266
|
+
#
|
|
267
|
+
# @param vm [DurableProxmox::VM] The VM to configure
|
|
268
|
+
# @param node [DurableProxmox::Node] The node
|
|
269
|
+
def apply_configuration(vm, node)
|
|
270
|
+
config_params = {}
|
|
271
|
+
|
|
272
|
+
config_params[:cores] = @cores if @cores
|
|
273
|
+
config_params[:memory] = @memory if @memory
|
|
274
|
+
|
|
275
|
+
# Set IP via cloud-init ipconfig
|
|
276
|
+
if @ip
|
|
277
|
+
# Format: ip=192.168.1.100/24,gw=192.168.1.1
|
|
278
|
+
# If IP already has a netmask, use it as-is; otherwise default to /24
|
|
279
|
+
ip_with_netmask = @ip.include?('/') ? @ip : "#{@ip}/24"
|
|
280
|
+
ipconfig = "ip=#{ip_with_netmask}"
|
|
281
|
+
|
|
282
|
+
# Use specified gateway, or preserve existing gateway if not specified
|
|
283
|
+
gateway_to_use = @gateway
|
|
284
|
+
if gateway_to_use.nil? && vm.config["ipconfig0"]
|
|
285
|
+
# Extract existing gateway from ipconfig0 (format: ip=x.x.x.x/24,gw=y.y.y.y)
|
|
286
|
+
existing_ipconfig = vm.config["ipconfig0"]
|
|
287
|
+
if existing_ipconfig =~ /gw=([^,]+)/
|
|
288
|
+
gateway_to_use = ::Regexp.last_match(1)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
ipconfig += ",gw=#{gateway_to_use}" if gateway_to_use
|
|
293
|
+
config_params[:ipconfig0] = ipconfig
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Set DNS servers via cloud-init nameserver
|
|
297
|
+
if @dns_servers && !@dns_servers.empty?
|
|
298
|
+
config_params[:nameserver] = @dns_servers.join(" ")
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
unless config_params.empty?
|
|
302
|
+
puts " Applying configuration: #{config_params.inspect}"
|
|
303
|
+
@client.post("nodes/#{node.node_id}/qemu/#{vm.vm_id}/config", config_params)
|
|
304
|
+
puts " Configuration applied successfully"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Plans VM creation without executing.
|
|
309
|
+
#
|
|
310
|
+
# @param node [DurableProxmox::Node] The target node
|
|
311
|
+
# @return [Hash] Plan with VM details
|
|
312
|
+
def plan_create(node)
|
|
313
|
+
planned_vmid = @vmid || node.get_next_vmid
|
|
314
|
+
planned_config = build_config_params
|
|
315
|
+
|
|
316
|
+
{
|
|
317
|
+
action: "create",
|
|
318
|
+
name: @name,
|
|
319
|
+
vmid: planned_vmid,
|
|
320
|
+
template_id: @template_id,
|
|
321
|
+
configuration: planned_config
|
|
322
|
+
}
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Plans VM update without executing.
|
|
326
|
+
#
|
|
327
|
+
# @param vm [DurableProxmox::VM] The existing VM
|
|
328
|
+
# @param node [DurableProxmox::Node] The node
|
|
329
|
+
# @return [Hash] Plan with VM details
|
|
330
|
+
def plan_update(vm, node)
|
|
331
|
+
current_config = vm.config
|
|
332
|
+
planned_config = build_config_params
|
|
333
|
+
|
|
334
|
+
# Only include changes
|
|
335
|
+
changes = {}
|
|
336
|
+
planned_config.each do |key, value|
|
|
337
|
+
current_value = if key == :ipconfig0
|
|
338
|
+
current_config["ipconfig0"]
|
|
339
|
+
else
|
|
340
|
+
current_config[key.to_s]
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
changes[key] = { from: current_value, to: value } if current_value != value
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
{
|
|
347
|
+
action: "update",
|
|
348
|
+
name: @name,
|
|
349
|
+
vmid: vm.vm_id,
|
|
350
|
+
changes: changes
|
|
351
|
+
}
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Builds configuration parameters from the definition.
|
|
355
|
+
#
|
|
356
|
+
# @return [Hash] Configuration parameters
|
|
357
|
+
def build_config_params
|
|
358
|
+
config_params = {}
|
|
359
|
+
|
|
360
|
+
config_params[:cores] = @cores if @cores
|
|
361
|
+
config_params[:memory] = @memory if @memory
|
|
362
|
+
if @ip
|
|
363
|
+
# If IP already has a netmask, use it as-is; otherwise default to /24
|
|
364
|
+
ip_with_netmask = @ip.include?('/') ? @ip : "#{@ip}/24"
|
|
365
|
+
ipconfig = "ip=#{ip_with_netmask}"
|
|
366
|
+
ipconfig += ",gw=#{@gateway}" if @gateway
|
|
367
|
+
config_params[:ipconfig0] = ipconfig
|
|
368
|
+
end
|
|
369
|
+
config_params[:nameserver] = @dns_servers.join(" ") if @dns_servers && !@dns_servers.empty?
|
|
370
|
+
|
|
371
|
+
config_params
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DurableProxmox
|
|
4
|
+
# Base error class for all DurableProxmox errors.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when authentication fails or credentials are invalid.
|
|
8
|
+
class AuthenticationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when a requested resource (VM, node, etc.) is not found.
|
|
11
|
+
class NotFoundError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when input validation fails.
|
|
14
|
+
class ValidationError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when the Proxmox API returns an error response.
|
|
17
|
+
class APIError < Error
|
|
18
|
+
# @return [Integer, nil] HTTP status code
|
|
19
|
+
attr_reader :status_code
|
|
20
|
+
|
|
21
|
+
# @return [Hash, nil] Response body from the API
|
|
22
|
+
attr_reader :response_body
|
|
23
|
+
|
|
24
|
+
# Creates a new APIError.
|
|
25
|
+
#
|
|
26
|
+
# @param message [String] Error message
|
|
27
|
+
# @param status_code [Integer, nil] HTTP status code
|
|
28
|
+
# @param response_body [Hash, nil] Response body from the API
|
|
29
|
+
def initialize(message, status_code: nil, response_body: nil)
|
|
30
|
+
super(message)
|
|
31
|
+
@status_code = status_code
|
|
32
|
+
@response_body = response_body
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Raised when a request times out.
|
|
37
|
+
class TimeoutError < Error; end
|
|
38
|
+
|
|
39
|
+
# Raised when the QEMU guest agent is not available or not responding.
|
|
40
|
+
class GuestAgentError < Error; end
|
|
41
|
+
|
|
42
|
+
# Raised when a network connection fails.
|
|
43
|
+
class ConnectionError < Error; end
|
|
44
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module DurableProxmox
|
|
6
|
+
# Utility class for managing groups of IP addresses.
|
|
7
|
+
#
|
|
8
|
+
# Provides methods for filtering, finding ranges, and identifying missing IPs.
|
|
9
|
+
class IPAddressGroup
|
|
10
|
+
# @return [Array] The group of IP addresses or interface data
|
|
11
|
+
attr_accessor :group
|
|
12
|
+
|
|
13
|
+
# Creates a new IP address group.
|
|
14
|
+
#
|
|
15
|
+
# @param group [Array] Array of IP addresses or interface data
|
|
16
|
+
def initialize(group)
|
|
17
|
+
@group = group
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the number of items in the group.
|
|
21
|
+
#
|
|
22
|
+
# @return [Integer] The length of the group
|
|
23
|
+
def length
|
|
24
|
+
@group.length
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Converts the group to an array.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array] The group as an array
|
|
30
|
+
def to_a
|
|
31
|
+
@group
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Filters the group to include only IPv4 addresses (for interfaces).
|
|
35
|
+
#
|
|
36
|
+
# @return [DurableProxmox::IPAddressGroup] New group with only IPv4 addresses
|
|
37
|
+
def ipv4
|
|
38
|
+
IPAddressGroup.new(
|
|
39
|
+
@group.select { |ip| ip["ip-address-type"] == "ipv4" }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the minimum IP address in the group.
|
|
44
|
+
#
|
|
45
|
+
# @return [IPAddr] The minimum IP address
|
|
46
|
+
def min
|
|
47
|
+
ip_addresses.min
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the maximum IP address in the group.
|
|
51
|
+
#
|
|
52
|
+
# @return [IPAddr] The maximum IP address
|
|
53
|
+
def max
|
|
54
|
+
ip_addresses.max
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the IP address range from min to max.
|
|
58
|
+
#
|
|
59
|
+
# @return [Range] The IP address range
|
|
60
|
+
def range
|
|
61
|
+
min..max
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Finds IP addresses that are missing from the range.
|
|
65
|
+
#
|
|
66
|
+
# @return [Array<IPAddr>] Array of missing IP addresses
|
|
67
|
+
def missing
|
|
68
|
+
range.to_a - ip_addresses
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Converts the group to IPAddr objects.
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<IPAddr>] Array of IPAddr objects
|
|
74
|
+
def ip_addresses
|
|
75
|
+
octets.map { |octet| IPAddr.new(octet) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Filters the group to include only addresses within specified subnets.
|
|
79
|
+
#
|
|
80
|
+
# @param subnets [Array<String>] Array of subnet specifications
|
|
81
|
+
# @return [DurableProxmox::IPAddressGroup] New group filtered by subnets
|
|
82
|
+
def in_subnets(*subnets)
|
|
83
|
+
subnets = subnets.map { |subnet| IPAddr.new(subnet) }
|
|
84
|
+
IPAddressGroup.new(
|
|
85
|
+
@group.select do |interface|
|
|
86
|
+
ip = IPAddr.new(interface["ip-address"] || interface["ipaddr"])
|
|
87
|
+
subnets.any? { |subnet| subnet.include?(ip) }
|
|
88
|
+
end
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Extracts IP address octets from the group data.
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<String>] Array of IP address strings
|
|
95
|
+
def octets
|
|
96
|
+
@group.map { |item| item["ip-address"] || item["ipaddr"] || item }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module DurableProxmox
|
|
7
|
+
# Represents a Proxmox node (physical server).
|
|
8
|
+
#
|
|
9
|
+
# Provides access to VMs, LXC containers, and node operations.
|
|
10
|
+
class Node
|
|
11
|
+
# @return [String] Node name
|
|
12
|
+
attr_reader :name
|
|
13
|
+
|
|
14
|
+
# @return [String] Node status
|
|
15
|
+
attr_reader :status
|
|
16
|
+
|
|
17
|
+
# @return [Float] CPU usage
|
|
18
|
+
attr_reader :cpu
|
|
19
|
+
|
|
20
|
+
# @return [Float] Memory usage
|
|
21
|
+
attr_reader :memory
|
|
22
|
+
|
|
23
|
+
# @return [DurableProxmox::Client] The client instance
|
|
24
|
+
attr_reader :client
|
|
25
|
+
|
|
26
|
+
# @return [String] Node identifier
|
|
27
|
+
attr_reader :node_id
|
|
28
|
+
|
|
29
|
+
# Creates a new node instance.
|
|
30
|
+
#
|
|
31
|
+
# @param data [Hash] Node data from the API
|
|
32
|
+
# @param client [DurableProxmox::Client] The client instance
|
|
33
|
+
def initialize(data, client)
|
|
34
|
+
@client = client
|
|
35
|
+
@name = data["name"]
|
|
36
|
+
@node_id = data["node"]
|
|
37
|
+
@status = data["status"]
|
|
38
|
+
@cpu = data["cpu"]
|
|
39
|
+
@memory = data["memory"]
|
|
40
|
+
@cache = {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Clears the internal cache.
|
|
44
|
+
#
|
|
45
|
+
# Useful when the node state has changed externally.
|
|
46
|
+
def clear_cache
|
|
47
|
+
@cache.clear
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Deletes a VM from this node.
|
|
51
|
+
#
|
|
52
|
+
# @param vm [DurableProxmox::VM] The VM to delete
|
|
53
|
+
# @return [Hash] The response body
|
|
54
|
+
def delete_vm(vm)
|
|
55
|
+
puts "Deleting VM #{vm.vm_id} (#{vm.name}) from node #{node_id}..."
|
|
56
|
+
resp = @client.delete("nodes/#{node_id}/qemu/#{vm.vm_id}")
|
|
57
|
+
upid = resp.body.dig("data")
|
|
58
|
+
|
|
59
|
+
if upid
|
|
60
|
+
@client.wait_for_upid(node_id, upid)
|
|
61
|
+
puts "VM #{vm.vm_id} (#{vm.name}) deleted successfully"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
resp.body
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Clones a VM from the source template and assigns it a hostname.
|
|
68
|
+
#
|
|
69
|
+
# @param hostname [String] The hostname for the new VM
|
|
70
|
+
# @return [Integer] The new VM ID
|
|
71
|
+
def spawn_vm(hostname)
|
|
72
|
+
source_vmid = (ENV["SOURCE_VMID"] || 911).to_i
|
|
73
|
+
new_vmid = get_next_vmid
|
|
74
|
+
|
|
75
|
+
puts "Spawning VM '#{hostname}' (vmid: #{new_vmid}) from template #{source_vmid}..."
|
|
76
|
+
|
|
77
|
+
params = { newid: new_vmid, target: @node_id, name: hostname, full: 1 }
|
|
78
|
+
|
|
79
|
+
resp = @client.post("nodes/#{node_id}/qemu/#{source_vmid}/clone", params).body.dig("data")
|
|
80
|
+
|
|
81
|
+
sleep 1
|
|
82
|
+
|
|
83
|
+
if resp
|
|
84
|
+
print "Cloning VM"
|
|
85
|
+
@client.wait_for_upid(node_id, resp)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
data = @client.get("nodes/#{node_id}/qemu/#{new_vmid}/config").body["data"] || {}
|
|
89
|
+
|
|
90
|
+
puts "VM '#{hostname}' spawned successfully (vmid: #{new_vmid})"
|
|
91
|
+
|
|
92
|
+
new_vmid
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Finds the next available VM ID.
|
|
96
|
+
#
|
|
97
|
+
# @return [Integer] The next available VM ID
|
|
98
|
+
def get_next_vmid
|
|
99
|
+
vmids = vms.map { |vm| vm.vm_id.to_i }.sort
|
|
100
|
+
lxcids = lxcs.map { |lxc| lxc["vmid"].to_i }.sort
|
|
101
|
+
excluded_vmids = [148]
|
|
102
|
+
|
|
103
|
+
upper = vmids.max || 100
|
|
104
|
+
lower = vmids.min || 0
|
|
105
|
+
|
|
106
|
+
missing = (lower..upper).to_a - vmids - lxcids - excluded_vmids
|
|
107
|
+
missing.first || upper + 1 || 101
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Retrieves LXC containers on this node.
|
|
111
|
+
#
|
|
112
|
+
# @return [Array<Hash>] Array of LXC container data
|
|
113
|
+
def lxcs
|
|
114
|
+
@cache[:lxcs] ||= @client.get("nodes/#{node_id}/lxc").body["data"]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Retrieves VMs on this node.
|
|
118
|
+
#
|
|
119
|
+
# @return [Array<DurableProxmox::VM>] Array of VM objects
|
|
120
|
+
def vms
|
|
121
|
+
@cache[:vms] ||= @client.get("nodes/#{node_id}/qemu").body["data"].map do |data|
|
|
122
|
+
VM.new(data, self, @client)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|