Class: Pvectl::Presenters::Node

Inherits:
Base
  • Object
show all
Defined in:
lib/pvectl/presenters/node.rb

Overview

Presenter for Proxmox cluster nodes.

Defines column layout and formatting for table output. Standard columns show essential node info. Wide columns add detailed metrics (load, swap, storage, kernel).

Examples:

Using with formatter

presenter = Node.new
formatter = Formatters::Table.new
output = formatter.format(nodes, presenter)

See Also:

Direct Known Subclasses

TopNode

Instance Method Summary collapse

Methods inherited from Base

#tags_array, #tags_display, #template_display, #to_wide_row, #wide_columns

Instance Method Details

#alertsArray<String>

Returns array of alert messages for this node.

Alerts are generated based on thresholds:

  • CPU >= 90%: critical

  • CPU >= 80%: warning

  • Memory >= 90%: critical

  • Memory >= 80%: warning

  • Status offline: always alert

Returns:

  • (Array<String>)

    list of alert messages



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/pvectl/presenters/node.rb', line 379

def alerts
  result = []
  result << "Node offline" if node.offline?

  if node.online?
    cpu_pct = node.cpu.nil? ? 0 : (node.cpu * 100).round
    mem_pct = (node.maxmem.nil? || node.maxmem.zero?) ? 0 : ((node.mem.to_f / node.maxmem) * 100).round

    result << "CPU critical (#{cpu_pct}%)" if cpu_pct >= 90
    result << "CPU warning (#{cpu_pct}%)" if cpu_pct >= 80 && cpu_pct < 90
    result << "Memory critical (#{mem_pct}%)" if mem_pct >= 90
    result << "Memory warning (#{mem_pct}%)" if mem_pct >= 80 && mem_pct < 90
  end

  result
end

#alerts_displayString

Returns alerts as comma-separated string for display.

Returns:

  • (String)

    alerts (e.g., “CPU critical (92%), Memory warning”) or “-” if none



399
400
401
# File 'lib/pvectl/presenters/node.rb', line 399

def alerts_display
  alerts.empty? ? "-" : alerts.join(", ")
end

#boot_modeString

Returns boot mode.

Returns:

  • (String)

    “UEFI” or “BIOS” or “-”



434
435
436
437
438
439
440
441
# File 'lib/pvectl/presenters/node.rb', line 434

def boot_mode
  mode = node.boot_info&.dig(:mode)
  case mode
  when "efi" then "UEFI"
  when "bios" then "BIOS"
  else "-"
  end
end

#columnsArray<String>

Returns column headers for standard table output.

Returns:

  • (Array<String>)

    column headers



23
24
25
# File 'lib/pvectl/presenters/node.rb', line 23

def columns
  %w[NAME STATUS VERSION CPU MEMORY GUESTS UPTIME]
end

#cpu_coresInteger?

Returns CPU core count (total).

Returns:

  • (Integer, nil)

    core count



427
428
429
# File 'lib/pvectl/presenters/node.rb', line 427

def cpu_cores
  node.cpuinfo&.dig(:cores)
end

#cpu_modelString?

Returns CPU model name.

Returns:

  • (String, nil)

    CPU model



413
414
415
# File 'lib/pvectl/presenters/node.rb', line 413

def cpu_model
  node.cpuinfo&.dig(:model)
end

#cpu_percentString

Returns CPU usage as percentage string.

Returns:

  • (String)

    CPU percentage (e.g., “23%”) or “-” if offline/unavailable



208
209
210
211
212
# File 'lib/pvectl/presenters/node.rb', line 208

def cpu_percent
  return "-" if node.offline? || node.cpu.nil?

  "#{(node.cpu * 100).round}%"
end

#cpu_socketsInteger?

Returns CPU socket count.

Returns:

  • (Integer, nil)

    socket count



420
421
422
# File 'lib/pvectl/presenters/node.rb', line 420

def cpu_sockets
  node.cpuinfo&.dig(:sockets)
end

#disk_total_gbFloat?

Returns total disk in GB.

Returns:

  • (Float, nil)

    total disk in GB, or nil if unavailable



254
255
256
257
258
# File 'lib/pvectl/presenters/node.rb', line 254

def disk_total_gb
  return nil if node.maxdisk.nil?

  (node.maxdisk.to_f / 1024 / 1024 / 1024).round(1)
end

#disk_used_gbFloat?

Returns disk used in GB.

Returns:

  • (Float, nil)

    disk used in GB, or nil if unavailable



245
246
247
248
249
# File 'lib/pvectl/presenters/node.rb', line 245

def disk_used_gb
  return nil if node.disk.nil?

  (node.disk.to_f / 1024 / 1024 / 1024).round(1)
end

#dns_nameserversString

Returns DNS nameservers.

