From 481b3dc3f15eebaf690c4c935642c9b37b67c7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miha=20Ple=C5=A1ko?= Date: Thu, 16 May 2019 15:21:38 +0200 Subject: [PATCH] Introduce FirmwareRegistry and FirmwareBinary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One can update physical server's firmware, but the firmware has to be accessible somewhere, usually on some HTTP/FTP/SAMBA link. Furthermore, there is usually some metadata associated with the firmware to tell what kind of hardware (manufacturer, model type) is it meant for. With this commit we introduce a new model, FirmwareRegistry, that supports various kinds of metadata parsing by leveraging STI. User is supposed to create FirmwareRegistry instance via UI feeding it url+authentication and then click "Refresh Relationships" on it. ManageIQ will then invoke ``` registry.sync_fw_binaries_queue ``` which in turn will connect to the metadata repository to list available firmwares and store them as FirmwareBinary (no binary data is really stored, only HTTP/FTP/SAMBA link and some metadata). In followup PRs we will then be able to list available firmware binaries to update physical server's firmware. Signed-off-by: Miha Pleško --- app/models/firmware_binary.rb | 15 ++ app/models/firmware_binary_firmware_target.rb | 6 + app/models/firmware_registry.rb | 37 ++++ .../firmware_registry/rest_api_depot.rb | 62 +++++++ app/models/firmware_target.rb | 33 ++++ app/models/physical_server.rb | 9 + spec/factories/asset_detail.rb | 6 + spec/factories/firmware_binary.rb | 14 ++ spec/factories/firmware_registry.rb | 11 ++ spec/factories/firmware_target.rb | 6 + spec/factories/physical_server.rb | 6 + spec/models/firmware_binary_spec.rb | 49 +++++ .../data/sample_firmware_binaries.json | 54 ++++++ .../firmware_registry/rest_api_depot_spec.rb | 175 ++++++++++++++++++ spec/models/firmware_registry_spec.rb | 64 +++++++ spec/models/firmware_target_spec.rb | 35 ++++ spec/models/physical_server_spec.rb | 31 ++++ .../rest_api_depot_when-200.yml | 92 +++++++++ .../rest_api_depot_when-bad-credentials.yml | 41 ++++ .../rest_api_depot_when-bad-json.yml | 39 ++++ 20 files changed, 785 insertions(+) create mode 100644 app/models/firmware_binary.rb create mode 100644 app/models/firmware_binary_firmware_target.rb create mode 100644 app/models/firmware_registry.rb create mode 100644 app/models/firmware_registry/rest_api_depot.rb create mode 100644 app/models/firmware_target.rb create mode 100644 spec/factories/asset_detail.rb create mode 100644 spec/factories/firmware_binary.rb create mode 100644 spec/factories/firmware_registry.rb create mode 100644 spec/factories/firmware_target.rb create mode 100644 spec/models/firmware_binary_spec.rb create mode 100644 spec/models/firmware_registry/data/sample_firmware_binaries.json create mode 100644 spec/models/firmware_registry/rest_api_depot_spec.rb create mode 100644 spec/models/firmware_registry_spec.rb create mode 100644 spec/models/firmware_target_spec.rb create mode 100644 spec/models/physical_server_spec.rb create mode 100644 spec/vcr_cassettes/firmware_registry/rest_api_depot_when-200.yml create mode 100644 spec/vcr_cassettes/firmware_registry/rest_api_depot_when-bad-credentials.yml create mode 100644 spec/vcr_cassettes/firmware_registry/rest_api_depot_when-bad-json.yml diff --git a/app/models/firmware_binary.rb b/app/models/firmware_binary.rb new file mode 100644 index 00000000000..790cbe0ccea --- /dev/null +++ b/app/models/firmware_binary.rb @@ -0,0 +1,15 @@ +class FirmwareBinary < ApplicationRecord + validates :name, :presence => true + has_many :endpoints, :dependent => :destroy, :as => :resource, :inverse_of => :resource + has_many :firmware_binary_firmware_targets, :dependent => :destroy + has_many :firmware_targets, :through => :firmware_binary_firmware_targets + belongs_to :firmware_registry + + def allow_duplicate_endpoint_url? + true + end + + def urls + endpoints.pluck(:url) + end +end diff --git a/app/models/firmware_binary_firmware_target.rb b/app/models/firmware_binary_firmware_target.rb new file mode 100644 index 00000000000..1459f1f4144 --- /dev/null +++ b/app/models/firmware_binary_firmware_target.rb @@ -0,0 +1,6 @@ +class FirmwareBinaryFirmwareTarget < ApplicationRecord + self.table_name = 'firmware_binaries_firmware_targets' + + belongs_to :firmware_binary + belongs_to :firmware_target +end diff --git a/app/models/firmware_registry.rb b/app/models/firmware_registry.rb new file mode 100644 index 00000000000..87ebc7529dc --- /dev/null +++ b/app/models/firmware_registry.rb @@ -0,0 +1,37 @@ +class FirmwareRegistry < ApplicationRecord + include NewWithTypeStiMixin + + has_many :firmware_binaries, :dependent => :destroy + has_one :endpoint, :as => :resource, :dependent => :destroy, :inverse_of => :resource + has_one :authentication, :as => :resource, :dependent => :destroy, :inverse_of => :resource + + validates :name, :presence => true, :uniqueness => true + + def sync_fw_binaries_queue + MiqQueue.put_unless_exists( + :class_name => self.class.name, + :instance_id => id, + :method_name => 'sync_fw_binaries' + ) + end + + def sync_fw_binaries + _log.info("Synchronizing FirmwareBinaries from #{self.class.name} [#{id}|#{name}]...") + sync_fw_binaries_raw + self.last_refresh_error = nil + _log.info("Synchronizing FirmwareBinaries from #{self.class.name} [#{id}|#{name}]... Complete") + rescue MiqException::Error => e + self.last_refresh_error = e + ensure + self.last_refresh_on = Time.now.utc + save! + end + + def sync_fw_binaries_raw + raise NotImplementedError, 'Must be implemented in subclass' + end + + def self.display_name(number = 1) + n_('Firmware Registry', 'Firmware Registries', number) + end +end diff --git a/app/models/firmware_registry/rest_api_depot.rb b/app/models/firmware_registry/rest_api_depot.rb new file mode 100644 index 00000000000..c216dc1c4c7 --- /dev/null +++ b/app/models/firmware_registry/rest_api_depot.rb @@ -0,0 +1,62 @@ +class FirmwareRegistry::RestApiDepot < FirmwareRegistry + def sync_fw_binaries_raw + remote_binaries.each do |binary_hash| + binary = FirmwareBinary.find_or_create_by(:firmware_registry => self, :external_ref => binary_hash['id']) + _log.info("Updating FirmwareBinary [#{binary.id} | #{binary.name}]...") + + binary.name = binary_hash['filename'] || binary_hash['id'] + binary.description = binary_hash['description'] + binary.version = binary_hash['version'] + binary.save! + + unless binary.urls.sort == binary_hash['urls'].sort + _log.info("Updating FirmwareBinary [#{binary.id} | #{binary.name}] endpoints...") + endpoints_by_url = binary.endpoints.index_by(&:url) + urls_to_delete = binary.urls - binary_hash['urls'] + urls_to_delete.each { |url| endpoints_by_url[url].destroy } + urls_to_create = binary_hash['urls'] - binary.urls + urls_to_create.each { |url| binary.endpoints.create!(:url => url) } + end + + currents = binary.firmware_targets.map(&:to_hash) + remotes = binary_hash['compatible_server_models'].map { |r| r.symbolize_keys.slice(*FirmwareTarget::MATCH_ATTRIBUTES) } + unless currents.map(&:to_a).sort == remotes.map(&:to_a).sort + _log.info("Updating FirmwareBinary [#{binary.id} | #{binary.name}] targets...") + binary.firmware_targets = remotes.map do |remote| + FirmwareTarget.find_compatible_with(remote, :create => true) + end + end + + _log.info("Updating FirmwareBinary [#{binary.id} | #{binary.name}]... completed.") + end + end + + def remote_binaries + self.class.fetch_from_remote( + endpoint.url, + authentication.userid, + authentication.password, + :verify_ssl => endpoint.security_protocol + ) + end + + def self.fetch_from_remote(url, username, password, verify_ssl: OpenSSL::SSL::VERIFY_PEER) + uri = URI.parse(url) + request = Net::HTTP::Get.new(uri.request_uri) + request['Content-Type'] = 'application/json' + request.basic_auth(username, password) + response = Net::HTTP.new(uri.host, uri.port).tap do |http| + http.open_timeout = 5.seconds + http.use_ssl = url.start_with?('https') + http.verify_mode = verify_ssl + end.request(request) + + raise MiqException::Error, "Bad status returned: #{response.code}" if response.code.to_i != 200 + + JSON.parse(response.body) + rescue SocketError => e + raise MiqException::Error, e + rescue JSON::ParserError => e + raise MiqException::Error, e + end +end diff --git a/app/models/firmware_target.rb b/app/models/firmware_target.rb new file mode 100644 index 00000000000..2a850d22fad --- /dev/null +++ b/app/models/firmware_target.rb @@ -0,0 +1,33 @@ +class FirmwareTarget < ApplicationRecord + has_many :firmware_binary_firmware_targets, :dependent => :destroy + has_many :firmware_binaries, :through => :firmware_binary_firmware_targets + + before_create :normalize + before_save :normalize + + # Attributes that need to match for target physical server to be assumed compatible. + MATCH_ATTRIBUTES = %i[manufacturer model].freeze + + def normalize + manufacturer.downcase! + model.downcase! + end + + # Firmware targets are nameless, but we need to show something on the UI. + def name + _('Firmware Target') + end + + def to_hash + attributes.symbolize_keys.slice(*MATCH_ATTRIBUTES) + end + + def self.find_compatible_with(attributes, create: false) + relevant_attrs = attributes.tap do |attrs| + attrs.symbolize_keys! + attrs.slice!(*MATCH_ATTRIBUTES) + attrs.transform_values!(&:downcase) + end + create ? find_or_create_by(relevant_attrs) : find_by(relevant_attrs) + end +end diff --git a/app/models/physical_server.rb b/app/models/physical_server.rb index 957461009d0..aebd511bcb3 100644 --- a/app/models/physical_server.rb +++ b/app/models/physical_server.rb @@ -112,4 +112,13 @@ def v_availability def v_host_os host.try(:vmm_product).nil? ? N_("") : host.vmm_product end + + def compatible_firmware_binaries + FirmwareTarget.find_compatible_with(asset_detail.attributes)&.firmware_binaries || [] + end + + def firmware_compatible?(firmware_binary) + filter = asset_detail.attributes.slice(*FirmwareTarget::MATCH_ATTRIBUTES).transform_values(&:downcase) + firmware_binary.firmware_targets.find_by(filter).present? + end end diff --git a/spec/factories/asset_detail.rb b/spec/factories/asset_detail.rb new file mode 100644 index 00000000000..36272fa444e --- /dev/null +++ b/spec/factories/asset_detail.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :asset_detail do + sequence(:manufacturer) { |n| "manufacturer_#{seq_padded_for_sorting(n)}" } + sequence(:model) { |n| "model_#{seq_padded_for_sorting(n)}" } + end +end diff --git a/spec/factories/firmware_binary.rb b/spec/factories/firmware_binary.rb new file mode 100644 index 00000000000..8ff728787f9 --- /dev/null +++ b/spec/factories/firmware_binary.rb @@ -0,0 +1,14 @@ +FactoryBot.define do + factory :firmware_binary do + sequence(:name) { |n| "firmware_binary_#{seq_padded_for_sorting(n)}" } + sequence(:description) { 'Firmware Binary Description' } + sequence(:version) { 'v1.2.3' } + + trait :with_endpoints do + after(:create) do |binary| + binary.endpoints << FactoryBot.create(:endpoint, :url => 'http://test.binary.1', :resource => binary) + binary.endpoints << FactoryBot.create(:endpoint, :url => 'http://test.binary.2', :resource => binary) + end + end + end +end diff --git a/spec/factories/firmware_registry.rb b/spec/factories/firmware_registry.rb new file mode 100644 index 00000000000..9335dd0347a --- /dev/null +++ b/spec/factories/firmware_registry.rb @@ -0,0 +1,11 @@ +FactoryBot.define do + factory :firmware_registry do + sequence(:name) { |n| "firmware_registry_#{seq_padded_for_sorting(n)}" } + endpoint { |n| FactoryBot.create(:endpoint, :url => "http://test.registry.#{n}") } + authentication { FactoryBot.create(:authentication) } + end + + factory :firmware_registry_rest_api_depot, + :parent => :firmware_registry, + :class => 'FirmwareRegistry::RestApiDepot' +end diff --git a/spec/factories/firmware_target.rb b/spec/factories/firmware_target.rb new file mode 100644 index 00000000000..b93ff3ab08a --- /dev/null +++ b/spec/factories/firmware_target.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :firmware_target do + sequence(:manufacturer) { |n| "manufacturer_#{seq_padded_for_sorting(n)}" } + sequence(:model) { |n| "model_#{seq_padded_for_sorting(n)}" } + end +end diff --git a/spec/factories/physical_server.rb b/spec/factories/physical_server.rb index b221e54ef11..61a7355cdf8 100644 --- a/spec/factories/physical_server.rb +++ b/spec/factories/physical_server.rb @@ -1,5 +1,11 @@ FactoryBot.define do factory :physical_server do vendor { "lenovo" } + + trait :with_asset_detail do + after :create do |server| + server.asset_detail = FactoryBot.create(:asset_detail) + end + end end end diff --git a/spec/models/firmware_binary_spec.rb b/spec/models/firmware_binary_spec.rb new file mode 100644 index 00000000000..c8999aeafa0 --- /dev/null +++ b/spec/models/firmware_binary_spec.rb @@ -0,0 +1,49 @@ +describe FirmwareBinary do + subject { FactoryBot.create(:firmware_binary) } + + describe '#allow_duplicate_endpoint_url?' do + let(:binary1) { FactoryBot.create(:firmware_binary) } + let(:binary2) { FactoryBot.create(:firmware_binary) } + + it 'has the flag set' do + expect(subject.allow_duplicate_endpoint_url?).to eq(true) + end + + it 'two different binaries are allowed to have same url' do + expect do + binary1.endpoints << FactoryBot.create(:endpoint, :url => 'same-url', :resource => binary1) + binary2.endpoints << FactoryBot.create(:endpoint, :url => 'same-url', :resource => binary2) + end.not_to raise_error + end + end + + describe '#urls' do + it 'lists from all endpoints' do + subject.endpoints << FactoryBot.create(:endpoint, :url => 'url1') + subject.endpoints << FactoryBot.create(:endpoint, :url => 'url2') + expect(subject.urls).to eq(%w[url1 url2]) + end + end + + describe '#has_many endpoints' do + before { subject.endpoints = [endpoint1, endpoint2] } + let(:endpoint1) { FactoryBot.create(:endpoint) } + let(:endpoint2) { FactoryBot.create(:endpoint) } + + it 'one to many connection exists' do + expect(subject.endpoints).to match_array([endpoint1, endpoint2]) + end + end + + describe '#has_many firmware_targets' do + before { subject.firmware_targets = [target1, target2] } + let(:target1) { FactoryBot.create(:firmware_target) } + let(:target2) { FactoryBot.create(:firmware_target) } + + it 'many to many connection exists' do + expect(subject.firmware_targets).to match_array([target1, target2]) + expect(target1.firmware_binaries).to match_array([subject]) + expect(target2.firmware_binaries).to match_array([subject]) + end + end +end diff --git a/spec/models/firmware_registry/data/sample_firmware_binaries.json b/spec/models/firmware_registry/data/sample_firmware_binaries.json new file mode 100644 index 00000000000..3fe585050ab --- /dev/null +++ b/spec/models/firmware_registry/data/sample_firmware_binaries.json @@ -0,0 +1,54 @@ +[ + { + "description": "DELL BIOS update", + "urls": [ + "http://172.16.93.126:8080/files/dell-bios-2019-03-23.bin", + "https://172.16.93.126:8443/files/dell-bios-2019-03-23.bin" + ], + "compatible_server_models": [ + { + "model": "Common Model", + "manufacturer": "Common Manufacturer" + } + ], + "id": 1, + "version": "10.4.3", + "filename": "dell-bios-2019-03-23.bin" + }, + { + "description": "DELL BIOS update", + "urls": [ + "http://172.16.93.126:8080/files/dell-bios-2019-04-13.bin", + "https://172.16.93.126:8443/files/dell-bios-2019-04-13.bin" + ], + "compatible_server_models": [ + { + "model": "Common Model", + "manufacturer": "Common Manufacturer" + } + ], + "id": 2, + "version": "10.4.4", + "filename": "dell-bios-2019-04-13.bin" + }, + { + "description": "SuperMicro NIC firmware", + "urls": [ + "http://172.16.93.126:8080/files/nic-update-3.exe", + "https://172.16.93.126:8443/files/nic-update-3.exe" + ], + "compatible_server_models": [ + { + "model": "SYS-6033A-GHJ7Z", + "manufacturer": "SuperMicro" + }, + { + "model": "SYS-5019D-FN8TP", + "manufacturer": "SuperMicro" + } + ], + "id": 3, + "version": "4.3.2-101-z", + "filename": "nic-update-3.exe" + } +] diff --git a/spec/models/firmware_registry/rest_api_depot_spec.rb b/spec/models/firmware_registry/rest_api_depot_spec.rb new file mode 100644 index 00000000000..8a2cfcba559 --- /dev/null +++ b/spec/models/firmware_registry/rest_api_depot_spec.rb @@ -0,0 +1,175 @@ +describe FirmwareRegistry::RestApiDepot do + before do + VCR.configure do |config| + config.filter_sensitive_data('AUTHORIZATION') { Base64.encode64("#{user}:#{pass}").chomp } + config.before_record { |interaction| interaction.request.uri.downcase! } + end + # Is seems some other test is turning VCR off in some random cases. + # We're best off manually turning it on. + VCR.turn_on! + end + + let(:host) { Rails.application.secrets.fwreg_rest_api_depot.try(:[], 'host') || 'host' } + let(:user) { Rails.application.secrets.fwreg_rest_api_depot.try(:[], 'userid') || 'username' } + let(:pass) { Rails.application.secrets.fwreg_rest_api_depot.try(:[], 'password') || 'password' } + let(:url) { "http://#{host}/images/" } + + describe '.fetch_from_remote' do + context 'when 200' do + it 'list of firmware binaries is returned' do + with_vcr('when-200') do + expect(described_class.fetch_from_remote(url, user, pass)).to be_an Array + end + end + end + + context 'when bad credentials' do + it 'managed error is raised' do + with_vcr('when-bad-credentials') do + expect { described_class.fetch_from_remote(url, 'bad-username', 'bad-password') }.to raise_error(MiqException::Error) + end + end + end + + context 'when bad host' do + it 'managed error is raised' do + with_vcr('when-bad-host') do + expect { described_class.fetch_from_remote('http://bad.host', 'user', 'pass') }.to raise_error(MiqException::Error) + end + end + end + + context 'when bad json' do + it 'managed error is raised' do + with_vcr('when-bad-json') do + expect { described_class.fetch_from_remote(url, user, pass) }.to raise_error(MiqException::Error) + end + end + end + end + + describe '#sync_fw_binaries_raw' do + before { allow(described_class).to receive(:fetch_from_remote).and_return(json) } + let(:json) { [] } + subject { FactoryBot.create(:firmware_registry_rest_api_depot) } + + context 'when on empty database' do + before { assert_counts(:firmware_registry => 0, :firmware_binary => 0, :firmware_target => 0) } + + context 'using simple inline fixture' do + let(:json) do + [ + { + 'description' => 'Some Binary Description', + 'urls' => [ + 'http://url.net =>1000', + 'https://url.net =>1443', + ], + 'compatible_server_models' => [ + { + 'model' => 'Common Model', + 'manufacturer' => 'Common Manufacturer' + } + ], + 'version' => 'v1.2.3', + 'filename' => 'some.binary.name' + } + ] + end + + it 'binary is inventoried' do + subject.sync_fw_binaries_raw + subject.reload + assert_counts(:firmware_binary => 1) + expect(FirmwareBinary.first).to have_attributes( + :name => 'some.binary.name', + :description => 'Some Binary Description', + :version => 'v1.2.3', + :firmware_registry => subject, + :firmware_targets => [FirmwareTarget.first] + ) + end + + it 'target is inventoried' do + subject.sync_fw_binaries_raw + subject.reload + assert_counts(:firmware_target => 1) + expect(FirmwareTarget.first).to have_attributes( + :manufacturer => 'common manufacturer', + :model => 'common model', + :firmware_binaries => [FirmwareBinary.first] + ) + end + + it 'refresh twice' do + 2.times { subject.sync_fw_binaries_raw } + assert_counts(:firmware_registry => 1, :firmware_binary => 1, :firmware_target => 1) + end + end + + context 'using json fixture' do + let(:json) { JSON.parse(File.read(File.join(File.dirname(__FILE__), 'data', 'sample_firmware_binaries.json'))) } + + it 'inventories specific entities' do + subject.sync_fw_binaries_raw + subject.reload + assert_counts(:firmware_registry => 1, :firmware_binary => json.size, :firmware_target => 3) + expect(FirmwareBinary.find_by(:name => 'dell-bios-2019-03-23.bin')).to have_attributes( + :description => 'DELL BIOS update', + :version => '10.4.3', + :firmware_registry => subject, + :firmware_targets => [FirmwareTarget.find_by(:model => 'common model')] + ) + end + end + end + + context 'when on existing database' do + let!(:binary) { FactoryBot.create(:firmware_binary, :firmware_registry => subject) } + let!(:target) { FactoryBot.create(:firmware_target, :firmware_binaries => [binary]) } + let(:json) do + [ + { + 'description' => 'Updated Description', + 'urls' => [ + 'http://url.net =>1000', + 'https://url.net =>1443', + ], + 'compatible_server_models' => [ + { + 'model' => 'Updated Model', + 'manufacturer' => 'Updated Manufacturer' + } + ], + 'version' => 'v1.2.3', + 'filename' => binary.name + } + ] + end + + it 'binary is updated' do + subject.sync_fw_binaries_raw + subject.reload + assert_counts(:firmware_binary => 1) + expect(FirmwareBinary.first).to have_attributes( + :name => binary.name, + :description => 'Updated Description', + :version => 'v1.2.3', + :firmware_registry => subject, + :firmware_targets => [FirmwareTarget.find_by(:model => 'updated model')] + ) + end + end + + def assert_counts(counts) + expect(FirmwareRegistry.count).to eq(counts[:firmware_registry]) if counts.include?(:firmware_registry) + expect(FirmwareBinary.count).to eq(counts[:firmware_binary]) if counts.include?(:firmware_binary) + expect(FirmwareTarget.count).to eq(counts[:firmware_target]) if counts.include?(:firmware_target) + end + end + + def with_vcr(suffix) + path = "#{described_class.name.underscore}_#{suffix}" + VCR.use_cassette(path, :match_requests_on => [:method, :path]) { yield } + end +end diff --git a/spec/models/firmware_registry_spec.rb b/spec/models/firmware_registry_spec.rb new file mode 100644 index 00000000000..02249a46c27 --- /dev/null +++ b/spec/models/firmware_registry_spec.rb @@ -0,0 +1,64 @@ +describe FirmwareRegistry do + before { EvmSpecHelper.create_guid_miq_server_zone } + subject { FactoryBot.create(:firmware_registry) } + + describe '#destroy' do + let!(:binary) { FactoryBot.create(:firmware_binary, :with_endpoints, :firmware_registry => subject) } + let!(:target) { FactoryBot.create(:firmware_target, :firmware_binaries => [binary]) } + + it 'deletes repository in cascade' do + expect(FirmwareRegistry.count).to eq(1) + expect(FirmwareBinary.count).to eq(1) + expect(Authentication.count).to eq(1) + expect(Endpoint.count).to eq(1 + 2) # registry endpoint + 2*binary endpoint + expect(FirmwareBinaryFirmwareTarget.count).to eq(1) + expect(FirmwareTarget.count).to eq(1) + + subject.destroy + + expect(FirmwareRegistry.count).to eq(0) + expect(FirmwareBinary.count).to eq(0) + expect(Endpoint.count).to eq(0) + expect(Authentication.count).to eq(0) + expect(FirmwareBinaryFirmwareTarget.count).to eq(0) + expect(FirmwareTarget.count).to eq(1) + end + end + + it '#sync_fw_binaries_raw' do + expect { subject.sync_fw_binaries_raw }.to raise_error(NotImplementedError) + end + + it '#sync_fw_binaries_queue' do + subject.sync_fw_binaries_queue + expect(MiqQueue.count).to eq(1) + expect(MiqQueue.find_by(:method_name => 'sync_fw_binaries')).to have_attributes( + :class_name => described_class.name, + :instance_id => subject.id + ) + end + + describe '#sync_fw_binaries' do + context 'when sync succeeds' do + before { allow(subject).to receive(:sync_fw_binaries_raw).and_return(nil) } + + it 'last_refresh_[on|error] is updated' do + subject.sync_fw_binaries + subject.reload + expect(subject.last_refresh_error).to be_nil + expect(subject.last_refresh_on).to be_within(5.seconds).of(Time.now.utc) + end + end + + context 'when sync errors' do + before { allow(subject).to receive(:sync_fw_binaries_raw).and_raise(MiqException::Error.new('MESSAGE')) } + + it 'last_refresh_[on|error] is updated' do + subject.sync_fw_binaries + subject.reload + expect(subject.last_refresh_error).to eq('MESSAGE') + expect(subject.last_refresh_on).to be_within(5.seconds).of(Time.now.utc) + end + end + end +end diff --git a/spec/models/firmware_target_spec.rb b/spec/models/firmware_target_spec.rb new file mode 100644 index 00000000000..7f36d728e26 --- /dev/null +++ b/spec/models/firmware_target_spec.rb @@ -0,0 +1,35 @@ +describe FirmwareTarget do + let(:attrs) { { :manufacturer => 'manu', :model => 'model' } } + let(:other_attrs) { { :manufacturer => 'other-manu', :model => 'other-model' } } + + subject! { FactoryBot.create(:firmware_target, **attrs) } + + describe '.find_compatible_with' do + it 'finds existing target' do + target = described_class.find_compatible_with(attrs) + expect(target).to eq(subject) + end + + it 'returns nil for unexisiting target' do + target = described_class.find_compatible_with(other_attrs) + expect(target).to eq(nil) + end + + describe 'when :create => true' do + it 'finds exisiting target' do + target = described_class.find_compatible_with(attrs, :create => true) + expect(target).to eq(subject) + end + + it 'creates unexisiting target' do + target = described_class.find_compatible_with(other_attrs, :create => true) + expect(target).not_to eq(subject) + expect(target).to be_a(described_class) + end + end + end + + it '#to_hash' do + expect(subject.to_hash).to eq(attrs) + end +end diff --git a/spec/models/physical_server_spec.rb b/spec/models/physical_server_spec.rb new file mode 100644 index 00000000000..1be6f7db535 --- /dev/null +++ b/spec/models/physical_server_spec.rb @@ -0,0 +1,31 @@ +describe PhysicalServer do + let(:attrs) { { :manufacturer => 'manu', :model => 'model' } } + let!(:binary1) { FactoryBot.create(:firmware_binary) } + let!(:binary2) { FactoryBot.create(:firmware_binary) } + let!(:target) { FactoryBot.create(:firmware_target, **attrs, :firmware_binaries => [binary1]) } + + subject { FactoryBot.create(:physical_server, :with_asset_detail) } + + describe '#compatible_firmware_binaries' do + before { subject.asset_detail.update(**attrs) } + + it 'when compatible are found' do + expect(subject.compatible_firmware_binaries).to eq([binary1]) + end + + it 'when no compatible are found' do + subject.asset_detail.update(:model => 'other-model') + expect(subject.compatible_firmware_binaries).to eq([]) + end + end + + describe '#firmware_compatible?' do + it 'when yes' do + expect(subject.firmware_compatible?(binary1)).to eq(true) + end + + it 'when no' do + expect(subject.firmware_compatible?(binary2)).to eq(false) + end + end +end diff --git a/spec/vcr_cassettes/firmware_registry/rest_api_depot_when-200.yml b/spec/vcr_cassettes/firmware_registry/rest_api_depot_when-200.yml new file mode 100644 index 00000000000..019ebbb2324 --- /dev/null +++ b/spec/vcr_cassettes/firmware_registry/rest_api_depot_when-200.yml @@ -0,0 +1,92 @@ +--- +http_interactions: +- request: + method: get + uri: http://fwreg_rest_api_depot_host/images/ + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/json + Authorization: + - Basic AUTHORIZATION + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Content-Length: + - '1559' + Server: + - Werkzeug/0.15.4 Python/3.5.2 + Date: + - Mon, 10 Jun 2019 13:17:30 GMT + body: + encoding: UTF-8 + string: | + [ + { + "filename": "dell-bios-2019-03-23.bin", + "compatible_server_models": [ + { + "manufacturer": "Dell", + "model": "PowerEdge R740" + } + ], + "version": "10.4.3", + "description": "DELL BIOS update", + "id": 1, + "urls": [ + "http://172.16.93.126:8080/files/dell-bios-2019-03-23.bin", + "https://172.16.93.126:8443/files/dell-bios-2019-03-23.bin" + ] + }, + { + "filename": "dell-bios-2019-04-13.bin", + "compatible_server_models": [ + { + "manufacturer": "Dell", + "model": "PowerEdge R740" + } + ], + "version": "10.4.4", + "description": "DELL BIOS update", + "id": 2, + "urls": [ + "http://172.16.93.126:8080/files/dell-bios-2019-04-13.bin", + "https://172.16.93.126:8443/files/dell-bios-2019-04-13.bin" + ] + }, + { + "filename": "nic-update-3.exe", + "compatible_server_models": [ + { + "manufacturer": "SuperMicro", + "model": "SYS-5019D-FN8TP" + }, + { + "manufacturer": "SuperMicro", + "model": "SYS-6033A-GHJ7Z" + } + ], + "version": "4.3.2-101-z", + "description": "SuperMicro NIC firmware", + "id": 3, + "urls": [ + "http://172.16.93.126:8080/files/nic-update-3.exe", + "https://172.16.93.126:8443/files/nic-update-3.exe" + ] + } + ] + http_version: + recorded_at: Mon, 10 Jun 2019 13:17:30 GMT +recorded_with: VCR 3.0.3 diff --git a/spec/vcr_cassettes/firmware_registry/rest_api_depot_when-bad-credentials.yml b/spec/vcr_cassettes/firmware_registry/rest_api_depot_when-bad-credentials.yml new file mode 100644 index 00000000000..bd8a7383431 --- /dev/null +++ b/spec/vcr_cassettes/firmware_registry/rest_api_depot_when-bad-credentials.yml @@ -0,0 +1,41 @@ +--- +http_interactions: +- request: + method: get + uri: http://fwreg_rest_api_depot_host/images/ + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/json + Authorization: + - Basic YmFkLXVzZXJuYW1lOmJhZC1wYXNzd29yZA== + response: + status: + code: 401 + message: UNAUTHORIZED + headers: + Content-Type: + - application/json + Content-Length: + - '242' + Server: + - Werkzeug/0.15.4 Python/3.5.2 + Date: + - Mon, 10 Jun 2019 13:17:30 GMT + body: + encoding: UTF-8 + string: | + { + "message": "The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad FWREG_REST_API_DEPOT_PASSWORDword), or your browser doesn't understand how to supply the credentials required." + } + http_version: + recorded_at: Mon, 10 Jun 2019 13:17:30 GMT +recorded_with: VCR 3.0.3 diff --git a/spec/vcr_cassettes/firmware_registry/rest_api_depot_when-bad-json.yml b/spec/vcr_cassettes/firmware_registry/rest_api_depot_when-bad-json.yml new file mode 100644 index 00000000000..f8490ac4998 --- /dev/null +++ b/spec/vcr_cassettes/firmware_registry/rest_api_depot_when-bad-json.yml @@ -0,0 +1,39 @@ +--- +http_interactions: +- request: + method: get + uri: http://fwreg_rest_api_depot_host/images/ + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Content-Type: + - application/json + Authorization: + - Basic AUTHORIZATION + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Content-Length: + - '1559' + Server: + - Werkzeug/0.15.4 Python/3.5.2 + Date: + - Mon, 10 Jun 2019 13:17:31 GMT + body: + encoding: UTF-8 + string: | + VERY VERY BAD JSON + http_version: + recorded_at: Mon, 10 Jun 2019 13:17:31 GMT +recorded_with: VCR 3.0.3