Class: Pvectl::Presenters::Node

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

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 Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#format_bytes, #format_firewall, #format_firewall_rules, #format_task_history, #tags_array, #tags_display, #template_display, #to_wide_row, #wide_columns

Instance Attribute Details

#nodeModels::Node (readonly) Also known as: resource



524
525
526
# File 'lib/pvectl/presenters/node.rb', line 524

def node
  @node
end

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


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.



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

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

#boot_modeString

Returns boot mode.



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.



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).



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.



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.



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.



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.



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.



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.



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.



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.



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.



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

#format_cpu_models(models) ⇒ Array<Hash>, String

Formats CPU models list as a table.



637
638
639
640
641
642
643
# File 'lib/pvectl/presenters/node.rb', line 637

def format_cpu_models(models)
  return "-" if models.nil? || models.empty?

  models.map do |m|
    { "Name" => m[:name] || "-", "Vendor" => m[:vendor] || "-" }
  end
end

#format_gib(bytes) ⇒ String

Formats bytes to GiB string.



560
561
562
563
564
# File 'lib/pvectl/presenters/node.rb', line 560

def format_gib(bytes)
  return "-" if bytes.nil?

  "#{(bytes.to_f / 1024 / 1024 / 1024).round(1)} GiB"
end

#format_machines(machines) ⇒ Array<Hash>, String

Formats machine types list as a table.



649
650
651
652
653
654
655
# File 'lib/pvectl/presenters/node.rb', line 649

def format_machines(machines)
  return "-" if machines.nil? || machines.empty?

  machines.map do |m|
    { "ID" => m[:id] || "-", "Type" => m[:type] || "-" }
  end
end

#format_network_interfaces(interfaces) ⇒ Array<Hash>, String

Formats network interfaces for table display.



570
571
572
573
574
575
576
577
578
579
580
581
# File 'lib/pvectl/presenters/node.rb', line 570

def format_network_interfaces(interfaces)
  return "-" if interfaces.nil? || interfaces.empty?

  interfaces.map do |iface|
    {
      "Name" => iface[:iface],
      "Type" => iface[:type],
      "Address" => iface[:address] || iface[:cidr] || "-",
      "Gateway" => iface[:gateway] || "-"
    }
  end
end

#format_physical_disks(disks) ⇒ Array<Hash>, String

Formats physical disks for table display.



618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/pvectl/presenters/node.rb', line 618

def format_physical_disks(disks)
  return "-" if disks.nil? || disks.empty?

  disks.map do |disk|
    size_gb = disk[:size] ? (disk[:size].to_f / 1024 / 1024 / 1024).round(1) : 0
    {
      "Device" => disk[:devpath],
      "Model" => disk[:model] || "-",
      "Size" => format_storage_size(size_gb),
      "Type" => disk[:type] || "-",
      "Health" => disk[:health] || "-"
    }
  end
end

#format_services(services) ⇒ Array<Hash>, String

Formats services for table display.



587
588
589
590
591
592
593
594
595
596
597
# File 'lib/pvectl/presenters/node.rb', line 587

def format_services(services)
  return "-" if services.nil? || services.empty?

  services.map do |svc|
    {
      "Name" => svc[:service] || svc[:name],
      "State" => svc[:state],
      "Description" => svc[:desc] || "-"
    }
  end
end

#format_storage_pool(pool) ⇒ Hash

Formats a single storage pool for display. Supports both Models::Storage and Hash formats.



705
706
707
708
709
710
711
712
713
# File 'lib/pvectl/presenters/node.rb', line 705

def format_storage_pool(pool)
  if pool.respond_to?(:name)
    # Models::Storage instance - use Storage presenter for display
    format_storage_pool_model(pool)
  else
    # Hash format (legacy)
    format_storage_pool_hash(pool)
  end
end

#format_storage_pool_hash(pool) ⇒ Hash

Formats a storage pool from Hash data (legacy format).



737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
# File 'lib/pvectl/presenters/node.rb', line 737

def format_storage_pool_hash(pool)
  total_gb = pool[:total] ? (pool[:total].to_f / 1024 / 1024 / 1024).round(1) : 0
  used_gb = pool[:used] ? (pool[:used].to_f / 1024 / 1024 / 1024).round(1) : 0
  avail_gb = pool[:avail] ? (pool[:avail].to_f / 1024 / 1024 / 1024).round(1) : 0
  usage_pct = pool[:total] && pool[:total] > 0 ? ((pool[:used].to_f / pool[:total]) * 100).round : 0

  {
    "Name" => pool[:storage],
    "Type" => pool[:type],
    "Total" => format_storage_size(total_gb),
    "Used" => format_storage_size(used_gb),
    "Available" => format_storage_size(avail_gb),
    "Usage" => "#{usage_pct}%"
  }
