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

Introduce FirmwareRegistry and FirmwareBinary (with FirmwareTargets) #18774

Merged
merged 1 commit into from
Jun 13, 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
15 changes: 15 additions & 0 deletions app/models/firmware_binary.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions app/models/firmware_binary_firmware_target.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class FirmwareBinaryFirmwareTarget < ApplicationRecord
self.table_name = 'firmware_binaries_firmware_targets'

belongs_to :firmware_binary
belongs_to :firmware_target
end
37 changes: 37 additions & 0 deletions app/models/firmware_registry.rb
Original file line number Diff line number Diff line change
@@ -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'
miha-plesko marked this conversation as resolved.
Show resolved Hide resolved
)
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
62 changes: 62 additions & 0 deletions app/models/firmware_registry/rest_api_depot.rb
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions app/models/firmware_target.rb
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to not store attributes that aren't needed instead of overriding to_hash?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We mainly get rid of created/updated timestamps here to get a "pure" hash with relevant attributes only (:manufacturer and :model). Perhaps I should just rename the function to something else?

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
9 changes: 9 additions & 0 deletions app/models/physical_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions spec/factories/asset_detail.rb
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions spec/factories/firmware_binary.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions spec/factories/firmware_registry.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions spec/factories/firmware_target.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions spec/factories/physical_server.rb
Original file line number Diff line number Diff line change
@@ -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
49 changes: 49 additions & 0 deletions spec/models/firmware_binary_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
]
Loading