radfish 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.
@@ -0,0 +1,759 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'yaml'
5
+ require 'json'
6
+ require 'colorize'
7
+ require 'radfish'
8
+
9
+ module Radfish
10
+ class CLI < Thor
11
+ class_option :host, aliases: '-h', desc: 'BMC host/IP address (env: RADFISH_HOST)'
12
+ class_option :username, aliases: '-u', desc: 'BMC username (env: RADFISH_USERNAME)'
13
+ class_option :password, aliases: '-p', desc: 'BMC password (env: RADFISH_PASSWORD)'
14
+ class_option :config, aliases: '-c', desc: 'Config file path'
15
+ class_option :vendor, aliases: '-v', desc: 'Vendor (dell, supermicro, hpe, etc) - auto-detect if not specified (env: RADFISH_VENDOR)'
16
+ class_option :port, type: :numeric, default: 443, desc: 'BMC port (env: RADFISH_PORT)'
17
+ class_option :insecure, type: :boolean, default: true, desc: 'Skip SSL verification'
18
+ class_option :verbose, type: :boolean, default: false, desc: 'Enable verbose output'
19
+ class_option :json, type: :boolean, default: false, desc: 'Output in JSON format'
20
+
21
+ desc "detect", "Detect the vendor of a BMC"
22
+ def detect
23
+ with_connection(skip_login: true) do |opts|
24
+ vendor = Radfish.detect_vendor(
25
+ host: opts[:host],
26
+ username: opts[:username],
27
+ password: opts[:password],
28
+ port: opts[:port],
29
+ verify_ssl: !opts[:insecure]
30
+ )
31
+
32
+ output_result({ vendor: vendor }, vendor ? "Detected vendor: #{vendor}" : "Could not detect vendor")
33
+ end
34
+ end
35
+
36
+ desc "info", "Show BMC and system information"
37
+ def info
38
+ with_client do |client|
39
+ info = {
40
+ vendor: client.vendor_name,
41
+ adapter: client.adapter_class.name,
42
+ features: client.supported_features,
43
+ firmware_version: safe_call { client.get_firmware_version },
44
+ redfish_version: safe_call { client.redfish_version },
45
+ system: safe_call { client.system_info }
46
+ }
47
+
48
+ if options[:json]
49
+ puts JSON.pretty_generate(info)
50
+ else
51
+ puts "=== BMC Information ===".green
52
+ puts "Vendor: #{info[:vendor]}".cyan
53
+ puts "Adapter: #{info[:adapter]}".cyan
54
+ puts "Firmware: #{info[:firmware_version]}".cyan
55
+ puts "Redfish: #{info[:redfish_version]}".cyan
56
+ puts "Features: #{info[:features].join(', ')}".cyan
57
+
58
+ if info[:system]
59
+ puts "\n=== System Information ===".green
60
+ info[:system].each do |key, value|
61
+ puts "#{key.to_s.gsub('_', ' ').capitalize}: #{value}".cyan
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ # Power Commands
69
+ desc "power SUBCOMMAND", "Power management"
70
+ option :force, type: :boolean, default: false, desc: "Force power operation (skip graceful shutdown)"
71
+ def power(subcommand = 'status')
72
+ with_client do |client|
73
+ case subcommand
74
+ when 'status', 'state'
75
+ status = client.power_status
76
+ output_result({ power_status: status }, "Power Status: #{status}", status == 'On' ? :green : :yellow)
77
+ when 'on'
78
+ result = client.power_on
79
+ output_result({ success: result }, result ? "System powered on" : "Failed to power on")
80
+ when 'off'
81
+ result = client.power_off(force: options[:force])
82
+ mode = options[:force] ? "force" : "graceful"
83
+ output_result({ success: result }, result ? "System powered off (#{mode})" : "Failed to power off")
84
+ when 'force-off'
85
+ result = client.power_off(force: true)
86
+ output_result({ success: result }, result ? "System force powered off" : "Failed to force power off")
87
+ when 'restart', 'reboot'
88
+ result = client.power_restart(force: options[:force])
89
+ mode = options[:force] ? "force" : "graceful"
90
+ output_result({ success: result }, result ? "System restarting (#{mode})" : "Failed to restart")
91
+ when 'force-restart', 'force-reboot'
92
+ result = client.power_restart(force: true)
93
+ output_result({ success: result }, result ? "System force restarting" : "Failed to force restart")
94
+ when 'cycle'
95
+ result = client.power_cycle
96
+ output_result({ success: result }, result ? "Power cycle initiated" : "Failed to power cycle")
97
+ else
98
+ error "Unknown power command: #{subcommand}"
99
+ puts "Available: status, on, off, force-off, restart, force-restart, cycle"
100
+ puts "Use --force flag with 'off' or 'restart' to skip graceful shutdown"
101
+ end
102
+ end
103
+ end
104
+
105
+ # System Commands
106
+ desc "system SUBCOMMAND", "System inventory"
107
+ def system(subcommand = 'all')
108
+ with_client do |client|
109
+ case subcommand
110
+ when 'all'
111
+ data = {
112
+ cpus: safe_call { client.cpus },
113
+ memory: safe_call { client.memory },
114
+ nics: safe_call { client.nics },
115
+ fans: safe_call { client.fans },
116
+ temps: safe_call { client.temperatures },
117
+ psus: safe_call { client.psus }
118
+ }
119
+
120
+ if options[:json]
121
+ puts JSON.pretty_generate(data)
122
+ else
123
+ show_system_component('cpus', data[:cpus])
124
+ show_system_component('memory', data[:memory])
125
+ show_system_component('nics', data[:nics])
126
+ show_system_component('fans', data[:fans])
127
+ show_system_component('temps', data[:temps])
128
+ show_system_component('psus', data[:psus])
129
+ end
130
+ when 'cpus', 'cpu'
131
+ data = client.cpus
132
+ if options[:json]
133
+ puts JSON.pretty_generate(data)
134
+ else
135
+ show_system_component('cpus', data)
136
+ end
137
+ when 'memory', 'mem', 'ram'
138
+ data = client.memory
139
+ if options[:json]
140
+ puts JSON.pretty_generate(data)
141
+ else
142
+ show_system_component('memory', data)
143
+ end
144
+ when 'nics', 'network'
145
+ data = client.nics
146
+ if options[:json]
147
+ puts JSON.pretty_generate(data)
148
+ else
149
+ show_system_component('nics', data)
150
+ end
151
+ when 'fans', 'cooling'
152
+ data = client.fans
153
+ if options[:json]
154
+ puts JSON.pretty_generate(data)
155
+ else
156
+ show_system_component('fans', data)
157
+ end
158
+ when 'temps', 'temperatures', 'thermal'
159
+ data = client.temperatures
160
+ if options[:json]
161
+ puts JSON.pretty_generate(data)
162
+ else
163
+ show_system_component('temps', data)
164
+ end
165
+ when 'psus', 'power-supplies'
166
+ data = client.psus
167
+ if options[:json]
168
+ puts JSON.pretty_generate(data)
169
+ else
170
+ show_system_component('psus', data)
171
+ end
172
+ else
173
+ error "Unknown system command: #{subcommand}"
174
+ puts "Available: all, cpus, memory, nics, fans, temps, psus"
175
+ end
176
+ end
177
+ end
178
+
179
+ # Virtual Media Commands
180
+ desc "media SUBCOMMAND [URL]", "Virtual media management"
181
+ def media(subcommand = 'status', url = nil)
182
+ with_client do |client|
183
+ case subcommand
184
+ when 'status', 'list'
185
+ media = client.virtual_media_status
186
+ if options[:json]
187
+ puts JSON.pretty_generate(media)
188
+ else
189
+ show_media_status(media)
190
+ end
191
+ when 'mount', 'insert'
192
+ if url.nil?
193
+ error "URL required for mount command"
194
+ return
195
+ end
196
+ result = client.insert_virtual_media(url)
197
+ output_result({ success: result }, result ? "Media mounted: #{url}" : "Failed to mount media")
198
+ when 'unmount', 'eject', 'remove'
199
+ result = client.unmount_all_media
200
+ output_result({ success: result }, result ? "All media unmounted" : "Failed to unmount media")
201
+ when 'boot'
202
+ if url.nil?
203
+ error "URL required for boot command"
204
+ return
205
+ end
206
+ result = client.mount_iso_and_boot(url)
207
+ output_result({ success: result }, result ? "ISO mounted and boot set" : "Failed to mount and boot")
208
+ else
209
+ error "Unknown media command: #{subcommand}"
210
+ puts "Available: status, mount URL, unmount, boot URL"
211
+ end
212
+ end
213
+ end
214
+
215
+ # Boot Commands
216
+ desc "boot SUBCOMMAND [TARGET]", "Boot configuration"
217
+ option :once, type: :boolean, desc: "Boot override for next boot only"
218
+ option :continuous, type: :boolean, desc: "Boot override until manually cleared"
219
+ option :uefi, type: :boolean, desc: "Use UEFI boot mode"
220
+ option :legacy, type: :boolean, desc: "Use Legacy/BIOS boot mode"
221
+ def boot(subcommand = 'options', target = nil)
222
+ with_client do |client|
223
+ case subcommand
224
+ when 'options', 'status'
225
+ opts = client.boot_options
226
+ if options[:json]
227
+ puts JSON.pretty_generate(opts)
228
+ else
229
+ puts "=== Boot Options ===".green
230
+ puts "Override: #{opts['boot_source_override_enabled']} -> #{opts['boot_source_override_target']}".cyan
231
+ puts "Mode: #{opts['boot_source_override_mode']}".cyan if opts['boot_source_override_mode']
232
+ puts "Allowed: #{opts['allowed_targets']&.join(', ')}".cyan if opts['allowed_targets']
233
+ end
234
+ when 'override', 'set'
235
+ if target.nil?
236
+ error "Target required (pxe, disk, cd, usb, bios)"
237
+ return
238
+ end
239
+
240
+ # Determine persistence setting
241
+ persistence = if options[:once]
242
+ 'Once'
243
+ elsif options[:continuous]
244
+ 'Continuous'
245
+ else
246
+ 'Once' # Default to Once
247
+ end
248
+
249
+ # Determine boot mode
250
+ boot_mode = if options[:uefi]
251
+ 'UEFI'
252
+ elsif options[:legacy]
253
+ 'Legacy'
254
+ else
255
+ nil # Don't change if not specified
256
+ end
257
+
258
+ result = client.set_boot_override(target.capitalize,
259
+ persistence: persistence,
260
+ mode: boot_mode)
261
+
262
+ mode_str = boot_mode ? " in #{boot_mode} mode" : ""
263
+ persist_str = " (#{persistence})"
264
+ output_result({ success: result },
265
+ result ? "Boot override set to #{target}#{mode_str}#{persist_str}" : "Failed to set boot override")
266
+ when 'clear', 'reset'
267
+ result = client.clear_boot_override
268
+ output_result({ success: result }, result ? "Boot override cleared" : "Failed to clear boot override")
269
+ when 'pxe'
270
+ result = set_boot_with_options(client, 'Pxe')
271
+ output_result({ success: result }, result ? "Boot to PXE set" : "Failed to set PXE boot")
272
+ when 'disk', 'hdd'
273
+ result = set_boot_with_options(client, 'Hdd')
274
+ output_result({ success: result }, result ? "Boot to disk set" : "Failed to set disk boot")
275
+ when 'cd', 'dvd'
276
+ result = set_boot_with_options(client, 'Cd')
277
+ output_result({ success: result }, result ? "Boot to CD set" : "Failed to set CD boot")
278
+ when 'usb'
279
+ result = set_boot_with_options(client, 'Usb')
280
+ output_result({ success: result }, result ? "Boot to USB set" : "Failed to set USB boot")
281
+ when 'bios', 'setup'
282
+ result = set_boot_with_options(client, 'BiosSetup')
283
+ output_result({ success: result }, result ? "Boot to BIOS setup set" : "Failed to set BIOS boot")
284
+ when 'config'
285
+ # New subcommand to configure boot settings
286
+ configure_boot_settings(client)
287
+ else
288
+ error "Unknown boot command: #{subcommand}"
289
+ puts "Available: options, set TARGET, clear, pxe, disk, cd, usb, bios, config"
290
+ puts "Options: --once, --continuous, --uefi, --legacy"
291
+ end
292
+ end
293
+ end
294
+
295
+ # SEL Commands
296
+ desc "sel SUBCOMMAND", "System Event Log management"
297
+ def sel(subcommand = 'show')
298
+ with_client do |client|
299
+ case subcommand
300
+ when 'show', 'list'
301
+ entries = client.sel_log
302
+ limit = 10
303
+ entries = entries.first(limit) if entries.length > limit
304
+
305
+ if options[:json]
306
+ puts JSON.pretty_generate(entries)
307
+ else
308
+ puts "=== System Event Log (last #{limit}) ===".green
309
+ entries.each do |entry|
310
+ severity_color = case entry['severity']
311
+ when 'Critical' then :red
312
+ when 'Warning' then :yellow
313
+ else :cyan
314
+ end
315
+ puts "[#{entry['created']}] #{entry['severity']}".send(severity_color)
316
+ puts " #{entry['message']}"
317
+ end
318
+ end
319
+ when 'clear'
320
+ result = client.clear_sel_log
321
+ output_result({ success: result }, result ? "SEL cleared" : "Failed to clear SEL")
322
+ else
323
+ error "Unknown SEL command: #{subcommand}"
324
+ puts "Available: show, clear"
325
+ end
326
+ end
327
+ end
328
+
329
+ # Storage Commands
330
+ desc "storage SUBCOMMAND", "Storage information"
331
+ def storage(subcommand = 'summary')
332
+ with_client do |client|
333
+ case subcommand
334
+ when 'summary', 'all'
335
+ data = client.storage_summary
336
+ output_result(data, nil) if options[:json]
337
+ when 'controllers'
338
+ data = client.storage_controllers
339
+ output_result(data, nil) if options[:json]
340
+ unless options[:json]
341
+ puts "=== Storage Controllers ===".green
342
+ data.each do |ctrl|
343
+ puts "#{ctrl['name']} (#{ctrl['id']})".cyan
344
+ end
345
+ end
346
+ when 'drives', 'disks'
347
+ data = client.drives
348
+ if options[:json]
349
+ puts JSON.pretty_generate(data)
350
+ else
351
+ puts "=== Physical Drives ===".green
352
+ data.each do |drive|
353
+ puts "#{drive['name']}: #{drive['capacity_gb']} GB - #{drive['status']}".cyan
354
+ end
355
+ end
356
+ when 'volumes', 'raids'
357
+ data = client.volumes
358
+ if options[:json]
359
+ puts JSON.pretty_generate(data)
360
+ else
361
+ puts "=== Volumes ===".green
362
+ data.each do |vol|
363
+ puts "#{vol['name']}: #{vol['capacity_gb']} GB - #{vol['raid_type']}".cyan
364
+ end
365
+ end
366
+ else
367
+ error "Unknown storage command: #{subcommand}"
368
+ puts "Available: summary, controllers, drives, volumes"
369
+ end
370
+ end
371
+ end
372
+
373
+ # Config Commands
374
+ desc "config SUBCOMMAND", "Configuration file management"
375
+ def config(subcommand = 'generate')
376
+ case subcommand
377
+ when 'generate', 'create'
378
+ config = {
379
+ 'host' => '192.168.1.100',
380
+ 'username' => 'admin',
381
+ 'password' => 'password',
382
+ 'vendor' => 'auto',
383
+ 'port' => 443,
384
+ 'insecure' => true
385
+ }
386
+ puts YAML.dump(config)
387
+ when 'validate'
388
+ if options[:config]
389
+ validate_config_file(options[:config])
390
+ else
391
+ error "Config file path required (use --config FILE)"
392
+ end
393
+ else
394
+ error "Unknown config command: #{subcommand}"
395
+ puts "Available: generate, validate"
396
+ end
397
+ end
398
+
399
+ desc "licenses", "Check BMC licenses (Supermicro only)"
400
+ def licenses
401
+ with_client do |client|
402
+ if client.respond_to?(:check_virtual_media_license)
403
+ # Supermicro-specific license check
404
+ license_info = client.check_virtual_media_license
405
+
406
+ if options[:json]
407
+ puts JSON.pretty_generate(license_info)
408
+ else
409
+ puts "=== License Status ===".green
410
+
411
+ case license_info[:available]
412
+ when true
413
+ puts "Virtual Media License: #{'Present'.green}"
414
+ puts "Licenses: #{license_info[:licenses].join(', ')}"
415
+ when false
416
+ puts "Virtual Media License: #{'Missing'.red}"
417
+ puts license_info[:message].yellow
418
+ else
419
+ puts "Virtual Media License: #{'Unknown'.yellow}"
420
+ puts license_info[:message].yellow
421
+ end
422
+
423
+ # Show all licenses if available
424
+ if client.respond_to?(:licenses)
425
+ all_licenses = client.licenses
426
+ if all_licenses.any?
427
+ puts "\n=== All Licenses ===".green
428
+ all_licenses.each do |lic|
429
+ puts " #{lic[:name]} (ID: #{lic[:id]})".cyan
430
+ end
431
+ end
432
+ end
433
+ end
434
+ else
435
+ error "License checking not supported for this vendor"
436
+ end
437
+ end
438
+ end
439
+
440
+ desc "version", "Show radfish version"
441
+ def version
442
+ puts "Radfish #{Radfish::VERSION}"
443
+ puts "Supported vendors: #{Radfish.supported_vendors.join(', ')}" if Radfish.supported_vendors.any?
444
+ end
445
+
446
+ private
447
+
448
+ def set_boot_with_options(client, target)
449
+ # Determine persistence setting
450
+ persistence = if options[:once]
451
+ 'Once'
452
+ elsif options[:continuous]
453
+ 'Continuous'
454
+ else
455
+ 'Once' # Default to Once
456
+ end
457
+
458
+ # Determine boot mode
459
+ boot_mode = if options[:uefi]
460
+ 'UEFI'
461
+ elsif options[:legacy]
462
+ 'Legacy'
463
+ else
464
+ nil # Don't change if not specified
465
+ end
466
+
467
+ client.set_boot_override(target, persistence: persistence, mode: boot_mode)
468
+ end
469
+
470
+ def configure_boot_settings(client)
471
+ # Configure just the boot settings without changing target
472
+ persistence = if options[:once]
473
+ 'Once'
474
+ elsif options[:continuous]
475
+ 'Continuous'
476
+ else
477
+ nil
478
+ end
479
+
480
+ boot_mode = if options[:uefi]
481
+ 'UEFI'
482
+ elsif options[:legacy]
483
+ 'Legacy'
484
+ else
485
+ nil
486
+ end
487
+
488
+ if persistence.nil? && boot_mode.nil?
489
+ error "Specify --once/--continuous and/or --uefi/--legacy"
490
+ return
491
+ end
492
+
493
+ result = client.configure_boot_settings(persistence: persistence, mode: boot_mode)
494
+
495
+ settings = []
496
+ settings << "persistence: #{persistence}" if persistence
497
+ settings << "mode: #{boot_mode}" if boot_mode
498
+
499
+ output_result({ success: result },
500
+ result ? "Boot settings updated (#{settings.join(', ')})" : "Failed to update boot settings")
501
+ end
502
+
503
+ def with_connection(skip_login: false)
504
+ opts = load_options
505
+
506
+ missing = []
507
+ missing << 'host' unless opts[:host]
508
+ missing << 'username' unless opts[:username]
509
+ missing << 'password' unless opts[:password]
510
+
511
+ unless missing.empty?
512
+ if options[:json]
513
+ STDERR.puts JSON.generate({
514
+ error: "Missing required options",
515
+ missing: missing,
516
+ message: "Use --host, --username, --password or specify a config file with --config"
517
+ })
518
+ else
519
+ error "Missing required options: #{missing.join(', ')}"
520
+ error "Use --host, --username, --password or specify a config file with --config"
521
+ end
522
+ exit 1
523
+ end
524
+
525
+ yield opts
526
+ end
527
+
528
+ def with_client(&block)
529
+ with_connection do |opts|
530
+ client_opts = {
531
+ host: opts[:host],
532
+ username: opts[:username],
533
+ password: opts[:password],
534
+ port: opts[:port],
535
+ verify_ssl: !opts[:insecure],
536
+ direct_mode: true
537
+ }
538
+
539
+ client_opts[:vendor] = opts[:vendor] if opts[:vendor]
540
+
541
+ begin
542
+ client = Radfish::Client.new(**client_opts)
543
+ client.verbosity = 1 if opts[:verbose]
544
+
545
+ client.login
546
+ yield client
547
+ rescue => e
548
+ error "Error: #{e.message}"
549
+ exit 1
550
+ ensure
551
+ client.logout if client rescue nil
552
+ end
553
+ end
554
+ end
555
+
556
+ def load_options
557
+ opts = {}
558
+
559
+ # Load from config file if specified
560
+ if options[:config]
561
+ config_file = File.expand_path(options[:config])
562
+ if File.exist?(config_file)
563
+ config = YAML.load_file(config_file)
564
+ opts = symbolize_keys(config)
565
+ else
566
+ error "Config file not found: #{config_file}"
567
+ exit 1
568
+ end
569
+ end
570
+
571
+ # Override with command line options
572
+ opts[:host] = options[:host] if options[:host]
573
+ opts[:username] = options[:username] if options[:username]
574
+ opts[:password] = options[:password] if options[:password]
575
+ opts[:vendor] = options[:vendor] if options[:vendor]
576
+ opts[:port] = options[:port] if options[:port]
577
+ opts[:insecure] = options[:insecure] if options.key?(:insecure)
578
+ opts[:verbose] = options[:verbose] if options.key?(:verbose)
579
+
580
+ # Check environment variables as fallback
581
+ opts[:host] ||= ENV['RADFISH_HOST']
582
+ opts[:username] ||= ENV['RADFISH_USERNAME']
583
+ opts[:password] ||= ENV['RADFISH_PASSWORD']
584
+ opts[:vendor] ||= ENV['RADFISH_VENDOR']
585
+ opts[:port] ||= ENV['RADFISH_PORT'].to_i if ENV['RADFISH_PORT']
586
+
587
+ opts
588
+ end
589
+
590
+ def symbolize_keys(hash)
591
+ hash.transform_keys(&:to_sym)
592
+ end
593
+
594
+ def safe_call
595
+ yield
596
+ rescue => e
597
+ options[:verbose] ? e.message : 'N/A'
598
+ end
599
+
600
+ def error(message)
601
+ if options[:json]
602
+ STDERR.puts JSON.generate({ error: message })
603
+ else
604
+ STDERR.puts message.red
605
+ end
606
+ end
607
+
608
+ def success(message)
609
+ puts message.green unless options[:json]
610
+ end
611
+
612
+ def info_msg(message)
613
+ puts message.cyan unless options[:json]
614
+ end
615
+
616
+ def output_result(data, message, color = :green)
617
+ if options[:json]
618
+ puts JSON.pretty_generate(data)
619
+ elsif message
620
+ puts message.send(color)
621
+ end
622
+ end
623
+
624
+ def show_media_status(media_list)
625
+ puts "\n=== Virtual Media Status ===".green
626
+
627
+ if media_list.nil? || media_list.empty?
628
+ puts "No virtual media devices found".yellow
629
+ return
630
+ end
631
+
632
+ # Check if any media is inserted
633
+ has_mounted_media = media_list.any? { |m| m[:inserted] || m[:image] }
634
+
635
+ media_list.each do |media|
636
+ # Display device name with ID if the name is generic
637
+ device_display = media[:name] || media[:device] || 'Unknown'
638
+ puts "\nDevice: #{device_display}".cyan
639
+
640
+ # Show media types if available
641
+ puts " Media Types: #{media[:media_types]&.join(', ')}" if media[:media_types]
642
+
643
+ # Check for CD/DVD support for boot guidance
644
+ supports_cd = media[:media_types]&.any? { |t| t.upcase.include?('CD') || t.upcase.include?('DVD') }
645
+
646
+ if media[:inserted] || media[:image]
647
+ # Determine actual status based on image presence
648
+ if media[:image] && !media[:image].empty? && media[:image] != "http://0.0.0.0/dummy.iso"
649
+ # Check connection status
650
+ if media[:connected_via]
651
+ case media[:connected_via]
652
+ when "NotConnected"
653
+ puts " Status: #{'NOT CONNECTED'.red} (ISO won't boot)"
654
+ puts " Image: #{media[:image]}"
655
+ puts " Connection: #{'Not Connected - Media will NOT boot!'.red}"
656
+ when "URI"
657
+ puts " Status: #{'Connected'.green} (Ready to boot)"
658
+ puts " Image: #{media[:image]}"
659
+ puts " Connection: #{'Active via URI'.green}"
660
+ when "Applet"
661
+ puts " Status: #{'Connected'.green} (Ready to boot)"
662
+ puts " Image: #{media[:image]}"
663
+ puts " Connection: Active via Applet"
664
+ else
665
+ puts " Status: Unknown"
666
+ puts " Image: #{media[:image]}"
667
+ puts " Connection: #{media[:connected_via]}"
668
+ end
669
+ else
670
+ puts " Image: #{media[:image]}"
671
+ end
672
+
673
+ if supports_cd
674
+ puts " Boot Command: radfish boot cd".light_blue
675
+ end
676
+ else
677
+ puts " Status: #{'Empty'.yellow}"
678
+ end
679
+ else
680
+ puts " Status: #{'Empty'.yellow}"
681
+ end
682
+ end
683
+
684
+ if has_mounted_media
685
+ puts "\nTo boot from virtual media:".green
686
+ puts " 1. radfish boot cd # Set boot to CD/DVD".cyan
687
+ puts " 2. radfish power restart # Restart the server".cyan
688
+ end
689
+ end
690
+
691
+ def show_system_component(type, data)
692
+ return unless data
693
+
694
+ case type
695
+ when 'cpus'
696
+ puts "\n=== CPUs ===".green
697
+ data.each do |cpu|
698
+ puts "#{cpu['socket']}: #{cpu['manufacturer']} #{cpu['model']}".cyan
699
+ puts " Cores: #{cpu['cores']}, Threads: #{cpu['threads']}, Speed: #{cpu['speed_mhz']} MHz"
700
+ end
701
+ when 'memory'
702
+ puts "\n=== Memory ===".green
703
+ total_gb = data.sum { |m| m["capacity_bytes"] || 0 } / (1024.0 ** 3)
704
+ puts "Total: #{total_gb.round(2)} GB".cyan
705
+ data.each do |dimm|
706
+ next if dimm["capacity_bytes"].nil? || dimm["capacity_bytes"] == 0
707
+ capacity_gb = dimm["capacity_bytes"] / (1024.0 ** 3)
708
+ puts "#{dimm['name']}: #{capacity_gb.round(2)} GB @ #{dimm['speed_mhz']} MHz"
709
+ end
710
+ when 'nics'
711
+ puts "\n=== Network Interfaces ===".green
712
+ data.each do |nic|
713
+ puts "#{nic['name']}: #{nic['mac']}".cyan
714
+ puts " Speed: #{nic['speed_mbps']} Mbps, Link: #{nic['link_status']}"
715
+ end
716
+ when 'fans'
717
+ puts "\n=== Fans ===".green
718
+ data.each do |fan|
719
+ status_color = fan['status'] == 'OK' ? :green : :red
720
+ puts "#{fan['name']}: #{fan['rpm']} RPM - #{fan['status']}".send(status_color)
721
+ end
722
+ when 'temps'
723
+ puts "\n=== Temperature Sensors ===".green
724
+ data.each do |temp|
725
+ next unless temp['reading_celsius']
726
+ status_color = temp['status'] == 'OK' ? :green : :red
727
+ puts "#{temp['name']}: #{temp['reading_celsius']}°C".send(status_color)
728
+ end
729
+ when 'psus'
730
+ puts "\n=== Power Supplies ===".green
731
+ data.each do |psu|
732
+ status_color = psu['status'] == 'OK' ? :green : :red
733
+ puts "#{psu['name']}: #{psu['watts']}W - #{psu['status']}".send(status_color)
734
+ end
735
+ end
736
+ end
737
+
738
+ def validate_config_file(file)
739
+ file = File.expand_path(file)
740
+ if File.exist?(file)
741
+ begin
742
+ config = YAML.load_file(file)
743
+ required = ['host', 'username', 'password']
744
+ missing = required - config.keys
745
+
746
+ if missing.empty?
747
+ success "Config file is valid"
748
+ else
749
+ error "Config file missing required fields: #{missing.join(', ')}"
750
+ end
751
+ rescue => e
752
+ error "Invalid config file: #{e.message}"
753
+ end
754
+ else
755
+ error "Config file not found: #{file}"
756
+ end
757
+ end
758
+ end
759
+ end