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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fae473e8260d010235ebf86c7ed40b33dae6b7a1c14c2576bce55fecc319c7be
4
- data.tar.gz: 8c115aeb8c6399d2d3d167204b160ef691b64ec41570b371912df697b23d9789
3
+ metadata.gz: bfa684ed67c2db2f200633f5db13d173a30f49c67b07b05da83844708bdacfa4
4
+ data.tar.gz: 1308339f369f5d9ec0c028f64effac7d43e39cc93dcfad88e2d5d88a6b954f53
5
5
  SHA512:
6
- metadata.gz: 278b81e8955021924c44c496b0df20c34f59e3630f9df3decda96ee519cadc0445901cddbf3c78c174313c36eef558780ce444fb09f3d870039adc23e7da5056
7
- data.tar.gz: c66f192778e2e32cf724b3b98ef45c2311a79ee8f0144e0f95a3106ff1d2d87f67f13c30eac4a47c2055e302bb5b2dc373cf37cfc760ff2566a8f949531db061
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')
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IDRAC
4
- VERSION = "0.10.6"
4
+ VERSION = "0.10.7"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idrac
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.6
4
+ version: 0.10.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel