radfish-idrac 0.1.2 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1a21b31684656fe1144a48dcbb5a93c6de22b59fecc31ba3800b2b51fb98430
4
- data.tar.gz: 6309a0f8229e954d4c0f7371cff7be8e8658c2bd2f4a70285a1a8fda42389a8d
3
+ metadata.gz: a58b55f6a0455e66cd44bed5363451e773c675d2a9fd65d7fa5e6bae77669778
4
+ data.tar.gz: fab0e351a3112e0376b6d065e5faeb6280f7a2dfea90db9d8845bca67e82e4fb
5
5
  SHA512:
6
- metadata.gz: 9b34336926552bdd6eff3528ad5cdec89fcafa82b4e026a8a7c240640216101a7be336bd757b149930d64e2caee339c55c5373c362822020343285b322fef109
7
- data.tar.gz: e12b7e09db36ac2636af35ade8d99d19cb473783d7e5029989dd5d545f0ffd5e28932fa19dc14b208ff33c0a1610b290e5ed5790c9e0f987baf665dae9ee48a1
6
+ metadata.gz: 05f39a32177f9ef7f07fed8d5911d5a0d6bccaad7d315868895356e645866a39ab6a2a6bb1063349925d82336cc65736ffee6a594dad88e643f501035289e658
7
+ data.tar.gz: 8e5e9cd3ef3f62d3152b695c35702bccd924df41540bac3434d20182f67c829eee7fa65ccd553ecaf43b75cd9bbbecfef1313269e63a2855aa5b984f831d0178
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Radfish
4
4
  module Idrac
5
- VERSION = "0.1.2"
5
+ VERSION = "0.1.4"
6
6
  end
7
7
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'radfish'
4
4
  require 'idrac'
5
+ require 'ostruct'
5
6
 
6
7
  module Radfish
7
8
  class IdracAdapter < Core::BaseClient
@@ -12,6 +13,7 @@ module Radfish
12
13
  include Core::Boot
13
14
  include Core::Jobs
14
15
  include Core::Utility
16
+ include Core::Network
15
17
 
16
18
  attr_reader :idrac_client
17
19
 
@@ -62,30 +64,95 @@ module Radfish
62
64
  @idrac_client.get_power_state
63
65
  end
64
66
 
65
- def power_on
66
- @idrac_client.power_on
67
- end
68
-
69
- def power_off(force: false)
70
- kind = force ? "ForceOff" : "GracefulShutdown"
71
- @idrac_client.power_off(kind: kind)
67
+ def power_on(wait: true)
68
+ result = @idrac_client.power_on
69
+
70
+ if wait && result
71
+ # Wait for power on to complete
72
+ max_attempts = 30
73
+ attempts = 0
74
+ while attempts < max_attempts
75
+ sleep 2
76
+ begin
77
+ status = power_state
78
+ break if status == "On"
79
+ rescue => e
80
+ # BMC might be temporarily unavailable during power operations
81
+ debug "Waiting for BMC to respond: #{e.message}", 1, :yellow
82
+ end
83
+ attempts += 1
84
+ end
85
+ end
86
+
87
+ result
72
88
  end
73
89
 
