ManageIQ/manageiq-automation_engine

View on GitHub
lib/miq_automation_engine/engine/miq_ae_method_service/miq_ae_service.rb

Summary

Maintainability
A
55 mins
Test Coverage
A
93%
require_relative './miq_ae_service_model_legacy'
require_relative './miq_ae_service_vmdb'
require_relative './miq_ae_service_rbac'
module MiqAeMethodService
  class MiqAeService
    include Vmdb::Logging
    include DRbUndumped
    include MiqAeMethodService::MiqAeServiceModelLegacy
    include MiqAeMethodService::MiqAeServiceVmdb
    include MiqAeMethodService::MiqAeServiceRbac

    attr_accessor :logger

    @@id_hash = {}
    @@current = []

    def self.current
      @@current.last
    end

    def self.find(id)
      @@id_hash[id.to_i]
    end

    def self.add(obj)
      @@id_hash[obj.object_id] = obj
      @@current << obj
    end

    def self.destroy(obj)
      @@id_hash.delete(obj.object_id)
      @@current.delete(obj)
    end

    def initialize(workspace, inputs = {}, logger = $miq_ae_logger)
      @tracking_label        = Thread.current["tracking_label"]
      @drb_server_references = []
      @inputs                = inputs
      @workspace             = workspace
      @persist_state_hash    = workspace.persist_state_hash
      @logger                = logger
      self.class.add(self)
      workspace.disable_rbac
    end

    delegate :enable_rbac, :disable_rbac, :rbac_enabled?, :to => :@workspace

    def stdout
      @stdout ||= Vmdb::Loggers::IoLogger.new(logger, :info, "Method STDOUT:")
    end

    def stderr
      @stderr ||= Vmdb::Loggers::IoLogger.new(logger, :error, "Method STDERR:")
    end

    def destroy
      self.class.destroy(self)
    end

    def disconnect_sql
      ActiveRecord::Base.connection_pool.release_connection
    end

    attr_writer :inputs

    attr_reader :inputs

    ####################################################

    def log(level, msg)
      Thread.current["tracking_label"] = @tracking_label
      $miq_ae_logger.send(level, "<AEMethod #{current_method}> #{ManageIQ::Password.sanitize_string(msg)}", :resource_id => @workspace.find_miq_request_id)
    end

    def set_state_var(name, value)
      @persist_state_hash[name] = value
    end

    def state_var_exist?(name)
      @persist_state_hash.key?(name)
    end

    def get_state_var(name)
      @persist_state_hash[name]
    end

    def delete_state_var(name)
      @persist_state_hash.delete(name)
    end

    def get_state_vars
      @persist_state_hash
    end

    def ansible_stats_vars
      MiqAeEngine::MiqAeAnsibleMethodBase.ansible_stats_from_hash(@persist_state_hash)
    end

    def set_service_var(name, value)
      if service_object.nil?
        $miq_ae_logger.error("Service object not found in root object, set_service_var skipped for #{name} = #{value}", :resource_id => @workspace.find_miq_request_id)
        return
      end

      service_object.root_service.set_service_vars_option(name, value)
    end

    def service_var_exists?(name)
      return false unless service_object

      service_object.root_service.service_vars_options.key?(name)
    end

    def get_service_var(name)
      return unless service_var_exists?(name)

      service_object.root_service.get_service_vars_option(name)
    end

    def delete_service_var(name)
      return unless service_var_exists?(name)

      service_object.root_service.delete_service_vars_option(name)
    end

    def prepend_namespace=(namespace)
      @workspace.prepend_namespace = namespace
    end

    def instantiate(uri)
      obj = @workspace.instantiate(uri, @workspace.ae_user, @workspace.current_object)
      return nil if obj.nil?

      MiqAeServiceObject.new(obj, self)
    rescue StandardError => e
      $miq_ae_logger.error("instantiate failed : #{e.message}", :resource_id => @workspace.find_miq_request_id)
      nil
    end

    def object(path = nil)
      obj = @workspace.get_obj_from_path(path)
      return nil if obj.nil?

      MiqAeServiceObject.new(obj, self)
    end

    def hash_to_query(hash)
      MiqAeEngine::MiqAeUri.hash2query(hash)
    end

    def query_to_hash(query)
      MiqAeEngine::MiqAeUri.query2hash(query)
    end

    def current_namespace
      @workspace.current_namespace
    end

    def current_class
      @workspace.current_class
    end

    def current_instance
      @workspace.current_instance
    end

    def current_message
      @workspace.current_message
    end

    def current_object
      @current_object ||= MiqAeServiceObject.new(@workspace.current_object, self)
    end

    def current_method
      @workspace.current_method
    end

    def current
      current_object
    end

    def root
      @root ||= object("/")
    end

    def parent
      @parent ||= object("..")
    end

    def objects(aobj)
      aobj.collect do |obj|
        obj = MiqAeServiceObject.new(obj, self) unless obj.kind_of?(MiqAeServiceObject)
        obj
      end
    end

    def datastore
    end

    def ldap
    end

    def execute(method_name, *args, **kwargs, &block)
      User.with_user(@workspace.ae_user) { execute_with_user(method_name, *args, **kwargs, &block) }
    end

    def execute_with_user(method_name, *args, **kwargs, &block)
      # Since each request from DRb client could run in a separate thread
      # We have to set the current_user in every thread.

      # For ruby 2.6-3.0+ support, we grab any kwargs and append as a hash
      # at the end of the args, as all MiqAeServiceMethods now don't accept
      # kwargs.  When we get to ruby 3, we can remove this and convert all to
      # kwargs.
      args << kwargs unless kwargs.blank?
      MiqAeServiceMethods.send(method_name, *args, &block)
    rescue NoMethodError => err
      raise MiqAeException::MethodNotFound, err.message
    end

    def notification_subject(values_hash)
      subject = values_hash[:subject] || @workspace.ae_user
      (ar_object(subject) || subject).tap do |object|
        raise ArgumentError, "Subject must be a valid Active Record object" unless object.kind_of?(ActiveRecord::Base)
      end
    end

    def ar_object(svc_obj)
      if svc_obj.kind_of?(MiqAeMethodService::MiqAeServiceModelBase)
        svc_obj.instance_variable_get('@object')
      end
    end

    def notification_type(values_hash)
      type = values_hash[:type].present? ? values_hash[:type].to_sym : default_notification_type(values_hash)
      type.tap do |t|
        $miq_ae_logger.info("Validating Notification type: #{t}", :resource_id => @workspace.find_miq_request_id)
        valid_type = NotificationType.find_by(:name => t)
        raise ArgumentError, "Invalid notification type specified" unless valid_type
      end
    end

    def create_notification(values_hash = {})
      create_notification!(values_hash)
    rescue StandardError
      nil
    end

    def create_notification!(values_hash = {})
      User.with_user(@workspace.ae_user) { create_notification_with_user!(values_hash) }
    end

    def create_notification_with_user!(values_hash)
      options = {}
      type = notification_type(values_hash)
      subject = notification_subject(values_hash)
      options[:message] = values_hash[:message] if values_hash[:message].present?

      $miq_ae_logger.info("Calling Create Notification type: #{type} subject type: #{subject.class.base_class.name} id: #{subject.id} options: #{options.inspect}", :resource_id => @workspace.find_miq_request_id)
      MiqAeServiceModelBase.wrap_results(Notification.create!(:type      => type,
                                                              :subject   => subject,
                                                              :options   => options,
                                                              :initiator => @workspace.ae_user))
    end

    def instance_exists?(path)
      _log.info("<< path=#{path.inspect}")
      !!__find_instance_from_path(path)
    end

    def instance_create(path, values_hash = {})
      _log.info("<< path=#{path.inspect}, values_hash=#{values_hash.inspect}")

      return false unless editable_instance?(path)

      ns, klass, instance = MiqAeEngine::MiqAePath.split(path)
      $miq_ae_logger.info("Instance Create for ns: #{ns} class #{klass} instance: #{instance}", :resource_id => @workspace.find_miq_request_id)

      aec = MiqAeClass.lookup_by_namespace_and_name(ns, klass)
      return false if aec.nil?

      aei = aec.ae_instances.detect { |i| instance.casecmp(i.name).zero? }
      return false unless aei.nil?

      aei = MiqAeInstance.create(:name => instance, :class_id => aec.id)
      values_hash.each { |key, value| aei.set_field_value(key, value) }

      true
    end

    def instance_get_display_name(path)
      _log.info("<< path=#{path.inspect}")
      aei = __find_instance_from_path(path)
      aei.try(:display_name)
    end

    def instance_set_display_name(path, display_name)
      _log.info("<< path=#{path.inspect}, display_name=#{display_name.inspect}")
      aei = __find_instance_from_path(path)
      return false if aei.nil?

      aei.update(:display_name => display_name)
      true
    end

    def instance_update(path, values_hash)
      _log.info("<< path=#{path.inspect}, values_hash=#{values_hash.inspect}")
      return false unless editable_instance?(path)

      aei = __find_instance_from_path(path)
      return false if aei.nil?

      values_hash.each { |key, value| aei.set_field_value(key, value) }
      true
    end

    def instance_find(path, options = {})
      _log.info("<< path=#{path.inspect}")
      result = {}

      ns, klass, instance = MiqAeEngine::MiqAePath.split(path)
      aec = MiqAeClass.lookup_by_namespace_and_name(ns, klass)
      unless aec.nil?
        instance.gsub!(".", '\.')
        instance.gsub!("*", ".*")
        instance.gsub!("?", ".{1}")
        instance_re = Regexp.new("^#{instance}$", Regexp::IGNORECASE)

        aec.ae_instances.select { |i| instance_re =~ i.name }.each do |aei|
          iname = if options[:path]
                    aei.fqname
                  else
                    aei.name
                  end
          result[iname] = aei.field_attributes
        end
      end

      result
    end

    def instance_get(path)
      _log.info("<< path=#{path.inspect}")
      aei = __find_instance_from_path(path)
      return nil if aei.nil?

      aei.field_attributes
    end

    def instance_delete(path)
      _log.info("<< path=#{path.inspect}")
      return false unless editable_instance?(path)

      aei = __find_instance_from_path(path)
      return false if aei.nil?

      aei.destroy
      true
    end

    def __find_instance_from_path(path)
      dom, ns, klass, instance = MiqAeEngine::MiqAePath.get_domain_ns_klass_inst(path)
      return false unless visible_domain?(dom)

      aec = MiqAeClass.lookup_by_namespace_and_name("#{dom}/#{ns}", klass)
      return nil if aec.nil?

      aec.ae_instances.detect { |i| instance.casecmp(i.name).zero? }
    end

    def field_timeout
      raise _("ae_state_max_retries is not set in automate field") if root['ae_state_max_retries'].blank?

      interval = root['ae_retry_interval'].present? ? root['ae_retry_interval'].to_i_with_method : 1
      interval * root['ae_state_max_retries'].to_i
    end

    private

    def service_object
      current['service'] || root['service']
    end

    def editable_instance?(path)
      dom, = MiqAeEngine::MiqAePath.get_domain_ns_klass_inst(path)
      return false unless owned_domain?(dom)

      domain = MiqAeDomain.lookup_by_fqname(dom, false)
      return false unless domain

      $miq_ae_logger.warn("path=#{path.inspect} : is not editable", :resource_id => @workspace.find_miq_request_id) unless domain.editable?(@workspace.ae_user)
      domain.editable?(@workspace.ae_user)
    end

    def owned_domain?(dom)
      domains = @workspace.ae_user.current_tenant.ae_domains.collect(&:name).map(&:upcase)
      return true if domains.include?(dom.upcase)

      $miq_ae_logger.warn("domain=#{dom} : is not editable", :resource_id => @workspace.find_miq_request_id)
      false
    end

    def visible_domain?(dom)
      domains = @workspace.ae_user.current_tenant.visible_domains.collect(&:name).map(&:upcase)
      return true if domains.include?(dom.upcase)

      $miq_ae_logger.warn("domain=#{dom} : is not viewable", :resource_id => @workspace.find_miq_request_id)
      false
    end

    def default_notification_type(values_hash)
      level = values_hash[:level] || "info"
      audience = values_hash[:audience] || "user"
      "automate_#{audience}_#{level}".downcase.to_sym
    end
  end
end