end

#format_storage_pool_model(pool) ⇒ Hash

Formats a storage pool from Models::Storage instance. Uses Storage presenter for display formatting.



720
721
722
723
724
725
726
727
728
729
730
731
# File 'lib/pvectl/presenters/node.rb', line 720

def format_storage_pool_model(pool)
  storage_presenter = Storage.new
  storage_presenter.to_row(pool)  # Set up @storage in presenter
  {
    "Name" => pool.name,
    "Type" => storage_presenter.type_display,
    "Total" => storage_presenter.total_display,
    "Used" => storage_presenter.used_display,
    "Available" => storage_presenter.avail_display,
    "Usage" => storage_presenter.usage_display
  }
end

#format_storage_pools(pools) ⇒ Array<Hash>, String

Formats storage pools for table display.

Supports both Models::Storage instances (new format) and Hash (legacy).



605
606
607
608
609
610
611
612
# File 'lib/pvectl/presenters/node.rb', line 605

def format_storage_pools(pools)
  return "-" if pools.nil? || pools.empty?

  # Handle both Models::Storage and Hash formats
  pools.select { |p| storage_enabled?(p) }.map do |pool|
    format_storage_pool(pool)
  end
end

#format_storage_size(gb) ⇒ String

Formats storage size with appropriate unit.



757
758
759
# File 'lib/pvectl/presenters/node.rb', line 757

def format_storage_size(gb)
  gb >= 1024 ? "#{(gb / 1024).round(1)} TiB" : "#{gb} GiB"
end

#has_alerts?Boolean

Checks if node has any alerts.



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

def has_alerts?
  !alerts.empty?
end

#ip_displayString

Returns IP address for display.



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

def ip_display
  node.ip || "-"
end

#kernel_displayString

Returns kernel display.



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

def kernel_display
  node.kernel || "-"
end

#load_1mFloat?

Returns 1-minute load average.



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.



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.



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".



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_percent(model) ⇒ Float?

Calculates memory usage percentage.



661
662
663
664
665
# File 'lib/pvectl/presenters/node.rb', line 661

def memory_percent(model)
  return nil if model.maxmem.nil? || model.maxmem.zero? || model.mem.nil?

  ((model.mem.to_f / model.maxmem) * 100).round(1)
end

#memory_percent_display(model) ⇒ String

Returns memory percentage for display.



542
543
544
545
# File 'lib/pvectl/presenters/node.rb', line 542

def memory_percent_display(model)
  pct = memory_percent(model)
  pct ? "#{pct.round}%" : "-"
end

#memory_total_gbInteger?

Returns total memory in GB.



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.



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

#offline_descriptionHash

Returns description for offline nodes.



530
531
532
533
534
535
536
# File 'lib/pvectl/presenters/node.rb', line 530

def offline_description
  {
    "Name" => node.name,
    "Status" => node.status,
    "Note" => node.offline_note || "Node is offline. Detailed metrics unavailable."
  }
end

#rootfs_displayString

Returns rootfs display.



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.



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.



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

#storage_enabled?(pool) ⇒ Boolean

Checks if storage pool is enabled. Supports both Models::Storage and Hash formats.



692
693
694
695
696
697
698
# File 'lib/pvectl/presenters/node.rb', line 692

def storage_enabled?(pool)
  if pool.respond_to?(:enabled?)
    pool.enabled?
  else
    pool[:enabled] != 0
  end
end

#storage_percent(model) ⇒ Float?

Calculates storage usage percentage.



681
682
683
684
685
# File 'lib/pvectl/presenters/node.rb', line 681

def storage_percent(model)
  return nil if model.maxdisk.nil? || model.maxdisk.zero? || model.disk.nil?

  ((model.disk.to_f / model.maxdisk) * 100).round(1)
end

#subscription_displayString

Returns subscription status display.



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".



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_percent(model) ⇒ Float?

Calculates swap usage percentage.



671
672
673
674
675
# File 'lib/pvectl/presenters/node.rb', line 671

def swap_percent(model)
  return nil if model.swap_total.nil? || model.swap_total.zero? || model.swap_used.nil?

  ((model.swap_used.to_f / model.swap_total) * 100).round(1)
end

#swap_percent_display(model) ⇒ String

Returns swap percentage for display.



551
552
553
554
# File 'lib/pvectl/presenters/node.rb', line 551

def swap_percent_display(model)
  pct = swap_percent(model)
  pct ? "#{pct.round}%" : "-"
end

#swap_total_gbInteger?

Returns total swap in GB.



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.



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.



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.



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.



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.



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.



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.



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

def version_display
  node.version || "-"
end