Returns:

  • (String)

    comma-separated nameservers or “-”



490
491
492
493
# File 'lib/pvectl/presenters/node.rb', line 490

def dns_nameservers
  servers = [node.dns&.dig(:dns1), node.dns&.dig(:dns2), node.dns&.dig(:dns3)].compact
  servers.empty? ? "-" : servers.join(", ")
end

#dns_searchString

Returns DNS search domain.

Returns:

  • (String)

    search domain or “-”



483
484
485
# File 'lib/pvectl/presenters/node.rb', line 483

def dns_search
  node.dns&.dig(:search) || "-"
end

#extra_columnsArray<String>

Returns additional column headers for wide output.

Returns:

  • (Array<String>)

    extra column headers



30
31
32
# File 'lib/pvectl/presenters/node.rb', line 30

def extra_columns
  %w[LOAD SWAP STORAGE VMS CTS KERNEL IP]
end

#extra_values(model, **_context) ⇒ Array<String>

Returns additional values for wide output.

Parameters:

  • model (Models::Node)

    Node model

  • context (Hash)

    optional context

Returns:

  • (Array<String>)

    extra values matching extra_columns order



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/pvectl/presenters/node.rb', line 57

def extra_values(model, **_context)
  @node = model
  [
    load_display,
    swap_display,
    storage_display,
    node.guests_vms.to_s,
    node.guests_cts.to_s,
    kernel_display,
    ip_display
  ]
end

#has_alerts?Boolean

Checks if node has any alerts.

Returns:

  • (Boolean)

    true if alerts exist



406
407
408
# File 'lib/pvectl/presenters/node.rb', line 406

def has_alerts?
  !alerts.empty?
end

#ip_displayString

Returns IP address for display.

Returns:

  • (String)

    IP address or “-” if unavailable



365
366
367
# File 'lib/pvectl/presenters/node.rb', line 365

def ip_display
  node.ip || "-"
end

#kernel_displayString

Returns kernel display.

Returns:

  • (String)

    kernel version or “-” if unavailable



358
359
360
# File 'lib/pvectl/presenters/node.rb', line 358

def kernel_display
  node.kernel || "-"
end

#load_1mFloat?

Returns 1-minute load average.

Returns:

  • (Float, nil)

    1-minute load average, or nil if unavailable



313
314
315
316
317
# File 'lib/pvectl/presenters/node.rb', line 313

def load_1m
  return nil if node.loadavg.nil? || node.loadavg.empty?

  node.loadavg[0]
end

#load_displayString

Returns load average display with high-load indicator.

Returns:

  • (String)

    load average (e.g., “0.45” or “2.31u2191”) or “-” if offline



322
323
324
325
326
327
# File 'lib/pvectl/presenters/node.rb', line 322

def load_display
  return "-" if node.offline? || load_1m.nil?

  load = load_1m.round(2)
  load > 2.0 ? "#{load}\u2191" : load.to_s
end

#local_timeString

Returns local time formatted.

Returns:

  • (String)

    local time or “-”



473
474
475
476
477
478
# File 'lib/pvectl/presenters/node.rb', line 473

def local_time
  localtime = node.time_info&.dig(:localtime)
  return "-" if localtime.nil?

  Time.at(localtime).strftime("%Y-%m-%d %H:%M:%S")
end

#memory_displayString

Returns memory formatted as “used/total GB”.

Returns:

  • (String)

    formatted memory (e.g., “45.2/128 GB”) or “-” if offline



235
236
237
238
239
240
# File 'lib/pvectl/presenters/node.rb', line 235

def memory_display
  return "-" if node.offline? || memory_total_gb.nil?
  return "-/#{memory_total_gb} GB" if memory_used_gb.nil?

  "#{memory_used_gb}/#{memory_total_gb} GB"
end

#memory_total_gbInteger?

Returns total memory in GB.

Returns:

  • (Integer, nil)

    total memory in GB, or nil if unavailable



226
227
228
229
230
# File 'lib/pvectl/presenters/node.rb', line 226

def memory_total_gb
  return nil if node.maxmem.nil?

  (node.maxmem.to_f / 1024 / 1024 / 1024).round(0)
end

#memory_used_gbFloat?

Returns memory used in GB.

Returns:

  • (Float, nil)

    memory used in GB, or nil if unavailable



217
218
219
220
221
# File 'lib/pvectl/presenters/node.rb', line 217

def memory_used_gb
  return nil if node.mem.nil?

  (node.mem.to_f / 1024 / 1024 / 1024).round(1)
end

#rootfs_displayString

Returns rootfs display.

Returns:

  • (String)

    e.g., “30% (1.2/4.0 TiB)”



507
508
509
510
511
512
513
514
515
516
517
518
519
# File 'lib/pvectl/presenters/node.rb', line 507