74
- def power_restart(force: false)
75
- # iDRAC uses reboot method, which is ForceRestart by default
76
- if force
77
- @idrac_client.reboot
78
- else
79
- # Try graceful restart first
80
- @idrac_client.power_off(kind: "GracefulRestart") rescue @idrac_client.reboot
90
+ def power_off(type: "GracefulShutdown", wait: true)
91
+ # Use the type parameter directly - it already uses Redfish standard values
92
+ result = @idrac_client.power_off(kind: type)
93
+
94
+ if wait && result
95
+ # Wait for power off to complete
96
+ max_attempts = 30
97
+ attempts = 0
98
+ while attempts < max_attempts
99
+ sleep 2
100
+ begin
101
+ status = power_state
102
+ break if status == "Off"
103
+ rescue => e
104
+ # BMC might be temporarily unavailable during power operations
105
+ debug "Waiting for BMC to respond: #{e.message}", 1, :yellow
106
+ end
107
+ attempts += 1
108
+ end
109
+ end
110
+
111
+ result
112
+ end
113
+
114
+ def reboot(type: "GracefulRestart", wait: true)
115
+ # Use the type parameter - iDRAC's power_off can handle restart types
116
+ begin
117
+ result = @idrac_client.power_off(kind: type)
118
+ rescue => e
119
+ # If graceful restart fails, fall back to force restart
120
+ if type == "GracefulRestart"
121
+ debug "Graceful restart failed, using force restart", 1, :yellow
122
+ result = @idrac_client.reboot # This is ForceRestart
123
+ else
124
+ raise e
125
+ end
126
+ end
127
+
128
+ if wait && result
129
+ # Wait for system to go down then come back up
130
+ max_attempts = 60
131
+ attempts = 0
132
+ went_down = false
133
+
134
+ while attempts < max_attempts
135
+ sleep 2
136
+ begin
137
+ status = power_state
138
+ went_down = true if status == "Off" && !went_down
139
+ break if went_down && status == "On"
140
+ rescue => e
141
+ # BMC might be temporarily unavailable during reboot
142
+ debug "Waiting for BMC during reboot: #{e.message}", 1, :yellow
143
+ end
144
+ attempts += 1
145
+ end
81
146
  end
147
+
148
+ result
82
149
  end
83
150
 
84
- def power_cycle
85
- # iDRAC doesn't have power_cycle, simulate with off then on
86
- power_off
151
+ def power_cycle(wait: true)
152
+ # Power cycle: turn off then on
153
+ power_off(type: "ForceOff", wait: wait)
87
154
  sleep 5
88
- power_on
155
+ power_on(wait: wait)
89
156
  end
90
157
 
91
158
  def reset_type_allowed
@@ -96,35 +163,123 @@ module Radfish
96
163
  # System information
97
164
 
98
165
  def system_info
99
- @idrac_client.system_info
166
+ # iDRAC gem returns string keys, convert to symbols for radfish
167
+ info = @idrac_client.system_info
168
+
169
+ # Dell servers always have "Dell Inc." as manufacturer
170
+ # Normalize for consistency
171
+ manufacturer = "Dell"
172
+
173
+ model = info["model"]
174
+ model = model&.gsub(/^PowerEdge\s+/i, '') if model # Strip PowerEdge prefix
175
+
176
+ {
177
+ service_tag: info["service_tag"],
178
+ manufacturer: manufacturer,
179
+ make: manufacturer,
180
+ model: model,
181
+ serial: info["service_tag"], # Dell uses service tag as serial
182
+ serial_number: info["service_tag"],
183
+ firmware_version: info["firmware_version"],
184
+ idrac_version: info["idrac_version"],
185
+ is_dell: info["is_dell"]
186
+ }.compact
187
+ end
188
+
189
+ # Individual accessor methods for Core::System interface
190
+ def service_tag
191
+ @service_tag ||= @idrac_client.system_info["service_tag"]
192
+ end
193
+
194
+ def make
195
+ "Dell"
196
+ end
197
+
198
+ def model
199
+ @model ||= begin
200
+ model = @idrac_client.system_info["model"]
201
+ model&.gsub(/^PowerEdge\s+/i, '') if model # Strip PowerEdge prefix
202
+ end
203
+ end
204
+
205
+ def serial
206
+ @serial ||= @idrac_client.system_info["service_tag"] # Dell uses service tag as serial
100
207
  end
101
208
 
102
209
  def cpus
