Class: Pvectl::Repositories::Node

Inherits:
Base
  • Object
show all
Defined in:
lib/pvectl/repositories/node.rb,
sig/pvectl/repositories/node.rbs

Overview

Repository for Proxmox cluster nodes.

Uses the /nodes API endpoint to list nodes. Optionally fetches additional details from per-node endpoints.

Examples:

Listing all nodes

repo = Node.new(connection)
nodes = repo.list
nodes.each { |n| puts "#{n.name}: #{n.status}" }

Getting node with extended details

node = repo.get("pve-node1", include_details: true)

See Also:

Instance Attribute Summary

Attributes inherited from Base

#connection

Instance Method Summary collapse

Methods inherited from Base

#extract_data, #models_from, #unwrap

Constructor Details

#initialize(connection, storage_repository: nil) ⇒ Node

Creates a new Node repository.

Parameters:

  • API connection

  • (defaults to: nil)

    optional storage repository for DI

  • (defaults to: nil)


28
29
30
31
# File 'lib/pvectl/repositories/node.rb', line 28

def initialize(connection, storage_repository: nil)
  super(connection)
  @storage_repository = storage_repository
end

Instance Method Details

#build_describe_model(data) ⇒ Models::Node

Builds Node model with describe-specific attributes.

Parameters:

  • aggregated data from API

Returns:

  • Node model



494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# File 'lib/pvectl/repositories/node.rb', line 494

def build_describe_model(data)
  Models::Node.new(
    # Basic fields (existing)
    name: data[:node] || data[:name],
    status: data[:status],
    cpu: data[:cpu],
    maxcpu: data[:maxcpu],
    mem: data[:mem],
    maxmem: data[:maxmem],
    disk: data[:disk],
    maxdisk: data[:maxdisk],
    uptime: data[:uptime],
    level: data[:level],
    version: data[:version],
    kernel: data[:kernel],
    loadavg: data[:loadavg],
    swap_used: data[:swap_used],
    swap_total: data[:swap_total],
    guests_vms: data[:guests_vms],
    guests_cts: data[:guests_cts],
    ip: data[:ip],
    # Extended fields for describe
    cpuinfo: data[:cpuinfo],
    boot_info: data[:boot_info],
    rootfs: data[:rootfs],
    subscription: data[:subscription],
    dns: data[:dns],
    time_info: data[:time_info],
    network_interfaces: data[:network_interfaces],
    services: data[:services],
    storage_pools: data[:storage_pools],
    physical_disks: data[:physical_disks],
    qemu_cpu_models: data[:qemu_cpu_models],
    qemu_machines: data[:qemu_machines],
    updates_available: data[:updates_available],
    updates: data[:updates],
    offline_note: data[:offline_note],
    firewall: data[:firewall],
    tasks: data[:tasks]
  )
end

#build_model(data) ⇒ Models::Node

Builds Node model from API response data.

Parameters:

  • API response hash

Returns:

  • Node model instance



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/pvectl/repositories/node.rb', line 143

def build_model(data)
  Models::Node.new(
    name: data[:node] || data[:name],
    status: data[:status],
    cpu: data[:cpu],
    maxcpu: data[:maxcpu],
    mem: data[:mem],
    maxmem: data[:maxmem],
    disk: data[:disk],
    maxdisk: data[:maxdisk],
    uptime: data[:uptime],
    level: data[:level],
    version: data[:version],
    kernel: data[:kernel],
    loadavg: data[:loadavg],
    swap_used: data[:swap_used],
    swap_total: data[:swap_total],
    guests_vms: data[:guests_vms],
    guests_cts: data[:guests_cts],
    ip: data[:ip]
  )
end

#describe(name) ⇒ Models::Node?

Describes a node with comprehensive details from multiple API endpoints.

Parameters:

  • node name

Returns:

  • Node model with full details, or nil if not found



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/pvectl/repositories/node.rb', line 75

