ManageIQ/manageiq

View on GitHub
app/models/host.rb

Summary

Maintainability
D
2 days
Test Coverage
F
55%
require 'ostruct'
require 'xml/xml_utils'
require 'cgi'               # Used for URL encoding/decoding
require 'metadata/linux/LinuxUsers'
require 'metadata/linux/LinuxUtils'
require 'metadata/ScanProfile/HostScanProfiles'

class Host < ApplicationRecord
  include SupportsFeatureMixin
  include NewWithTypeStiMixin
  include TenantIdentityMixin
  include DeprecationMixin
  include CustomActionsMixin
  include EmsRefreshMixin

  VENDOR_TYPES = {
    # DB            Displayed
    "microsoft"       => "Microsoft",
    "redhat"          => "Red Hat",
    "ovirt"           => "oVirt",
    "kubevirt"        => "KubeVirt",
    "vmware"          => "VMware",
    "openstack_infra" => "OpenStack Infrastructure",
    "ibm_power_hmc"   => "IBM Power HMC",
    "unknown"         => "Unknown",
    nil               => "Unknown",
  }.freeze

  validates_presence_of     :name
  validates_inclusion_of    :user_assigned_os, :in => ["linux_generic", "windows_generic", nil]
  validates_inclusion_of    :vmm_vendor, :in => VENDOR_TYPES.keys

  belongs_to                :ext_management_system, :foreign_key => "ems_id"
  belongs_to                :ems_cluster
  has_one                   :operating_system, :dependent => :destroy
  has_one                   :hardware, :dependent => :destroy
  has_many                  :vms_and_templates, :dependent => :nullify
  has_many                  :vms, :inverse_of => :host
  has_many                  :miq_templates, :inverse_of => :host
  has_many                  :host_storages, :dependent => :destroy
  has_many                  :storages, :through => :host_storages
  has_many                  :writable_accessible_host_storages, -> { writable_accessible }, :class_name => "HostStorage"
  has_many                  :writable_accessible_storages, :through => :writable_accessible_host_storages, :source => :storage

  has_many                  :host_virtual_switches, :class_name => "Switch", :dependent => :destroy, :inverse_of => :host
  has_many                  :host_switches, :dependent => :destroy
  has_many                  :switches, :through => :host_switches
  has_many                  :lans,     :through => :switches
  has_many                  :host_virtual_lans, :through => :host_virtual_switches, :source => :lans

  has_many                  :subnets,  :through => :lans
  has_many                  :networks, :through => :hardware
  has_many                  :patches, :dependent => :destroy
  has_many                  :system_services, :dependent => :destroy
  has_many                  :host_services, :class_name => "SystemService", :foreign_key => "host_id", :inverse_of => :host

  has_many                  :metrics,        :as => :resource  # Destroy will be handled by purger
  has_many                  :metric_rollups, :as => :resource  # Destroy will be handled by purger
  has_many                  :vim_performance_states, :as => :resource  # Destroy will be handled by purger

  has_many                  :ems_events,
                            ->(host) { where("host_id = ? OR dest_host_id = ?", host.id, host.id).order(:timestamp) },
                            :class_name => "EmsEvent"
  has_many                  :ems_events_src, :class_name => "EmsEvent"
  has_many                  :ems_events_dest, :class_name => "EmsEvent", :foreign_key => :dest_host_id

  has_many                  :policy_events, -> { order("timestamp") }
  has_many                  :guest_applications, :dependent => :destroy

  has_many                  :miq_events, :as => :target, :dependent => :destroy

  has_many                  :filesystems, :as => :resource, :dependent => :destroy
  has_many                  :directories, -> { where(:rsc_type => 'dir') },  :as => :resource, :class_name => "Filesystem"
  has_many                  :files,       -> { where(:rsc_type => 'file') }, :as => :resource, :class_name => "Filesystem"

  # Accounts - Users and Groups
  has_many                  :accounts, :dependent => :destroy
  has_many                  :users,  -> { where(:accttype => 'user') },  :class_name => "Account", :foreign_key => "host_id"
  has_many                  :groups, -> { where(:accttype => 'group') }, :class_name => "Account", :foreign_key => "host_id"

  has_many                  :advanced_settings, :as => :resource, :dependent => :destroy

  has_many                  :miq_alert_statuses, :dependent => :destroy, :as => :resource

  has_many                  :host_service_groups, :dependent => :destroy

  has_many                  :cloud_services, :dependent => :nullify
  has_many                  :host_cloud_services, :class_name => "CloudService", :foreign_key => "host_id",
                            :inverse_of => :host
  has_many                  :host_aggregate_hosts, :dependent => :destroy
  has_many                  :host_aggregates, :through => :host_aggregate_hosts
  has_many :host_hardwares, :class_name => 'Hardware', :dependent => :nullify
  has_many :vm_hardwares,   :class_name => 'Hardware', :through => :vms_and_templates, :source => :hardware

  # Physical server reference
  belongs_to :physical_server, :inverse_of => :host

  serialize :settings, Hash

  deprecate_attribute :address,  :hostname, :type => :string
  alias_attribute     :state,    :power_state
  alias_attribute     :to_s,     :name

  include ProviderObjectMixin
  include EventMixin

  include CustomAttributeMixin
  has_many :ems_custom_attributes, -> { where(:source => 'VC') }, # rubocop:disable Rails/HasManyOrHasOneDependent
           :class_name => "CustomAttribute",
           :as         => :resource,
           :inverse_of => :resource
  has_many :filesystems_custom_attributes, :through => :filesystems, :source => 'custom_attributes'

  acts_as_miq_taggable

  virtual_column :os_image_name,                :type => :string,      :uses => [:operating_system, :hardware]
  virtual_column :platform,                     :type => :string,      :uses => [:operating_system, :hardware]
  virtual_delegate :v_owning_cluster, :to => "ems_cluster.name", :allow_nil => true, :default => "", :type => :string
  virtual_column :v_owning_datacenter,          :type => :string,      :uses => :all_relationships
  virtual_column :v_owning_folder,              :type => :string,      :uses => :all_relationships
  virtual_delegate :cpu_total_cores, :cpu_cores_per_socket, :to => :hardware, :allow_nil => true, :default => 0, :type => :integer
  virtual_delegate :num_cpu,     :to => "hardware.cpu_sockets",        :allow_nil => true, :default => 0, :type => :integer
  virtual_delegate :total_vcpus, :to => "hardware.cpu_total_cores",    :allow_nil => true, :default => 0, :type => :integer
  virtual_delegate :ram_size,    :to => "hardware.memory_mb",          :allow_nil => true, :default => 0, :type => :integer
  virtual_column :enabled_inbound_ports,        :type => :numeric_set  # The following are not set to use anything
  virtual_column :enabled_outbound_ports,       :type => :numeric_set  # because get_ports ends up re-querying the
  virtual_column :enabled_udp_inbound_ports,    :type => :numeric_set  # database anyway.
  virtual_column :enabled_udp_outbound_ports,   :type => :numeric_set
  virtual_column :enabled_tcp_inbound_ports,    :type => :numeric_set
  virtual_column :enabled_tcp_outbound_ports,   :type => :numeric_set
  virtual_column :all_enabled_ports,            :type => :numeric_set
  virtual_column :service_names,                :type => :string_set,  :uses => :system_services
  virtual_column :enabled_run_level_0_services, :type => :string_set,  :uses => :host_services
  virtual_column :enabled_run_level_1_services, :type => :string_set,  :uses => :host_services
  virtual_column :enabled_run_level_2_services, :type => :string_set,  :uses => :host_services
  virtual_column :enabled_run_level_3_services, :type => :string_set,  :uses => :host_services
  virtual_column :enabled_run_level_4_services, :type => :string_set,  :uses => :host_services
  virtual_column :enabled_run_level_5_services, :type => :string_set,  :uses => :host_services
  virtual_column :enabled_run_level_6_services, :type => :string_set,  :uses => :host_services
  virtual_delegate :annotation, :to => :hardware, :prefix => "v", :allow_nil => true, :type => :string
  virtual_column :vmm_vendor_display,           :type => :string
  virtual_column :ipmi_enabled,                 :type => :boolean
  virtual_attribute :archived, :boolean, :arel => ->(t) { t.grouping(t[:ems_id].eq(nil)) }
  virtual_column :normalized_state, :type => :string

  virtual_has_many   :resource_pools,                               :uses => :all_relationships
  virtual_has_many   :miq_scsi_luns,                                :uses => {:hardware => {:storage_adapters => {:miq_scsi_targets => :miq_scsi_luns}}}
  virtual_has_many   :processes,       :class_name => "OsProcess",  :uses => {:operating_system => :processes}
  virtual_has_many   :event_logs,                                   :uses => {:operating_system => :event_logs}
  virtual_has_many   :firewall_rules,                               :uses => {:operating_system => :firewall_rules}

  virtual_total :v_total_storages, :host_storages
  virtual_total :v_total_vms, :vms
  virtual_total :v_total_miq_templates, :miq_templates

  scope :active,   -> { where.not(:ems_id => nil) }
  scope :archived, -> { where(:ems_id => nil) }

  alias_method :datastores, :storages    # Used by web-services to return datastores as the property name

  alias_method :parent_cluster, :ems_cluster
  alias_method :owning_cluster, :ems_cluster

  include RelationshipMixin
  self.default_relationship_type = "ems_metadata"

  include DriftStateMixin
  virtual_delegate :last_scan_on, :to => "last_drift_state_timestamp_rec.timestamp", :allow_nil => true, :type => :datetime

  delegate :queue_name_for_ems_operations, :to => :ext_management_system, :allow_nil => true

  include UuidMixin
  include MiqPolicyMixin
  include AlertMixin
  include Metric::CiMixin
  include FilterableMixin
  include AuthenticationMixin
  include AsyncDeleteMixin
  include ComplianceMixin
  include AggregationMixin

  before_create :make_smart
  after_save    :process_events

  supports     :check_compliance_queue
  supports     :destroy
  supports     :scan_and_check_compliance_queue
  supports     :ipmi do
    if ipmi_address.blank?
      _("The Host is not configured for IPMI")
    elsif authentication_type(:ipmi).nil?
      _("The Host has no IPMI credentials")
    elsif authentication_userid(:ipmi).blank? || authentication_password(:ipmi).blank?
      _("The Host has invalid IPMI credentials")
    end
  end

  # if you change this, please check in on VmWare#start
  supports :start do
    if !supports?(:ipmi)
      unsupported_reason(:ipmi)
    elsif power_state != "off"
      _("The Host is not in power state off")
    end
  end

  supports :stop do
    if !supports?(:ipmi)
      unsupported_reason(:ipmi)
    elsif power_state != "on"
      _("The Host is not in powered on")
    end
  end

  supports(:reset) { unsupported_reason(:ipmi) }

  def self.non_clustered
    where(:ems_cluster_id => nil)
  end

  def self.clustered
    where.not(:ems_cluster_id => nil)
  end

  def self.failover
    where(:failover => true)
  end

  def authentication_check_role
    'smartstate'
  end

  def my_zone
    ems = ext_management_system
    ems ? ems.my_zone : MiqServer.my_zone
  end

  def make_smart
    self.smart = true
  end

  def process_events
    return unless saved_change_to_ems_cluster_id?

    raise_cluster_event(ems_cluster_id_before_last_save, "host_remove_from_cluster") if ems_cluster_id_before_last_save
    raise_cluster_event(ems_cluster, "host_add_to_cluster") if ems_cluster_id
  end # after_save

  def raise_cluster_event(ems_cluster, event)
    # accept ids or objects
    ems_cluster = EmsCluster.find(ems_cluster) unless ems_cluster.kind_of?(EmsCluster)
    inputs = {:ems_cluster => ems_cluster, :host => self}
    begin
      MiqEvent.raise_evm_event(self, event, inputs)
      _log.info("Raised EVM Event: [#{event}, host: #{name}(#{id}), cluster: #{ems_cluster.name}(#{ems_cluster.id})]")
    rescue => err
      _log.warn("Error raising EVM Event: [#{event}, host: #{name}(#{id}), cluster: #{ems_cluster.name}(#{ems_cluster.id})], '#{err.message}'")
    end
  end
  private :raise_cluster_event

  def has_active_ems?
    !!ext_management_system
  end

  def run_ipmi_command(verb)
    require 'miq-ipmi'
    _log.info("Invoking [#{verb}] for Host: [#{name}], IPMI Address: [#{ipmi_address}], IPMI Username: [#{authentication_userid(:ipmi)}]")
    ipmi = MiqIPMI.new(ipmi_address, *auth_user_pwd(:ipmi))
    ipmi.send(verb)
  end

  # event:   the event sent to automate for policy resolution
  # cb_method: the MiqQueue callback method along with the parameters that is called
  #            when automate process is done and the request is not prevented to proceed by policy
  def check_policy_prevent(event, *cb_method)
    MiqEvent.raise_evm_event(self, event, {:host => self}, {:miq_callback => prevent_callback_settings(*cb_method)})
  end

  def ipmi_power_on
    run_ipmi_command(:power_on)
  end

  def ipmi_power_off
    run_ipmi_command(:power_off)
  end

  def ipmi_power_reset
    run_ipmi_command(:power_reset)
  end

  def reset
    if verbose_supports?(:reset)
      check_policy_prevent("request_host_reset", "ipmi_power_reset")
    end
  end

  def start
    if verbose_supports?(:start) && supports?(:ipmi)
      pstate = run_ipmi_command(:power_state)
      if pstate == "off"
        check_policy_prevent("request_host_start", "ipmi_power_on")
      else
        _log.warn("Non-Startable IPMI power state = <#{pstate.inspect}>")
      end
    end
  end

  def stop
    if verbose_supports?(:stop)
      check_policy_prevent("request_host_stop", "ipmi_power_off")
    end
  end

  def standby
    if verbose_supports?(:standby)
      check_policy_prevent("request_host_standby", "vim_power_down_to_standby")
    end
  end

  def enter_maint_mode
    if verbose_supports?(:enter_maint_mode)
      check_policy_prevent("request_host_enter_maintenance_mode", "vim_enter_maintenance_mode")
    end
  end

  def exit_maint_mode
    if verbose_supports?(:exit_maint_mode)
      check_policy_prevent("request_host_exit_maintenance_mode", "vim_exit_maintenance_mode")
    end
  end

  def shutdown
    if verbose_supports?(:shutdown)
      check_policy_prevent("request_host_shutdown", "vim_shutdown")
    end
  end

  def reboot
    if verbose_supports?(:reboot)
      check_policy_prevent("request_host_reboot", "vim_reboot")
    end
  end

  def enable_vmotion
    if verbose_supports?(:enable_vmotion)
      check_policy_prevent("request_host_enable_vmotion", "vim_enable_vmotion")
    end
  end

  def disable_vmotion
    if verbose_supports?(:disable_vmotion)
      check_policy_prevent("request_host_disable_vmotion", "vim_disable_vmotion")
    end
  end

  def vmotion_enabled?
    if verbose_supports?(:vmotion_enabled, "check if vmotion is enabled")
      vim_vmotion_enabled?
    end
  end

  # Scan for VMs in a path defined in a repository
  def add_elements(data)
    if data.kind_of?(Hash) && data[:type] == :ems_events
      _log.info("Adding HASH elements for Host id:[#{id}]-[#{name}] from [#{data[:type]}]")
      add_ems_events(data)
    end
  rescue => err
    _log.log_backtrace(err)
  end

  def ipaddresses
    hardware.nil? ? [] : hardware.ipaddresses
  end

  def hostnames
    hardware.nil? ? [] : hardware.hostnames
  end

  def mac_addresses
    hardware.nil? ? [] : hardware.mac_addresses
  end

  def has_config_data?
    !operating_system.nil? && !hardware.nil?
  end

  def os_image_name
    OperatingSystem.image_name(self)
  end

  def platform
    OperatingSystem.platform(self)
  end

  def product_name
    operating_system.nil? ? "" : operating_system.product_name
  end

  def service_pack
    operating_system.nil? ? "" : operating_system.service_pack
  end

  def arch
    if vmm_product.to_s.include?('ESX')
      return 'x86_64' if vmm_version.to_i >= 4

      return 'x86'
    end

    return "unknown" unless hardware && !hardware.cpu_type.nil?

    cpu = hardware.cpu_type.to_s.downcase
    return cpu if cpu.include?('x86')
    return "x86" if cpu.starts_with?("intel")

    "unknown"
  end

  def platform_arch
    ret = [os_image_name.split("_")[0], arch == "unknown" ? "x86" : arch]
    ret.include?("unknown") ? nil : ret
  end

  def refreshable_status
    if ext_management_system
      return {:show => true, :enabled => true, :message => ""}
    end

    {:show => false, :enabled => false, :message => "Host not configured for refresh"}
  end

  def scannable_status
    s = refreshable_status
    return s if s[:show] || s[:enabled]

    s[:show] = true
    if has_credentials?(:ipmi) && ipmi_address.present?
      s.merge!(:enabled => true, :message => "")
    elsif ipmi_address.blank?
      s.merge!(:enabled => false, :message => "Provide an IPMI Address")
    elsif missing_credentials?(:ipmi)
      s.merge!(:enabled => false, :message => "Provide credentials for IPMI")
    end

    s
  end

  def is_refreshable?
    refreshable_status[:show]
  end

  def is_refreshable_now?
    refreshable_status[:enabled]
  end

  def is_refreshable_now_error_message
    refreshable_status[:message]
  end

  def is_scannable?
    scannable_status[:show]
  end

  def is_scannable_now?
    scannable_status[:enabled]
  end

  def is_scannable_now_error_message
    scannable_status[:message]
  end

  def is_vmware?
    vmm_vendor == 'vmware'
  end

  def is_vmware_esx?
    is_vmware? && vmm_product.to_s.strip.downcase.starts_with?('esx')
  end

  def is_vmware_esxi?
    product = vmm_product.to_s.strip.downcase
    is_vmware? && product.starts_with?('esx') && product.ends_with?('i')
  end

  def vmm_vendor_display
    VENDOR_TYPES[vmm_vendor]
  end

  #
  # Relationship methods
  #

  def disconnect_inv
    disconnect_ems
    remove_all_parents(:of_type => ['EmsFolder', 'EmsCluster'])
  end

  def connect_ems(e)
    return if ext_management_system == e

    _log.debug("Connecting Host [#{name}] id [#{id}] to EMS [#{e.name}] id [#{e.id}]")
    self.ext_management_system = e
    save
  end

  def disconnect_ems(e = nil)
    if e.nil? || ext_management_system == e
      log_text = " from EMS [#{ext_management_system.name}] id [#{ext_management_system.id}]" unless ext_management_system.nil?
      _log.info("Disconnecting Host [#{name}] id [#{id}]#{log_text}")

      self.ext_management_system = nil
      self.ems_cluster = nil
      self.state = "unknown"
      save
    end
  end

  def connect_storage(s)
    unless storages.include?(s)
      _log.debug("Connecting Host [#{name}] id [#{id}] to Storage [#{s.name}] id [#{s.id}]")
      storages << s
      save
    end
  end

  def disconnect_storage(s)
    _log.info("Disconnecting Host [#{name}] id [#{id}] from Storage [#{s.name}] id [#{s.id}]")
    storages.delete(s)
    save
  end

  # Vm relationship methods
  def direct_vms
    # Look for only the Vms at the second depth (default RP + 1)
    grandchildren(:of_type => 'Vm').sort_by { |r| r.name.downcase }
  end

  # Resource Pool relationship methods
  def default_resource_pool
    Relationship.resource(child_rels(:of_type => 'ResourcePool').first)
  end

  def resource_pools
    Relationship.resources(grandchild_rels(:of_type => 'ResourcePool'))
  end

  def resource_pools_with_default
    Relationship.resources(child_and_grandchild_rels(:of_type => 'ResourcePool'))
  end

  # All RPs under this Host and all child RPs
  def all_resource_pools
    # descendants typically returns the default_rp first but sporadically it
    # will not due to a bug in the ancestry gem, this means we cannot simply
    # drop the first value and need to check is_default
    descendants(:of_type => 'ResourcePool').select { |r| !r.is_default }.sort_by { |r| r.name.downcase }
  end

  def all_resource_pools_with_default
    descendants(:of_type => 'ResourcePool').sort_by { |r| r.name.downcase }
  end

  # Parent relationship methods
  def parent_folder
    p = parent
    p if p.kind_of?(EmsFolder)
  end

  def owning_folder
    detect_ancestor(:of_type => "EmsFolder") { |a| !a.kind_of?(Datacenter) && !%w[host vm].include?(a.name) }
  end

  def parent_datacenter
    detect_ancestor(:of_type => "EmsFolder") { |a| a.kind_of?(Datacenter) }
  end
  alias_method :owning_datacenter, :parent_datacenter

  def self.save_metadata(id, dataArray)
    _log.info("for host [#{id}]")
    host = Host.find_by(:id => id)
    data, data_type = dataArray
    data.replace(MIQEncode.decode(data)) if data_type.include?('b64,zlib')
    doc = data_type.include?('yaml') ? YAML.load(data) : MiqXml.load(data)
    host.add_elements(doc)
    host.save!
    _log.info("for host [#{id}] host saved")
  rescue => err
    _log.log_backtrace(err)
    false
  end

  def self.batch_update_authentication(host_ids, creds = {})
    errors = []
    return true if host_ids.blank?

    host_ids.each do |id|
      begin
        host = Host.find(id)
        host.update_authentication(creds)
      rescue ActiveRecord::RecordNotFound => err
        _log.warn("#{err.class.name}-#{err}")
        next
      rescue => err
        errors << err.to_s
        _log.error("#{err.class.name}-#{err}")
        next
      end
    end
    errors.empty? ? true : errors
  end

  def verify_credentials_task(userid, auth_type = nil, options = {})
    task_opts = {
      :action => "Verify Host Credentials",
      :userid => userid
    }

    queue_opts = {
      :args        => [auth_type, options],
      :class_name  => self.class.name,
      :instance_id => id,
      :method_name => "verify_credentials?",
      :queue_name  => queue_name_for_ems_operations,
      :role        => "ems_operations",
      :zone        => my_zone
    }

    MiqTask.generic_action_with_callback(task_opts, queue_opts)
  end

  def verify_credentials?(auth_type = nil, options = {})
    # Prevent the connection details, including the password, from being leaked into the logs
    # and MiqQueue by only returning true/false
    auth = options.delete("authentications")
    update_authentication(auth.deep_symbolize_keys, :save => false) if auth.present?

    !!verify_credentials(auth_type, options)
  end

  def verify_credentials(auth_type = nil, options = {})
    raise MiqException::MiqHostError, _("No credentials defined") if missing_credentials?(auth_type)
    if auth_type.to_s != 'ipmi' && os_image_name !~ /linux_*/
      raise MiqException::MiqHostError, _("Logon to platform [%{os_name}] not supported") % {:os_name => os_image_name}
    end

    case auth_type.to_s
    when 'remote' then verify_credentials_with_ssh(auth_type, options)
    when 'ws'     then verify_credentials_with_ws(auth_type)
    when 'ipmi'   then verify_credentials_with_ipmi(auth_type)
    else
      verify_credentials_default(auth_type, options)
    end

    true
  end

  # different providers use different default credential checks
  def verify_credentials_default(auth_type, options)
    verify_credentials_with_ssh(auth_type, options)
  end

  def verify_credentials_with_ws(_auth_type = nil, _options = {})
    raise MiqException::MiqHostError, _("Web Services authentication is not supported for hosts of this type.")
  end

  def verify_credentials_with_ssh(auth_type = nil, options = {})
    raise MiqException::MiqHostError, _("No credentials defined") if missing_credentials?(auth_type)
    unless /linux_*/.match?(os_image_name)
      raise MiqException::MiqHostError, _("Logon to platform [%{os_name}] not supported") % {:os_name => os_image_name}
    end

    begin
      # connect_ssh logs address and user name(s) being used to make connection
      _log.info("Verifying Host SSH credentials for [#{name}]")
      connect_ssh(options) { |ssu| ssu.exec("uname -a") }
    rescue Net::SSH::AuthenticationFailed => err
      raise err, _("Login failed due to a bad username or password.")
    rescue Net::SSH::HostKeyMismatch
      raise # Re-raise the error so the UI can prompt the user to allow the keys to be reset.
    rescue Exception => err
      _log.warn(err.inspect)
      raise MiqException::MiqHostError, _("Unexpected response returned from system, see log for details")
    else
      true
    end
  end

  def verify_credentials_with_ipmi(auth_type = nil)
    raise _("No credentials defined for IPMI") if missing_credentials?(auth_type)

    require 'miq-ipmi'
    address = ipmi_address
    raise MiqException::MiqHostError, _("IPMI address is not configured for this Host") if address.blank?

    if MiqIPMI.is_available?(address)
      ipmi = MiqIPMI.new(address, *auth_user_pwd(auth_type))
      unless ipmi.connected?
        raise MiqException::MiqInvalidCredentialsError, _("Login failed due to a bad username or password.")
      end
    else
      raise MiqException::MiqHostError, _("IPMI is not available on this Host")
    end
  end

  def self.get_hostname(ipAddress)
    _log.info("Resolving hostname: [#{ipAddress}]")
    begin
      ret = Socket.gethostbyname(ipAddress)
      name = ret.first
    rescue => err
      _log.error("ERROR:  #{err}")
      return nil
    end
    _log.info("Resolved hostname: [#{name}] to [#{ipAddress}]")
    name
  end

  def ssh_users_and_passwords
    if has_authentication_type?(:remote)
      rl_user, rl_password = auth_user_pwd(:remote)
      su_user, su_password = auth_user_pwd(:root)
    else
      rl_user, rl_password = auth_user_pwd(:root)
      su_user, su_password = nil, nil
    end
    return rl_user, rl_password, su_user, su_password, {}
  end

  def connect_ssh(options = {})
    require 'manageiq-ssh-util'

    rl_user, rl_password, su_user, su_password, additional_options = ssh_users_and_passwords
    options.merge!(additional_options)

    prompt_delay = ::Settings.ssh.try(:authentication_prompt_delay)
    options[:authentication_prompt_delay] = prompt_delay unless prompt_delay.nil?

    users = su_user.nil? ? rl_user : "#{rl_user}/#{su_user}"
    # Obfuscate private keys in the log with ****, so it's visible that field was used, but no user secret is exposed
    logged_options = options.dup
    logged_options[:key_data] = "[FILTERED]" if logged_options[:key_data]

    _log.info("Initiating SSH connection to Host:[#{name}] using [#{hostname}] for user:[#{users}].  Options:[#{logged_options.inspect}]")
    begin
      ManageIQ::SSH::Util.shell_with_su(hostname, rl_user, rl_password, su_user, su_password, options) do |ssu, _shell|
        _log.info("SSH connection established to [#{hostname}]")
        yield(ssu)
      end
      _log.info("SSH connection completed to [#{hostname}]")
    rescue Exception => err
      _log.error("SSH connection failed for [#{hostname}] with [#{err.class}: #{err}]")
      raise err
    end
  end

  def refresh_patches(ssu)
    return unless vmm_buildnumber && vmm_buildnumber != patches.highest_patch_level

    patches = []
    begin
      sb = ssu.shell_exec("esxupdate query")
      t = Time.now
      sb.each_line do |line|
        next if /-{5,}/.match?(line) # skip any header/footer rows

        data = line.split(" ")
        # Find the lines we should skip
        begin
          next if data[1, 2].nil?

          dhash = {:name => data[0], :vendor => "VMware", :installed_on => Time.parse(data[1, 2].join(" ")).utc}
          next if dhash[:installed_on] - t >= 0

          dhash[:description] = data[3..-1].join(" ") unless data[3..-1].nil?
          patches << dhash
        rescue ArgumentError => err
          _log.log_backtrace(err)
          next
        rescue => err
          _log.log_backtrace(err)
        end
      end
    rescue
    end

    Patch.refresh_patches(self, patches)
  end

  def collect_services(ssu)
    services = ssu.shell_exec("systemctl -a --type service --no-legend")
    if services
      # If there is a systemd use only that, chconfig is calling systemd on the background, but has misleading results
      MiqLinux::Utils.parse_systemctl_list(services)
    else
      services = ssu.shell_exec("chkconfig --list")
      MiqLinux::Utils.parse_chkconfig_list(services)
    end
  end

  def refresh_services(ssu)
    xml = MiqXml.createDoc(:miq).root.add_element(:services)

    services = collect_services(ssu)

    services.each do |service|
      s = xml.add_element(:service,
                          'name'           => service[:name],
                          'systemd_load'   => service[:systemd_load],
                          'systemd_sub'    => service[:systemd_sub],
                          'description'    => service[:description],
                          'running'        => service[:running],
                          'systemd_active' => service[:systemd_active],
                          'typename'       => service[:typename])
      service[:enable_run_level].each  { |l| s.add_element(:enable_run_level,  'value' => l) } unless service[:enable_run_level].nil?
      service[:disable_run_level].each { |l| s.add_element(:disable_run_level, 'value' => l) } unless service[:disable_run_level].nil?
    end
    SystemService.add_elements(self, xml.root)
  rescue
  end

  def refresh_linux_packages(ssu)
    pkg_xml = MiqXml.createDoc(:miq).root.add_element(:software).add_element(:applications)
    rpm_list = ssu.shell_exec("rpm -qa --queryformat '%{NAME}|%{VERSION}|%{ARCH}|%{GROUP}|%{RELEASE}|%{SUMMARY}\n'").force_encoding("utf-8")
    rpm_list.each_line do |line|
      l = line.split('|')
      pkg_xml.add_element(:application, 'name' => l[0], 'version' => l[1], 'arch' => l[2], 'typename' => l[3], 'release' => l[4], 'description' => l[5])
    end
    GuestApplication.add_elements(self, pkg_xml.root)
  rescue
  end

  def refresh_user_groups(ssu)
    xml = MiqXml.createDoc(:miq)
    node = xml.root.add_element(:accounts)
    MiqLinux::Users.new(ssu).to_xml(node)
    Account.add_elements(self, xml.root)
  rescue
    # _log.log_backtrace($!)
  end

  def refresh_ssh_config(ssu)
    self.ssh_permit_root_login = nil
    permit_list = ssu.shell_exec("grep PermitRootLogin /etc/ssh/sshd_config")
    # Setting default value to yes, which is default according to man sshd_config, if ssh returned something
    self.ssh_permit_root_login = 'yes' if permit_list
    permit_list.each_line do |line|
      la = line.split(' ')
      if la.length == 2
        next if la.first[0, 1] == '#'

        self.ssh_permit_root_login = la.last.to_s.downcase
        break
      end
    end
  rescue
    # _log.log_backtrace($!)
  end

  def refresh_fs_files(ssu)
    sp = HostScanProfiles.new(ScanItem.get_profile("host default"))
    files = sp.parse_data_files(ssu)
    EmsRefresh.save_filesystems_inventory(self, files) if files
  rescue
    # _log.log_backtrace($!)
  end

  def refresh_ipmi
    if ipmi_config_valid?
      require 'miq-ipmi'
      address = ipmi_address

      if MiqIPMI.is_available?(address)
        ipmi = MiqIPMI.new(address, *auth_user_pwd(:ipmi))
        if ipmi.connected?
          self.power_state = ipmi.power_state
          mac = ipmi.mac_address
          self.mac_address = mac if mac.present?

          hw_info = {:manufacturer => ipmi.manufacturer, :model => ipmi.model}
          if hardware.nil?
            EmsRefresh.save_hardware_inventory(self, hw_info)
          else
            hardware.update(hw_info)
          end
        else
          _log.warn("IPMI Login failed due to a bad username or password.")
        end
      else
        _log.info("IPMI is not available on this Host")
      end
    end
  end

  def ipmi_config_valid?(include_mac_addr = false)
    return false unless ipmi_address.present? && has_credentials?(:ipmi)

    include_mac_addr == true ? mac_address.present? : true
  end
  alias_method :ipmi_enabled, :ipmi_config_valid?

  def set_custom_field(attribute, value)
    return unless is_vmware?
    raise _("Host has no EMS, unable to set custom attribute") unless ext_management_system

    ext_management_system.set_custom_field(self, :attribute => attribute, :value => value)
  end

  def quickStats
    return @qs if @qs
    return {} unless supports?(:quick_stats)

    begin
      raise _("Host has no EMS, unable to get host statistics") unless ext_management_system

      @qs = ext_management_system.host_quick_stats(self)
    rescue => err
      _log.warn("Error '#{err.message}' encountered attempting to get host quick statistics")
      return {}
    end
    @qs
  end

  def current_memory_usage
    quickStats["overallMemoryUsage"].to_i
  end

  def current_cpu_usage
    quickStats["overallCpuUsage"].to_i
  end

  def current_memory_headroom
    ram_size - current_memory_usage
  end

  def firewall_rules
    return [] if operating_system.nil?

    operating_system.firewall_rules
  end

  def enforce_policy(vm, event)
    inputs = {:vm => vm, :host => self}
    MiqEvent.raise_evm_event(vm, event, inputs)
  end

  def first_cat_entry(name)
    Classification.first_cat_entry(name, self)
  end

  def scan(userid = "system", options = {})
    _log.info("Requesting scan of #{log_target}")
    check_policy_prevent(:request_host_scan, :scan_queue, userid, options)
  end

  def scan_queue(userid = 'system', _options = {})
    _log.info("Queuing scan of #{log_target}")

    task = MiqTask.create(:name => "SmartState Analysis for '#{name}' ", :userid => userid)
    return unless validate_task(task)

    timeout = ::Settings.host_scan.queue_timeout.to_i_with_method
    cb = {:class_name => task.class.name, :instance_id => task.id, :method_name => :queue_callback_on_exceptions, :args => ['Finished']}
    MiqQueue.put(
      :class_name   => self.class.name,
      :instance_id  => id,
      :args         => [task.id],
      :method_name  => "scan_from_queue",
      :miq_callback => cb,
      :msg_timeout  => timeout,
      :role         => "ems_operations",
      :queue_name   => queue_name_for_ems_operations,
      :zone         => my_zone
    )
  end

  def scan_from_queue(taskid = nil)
    unless taskid.nil?
      task = MiqTask.find_by(:id => taskid)
      task.state_active if task
    end

    _log.info("Scanning #{log_target}...")

    task.update_status("Active", "Ok", "Scanning") if task

    _dummy, t = Benchmark.realtime_block(:total_time) do
      if supports?(:refresh_firewall_rules)
        # Firewall Rules and Advanced Settings go through EMS so we don't need Host credentials
        _log.info("Refreshing Firewall Rules for #{log_target}")
        task.update_status("Active", "Ok", "Refreshing Firewall Rules") if task
        Benchmark.realtime_block(:refresh_firewall_rules) { refresh_firewall_rules }
      end

      if supports?(:refresh_advanced_settings)
        _log.info("Refreshing Advanced Settings for #{log_target}")
        task.update_status("Active", "Ok", "Refreshing Advanced Settings") if task
        Benchmark.realtime_block(:refresh_advanced_settings) { refresh_advanced_settings }
      end

      if ext_management_system.nil?
        _log.info("Refreshing IPMI information for #{log_target}")
        task.update_status("Active", "Ok", "Refreshing IPMI Information") if task
        Benchmark.realtime_block(:refresh_ipmi) { refresh_ipmi }
      end

      save

      # Skip SSH for ESXi hosts
      unless is_vmware_esxi?
        if hostname.blank?
          _log.warn("No hostname defined for #{log_target}")
          task.update_status("Finished", "Warn", "Scanning incomplete due to missing hostname")  if task
          return
        end

        update_ssh_auth_status! if respond_to?(:update_ssh_auth_status!)

        if missing_credentials?
          _log.warn("No credentials defined for #{log_target}")
          task.update_status("Finished", "Warn", "Scanning incomplete due to Credential Issue")  if task
          return
        end

        begin
          connect_ssh do |ssu|
            _log.info("Refreshing Patches for #{log_target}")
            task.update_status("Active", "Ok", "Refreshing Patches") if task
            Benchmark.realtime_block(:refresh_patches) { refresh_patches(ssu) }

            _log.info("Refreshing Services for #{log_target}")
            task.update_status("Active", "Ok", "Refreshing Services") if task
            Benchmark.realtime_block(:refresh_services) { refresh_services(ssu) }

            _log.info("Refreshing Linux Packages for #{log_target}")
            task.update_status("Active", "Ok", "Refreshing Linux Packages") if task
            Benchmark.realtime_block(:refresh_linux_packages) { refresh_linux_packages(ssu) }

            _log.info("Refreshing User Groups for #{log_target}")
            task.update_status("Active", "Ok", "Refreshing User Groups") if task
            Benchmark.realtime_block(:refresh_user_groups) { refresh_user_groups(ssu) }

            _log.info("Refreshing SSH Config for #{log_target}")
            task.update_status("Active", "Ok", "Refreshing SSH Config") if task
            Benchmark.realtime_block(:refresh_ssh_config) { refresh_ssh_config(ssu) }

            _log.info("Refreshing FS Files for #{log_target}")
            task.update_status("Active", "Ok", "Refreshing FS Files") if task
            Benchmark.realtime_block(:refresh_fs_files) { refresh_fs_files(ssu) }

            if supports?(:refresh_network_interfaces)
              _log.info("Refreshing network interfaces for #{log_target}")
              task.update_status("Active", "Ok", "Refreshing network interfaces") if task
              Benchmark.realtime_block(:refresh_network_interfaces) { refresh_network_interfaces(ssu) }
            end

            # refresh_openstack_services should run after refresh_services and refresh_fs_files
            if respond_to?(:refresh_openstack_services)
              _log.info("Refreshing OpenStack Services for #{log_target}")
              task.update_status("Active", "Ok", "Refreshing OpenStack Services") if task
              Benchmark.realtime_block(:refresh_openstack_services) { refresh_openstack_services(ssu) }
            end

            save
          end
        rescue Net::SSH::HostKeyMismatch
          # Keep from dumping stack trace for this error which is sufficiently logged in the connect_ssh method
        rescue => err
          _log.log_backtrace(err)
        end
      end

      if supports?(:refresh_logs)
        _log.info("Refreshing Log information for #{log_target}")
        task.update_status("Active", "Ok", "Refreshing Log Information") if task
        Benchmark.realtime_block(:refresh_logs) { refresh_logs }
      end

      _log.info("Saving state for #{log_target}")
      task.update_status("Active", "Ok", "Saving Drift State") if task
      Benchmark.realtime_block(:save_driftstate) { save_drift_state }

      begin
        MiqEvent.raise_evm_job_event(self, :type => "scan", :suffix => "complete")
      rescue => err
        _log.warn("Error raising complete scan event for #{log_target}: #{err.message}")
      end
    end

    task.update_status("Finished", "Ok", "Scanning Complete") if task
    _log.info("Scanning #{log_target}...Complete - Timings: #{t.inspect}")
  end

  def validate_task(task)
    if ext_management_system&.zone&.maintenance?
      task.update_status(MiqTask::STATE_FINISHED, MiqTask::STATUS_ERROR, "#{ext_management_system.name} is paused")
      return false
    end
    true
  end

  def ssh_run_script(script)
    connect_ssh { |ssu| return ssu.shell_exec(script) }
  end

  def add_ems_events(event_hash)
    event_hash[:events].each do |event|
      event[:ems_id] = ems_id
      event[:host_name] = name
      event[:host_id] = id
      begin
        EmsEvent.add(ems_id, event)
      rescue => err
        _log.log_backtrace(err)
      end
    end
  end

  # Virtual columns for folder and datacenter
  def v_owning_folder
    o = owning_folder
    o ? o.name : ""
  end

  def v_owning_datacenter
    o = owning_datacenter
    o ? o.name : ""
  end

  def miq_scsi_luns
    luns = []
    return luns if hardware.nil?

    hardware.storage_adapters.each do |sa|
      sa.miq_scsi_targets.each do |st|
        luns.concat(st.miq_scsi_luns)
      end
    end
    luns
  end

  def enabled_inbound_ports
    get_ports("in")
  end

  def enabled_outbound_ports
    get_ports("out")
  end

  def enabled_tcp_inbound_ports
    get_ports("in", "tcp")
  end

  def enabled_tcp_outbound_ports
    get_ports("out", "tcp")
  end

  def enabled_udp_inbound_ports
    get_ports("in", "udp")
  end

  def enabled_udp_outbound_ports
    get_ports("out", "udp")
  end

  def all_enabled_ports
    get_ports
  end

  def get_ports(direction = nil, host_protocol = nil)
    return [] if operating_system.nil?

    conditions = {:enabled => true}
    conditions[:direction] = direction if direction
    conditions[:host_protocol] = host_protocol if host_protocol

    operating_system.firewall_rules.where(conditions)
      .flat_map { |rule| rule.port_range.to_a }
      .uniq.sort
  end

  def service_names
    system_services.collect(&:name).uniq.sort
  end

  def enabled_run_level_0_services
    get_service_names(0)
  end

  def enabled_run_level_1_services
    get_service_names(2)
  end

  def enabled_run_level_2_services
    get_service_names(2)
  end

  def enabled_run_level_3_services
    get_service_names(3)
  end

  def enabled_run_level_4_services
    get_service_names(4)
  end

  def enabled_run_level_5_services
    get_service_names(5)
  end

  def enabled_run_level_6_services
    get_service_names(6)
  end

  def get_service_names(*args)
    if args.length == 0
      services = host_services
    elsif args.length == 1
      services = host_services.where("enable_run_levels LIKE ?", "%#{args.first}%")
    end
    services.order(:name).uniq.pluck(:name)
  end

  def event_where_clause(assoc = :ems_events)
    case assoc.to_sym
    when :ems_events, :event_streams
      ["host_id = ? OR dest_host_id = ?", id, id]
    when :policy_events
      ["host_id = ?", id]
    end
  end

  def has_vm_scan_affinity?
    with_relationship_type("vm_scan_affinity") { parent_count > 0 }
  end

  def vm_scan_affinity=(list)
    list = [list].flatten
    with_relationship_type("vm_scan_affinity") do
      remove_all_parents
      list.each { |parent| set_parent(parent) }
    end
    true
  end
  alias_method :set_vm_scan_affinity, :vm_scan_affinity=

  def vm_scan_affinity
    with_relationship_type("vm_scan_affinity") { parents }
  end
  alias_method :get_vm_scan_affinity, :vm_scan_affinity

  def processes
    operating_system.try(:processes) || []
  end

  def event_logs
    operating_system.try(:event_logs) || []
  end

  def get_reserve(field)
    default_resource_pool.try(:send, field)
  end

  def cpu_reserve
    get_reserve(:cpu_reserve)
  end

  def memory_reserve
    get_reserve(:memory_reserve)
  end

  def total_vm_cpu_reserve
    vms.inject(0) { |t, vm| t + (vm.cpu_reserve || 0) }
  end

  def total_vm_memory_reserve
    vms.inject(0) { |t, vm| t + (vm.memory_reserve || 0) }
  end

  def vcpus_per_core
    cores = total_vcpus
    return 0 if cores == 0

    total_vm_vcpus = vms.inject(0) { |t, vm| t + (vm.num_cpu || 0) }
    (total_vm_vcpus / cores)
  end

  def domain
    names = hostname.to_s.split(',').first.to_s.split('.')
    return names[1..-1].join('.') if names.present?

    nil
  end

  #
  # Metric methods
  #

  PERF_ROLLUP_CHILDREN = [:vms]

  def perf_rollup_parents(interval_name = nil)
    if interval_name == 'realtime'
      [ems_cluster].compact if ems_cluster
    else
      [ems_cluster || ext_management_system].compact
    end
  end

  def get_performance_metric(capture_interval, metric, range, function = nil)
    # => capture_interval = 'realtime' | 'hourly' | 'daily'
    # => metric = perf column name (real or virtual)
    # => function = :avg | :min | :max
    # => range = [start_time, end_time] | start_time | number in seconds to go back

    time_range = if range.kind_of?(Array)
                   range
                 elsif range.kind_of?(Time)
                   [range.utc, Time.now.utc]
                 elsif range.kind_of?(String)
                   [range.to_time(:utc), Time.now.utc]
                 elsif range.kind_of?(Integer)
                   [range.seconds.ago.utc, Time.now.utc]
                 else
                   raise "Range #{range} is invalid"
                 end

    klass = case capture_interval.to_s
            when 'realtime' then HostMetric
            else HostPerformance
            end

    perfs = klass.where(
      [
        "resource_id = ? AND capture_interval_name = ? AND timestamp >= ? AND timestamp <= ?",
        id,
        capture_interval.to_s,
        time_range[0],
        time_range[1]
      ]
    ).order("timestamp")

    if capture_interval.to_sym == :realtime && metric.to_s.starts_with?("v_pct_cpu_")
      vm_vals_by_ts = get_pct_cpu_metric_from_child_vm_performances(metric, capture_interval, time_range)
      values = perfs.collect { |p| vm_vals_by_ts[p.timestamp] || 0 }
    else
      values = perfs.collect(&metric.to_sym)
    end

    # => returns value | [array of values] (if function.nil?)
    return values if function.nil?

    case function.to_sym
    when :min, :max then values.send(function)
    when :avg
      return 0 if values.length == 0

      (values.compact.sum / values.length)
    else
      raise _("Function %{function} is invalid, should be one of :min, :max, :avg or nil") % {:function => function}
    end
  end

  def get_pct_cpu_metric_from_child_vm_performances(metric, capture_interval, time_range)
    klass = case capture_interval.to_s
            when 'realtime' then VmMetric
            else VmPerformance
            end

    vm_perfs = klass.where(
      "parent_host_id = ? AND capture_interval_name = ? AND timestamp >= ? AND timestamp <= ?",
      id,
      capture_interval.to_s,
      time_range[0],
      time_range[1])

    perf_hash = {}
    vm_perfs.each do |p|
      perf_hash[p.timestamp] ||= []
      perf_hash[p.timestamp] << p.send(metric)
    end

    perf_hash.each_key do |ts|
      tot = perf_hash[ts].compact.sum
      perf_hash[ts] = perf_hash[ts].empty? ? 0 : (tot / perf_hash[ts].length.to_f)
    end
    perf_hash
  end

  # Display or hide certain charts
  def cpu_mhz_available?
    true
  end

  def cpu_ready_available?
    true
  end

  def cpu_percent_available?
    false
  end

  def writable_storages
    if host_storages.loaded? && host_storages.all? { |hs| hs.association(:storage).loaded? }
      host_storages.reject(&:read_only).map(&:storage)
    else
      storages.where(:host_storages => {:read_only => [false, nil]})
    end
  end

  def read_only_storages
    if host_storages.loaded? && host_storages.all? { |hs| hs.association(:storage).loaded? }
      host_storages.select(&:read_only).map(&:storage)
    else
      storages.where(:host_storages => {:read_only => true})
    end
  end

  def archived
    has_attribute?("archived") ? self["archived"] : ems_id.nil?
  end
  alias archived? archived

  def normalized_state
    return 'archived' if archived?
    return power_state if power_state.present?

    "unknown"
  end

  def self.display_name(number = 1)
    n_('Host', 'Hosts', number)
  end

  def verbose_supports?(feature, description = nil)
    supports?(feature).tap do |value|
      unless value
        description ||= feature.to_s.humanize(:capitalize => false)
        _log.warn("Cannot #{description} because <#{unsupported_reason(feature)}>")
      end
    end
  end
end