103
- @idrac_client.cpus
210
+ # The idrac gem returns a summary hash, but radfish expects an array of CPU objects
211
+ # For Dell servers, typically all CPUs are identical, so we create objects based on the summary
212
+ cpu_summary = @idrac_client.cpus
213
+
214
+ # Create CPU objects that support dot notation
215
+ count = cpu_summary["count"] || 0
216
+ return [] if count == 0
217
+
218
+ # For each CPU socket, create an object
219
+ # Dell typically has identical CPUs, so we use the summary data for each
220
+ (1..count).map do |socket_num|
221
+ OpenStruct.new(
222
+ socket: socket_num,
223
+ manufacturer: "Intel", # Dell servers typically use Intel
224
+ model: cpu_summary["model"],
225
+ speed_mhz: nil, # Not provided in summary
226
+ cores: cpu_summary["cores"] ? (cpu_summary["cores"] / count) : nil,
227
+ threads: cpu_summary["threads"] ? (cpu_summary["threads"] / count) : nil,
228
+ health: cpu_summary["status"]
229
+ )
230
+ end
104
231
  end
105
232
 
106
233
  def memory
107
- @idrac_client.memory
234
+ mem_data = @idrac_client.memory
235
+ return [] unless mem_data
236
+
237
+ # Convert to OpenStruct for dot notation access
238
+ mem_data.map { |m| OpenStruct.new(m) }
108
239
  end
109
240
 
110
241
  def nics
111
- @idrac_client.nics
242
+ nic_data = @idrac_client.nics
243
+ return [] unless nic_data
244
+
245
+ # Convert to OpenStruct for dot notation access, including nested ports
246
+ nic_data.map do |nic|
247
+ if nic["ports"]
248
+ nic["ports"] = nic["ports"].map { |port| OpenStruct.new(port) }
249
+ end
250
+ OpenStruct.new(nic)
251
+ end
112
252
  end
113
253
 
114
254
  def fans
115
- @idrac_client.fans
255
+ # Convert hash array to OpenStruct objects for dot notation access
256
+ fan_data = @idrac_client.fans
257
+
258
+ fan_data.map do |fan|
259
+ OpenStruct.new(fan)
260
+ end
116
261
  end
117
262
 
118
263
  def temperatures
119
- @idrac_client.temperatures
264
+ # iDRAC doesn't provide a dedicated temperatures method
265
+ # Return empty array to satisfy the interface
266
+ []
120
267
  end
121
268
 
122
269
  def psus
123
- @idrac_client.psus
270
+ # Convert hash array to OpenStruct objects for dot notation access
271
+ psu_data = @idrac_client.psus
272
+
273
+ psu_data.map do |psu|
274
+ OpenStruct.new(psu)
275
+ end
124
276
  end
125
277
 
126
278
  def power_consumption
127
- @idrac_client.power_consumption
279
+ # Return a hash with power consumption data for radfish
280
+ {
281
+ consumed_watts: @idrac_client.get_power_usage_watts
282
+ }
128
283
  end
129
284
 
130
285
  def power_consumption_watts
@@ -134,7 +289,17 @@ module Radfish
134
289
  # Storage
135
290
 
136
291
  def storage_controllers
137
- @idrac_client.storage_controllers
292
+ # Convert hash array to OpenStruct objects for dot notation access
293
+ # Note: idrac gem uses 'controllers' not 'storage_controllers'
294
+ controller_data = @idrac_client.controllers
295
+
296
+ controller_data.map do |controller|
297
+ # Convert drives array to OpenStruct objects if present
298
+ if controller["drives"]
299
+ controller["drives"] = controller["drives"].map { |drive| OpenStruct.new(drive) }
300
+ end
301
+ OpenStruct.new(controller)
302
+ end
138
303
  end
139
304
 
140
305
  def drives
@@ -155,12 +320,41 @@ module Radfish
155
320
  @idrac_client.virtual_media
156
321
  end
157
322
 
158
- def insert_virtual_media(iso_url, device: "CD")
323
+ def insert_virtual_media(iso_url, device: nil)
324
+ # Default to "CD" for iDRAC if not specified
325
+ device ||= "CD"
159
326
  @idrac_client.insert_virtual_media(iso_url, device: device)
