app/models/ems_refresh/save_inventory.rb
module EmsRefresh::SaveInventory
#
# Shared between Cloud and Infra
#
def save_vms_inventory(ems, hashes, target = nil)
return if hashes.nil?
target = ems if target.nil?
log_header = "EMS: [#{ems.name}], id: [#{ems.id}]"
disconnects = if target.kind_of?(ExtManagementSystem) || target.kind_of?(Host)
target.vms_and_templates.reload.to_a
elsif target.kind_of?(Vm)
[target.ruby_clone]
else
[]
end
child_keys = [:operating_system, :hardware, :custom_attributes, :snapshots, :advanced_settings, :labels, :tags]
extra_infra_keys = [:host, :ems_cluster, :storage, :storages, :storage_profile, :raw_power_state, :parent_vm]
extra_cloud_keys = [
:resource_group,
:flavor,
:availability_zone,
:cloud_tenant,
:cloud_tenants,
:cloud_network,
:cloud_subnet,
:security_groups,
:key_pairs,
:orchestration_stack,
]
remove_keys = child_keys + extra_infra_keys + extra_cloud_keys
vms_by_ems_ref = ems.vms_and_templates.group_by(&:ems_ref).except(nil)
# Query for all of the Vms once across all EMSes, to handle any moving VMs
vms_uids = hashes.collect { |h| h[:uid_ems] }.compact
vms = VmOrTemplate.where(:uid_ems => vms_uids).to_a
disconnects_index = disconnects.index_by { |vm| vm }
vms_by_uid_ems = vms.group_by(&:uid_ems)
dup_vms_uids = (vms_uids.duplicates + vms.collect(&:uid_ems).duplicates).uniq.sort
_log.info("#{log_header} Duplicate unique values found: #{dup_vms_uids.inspect}") unless dup_vms_uids.empty?
invalids_found = false
# Clear vms, so GC can clean them
vms = nil
ActiveRecord::Base.transaction do
hashes.each do |h|
# Backup keys that cannot be written directly to the database
key_backup = backup_keys(h, remove_keys)
h[:ems_id] = ems.id
h[:host_id] = key_backup.fetch_path(:host, :id) || key_backup.fetch_path(:host).try(:id)
h[:ems_cluster_id] = key_backup.fetch_path(:ems_cluster, :id) || key_backup.fetch_path(:ems_cluster).try(:id)
h[:storage_id] = key_backup.fetch_path(:storage, :id)
h[:storage_profile_id] = key_backup.fetch_path(:storage_profile, :id)
h[:flavor_id] = key_backup.fetch_path(:flavor, :id)
h[:availability_zone_id] = key_backup.fetch_path(:availability_zone, :id)
h[:cloud_network_id] = key_backup.fetch_path(:cloud_network, :id)
h[:cloud_subnet_id] = key_backup.fetch_path(:cloud_subnet, :id)
h[:cloud_tenant_id] = key_backup.fetch_path(:cloud_tenant, :id)
h[:cloud_tenant_ids] = key_backup.fetch_path(:cloud_tenants).compact.map { |x| x[:id] } if key_backup.fetch_path(:cloud_tenants, 0, :id)
h[:orchestration_stack_id] = key_backup.fetch_path(:orchestration_stack, :id)
h[:resource_group_id] = key_backup.fetch_path(:resource_group, :id)
begin
raise MiqException::MiqIncompleteData if h[:invalid]
# Find the Vm in the database with the current uid_ems. In the event
# of duplicates, try to determine which one is correct.
found = vms_by_ems_ref[h[:ems_ref]] || vms_by_uid_ems[h[:uid_ems]] || []
if found.length > 1 || (found.length == 1 && found.first.ems_id)
found_dups = found
found = found_dups.select { |v| v.ems_id == h[:ems_id] && (v.ems_ref.nil? || v.ems_ref == h[:ems_ref]) }
if found.empty?
found_dups = found_dups.select { |v| v.ems_id.nil? }
found = found_dups.select { |v| v.ems_ref == h[:ems_ref] }
found = found_dups if found.empty?
end
end
type = h[:template] ? "Template" : "Vm"
found = found.first
if found.nil?
_log.info("#{log_header} Creating #{type} [#{h[:name]}] location: [#{h[:location]}] storage id: [#{h[:storage_id]}] uid_ems: [#{h[:uid_ems]}] ems_ref: [#{h[:ems_ref]}]")
# Handle the off chance that we are adding an "unknown" Vm to the db
h[:location] = "unknown" if h[:location].blank?
# build a type-specific vm or template
found = ems.vms_and_templates.klass.new(h)
else
vms_by_uid_ems[h[:uid_ems]]&.delete(found)
h.delete(:type)
_log.info("#{log_header} Updating #{type} [#{found.name}] id: [#{found.id}] location: [#{found.location}] storage id: [#{found.storage_id}] uid_ems: [#{found.uid_ems}] ems_ref: [#{h[:ems_ref]}]")
found.update!(h)
disconnects_index.delete(found)
end
# Set the raw power state
found.raw_power_state = key_backup[:raw_power_state]
link_habtm(found, key_backup[:storages], :storages, Storage)
link_habtm(found, key_backup[:key_pairs], :key_pairs, ManageIQ::Providers::CloudManager::AuthKeyPair)
# Habtm somehow saves and caches incomplete object. So found.key_pairs.first.attributes returns e.g.
# {"id"=>23, "name"=>"EmsRefreshSpec-KeyPair-OtherRegion"} and that fails later on e.g. set_tenant. The
# behaviour is that the key_pair.respond_to(:group) returns true, but when we call it, it returns
# missing attribute: miq_group_id.
found.try(:key_pairs).try(:reload)
save_child_inventory(found, key_backup, child_keys)
found.save!
h[:id] = found.id
rescue => err
# If a vm failed to process, mark it as invalid and log an error
h[:invalid] = invalids_found = true
name = h[:name] || h[:uid_ems] || h[:ems_ref]
if err.kind_of?(MiqException::MiqIncompleteData)
_log.warn("#{log_header} Processing Vm: [#{name}] failed with error [#{err}]. Skipping Vm.")
else
raise if EmsRefresh.debug_failures
_log.error("#{log_header} Processing Vm: [#{name}] failed with error [#{err}]. Skipping Vm.")
_log.log_backtrace(err)
end
ensure
restore_keys(h, remove_keys, key_backup)
end
end
end
# Handle genealogy link ups
# TODO: can we use _object
vm_ids = hashes.flat_map { |h| !h[:invalid] && h.has_key_path?(:parent_vm, :id) ? [h[:id], h.fetch_path(:parent_vm, :id)] : [] }.uniq
unless vm_ids.empty?
_log.info("#{log_header} Updating genealogy connections.")
vms = VmOrTemplate.where(:id => vm_ids).index_by(&:id)
hashes.each do |h|
parent = vms[h.fetch_path(:parent_vm, :id)]
child = vms[h[:id]]
child.update!(:genealogy_parent => parent) if parent && child
end
end
disconnects = disconnects_index.values
unless disconnects.empty?
if invalids_found
_log.warn("#{log_header} Since failures occurred, not disconnecting for Vms #{log_format_deletes(disconnects)}")
elsif target.kind_of?(Host)
# The disconnected VMs may actually just be moved to another Host. We
# don't have enough information to fully disconnect from the EMS, so
# queue up a targeted refresh on that VM.
$log.warn("#{log_header} Queueing targeted refresh, since we do not have enough " \
"information to fully disconnect Vms #{log_format_deletes(disconnects)}")
EmsRefresh.queue_refresh(disconnects)
$log.info("#{log_header} Partially disconnecting Vms #{log_format_deletes(disconnects)}")
disconnects.each(&:disconnect_host)
else
_log.info("#{log_header} Disconnecting Vms #{log_format_deletes(disconnects)}")
disconnects.each(&:disconnect_inv)
end
end
end
# Convert all mapped hashes into actual tags and associate them with the object.
# The collection or collection[:tags] object should be an array of values
# created by the ProviderTagMapping::Mapper#map_labels method
# that should already have ids set by `Mapper#find_or_create_tags` method.
#
# The +collection+ argument can either be a Hash, in which case the argument
# should have a single :tags key, or a simple Array.
#
def save_tags_inventory(object, collection, _target = nil)
return if collection.nil?
tags = collection.kind_of?(Hash) ? collection[:tags] : collection
ProviderTagMapping.retag_entity(object, tags)
rescue => err
raise if EmsRefresh.debug_failures
_log.error("Auto-tagging failed on #{object.class} [#{object.name}] with error [#{err}].")
_log.log_backtrace(err)
end
def save_operating_system_inventory(parent, hash)
return if hash.nil?
# Only set a value if we do not have one and we have not collected scan metadata.
# Otherwise an ems may not contain the proper value and we do not want to overwrite
# the value collected during our metadata scan.
return if parent.kind_of?(Vm) && !(parent.drift_states.size.zero? || parent.operating_system.nil? || parent.operating_system.product_name.blank?)
save_inventory_single(:operating_system, parent, hash)
end
def save_hardware_inventory(parent, hash)
return if hash.nil?
save_inventory_single(:hardware, parent, hash, [:disks, :guest_devices, :networks, :firmwares])
parent.save!
end
def save_guest_devices_inventory(hardware, hashes)
return if hashes.nil?
# Update the associated ids
hashes.each do |h|
h[:switch_id] = h.fetch_path(:switch, :id)
h[:lan_id] = h.fetch_path(:lan, :id)
if h[:network]
# Save the hardware to force an id if not found
hardware.save! if hardware.id.nil?
h[:network][:hardware_id] = hardware.id
end
if h[:child_devices]
# Save the hardware to force an id if not found
hardware.save! if hardware.id.nil?
h[:child_devices].each do |child_device|
child_device[:hardware_id] = hardware.id
end
end
end
deletes = hardware.guest_devices.where(:device_type => ["ethernet", "storage"])
find_key = %i[device_type uid_ems]
child_keys = %i[network miq_scsi_targets firmwares physical_network_ports]
extra_keys = %i[switch lan]
save_inventory_multi(hardware.guest_devices, hashes, deletes, find_key, child_keys, extra_keys)
store_ids_for_new_records(hardware.guest_devices, hashes, find_key)
end
def save_child_devices_inventory(guest_device, hashes)
return if hashes.nil?
deletes = guest_device.child_devices.where(:device_type => ["ethernet", "storage"])
save_inventory_multi(guest_device.child_devices, hashes, deletes, [:device_type, :uid_ems], [:network, :miq_scsi_targets], [:switch, :lan])
store_ids_for_new_records(guest_device.child_devices, hashes, [:device_type, :uid_ems])
end
def save_disks_inventory(hardware, hashes)
return if hashes.nil?
# Update the associated ids
hashes.each do |h|
h[:storage_id] = h.fetch_path(:storage, :id)
h[:backing_id] = h.fetch_path(:backing, :id)
h[:storage_profile_id] = h.fetch_path(:storage_profile, :id)
end
save_inventory_multi(hardware.disks, hashes, :use_association, [:controller_type, :location], nil, [:storage, :backing, :storage_profile])
end
def save_network_inventory(guest_device, hash)
if hash.nil?
guest_device.network = nil
else
save_inventory_single(:network, guest_device, hash, nil, :guest_device)
hash[:id] = guest_device.network.id
end
end
def save_networks_inventory(hardware, hashes, mode = :refresh)
return if hashes.nil?
case mode
when :refresh
deletes = hardware.networks.reload.to_a
# Remove networks that were already saved via guest devices
saved_hashes, new_hashes = hashes.partition { |h| h[:id] }
saved_hashes.each { |h| deletes.delete_if { |d| d.id == h[:id] } } unless deletes.empty? || saved_hashes.empty?
save_inventory_multi(hardware.networks, new_hashes, deletes, [:ipaddress, :ipv6address], nil, :guest_device)
when :scan
save_inventory_multi(hardware.networks, hashes, :use_association, [:description, :guid])
end
end
def save_firmwares_inventory(hardware, hashes)
return if hashes.nil?
save_inventory_multi(hardware.firmwares, hashes, :use_association, [:name])
end
def save_computer_system_inventory(parent, hash, _target = nil)
save_inventory_single(:computer_system, parent, hash, [:hardware, :operating_system])
end
def save_system_services_inventory(parent, hashes, mode = :refresh)
return if hashes.nil?
deletes = case mode
when :refresh then nil
when :scan then :use_association
end
save_inventory_multi(parent.system_services, hashes, deletes, [:typename, :name])
end
def save_guest_applications_inventory(parent, hashes)
save_inventory_multi(parent.guest_applications, hashes, :use_association, [:arch, :typename, :name, :version])
end
def save_advanced_settings_inventory(parent, hashes)
save_inventory_multi(parent.advanced_settings, hashes, :use_association, [:name])
end
def save_patches_inventory(parent, hashes)
save_inventory_multi(parent.patches, hashes, :use_association, [:name])
end
def save_os_processes_inventory(os, hashes)
save_inventory_multi(os.processes, hashes, :use_association, [:pid])
end
def save_custom_attributes_inventory(parent, hashes, mode = :refresh)
return if hashes.nil?
deletes = case mode
when :refresh then nil
when :scan then :use_association
end
save_inventory_multi(parent.custom_attributes, hashes, deletes, [:name, :section])
end
def save_ems_custom_attributes_inventory(parent, hashes)
return if hashes.nil?
save_inventory_multi(parent.ems_custom_attributes, hashes, :use_association, [:section, :name])
end
def save_filesystems_inventory(parent, hashes)
save_inventory_multi(parent.filesystems, hashes, :use_association, [:name])
end
def save_snapshots_inventory(vm, hashes)
return if hashes.nil?
hashes.each { |h| h[:parent_id] = nil } # Delink all snapshots
save_inventory_multi(vm.snapshots, hashes, :use_association, [:uid])
# Reset the relationship tree for the snapshots
vm.snapshots.each do |s|
if s.parent_uid
parent = vm.snapshots.detect { |s2| s2.uid == s.parent_uid }
s.update_attribute(:parent_id, parent.try(:id))
end
end
end
def save_event_logs_inventory(os, hashes)
save_inventory_multi(os.event_logs, hashes, :use_association, [:uid])
end
#
# Storage managers can support many different types of storages. We thus rely
# on the supports feature of the manager to choose which parts of the
# inventory to save.
#
def save_ems_storage_inventory(ems, hashes, target = nil)
save_ems_block_storage_inventory(ems, hashes, target) if ems.supports?(:block_storage)
save_ems_object_storage_inventory(ems, hashes, target) if ems.supports?(:object_storage)
end
end