radfish-idrac 0.1.3 → 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: 21ca9aaace52a026497cd359cb588c3d5263246834b815f186fa5ab8fc2128f8
4
- data.tar.gz: 8c7a0867b543a73eec9cebfc9a86367e4a6c9cbcd415ca09c59483f88a88d110
3
+ metadata.gz: a58b55f6a0455e66cd44bed5363451e773c675d2a9fd65d7fa5e6bae77669778
4
+ data.tar.gz: fab0e351a3112e0376b6d065e5faeb6280f7a2dfea90db9d8845bca67e82e4fb
5
5
  SHA512:
6
- metadata.gz: 76bdaa10dd93e366ba582f6e9e06faca9faa6d722c9d425abb6ff209b7e7aad904ab1764050bdbad660eb34b6532902a943228dd96979c1b5d88fc2b780083ad
7
- data.tar.gz: d7befbdcc8e1652e2b2b11fd3b2b086f6a0f3a65978873a5aec6688d7dd5f948bdc89df5d0109075991123d71b5b50d966f9ebd5c670e522e4642c060c493288
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.3"
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
@@ -63,30 +64,95 @@ module Radfish
63
64
  @idrac_client.get_power_state
64
65
  end
65
66
 
66
- def power_on
67
- @idrac_client.power_on
68
- end
69
-
70
- def power_off(force: false)
71
- kind = force ? "ForceOff" : "GracefulShutdown"
72
- @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
73
88
  end
74
89
 
75
- def power_restart(force: false)
76
- # iDRAC uses reboot method, which is ForceRestart by default
77
- if force
78
- @idrac_client.reboot
79
- else
80
- # Try graceful restart first
81
- @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
82
146
  end
147
+
148
+ result
83
149
  end
84
150
 
85
- def power_cycle
86
- # iDRAC doesn't have power_cycle, simulate with off then on
87
- power_off
151
+ def power_cycle(wait: true)
152
+ # Power cycle: turn off then on
153
+ power_off(type: "ForceOff", wait: wait)
88
154
  sleep 5
89
- power_on
155
+ power_on(wait: wait)
90
156
  end
91
157
 
92
158
  def reset_type_allowed
@@ -97,35 +163,123 @@ module Radfish
97
163
  # System information
98
164
 
99
165
  def system_info
100
- @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
101
207
  end
102
208
 
103
209
  def cpus
104
- @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
105
231
  end
106
232
 
107
233
  def memory
108
- @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) }
109
239
  end
110
240
 
111
241
  def nics
112
- @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
113
252
  end
114
253
 
115
254
  def fans
116
- @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
117
261
  end
118
262
 
119
263
  def temperatures
120
- @idrac_client.temperatures
264
+ # iDRAC doesn't provide a dedicated temperatures method
265
+ # Return empty array to satisfy the interface
266
+ []
121
267
  end
122
268
 
123
269
  def psus
124
- @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
125
276
  end
126
277
 
127
278
  def power_consumption
128
- @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
+ }
129
283
  end
130
284
 
131
285
  def power_consumption_watts
@@ -135,7 +289,17 @@ module Radfish
135
289
  # Storage
136
290
 
137
291
  def storage_controllers
138
- @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
139
303
  end
140
304
 
141
305
  def drives
@@ -156,12 +320,41 @@ module Radfish
156
320
  @idrac_client.virtual_media
157
321
  end
158
322
 
159
- 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"
160
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}"
161
346
  end
162
347
 
163
348
  def eject_virtual_media(device: "CD")
164
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}"
165
358
  end
166
359
 
167
360
  def virtual_media_status
@@ -188,12 +381,24 @@ module Radfish
188
381
 
189
382
  # Boot configuration
190
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
+
191
394
  def boot_options
192
- @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) }
193
398
  end
194
399
 
195
- def set_boot_override(target, persistent: false)
196
- @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)
197
402
  end
198
403
 
199
404
  def clear_boot_override
@@ -208,24 +413,45 @@ module Radfish
208
413
  @idrac_client.get_boot_devices
209
414
  end
210
415
 
211
- def boot_to_pxe
212
- @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)
213
422
  end
214
423
 
215
- def boot_to_disk
216
- @idrac_client.boot_to_disk
424
+ def boot_to_cd(enabled: "Once", mode: nil)
425
+ @idrac_client.boot_to_cd(enabled: enabled, mode: mode)
217
426
  end
218
427
 
219
- def boot_to_cd
220
- @idrac_client.boot_to_cd
428
+ def boot_to_usb(enabled: "Once", mode: nil)
429
+ @idrac_client.boot_to_usb(enabled: enabled, mode: mode)
221
430
  end
222
431
 
223
- def boot_to_usb
224
- @idrac_client.boot_to_usb
432
+ def boot_to_bios_setup(enabled: "Once", mode: nil)
433
+ @idrac_client.boot_to_bios_setup(enabled: enabled, mode: mode)
225
434
  end
226
435
 
227
- def boot_to_bios_setup
228
- @idrac_client.boot_to_bios_setup
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) }
444
+ end
445
+
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) }
229
455
  end
230
456
 
231
457
  # Jobs
@@ -246,14 +472,112 @@ module Radfish
246
472
  @idrac_client.cancel_job(job_id)
247
473
  end
248
474
 
249
- def clear_completed_jobs
250
- @idrac_client.clear_jobs
475
+ def clear_jobs!
476
+ @idrac_client.clear_jobs!
251
477
  end
252
478
 
253
479
  def jobs_summary
254
480
  jobs
255
481
  end
256
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
+
257
581
  # Utility
258
582
 
259
583
  def sel_log
@@ -296,18 +620,50 @@ module Radfish
296
620
  @idrac_client.get_firmware_version
297
621
  end
298
622
 
299
- # Additional iDRAC-specific methods
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
300
652
 
301
- def screenshot
302
- @idrac_client.screenshot if @idrac_client.respond_to?(:screenshot)
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)
303
657
  end
304
658
 
305
- def licenses
306
- @idrac_client.licenses if @idrac_client.respond_to?(:licenses)
659
+ # Additional iDRAC-specific methods
660
+
661
+ def screenshot
662
+ @idrac_client.screenshot
307
663
  end
308
664
 
309
665
  def license_info
310
- @idrac_client.license_info if @idrac_client.respond_to?(:license_info)
666
+ @idrac_client.license_info
311
667
  end
312
668
 
313
669
  # Network management
@@ -316,12 +672,12 @@ module Radfish
316
672
  @idrac_client.get_bmc_network
317
673
  end
318
674
 
319
- def set_bmc_network(ip_address: nil, subnet_mask: nil, gateway: nil,
675
+ def set_bmc_network(ipv4: nil, mask: nil, gateway: nil,
320
676
  dns_primary: nil, dns_secondary: nil, hostname: nil,
321
677
  dhcp: false)
322
678
  @idrac_client.set_bmc_network(
323
- ip_address: ip_address,
324
- subnet_mask: subnet_mask,
679
+ ipv4: ipv4,
680
+ mask: mask,
325
681
  gateway: gateway,
326
682
  dns_primary: dns_primary,
327
683
  dns_secondary: dns_secondary,
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.3
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