327
+ rescue Idrac::Error => e
328
+ # Translate iDRAC errors to Radfish errors with context
329
+ error_message = e.message
330
+
331
+ if error_message.include?("connection refused") || error_message.include?("unreachable")
332
+ raise Radfish::VirtualMediaConnectionError, "BMC cannot reach ISO server: #{error_message}"
333
+ elsif error_message.include?("already attached") || error_message.include?("in use")
334
+ raise Radfish::VirtualMediaBusyError, "Virtual media device busy: #{error_message}"
335
+ elsif error_message.include?("not found") || error_message.include?("does not exist")
336
+ raise Radfish::VirtualMediaNotFoundError, "Virtual media device not found: #{error_message}"
337
+ elsif error_message.include?("timeout")
338
+ raise Radfish::TaskTimeoutError, "Virtual media operation timed out: #{error_message}"
339
+ else
340
+ # Generic virtual media error
341
+ raise Radfish::VirtualMediaError, error_message
342
+ end
343
+ rescue StandardError => e
344
+ # Catch any other errors and wrap them
345
+ raise Radfish::VirtualMediaError, "Virtual media insertion failed: #{e.message}"
160
346
  end
161
347
 
162
348
  def eject_virtual_media(device: "CD")
163
349
  @idrac_client.eject_virtual_media(device: device)
350
+ rescue Idrac::Error => e
351
+ if e.message.include?("not found") || e.message.include?("does not exist")
352
+ raise Radfish::VirtualMediaNotFoundError, "Virtual media device not found: #{e.message}"
353
+ else
354
+ raise Radfish::VirtualMediaError, "Failed to eject virtual media: #{e.message}"
355
+ end
356
+ rescue StandardError => e
357
+ raise Radfish::VirtualMediaError, "Failed to eject virtual media: #{e.message}"
164
358
  end
165
359
 
166
360
  def virtual_media_status
@@ -187,12 +381,24 @@ module Radfish
187
381
 
188
382
  # Boot configuration
189
383
 
384
+ def boot_config
385
+ # Return hash for consistent data structure
386
+ @idrac_client.boot_config
387
+ end
388
+
389
+ # Shorter alias for convenience
390
+ def boot
391
+ boot_config
392
+ end
393
+
190
394
  def boot_options
191
- @idrac_client.boot_options
395
+ # Return array of OpenStructs for boot options
396
+ options = @idrac_client.boot_options
397
+ options.map { |opt| OpenStruct.new(opt) }
192
398
  end
193
399
 
194
- def set_boot_override(target, persistent: false)
195
- @idrac_client.set_boot_override(target, persistent: persistent)
400
+ def set_boot_override(target, enabled: "Once", mode: nil)
401
+ @idrac_client.set_boot_override(target, enabled: enabled, mode: mode)
196
402
  end
197
403
 
198
404
  def clear_boot_override
@@ -207,24 +413,45 @@ module Radfish
207
413
  @idrac_client.get_boot_devices
208
414
  end
209
415
 
210
- def boot_to_pxe
211
- @idrac_client.boot_to_pxe
416
+ def boot_to_pxe(enabled: "Once", mode: nil)
417
+ @idrac_client.boot_to_pxe(enabled: enabled, mode: mode)
418
+ end
419
+
420
+ def boot_to_disk(enabled: "Once", mode: nil)
421
+ @idrac_client.boot_to_disk(enabled: enabled, mode: mode)
422
+ end
423
+
424
+ def boot_to_cd(enabled: "Once", mode: nil)
425
+ @idrac_client.boot_to_cd(enabled: enabled, mode: mode)
212
426
  end
213
427
 
214
- def boot_to_disk
215
- @idrac_client.boot_to_disk
428
+ def boot_to_usb(enabled: "Once", mode: nil)
429
+ @idrac_client.boot_to_usb(enabled: enabled, mode: mode)
216
430
  end
217
431
 
218
- def boot_to_cd
219
- @idrac_client.boot_to_cd
432
+ def boot_to_bios_setup(enabled: "Once", mode: nil)
433
+ @idrac_client.boot_to_bios_setup(enabled: enabled, mode: mode)
220
434
  end
221
435
 
222
- def boot_to_usb
223
- @idrac_client.boot_to_usb
436
+ # PCI Devices
437
+
438
+ def pci_devices
439
+ devices = @idrac_client.pci_devices
440
+ return [] unless devices
441
+
442
+ # Convert to OpenStruct for dot notation access
443
+ devices.map { |device| OpenStruct.new(device) }
224
444
  end