def rootfs_display
  return "-" if node.rootfs.nil?

  used_gb = (node.rootfs[:used].to_f / 1024 / 1024 / 1024).round(1)
  total_gb = (node.rootfs[:total].to_f / 1024 / 1024 / 1024).round(1)
  pct = rootfs_usage_percent || 0

  if total_gb >= 1024
    "#{pct}% (#{(used_gb / 1024).round(1)}/#{(total_gb / 1024).round(1)} TiB)"
  else
    "#{pct}% (#{used_gb}/#{total_gb} GiB)"
  end
end

#rootfs_usage_percentInteger?

Returns rootfs usage percentage.

Returns:

  • (Integer, nil)

    percentage



498
499
500
501
502
# File 'lib/pvectl/presenters/node.rb', line 498

def rootfs_usage_percent
  return nil if node.rootfs.nil? || node.rootfs[:total].nil? || node.rootfs[:total].zero?

  ((node.rootfs[:used].to_f / node.rootfs[:total]) * 100).round
end

#storage_displayString

Returns disk formatted with appropriate unit (GB or TB).

Uses GB for disks under 1 TB, TB for larger disks. This provides better readability for smaller storage.

Returns:

  • (String)

    formatted disk (e.g., “85/100 GB” or “1.2/4.0 TB”) or “-” if offline



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/pvectl/presenters/node.rb', line 266

def storage_display
  return "-" if node.offline? || node.maxdisk.nil?

  total_gb = disk_total_gb
  used_gb = disk_used_gb || 0.0

  if total_gb >= 1024
    # Use TB for disks >= 1 TB
    used_tb = (used_gb / 1024).round(1)
    total_tb = (total_gb / 1024).round(1)
    "#{used_tb}/#{total_tb} TB"
  else
    # Use GB for smaller disks
    "#{used_gb.round(0).to_i}/#{total_gb.round(0).to_i} GB"
  end
end

#subscription_displayString

Returns subscription status display.

Returns:

  • (String)

    e.g., “Active (Community)” or “Inactive”



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
# File 'lib/pvectl/presenters/node.rb', line 446

def subscription_display
  return "-" if node.subscription.nil?

  status = node.subscription[:status]
  level = node.subscription[:level]

  level_name = case level
               when "c" then "Community"
               when "b" then "Basic"
               when "s" then "Standard"
               when "p" then "Premium"
               else level
               end

  status == "Active" ? "Active (#{level_name})" : "Inactive"
end

#swap_displayString

Returns swap formatted as “used/total GB”.

Returns:

  • (String)

    formatted swap (e.g., “0.0/8 GB”) or “-” if offline



304
305
306
307
308
# File 'lib/pvectl/presenters/node.rb', line 304

def swap_display
  return "-" if node.offline? || swap_total_gb.nil?

  "#{swap_used_gb}/#{swap_total_gb} GB"
end

#swap_total_gbInteger?

Returns total swap in GB.

Returns:

  • (Integer, nil)

    total swap in GB, or nil if unavailable



295
296
297
298
299
# File 'lib/pvectl/presenters/node.rb', line 295

def swap_total_gb
  return nil if node.swap_total.nil?

  (node.swap_total.to_f / 1024 / 1024 / 1024).round(0)
end

#swap_used_gbFloat?

Returns swap used in GB.

Returns:

  • (Float, nil)

    swap used in GB, or nil if unavailable



286
287
288
289
290
# File 'lib/pvectl/presenters/node.rb', line 286

def swap_used_gb
  return nil if node.swap_used.nil?

  (node.swap_used.to_f / 1024 / 1024 / 1024).round(1)
end

#timezoneString

Returns timezone.

Returns:

  • (String)

    timezone or “-”



466
467
468
# File 'lib/pvectl/presenters/node.rb', line 466

def timezone
  node.time_info&.dig(:timezone) || "-"
end

#to_description(model) ⇒ Hash

Converts Node model to description format for describe command.

Returns a structured Hash with sections for kubectl-style vertical output. Nested Hashes create indented subsections. Arrays of Hashes render as inline tables.

Parameters:

Returns:

  • (Hash)

    structured hash for describe formatter



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/pvectl/presenters/node.rb', line 134

