ManageIQ/manageiq

View on GitHub
app/models/miq_action.rb

Summary

Maintainability
D
2 days
Test Coverage
F
48%
require 'awesome_spawn'

class MiqAction < ApplicationRecord
  include UuidMixin
  before_validation :default_name_to_guid, :on => :create
  before_destroy    :check_policy_contents_empty_on_destroy
  before_save       :round_if_memory_reconfigured

  silence_warnings do
    const_set(:TYPES,
              "create_snapshot"         => N_("Create a Snapshot"),
              "email"                   => N_("Send an E-mail"),
              "snmp_trap"               => N_("Send an SNMP Trap"),
              "tag"                     => N_("Tag"),
              "reconfigure_memory"      => N_("Reconfigure Memory"),
              "reconfigure_cpus"        => N_("Reconfigure CPUs"),
              "custom_automation"       => N_("Invoke a Custom Automation"),
              "evaluate_alerts"         => N_("Evaluate Alerts"),
              "assign_scan_profile"     => N_("Assign Profile to Analysis Task"),
              "set_custom_attribute"    => N_("Set a Custom Attribute in vCenter"),
              "inherit_parent_tags"     => N_("Inherit Parent Tags"),
              "remove_tags"             => N_("Remove Tags"),
              "delete_snapshots_by_age" => N_("Delete Snapshots by Age"),
              "run_ansible_playbook"    => N_("Run Ansible Playbook")
             )
  end

  validates :action_type,        :presence => true
  validates :name, :description, :presence => true, :uniqueness_when_changed => true
  validates_format_of       :name, :with => /\A[a-z0-9_-]+\z/i,
    :allow_nil => true, :message => "must only contain alpha-numeric, underscore and hyphen chatacters without spaces"

  acts_as_miq_taggable
  acts_as_miq_set_member

  has_many :miq_policy_contents

  serialize :options, :type => Hash

  # Add a instance method to store the sequence and synchronous values from the policy contents
  attr_accessor :sequence, :synchronous, :reserved

  SCRIPT_DIR = Rails.root.join("product/conditions/scripts").expand_path
  SCRIPT_DIR.mkpath

  RE_SUBST = /\$\{([^}]+)\}/

  RC_HASH = {
    0  => "MIQ_OK",
    4  => "MIQ_WARN",
    8  => "MIQ_STOP",
    16 => "MIQ_ABORT"
  }

  SH_PREAMBLE = begin
    preamble = "#!/bin/sh\n"
    RC_HASH.each { |k, v| preamble += "#{v}=#{k}\n" }
    preamble
  end

  RB_PREAMBLE = "
    RC_HASH = #{RC_HASH.inspect}
    RC_HASH.each { |k, v| Object.const_set(v, k) }
  "

  virtual_column :v_synchronicity,         :type => :string
  virtual_column :action_type_description, :type => :string

  def v_synchronicity
    synchronous ? "Synchronous" : "Asynchronous"
  end

  def action_type_description
    TYPES[action_type] || description
  end

  def validate
    case action_type
    when "email"
      self.options ||= {}
      self.options[:to] ||= ""
      [:from, :to].each do |k|
        if self.options && self.options[k]
          next if k == :from && self.options[k].blank? # allow blank from addres, we use the default.

          match = self.options[k] =~ /^\A([\w.\-+]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z$/i
          errors.add(k, "must be a valid email address") unless match
        end
      end
    when "tag"
      errors.add("tag", "no tags provided") unless self.options && self.options[:tags]
    when "snapshot_create"
      errors.add("snapshot_create", "no snapshot name provided") unless self.options && self.options[:name]
    when "reconfigure_cpus"
      errors.add("reconfigure_cpus", "CPUs valie must be 1, 2 or 4") unless self.options && [1, 2, 4].include?(self.options[:value].to_i)
    when "reconfigure_memory"
      errors.add("reconfigure_cpus", "Memory value must be between 4 and 65,536") unless self.options && self.options[:value].to_i >= 4 && self.options[:value].to_i <= 65536
    end
  end

  def miq_policies
    miq_policy_contents.collect(&:miq_policy).uniq
  end

  def self.invoke_actions(apply_policies_to, inputs, succeeded, failed)
    deferred = []
    results = {}

    begin
      succeeded.each do |p|
        actions = case p
                  when MiqPolicy then p.actions_for_event(inputs[:event], :success).uniq
                  else            p.actions_for_event
                  end

        actions.each do |a|
          inputs = inputs.merge(:policy => p, :result => true, :sequence => a.sequence, :synchronous => a.synchronous)
          _log.debug("action: [#{a.name}], seq: [#{a.sequence}], sync: [#{a.synchronous}], inputs to action: seq: [#{inputs[:sequence]}], sync: [#{inputs[:synchronous]}]")

          if a.name == "prevent"
            deferred.push([a, apply_policies_to, inputs])
            next
          end

          name = a.action_type == "default" ? a.name.to_sym : a.action_type.to_sym
          results[name] ||= []
          results[name] << {:policy_id => p.kind_of?(MiqPolicy) ? p.id : nil, :policy_status => :success, :result => a.invoke(apply_policies_to, inputs)}
        end
      end

      failed.each do |p|
        next unless p.kind_of?(MiqPolicy) # built-in policies are OpenStructs whose actions will be invoked only on success

        actions = p.actions_for_event(inputs[:event], :failure).uniq

        actions.each do |a|
          # merge in the synchronous flag and possibly the sequence if not already sorted by this
          inputs = inputs.merge(:policy => p, :result => false, :sequence => a.sequence, :synchronous => a.synchronous)
          _log.debug("action: [#{a.name}], seq: [#{a.sequence}], sync: [#{a.synchronous}], inputs to action: seq: [#{inputs[:sequence]}], sync: [#{inputs[:synchronous]}]")

          if a.name == "prevent"
            deferred.push([a, apply_policies_to, inputs])
            next
          end

          name = a.action_type == "default" ? a.name.to_sym : a.action_type.to_sym
          results[name] ||= []
          results[name] << {:policy_id => p.kind_of?(MiqPolicy) ? p.id : nil, :policy_status => :failure, :result => a.invoke(apply_policies_to, inputs)}
        end
      end

      deferred.each do |arr|
        a, apply_policies_to, inputs = arr
        a.invoke(apply_policies_to, inputs)
      end
    rescue MiqException::StopAction => err
      MiqPolicy.logger.error("MIQ(action-invoke) Stopping action invocation [#{err.message}]")
      return
    rescue MiqException::UnknownActionRc => err
      MiqPolicy.logger.error("MIQ(action-invoke) Aborting action invocation [#{err.message}]")
      raise
    rescue MiqException::PolicyPreventAction => err
      MiqPolicy.logger.info("MIQ(action-invoke) [#{err}]")
      raise
    end

    results
  end

  def invoke(rec, inputs)
    atype = action_type
    atype = name if atype.nil? || atype == "default"
    method = "action_" + atype
    unless respond_to?(method)
      MiqPolicy.logger.info("MIQ(action-invoke) '#{name}', not supported")
      return
    end

    if inputs[:result]
      phrase = "for successful policy"
    else
      phrase = "for failed policy"
    end
    MiqPolicy.logger.info("MIQ(action-invoke) Invoking action [#{description}] #{phrase} [#{inputs[:policy].description}], event: [#{inputs[:event].description}], entity name: [#{rec.name}], entity type: [#{Dictionary.gettext(rec.class.to_s, :type => :model)}], sequence: [#{inputs[:sequence]}], synchronous? [#{inputs[:synchronous]}]")
    send(method.to_sym, self, rec, inputs)
  end

  def invoke_action_for_built_in_policy(rec, inputs)
    atype = action_type
    atype ||= name
    method = "action_" + atype
    unless respond_to?(method)
      MiqPolicy.logger.info("MIQ(action-invoke) '#{name}', not supported")
      return
    end

    MiqPolicy.logger.info("MIQ(action-invoke) Invoking action [#{description}] for built-in policy [#{inputs[:built_in_policy]}], event: [#{inputs[:event]}], entity name: [#{rec.name}], entity type: [#{Dictionary.gettext(rec.class.to_s, :type => :model)}]")
    send(method.to_sym, self, rec, inputs)
  end

  def action_prevent(_action, _rec, _inputs)
    raise MiqException::PolicyPreventAction, "preventing current process from proceeding due to policy failure"
  end

  def action_log(_action, rec, inputs)
    if inputs[:result]
      MiqPolicy.logger.info("MIQ(action-log): Policy success: policy: [#{inputs[:policy].description}], event: [#{inputs[:event].description}], entity name: [#{rec.name}], entity type: [#{Dictionary.gettext(rec.class.to_s, :type => :model)}]")
    else
      MiqPolicy.logger.warn("MIQ(action-log): Policy failure: policy: [#{inputs[:policy].description}], event: [#{inputs[:event].description}], entity name: [#{rec.name}], entity type: [#{Dictionary.gettext(rec.class.to_s, :type => :model)}]")
    end
  end

  def action_audit(_action, rec, inputs)
    msg = inputs[:result] ? "success" : "failure"
    AuditEvent.send(msg,
                    :event        => inputs[:event].name,
                    :target_id    => rec.id,
                    :target_class => rec.class.base_class.name,
                    :message      => "Policy #{msg}: policy: [#{inputs[:policy].description}], event: [#{inputs[:event].description}]")
  end

  def action_run_ansible_playbook(action, rec, inputs)
    service_template = ServiceTemplate.find(action.options[:service_template_id])
    dialog_options = {:hosts => target_hosts(action, rec)}
    request_options = {:manageiq_extra_vars => {'event_target' => rec.href_slug,
                                                  'event_name'   => inputs[:event].try(:name)},
                        :initiator           => 'control'}
    service_template.provision_request(target_user(rec), dialog_options, request_options)
  end

  def action_snmp_trap(action, rec, inputs)
    # Validate SNMP Version
    snmp_version = action.options[:snmp_version]
    snmp_version = 2 if action.options[:snmp_version] == "v2"
    snmp_version = 1 if action.options[:snmp_version] == "v1"
    snmp_version = 1 unless [1, 2].include?(snmp_version)
    method_name = "trap_v#{snmp_version}"

    snmp_inputs = {}
    snmp_inputs[:host] = action.options[:host]
    trap_id_key = (snmp_version == 1) ? :specific_trap : :trap_oid
    snmp_inputs[trap_id_key] = action.options[:trap_id]

    vars = []
    action.options[:variables].each do |h|
      value = h[:value]

      value = value.gsub(RE_SUBST) do |_s|
        # s  is ${anything_in_between}
        # $1 is   anything_in_between
        subst = ""
        what, method = $1.strip.split(".")

        what   = what.strip.downcase   unless what.nil?
        method = method.strip.downcase unless method.nil?
        # ${Cause.Description}
        if what == "cause"
          if method == "description"
            subst = "Policy: #{inputs[:policy].description}" if inputs[:policy].kind_of?(MiqPolicy)
            subst = "Alert: #{inputs[:policy].description}"  if inputs[:policy].kind_of?(MiqAlert)
          end
        end

        # ${Object.method}
        if what == "object"
          if method == "type"
            subst = rec.class.to_s
          elsif method == "ems" && rec.respond_to?(:ext_management_system)
            ems = rec.ext_management_system
            subst = "vCenter #{ems.hostname}/#{ems.ipaddress}" unless ems.nil?
          elsif rec.respond_to?(method)
            subst = rec.send(method)
          end
        end

        subst
      end unless value.nil?

      h[:value] = value
      vars << h
    end unless action.options[:variables].nil?

    snmp_inputs[:object_list] = vars

    invoke_or_queue(
      inputs[:synchronous], __method__, "notifier", nil, MiqSnmp, method_name, [snmp_inputs],
      "SNMP Trap [#{rec[:name]}]")
  end

  def action_email(action, rec, inputs)
    return unless MiqRegion.my_region.role_assigned?('notifier')

    action.options[:from] = ::Settings.smtp.from if action.options[:from].blank?

    email_options = {
      :to   => action.options[:to],
      :from => action.options[:from],
    }
    if inputs[:policy].kind_of?(MiqPolicy)
      presult = inputs[:result] ? "Succeeded" : "Failed"
      email_options[:subject] = "Policy #{presult}: #{inputs[:policy].description}, for (#{rec.class.to_s.upcase}) #{rec.name}"
      email_options[:miq_action_hash] = {
        :header            => inputs[:result] ? "Policy Succeeded" : "Policy Failed",
        :policy_detail     => "Policy '#{inputs[:policy].description}', #{presult}",
        :event_description => inputs[:event].description,
        :entity_type       => rec.class.to_s,
        :entity_name       => rec.name
      }
    elsif inputs[:policy].kind_of?(MiqAlert)
      email_options[:subject] = "Alert Triggered: #{inputs[:policy].description}, for (#{rec.class.to_s.upcase}) #{rec.name}"
      email_options[:miq_action_hash] = {
        :header            => "Alert Triggered",
        :policy_detail     => "Alert '#{inputs[:policy].description}', triggered",
        :event_description => inputs[:event].description,
        :event_details     => Notification.notification_text(inputs[:triggering_type], inputs[:triggering_data]),
        :entity_type       => rec.class.to_s,
        :entity_name       => rec.name
      }
    end

    invoke_or_queue(inputs[:synchronous], __method__, "notifier", nil, MiqAction, 'queue_email', [email_options])
  end

  def self.queue_email(options)
    GenericMailer.deliver_queue(:policy_action_email, options)
  rescue Exception => err
    MiqPolicy.logger.log_backtrace(err)
  end

  def action_evm_event(action, rec, inputs)
    default_options = {
      :event_type => "EVMAlertEvent",
      :message    => "#{inputs[:policy].kind_of?(MiqAlert) ? "Alert:" : "Policy:"} #{inputs[:policy].description}",
      :is_task    => false,
      :source     => inputs[:policy].class.name,
      :timestamp  => Time.now.utc
    }
    opts = default_options.merge(action.options || {})
    opts[:target] = rec

    opts[:ems_id] = rec.ext_management_system.id if rec.respond_to?(:ext_management_system) && rec.ext_management_system
    case rec
    when VmOrTemplate
      opts[:vm_or_template_id] = rec.id
      opts[:vm_name]           = rec.name
      opts[:vm_location]       = rec.location
      unless rec.host.nil?
        opts[:host_id]    = rec.host.id
        opts[:host_name]  = rec.host.name
      end
    when Host
      opts[:host_id]    = rec.id
      opts[:host_name]  = rec.name
    when EmsCluster
      opts[:ems_cluster_id]    = rec.id
      opts[:ems_cluster_name]  = rec.name
      opts[:ems_cluster_uid]   = rec.uid_ems
    end

    MiqEvent.create(opts)
  end

  def action_compliance_failed(action, rec, _inputs)
    # Nothing to do here. This action will get added to the :actions list in results and will be acted on by the Compliance.check_compliance methods
    MiqPolicy.logger.info("MIQ(action_compliance_failed): Now executing [#{action.description}] of #{rec.class.name} [#{rec.name}]")
  end

  def action_check_compliance(action, rec, inputs)
    unless rec.respond_to?(:check_compliance_queue)
      MiqPolicy.logger.error("MIQ(action_check_compliance): Unable to perform action [#{action.description}], object [#{rec.inspect}] does not support compliance checking")
      return
    end

    if inputs[:policy].mode == 'compliance'
      MiqPolicy.logger.warn("MIQ(action_check_compliance): Invoking action [#{action.description}] for event [#{inputs[:event].description}] would cause infinite loop, skipping")
      return
    end

    if inputs[:synchronous]
      MiqPolicy.logger.info("MIQ(action_check_compliance): Now executing [#{action.description}] of #{rec.class.name} [#{rec.name}]")
      rec.check_compliance
    else
      MiqPolicy.logger.info("MIQ(action_vm_migrate): Queueing [#{action.description}] of #{rec.class.name} [#{rec.name}]")
      rec.check_compliance_queue
    end
  end

  def action_tag(action, rec, _inputs)
    MiqPolicy.logger.info("MIQ(action_tag): Applying tags [#{action.options[:tags].inspect}] to [(#{rec.class}) #{rec.name}]")
    action.options[:tags].each { |t| Classification.classify_by_tag(rec, t) }
  end

  def action_tag_inherit(_action, rec, inputs)
    get_policies_from = inputs[:get_policies_from]
    MiqPolicy.logger.info("MIQ(action_tag_inherit): Applying tags from [(#{get_policies_from.class}) #{get_policies_from.name}] to [(#{rec.class}) #{rec.name}]")
    tags = get_policies_from.tag_list(:ns => "/managed").split
    tags.delete_if { |t| t =~ /^power_state/ } # omit power state since this is assigned by the system

    tags.each { |t| Classification.classify_by_tag(rec, File.join("/managed", t)) }
  end

  def action_inherit_parent_tags(_action, rec, _inputs)
    # options = {
    #   :parent_type => host | ems_cluster | storage
    #   :cats        => [array of categories]
    # }
    parent = rec.send(options[:parent_type]) if rec.respond_to?(options[:parent_type])
    if parent.nil?
      MiqPolicy.logger.warn("MIQ(action_inherit_parent_tags): [(#{rec.class}) #{rec.name}] does not have a parent of type [#{options[:parent_type]}], no action will be taken")
      return
    end

    options[:cats].each do |cat|
      MiqPolicy.logger.info("MIQ(action_inherit_parent_tags): Removing tags from category [(#{cat}) from [(#{rec.class}) #{rec.name}]")
      rec.tag_with("", :ns => "/managed/#{cat}")
    end
    rec.reload

    Classification.get_tags_from_object(parent).each do |t|
      cat, _ent = t.split("/")
      next unless options[:cats].include?(cat)

      MiqPolicy.logger.info("MIQ(action_inherit_parent_tags): Applying tag [#{t}] from [(#{parent.class}) #{parent.name}] to [(#{rec.class}) #{rec.name}]")
      Classification.classify_by_tag(rec, "/managed/#{t}", false)
    end
  end

  def action_remove_tags(_action, rec, _inputs)
    # options = {
    #   :cats        => [array of categories]
    # }
    Classification.get_tags_from_object(rec).each do |t|
      cat, _ent = t.split("/")
      next unless options[:cats].include?(cat)

      MiqPolicy.logger.info("MIQ(action_remove_tags): Removing tag [#{t}] from [(#{rec.class}) #{rec.name}]")
      Classification.unclassify_by_tag(rec, "/managed/#{t}", false)
    end
  end

  def self.inheritable_cats
    Classification.in_my_region.categories.inject([]) do |arr, c|
      next(arr) if c.name.starts_with?("folder_path_") || c.entries.empty?

      arr << c
    end
  end

  def run_script(rec)
    filename = self.options[:filename]
    raise _("unable to execute script, no file name specified") if filename.nil?

    unless File.exist?(filename)
      raise _("unable to execute script, file name [%{file_name}] does not exist") % {:file_name => filename}
    end

    command_result = nil
    ruby_file = File.extname(filename) == '.rb'

    Tempfile.open('miq_action', SCRIPT_DIR) do |fd|
      fd.puts ruby_file ? RB_PREAMBLE : SH_PREAMBLE
      fd.puts File.read(filename)
      fd.chmod(0o755)

      MiqPolicy.logger.info("MIQ(action_script): Executing: [#{filename}]")

      if ruby_file
        runner_cmd = MiqEnvironment::Command.runner_command
        MiqPolicy.logger.info("MIQ(action_script): Running: [#{runner_cmd} #{fd.path} '#{rec.name}'}]")
        command_result = AwesomeSpawn.run(runner_cmd, :params => [fd.path, rec.name])
      else
        MiqPolicy.logger.info("MIQ(action_script): Running: [#{fd.path}]")
        command_result = AwesomeSpawn.run(fname)
      end
    end

    rc = command_result.exit_status
    rc_verbose = RC_HASH[rc] || "Unknown RC: [#{rc}]"

    MiqPolicy.logger.info("MIQ(action_script): Result:\n#{result}")

    info_msg = "MIQ(action_script): Result: #{command_result.output}, rc: #{rc_verbose}"

    fail_msg = _("Action script exited with rc=%{rc_value}, error=%{error_text}") %
      {:rc_value => rc_verbose, :error_text => command_result.error}

    case rc
    when 0  # Success
      MiqPolicy.logger.info(info_msg)
    when 4  # Interrupted
      MiqPolicy.logger.warn(info_msg)
    when 8  # Exec format error
      raise MiqException::StopAction, fail_msg
    when 16 # Resource busy
      raise MiqException::AbortAction, fail_msg
    else
      raise MiqException::UnknownActionRc, fail_msg
    end
  end

  def action_script(action, rec, inputs)
    invoke_or_queue(inputs[:synchronous], __method__, nil, nil, action, 'run_script', [rec],
                    "Action Script [#{rec[:name]}]")
  end

  VM_ACTIONS_WITH_NO_ARGS = {
    "action_vm_start"                     => "start",
    "action_vm_stop"                      => "stop",
    "action_vm_suspend"                   => "suspend",
    "action_vm_shutdown_guest"            => "shutdown_guest",
    "action_vm_standby_guest"             => "standby_guest",
    "action_vm_unregister"                => "unregister",
    "action_vm_mark_as_template"          => "mark_as_template",
    "action_vm_analyze"                   => "scan",
    "action_vm_destroy"                   => "vm_destroy",
    "action_delete_all_snapshots"         => "remove_all_snapshots",
    "action_vm_connect_all"               => "connect_all_devices",
    "action_vm_connect_floppy"            => "connect_floppies",
    "action_vm_connect_cdrom"             => "connect_cdroms",
    "action_vm_disconnect_all"            => "disconnect_all_devices",
    "action_vm_disconnect_floppy"         => "disconnect_floppies",
    "action_vm_disconnect_cdrom"          => "disconnect_cdroms",
    "action_vm_collect_running_processes" => "collect_running_processes",
  }

  VM_ACTIONS_WITH_NO_ARGS.each do |action_method, vm_method|
    define_method(action_method) do |action, rec, inputs|
      unless rec.kind_of?(VmOrTemplate)
        MiqPolicy.logger.error("MIQ(#{action_method}): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
        return
      end

      invoke_or_queue(
        inputs[:synchronous], action_method, vm_method == "scan" ? "smartstate" : "ems_operations", rec.my_zone,
        rec, vm_method, [], "[#{action.description}] of VM [#{rec.name}]")
    end
  end

  def action_physical_server_power_on(action, rec, inputs)
    unless rec.kind_of?(PhysicalServer)
      MiqPolicy.logger.error("MIQ(physical_server_power_on): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a physical server")
      return
    end

    invoke_or_queue(inputs[:synchronous], __method__, "ems_operations", rec.my_zone, rec, 'power_on',
                    [], "[#{action.description}] of physical server [#{rec.name}]")
  end

  def action_vm_mark_as_vm(action, rec, inputs)
    unless rec.kind_of?(VmOrTemplate)
      MiqPolicy.logger.error("MIQ(action_vm_mark_as_vm): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
      return
    end

    invoke_or_queue(inputs[:synchronous], __method__, "ems_operations", rec.my_zone, rec, 'mark_as_vm',
                    [action.options[:pool], action.options[:host]], "[#{action.description}] of VM [#{rec.name}]")
  end

  def action_vm_migrate(action, rec, inputs)
    unless rec.kind_of?(VmOrTemplate)
      MiqPolicy.logger.error("MIQ(action_vm_migrate): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
      return
    end

    invoke_or_queue(inputs[:synchronous], __method__, "ems_operations", rec.my_zone, rec, 'migrate',
                    [action.options[:host], action.options[:pool], action.options[:priority], action.options[:state]],
                    "[#{action.description}] of VM [#{rec.name}]")
  end

  def action_vm_clone(action, rec, inputs)
    unless rec.kind_of?(VmOrTemplate)
      MiqPolicy.logger.error("MIQ(action_vm_clone): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
      return
    end

    invoke_or_queue(
      inputs[:synchronous], __method__, "ems_operations", rec.my_zone, rec, 'clone',
      [
        action.options[:name], action.options[:folder], action.options[:pool], action.options[:host],
        action.options[:datastore], action.options[:powerOn], action.options[:template], action.options[:transform],
        action.options[:config], action.options[:customization], action.options[:disk]
      ],
      "[#{action.description}] of VM [#{rec.name}]")
  end

  # Legacy: Replaces by action_vm_analyze
  def action_vm_scan(action, rec, inputs)
    action_vm_analyze(action, rec, inputs)
  end

  def action_vm_retire(action, rec, inputs)
    unless rec.kind_of?(VmOrTemplate)
      MiqPolicy.logger.error("MIQ(action_vm_retire): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
      return
    end

    target = inputs[:synchronous] ? VmOrTemplate : rec.class
    invoke_or_queue(
      inputs[:synchronous], __method__, "ems_operations", rec.my_zone, target, 'retire',
      [[rec], {:date => 1.day.ago}],
      "VM Retire for VM [#{rec.name}]")
  end

  def action_create_snapshot(action, rec, inputs)
    unless rec.kind_of?(VmOrTemplate)
      MiqPolicy.logger.error("MIQ(action_create_snapshot): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
      return
    end
    action.options[:description] ||= "Created by EVM Policy Action"

    invoke_or_queue(inputs[:synchronous], __method__, "ems_operations", rec.my_zone, rec, 'create_snapshot',
                    [action.options[:name], action.options[:description]],
                    "Create Snapshot [#{action.options[:name]}] for VM [#{rec.name}]")
  end

  def action_delete_snapshots_by_age(action, rec, _inputs)
    log_prefix = "MIQ(action_delete_snapshots_by_age):"
    unless rec.kind_of?(VmOrTemplate)
      MiqPolicy.logger.error("#{log_prefix} Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
      return
    end
    log_prefix += " VM: [#{rec.name}] Id: [#{rec.id}]"

    age_threshold = (Time.now.utc - action.options[:age])
    snaps_to_delete = rec.snapshots.each_with_object([]) do |s, arr|
      next if s.is_a_type?(:evm_snapshot)

      arr << s if s.create_time < age_threshold
    end

    if snaps_to_delete.empty?
      MiqPolicy.logger.info("#{log_prefix} has no snapshots older than [#{age_threshold}]")
      return
    end

    task_id = "action_#{action.id}_vm_#{rec.id}"
    snaps_to_delete.sort_by(&:create_time).reverse_each do |s| # Delete newest to oldest
      MiqPolicy.logger.info("#{log_prefix} Deleting Snapshot: Name: [#{s.name}] Id: [#{s.id}] Create Time: [#{s.create_time}]")
      rec.remove_snapshot_queue(s.id, task_id)
    end
  end

  def action_delete_most_recent_snapshot(action, rec, _inputs)
    log_prefix = "MIQ(action_delete_most_recent_snapshot):"
    unless rec.kind_of?(VmOrTemplate)
      MiqPolicy.logger.error("#{log_prefix} Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
      return
    end
    log_prefix += " VM: [#{rec.name}] Id: [#{rec.id}]"

    snap   = nil
    rec.snapshots.order("create_time DESC").each do |s|
      next if s.is_a_type?(:evm_snapshot)

      snap ||= s # Take the first eligable snapshot
    end

    if snap.nil?
      MiqPolicy.logger.info("#{log_prefix} has no snapshots available to delete")
      return
    end

    MiqPolicy.logger.info("#{log_prefix} Deleting Snapshot: Name: [#{snap.name}] Id: [#{snap.id}] Create Time: [#{snap.create_time}]")
    rec.remove_snapshot_queue(snap.id)
  end

  def action_reconfigure_memory(action, rec, inputs)
    unless rec.kind_of?(VmOrTemplate)
      MiqPolicy.logger.error("MIQ(action_reconfigure_memory): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
      return
    end

    unless action.options[:value]
      MiqPolicy.logger.error("MIQ(action_reconfigure_memory): Unable to perform action [#{action.description}], object [#{rec.inspect}] no memory value given")
      return
    end

    invoke_or_queue(inputs[:synchronous], __method__, "ems_operations", rec.my_zone, rec, 'set_memory',
                    [action.options[:value]],
                    "[#{action.description}] for VM [#{rec.name}], Memory value: [#{action.options[:value]}]")
  end

  def action_reconfigure_cpus(action, rec, inputs)
    unless rec.kind_of?(VmOrTemplate)
      MiqPolicy.logger.error("MIQ(action_reconfigure_cpus): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM")
      return
    end

    unless action.options[:value]
      MiqPolicy.logger.error("MIQ(action_reconfigure_cpus): Unable to perform action [#{action.description}], object [#{rec.inspect}] no cpu value given")
      return
    end

    invoke_or_queue(inputs[:synchronous], __method__, "ems_operations", rec.my_zone, rec, 'set_number_of_cpus',
                    [[action.options[:value]]],
                    "Reconfigure CPUs for VM [#{rec.name}], CPUs value: [#{action.options[:value]}]")
  end

  def action_ems_refresh(action, rec, inputs)
    unless rec.respond_to?(:ext_management_system) && !rec.ext_management_system.nil?
      MiqPolicy.logger.error("MIQ(action_ems_refresh): Unable to perform action [#{action.description}], object [#{rec.inspect}] does not have a Provider")
      return
    end

    if inputs[:synchronous]
      MiqPolicy.logger.info("MIQ(action_ems_refresh): Now executing EMS refresh of [#{rec.name}]")
      EmsRefresh.refresh(rec)
    else
      MiqPolicy.logger.info("MIQ(action_ems_refresh): Queueing EMS refresh of [#{rec.name}]")
      EmsRefresh.queue_refresh(rec)
    end
  end

  def action_container_image_analyze(action, rec, inputs)
    unless rec.kind_of?(ContainerImage)
      MiqPolicy.logger.error("MIQ(#{__method__}): Unable to perform action [#{action.description}]," \
                             " object [#{rec.inspect}] is not a Container Image")
      return
    end

    if inputs[:event].name == "request_containerimage_scan"
      MiqPolicy.logger.warn("MIQ(#{__method__}): Invoking action [#{action.description}] for event" \
                            " [#{inputs[:event].description}] would cause infinite loop, skipping")
      return
    end

    MiqPolicy.logger.info("MIQ(#{__method__}): Now executing [#{action.description}] of Container Image " \
                            "[#{rec.name}]")
    rec.scan
  end

  def action_container_image_annotate_scan_results(action, rec, inputs)
    MiqPolicy.logger.info("MIQ(#{__method__}): Now executing  [#{action.description}]")
    error_prefix = "MIQ(#{__method__}): Unable to perform action [#{action.description}], "
    unless rec.kind_of?(ContainerImage)
      MiqPolicy.logger.error("#{error_prefix} object [#{rec.inspect}] is not a Container Image")
      return
    end

    unless rec.respond_to?(:annotate_scan_policy_results)
      MiqPolicy.logger.error("#{error_prefix} ContainerImage is not linked with an OpenShift image")
      return
    end

    if inputs[:synchronous]
      MiqPolicy.logger.info("MIQ(#{__method__}): Now executing  [#{action.description}] for event " \
                            "[#{inputs[:event].description}]")
      rec.annotate_scan_policy_results(inputs[:policy].name, inputs[:result])
    else
      MiqPolicy.logger.info("MIQ(#{__method__}): Queueing [#{action.description}] for event " \
                            "[#{inputs[:event].description}]")
      MiqQueue.submit_job(
        :service     => "ems_operations",
        :affinity    => rec.ext_management_system,
        :class_name  => rec.class.name,
        :method_name => :annotate_scan_policy_results,
        :args        => [inputs[:policy].name, inputs[:result]],
        :instance_id => rec.id,
        :priority    => MiqQueue::HIGH_PRIORITY
      )
    end
  end

  def action_host_analyze(action, rec, inputs)
    action_method = "action_host_analyze"
    if inputs[:event].name == "request_host_scan"
      MiqPolicy.logger.warn("MIQ(#{action_method}): Invoking action [#{action.description}] for event [#{inputs[:event].description}] would cause infinite loop, skipping")
      return
    end

    unless rec.kind_of?(Host)
      MiqPolicy.logger.error("MIQ(#{action_method}): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a Host")
      return
    end

    if inputs[:synchronous]
      MiqPolicy.logger.info("MIQ(#{action_method}): Now executing [#{action.description}] of Host [#{rec.name}]")
      rec.scan
    else
      MiqPolicy.logger.info("MIQ(#{action_method}): Queueing [#{action.description}] of Host [#{rec.name}]")
      MiqQueue.submit_job(
        :service     => "smartstate",
        :affinity    => rec.ext_management_system,
        :class_name  => "Host",
        :method_name => "scan_from_queue",
        :instance_id => rec.id,
        :priority    => MiqQueue::HIGH_PRIORITY
      )
    end
  end

  def action_cancel_task(action, rec, inputs)
    unless rec.respond_to?(:ext_management_system) && !rec.ext_management_system.nil?
      MiqPolicy.logger.error("MIQ(action_cancel_task): Unable to perform action [#{action.description}], object [#{rec.inspect}] does not have a Provider")
      return
    end

    source_event = inputs[:source_event]
    task_mor = source_event.full_data.try(:fetch_path, 'info', 'task')
    unless task_mor
      MiqPolicy.logger.warn("MIQ(action_cancel_task): Event record does not have a task reference, no action will be taken")
      return
    end

    MiqPolicy.logger.info("MIQ(action_cancel_task): Now executing Cancel of task [#{source_event.event_type}] on VM [#{source_event.vm_name}]")
    ems = ExtManagementSystem.find_by(:id => source_event.ems_id)
    raise _("unable to find vCenter with id [%{id}]") % {:id => source_event.ems_id} if ems.nil?

    vim = ems.connect
    vim.cancelTask(task_mor)
  end

  def action_custom_automation(action, rec, inputs)
    ae_hash = action.options[:ae_hash] || {}
    automate_attrs = ae_hash.reject { |key, _value| MiqAeEngine::DEFAULT_ATTRIBUTES.include?(key) }
    automate_attrs[:request] = action.options[:ae_request]
    MiqAeEngine.set_automation_attributes_from_objects([inputs[:policy], inputs[:source_event]], automate_attrs)

    user = rec.tenant_identity
    unless user
      raise _("A user is needed to raise an action to automate. [%{name}] id:[%{id}] action: [%{description}]") %
              {:name => rec.class.name, :id => rec.id, :description => action.description}
    end

    args = {
      :object_type      => rec.class.base_class.name,
      :object_id        => rec.id,
      :user_id          => user.id,
      :miq_group_id     => user.current_group.id,
      :tenant_id        => user.current_tenant.id,
      :attrs            => automate_attrs,
      :instance_name    => "REQUEST",
      :automate_message => action.options[:ae_message] || "create",
    }

    invoke_or_queue(inputs[:synchronous], __method__, "automate", nil, MiqAeEngine, 'deliver', [args],
                    "MiqAeEngine.deliver for #{automate_attrs[:request]} with args=#{args.inspect}")
  end

  def action_raise_automation_event(_action, rec, inputs)
    event = inputs[:event].name
    aevent = {}
    aevent[:vm]     = inputs[:vm]
    aevent[:host]   = inputs[:host]
    aevent[:ems]    = inputs[:ext_management_systems]
    aevent[:policy] = inputs[:policy]

    case rec
    when VmOrTemplate
      aevent[:vm]    = rec
    when Host
      aevent[:host]  = rec
    when EmsCluster
      aevent[:ems]   = rec
    end

    invoke_or_queue(inputs[:synchronous], __method__, "automate", rec.my_zone, MiqAeEvent, 'raise_synthetic_event',
                    [rec, event, aevent], "Raise Automation Event, Event: [#{event}]")
  end

  def action_evaluate_alerts(action, rec, inputs)
    action.options[:alert_guids].each do |guid|
      alert = MiqAlert.find_by(:guid => guid)
      unless alert
        MiqPolicy.logger.warn("MIQ(action_evaluate_alert): Unable to perform action [#{action.description}], unable to find alert: [#{action.options[:alert_guid]}]")
        next
      end

      invoke_or_queue(inputs[:synchronous], __method__, nil, rec.my_zone, alert, 'evaluate', [rec, inputs],
                      "Evaluate Alert, Alert: [#{alert.description}]")
    end
  end

  def action_assign_scan_profile(action, _rec, _inputs)
    ScanItem  # Cause the ScanItemSet class to load, if not already loaded
    profile = ScanItemSet.find_by(:name => action.options[:scan_item_set_name])
    if profile
      MiqPolicy.logger.info("MIQ(action_assign_scan_profile): Action [#{action.description}], using analysis profile: [#{profile.description}]")
      ScanItem.get_profile(profile.name)
    else
      MiqPolicy.logger.warn("MIQ(action_assign_scan_profile): Unable to perform action [#{action.description}], unable to find analysis profile: [#{action.options[:scan_item_set_name]}]")
      nil
    end
  end

  def action_set_custom_attribute(action, rec, inputs)
    unless rec.kind_of?(VmOrTemplate) || rec.kind_of?(Host)
      MiqPolicy.logger.error("MIQ(action_set_custom_attribute): Unable to perform action [#{action.description}], object [#{rec.inspect}] is not a VM or a Host")
      return
    end

    invoke_or_queue(inputs[:synchronous], __method__, "ems_operations", rec.my_zone, rec, 'set_custom_field',
                    [action.options[:attribute], action.options[:value]],
                    "#{action.description} [#{action.options[:attribute]}] for VM [#{rec.name}]")
  end

  def export_to_array
    h = attributes
    ["id", "created_on", "updated_on"].each { |k| h.delete(k) }
    [self.class.to_s => h]
  end

  def self.import_from_hash(action, options = {})
    status = {:class => name, :description => action["description"]}
    a = MiqAction.find_by(:description => action["description"])
    msg_pfx = "Importing Action: description=[#{action["description"]}]"

    if a.nil?
      a = MiqAction.new(action)
      status[:status] = :add
    else
      a.attributes = action
      status[:status] = :update
    end

    unless a.valid?
      status[:status]   = :conflict
      status[:messages] = a.errors.full_messages
    end

    msg = "#{msg_pfx}, Status: #{status[:status]}"
    msg += ", Messages: #{status[:messages].join(",")}" if status[:messages]
    if options[:preview] == true
      MiqPolicy.logger.info("[PREVIEW] #{msg}")
    else
      MiqPolicy.logger.info(msg)
      a.save!
    end

    return a, status
  end

  def check_policy_contents_empty_on_destroy
    unless miq_policy_contents.empty?
      errors.add(:base, _("Action is referenced in at least one policy and cannot be deleted"))
      throw :abort
    end
  end

  def round_if_memory_reconfigured
    # round memory value to the nearest 4mb
    self.options[:value] = round_to_nearest_4mb(self.options[:value]) if action_type == "reconfigure_memory"
  end

  def round_to_nearest_4mb(num)
    num = num.to_i
    pad = (-num) % 4
    num + pad
  end

  def self.seed
    create_default_actions
    create_script_actions_from_directory
  end

  def self.create_default_actions
    CSV.foreach(fixture_path, :headers => true, :skip_lines => /^#/) do |csv_row|
      action_attributes = csv_row.to_hash
      action_attributes['action_type'] = 'default'

      create_or_update(action_attributes)
    end
  end

  def self.create_script_actions_from_directory
    Dir.glob(SCRIPT_DIR.join("*")).sort.each do |f|
      create_or_update(
        'name'        => File.basename(f).tr(".", "_"),
        'description' => "Execute script: #{File.basename(f)}",
        'action_type' => "script",
        'options'     => {:filename => f}
      )
    end
  end

  def self.create_or_update(action_attributes)
    name = action_attributes['name']
    action = find_by(:name => name)
    if action
      action.attributes = action_attributes
      if action.changed? || action.options_was != action.options
        _log.info("Updating [#{name}]")
        action.save
      end
    else
      _log.info("Creating [#{name}]")
      create(action_attributes)
    end
  end

  def self.allowed_for_policies(mode)
    mode == 'compliance' ? where.not(:name => 'check_compliance') : all
  end

  def self.fixture_path
    FIXTURE_DIR.join("#{to_s.pluralize.underscore}.csv")
  end

  def self.display_name(number = 1)
    n_('Action', 'Actions', number)
  end

  private

  def invoke_or_queue(
    synchronous,
    calling_method,
    role,
    zone,
    target,
    target_method,
    target_args = [],
    log_suffix = nil
  )
    log_prefix = "MIQ(#{calling_method}):"
    log_suffix ||= calling_method.to_s.titleize[7..-1] # remove 'Action '
    static = target.instance_of?(Class) || target.instance_of?(Module)

    if synchronous
      MiqPolicy.logger.info("#{log_prefix} Now executing #{log_suffix}")
      target.send(target_method, *target_args)
    else
      MiqPolicy.logger.info("#{log_prefix} Queueing #{log_suffix}")

      options = {
        :class_name  => static ? target.name : target.class.name,
        :method_name => target_method,
        :instance_id => static ? nil : target.id,
        :args        => target_args,
        :role        => role,
        :priority    => MiqQueue::HIGH_PRIORITY,
        :zone        => zone
      }

      if role == 'ems_operations'
        options[:queue_name] = target.try(:queue_name_for_ems_operations) || 'generic'
      end

      MiqQueue.put(options)
    end
  end

  def target_hosts(action, rec)
    if action.options[:use_event_target]
      ipaddress(rec)
    elsif action.options[:use_localhost]
      'localhost'
    else
      action.options[:hosts]
    end
  end

  def ipaddress(record)
    record.ipaddresses[0] if record.respond_to?(:ipaddresses)
  end

  def target_user(record)
    record.respond_to?(:tenant_identity) ? record.tenant_identity : default_user
  end

  def default_user
    User.super_admin.tap { |u| u.current_group = Tenant.root_tenant.default_miq_group }
  end
end