225
445
 
226
- def boot_to_bios_setup
227
- @idrac_client.boot_to_bios_setup
446
+ def nics_with_pci_info
447
+ nics = @idrac_client.nics
448
+ pci = pci_devices
449
+
450
+ # Use the existing nics_to_pci method from idrac gem
451
+ nics_with_pci = @idrac_client.nics_to_pci(nics, pci.map(&:to_h))
452
+
453
+ # Convert to OpenStruct
454
+ nics_with_pci.map { |nic| OpenStruct.new(nic) }
228
455
  end
229
456
 
230
457
  # Jobs
@@ -245,14 +472,112 @@ module Radfish
245
472
  @idrac_client.cancel_job(job_id)
246
473
  end
247
474
 
248
- def clear_completed_jobs
249
- @idrac_client.clear_jobs
475
+ def clear_jobs!
476
+ @idrac_client.clear_jobs!
250
477
  end
251
478
 
252
479
  def jobs_summary
253
480
  jobs
254
481
  end
255
482
 
483
+ # BMC Management
484
+
485
+ def ensure_vendor_specific_bmc_ready!
486
+ # For iDRAC, ensure the Lifecycle Controller is enabled
487
+ @idrac_client.ensure_lifecycle_controller!
488
+ end
489
+
490
+ # BIOS Configuration
491
+
492
+ def bios_error_prompt_disabled?
493
+ @idrac_client.bios_error_prompt_disabled?
494
+ end
495
+
496
+ def bios_hdd_placeholder_enabled?
497
+ @idrac_client.bios_hdd_placeholder_enabled?
498
+ end
499
+
500
+ def bios_os_power_control_enabled?
501
+ @idrac_client.bios_os_power_control_enabled?
502
+ end
503
+
504
+ def ensure_uefi_boot
505
+ @idrac_client.ensure_uefi_boot
506
+ end
507
+
508
+ def set_one_time_boot_to_virtual_media
509
+ # Use iDRAC's existing method for setting one-time boot to virtual media
510
+ @idrac_client.set_one_time_virtual_media_boot
511
+ end
512
+
513
+ def set_boot_order_hd_first
514
+ # Use iDRAC's existing method for setting boot order to HD first
515
+ @idrac_client.set_boot_order_hd_first
516
+ end
517
+
518
+ def ensure_sensible_bios!(options = {})
519
+ # Check current state
520
+ if bios_error_prompt_disabled? &&
521
+ bios_hdd_placeholder_enabled? &&
522
+ bios_os_power_control_enabled?
523
+ puts "BIOS settings already configured correctly".green
524
+ return { changes_made: false }
525
+ end
526
+
527
+ puts "Configuring BIOS settings...".yellow
528
+
529
+ # Build the System Configuration Profile (SCP)
530
+ scp = {}
531
+
532
+ # Disable error prompt (don't halt on errors)
533
+ if !bios_error_prompt_disabled?
534
+ scp = @idrac_client.merge_scp(scp, {
535
+ "BIOS.Setup.1-1" => {
536
+ "ErrPrompt" => "Disabled"
537
+ }
538
+ })
539
+ end
540
+
541
+ # Enable HDD placeholder for boot order control
542
+ if !bios_hdd_placeholder_enabled?
543
+ scp = @idrac_client.merge_scp(scp, {
544
+ "BIOS.Setup.1-1" => {
545
+ "HddPlaceholder" => "Enabled"
546
+ }
547
+ })
548
+ end
549
+
550
+ # Enable OS power control
551
+ if !bios_os_power_control_enabled?
552
+ scp = @idrac_client.merge_scp(scp, {
553
+ "BIOS.Setup.1-1" => {
554
+ "ProcCStates" => "Enabled",
555
+ "SysProfile" => "PerfPerWattOptimizedOs",
556
+ "ProcPwrPerf" => "OsDbpm"
557
+ }
558
+ })
559
+ end
560
+
561
+ # Set UEFI boot mode
562
+ scp = @idrac_client.merge_scp(scp, {
563
+ "BIOS.Setup.1-1" => {
564
+ "BootMode" => "Uefi"
565
+ }
566
+ })
567
+
568
+ # Disable host header check for better compatibility
569
+ scp = @idrac_client.merge_scp(scp, {
570
+ "iDRAC.Embedded.1" => {
571
+ "WebServer.1#HostHeaderCheck" => "Disabled"
572
+ }
573
+ })
574
+
575
+ # Apply the configuration
576
+ @idrac_client.set_system_configuration_profile(scp)
577
+
578
+ { changes_made: true }
579
+ end
580
+
256
581
  # Utility