def to_description(model)
  @node = model
  return offline_description if node.offline?

  {
    "Name" => node.name,
    "Status" => node.status,
    "Subscription" => subscription_display,
    "System" => {
      "Version" => version_display,
      "Kernel" => kernel_display,
      "Boot Mode" => boot_mode,
      "Uptime" => uptime_human
    },
    "CPU" => {
      "Model" => cpu_model || "-",
      "Cores" => cpu_cores || node.maxcpu || "-",
      "Sockets" => cpu_sockets || "-",
      "Usage" => cpu_percent
    },
    "Memory" => {
      "Usage" => memory_percent_display(node),
      "Used" => format_gib(node.mem),
      "Total" => format_gib(node.maxmem)
    },
    "Swap" => {
      "Usage" => swap_percent_display(node),
      "Used" => format_gib(node.swap_used),
      "Total" => format_gib(node.swap_total)
    },
    "Load Average" => {
      "1 min" => node.loadavg&.dig(0)&.round(2) || "-",
      "5 min" => node.loadavg&.dig(1)&.round(2) || "-",
      "15 min" => node.loadavg&.dig(2)&.round(2) || "-"
    },
    "Root Filesystem" => rootfs_display,
    "Network Interfaces" => format_network_interfaces(node.network_interfaces),
    "DNS" => {
      "Search" => dns_search,
      "Nameservers" => dns_nameservers
    },
    "Time" => {
      "Timezone" => timezone,
      "Local Time" => local_time
    },
    "Services" => format_services(node.services),
    "Storage Pools" => format_storage_pools(node.storage_pools),
    "Physical Disks" => format_physical_disks(node.physical_disks),
    "Capabilities" => {
      "QEMU CPU Models" => format_cpu_models(node.qemu_cpu_models),
      "QEMU Machines" => format_machines(node.qemu_machines)
    },
    "Guests" => {
      "VMs" => node.guests_vms,
      "Containers" => node.guests_cts,
      "Total" => node.guests_total
    },
    "Updates" => {
      "Available" => "#{node.updates_available} packages"
    },
    "Firewall" => format_firewall(node.firewall),
    "Firewall Rules" => format_firewall_rules(node.firewall),
    "Task History" => format_task_history(node.tasks),
    "Alerts" => alerts_display
  }
end

#to_hash(model) ⇒ Hash

Converts Node model to hash for JSON/YAML output.

Returns a structured hash with nested objects for complex data like CPU, memory, disk, uptime, swap, load, and guests.

Parameters:

Returns:

  • (Hash)

    hash representation with string keys



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/pvectl/presenters/node.rb', line 77

def to_hash(model)
  @node = model
  {
    "name" => node.name,
    "status" => node.status,
    "version" => node.version,
    "kernel" => node.kernel,
    "cpu" => {
      "usage_percent" => node.cpu.nil? ? nil : (node.cpu * 100).round,
      "cores" => node.maxcpu
    },
    "memory" => {
      "used_bytes" => node.mem,
      "total_bytes" => node.maxmem,
      "used_gb" => memory_used_gb,
      "total_gb" => memory_total_gb,
      "usage_percent" => memory_percent(node)
    },
    "swap" => {
      "used_bytes" => node.swap_used,
      "total_bytes" => node.swap_total,
      "usage_percent" => swap_percent(node)
    },
    "storage" => {
      "used_bytes" => node.disk,
      "total_bytes" => node.maxdisk,
      "usage_percent" => storage_percent(node)
    },
    "load" => {
      "avg1" => node.loadavg&.dig(0),
      "avg5" => node.loadavg&.dig(1),
      "avg15" => node.loadavg&.dig(2)
    },
    "guests" => {
      "total" => node.guests_total,
      "vms" => node.guests_vms,
      "cts" => node.guests_cts
    },
    "uptime" => {
      "seconds" => node.uptime,
      "human" => uptime_human
    },
    "alerts" => alerts,
    "network" => {
      "ip" => node.ip
    }
  }
end

#to_row(model, **_context) ⇒ Array<String>

Converts Node model to table row values.

Parameters:

  • model (Models::Node)

    Node model

  • context (Hash)

    optional context

Returns:

  • (Array<String>)

    row values matching columns order



39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/pvectl/presenters/node.rb', line 39

def to_row(model, **_context)
  @node = model
  [
    node.name,
    node.status,
    version_display,
    cpu_percent,
    memory_display,
    node.guests_total.to_s,
    uptime_human
  ]
end

#uptime_humanString

Returns uptime in human-readable format.

Returns:

  • (String)

    formatted uptime (e.g., “45d 3h”) or “-” if offline



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/pvectl/presenters/node.rb', line 332

def uptime_human
  return "-" if node.offline? || node.uptime.nil? || node.uptime.zero?

  days = node.uptime / 86_400
  hours = (node.uptime % 86_400) / 3600
  minutes = (node.uptime % 3600) / 60

  if days.positive?
    "#{days}d #{hours}h"
  elsif hours.positive?
    "#{hours}h #{minutes}m"
  else
    "#{minutes}m"
  end
end

#version_displayString

Returns version display.

Returns:

  • (String)

    version (e.g., “8.3.2”) or “-” if unavailable



351
352
353
# File 'lib/pvectl/presenters/node.rb', line 351

def version_display
  node.version || "-"
end