app/models/miq_request.rb
class MiqRequest < ApplicationRecord
extend InterRegionApiMethodRelay
ACTIVE_STATES = %w[active queued]
REQUEST_UNIQUE_KEYS = %w[id state status created_on updated_on type].freeze
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
belongs_to :source, :polymorphic => true
belongs_to :destination, :polymorphic => true
belongs_to :requester, :class_name => "User"
belongs_to :tenant
belongs_to :service_order
belongs_to :parent, :class_name => "MiqRequest"
has_many :miq_approvals, :dependent => :destroy
has_many :miq_request_tasks, :dependent => :destroy
has_many :request_logs, :foreign_key => :resource_id, :dependent => :destroy
alias_attribute :state, :request_state
serialize :options, Hash
default_value_for(:message) { |r| "#{r.class::TASK_DESCRIPTION} - Request Created" }
default_value_for :options, {}
default_value_for :request_state, 'pending'
default_value_for(:request_type) { |r| r.request_types.first }
default_value_for :status, 'Ok'
default_value_for :process, true
validates_inclusion_of :approval_state, :in => %w[pending_approval approved denied], :message => "should be 'pending_approval', 'approved' or 'denied'"
validates_inclusion_of :status, :in => %w[Ok Warn Error Timeout Denied]
validates :initiated_by, :inclusion => {:in => %w[user system]}, :allow_blank => true
validates :cancelation_status, :inclusion => {:in => CANCEL_STATUS,
:allow_nil => true,
:message => "should be one of #{CANCEL_STATUS.join(", ")}"}
validate :validate_class, :validate_request_type
include TenancyMixin
virtual_column :reason, :type => :string, :uses => :miq_approvals
virtual_column :v_approved_by, :type => :string, :uses => :miq_approvals
virtual_column :v_approved_by_email, :type => :string, :uses => {:miq_approvals => :stamper}
virtual_column :stamped_on, :type => :datetime, :uses => :miq_approvals
virtual_column :v_allowed_tags, :type => :string, :uses => :workflow
virtual_column :v_workflow_class, :type => :string, :uses => :workflow
virtual_column :request_type_display, :type => :string
virtual_column :resource_type, :type => :string
virtual_column :state, :type => :string
delegate :allowed_tags, :to => :workflow, :prefix => :v, :allow_nil => true
delegate :class, :to => :workflow, :prefix => :v_workflow
delegate :deny, :reason, :stamped_on, :to => :first_approval
delegate :userid, :to => :requester, :prefix => true
delegate :request_task_class, :request_types, :task_description, :to => :class
virtual_has_one :workflow
before_validation :initialize_attributes, :on => :create
include MiqRequestMixin
scope :created_recently, ->(days_ago) { where("miq_requests.created_on > ?", days_ago.days.ago) }
scope :with_approval_state, ->(state) { where(:approval_state => state) }
scope :with_cancel_status, ->(status) { where(:cancelation_status => status) }
scope :with_type, ->(type) { where(:type => type) }
scope :with_request_type, ->(type) { where(:request_type => type) }
scope :with_requester, ->(id) { where(:requester_id => User.in_all_regions(id).select(:id)) }
MODEL_REQUEST_TYPES = {
:Automate => {
:AutomationRequest => {
:automation => N_("Automation")
}
},
:Service => {
:MiqProvisionConfiguredSystemRequest => {
:provision_via_foreman => N_("%{config_mgr_type} Provision") % {:config_mgr_type => ui_lookup(:ui_title => 'foreman')}
},
:MiqProvisionRequest => {
:template => N_("VM Provision"),
:clone_to_vm => N_("VM Clone"),
:clone_to_template => N_("VM Publish"),
},
:OrchestrationStackRetireRequest => {
:orchestration_stack_retire => N_("Orchestration Stack Retire")
},
:PhysicalServerProvisionRequest => {
:provision_physical_server => N_("Physical Server Provision")
},
:PhysicalServerFirmwareUpdateRequest => {
:physical_server_firmware_update => N_("Physical Server Firmware Update")
},
:ServiceRetireRequest => {
:service_retire => N_("Service Retire")
},
:ServiceReconfigureRequest => {
:service_reconfigure => N_("Service Reconfigure")
},
:ServiceTemplateProvisionRequest => {
:clone_to_service => N_("Service Provision")
},
:VmCloudReconfigureRequest => {
:vm_cloud_reconfigure => N_("VM Cloud Reconfigure")
},
:VmMigrateRequest => {
:vm_migrate => N_("VM Migrate")
},
:VmReconfigureRequest => {
:vm_reconfigure => N_("VM Reconfigure")
},
:VmRetireRequest => {
:vm_retire => N_("VM Retire")
},
},
}.freeze
REQUEST_TYPES_BACKEND_ONLY = {
:MiqProvisionRequestTemplate => {:template => "VM Provision Template"},
}
REQUEST_TYPES = MODEL_REQUEST_TYPES.values.each_with_object(REQUEST_TYPES_BACKEND_ONLY) { |i, h| i.each { |k, v| h[k] = v } }
REQUEST_TYPE_TO_MODEL = MODEL_REQUEST_TYPES.values.each_with_object({}) do |i, h|
i.each { |k, v| v.keys.each { |vk| h[vk] = k } }
end
def self.class_from_request_data(data)
request_type = (data[:__request_type__] || data[:request_type]).try(:to_sym)
model_symbol = REQUEST_TYPE_TO_MODEL[request_type] || raise(ArgumentError, "Invalid request_type")
model_symbol.to_s.constantize
end
def self.with_reason_like(reason)
# Reason string uses * wildcard, scope is required to convert it into % wildcard for LIKE statement
reason = reason.match(/\A(?<start>\*?)(?<content>.*?)(?<end>\*?)\z/)
joins(:miq_approvals).where("miq_approvals.reason LIKE (?)", "#{reason[:start] ? '%' : ''}#{sanitize_sql_like(reason[:content])}#{reason[:end] ? '%' : ''}")
end
def self.user_or_group_owned(user, miq_group)
if user && miq_group
user_owned(user).or(group_owned(miq_group))
elsif user
user_owned(user)
elsif miq_group
group_owned(miq_group)
else
none
end
end
def self.user_owned(user)
where(:requester_id => user.regional_users.select(:id))
end
def self.group_owned(miq_group)
where(:requester_id => miq_group.regional_groups.joins(:users).select("users.id"))
end
# Supports old-style requests where specific request was a separate table connected as a resource
def resource
self
end
def miq_request
self
end
def create_request
self
end
def resource_type
self.class.name
end
def tracking_label_id
"r#{id}_#{self.class.name.underscore}_#{id}"
end
def initialize_attributes
self.approval_state ||= "pending_approval"
miq_approvals << build_default_approval if miq_approvals.empty?
return unless requester
self.requester_name ||= requester.name
self.userid ||= requester.userid
self.tenant ||= requester.current_tenant
end
def must_have_user
errors.add(:userid, "must have valid user") unless requester
end
def call_automate_event_queue(event_name)
MiqQueue.put(
:class_name => self.class.name,
:instance_id => id,
:method_name => "call_automate_event",
:args => [event_name],
:zone => options.fetch(:miq_zone, my_zone),
:msg_timeout => 3600
)
end
def build_request_event(event_name)
event_obj = RequestEvent.create(
:event_type => event_name,
:target => self,
:source => 'Request'
)
{'EventStream::event_stream' => event_obj.id,
:event_stream_id => event_obj.id
}
end
def call_automate_event(event_name, synchronous: false)
request_log(:info, "Raising event [#{event_name}] to Automate#{' synchronously' if synchronous}", :resource_id => id)
MiqAeEvent.raise_evm_event(event_name, self, build_request_event(event_name), :synchronous => synchronous).tap do
request_log(:info, "Raised event [#{event_name}] to Automate", :resource_id => id)
end
rescue MiqAeException::Error => err
message = _("Error returned from %{name} event processing in Automate: %{error_message}") % {:name => event_name, :error_message => err.message}
request_log(:error, message, :resource_id => id)
raise
end
def automate_event_failed?(event_name)
ws = call_automate_event(event_name, :synchronous => true)
if ws.nil?
request_log(:warn, "Aborting because Automate failed for event <#{event_name}>", :resource_id => id)
return true
end
if ws.root['ae_result'] == 'error'
request_log(:warn, "Aborting because Automate returned ae_result=<#{ws.root['ae_result']}> for event <#{event_name}>", :resource_id => id)
return true
end
false
end
def pending
call_automate_event_queue("request_pending")
end
def approval_approved
unless approved?
_log.info("Request: [#{description}] has outstanding approvals")
return false
end
update(:approval_state => "approved")
call_automate_event_queue("request_approved")
# execute parent now that request is approved
request_log(:info, "Request: [#{description}] has all approvals approved, proceeding with execution", :resource_id => id)
begin
execute
rescue => err
request_log(:error, "#{err.message}, attempting to execute request: [#{description}]", :resource_id => id)
_log.log_backtrace(err) # TODO: discuss adding this to request_log as well
end
true
end
def approval_denied
update(:approval_state => "denied", :request_state => "finished", :status => "Denied")
call_automate_event_queue("request_denied")
end
def approved?
miq_approvals.all? { |a| a.state == "approved" }
end
def v_approved_by
miq_approvals.collect(&:stamper_name).compact.join(", ")
end
def v_approved_by_email
miq_approvals.collect { |i| i.stamper.try(:email) }.compact.join(", ")
end
def get_options
options || {}
end
def request_type_display
request_type.nil? ? "Unknown" : REQUEST_TYPES.fetch_path(type.to_sym, request_type.to_sym)
end
def self.request_types
REQUEST_TYPES[name.to_sym].keys.collect(&:to_s)
end
def request_status
return status if self.approval_state == 'approved' && !status.nil?
case self.approval_state
when 'pending_approval' then 'Unknown'
when 'denied' then 'Error'
else 'Unknown'
end
end
def build_default_approval
MiqApproval.new(:description => "Default Approval")
end
# TODO: Helper methods to support UI in legacy mode - single approval by role
# These should be removed once multi-approver is fully supported.
def first_approval
miq_approvals.first || build_default_approval
end
def approve(userid, reason)
first_approval.approve(userid, reason) unless approved?
end
api_relay_method(:approve) { |_userid, reason| {:reason => reason} }
api_relay_method(:deny) { |_userid, reason| {:reason => reason} }
def stamped_by
first_approval.stamper.try(:userid)
end
def approver
first_approval.approver.try(:name)
end
alias_method :approver_role, :approver # TODO: Is this needed anymore?
def workflow_class
klass = self.class.workflow_class
klass = klass.class_for_source(source) if klass.respond_to?(:class_for_source)
klass
end
def self.workflow_class
@workflow_class ||= name.underscore.chomp("_template").gsub(/_request$/, "_workflow").camelize.constantize rescue nil
end
def self.request_task_class
@request_task_class ||= begin
case name
when 'MiqProvisionRequest'
name.underscore.chomp('_request').camelize.constantize
else
name.underscore.gsub(/_request$/, "_task").camelize.constantize
end
end
end
def requested_task_idx
options[:src_ids]
end
def customize_request_task_attributes(req_task_attrs, idx)
req_task_attrs[:source_id] = idx
req_task_attrs[:source_type] = self.class::SOURCE_CLASS_NAME
end
def set_description(force = false)
if description.nil? || force == true
description = default_description || request_task_class.get_description(self)
update(:description => description)
end
end
def update_request_status
states = Hash.new { |h, k| h[k] = 0 }
status = Hash.new { |h, k| h[k] = 0 }
task_count = miq_request_tasks.count
miq_request_tasks.each do |p|
states[p.state] += 1
status[p.status] += 1
end
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"
update_attribute(:fulfilled_on, Time.now.utc)
msg = (req_status == 'Ok') ? "Request complete" : "Request completed with errors"
end
# If there is only 1 request_task, set the parent message the same
if task_count == 1
child = miq_request_tasks.first
msg = child.message unless child.nil?
end
update(:request_state => req_state, :status => req_status, :message => display_message(msg))
end
def post_create_request_tasks
end
def my_zone
MiqServer.my_zone
end
def my_role(_action = nil)
nil
end
def my_queue_name
nil
end
def task_check_on_execute
if self.class::ACTIVE_STATES.include?(request_state)
raise _("%{task} request is already being processed") % {:task => self.class::TASK_DESCRIPTION}
end
if request_state == "finished"
raise _("%{task} request has already been processed") % {:task => self.class::TASK_DESCRIPTION}
end
raise _("approval is required for %{task}") % {:task => self.class::TASK_DESCRIPTION} unless approved?
end
def execute
task_check_on_execute
deliver_on = nil
if get_option(:schedule_type) == "schedule"
deliver_on = get_option(:schedule_time).utc rescue nil
end
# self.create_request_tasks
MiqQueue.put(
:class_name => self.class.name,
:instance_id => id,
:method_name => "create_request_tasks",
:zone => options.fetch(:miq_zone, my_zone),
:role => my_role(:create_request_tasks),
:tracking_label => tracking_label_id,
:msg_timeout => 3600,
:deliver_on => deliver_on
)
end
def create_request_tasks
if cancel_requested?
do_cancel
return
end
# Quota denial will result in automate_event_failed? being true
return if automate_event_failed?("request_starting")
request_log(:info, "Creating request task instances for: <#{description}>...", :resource_id => id)
# Create a MiqRequestTask object for each requested item
options[:delivered_on] = Time.now.utc
update_attribute(:options, options)
begin
requested_tasks = requested_task_idx
request_task_created = 0
requested_tasks.each do |idx|
req_task = create_request_task(idx)
miq_request_tasks << req_task
req_task.deliver_queue
request_task_created += 1
end
update_request_status
post_create_request_tasks
rescue
_log.log_backtrace($ERROR_INFO) # TODO: Add to Request Logs
request_state, status = request_task_created.zero? ? %w[finished Error] : %w[active Warn]
update(:request_state => request_state, :status => status, :message => "Error: #{$ERROR_INFO}")
end
end
def self.new_request_task(attribs)
request_task_class.new(attribs)
end
def create_request_task(idx)
req_task_attribs = clean_up_keys_for_request_task
customize_request_task_attributes(req_task_attribs, idx)
req_task = self.class.new_request_task(req_task_attribs)
req_task.miq_request = self
yield req_task if block_given?
req_task.save!
req_task.after_request_task_create
req_task
end
# Helper method when not using workflow
def self.make_request(request, values, requester, auto_approve = false)
if request
update_request(request, values, requester)
else
create_request(values, requester, auto_approve)
end
end
def self.create_request(values, requester, auto_approve = false)
values[:src_ids] = Array.wrap(values[:src_ids]) unless values[:src_ids].nil?
request_type = values.delete(:__request_type__) || request_types.first
initiator = values.delete(:__initiated_by__) || 'user'
parent_id = values[:request_options]&.delete(:parent_id)
request = create!(:options => values, :requester => requester, :request_type => request_type, :initiated_by => initiator, :parent_id => parent_id)
request.post_create(auto_approve)
end
api_relay_class_method(:create_request, :create) do |values, requester, auto_approve|
[
find_source_id_from_values(values),
{
:options => values,
:requester => {"user_name" => requester.userid},
:auto_approve => auto_approve
}
]
end
def self.find_source_id_from_values(values)
MiqRequestMixin.get_option(:src_vm_id, nil, values) ||
MiqRequestMixin.get_option(:src_id, nil, values) ||
MiqRequestMixin.get_option(:src_ids, nil, values)
end
private_class_method :find_source_id_from_values
def post_create(auto_approve)
set_description
audit_request_success(requester, :created)
if process_on_create?
call_automate_event_queue("request_created")
approve(User.super_admin.userid, "Auto-Approved") if auto_approve
reload if auto_approve
end
self
end
# Helper method when not using workflow
def self.update_request(request, values, requester)
request = MiqRequest.find(request) unless request.kind_of?(MiqRequest)
request.update_request(values, requester)
end
def update_request(values, requester)
update(:options => options.merge(values))
self.user_message = values[:user_message] if values[:user_message].present?
after_update_options(requester) unless values.keys == [:user_message]
self
end
api_relay_method(:update_request, :edit) do |values, requester|
{
:options => values,
:requester => {"user_name" => requester.userid}
}
end
def audit_request_success(requester_id, mode)
requester_id = requester_id.userid if requester_id.respond_to?(:userid)
status_message = mode == :created ? "requested" : "request updated"
event_message = "#{self.class::TASK_DESCRIPTION} #{status_message} by <#{requester_id}> for #{my_records}"
AuditEvent.success(
:event => event_name(mode),
:target_class => self.class::SOURCE_CLASS_NAME,
:userid => requester_id,
:message => event_message
)
end
def event_name(mode)
"#{self.class.name.underscore}_#{mode}"
end
def process_on_create?
true
end
def request_pending_approval?
approval_state == "pending_approval"
end
def request_approved?
approval_state == "approved"
end
def request_denied?
approval_state == "denied"
end
def my_records
"#{self.class::SOURCE_CLASS_NAME}:#{requested_task_idx.inspect}"
end
def self.display_name(number = 1)
n_('Request', 'Requests', number)
end
def cancel
raise _("Cancel operation is not supported for %{class}") % {:class => self.class.name}
end
def cancel_requested?
cancelation_status == CANCEL_STATUS_REQUESTED
end
def canceling?
cancelation_status == CANCEL_STATUS_PROCESSING
end
def canceled?
cancelation_status == CANCEL_STATUS_FINISHED
end
# Helper method to log the request to both the request_logs table and $log
def self.request_log(severity, message = nil, resource_id: nil)
formatted_severity = severity.to_s.upcase
level = Logger.const_get(formatted_severity)
# Partially copied from Logger#add
return true if level < $log.level
message = yield if message.nil? && block_given?
RequestLog.create(:message => message, :severity => formatted_severity, :resource_id => resource_id) if resource_id
$log.public_send(severity, message)
end
def request_log(severity, message = nil, resource_id: nil, &block)
self.class.request_log(severity, message, resource_id: resource_id, &block)
end
private
def do_cancel
update(:cancelation_status => CANCEL_STATUS_FINISHED, :request_state => "finished", :status => "Error", :message => "Request is canceled by user.")
request_log(:info, "Request #{description} is canceled by user.", :resource_id => id)
end
def clean_up_keys_for_request_task
req_task_attributes = attributes.dup
(req_task_attributes.keys - MiqRequestTask.column_names + REQUEST_UNIQUE_KEYS).each { |key| req_task_attributes.delete(key) }
req_task_attributes["options"].delete(:user_message)
_log.debug("#{self.class.name} Attributes: [#{req_task_attributes.inspect}]...")
req_task_attributes
end
def default_description
end
def validate_class
errors.add(:type, "should be a descendant of MiqRequest") if instance_of?(MiqRequest)
end
def validate_request_type
errors.add(:request_type, "should be #{request_types.join(", ")}") unless request_types.include?(request_type)
end
def after_update_options(requester)
set_description(true)
audit_request_success(requester, :updated)
call_automate_event_queue("request_updated")
end
end