-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #69 from xlab-si/firmware-update
Internal state machine implementation for firmware update
- Loading branch information
Showing
6 changed files
with
263 additions
and
0 deletions.
There are no files selected for viewing
3 changes: 3 additions & 0 deletions
3
app/models/manageiq/providers/redfish/physical_infra_manager/firmware_update_task.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
class ManageIQ::Providers::Redfish::PhysicalInfraManager::FirmwareUpdateTask < ManageIQ::Providers::PhysicalInfraManager::FirmwareUpdateTask | ||
include_concern 'StateMachine' | ||
end |
38 changes: 38 additions & 0 deletions
38
...s/manageiq/providers/redfish/physical_infra_manager/firmware_update_task/state_machine.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
module ManageIQ::Providers::Redfish::PhysicalInfraManager::FirmwareUpdateTask::StateMachine | ||
def start_firmware_update | ||
update_and_notify_parent(:message => msg('start firmware update')) | ||
signal :trigger_firmware_update | ||
end | ||
|
||
def trigger_firmware_update | ||
update_and_notify_parent(:message => msg('trigger firmware update via Redfish')) | ||
unless (firmware_binary = FirmwareBinary.find_by(:id => options[:firmware_binary_id])) | ||
raise MiqException::MiqFirmwareUpdateError, "FirmwareBinary with id #{options[:firmware_binary_id]} not found" | ||
end | ||
unless (servers = PhysicalServer.where(:id => options[:src_ids])) && servers.size == options[:src_ids].size | ||
raise MiqException::MiqFirmwareUpdateError, "At least one PhysicalServer of #{options[:src_ids]} not found" | ||
end | ||
|
||
response = miq_request.affected_ems.update_firmware_async(firmware_binary, servers) | ||
if !response.done? | ||
phase_context[:monitor] = response.to_h | ||
signal :poll_firmware_update | ||
else | ||
signal :done_firmware_update | ||
end | ||
end | ||
|
||
def poll_firmware_update | ||
update_and_notify_parent(:message => msg('poll firmware update')) | ||
miq_request.affected_ems.with_provider_connection do |client| | ||
old_response = RedfishClient::Response.from_hash(phase_context[:monitor]) | ||
response = client.get(:path => old_response.monitor) | ||
if response.done? | ||
signal :done_firmware_update | ||
else | ||
phase_context[:monitor] = response.to_h | ||
requeue_phase | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
app/models/manageiq/providers/redfish/physical_infra_manager/operations/firmware.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
module ManageIQ::Providers::Redfish | ||
module PhysicalInfraManager::Operations::Firmware | ||
def update_firmware_async(firmware_binary, servers) | ||
validate_update_firmware(firmware_binary, servers) | ||
|
||
with_provider_connection do |client| | ||
update_service = client.UpdateService | ||
raise MiqException::MiqFirmwareUpdateError, 'UpdateService is not enabled' unless update_service | ||
|
||
protocol, url = compatible_firmware_url(update_service, firmware_binary) | ||
|
||
response = update_service.Actions["#UpdateService.SimpleUpdate"].post( | ||
:field => 'target', | ||
:payload => { | ||
:ImageURI => url, | ||
:TransferProtocol => protocol, | ||
:Targets => servers.map(&:ems_ref), | ||
} | ||
) | ||
|
||
unless [200, 202].include?(response.status) | ||
raise MiqException::MiqFirmwareUpdateError, | ||
"Cannot update firmware: (#{response.status}) #{response.data[:body]}" | ||
end | ||
|
||
response | ||
end | ||
end | ||
|
||
def compatible_firmware_url(update_service, firmware_binary) | ||
update_action = update_service.Actions['#UpdateService.SimpleUpdate'] | ||
requested = update_action['[email protected]'] | ||
requested ||= begin | ||
params = update_action['[email protected]']&.Parameters || [] | ||
params.find { |p| p.Name == 'TransferProtocol' }&.AllowableValues | ||
end | ||
if requested.blank? | ||
raise MiqException::MiqFirmwareUpdateError, 'Redfish supports zero transfer protocols' | ||
end | ||
|
||
url = firmware_binary.endpoints.map(&:url).find { |u| requested.include?(u.split('://').first.upcase) } | ||
raise MiqException::MiqFirmwareUpdateError, 'No compatible transfer protocol' unless url | ||
|
||
[url.split('://').first.upcase, url] | ||
end | ||
|
||
private | ||
|
||
def validate_update_firmware(firmware_binary, servers) | ||
raise MiqException::MiqFirmwareUpdateError, 'At least one server must be selected' if servers.empty? | ||
|
||
incompatible = servers.reject { |s| s.firmware_compatible?(firmware_binary) } | ||
unless incompatible.empty? | ||
raise MiqException::MiqFirmwareUpdateError, "Servers not compatible with firmware: #{incompatible.map(&:id)}" | ||
end | ||
end | ||
end | ||
end |
61 changes: 61 additions & 0 deletions
61
...s/manageiq/providers/redfish/physical_infra_manager/firmware_update/state_machine_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
describe ManageIQ::Providers::Redfish::PhysicalInfraManager::FirmwareUpdateTask do | ||
before { EvmSpecHelper.create_guid_miq_server_zone } | ||
|
||
let(:ems) { FactoryBot.create(:ems_redfish_physical_infra) } | ||
let(:server) { FactoryBot.create(:physical_server, :ext_management_system => ems) } | ||
let(:request) { FactoryBot.create(:physical_server_firmware_update_request, :options => options) } | ||
let(:firmware_binary) { FactoryBot.create(:firmware_binary) } | ||
|
||
subject { described_class.new(:source => server, :miq_request => request) } | ||
|
||
describe 'run state machine' do | ||
before { subject.update(:options => options) } | ||
before { allow(subject).to receive(:requeue_phase) { subject.send(subject.phase) } } | ||
before do | ||
allow(subject).to receive(:signal) do |method| | ||
subject.phase = method | ||
subject.send(method) | ||
end | ||
end | ||
|
||
context 'abort when missing firmware binary' do | ||
let(:options) { { :firmware_binary_id => 'missing' } } | ||
it do | ||
expect { subject.start_firmware_update }.to raise_error(MiqException::MiqFirmwareUpdateError) | ||
end | ||
end | ||
|
||
context 'abort when missing one of servers' do | ||
let(:options) { { :src_ids => [server.id, 'missing'], :firmware_binary_id => firmware_binary.id } } | ||
it do | ||
expect { subject.start_firmware_update }.to raise_error(MiqException::MiqFirmwareUpdateError) | ||
end | ||
end | ||
|
||
context 'when all steps succeed' do | ||
let(:options) { { :src_ids => [server.id], :firmware_binary_id => firmware_binary.id } } | ||
let(:response) { double('response', :done? => true) } | ||
it do | ||
expect(request).to receive(:affected_ems).and_return(ems) | ||
expect(ems).to receive(:update_firmware_async).with(firmware_binary, [server]).and_return(response) | ||
expect(subject).to receive(:done_firmware_update) | ||
subject.start_firmware_update | ||
end | ||
end | ||
|
||
context 'when all steps succeed after polling' do | ||
before { allow(ems).to receive(:with_provider_connection).and_yield(client) } | ||
before { allow(client).to receive(:get).and_return(response, response, response_ok) } | ||
before { allow(request).to receive(:affected_ems).and_return(ems) } | ||
let(:options) { { :src_ids => [server.id], :firmware_binary_id => firmware_binary.id } } | ||
let(:client) { double('client') } | ||
let(:response) { double('response', :done? => false, :monitor => '/monitor', :to_h => {}) } | ||
let(:response_ok) { double('response-ok', :done? => true) } | ||
it do | ||
expect(ems).to receive(:update_firmware_async).with(firmware_binary, [server]).and_return(response) | ||
expect(subject).to receive(:done_firmware_update) | ||
subject.start_firmware_update | ||
end | ||
end | ||
end | ||
end |
102 changes: 102 additions & 0 deletions
102
spec/models/manageiq/providers/redfish/physical_infra_manager/operations/firmware_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
describe ManageIQ::Providers::Redfish::PhysicalInfraManager do | ||
before { EvmSpecHelper.create_guid_miq_server_zone } | ||
before { allow(server1).to receive(:firmware_compatible?).with(firmware_binary).and_return(true) } | ||
before { allow(server2).to receive(:firmware_compatible?).with(firmware_binary).and_return(true) } | ||
before { allow(subject).to receive(:with_provider_connection).and_yield(client) } | ||
|
||
let(:ems) { FactoryBot.create(:ems_physical_infra) } | ||
let(:server1) { FactoryBot.create(:physical_server, :ext_management_system => ems) } | ||
let(:server2) { FactoryBot.create(:physical_server, :ext_management_system => ems) } | ||
let(:servers) { [server1, server2] } | ||
let(:request) { FactoryBot.create(:physical_server_firmware_update_request) } | ||
let(:firmware_binary) { FactoryBot.create(:firmware_binary) } | ||
let(:client) { double('client', :UpdateService => update_service) } | ||
let(:response) { double('response', :status => 200) } | ||
let(:actions) { { '#UpdateService.SimpleUpdate' => update_action } } | ||
let(:update_service) { double('update_service', :Actions => actions) } | ||
let(:update_action) do | ||
double('update action', :target => 'update-service-url').tap do |d| | ||
allow(d).to receive(:post).and_return(response) | ||
allow(d).to receive(:[]).with('[email protected]').and_return(['HTTP']) | ||
end | ||
end | ||
|
||
describe '#update_firmware_async' do | ||
before { allow(subject).to receive(:compatible_firmware_url).and_return(%w[HTTP http://url]) } | ||
|
||
context 'when firmware update succeeds' do | ||
it 'no error is raised' do | ||
expect { subject.update_firmware_async(firmware_binary, servers) }.not_to raise_error | ||
end | ||
end | ||
|
||
context 'when missing server' do | ||
it 'handled error is raised' do | ||
expect { subject.update_firmware_async(firmware_binary, []) }.to raise_error(MiqException::MiqFirmwareUpdateError) | ||
end | ||
end | ||
|
||
context 'when firmware and server are incompatible' do | ||
before { allow(server1).to receive(:firmware_compatible?).with(firmware_binary).and_return(false) } | ||
it 'handled error is raised' do | ||
expect { subject.update_firmware_async(firmware_binary, servers) }.to raise_error(MiqException::MiqFirmwareUpdateError) | ||
end | ||
end | ||
|
||
context 'when update service not available' do | ||
let(:update_service) { nil } | ||
it 'handled error is raised' do | ||
expect { subject.update_firmware_async(firmware_binary, servers) }.to raise_error(MiqException::MiqFirmwareUpdateError) | ||
end | ||
end | ||
|
||
context 'when update service returns bad response' do | ||
let(:response) { double('response', :status => 500, :data => { :body => 'MSG' }) } | ||
it 'handled error is raised' do | ||
expect { subject.update_firmware_async(firmware_binary, servers) }.to raise_error(MiqException::MiqFirmwareUpdateError) | ||
end | ||
end | ||
end | ||
|
||
describe '#compatible_firmware_url' do | ||
before { firmware_binary.endpoints = [binary_http, binary_https] } | ||
|
||
let(:binary_http) { FactoryBot.create(:endpoint, :url => 'http://test') } | ||
let(:binary_https) { FactoryBot.create(:endpoint, :url => 'https://test') } | ||
|
||
context 'when protocols are listed in @Redfish.AllowableValues' do | ||
it 'HTTP endpoint is returned' do | ||
expect(update_action).to receive(:[]).with('[email protected]').and_return(['HTTP']) | ||
protocol, url = subject.compatible_firmware_url(update_service, firmware_binary) | ||
expect(protocol).to eq('HTTP') | ||
expect(url).to eq(binary_http.url) | ||
end | ||
end | ||
|
||
context 'when protocols are listed in @Redfish.ActionInfo' do | ||
let(:params) { [double('param1', :Name => 'TransferProtocol', :AllowableValues => ['HTTP'])] } | ||
|
||
it 'HTTP endpoint is returned' do | ||
expect(update_action).to receive(:[]).with('[email protected]').and_return(nil) | ||
expect(update_action).to receive(:[]).with('[email protected]').and_return(double(:Parameters => params)) | ||
protocol, url = subject.compatible_firmware_url(update_service, firmware_binary) | ||
expect(protocol).to eq('HTTP') | ||
expect(url).to eq(binary_http.url) | ||
end | ||
end | ||
|
||
context 'when no protocol is listed as supported' do | ||
it 'managed error is raised' do | ||
expect(update_action).to receive(:[]).with('[email protected]').and_return([]) | ||
expect { subject.compatible_firmware_url(update_service, firmware_binary) }.to raise_error(MiqException::MiqFirmwareUpdateError) | ||
end | ||
end | ||
|
||
context 'when no supported protocol has corresponding endpoint' do | ||
it 'managed error is raised' do | ||
expect(update_action).to receive(:[]).with('[email protected]').and_return(['MISSING']) | ||
expect { subject.compatible_firmware_url(update_service, firmware_binary) }.to raise_error(MiqException::MiqFirmwareUpdateError) | ||
end | ||
end | ||
end | ||
end |