def describe(name)
  # First check if node exists in cluster
  nodes_data = unwrap(connection.client["nodes"].get)
  basic_data = nodes_data.find { |n| (n[:node] || n[:name]) == name }
  return nil if basic_data.nil?

  # Merge guest counts
  guest_counts = guest_counts_for_cluster
  data = basic_data.merge(
    guests_vms: guest_counts.dig(name, :vms) || 0,
    guests_cts: guest_counts.dig(name, :cts) || 0
  )

  # For offline nodes, return basic data with offline note
  unless basic_data[:status] == "online"
    data[:offline_note] = "Node offline - detailed metrics unavailable"
    return build_describe_model(data)
  end

  # Fetch comprehensive details
  data = data.merge(describe_details_for(name))

  build_describe_model(data)
end

#describe_details_for(node_name) ⇒ Hash

Fetches comprehensive details for describe command. Reuses existing helper methods where possible.

Parameters:

  • node name

Returns:

  • aggregated data from multiple endpoints



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/pvectl/repositories/node.rb', line 285

def describe_details_for(node_name)
  result = {}

  # Reuse existing details_for for version/status/network
  result.merge!(details_for(node_name))

  # Additional describe-specific endpoints
  result.merge!(subscription_for(node_name))
  result.merge!(dns_for(node_name))
  result.merge!(time_for(node_name))
  result.merge!(services_for(node_name))
  result.merge!(storage_pools_for(node_name))
  result.merge!(disks_for(node_name))
  result.merge!(qemu_cpu_for(node_name))
  result.merge!(qemu_machines_for(node_name))
  result.merge!(updates_for(node_name))
  result.merge!(extended_status_for(node_name))
  result.merge!(firewall_for(node_name))
  result.merge!(tasks_for(node_name))

  result
end

#details_for(node_name) ⇒ Hash

Fetches extended details for a node (version, status).

Parameters:

  • node name

Returns:

  • merged version and status data



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/pvectl/repositories/node.rb', line 193

def details_for(node_name)
  result = {}

  # Fetch version
  begin
    version_resp = connection.client["nodes/#{node_name}/version"].get
    version_data = extract_data(version_resp)
    result[:version] = version_data[:version]
    result[:kernel] = version_data[:kernel]
  rescue StandardError
    # Ignore errors fetching version
  end

  # Fetch status (for load, swap)
  begin
    status_resp = connection.client["nodes/#{node_name}/status"].get
    status_data = extract_data(status_resp)
    result[:loadavg] = status_data[:loadavg]&.map(&:to_f)
    result[:kernel] ||= extract_kernel_version(status_data[:kversion])
    if status_data[:swap]
      result[:swap_used] = status_data[:swap][:used]
      result[:swap_total] = status_data[:swap][:total]
    end
  rescue StandardError
    # Ignore errors fetching status
  end

  # Fetch network (for IP)
  result[:ip] = ip_for(node_name)

  result
end

#disks_for(node_name) ⇒ Hash

Fetches physical disks for a node.

Parameters:

  • node name

Returns:

  • disks data (raw hashes, not models - used in describe)



391
392
393
394
395
396
# File 'lib/pvectl/repositories/node.rb', line 391

def disks_for(node_name)
  resp = connection.client["nodes/#{node_name}/disks/list"].get
  { physical_disks: unwrap(resp) }
rescue StandardError
  { physical_disks: [] }
end

#dns_for(node_name) ⇒ Hash

Fetches DNS configuration for a node.

Parameters:

  • node name

Returns:

  • DNS data



335
336
337
338
339
340
341
# File 'lib/pvectl/repositories/node.rb', line 335

def dns_for(node_name)
  resp = connection.client["nodes/#{node_name}/dns"].get
  data = extract_data(resp)
  { dns: data }
rescue StandardError
  { dns: nil }
end

#extended_status_for(node_name) ⇒ Hash

Fetches extended status (cpuinfo, boot_info, rootfs, network_interfaces).

Parameters:

  • node name

Returns:

  • extended status data



