ManageIQ/manageiq

View on GitHub
app/models/vm_or_template.rb

Summary

Maintainability
B
4 hrs
Test Coverage
C
76%
require 'ancestry'
require 'ostruct'
require 'cgi'
require 'uri'

class VmOrTemplate < ApplicationRecord
  include NewWithTypeStiMixin
  include RetirementMixin
  include ScanningMixin
  include SupportsFeatureMixin
  include SupportsAttribute
  include EmsRefreshMixin

  self.table_name = 'vms'
  has_ancestry

  include Operations
  include RetirementManagement
  include RightSizing
  include Scanning
  include Snapshotting

  attr_accessor :surrogate_host

  @surrogate_host = nil

  include ProviderObjectMixin
  include ComplianceMixin
  include OwnershipMixin
  include CustomAttributeMixin

  include EventMixin
  include ProcessTasksMixin
  include TenancyMixin
  include ManageIQ::Providers::Inflector::Methods

  VENDOR_TYPES = {
    # DB            Displayed
    "azure"           => "Azure",
    "azure_stack"     => "AzureStack",
    "vmware"          => "VMware",
    "microsoft"       => "Microsoft",
    "xen"             => "XenSource",
    "parallels"       => "Parallels",
    "amazon"          => "Amazon",
    "redhat"          => "Red Hat",
    "ovirt"           => "oVirt",
    "openstack"       => "OpenStack",
    "openshift_infra" => "OpenShift Virtualization",
    "oracle"          => "Oracle",
    "google"          => "Google",
    "kubevirt"        => "KubeVirt",
    "ibm_cloud"       => "IBM Cloud",
    "ibm_power_vs"    => "IBM Power Systems Virtual Server",
    "ibm_power_vc"    => "IBM PowerVC",
    "ibm_power_hmc"   => "IBM Power HMC",
    "ibm_z_vm"        => "IBM Z/VM",
    "unknown"         => "Unknown"
  }

  POWER_OPS = %w[start stop suspend reset shutdown_guest standby_guest reboot_guest]
  REMOTE_REGION_TASKS = POWER_OPS + %w[retire_now]

  validates_presence_of     :name, :location
  validates                 :vendor, :inclusion => {:in => VENDOR_TYPES.keys}

  has_one                   :operating_system, :dependent => :destroy
  has_one                   :openscap_result, :as => :resource, :dependent => :destroy
  has_one                   :hardware, :dependent => :destroy
  has_one                   :miq_provision, :dependent => :nullify, :as => :destination
  has_one                   :miq_provision_template, :through => "miq_provision", :source => "source", :source_type => "VmOrTemplate"
  has_one                   :miq_server, :foreign_key => :vm_id, :inverse_of => :vm

  belongs_to                :host
  belongs_to                :ems_cluster
  belongs_to                :cloud_tenant
  belongs_to                :flavor

  belongs_to                :placement_group

  belongs_to                :storage
  belongs_to                :storage_profile
  belongs_to                :ext_management_system, :foreign_key => "ems_id", :inverse_of => :vms_and_templates
  belongs_to                :resource_group
  belongs_to                :tenant

  # Accounts - Users and Groups
  has_many                  :accounts, :dependent => :destroy
  has_many                  :users, -> { where(:accttype => 'user') }, :class_name => "Account"
  has_many                  :groups, -> { where(:accttype => 'group') }, :class_name => "Account"
  has_many                  :disks, :through => :hardware
  has_many                  :networks, :through => :hardware
  has_many                  :nics, :through => :hardware
  has_many                  :miq_provisions_from_template, :class_name => "MiqProvision", :as => :source, :dependent => :nullify
  has_many                  :miq_provision_vms, :through => :miq_provisions_from_template, :source => :destination, :source_type => "VmOrTemplate"
  has_many                  :miq_provision_requests, :as => :source
  has_many                  :guest_applications, :dependent => :destroy
  has_many                  :patches, :dependent => :destroy
  # System Services - Win32_Services, Kernel drivers, Filesystem drivers
  has_many                  :system_services, :dependent => :destroy
  has_many                  :win32_services, -> { where("typename = 'win32_service'") }, :class_name => "SystemService"
  has_many                  :kernel_drivers, -> { where("typename = 'kernel' OR typename = 'misc'") }, :class_name => "SystemService"
  has_many                  :filesystem_drivers, -> { where("typename = 'filesystem'") },  :class_name => "SystemService"
  has_many                  :linux_initprocesses, -> { where("typename = 'linux_initprocess' OR typename = 'linux_systemd'") }, :class_name => "SystemService"

  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"

  has_many                  :scan_histories,    :dependent => :destroy
  has_many                  :lifecycle_events,  :class_name => "LifecycleEvent"
  has_many                  :advanced_settings, :as => :resource, :dependent => :destroy

  # Scan Items
  has_many                  :registry_items, :dependent => :destroy

  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                  :storage_files, :dependent => :destroy
  has_many                  :storage_files_files, -> { where("rsc_type = 'file'") }, :class_name => "StorageFile"

  # EMS Events
  has_many                  :ems_events, ->(vmt) { unscope(:where => :vm_or_template_id).where(["vm_or_template_id = ? OR dest_vm_or_template_id = ?", vmt.id, vmt.id]).order(:timestamp) },
                            :class_name => "EmsEvent", :inverse_of => :vm_or_template

  has_many                  :ems_events_src,  :class_name => "EmsEvent"
  has_many                  :ems_events_dest, :class_name => "EmsEvent", :foreign_key => :dest_vm_or_template_id

  has_many                  :policy_events, ->(vm) { where(["target_id = ? AND target_class = 'VmOrTemplate'", vm.id]).order(:timestamp) }, :foreign_key => "target_id"

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

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

  has_many                  :service_resources, :as => :resource
  has_many                  :direct_services, :through => :service_resources, :source => :service
  has_many                  :connected_shares, -> { where(:resource_type => "VmOrTemplate") }, :foreign_key => :resource_id, :class_name => "Share"
  has_many                  :labels, -> { where(:section => "labels") }, # rubocop:disable Rails/HasManyOrHasOneDependent
                            :class_name => "CustomAttribute",
                            :as         => :resource,
                            :inverse_of => :resource
  has_many                  :ems_custom_attributes, -> { where(:source => 'VC') }, # rubocop:disable Rails/HasManyOrHasOneDependent
                            :class_name => "CustomAttribute",
                            :as         => :resource,
                            :inverse_of => :resource
  has_many                  :counterparts, :as => :counterpart, :class_name => "ConfiguredSystem", :dependent => :nullify

  has_and_belongs_to_many   :storages, :join_table => 'storages_vms_and_templates'

  acts_as_miq_taggable

  virtual_column :is_evm_appliance,                     :type => :boolean,    :uses => :miq_server
  virtual_column :os_image_name,                        :type => :string,     :uses => [:operating_system, :hardware]
  virtual_column :platform,                             :type => :string,     :uses => [:operating_system, :hardware]
  virtual_column :product_name,                         :type => :string,     :uses => [:operating_system]
  virtual_column :vendor_display,                       :type => :string
  virtual_column :v_owning_cluster,                     :type => :string,     :uses => :ems_cluster
  virtual_column :v_owning_resource_pool,               :type => :string,     :uses => :all_relationships
  virtual_column :v_owning_datacenter,                  :type => :string,     :uses => {:ems_cluster => :all_relationships}
  virtual_column :v_owning_folder,                      :type => :string,     :uses => {:ems_cluster => :all_relationships}
  virtual_column :v_owning_folder_path,                 :type => :string,     :uses => {:ems_cluster => :all_relationships}
  virtual_column :v_owning_blue_folder,                 :type => :string,     :uses => :all_relationships
  virtual_column :v_owning_blue_folder_path,            :type => :string,     :uses => :all_relationships
  virtual_column :v_datastore_path,                     :type => :string,     :uses => :storage
  virtual_column :v_parent_blue_folder_display_path,    :type => :string,     :uses => :all_relationships
  virtual_column :thin_provisioned,                     :type => :boolean,    :uses => {:hardware => :disks}
  virtual_column :used_storage,                         :type => :integer,    :uses => [:used_disk_storage, :mem_cpu]
  virtual_column :used_storage_by_state,                :type => :integer,    :uses => :used_storage
  virtual_column :uncommitted_storage,                  :type => :integer,    :uses => [:provisioned_storage, :used_storage_by_state]
  virtual_column :ipaddresses,                          :type => :string_set, :uses => {:hardware => :ipaddresses}
  virtual_column :hostnames,                            :type => :string_set, :uses => {:hardware => :hostnames}
  virtual_column :mac_addresses,                        :type => :string_set, :uses => {:hardware => :mac_addresses}
  virtual_column :memory_exceeds_current_host_headroom, :type => :string,     :uses => [:mem_cpu, {:host => [:hardware, :ext_management_system]}]
  virtual_column :has_rdm_disk,                         :type => :boolean,    :uses => {:hardware => :disks}
  virtual_column :disks_aligned,                        :type => :string,     :uses => {:hardware => {:hard_disks => :partitions_aligned}}

  virtual_has_many   :processes,              :class_name => "OsProcess",    :uses => {:operating_system => :processes}
  virtual_has_many   :event_logs,                                            :uses => {:operating_system => :event_logs}
  virtual_has_many   :lans,                                                  :uses => {:hardware => {:nics => :lan}}
  virtual_has_many   :child_resources,        :class_name => "VmOrTemplate"

  virtual_belongs_to :parent_resource_pool,   :class_name => "ResourcePool", :uses => :all_relationships

  virtual_has_one   :direct_service,       :class_name => 'Service'
  virtual_has_one   :service,              :class_name => 'Service'
  virtual_has_one   :parent_resource,      :class_name => "VmOrTemplate"

  virtual_delegate :name, :to => :host, :prefix => true, :allow_nil => true, :type => :string
  virtual_delegate :name, :to => :storage, :prefix => true, :allow_nil => true, :type => :string
  virtual_delegate :name, :to => :ems_cluster, :prefix => true, :allow_nil => true, :type => :string
  virtual_delegate :vmm_product, :to => :host, :prefix => :v_host, :allow_nil => true, :type => :string
  virtual_delegate :v_pct_free_disk_space, :v_pct_used_disk_space, :to => :hardware, :allow_nil => true, :type => :float
  virtual_delegate :num_cpu, :to => "hardware.cpu_sockets", :allow_nil => true, :default => 0, :type => :integer
  virtual_delegate :cpu_total_cores, :cpu_cores_per_socket, :to => :hardware, :allow_nil => true, :default => 0, :type => :integer
  virtual_delegate :annotation, :to => :hardware, :prefix => "v", :allow_nil => true, :type => :string
  virtual_delegate :ram_size_in_bytes,                  :to => :hardware, :allow_nil => true, :default => 0, :type => :integer
  virtual_delegate :mem_cpu,                            :to => "hardware.memory_mb", :allow_nil => true, :default => 0, :type => :integer
  virtual_delegate :ram_size,                           :to => "hardware.memory_mb", :allow_nil => true, :default => 0, :type => :integer

  delegate :connect_lans, :disconnect_lans, :to => :hardware, :allow_nil => true
  delegate :queue_name_for_ems_operations, :to => :ext_management_system, :allow_nil => true

  supports_attribute :feature => :reconfigure_disks
  supports_attribute :feature => :reconfigure_disksize
  supports_attribute :feature => :reconfigure_cdroms
  supports_attribute :feature => :reconfigure_network_adapters

  after_save :save_genealogy_information

  scope :active,       ->       { where.not(:ems_id => nil) }
  scope :with_type,    ->(type) { where(:type => type) }
  scope :archived,     ->       { where(:ems_id => nil, :storage_id => nil) }
  scope :orphaned,     ->       { where(:ems_id => nil).where.not(:storage_id => nil) }
  scope :retired,      ->       { where(:retired => true) }
  scope :not_active,   ->       { where(:ems_id => nil) }
  scope :not_archived, ->       { where.not(:ems_id => nil).or(where.not(:storage_id => nil)) }
  scope :not_orphaned, ->       { where.not(:ems_id => nil).or(where(:storage_id => nil)) }
  scope :not_retired,  ->       { where(:retired => false).or(where(:retired => nil)) }

  scope :from_cloud_managers, -> { where(:ext_management_system => ManageIQ::Providers::CloudManager.all) }
  scope :from_infra_managers, -> { where(:ext_management_system => ManageIQ::Providers::InfraManager.all) }

  def from_cloud_manager?
    ext_management_system&.kind_of?(ManageIQ::Providers::CloudManager)
  end

  def from_infra_manager?
    ext_management_system&.kind_of?(ManageIQ::Providers::InfraManager)
  end

  # The SQL form of `#registered?`, with its inverse as well.
  # TODO: Vmware Specific (copied (old) TODO from #registered?)
  scope :registered, (lambda do
    where(arel_table[:template].eq(false).or(arel_table[:ems_id].not_eq(nil)).and(arel_table[:host_id].not_eq(nil)))
  end)
  scope :unregistered, (lambda do
    where(arel_table[:template].eq(true).and(arel_table[:ems_id].eq(nil)).or(arel_table[:host_id].eq(nil)))
  end)

  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

  # Add virtual columns/methods for specific things derived from advanced_settings
  REQUIRED_ADVANCED_SETTINGS = {
    'vmi.present'         => [:paravirtualization,   :boolean],
    'vmsafe.enable'       => [:vmsafe_enable,        :boolean],
    'vmsafe.agentAddress' => [:vmsafe_agent_address, :string],
    'vmsafe.agentPort'    => [:vmsafe_agent_port,    :integer],
    'vmsafe.failOpen'     => [:vmsafe_fail_open,     :boolean],
    'vmsafe.immutableVM'  => [:vmsafe_immutable_vm,  :boolean],
    'vmsafe.timeoutMS'    => [:vmsafe_timeout_ms,    :integer],
    'entitled_processors' => [:entitled_processors,  :float],
    'processor_type'      => [:processor_share_type, :string],
    'pin_policy'          => [:processor_pin_policy, :string],
    'software_licenses'   => [:software_licenses,    :string],
  }
  REQUIRED_ADVANCED_SETTINGS.each do |k, (m, t)|
    define_method(m) do
      as = advanced_settings.detect { |setting| setting.name == k }
      return nil if as.nil? || as.value.nil?

      case t
             when :boolean then ActiveRecord::Type::Boolean.new.cast(as.value)
             when :integer then as.value.to_i
             when :float then as.value.to_f
             else as.value.to_s
             end
    end

    virtual_column m, :type => t, :uses => :advanced_settings
  end

  # Add virtual columns/methods for details about each disk
  (1..9).each do |i|
    disk_methods = [
      ['disk_type',                   :string],
      ['mode',                        :string],
      ['size',                        :integer],
      ['size_on_disk',                :integer],
      ['used_percent_of_provisioned', :float],
      ['partitions_aligned',          :string]
    ]

    disk_methods.each do |k, t|
      m  = "disk_#{i}_#{k}".to_sym

      define_method(m) do
        return nil if hardware.nil?
        return nil if hardware.hard_disks.length < i

        hardware.hard_disks[i - 1].send(k)
      end

      virtual_column m, :type => t, :uses => {:hardware => :hard_disks}
    end
  end

  # Add virtual columns/methods for accessing individual folders in a path
  (1..9).each do |i|
    m = "parent_blue_folder_#{i}_name".to_sym

    define_method(m) do
      f = parent_blue_folders(:exclude_root_folder => true, :exclude_non_display_folders => true)[i - 1]
      f.nil? ? "" : f.name
    end

    virtual_column m, :type => :string, :uses => :all_relationships
  end

  include RelationshipMixin
  self.default_relationship_type = "genealogy"
  self.skip_relationships += ["genealogy"]

  include MiqPolicyMixin
  include AlertMixin
  include DriftStateMixin
  include UuidMixin
  include Metric::CiMixin

  include FilterableMixin
  include StorageMixin

  def self.manager_class
    if module_parent == Object
      ExtManagementSystem
    else
      module_parent
    end
  end

  def self.model_suffix
    manager_class.short_token
  end

  def to_s
    name
  end

  def is_evm_appliance?
    !!miq_server
  end
  alias_method :is_evm_appliance,  :is_evm_appliance?

  # Determines if the VM is on an EMS or Host
  def registered?
    # TODO: Vmware specific
    return false if template? && ems_id.nil?

    host_id.present?
  end

  # TODO: Vmware specific, and is this even being used anywhere?
  def connected_to_ems?
    connection_state == 'connected' || connection_state.nil?
  end

  def terminated?
    current_state == 'terminated'
  end

  def makesmart(_options = {})
    self.smart = true
    save
  end

  def run_command_via_parent(verb, options = {})
    unless ext_management_system
      raise _("VM/Template <%{name}> with Id: <%{id}> is not associated with a provider.") % {:name => name, :id => id}
    end
    unless ext_management_system.authentication_status_ok?
      raise _("VM/Template <%{name}> with Id: <%{id}>: Provider authentication failed.") % {:name => name, :id => id}
    end

    # TODO: Need to break this logic out into a method that can look at the verb and the vm and decide the best way to invoke it - Virtual Center WS, ESX WS, Storage Proxy.
    _log.info("Invoking [#{verb}] through EMS: [#{ext_management_system.name}]")
    options = {:user_event => "Console Request Action [#{verb}], VM [#{name}]"}.merge(options)
    ext_management_system.send(verb, self, options)
  end

  def run_command_via_task(task_options, queue_options)
    MiqTask.generic_action_with_callback(task_options, command_queue_options(queue_options))
  end

  def run_command_via_queue(method_name, queue_options = {})
    queue_options[:method_name] = method_name
    MiqQueue.put(command_queue_options(queue_options))
  end

  def make_retire_request(requester_id)
    self.class.make_retire_request(id, User.find(requester_id))
  end

  # keep the same method signature as others in retirement mixin
  def self.make_retire_request(*src_ids, requester, initiated_by: 'user')
    vms = where(:id => src_ids)

    missing_ids = src_ids - vms.pluck(:id)
    _log.error("Retirement of [Vm] IDs: [#{missing_ids.join(', ')}] skipped - target(s) does not exist") if missing_ids.present?

    vms.each do |target|
      target.check_policy_prevent('request_vm_retire', "retire_request_after_policy_check", requester.userid, :initiated_by => initiated_by)
    end
  end

  def retire_request_after_policy_check(userid, initiated_by: 'user')
    options = {:src_ids => [id], :__initiated_by__ => initiated_by, :__request_type__ => VmRetireRequest.request_types.first}
    requester = User.find_by(:userid => userid)
    self.class.set_retirement_requester(options[:src_ids], requester)
    VmRetireRequest.make_request(nil, options, requester)
  end

  # policy_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 event is not prevented to proceed by policy
  def check_policy_prevent(policy_event, *cb_method)
    enforce_policy(policy_event, {}, {:miq_callback => prevent_callback_settings(*cb_method)}) unless policy_event.nil?
  end

  def enforce_policy(event, inputs = {}, options = {})
    return {"result" => true, :details => []} if event.to_s == "rsop" && host.nil?
    raise _("vm does not belong to any host") if host.nil? && ext_management_system.nil?

    inputs[:vm]                    = self
    inputs[:host]                  = host                  unless host.nil?
    inputs[:ext_management_system] = ext_management_system unless ext_management_system.nil?
    MiqEvent.raise_evm_event(self, event, inputs, options)
  end

  # override
  def self.validate_task(task, vm, options)
    return false unless super
    return false if options[:task] == "destroy" || options[:task] == "check_compliance_queue"
    return false if vm.has_required_host?

    # VM has no host or storage affiliation
    if vm.storage.nil?
      task.error("#{vm.name}: There is no owning Host or Datastore for this VM, " \
                 "'#{options[:task]}' is not allowed")
      return false
    end

    # VM belongs to a storage/repository location
    # TODO: The following never gets run since the invoke tasks invokes it as a job, and only tasks get to this point ?
    unless %w[scan sync].include?(options[:task])
      task.error("#{vm.name}: There is no owning Host for this VM, '#{options[:task]}' is not allowed")
      return false
    end
    spid = ::Settings.repository_scanning.defaultsmartproxy
    if spid.nil?                          # No repo scanning SmartProxy configured
      task.error("#{vm.name}: No Default Repository SmartProxy is configured, contact your EVM administrator")
      return false
    elsif MiqProxy.exists?(spid) == false
      task.error("#{vm.name}: The Default Repository SmartProxy no longer exists, contact your EVM Administrator")
      return false
    end
    if MiqProxy.find(spid).state != "on"                     # Repo scanning host iagent s not running
      task.error("#{vm.name}: The Default Repository SmartProxy, '#{sp.name}', is not running. " \
                 "'#{options[:task]}' not attempted")
      return false
    end
    true
  end
  private_class_method :validate_task

  # override
  def self.task_invoked_by(options)
    %w[scan sync].include?(options[:task]) ? :job : super
  end
  private_class_method :task_invoked_by

  # override
  def self.task_arguments(options)
    case options[:task]
    when "scan", "sync"
      [options[:userid]]
    when "remove_snapshot", "revert_to_snapshot"
      [options[:snap_selected]]
    when "create_snapshot"
      [options[:name], options[:description], options[:memory]]
    else
      super
    end
  end
  private_class_method :task_arguments

  def powerops_callback(task_id, status, msg, result, _queue_item)
    task = MiqTask.find_by(:id => task_id)
    task.queue_callback("Finished", status, msg, result) if task
  end

  # override
  def self.invoke_task_local(task, vm, options, args)
    user = User.current_user
    cb = nil
    if task
      cb =
        if POWER_OPS.include?(options[:task])
          {
            :class_name  => vm.class.base_class.name,
            :instance_id => vm.id,
            :method_name => :powerops_callback,
            :args        => [task.id]
          }
        else
          {
            :class_name  => task.class.to_s,
            :instance_id => task.id,
            :method_name => :queue_callback,
            :args        => ["Finished"]
          }
        end
    end

    q_hash =
      if options[:task] == "destroy"
        {
          :class_name   => base_class.name,
          :instance_id  => vm.id,
          :method_name  => options[:task],
          :args         => args,
          :miq_task_id  => task&.id,
          :miq_callback => cb,
        }
      else
        {
          :service      => options[:invoke_by] == :job ? "smartstate" : "ems_operations",
          :affinity     => vm.ext_management_system,
          :class_name   => base_class.name,
          :instance_id  => vm.id,
          :method_name  => options[:task],
          :args         => args,
          :miq_task_id  => task&.id,
          :miq_callback => cb,
        }
      end
    q_hash.merge!(:user_id => user.id, :group_id => user.current_group.id, :tenant_id => user.current_tenant.id) if user
    MiqQueue.submit_job(q_hash)
  end

  def self.action_for_task(task)
    case task
    when "retire_now"
      "retire"
    else
      task
    end
  end

  def scan_data_current?
    !(last_scan_on.nil? || last_scan_on > last_sync_on)
  end

  def genealogy_parent
    with_relationship_type("genealogy") { parent }
  end

  def genealogy_parent=(parent)
    with_relationship_type('genealogy') do
      if use_ancestry?
        self.parent = parent
      else
        @genealogy_parent_object = parent
      end
    end
  end

  # save_genealogy_information is only necessary for relationships using genealogy
  # when using ancestry, the relationship will be saved after the fact
  # when not using ancestry, the relationship is saved on assignment, necessitating the prior save of the vm/template record
  # this variable is used to delay that assignment
  def save_genealogy_information
    if defined?(@genealogy_parent_object) && @genealogy_parent_object
      with_relationship_type('genealogy') { self.parent = @genealogy_parent_object }
    end
  end

  def os_image_name
    name = OperatingSystem.image_name(self)
    if name == 'unknown'
      parent = genealogy_parent
      name = OperatingSystem.image_name(parent) unless parent.nil?
    end
    name
  end

  def platform
    name = OperatingSystem.platform(self)
    if name == 'unknown'
      parent = genealogy_parent
      name = OperatingSystem.platform(parent) unless parent.nil?
    end
    name
  end

  def product_name
    name   = try(:operating_system).try(:product_name)
    name ||= genealogy_parent.try(:operating_system).try(:product_name)
    name ||= ""
    name
  end

  def service_pack
    name   = try(:operating_system).try(:service_pack)
    name ||= genealogy_parent.try(:operating_system).try(:service_pack)
    name ||= ""
    name
  end

  def vendor_display
    VENDOR_TYPES[vendor]
  end

  #
  # Path/location methods
  #

  # TODO: Vmware specific URI methods?  Next 3 methods
  def self.location2uri(location, scheme = "file")
    pat = %r{^(file|http|miq)://([^/]*)/(.+)$}
    unless pat&.match?(location)
      # location = scheme<<"://"<<self.myhost.ipaddress<<":1139/"<<location
      location = scheme << ":///" << location
    end
    location
  end

  def save_scan_history(datahash)
    result = scan_histories.build(
      :status      => datahash['status'],
      :status_code => datahash['status_code'].to_i,
      :message     => datahash['message'],
      :started_on  => Time.parse(datahash['start_time']),
      :finished_on => Time.parse(datahash['end_time']),
      :task_id     => datahash['taskid']
    )
    self.last_scan_on = Time.parse(datahash['start_time'])
    save
    result
  end

  def self.repository_parse_path(path)
    path.tr!("\\", "/")
    # it's empty string for local type
    storage_name = ""
    # NAS
    relative_path = if path.starts_with?("//")
                      raise _("path, '%{path}', is malformed") % {:path => path} unless %r{^//[^/].*/.+$}.match?(path)

                      # path is a UNC
                      storage_name = path.split("/")[0..3].join("/")
                      path.split("/")[4..path.length].join("/") if path.length > 4
                    # VMFS
                    elsif path.starts_with?("[")
                      raise _("path, '%{path}', is malformed") % {:path => path} unless /^\[[^\]].+\].*$/.match?(path)

                      # path is a VMWare storage name
                      /^\[(.*)\](.*)$/ =~ path
                      storage_name = $1
                      temp_path = $2.strip
                      # Some esx servers add a leading "/".
                      # This needs to be stripped off to allow matching on location
                      temp_path.sub(/^\//, '')
                    # local
                    else
                      raise _("path, '%{path}', is malformed") % {:path => path}
                    end
    return storage_name, (relative_path.empty? ? "/" : relative_path)
  end
  #
  # Relationship methods
  #

  def disconnect_inv
    disconnect_storage
    disconnect_ems

    classify_with_parent_folder_path(false)

    with_relationship_type('ems_metadata') do
      remove_all_parents(:of_type => ['EmsFolder', 'ResourcePool'])
    end

    disconnect_host
    disconnect_stack if respond_to?(:orchestration_stack)
  end

  def disconnect_stack(stack = nil)
    return unless orchestration_stack
    return if stack && stack != orchestration_stack

    log_text = " from stack [#{orchestration_stack.name}] id [#{orchestration_stack.id}]"
    _log.info("Disconnecting Vm [#{name}] id [#{id}]#{log_text}")

    self.orchestration_stack = nil
    save
  end

  def connect_ems(e)
    unless ext_management_system == e
      _log.debug("Connecting Vm [#{name}] id [#{id}] to EMS [#{e.name}] id [#{e.id}]")
      self.ext_management_system = e
      save
    end
  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 Vm [#{name}] id [#{id}]#{log_text}")

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

  def connect_host(h)
    unless host == h
      _log.debug("Connecting Vm [#{name}] id [#{id}] to Host [#{h.name}] id [#{h.id}]")
      self.host = h
      save

      # Also connect any nics to their lans
      connect_lans(h.lans)
    end
  end

  def disconnect_host(h = nil)
    if h.nil? || host == h
      log_text = " from Host [#{host.name}] id [#{host.id}]" unless host.nil?
      _log.info("Disconnecting Vm [#{name}] id [#{id}]#{log_text}")

      self.host = nil
      save

      # Also disconnect any nics from their lans
      disconnect_lans
    end
  end

  def connect_storage(s)
    unless storage == s
      _log.debug("Connecting Vm [#{name}] id [#{id}] to Datastore [#{s.name}] id [#{s.id}]")
      self.storage = s
      save
    end
  end

  def disconnect_storage(s = nil)
    if s.nil? || storage == s || storages.include?(s)
      stores = s.nil? ? ([storage] + storages).compact.uniq : [s]
      log_text = stores.collect { |x| "Datastore [#{x.name}] id [#{x.id}]" }.join(", ")
      _log.info("Disconnecting Vm [#{name}] id [#{id}] from #{log_text}")

      if s.nil?
        self.storage = nil
        self.storages = []
      else
        self.storage = nil if storage == s
        storages.delete(s)
      end

      save
    end
  end

  # Parent rp, folder and dc methods
  # TODO: Replace all with ancestors lookup once multiple parents is sorted out
  def parent_resource_pool
    with_relationship_type('ems_metadata') do
      parent(:of_type => "ResourcePool")
    end
  end
  alias_method :owning_resource_pool, :parent_resource_pool

  def parent_blue_folder
    with_relationship_type('ems_metadata') do
      parent(:of_type => "EmsFolder")
    end
  end
  alias_method :owning_blue_folder, :parent_blue_folder

  def parent_blue_folders(*args)
    f = parent_blue_folder
    f.nil? ? [] : f.folder_path_objs(*args)
  end

  def under_blue_folder?(folder)
    return false unless folder.kind_of?(EmsFolder)

    parent_blue_folders.any? { |f| f == folder }
  end

  def parent_blue_folder_path(*args)
    f = parent_blue_folder
    f.nil? ? "" : f.folder_path(*args)
  end
  alias_method :owning_blue_folder_path, :parent_blue_folder_path

  def parent_folder
    ems_cluster.try(:parent_folder)
  end
  alias_method :owning_folder, :parent_folder
  alias_method :parent_yellow_folder, :parent_folder

  def parent_folders(*args)
    f = parent_folder
    f.nil? ? [] : f.folder_path_objs(*args)
  end
  alias_method :parent_yellow_folders, :parent_folders

  def parent_folder_path(*args)
    f = parent_folder
    f.nil? ? "" : f.folder_path(*args)
  end
  alias_method :owning_folder_path, :parent_folder_path
  alias_method :parent_yellow_folder_path, :parent_folder_path

  def parent_datacenter
    ems_cluster.try(:parent_datacenter)
  end
  alias_method :owning_datacenter, :parent_datacenter

  def parent_blue_folder_display_path
    parent_blue_folder_path(:exclude_non_display_folders => true)
  end
  alias_method :v_parent_blue_folder_display_path, :parent_blue_folder_display_path

  def lans
    !hardware.nil? ? hardware.nics.collect(&:lan).compact : []
  end

  # Create a hash of this Vm's EMS and Host and their credentials
  def ems_host_list
    params = {}
    [ext_management_system, "ems", host, "host"].each_slice(2) do |ems, type|
      if ems
        params[type] = {
          :hostname   => ems.hostname,
          :ipaddress  => ems.ipaddress,
          :username   => ems.authentication_userid,
          :password   => ems.authentication_password_encrypted,
          :class_name => ems.class.name
        }
        params[type][:port] = ems.port if ems.respond_to?(:port) && ems.port.present?
      end
    end
    params
  end

  def reconnect_events
    events = EmsEvent.where("ems_id = ? AND ((vm_ems_ref = ? AND vm_or_template_id IS NULL) OR (dest_vm_ems_ref = ? AND dest_vm_or_template_id IS NULL))", ext_management_system.id, ems_ref, ems_ref)
    events.each do |e|
      do_save = false

      src_vm = e.src_vm_or_template
      if src_vm.nil? && e.vm_ems_ref == ems_ref
        src_vm = self
        e.vm_or_template_id = src_vm.id
        e.vm_name = src_vm.name
        do_save = true
      end

      dest_vm = e.dest_vm_or_template
      if dest_vm.nil? && e.dest_vm_ems_ref == ems_ref
        dest_vm = self
        e.dest_vm_or_template_id = dest_vm.id
        do_save = true
      end

      e.save if do_save

      # Hook up genealogy after a Clone Task
      src_vm.add_genealogy_child(dest_vm) if src_vm && dest_vm && e.event_type == EmsEvent::CLONE_TASK_COMPLETE
    end

    true
  end

  def add_genealogy_child(child)
    with_relationship_type('genealogy') do
      set_child(child)
    end
  end

  def myhost
    return @surrogate_host if @surrogate_host
    return host unless host.nil?

    self.class.proxy_host_for_repository_scans
  end

  def self.scan_via_ems?
    !::Settings.coresident_miqproxy.scan_via_host
  end

  delegate :scan_via_ems?, :to => :class

  # Cache the proxy host for repository scans because the JobProxyDispatch calls this for each Vm scan job in a loop
  cache_with_timeout(:proxy_host_for_repository_scans, 30.seconds) do
    defaultsmartproxy = ::Settings.repository_scanning.defaultsmartproxy

    proxy = nil
    proxy = MiqProxy.find_by(:id => defaultsmartproxy.to_i) if defaultsmartproxy
    proxy.try(:host)
  end

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

  def my_zone_obj
    Zone.find_by(:name => my_zone)
  end

  #
  # Proxy methods
  #

  # TODO: Come back to this
  def proxies4job(_job = nil)
    _log.debug("Enter")

    all_proxy_list = storage2proxies
    proxies = storage2active_proxies(all_proxy_list)
    _log.debug("# proxies = #{proxies.length}")

    msg = if all_proxy_list.empty?
            "No active SmartProxies found to analyze this VM"
          elsif proxies.empty?
            "Provide credentials for this VM's Host to perform SmartState Analysis"
          else
            'Perform SmartState Analysis on this VM'
          end

    log_all_proxies(all_proxy_list, msg) if proxies.empty?
    {:proxies => proxies.flatten, :message => msg}
  end

  def log_all_proxies(all_proxy_list, message)
    proxies = all_proxy_list.collect { |a| "[#{log_proxies_format_instance(a)}]" }
    proxies_text = proxies.empty? ? "[none]" : proxies.join(" -- ")
    _log.warn("Proxies for #{log_proxies_vm_config} : #{proxies_text}")
    _log.warn("Proxies message: #{message}")
  end

  def log_proxies_vm_config
    msg = "[#{log_proxies_format_instance(self)}] on host [#{log_proxies_format_instance(host)}] datastore "
    msg << (storage ? "[#{storage.name}-#{storage.store_type}]" : "No storage")
  end

  def log_proxies_format_instance(object)
    return 'Nil' if object.nil?

    "#{object.class.name}:#{object.id}-#{object.name}:#{object.try(:state)}"
  end

  def storage2proxies
    @storage_proxies ||= begin
      # Support vixDisk scanning of VMware VMs from the vmdb server
      miq_server_proxies
    end
  end

  def storage2active_proxies(all_proxy_list = nil)
    all_proxy_list ||= storage2proxies
    _log.debug("all_proxy_list.length = #{all_proxy_list.length}")
    proxies = all_proxy_list.select(&:is_proxy_active?)
    _log.debug("proxies1.length = #{proxies.length}")

    # MiqServer coresident proxy needs to contact the host and provide credentials.
    # Remove any MiqServer instances if we do not have credentials
    rsc = scan_via_ems? ? ext_management_system : host
    proxies.delete_if { |p| p.is_a?(MiqServer) } if rsc && !rsc.authentication_status_ok?
    _log.debug("proxies2.length = #{proxies.length}")

    proxies
  end

  def has_active_proxy?
    storage2active_proxies.any?
  end

  def has_proxy?
    storage2proxies.any?
  end

  # Cache the servers because the JobProxyDispatch calls this for each Vm scan job in a loop
  cache_with_timeout(:miq_servers_for_scan, 30.seconds) do
    MiqServer.where(:status => "started").includes([:zone, :server_roles]).to_a
  end

  def miq_server_proxies
    case vendor
    when 'vmware'
      # VM cannot be scanned by server if they are on a repository
      return [] if storage_id.blank? || repository_vm?
    when 'microsoft'
      return [] if storage_id.blank?
    else
      _log.debug("else")
      return []
    end

    host_server_ids = host ? host.vm_scan_affinity.collect(&:id) : []
    _log.debug("host_server_ids.length = #{host_server_ids.length}")

    storage_server_ids = storages.collect { |s| s.vm_scan_affinity.collect(&:id) }.reject(&:blank?)
    _log.debug("storage_server_ids.length = #{storage_server_ids.length}")

    all_storage_server_ids = storage_server_ids.inject(:&) || []
    _log.debug("all_storage_server_ids.length = #{all_storage_server_ids.length}")

    srs = self.class.miq_servers_for_scan
    _log.debug("srs.length = #{srs.length}")

    miq_servers = srs.select do |svr|
      (svr.vm_scan_host_affinity? ? host_server_ids.detect { |id| id == svr.id } : host_server_ids.empty?) &&
      (svr.vm_scan_storage_affinity? ? all_storage_server_ids.detect { |id| id == svr.id } : storage_server_ids.empty?)
    end
    _log.debug("miq_servers1.length = #{miq_servers.length}")

    miq_servers.select! do |svr|
      result = svr.status == "started" && svr.has_zone?(my_zone)
      result &&= svr.is_vix_disk? if vendor == 'vmware'
      result
    end
    _log.debug("miq_servers2.length = #{miq_servers.length}")
    miq_servers
  end

  def active_proxy_error_message
    proxies4job[:message]
  end

  # TODO: Vmware specific
  def repository_vm?
    host.nil?
  end

  # TODO: Vmware specfic
  def template=(val)
    return val unless val ^ template # Only continue if toggling setting

    write_attribute(:template, val)

    self.type = corresponding_model.name if (template? && kind_of?(Vm)) || (!template? && kind_of?(MiqTemplate))
    d = template? ? [/\.vmx$/, ".vmtx", 'never'] : [/\.vmtx$/, ".vmx", state == 'never' ? 'unknown' : raw_power_state]
    self.location = location.sub(d[0], d[1]) unless location.nil?
    self.raw_power_state = d[2]
  end

  # TODO: Vmware specfic
  def runnable?
    host_id.present? && current_state != "never"
  end

  def self.post_refresh_ems(ems_id, update_start_time)
    update_start_time = update_start_time.utc
    ems = ExtManagementSystem.find(ems_id)

    # Collect the newly added VMs
    added_vms = ems.vms_and_templates.where("created_on >= ?", update_start_time)

    # Create queue items to do additional process like apply tags and link events
    unless added_vms.empty?
      added_vm_ids = []
      added_vms.find_each do |v|
        v.post_create_actions_queue
        added_vm_ids << v.id
      end
    end

    post_refresh_ems_folder_updates(ems, update_start_time, added_vms)
  end

  def self.post_refresh_ems_folder_updates(ems, update_start_time, added_vms)
    # Collect the updated folder relationships to determine which vms need updated path information
    ems_folders = ems.ems_folders
    MiqPreloader.preload(ems_folders, :all_relationships)

    # Find any VMs that were created or moved into a new folder
    updated_vm_rels = ems_folders.collect do |f|
      f.relationships.collect do |r|
        r.children.select do |child_r|
          child_r.resource_type == "VmOrTemplate" &&
            (child_r.created_at >= update_start_time || child_r.updated_at >= update_start_time)
        end
      end
    end.flatten

    # Now find any Folders that were renamed or moved into a new parent folder
    updated_folders = ems_folders.select do |f|
      f.created_on >= update_start_time || f.updated_on >= update_start_time ||  # Has the folder itself changed (e.g. renamed)?
        f.relationships.any? do |r|
          r.created_at >= update_start_time || r.updated_at >= update_start_time # Has the relationship changed (e.g. this folder moved under another folder)?
        end
    end

    updated_vms  = VmOrTemplate.where(:id => updated_vm_rels.collect(&:resource_id))
    updated_vms += updated_folders.flat_map(&:all_vms_and_templates)
    updated_vms  = updated_vms.uniq - added_vms
    updated_vms.each(&:classify_with_parent_folder_path_queue)
  end
  private_class_method :post_refresh_ems_folder_updates

  def post_create_actions_queue
    MiqQueue.submit_job(
      :class_name  => self.class.name,
      :instance_id => id,
      :method_name => 'post_create_actions'
    )
  end

  def post_create_actions
    reconnect_events
    classify_with_parent_folder_path
    raise_created_event
  end

  def raise_created_event
    raise NotImplementedError, _("raise_created_event must be implemented in a subclass")
  end

  # TODO: Vmware specific
  # Determines the full path from the Storage and location
  def path
    # If Storage id is blank return the location stored for the vm after removing the uri data
    # Otherwise build the path from the storage data and vm location.
    return location if storage_id.blank?
    # Return location if it contains a fully-qualified file URI
    return location if location.starts_with?('file://')
    # Return location for RHEV-M VMs
    return rhevm_config_path if vendor.to_s.downcase == 'redhat'

    if storage.store_type == "NAS"
      File.join(storage.name, location)
    elsif storage.storage_type_supported_for_ssa?
      "[#{storage.name}] #{location}"
    else
      _log.warn("VM [#{name}] storage type [#{storage.store_type}] not supported")
      @path = location
    end
  end

  def rhevm_config_path
    # /rhev/data-center/<datacenter_id>/mastersd/master/vms/<vm_guid>/<vm_guid>.ovf/
    datacenter = parent_datacenter
    return location if datacenter.blank?

    File.join('/rhev/data-center', datacenter.uid_ems, 'mastersd/master/vms', uid_ems, location)
  end

  def state
    (power_state || "unknown").downcase
  end
  alias_method :current_state, :state

  # Override raw_power_state= attribute setter in order to impose side effects
  # of setting previous_state and updating state_changed_on
  def raw_power_state=(new_state)
    return unless new_state

    unless raw_power_state == new_state
      self.previous_state   = raw_power_state
      self.state_changed_on = Time.now.utc
      super
      self.power_state = calculate_power_state
    end
    new_state
  end

  def self.calculate_power_state(raw_power_state)
    (raw_power_state == "never") ? "never" : "unknown"
  end

  def archived?
    ems_id.nil? && storage_id.nil?
  end
  alias_method :archived, :archived?
  virtual_attribute :archived, :boolean, :arel => (lambda do |t|
    t.grouping(t[:ems_id].eq(nil).and(t[:storage_id].eq(nil)))
  end)

  def orphaned?
    ems_id.nil? && !storage_id.nil?
  end
  alias_method :orphaned, :orphaned?
  virtual_attribute :orphaned, :boolean, :arel => (lambda do |t|
    t.grouping(t[:ems_id].eq(nil).and(t[:storage_id].not_eq(nil)))
  end)

  def active?
    !archived? && !orphaned? && !retired? && !template?
  end
  alias_method :active, :active?
  # in sql nil != false ==> false
  virtual_attribute :active, :boolean, :arel => (lambda do |t|
    t.grouping(t[:ems_id].not_eq(nil)
     .and(t[:retired].eq(nil).or(t[:retired].eq(t.create_false)))
     .and(t[:template].eq(nil).or(t[:template].eq(t.create_false))))
  end)

  def disconnected?
    return self["disconnected"] if has_attribute?("disconnected")

    !connected_to_ems?
  end
  virtual_attribute :disconnected, :boolean, :arel => (lambda do |t|
    t.grouping(t[:connection_state].not_eq(nil).and(t[:connection_state].not_eq("connected")))
  end)
  alias_method :disconnected, :disconnected?

  def normalized_state
    return self["normalized_state"] if has_attribute?("normalized_state")

    %w[archived orphaned template retired disconnected].each do |s|
      return s if send(:"#{s}?")
    end
    return power_state.downcase unless power_state.nil?

    "unknown"
  end
  virtual_attribute :normalized_state, :string, :arel => (lambda do |t|
    t.grouping(
      Arel::Nodes::Case.new
      .when(arel_table[:archived]).then(Arel.sql("'archived'"))
      .when(arel_table[:orphaned]).then(Arel.sql("'orphaned'"))
      .when(t[:template].eq(t.create_true)).then(Arel.sql("'template'"))
      .when(t[:retired].eq(t.create_true)).then(Arel.sql("'retired'"))
      .when(arel_table[:disconnected]).then(Arel.sql("'disconnected'"))
      .else(t.lower(
              t.coalesce([t[:power_state], Arel.sql("'unknown'")])
      ))
    )
  end)

  def classify_with_parent_folder_path_queue(add = true)
    MiqQueue.submit_job(
      :class_name  => self.class.name,
      :instance_id => id,
      :method_name => 'classify_with_parent_folder_path',
      :args        => [add],
      :priority    => MiqQueue::MIN_PRIORITY
    )
  end

  def classify_with_parent_folder_path(add = true)
    [:blue, :yellow].each do |folder_type|
      path = send(:"parent_#{folder_type}_folder_path")
      next if path.blank?

      cat = self.class.folder_category(folder_type)
      ent = self.class.folder_entry(path, cat)

      _log.info("#{add ? "C" : "Unc"}lassifying VM: [#{name}] with Category: [#{cat.name} => #{cat.description}], Entry: [#{ent.name} => #{ent.description}]")
      ent.send(add ? :assign_entry_to : :remove_entry_from, self, false)
    end
  end

  def self.folder_category(folder_type)
    cat_name = "folder_path_#{folder_type}"
    cat = Classification.lookup_by_name(cat_name)
    unless cat
      cat = Classification.is_category.new(
        :name         => cat_name,
        :description  => "Parent Folder Path (#{folder_type == :blue ? "VMs & Templates" : "Hosts & Clusters"})",
        :single_value => true,
        :read_only    => true
      )
      cat.save(:validate => false)
    end
    cat
  end

  def self.folder_entry(ent_desc, cat)
    ent_name = ent_desc.downcase.tr(" ", "_").split("/").join(":")
    ent = cat.find_entry_by_name(ent_name)
    unless ent
      ent = cat.children.new(:name => ent_name, :description => ent_desc)
      ent.save(:validate => false)
    end
    ent
  end

  def event_where_clause(assoc = :ems_events)
    case assoc.to_sym
    when :ems_events, :event_streams
      ["vm_or_template_id = ? OR dest_vm_or_template_id = ? ", id, id]
    when :policy_events
      ["target_id = ? and target_class = ? ", id, self.class.base_class.name]
    end
  end

  # Virtual columns for owning resource pool, folder and datacenter
  def v_owning_cluster
    o = owning_cluster
    o ? o.name : ""
  end

  def v_owning_resource_pool
    o = owning_resource_pool
    o ? o.name : ""
  end

  def v_owning_folder
    o = owning_folder
    o ? o.name : ""
  end

  alias_method :v_owning_folder_path, :owning_folder_path

  def v_owning_blue_folder
    o = owning_blue_folder
    o ? o.name : ""
  end

  alias_method :v_owning_blue_folder_path, :owning_blue_folder_path

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

  def v_is_a_template
    template?.to_s.capitalize
  end
  # technically it is capitalized, but for sorting, not a concern
  # but we do need nil to become false
  virtual_attribute :v_is_a_template, :string, :arel => (lambda do |t|
    t.grouping(t.coalesce([t[:template], t.create_false]))
  end)

  def v_datastore_path
    datastorepath = location || ""
    storage ? "#{storage.name}/#{datastorepath}" : datastorepath
  end

  def event_threshold?(options = {:time_threshold => 30.minutes, :event_types => ["MigrateVM_Task_Complete"], :freq_threshold => 2})
    raise _("option :event_types is required")    unless options[:event_types]
    raise _("option :time_threshold is required") unless options[:time_threshold]
    raise _("option :freq_threshold is required") unless options[:freq_threshold]

    EmsEvent
      .where(:event_type => options[:event_types])
      .where("vm_or_template_id = :id OR dest_vm_or_template_id = :id", :id => id)
      .where("timestamp >= ?", options[:time_threshold].to_i.seconds.ago.utc)
      .count >= options[:freq_threshold].to_i
  end

  def reconfigured_hardware_value?(options)
    attr = options[:hdw_attr]
    raise _(":hdw_attr required") if attr.nil?

    operator = options[:operator] || ">"
    operator = operator.downcase == "increased" ? ">" : operator.downcase == "decreased" ? "<" : operator

    current_state, prev_state = drift_states.order("timestamp DESC").limit(2)
    if current_state.nil? || prev_state.nil?
      _log.info("Unable to evaluate, not enough state data available")
      return false
    end

    current_value  = current_state.data_obj.hardware.send(attr).to_i
    previous_value = prev_state.data_obj.hardware.send(attr).to_i
    result         = current_value.send(operator, previous_value)
    _log.info("Evaluate: (Current: #{current_value} #{operator} Previous: #{previous_value}) = #{result}")

    result
  end

  def changed_vm_value?(options)
    attr = options[:attr] || options[:hdw_attr]
    raise _(":attr required") if attr.nil?

    operator = options[:operator]

    data0, data1 = drift_states.order("timestamp DESC").limit(2)

    if data0.nil? || data1.nil?
      _log.info("Unable to evaluate, not enough state data available")
      return false
    end

    v0 = data0.data_obj.send(attr) || ""
    v1 = data1.data_obj.send(attr) || ""
    if operator.downcase == "changed"
      result = !(v0 == v1)
    else
      raise _("operator '%{operator}' is not supported") % {:operator => operator}
    end
    _log.info("Evaluate: !(#{v1} == #{v0}) = #{result}")

    result
  end

  #
  # Hardware Disks/Memory storage methods
  #

  virtual_delegate :allocated_disk_storage, :used_disk_storage,
                   :to => :hardware, :allow_nil => true, :uses => {:hardware => :disks}, :type => :integer

  virtual_delegate :provisioned_storage, :to => :hardware, :allow_nil => true, :default => 0, :type => :integer
  virtual_delegate :num_disks, :to => :hardware, :allow_nil => true, :default => 0, :type => :integer, :uses => {:hardware => :disks}
  virtual_delegate :num_hard_disks, :to => :hardware, :allow_nil => true, :default => 0, :type => :integer, :uses => {:hardware => :hard_disks}

  def used_storage
    used_disk_storage.to_i + ram_size_in_bytes
  end

  def used_storage_by_state
    used_disk_storage.to_i + ram_size_in_bytes_by_state
  end

  def uncommitted_storage
    provisioned_storage.to_i - used_storage_by_state.to_i
  end

  def thin_provisioned
    hardware.nil? ? false : hardware.disks.any? { |d| d.disk_type == 'thin' }
  end

  def ram_size_by_state
    state == 'on' ? ram_size : 0
  end

  def ram_size_in_bytes_by_state
    ram_size_by_state * 1.megabyte
  end

  def has_rdm_disk
    return false if hardware.nil?

    !hardware.disks.detect(&:rdm_disk?).nil?
  end

  def disks_aligned
    dlist = hardware ? hardware.hard_disks : []
    dlist = dlist.reject(&:rdm_disk?) # Skip RDM disks
    return "Unknown" if dlist.empty?
    return "True"    if dlist.all? { |d| d.partitions_aligned == "True" }
    return "False"   if dlist.any? { |d| d.partitions_aligned == "False" }

    "Unknown"
  end

  def memory_exceeds_current_host_headroom
    return false if host.nil?

    (ram_size > host.current_memory_headroom)
  end

  def collect_running_processes(_options = {})
    OsProcess.add_elements(self, running_processes)
    operating_system.save unless operating_system.nil?
  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 processes
    operating_system.nil? ? [] : operating_system.processes
  end

  def event_logs
    operating_system.nil? ? [] : operating_system.event_logs
  end

  def direct_service
    direct_services.first
  end

  def service
    direct_service.try(:root_service)
  end

  def has_required_host?
    !host.nil?
  end

  def has_active_ems?
    return true unless ext_management_system.nil?

    false
  end

  #
  # Metric methods
  #

  PERF_ROLLUP_CHILDREN = []

  def perf_rollup_parents(interval_name = nil)
    [host, service].compact unless interval_name == 'realtime'
  end

  # Called from integrate ws to kick off scan for vdi VMs
  def self.vms_by_ipaddress(ipaddress)
    ipaddresses = ipaddress.split(',')
    Network.where("ipaddress in (?)", ipaddresses).each do |network|
      begin
        vm = network.hardware.vm
        yield(vm)
      rescue
      end
    end
  end

  # This creates the following SQL conditional:
  #
  #   1 = (SELECT 1
  #        FROM hardwares
  #        JOIN networks ON networks.hardware_id = hardwares.id
  #        WHERE hardwares.vm_or_template_id = vms.id
  #          AND (networks.ipaddress LIKE "%IPADDRESS%"
  #               OR networks.ipv6address LIKE "%IPADDRESS%")
  #        LIMIT 1
  #       )
  #
  # This is simply an existance check, so when one record is found matching the
  # following conditions:
  #
  #   - It is a hardware record that is associated with the vm
  #   - It has an ipaddress or ipv6address that matches the search
  #
  # It will return the VM record.
  def self.miq_expression_includes_any_ipaddresses_arel(ipaddress)
    vms       = arel_table
    networks  = Network.arel_table
    hardwares = Hardware.arel_table

    match_grouping = networks[:ipaddress].matches("%#{ipaddress}%")
                       .or(networks[:ipv6address].matches("%#{ipaddress}%"))

    query = hardwares.project(1)
                     .join(networks).on(networks[:hardware_id].eq(hardwares[:id]))
                     .where(hardwares[:vm_or_template_id].eq(vms[:id]).and(match_grouping))
                     .take(1)
    Arel.sql("1").eq(query)
  end

  def self.scan_by_property(property, value, _options = {})
    _log.info("scan_vm_by_property called with property:[#{property}] value:[#{value}]")
    case property
    when "ipaddress"
      vms_by_ipaddress(value) do |vm|
        if vm.state == "on"
          _log.info("Initiating VM scan for [#{vm.id}:#{vm.name}]")
          vm.scan
        end
      end
    else
      raise _("Unsupported property type [%{property}]") % {:property => property}
    end
  end

  def self.event_by_property(property, value, event_type, event_message, event_time = nil, _options = {})
    _log.info("event_vm_by_property called with property:[#{property}] value:[#{value}] type:[#{event_type}] message:[#{event_message}] event_time:[#{event_time}]")
    event_timestamp = event_time.blank? ? Time.now.utc : event_time.to_time(:utc)

    case property
    when "ipaddress"
      vms_by_ipaddress(value) do |vm|
        vm.add_ems_event(event_type, event_message, event_timestamp)
      end
    when "uid_ems"
      vm = VmOrTemplate.find_by(:uid_ems => value)
      unless vm.nil?
        vm.add_ems_event(event_type, event_message, event_timestamp)
      end
    else
      raise _("Unsupported property type [%{property}]") % {:property => property}
    end
  end

  def add_ems_event(event_type, event_message, event_timestamp)
    event = {
      :event_type        => event_type,
      :is_task           => false,
      :source            => 'EVM',
      :message           => event_message,
      :timestamp         => event_timestamp,
      :vm_or_template_id => id,
      :vm_name           => name,
      :vm_location       => path,
    }
    event[:ems_id] = ems_id unless ems_id.nil?

    unless host_id.nil?
      event[:host_id]   = host_id
      event[:host_name] = host.name
    end

    EmsEvent.add(ems_id, event)
  end

  def console_supported?(_type)
    false
  end

  # Stop certain charts from showing unless the subclass allows
  def non_generic_charts_available?
    false
  end
  alias_method :cpu_ready_available?,    :non_generic_charts_available?
  alias_method :cpu_mhz_available?,      :non_generic_charts_available?
  alias_method :cpu_percent_available?,  :non_generic_charts_available?
  alias_method :memory_mb_available?,    :non_generic_charts_available?

  def self.includes_template?(ids)
    MiqTemplate.where(:id => ids).exists?
  end

  supports :destroy

  # Stop showing Reconfigure VM task unless the subclass allows
  def reconfigurable?
    false
  end

  def self.reconfigurable?(ids)
    vms = VmOrTemplate.where(:id => ids)
    return false if vms.blank?

    vms.all?(&:reconfigurable?)
  end

  PUBLIC_TEMPLATE_CLASSES = %w[ManageIQ::Providers::Openstack::CloudManager::Template].freeze

  def self.tenant_id_clause(user_or_group)
    template_tenant_ids = MiqTemplate.accessible_tenant_ids(user_or_group, Rbac.accessible_tenant_ids_strategy(MiqTemplate))
    vm_tenant_ids       = Vm.accessible_tenant_ids(user_or_group, Rbac.accessible_tenant_ids_strategy(Vm))
    return if template_tenant_ids.empty? && vm_tenant_ids.empty?

    tenant = user_or_group.current_tenant
    tenant_vms       = "vms.template = false AND vms.tenant_id IN (?)"
    public_templates = "vms.template = true AND vms.publicly_available = true AND vms.type IN (?)"
    tenant_templates = "vms.template = true AND vms.tenant_id IN (?)"

    if tenant.source_id
      private_tenant_templates = "vms.template = true AND vms.tenant_id = (?) AND vms.publicly_available = false"
      tenant_templates += " AND vms.type NOT IN (?)"
      ["#{private_tenant_templates} OR #{tenant_vms} OR #{tenant_templates} OR #{public_templates}", tenant.id, vm_tenant_ids, template_tenant_ids, PUBLIC_TEMPLATE_CLASSES, PUBLIC_TEMPLATE_CLASSES]
    else
      ["#{tenant_templates} OR #{public_templates} OR #{tenant_vms}", template_tenant_ids, PUBLIC_TEMPLATE_CLASSES, vm_tenant_ids]
    end
  end

  def self.with_ownership
    includes(:ext_management_system).where(:ext_management_systems => {:tenant_mapping_enabled => [false, nil]})
  end

  def tenant_identity
    user = evm_owner
    user = User.super_admin.tap { |u| u.current_group = miq_group } if user.nil? || !user.miq_group_ids.include?(miq_group_id)
    user
  end

  supports(:console) { N_("Console not supported") unless console_supported?('spice') || console_supported?('vnc') }

  def child_resources
    children
  end

  def parent_resource
    parent
  end

  def self.display_name(number = 1)
    n_('VM or Template', 'VMs or Templates', number)
  end

  private

  def power_state=(new_power_state)
    super
  end

  def calculate_power_state
    self.class.calculate_power_state(raw_power_state)
  end

  # deprecated, use unsupported_reason(:action) instead
  def check_feature_support(_message_prefix)
    reason = unsupported_reason(:action)
    [!reason, reason]
  end

  def create_notification(type, options)
    Notification.create!(
      :type    => type,
      :subject => self,
      :options => options
    )
  end

  def command_queue_options(queue_options)
    {
      :class_name  => self.class.name,
      :instance_id => id,
      :role        => "ems_operations",
      :queue_name  => queue_name_for_ems_operations,
      :zone        => my_zone,
    }.merge(queue_options)
  end
end