idrac 0.10.6 → 0.11.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.
- checksums.yaml +4 -4
- data/lib/idrac/boot.rb +124 -14
- data/lib/idrac/firmware.rb +6 -6
- data/lib/idrac/jobs.rb +50 -0
- data/lib/idrac/system_config.rb +7 -1
- data/lib/idrac/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aae19c4ce800e57cb1b15ca37be3d48d1c22f52d2eb455a877fe517f21bd65f4
|
|
4
|
+
data.tar.gz: 1f772c3a6c316578e572e8b797695e19096e1a04e424f38633f6dfad56b6a12c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51c2314dee2b317013816c1e87aacd0d2f5808bb63921826da788d50248e7b84fb871b14abcbc92f92b56ae7cea18d69b5ed32727939549351ee8026510356a1
|
|
7
|
+
data.tar.gz: e2fe2aad75892982b41819ac1dafd13a7317c85e7d427ee6379b21a8b2daa1e2563c87b1ac97d1910ff9f5900058562a727b06b3e104e9a3183cb82850843db0
|
data/lib/idrac/boot.rb
CHANGED
|
@@ -140,7 +140,8 @@ module IDRAC
|
|
|
140
140
|
end
|
|
141
141
|
|
|
142
142
|
# Set boot override for next boot
|
|
143
|
-
def set_boot_override(target,
|
|
143
|
+
def set_boot_override(target, persistence: nil, mode: nil)
|
|
144
|
+
persistence = "Once" unless persistence
|
|
144
145
|
# Validate target against allowed values
|
|
145
146
|
boot_data = boot
|
|
146
147
|
valid_targets = boot_data["allowed_override_targets"]
|
|
@@ -150,11 +151,11 @@ module IDRAC
|
|
|
150
151
|
raise Error, "Invalid boot target: #{target}"
|
|
151
152
|
end
|
|
152
153
|
|
|
153
|
-
debug "Setting boot override to #{target} (#{
|
|
154
|
+
debug "Setting boot override to #{target} (#{persistence})...", 1, :yellow
|
|
154
155
|
|
|
155
156
|
body = {
|
|
156
157
|
"Boot" => {
|
|
157
|
-
"BootSourceOverrideEnabled" =>
|
|
158
|
+
"BootSourceOverrideEnabled" => persistence, # Disabled/Once/Continuous
|
|
158
159
|
"BootSourceOverrideTarget" => target # None/Pxe/Hdd/Cd/etc
|
|
159
160
|
}
|
|
160
161
|
}
|
|
@@ -225,24 +226,24 @@ module IDRAC
|
|
|
225
226
|
end
|
|
226
227
|
|
|
227
228
|
# Convenience methods for common boot targets
|
|
228
|
-
def boot_to_pxe(
|
|
229
|
-
set_boot_override("Pxe",
|
|
229
|
+
def boot_to_pxe(persistence: nil, mode: nil)
|
|
230
|
+
set_boot_override("Pxe", persistence: persistence, mode: mode)
|
|
230
231
|
end
|
|
231
232
|
|
|
232
|
-
def boot_to_disk(
|
|
233
|
-
set_boot_override("Hdd",
|
|
233
|
+
def boot_to_disk(persistence: nil, mode: nil)
|
|
234
|
+
set_boot_override("Hdd", persistence: persistence, mode: mode)
|
|
234
235
|
end
|
|
235
236
|
|
|
236
|
-
def boot_to_cd(
|
|
237
|
-
set_boot_override("Cd",
|
|
237
|
+
def boot_to_cd(persistence: nil, mode: nil)
|
|
238
|
+
set_boot_override("Cd", persistence: persistence, mode: mode)
|
|
238
239
|
end
|
|
239
240
|
|
|
240
|
-
def boot_to_usb(
|
|
241
|
-
set_boot_override("Usb",
|
|
241
|
+
def boot_to_usb(persistence: nil, mode: nil)
|
|
242
|
+
set_boot_override("Usb", persistence: persistence, mode: mode)
|
|
242
243
|
end
|
|
243
244
|
|
|
244
|
-
def boot_to_bios_setup(
|
|
245
|
-
set_boot_override("BiosSetup",
|
|
245
|
+
def boot_to_bios_setup(persistence: nil, mode: nil)
|
|
246
|
+
set_boot_override("BiosSetup", persistence: persistence, mode: mode)
|
|
246
247
|
end
|
|
247
248
|
|
|
248
249
|
private
|
|
@@ -663,5 +664,114 @@ module IDRAC
|
|
|
663
664
|
# Same operation as set_system_configuration_profile: wait on the import JOB.
|
|
664
665
|
return wait_for_scp_import(response.headers["location"])
|
|
665
666
|
end
|
|
667
|
+
|
|
668
|
+
########################################################
|
|
669
|
+
# Boot mechanics moved out of the app's raw Redfish (radfish #40). These used to live in the
|
|
670
|
+
# app's Infra::OsInstall as hand-rolled authenticated_request calls; the iDRAC BootSources /
|
|
671
|
+
# one-time-boot mechanics belong here, the adapter and the radfish facade just expose them.
|
|
672
|
+
########################################################
|
|
673
|
+
|
|
674
|
+
# Stale UEFI boot placeholders a prior OS can leave behind. Dell names them
|
|
675
|
+
# "Unknown.Unknown.<n>-<n>"; left ENABLED they make UEFI loop over dead entries and can keep a
|
|
676
|
+
# node from ever reaching an install (this trapped n008: PXE plus five "Unknown.Unknown" ghosts,
|
|
677
|
+
# no disk entry). The default match targets exactly them.
|
|
678
|
+
STALE_UEFI_BOOT_ENTRY = /\AUnknown\.Unknown\./
|
|
679
|
+
|
|
680
|
+
# The still-ENABLED UEFI boot entries whose Name matches +match+ (default: the stale
|
|
681
|
+
# "Unknown.Unknown.*" placeholders). Returns the raw BootSources entry hashes so callers can read
|
|
682
|
+
# Name/Id/Index; [] when there are none. Read-only.
|
|
683
|
+
def stale_uefi_boot_entries(match: STALE_UEFI_BOOT_ENTRY)
|
|
684
|
+
res = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/BootSources")
|
|
685
|
+
body = res.body.is_a?(String) ? JSON.parse(res.body) : res.body
|
|
686
|
+
seq = body.dig("Attributes", "UefiBootSeq") || []
|
|
687
|
+
seq.select { |e| e["Name"].to_s.match?(match) && e["Enabled"] != false }
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# Disable the UEFI boot entries whose Name matches +match+ (default: the stale
|
|
691
|
+
# "Unknown.Unknown.*" placeholders) and return the NAMES disabled ([] when there was nothing to
|
|
692
|
+
# do). Drains any pending Lifecycle Controller config job FIRST so scheduling ours never trips
|
|
693
|
+
# LC068, then PATCHes BootSources/Settings and POSTs the BIOS config job that applies the change.
|
|
694
|
+
# The disable is applied by a reboot -- the LifecycleController runs the pending job during POST
|
|
695
|
+
# -- so by default this only SCHEDULES it and the caller owns power. Pass wait: true to poll the
|
|
696
|
+
# scheduled BIOS config job to a terminal state here (via wait_config_job) before returning.
|
|
697
|
+
def disable_boot_entries(match: STALE_UEFI_BOOT_ENTRY, wait: false, timeout: 900)
|
|
698
|
+
res = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/BootSources")
|
|
699
|
+
body = res.body.is_a?(String) ? JSON.parse(res.body) : res.body
|
|
700
|
+
seq = body.dig("Attributes", "UefiBootSeq") || []
|
|
701
|
+
stale = seq.select { |e| e["Name"].to_s.match?(match) && e["Enabled"] != false }
|
|
702
|
+
return [] if stale.empty?
|
|
703
|
+
|
|
704
|
+
# A stale pending job would make the config-job POST below hard-fail with LC068; drain first.
|
|
705
|
+
drain_pending_config_jobs!
|
|
706
|
+
|
|
707
|
+
newseq = seq.each_with_index.map do |e, i|
|
|
708
|
+
off = e["Name"].to_s.match?(match)
|
|
709
|
+
{ "Enabled" => (off ? false : e["Enabled"]), "Id" => e["Id"], "Index" => i, "Name" => e["Name"] }
|
|
710
|
+
end
|
|
711
|
+
patch = authenticated_request(:patch, "/redfish/v1/Systems/System.Embedded.1/BootSources/Settings",
|
|
712
|
+
body: JSON.generate("Attributes" => { "UefiBootSeq" => newseq }))
|
|
713
|
+
raise Error, "Disabling stale boot sources failed (HTTP #{patch.status}): #{patch.body}" unless patch.status.between?(200, 299)
|
|
714
|
+
|
|
715
|
+
job = authenticated_request(:post, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs",
|
|
716
|
+
body: JSON.generate("TargetSettingsURI" => "/redfish/v1/Systems/System.Embedded.1/BootSources/Settings"))
|
|
717
|
+
raise Error, "BIOS config job for boot sources failed (HTTP #{job.status}): #{job.body}" unless job.status.between?(200, 299)
|
|
718
|
+
|
|
719
|
+
# Default: the caller's reboot applies the change (LC runs the pending job during POST). With
|
|
720
|
+
# wait: true, poll the scheduled config job to a terminal state -- reuse Jobs#wait_config_job.
|
|
721
|
+
if wait
|
|
722
|
+
headers = job.respond_to?(:headers) ? (job.headers || {}) : {}
|
|
723
|
+
jid = (headers["location"] || headers["Location"]).to_s[/(JID_\w+)/, 1] || job.body.to_s[/(JID_\w+)/, 1]
|
|
724
|
+
wait_config_job(jid, timeout: timeout) if jid
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
names = stale.map { |e| e["Name"] }
|
|
728
|
+
puts "Disabled #{names.size} stale UEFI boot #{names.size == 1 ? 'entry' : 'entries'} " \
|
|
729
|
+
"(#{names.join(', ')}); a BIOS config job applies it at the next boot.".green
|
|
730
|
+
names
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Redfish BootProgress.LastState mapped to the snake_case symbols radfish orders boots by
|
|
734
|
+
# (Radfish::Client::BOOT_PROGRESS_ORDER). Explicit so the acronym cases (PCI, OS) normalize
|
|
735
|
+
# correctly instead of through a naive underscore.
|
|
736
|
+
BOOT_PROGRESS_STATES = {
|
|
737
|
+
"None" => :none,
|
|
738
|
+
"PrimaryProcessorInitializationStarted" => :primary_processor_initialization_started,
|
|
739
|
+
"BusInitializationStarted" => :bus_initialization_started,
|
|
740
|
+
"MemoryInitializationStarted" => :memory_initialization_started,
|
|
741
|
+
"SecondaryProcessorInitializationStarted" => :secondary_processor_initialization_started,
|
|
742
|
+
"PCIResourceConfigStarted" => :pci_resource_config_started,
|
|
743
|
+
"SystemHardwareInitializationComplete" => :system_hardware_initialization_complete,
|
|
744
|
+
"SetupEntered" => :setup_entered,
|
|
745
|
+
"OSBootStarted" => :os_boot_started,
|
|
746
|
+
"OSRunning" => :os_running
|
|
747
|
+
}.freeze
|
|
748
|
+
|
|
749
|
+
# Normalized last BootProgress state (a snake_case Symbol from BOOT_PROGRESS_STATES), or nil when
|
|
750
|
+
# the BMC does not report BootProgress at all (iDRAC8 omits it). nil means "cannot observe", never
|
|
751
|
+
# "not running" -- callers fall back to other signals.
|
|
752
|
+
def boot_progress
|
|
753
|
+
res = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1?$select=BootProgress")
|
|
754
|
+
body = res.body.is_a?(String) ? JSON.parse(res.body) : res.body
|
|
755
|
+
last = body.is_a?(Hash) ? body.dig("BootProgress", "LastState") : nil
|
|
756
|
+
return nil if last.nil? || last.to_s.empty?
|
|
757
|
+
BOOT_PROGRESS_STATES[last] || last.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
# Dell one-time boot to the virtual CD via an SCP import (the reliable path on this fleet). Drains
|
|
761
|
+
# any pending Lifecycle Controller config job first (a stale one makes the import hard-fail with
|
|
762
|
+
# LC068 "a configuration job is already scheduled"), then imports ServerBoot.1#BootOnce +
|
|
763
|
+
# FirstBootDevice=VCD-DVD. BootOnce makes the BIOS fall back to the standing order after one boot,
|
|
764
|
+
# so no boot-order reorder is needed here. Raises on a failed import; returns the import result hash.
|
|
765
|
+
def set_one_time_cd_boot(reboot: false)
|
|
766
|
+
drain_pending_config_jobs!
|
|
767
|
+
scp = { "FQDD" => "iDRAC.Embedded.1", "Attributes" => [
|
|
768
|
+
{ "Name" => "ServerBoot.1#BootOnce", "Value" => "Enabled", "Set On Import" => "True" },
|
|
769
|
+
{ "Name" => "ServerBoot.1#FirstBootDevice", "Value" => "VCD-DVD", "Set On Import" => "True" } ] }
|
|
770
|
+
res = set_system_configuration_profile(scp, target: "ALL", reboot: reboot)
|
|
771
|
+
unless res.is_a?(Hash) && res[:status] == :success
|
|
772
|
+
raise Error, "SCP one-time vCD boot failed: #{res[:job_state]} #{res[:error] || res[:message]} (job #{res[:job_id]})"
|
|
773
|
+
end
|
|
774
|
+
res
|
|
775
|
+
end
|
|
666
776
|
end
|
|
667
|
-
end
|
|
777
|
+
end
|
data/lib/idrac/firmware.rb
CHANGED
|
@@ -590,11 +590,10 @@ module IDRAC
|
|
|
590
590
|
end
|
|
591
591
|
|
|
592
592
|
# Construct the firmware URI from the upload response
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
end
|
|
593
|
+
# Prefer @odata.id from response body, then Location header, then constructed path
|
|
594
|
+
image_uri = upload_data&.dig('@odata.id') ||
|
|
595
|
+
upload_response.headers['Location'] ||
|
|
596
|
+
(firmware_id ? "#{http_push_uri}/#{firmware_id}" : http_push_uri)
|
|
598
597
|
|
|
599
598
|
# Check if Dell OEM Install action is available (iDRAC8)
|
|
600
599
|
# This is more reliable than SimpleUpdate on older iDRACs
|
|
@@ -604,9 +603,10 @@ module IDRAC
|
|
|
604
603
|
if dell_install_target
|
|
605
604
|
puts "Using Dell OEM Install action (iDRAC8)...".light_cyan
|
|
606
605
|
puts "Firmware URI: #{image_uri}".light_cyan
|
|
606
|
+
# iDRAC8 DellUpdateService.Install expects @odata.id URIs
|
|
607
607
|
install_payload = {
|
|
608
608
|
"SoftwareIdentityURIs" => [image_uri],
|
|
609
|
-
"InstallUpon" => "
|
|
609
|
+
"InstallUpon" => "NowAndReboot"
|
|
610
610
|
}
|
|
611
611
|
update_response = client.authenticated_request(
|
|
612
612
|
:post,
|
data/lib/idrac/jobs.rb
CHANGED
|
@@ -234,6 +234,56 @@ module IDRAC
|
|
|
234
234
|
error: "Timed out after #{timeout}s waiting for job #{job_id} (last state: #{state || last_error || 'unknown'})" }
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
+
# Delete every Lifecycle Controller config job that is NOT Completed, and return the ids drained
|
|
238
|
+
# ([] when there was nothing to drain). The iDRAC serializes LC config jobs: one left
|
|
239
|
+
# Scheduled/Running/New/Pending makes the NEXT SCP import or config-job POST hard-fail with LC068
|
|
240
|
+
# ("a configuration job is already scheduled"). Repeated install/break-glass runs leave exactly
|
|
241
|
+
# such a stale job behind (this wedged n000), so anything that schedules its own config job drains
|
|
242
|
+
# first. Completed jobs are harmless and are kept. Best-effort: a failed queue read/delete is
|
|
243
|
+
# logged and swallowed (returns []), and the later schedule surfaces LC068 the old way rather than
|
|
244
|
+
# this raising. Moved out of the app's raw Redfish (radfish #40).
|
|
245
|
+
def drain_pending_config_jobs!
|
|
246
|
+
resp = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs?$expand=*($levels=1)")
|
|
247
|
+
return [] unless resp.status.to_i == 200
|
|
248
|
+
pending = (JSON.parse(resp.body)["Members"] || []).reject { |j| j["JobState"].to_s == "Completed" }
|
|
249
|
+
return [] if pending.empty?
|
|
250
|
+
puts "Draining #{pending.size} pending config job(s) that would block a new one (LC068): " \
|
|
251
|
+
"#{pending.map { |j| "#{j['Id']}=#{j['JobState']}" }.join(', ')}".yellow
|
|
252
|
+
pending.each { |j| authenticated_request(:delete, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/#{j['Id']}") }
|
|
253
|
+
pending.map { |j| j["Id"] }
|
|
254
|
+
rescue StandardError => e
|
|
255
|
+
puts "Could not drain pending config jobs (#{e.class}: #{e.message.lines.first.to_s.strip}); proceeding.".yellow
|
|
256
|
+
[]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Job states that mean a BIOS/config job has finished, one way or another.
|
|
260
|
+
CONFIG_JOB_TERMINAL_STATES = %w[Completed Failed CompletedWithErrors].freeze
|
|
261
|
+
|
|
262
|
+
# Poll a Lifecycle Controller job (e.g. the BIOS config job that a BootSources change schedules)
|
|
263
|
+
# until it reaches a terminal state, and RETURN that state string. Returns nil on timeout rather
|
|
264
|
+
# than raising -- the caller proceeds without confirmation (a still-pending job costs a slower
|
|
265
|
+
# boot, not a wrong one). A transient read error is treated as "keep polling". Moved out of the
|
|
266
|
+
# app's raw Redfish (radfish #40).
|
|
267
|
+
def wait_config_job(jid, timeout: 900, interval: 20)
|
|
268
|
+
deadline = Time.now + timeout
|
|
269
|
+
loop do
|
|
270
|
+
state = begin
|
|
271
|
+
res = authenticated_request(:get, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/#{jid}")
|
|
272
|
+
body = res.body.is_a?(String) ? JSON.parse(res.body) : res.body
|
|
273
|
+
body.is_a?(Hash) ? body["JobState"] : nil
|
|
274
|
+
rescue StandardError
|
|
275
|
+
nil
|
|
276
|
+
end
|
|
277
|
+
return state if CONFIG_JOB_TERMINAL_STATES.include?(state)
|
|
278
|
+
if Time.now > deadline
|
|
279
|
+
puts "Config job #{jid} did not finish within #{timeout}s (last state #{state.inspect}); " \
|
|
280
|
+
"proceeding without confirmation.".yellow
|
|
281
|
+
return nil
|
|
282
|
+
end
|
|
283
|
+
sleep interval
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
237
287
|
# Get system tasks
|
|
238
288
|
def tasks
|
|
239
289
|
response = authenticated_request(:get, '/redfish/v1/TaskService/Tasks')
|
data/lib/idrac/system_config.rb
CHANGED
|
@@ -90,7 +90,13 @@ module IDRAC
|
|
|
90
90
|
#
|
|
91
91
|
# { status: :success | :failed | :timeout, job_id:, job_state:, message:,
|
|
92
92
|
# messages: [message], job: <raw job data>, error: <message unless :success> }
|
|
93
|
-
def set_system_configuration_profile(scp, target: "ALL", reboot: false, timeout: 600, retry_count: 0)
|
|
93
|
+
def set_system_configuration_profile(scp, target: "ALL", reboot: false, timeout: 600, retry_count: 0, drain: true)
|
|
94
|
+
# A stale Lifecycle Controller job left pending/scheduled makes this import hard-fail with
|
|
95
|
+
# RED/LC068 ("a configuration job is already scheduled"). Anticipate it: drain any still-
|
|
96
|
+
# incomplete config job first (self-heal, don't hard-fail) so the import goes through. Pass
|
|
97
|
+
# drain: false to keep an in-flight job (e.g. when a long-running job must not be disturbed).
|
|
98
|
+
drain_pending_config_jobs! if drain
|
|
99
|
+
|
|
94
100
|
# Ensure scp has the proper structure with SystemConfiguration wrapper
|
|
95
101
|
scp_to_apply = if scp.is_a?(Hash) && scp["SystemConfiguration"]
|
|
96
102
|
scp
|
data/lib/idrac/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: idrac
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonathan Siegel
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: httparty
|
|
@@ -281,6 +282,7 @@ licenses:
|
|
|
281
282
|
metadata:
|
|
282
283
|
homepage_uri: https://github.com/buildio/idrac
|
|
283
284
|
source_code_uri: https://github.com/buildio/idrac
|
|
285
|
+
post_install_message:
|
|
284
286
|
rdoc_options: []
|
|
285
287
|
require_paths:
|
|
286
288
|
- lib
|
|
@@ -295,7 +297,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
295
297
|
- !ruby/object:Gem::Version
|
|
296
298
|
version: '0'
|
|
297
299
|
requirements: []
|
|
298
|
-
rubygems_version: 3.
|
|
300
|
+
rubygems_version: 3.5.16
|
|
301
|
+
signing_key:
|
|
299
302
|
specification_version: 4
|
|
300
303
|
summary: API Client for Dell iDRAC
|
|
301
304
|
test_files: []
|