ManageIQ/manageiq

View on GitHub
app/models/miq_request_task.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
90%
class MiqRequestTask < ApplicationRecord
  include Dumping
  include PostInstallCallback
  include StateMachine

  belongs_to :miq_request
  belongs_to :source,            :polymorphic => true
  belongs_to :destination,       :polymorphic => true
  has_many   :miq_request_tasks, :dependent   => :destroy
  belongs_to :miq_request_task
  belongs_to :tenant

  serialize   :phase_context, Hash
  serialize   :options,       Hash

  default_value_for :phase_context, {}
  default_value_for :options,       {}
  default_value_for :state,         'pending'
  default_value_for :status,        'Ok'

  delegate :request_class, :task_description, :to => :class

  validates_inclusion_of :status, :in => %w[Ok Warn Error Timeout]

  include MiqRequestMixin
  include TenancyMixin

  CANCEL_STATUS_REQUESTED  = "cancel_requested".freeze
  CANCEL_STATUS_PROCESSING = "canceling".freeze
  CANCEL_STATUS_FINISHED   = "canceled".freeze
  CANCEL_STATUS            = [CANCEL_STATUS_REQUESTED, CANCEL_STATUS_PROCESSING, CANCEL_STATUS_FINISHED].freeze

  validates :cancelation_status, :inclusion => {:in        => CANCEL_STATUS,
                                                 :allow_nil => true,
                                                 :message   => "should be one of #{CANCEL_STATUS.join(", ")}"}

  def approved?
    if miq_request.class.name.include?('Template') && miq_request_task
      miq_request_task.miq_request.approved?
    else
      miq_request.approved?
    end
  end

  def after_request_task_create
  end

  def update_and_notify_parent(upd_attr)
    upd_attr[:message] = upd_attr[:message][0, 255] if upd_attr.key?(:message)
    update!(upd_attr)

    # If this request has a miq_request_task parent use that, otherwise the parent is the miq_request
    parent = miq_request_task || miq_request
    parent.reload
    parent.update_request_status
  end

  def update_request_status
    states = Hash.new { |h, k| h[k] = 0 }
    status = Hash.new { |h, k| h[k] = 0 }

    child_requests = miq_request_tasks
    task_count = child_requests.size
    child_requests.each do |child_req|
      states[child_req.state] += 1
      states[:total] += 1
      status[child_req.status] += 1
    end
    total = states.delete(:total).to_i
    unknown_state = task_count - total
    states["unknown"] = unknown_state unless unknown_state.zero?
    msg = states.sort.collect { |s| "#{s[0].capitalize} = #{s[1]}" }.join("; ")

    req_state = (states.length == 1) ? states.keys.first : "active"

    # Determine status to report
    req_status = status.slice('Error', 'Timeout', 'Warn').keys.first || 'Ok'

    if req_state == "finished" && state != "finished"
      req_state = req_status == 'Ok' ? completed_state : "finished"
      $log.info("Child tasks finished but current task still processing. Setting state to: [#{req_state}]...")
    end

    if req_state == "finished"
      msg = (req_status == 'Ok') ? "Task complete" : "Task completed with errors"
    end

    # If there is only 1 request_task, set the parent message the same
    if task_count == 1
      child = child_requests.first
      msg = child.message unless child.nil?
    end

    update_and_notify_parent(:state => req_state, :status => req_status, :message => display_message(msg))
  end

  def completed_state
    "provisioned"
  end

  def execute_callback(state, message, _result)
    unless state.to_s.downcase == "ok"
      update_and_notify_parent(:state => "finished", :status => "Error", :message => "Error: #{message}")
    end
  end

  def self.request_class
    if self <= MiqProvision
      MiqProvisionRequest
    else
      name.underscore.gsub(/_task$/, "_request").camelize.constantize
    end
  end

  def self.task_description
    request_class::TASK_DESCRIPTION
  end

  def get_description
    self.class.get_description(self)
  end

  def task_check_on_delivery
    if request_class::ACTIVE_STATES.include?(state)
      raise _("%{task} request is already being processed") % {:task => request_class::TASK_DESCRIPTION}
    end

    task_check_on_execute
  end

  def task_check_on_execute
    if state == "finished"
      raise _("%{task} request has already been processed") % {:task => request_class::TASK_DESCRIPTION}
    end
    raise _("approval is required for %{task}") % {:task => request_class::TASK_DESCRIPTION} unless approved?
  end

  def deliver_queue(req_type = request_type, zone = nil)
    task_check_on_delivery

    _log.info("Queuing #{request_class::TASK_DESCRIPTION}: [#{description}]...")

    workflow_id = options[:configuration_script_payload_id]
    workflow    = ConfigurationScriptPayload.find(workflow_id) if workflow_id
    if workflow
      miq_task_id = workflow.run(:inputs => workflow_inputs, :userid => get_user.userid, :zone => zone, :object => self)

      options[:miq_task_id]                     = miq_task_id
      options[:configuration_script_payload_id] = workflow.id
      options[:configuration_script_id]         = MiqTask.find(miq_task_id).context_data[:workflow_instance_id]
      save!
    elsif self.class::AUTOMATE_DRIVES
      deliver_to_automate(req_type, zone)
    else
      execute_queue
    end
  end

  def deliver_to_automate(req_type = request_type, zone = nil)
    args = {
      :object_type   => self.class.name,
      :object_id     => id,
      :attrs         => {"request" => req_type},
      :instance_name => "AUTOMATION",
      :user_id       => get_user.id,
      :miq_group_id  => get_user.current_group.id,
      :tenant_id     => get_user.current_tenant.id,
    }

    zone ||= source.respond_to?(:my_zone) ? source.my_zone : MiqServer.my_zone
    MiqQueue.put(
      :class_name     => 'MiqAeEngine',
      :method_name    => 'deliver',
      :args           => [args],
      :role           => 'automate',
      :zone           => options.fetch(:miq_zone, zone),
      :tracking_label => tracking_label_id
    )
    update_and_notify_parent(:state => "pending", :status => "Ok", :message => "Automation Starting")
  end

  def execute_queue(queue_options = {})
    task_check_on_execute

    _log.info("Queuing #{request_class::TASK_DESCRIPTION}: [#{description}]...")

    deliver_on = nil
    if get_option(:schedule_type) == "schedule"
      deliver_on = get_option(:schedule_time).utc rescue nil
    end

    zone = source.respond_to?(:my_zone) ? source.my_zone : MiqServer.my_zone

    queue_options.reverse_merge!(
      :class_name     => self.class.name,
      :instance_id    => id,
      :method_name    => "execute",
      :zone           => options.fetch(:miq_zone, zone),
      :role           => miq_request.my_role,
      :queue_name     => miq_request.my_queue_name,
      :tracking_label => tracking_label_id,
      :deliver_on     => deliver_on,
      :miq_callback   => {:class_name => self.class.name, :instance_id => id, :method_name => :execute_callback}
    )
    MiqQueue.put(queue_options)

    update_and_notify_parent(:state => "queued", :status => "Ok", :message => "State Machine Initializing")
  end

  def execute
    _log.info("Executing #{request_class::TASK_DESCRIPTION} request: [#{description}]")
    update_and_notify_parent(:state => "active", :status => "Ok", :message => "In Process")

    begin
      message = "#{request_class::TASK_DESCRIPTION} initiated"
      _log.info(message)
      update_and_notify_parent(:message => message)

      # Process the request
      do_request
    rescue => err
      message = "Error: #{err.message}"
      _log.error("[#{message}] encountered during #{request_class::TASK_DESCRIPTION}")
      _log.log_backtrace(err)
      update_and_notify_parent(:state => "finished", :status => "Error", :message => message)
      nil
    end
  end

  def resource_action
    # Override in child class if the source has resource_actions
    nil
  end

  def self.display_name(number = 1)
    n_('Request Task', 'Request Tasks', number)
  end

  def cancel
    raise _("Cancel operation is not supported for %{class}") % {:class => self.class.name}
  end

  def cancel_requested?
    cancelation_status == MiqRequestTask::CANCEL_STATUS_REQUESTED
  end

  def canceling?
    cancelation_status == MiqRequestTask::CANCEL_STATUS_PROCESSING
  end

  def canceled?
    cancelation_status == MiqRequestTask::CANCEL_STATUS_FINISHED
  end

  private

  def validate_request_type
    errors.add(:request_type, "should be #{request_class.request_types.join(", ")}") unless request_class.request_types.include?(request_type)
  end

  def validate_state
    errors.add(:state, "should be #{valid_states.join(", ")}") unless valid_states.include?(state)
  end

  def valid_states
    %w[pending finished] + request_class::ACTIVE_STATES
  end

  def workflow_inputs
    {:dialog => dialog_values}
  end

  def dialog_values
    options[:dialog] || {}
  end
end