439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/pvectl/repositories/node.rb', line 439

def extended_status_for(node_name)
  resp = connection.client["nodes/#{node_name}/status"].get
  data = extract_data(resp)
  {
    cpuinfo: data[:cpuinfo],
    boot_info: data[:"boot-info"],
    rootfs: data[:rootfs],
    network_interfaces: network_interfaces_for(node_name)
  }
rescue StandardError
  {}
end

#extract_ip_from_network(interfaces) ⇒ String?

Extracts IP from network interfaces.

Algorithm (KISS):

  1. Find first interface with non-empty gateway field
  2. Extract IP from address field
  3. Remove CIDR suffix if present (e.g., "192.168.1.10/24" -> "192.168.1.10")

Parameters:

  • network interfaces from API

Returns:

  • IP address or nil



250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/pvectl/repositories/node.rb', line 250

def extract_ip_from_network(interfaces)
  return nil if interfaces.nil? || interfaces.empty?

  # Find first interface with gateway (default route interface)
  iface = interfaces.find { |i| i[:gateway] && !i[:gateway].to_s.empty? }
  return nil unless iface

  # Extract IP, removing CIDR suffix if present
  address = iface[:address] || iface[:cidr]
  return nil if address.nil? || address.to_s.empty?

  address.to_s.split("/").first
end

#extract_kernel_version(kversion) ⇒ String?

Extracts kernel version from kversion string.

The kversion string from Proxmox API looks like:

"Linux 6.8.12-1-pve #1 SMP PREEMPT_DYNAMIC..."

This method extracts just the version: "6.8.12-1-pve"

Parameters:

  • full kernel version string

Returns:

  • extracted kernel version or nil



272
273
274
275
276
277
278
# File 'lib/pvectl/repositories/node.rb', line 272

def extract_kernel_version(kversion)
  return nil if kversion.nil? || kversion.empty?

  # Match pattern: "Linux X.Y.Z-something ..."
  match = kversion.match(/Linux\s+([\d.]+-[\w.-]+)/)
  match ? match[1] : kversion
end

#fetch_config(node_name) ⇒ Hash

Fetches configuration for a node.

Parameters:

  • node name

Returns:

  • node configuration



105
106
107
108
109
110
# File 'lib/pvectl/repositories/node.rb', line 105

def fetch_config(node_name)
  resp = connection.client["nodes/#{node_name}/config"].get
  extract_data(resp)
rescue StandardError
  {}
end

#firewall_for(node_name) ⇒ Hash

Fetches firewall configuration (options, rules, aliases, IP sets).

Parameters:

  • node name

Returns:

  • firewall data under :firewall key



467
468
469
470
471
472
473
474
475
476
# File 'lib/pvectl/repositories/node.rb', line 467

def firewall_for(node_name)
  base = "nodes/#{node_name}/firewall"
  options = (extract_data(connection.client["#{base}/options"].get) rescue {})
  rules = (unwrap(connection.client["#{base}/rules"].get) rescue [])
  aliases_data = (unwrap(connection.client["#{base}/aliases"].get) rescue [])
  ipset = (unwrap(connection.client["#{base}/ipset"].get) rescue [])
  { firewall: { options: options, rules: rules, aliases: aliases_data, ipset: ipset } }
rescue StandardError
  {}
end

#get(name, include_details: false) ⇒ Models::Node?

Gets a single node by name.

Parameters:

  • node name

  • (defaults to: false)

    fetch version/status details

  • (defaults to: false)

Returns:

  • Node model or nil if not found



67
68
69
# File 'lib/pvectl/repositories/node.rb', line 67

def get(name, include_details: false)
  list(include_details: include_details).find { |n| n.name == name }
end

#guest_counts_for_clusterHash

Fetches guest counts per node from cluster/resources.

Returns:

  • { "node_name" => { vms: N, cts: M } }



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/pvectl/repositories/node.rb', line 171

