idrac 0.10.6 → 0.10.7
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 +110 -1
- data/lib/idrac/jobs.rb +50 -0
- data/lib/idrac/system_config.rb +7 -1
- data/lib/idrac/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfa684ed67c2db2f200633f5db13d173a30f49c67b07b05da83844708bdacfa4
|
|
4
|
+
data.tar.gz: 1308339f369f5d9ec0c028f64effac7d43e39cc93dcfad88e2d5d88a6b954f53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abc37992c28ab713b5960b51144bcda9f3d80ed197e7a35284c5509392f82b258072bfce2b3a3b1c3d30956b2996459b42a18582de2896d61a1e3df6851a2946
|
|
7
|
+
data.tar.gz: 65219a49a799565089f2bb59722314ba3433a2c5dc17c035d92a2bf0d6a1712b4c964f0489fa0d1a5935a65390ed9d1fbd4c99a7832d055b11730739d5068f6a
|
data/lib/idrac/boot.rb
CHANGED
|
@@ -663,5 +663,114 @@ module IDRAC
|
|
|
663
663
|
# Same operation as set_system_configuration_profile: wait on the import JOB.
|
|
664
664
|
return wait_for_scp_import(response.headers["location"])
|
|
665
665
|
end
|
|
666
|
+
|
|
667
|
+
########################################################
|
|
668
|
+
# Boot mechanics moved out of the app's raw Redfish (radfish #40). These used to live in the
|
|
669
|
+
# app's Infra::OsInstall as hand-rolled authenticated_request calls; the iDRAC BootSources /
|
|
670
|
+
# one-time-boot mechanics belong here, the adapter and the radfish facade just expose them.
|
|
671
|
+
########################################################
|
|
672
|
+
|
|
673
|
+
# Stale UEFI boot placeholders a prior OS can leave behind. Dell names them
|
|
674
|
+
# "Unknown.Unknown.<n>-<n>"; left ENABLED they make UEFI loop over dead entries and can keep a
|
|
675
|
+
# node from ever reaching an install (this trapped n008: PXE plus five "Unknown.Unknown" ghosts,
|
|
676
|
+
# no disk entry). The default match targets exactly them.
|
|
677
|
+
STALE_UEFI_BOOT_ENTRY = /\AUnknown\.Unknown\./
|
|
678
|
+
|
|
679
|
+
# The still-ENABLED UEFI boot entries whose Name matches +match+ (default: the stale
|
|
680
|
+
# "Unknown.Unknown.*" placeholders). Returns the raw BootSources entry hashes so callers can read
|
|
681
|
+
# Name/Id/Index; [] when there are none. Read-only.
|
|
682
|
+
def stale_uefi_boot_entries(match: STALE_UEFI_BOOT_ENTRY)
|
|
683
|
+
res = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/BootSources")
|
|
684
|
+
body = res.body.is_a?(String) ? JSON.parse(res.body) : res.body
|
|
685
|
+
seq = body.dig("Attributes", "UefiBootSeq") || []
|
|
686
|
+
seq.select { |e| e["Name"].to_s.match?(match) && e["Enabled"] != false }
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
# Disable the UEFI boot entries whose Name matches +match+ (default: the stale
|
|
690
|
+
# "Unknown.Unknown.*" placeholders) and return the NAMES disabled ([] when there was nothing to
|
|
691
|
+
# do). Drains any pending Lifecycle Controller config job FIRST so scheduling ours never trips
|
|
692
|
+
# LC068, then PATCHes BootSources/Settings and POSTs the BIOS config job that applies the change.
|
|
693
|
+
# The disable is applied by a reboot -- the LifecycleController runs the pending job during POST
|
|
694
|
+
# -- so by default this only SCHEDULES it and the caller owns power. Pass wait: true to poll the
|
|
695
|
+
# scheduled BIOS config job to a terminal state here (via wait_config_job) before returning.
|
|
696
|
+
def disable_boot_entries(match: STALE_UEFI_BOOT_ENTRY, wait: false, timeout: 900)
|
|
697
|
+
res = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1/BootSources")
|
|
698
|
+
body = res.body.is_a?(String) ? JSON.parse(res.body) : res.body
|
|
699
|
+
seq = body.dig("Attributes", "UefiBootSeq") || []
|
|
700
|
+
stale = seq.select { |e| e["Name"].to_s.match?(match) && e["Enabled"] != false }
|
|
701
|
+
return [] if stale.empty?
|
|
702
|
+
|
|
703
|
+
# A stale pending job would make the config-job POST below hard-fail with LC068; drain first.
|
|
704
|
+
drain_pending_config_jobs!
|
|
705
|
+
|
|
706
|
+
newseq = seq.each_with_index.map do |e, i|
|
|
707
|
+
off = e["Name"].to_s.match?(match)
|
|
708
|
+
{ "Enabled" => (off ? false : e["Enabled"]), "Id" => e["Id"], "Index" => i, "Name" => e["Name"] }
|
|
709
|
+
end
|
|
710
|
+
patch = authenticated_request(:patch, "/redfish/v1/Systems/System.Embedded.1/BootSources/Settings",
|
|
711
|
+
body: JSON.generate("Attributes" => { "UefiBootSeq" => newseq }))
|
|
712
|
+
raise Error, "Disabling stale boot sources failed (HTTP #{patch.status}): #{patch.body}" unless patch.status.between?(200, 299)
|
|
713
|
+
|
|
714
|
+
job = authenticated_request(:post, "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs",
|
|
715
|
+
body: JSON.generate("TargetSettingsURI" => "/redfish/v1/Systems/System.Embedded.1/BootSources/Settings"))
|
|
716
|
+
raise Error, "BIOS config job for boot sources failed (HTTP #{job.status}): #{job.body}" unless job.status.between?(200, 299)
|
|
717
|
+
|
|
718
|
+
# Default: the caller's reboot applies the change (LC runs the pending job during POST). With
|
|
719
|
+
# wait: true, poll the scheduled config job to a terminal state -- reuse Jobs#wait_config_job.
|
|
720
|
+
if wait
|
|
721
|
+
headers = job.respond_to?(:headers) ? (job.headers || {}) : {}
|
|
722
|
+
jid = (headers["location"] || headers["Location"]).to_s[/(JID_\w+)/, 1] || job.body.to_s[/(JID_\w+)/, 1]
|
|
723
|
+
wait_config_job(jid, timeout: timeout) if jid
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
names = stale.map { |e| e["Name"] }
|
|
727
|
+
puts "Disabled #{names.size} stale UEFI boot #{names.size == 1 ? 'entry' : 'entries'} " \
|
|
728
|
+
"(#{names.join(', ')}); a BIOS config job applies it at the next boot.".green
|
|
729
|
+
names
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# Redfish BootProgress.LastState mapped to the snake_case symbols radfish orders boots by
|
|
733
|
+
# (Radfish::Client::BOOT_PROGRESS_ORDER). Explicit so the acronym cases (PCI, OS) normalize
|
|
734
|
+
# correctly instead of through a naive underscore.
|
|
735
|
+
BOOT_PROGRESS_STATES = {
|
|
736
|
+
"None" => :none,
|
|
737
|
+
"PrimaryProcessorInitializationStarted" => :primary_processor_initialization_started,
|
|
738
|
+
"BusInitializationStarted" => :bus_initialization_started,
|
|
739
|
+
"MemoryInitializationStarted" => :memory_initialization_started,
|
|
740
|
+
"SecondaryProcessorInitializationStarted" => :secondary_processor_initialization_started,
|
|
741
|
+
"PCIResourceConfigStarted" => :pci_resource_config_started,
|
|
742
|
+
"SystemHardwareInitializationComplete" => :system_hardware_initialization_complete,
|
|
743
|
+
"SetupEntered" => :setup_entered,
|
|
744
|
+
"OSBootStarted" => :os_boot_started,
|
|
745
|
+
"OSRunning" => :os_running
|
|
746
|
+
}.freeze
|
|
747
|
+
|
|
748
|
+
# Normalized last BootProgress state (a snake_case Symbol from BOOT_PROGRESS_STATES), or nil when
|
|
749
|
+
# the BMC does not report BootProgress at all (iDRAC8 omits it). nil means "cannot observe", never
|
|
750
|
+
# "not running" -- callers fall back to other signals.
|
|
751
|
+
def boot_progress
|
|
752
|
+
res = authenticated_request(:get, "/redfish/v1/Systems/System.Embedded.1?$select=BootProgress")
|
|
753
|
+
body = res.body.is_a?(String) ? JSON.parse(res.body) : res.body
|
|
754
|
+
last = body.is_a?(Hash) ? body.dig("BootProgress", "LastState") : nil
|
|
755
|
+
return nil if last.nil? || last.to_s.empty?
|
|
756
|
+
BOOT_PROGRESS_STATES[last] || last.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# Dell one-time boot to the virtual CD via an SCP import (the reliable path on this fleet). Drains
|
|
760
|
+
# any pending Lifecycle Controller config job first (a stale one makes the import hard-fail with
|
|
761
|
+
# LC068 "a configuration job is already scheduled"), then imports ServerBoot.1#BootOnce +
|
|
762
|
+
# FirstBootDevice=VCD-DVD. BootOnce makes the BIOS fall back to the standing order after one boot,
|
|
763
|
+
# so no boot-order reorder is needed here. Raises on a failed import; returns the import result hash.
|
|
764
|
+
def set_one_time_cd_boot(reboot: false)
|
|
765
|
+
drain_pending_config_jobs!
|
|
766
|
+
scp = { "FQDD" => "iDRAC.Embedded.1", "Attributes" => [
|
|
767
|
+
{ "Name" => "ServerBoot.1#BootOnce", "Value" => "Enabled", "Set On Import" => "True" },
|
|
768
|
+
{ "Name" => "ServerBoot.1#FirstBootDevice", "Value" => "VCD-DVD", "Set On Import" => "True" } ] }
|
|
769
|
+
res = set_system_configuration_profile(scp, target: "ALL", reboot: reboot)
|
|
770
|
+
unless res.is_a?(Hash) && res[:status] == :success
|
|
771
|
+
raise Error, "SCP one-time vCD boot failed: #{res[:job_state]} #{res[:error] || res[:message]} (job #{res[:job_id]})"
|
|
772
|
+
end
|
|
773
|
+
res
|
|
774
|
+
end
|
|
666
775
|
end
|
|
667
|
-
end
|
|
776
|
+
end
|
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