257
582
 
258
583
  def sel_log
@@ -295,18 +620,74 @@ module Radfish
295
620
  @idrac_client.get_firmware_version
296
621
  end
297
622
 
623
+ def bmc_info
624
+ # Map iDRAC gem data to radfish format
625
+ info = {}
626
+
627
+ # Get firmware version from idrac gem
628
+ info[:firmware_version] = @idrac_client.get_firmware_version
629
+
630
+ # Get iDRAC generation (7/8/9) from idrac gem's license_version method
631
+ info[:license_version] = @idrac_client.license_version&.to_s
632
+
633
+ # Get Redfish version from idrac gem
634
+ info[:redfish_version] = @idrac_client.redfish_version
635
+
636
+ # Get network info for MAC and IP
637
+ network = @idrac_client.get_bmc_network
638
+ if network.is_a?(Hash)
639
+ info[:mac_address] = network["mac"]
640
+ info[:ip_address] = network["ipv4"]
641
+ info[:hostname] = network["hostname"] || network["fqdn"]
642
+ end
643
+
644
+ # Get health status from system info
645
+ system = @idrac_client.system_info
646
+ if system.is_a?(Hash)
647
+ info[:health] = system.dig("Status", "Health") || system.dig("Status", "HealthRollup")
648
+ end
649
+
650
+ info
651
+ end
652
+
653
+ def system_health
654
+ # Convert hash to OpenStruct for dot notation access
655
+ health_data = @idrac_client.system_health
656
+ OpenStruct.new(health_data)
657
+ end
658
+
298
659
  # Additional iDRAC-specific methods
299
660
 
300
661
  def screenshot
301
- @idrac_client.screenshot if @idrac_client.respond_to?(:screenshot)
662
+ @idrac_client.screenshot
302
663
  end
303
664
 
304
- def licenses
305
- @idrac_client.licenses if @idrac_client.respond_to?(:licenses)
665
+ def license_info
666
+ @idrac_client.license_info
306
667
  end
307
668
 
308
- def license_info
309
- @idrac_client.license_info if @idrac_client.respond_to?(:license_info)
669
+ # Network management
670
+
671
+ def get_bmc_network
672
+ @idrac_client.get_bmc_network
673
+ end
674
+
675
+ def set_bmc_network(ipv4: nil, mask: nil, gateway: nil,
676
+ dns_primary: nil, dns_secondary: nil, hostname: nil,
677
+ dhcp: false)
678
+ @idrac_client.set_bmc_network(
679
+ ipv4: ipv4,
680
+ mask: mask,
681
+ gateway: gateway,
682
+ dns_primary: dns_primary,
683
+ dns_secondary: dns_secondary,
684
+ hostname: hostname,
685
+ dhcp: dhcp
686
+ )
687
+ end
688
+
689
+ def set_bmc_dhcp
690
+ @idrac_client.set_bmc_dhcp
310
691
  end
311
692
  end
312
693
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: radfish-idrac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-28 00:00:00.000000000 Z
11
+ date: 2025-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: radfish
@@ -85,7 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
85
  - !ruby/object:Gem::Version
86
86
  version: '0'
87
87
  requirements: []
88
- rubygems_version: 3.3.26
88
+ rubygems_version: 3.5.22
89
89
  signing_key:
90
90
  specification_version: 4
91
91
  summary: Dell iDRAC adapter for Radfish