def guest_counts_for_cluster
  response = connection.client["cluster/resources"].get(params: { type: "vm" })
  resources = unwrap(response)

  counts = Hash.new { |h, k| h[k] = { vms: 0, cts: 0 } }
  resources.each do |r|
    node = r[:node]
    next if node.nil?

    if r[:type] == "qemu"
      counts[node][:vms] += 1
    elsif r[:type] == "lxc"
      counts[node][:cts] += 1
    end
  end
  counts
end

#ip_for(node_name) ⇒ String?

Fetches IP address from node network configuration.

Finds the first interface with a gateway configured (default route) and extracts its IP address.

Parameters:

  • node name

Returns:

  • IP address or nil if unavailable



233
234
235
236
237
238
239
# File 'lib/pvectl/repositories/node.rb', line 233

def ip_for(node_name)
  network_resp = connection.client["nodes/#{node_name}/network"].get
  interfaces = unwrap(network_resp)
  extract_ip_from_network(interfaces)
rescue StandardError
  nil
end

#list(include_details: false) ⇒ Array<Models::Node>

Lists all nodes in the cluster.

Parameters:

  • (defaults to: false)

    fetch version/status details (extra API calls)

  • (defaults to: false)

Returns:

  • collection of Node models



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/pvectl/repositories/node.rb', line 37

def list(include_details: false)
  response = connection.client["nodes"].get
  nodes_data = unwrap(response)

  # Get guest counts from cluster/resources
  guest_counts = guest_counts_for_cluster

  nodes_data.map do |data|
    node_name = data[:node] || data[:name]

    # Merge guest counts
    data = data.merge(
      guests_vms: guest_counts.dig(node_name, :vms) || 0,
      guests_cts: guest_counts.dig(node_name, :cts) || 0
    )

    # Fetch extended details if requested
    if include_details && (data[:status] == "online")
      data = data.merge(details_for(node_name))
    end

    build_model(data)
  end
end

#network_interfaces_for(node_name) ⇒ Array<Hash>

Fetches network interfaces for a node.

Parameters:

  • node name

Returns:

  • network interfaces (raw hashes for describe output)



456
457
458
459
460
461
# File 'lib/pvectl/repositories/node.rb', line 456

def network_interfaces_for(node_name)
  resp = connection.client["nodes/#{node_name}/network"].get
  unwrap(resp)
rescue StandardError
  []
end

#qemu_cpu_for(node_name) ⇒ Hash

Fetches QEMU CPU models for a node.

Parameters:

  • node name

Returns:

  • QEMU CPU models



402
403
404
405
406
407
# File 'lib/pvectl/repositories/node.rb', line 402

def qemu_cpu_for(node_name)
  resp = connection.client["nodes/#{node_name}/capabilities/qemu/cpu"].get
  { qemu_cpu_models: unwrap(resp) }
rescue StandardError
  { qemu_cpu_models: [] }
end

#qemu_machines_for(node_name) ⇒ Hash

Fetches QEMU machine types for a node.

Parameters:

  • node name

Returns:

  • QEMU machine types



413
414
415
416
417
418
# File 'lib/pvectl/repositories/node.rb', line 413

def qemu_machines_for(node_name)
  resp = connection.client["nodes/#{node_name}/capabilities/qemu/machines"].get
  { qemu_machines: unwrap(resp) }
rescue StandardError
  { qemu_machines: [] }
end

#services_for(node_name) ⇒ Hash

Fetches services for a node.

Parameters:

  • node name

Returns:

  • services data (raw hashes, not models - used in describe)



359
360
361
362
363
364
# File 'lib/pvectl/repositories/node.rb', line 359

def services_for(node_name)
  resp = connection.client["nodes/#{node_name}/services"].get
  { services: unwrap(resp) }
rescue StandardError
  { services: [] }
end

#storage_pools_for(node_name) ⇒ Hash

Fetches storage pools for a node.

Delegates to Repositories::Storage#list_for_node for consistent model creation and DRY compliance.

Parameters:

  • node name

Returns:



