Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internal state machine implementation for firmware update #69

Merged
merged 1 commit into from
Jul 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ module PhysicalInfraManager::Operations

include_concern "Power"
include_concern "Led"
include_concern "Firmware"
end
end
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
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
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