From 9b48f9f68775cd16324d995d907dcc72482bd6e3 Mon Sep 17 00:00:00 2001 From: Yusuke Hirota Date: Tue, 6 Jan 2026 14:42:39 +0900 Subject: [PATCH] Fixes #38914 - Use ETag-aware patch_if_match for Redfish operations Some Redfish implementations reject PATCH requests without If-Match. This change updates boot device operations to use patch_if_match, enabling ETag-based conditional PATCH when needed. --- bundler.d/bmc.rb | 5 +- modules/bmc/redfish.rb | 13 ++-- test/bmc/bmc_redfish_test.rb | 126 +++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 8 deletions(-) diff --git a/bundler.d/bmc.rb b/bundler.d/bmc.rb index fc737e5b4..877ff6c32 100644 --- a/bundler.d/bmc.rb +++ b/bundler.d/bmc.rb @@ -1,6 +1,9 @@ group :bmc do gem 'rubyipmi', '>= 0.10.0' - gem 'redfish_client', '>= 0.6.0' + # Temporarily use forked redfish_client with ETag support (draft PR) + # TODO: Update to released version once PR is merged + gem 'redfish_client', git: 'https://github.com/spesnova717/redfish-client-ruby.git', branch: 'feature/redfish-etag-support' + # gem 'redfish_client', '>= 0.7.0' # Uncomment after PR is merged # observer is a transitive dependency of rubyipmi # Changed from a default gem to a bundled gem in Ruby 3.4 See https://stdgems.org/new-in/3.4/ # This is a workaround, till https://github.com/logicminds/rubyipmi/pull/61 is live diff --git a/modules/bmc/redfish.rb b/modules/bmc/redfish.rb index ff9f43f4e..4e959a7a7 100644 --- a/modules/bmc/redfish.rb +++ b/modules/bmc/redfish.rb @@ -131,13 +131,12 @@ def bootdevice=(args = { :device => nil, :reboot => false, :persistent => false 'disk' => 'Hdd', 'pxe' => 'Pxe' } - system.patch( - payload: { - 'Boot' => { - 'BootSourceOverrideTarget' => devmap[args[:device]], - 'BootSourceOverrideEnabled' => args[:persistent] ? 'Enabled' : 'Once', - }, - }) + system.patch_if_match({ + 'Boot' => { + 'BootSourceOverrideTarget' => devmap[args[:device]], + 'BootSourceOverrideEnabled' => args[:persistent] ? 'Enabled' : 'Once', + }, + }) powercycle if args[:reboot] end diff --git a/test/bmc/bmc_redfish_test.rb b/test/bmc/bmc_redfish_test.rb index 3c0064bbb..024af4bcf 100644 --- a/test/bmc/bmc_redfish_test.rb +++ b/test/bmc/bmc_redfish_test.rb @@ -35,4 +35,130 @@ def test_redfish_provider_reboot to_return(status: 200, body: JSON.generate({})) assert @bmc.powerreboot end + + def test_bootdevice_pxe_uses_patch_if_match + # Test that bootdevice uses patch_if_match for PXE boot + system_mock = mock('system') + + system_mock.expects(:patch_if_match).with( + 'Boot' => { + 'BootSourceOverrideTarget' => 'Pxe', + 'BootSourceOverrideEnabled' => 'Once', + } + ).returns(true) + + @bmc.expects(:system).returns(system_mock) + @bmc.expects(:powercycle).never + + result = @bmc.bootdevice = { :device => 'pxe', :reboot => false, :persistent => false } + assert_not_nil result + end + + def test_bootdevice_disk_persistent_uses_patch_if_match + # Test that bootdevice uses patch_if_match with persistent boot + system_mock = mock('system') + + system_mock.expects(:patch_if_match).with( + 'Boot' => { + 'BootSourceOverrideTarget' => 'Hdd', + 'BootSourceOverrideEnabled' => 'Enabled', + } + ).returns(true) + + @bmc.expects(:system).returns(system_mock) + @bmc.expects(:powercycle).never + + result = @bmc.bootdevice = { :device => 'disk', :reboot => false, :persistent => true } + assert_not_nil result + end + + def test_bootdevice_with_reboot + # Test bootdevice with reboot option + system_mock = mock('system') + + system_mock.expects(:patch_if_match).with( + 'Boot' => { + 'BootSourceOverrideTarget' => 'Pxe', + 'BootSourceOverrideEnabled' => 'Enabled', + } + ).returns(true) + + @bmc.expects(:system).returns(system_mock) + @bmc.expects(:powercycle).once + + result = @bmc.bootdevice = { :device => 'pxe', :reboot => true, :persistent => true } + assert_not_nil result + end + + def test_bootpxe_calls_bootdevice + # Test that convenience method bootpxe works + system_mock = mock('system') + + system_mock.expects(:patch_if_match).with( + 'Boot' => { + 'BootSourceOverrideTarget' => 'Pxe', + 'BootSourceOverrideEnabled' => 'Once', + } + ).returns(true) + + @bmc.expects(:system).returns(system_mock) + @bmc.expects(:powercycle).never + + result = @bmc.bootpxe(false, false) + assert_not_nil result + end + + def test_bootdisk_calls_bootdevice + # Test that convenience method bootdisk works + system_mock = mock('system') + + system_mock.expects(:patch_if_match).with( + 'Boot' => { + 'BootSourceOverrideTarget' => 'Hdd', + 'BootSourceOverrideEnabled' => 'Once', + } + ).returns(true) + + @bmc.expects(:system).returns(system_mock) + @bmc.expects(:powercycle).never + + result = @bmc.bootdisk(false, false) + assert_not_nil result + end + + def test_bootbios_calls_bootdevice + # Test that convenience method bootbios works + system_mock = mock('system') + + system_mock.expects(:patch_if_match).with( + 'Boot' => { + 'BootSourceOverrideTarget' => 'BiosSetup', + 'BootSourceOverrideEnabled' => 'Once', + } + ).returns(true) + + @bmc.expects(:system).returns(system_mock) + @bmc.expects(:powercycle).never + + result = @bmc.bootbios(false, false) + assert_not_nil result + end + + def test_bootcdrom_calls_bootdevice + # Test that convenience method bootcdrom works + system_mock = mock('system') + + system_mock.expects(:patch_if_match).with( + 'Boot' => { + 'BootSourceOverrideTarget' => 'Cd', + 'BootSourceOverrideEnabled' => 'Once', + } + ).returns(true) + + @bmc.expects(:system).returns(system_mock) + @bmc.expects(:powercycle).never + + result = @bmc.bootcdrom(false, false) + assert_not_nil result + end end