373
374
375
376
377
# File 'lib/pvectl/repositories/node.rb', line 373

def storage_pools_for(node_name)
  { storage_pools: storage_repository.list_for_node(node_name) }
rescue StandardError
  { storage_pools: [] }
end

#storage_repositoryRepositories::Storage

Returns storage repository instance. Uses injected repository if provided, otherwise creates new one.

Returns:

  • storage repository



383
384
385
# File 'lib/pvectl/repositories/node.rb', line 383

def storage_repository
  @storage_repository ||= Repositories::Storage.new(connection)
end

#subscription_for(node_name) ⇒ Hash

Fetches subscription info for a node.

Filters sensitive fields (license key) to prevent accidental exposure. Only safe display data is returned.

Parameters:

  • node name

Returns:

  • subscription data (filtered for safety)



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/pvectl/repositories/node.rb', line 315

def subscription_for(node_name)
  resp = connection.client["nodes/#{node_name}/subscription"].get
  data = extract_data(resp)
  # Filter sensitive fields - keep only safe display data
  safe_data = {
    status: data[:status],
    level: data[:level],
    productname: data[:productname],
    regdate: data[:regdate],
    checktime: data[:checktime]
  }
  { subscription: safe_data }
rescue StandardError
  { subscription: nil }
end

#tasks_for(node_name, limit: 10) ⇒ Hash

Fetches recent task history for the node.

Parameters:

  • node name

  • (defaults to: 10)

    max entries (default 10)

  • (defaults to: 10)

Returns:

  • tasks data under :tasks key



483
484
485
486
487
488
# File 'lib/pvectl/repositories/node.rb', line 483

def tasks_for(node_name, limit: 10)
  task_list_repo = TaskList.new(connection)
  { tasks: task_list_repo.list(node: node_name, limit: limit) }
rescue StandardError
  { tasks: [] }
end

#time_for(node_name) ⇒ Hash

Fetches time configuration for a node.

Parameters:

  • node name

Returns:

  • time data



347
348
349
350
351
352
353
# File 'lib/pvectl/repositories/node.rb', line 347

def time_for(node_name)
  resp = connection.client["nodes/#{node_name}/time"].get
  data = extract_data(resp)
  { time_info: data }
rescue StandardError
  { time_info: nil }
end

#update(node_name, params = {}) ⇒ void

This method returns an undefined value.

Updates node configuration.

Parameters:

  • node name

  • (defaults to: {})

    configuration parameters to update



117
118
119
# File 'lib/pvectl/repositories/node.rb', line 117

def update(node_name, params = {})
  connection.client["nodes/#{node_name}/config"].put(params)
end

#updates_for(node_name) ⇒ Hash

Fetches available updates for a node.

Parameters:

  • node name

Returns:

  • updates data



424
425
426
427
428
429
430
431
432
433
# File 'lib/pvectl/repositories/node.rb', line 424

def updates_for(node_name)
  resp = connection.client["nodes/#{node_name}/apt/versions"].get
  packages = unwrap(resp)
  upgradable = packages.select do |p|
    p[:AvailableVersion] && p[:AvailableVersion] != p[:CurrentVersion]
  end
  { updates_available: upgradable.size, updates: upgradable }
rescue StandardError
  { updates_available: 0, updates: [] }
end

#wakeonlan(node_name) ⇒ String?

Sends a Wake-on-LAN packet to a node.

Calls POST /nodes/{node}/wakeonlan. The target node must have its MAC address registered in the cluster configuration beforehand (via pvecm or the web UI). The API returns the MAC address used for the magic packet.

Parameters:

  • cluster node name

Returns:

  • MAC address used to assemble the WoL packet

Raises:

  • propagates any API error (e.g., missing MAC)



131
132
133
134
135
# File 'lib/pvectl/repositories/node.rb', line 131

def wakeonlan(node_name)
  response = connection.client["nodes/#{node_name}/wakeonlan"].post
  data = extract_data(response)
  data.is_a?(String) ? data : nil
end