Katello/katello

View on GitHub
app/models/katello/ping.rb

Summary

Maintainability
B
5 hrs
Test Coverage
module Katello
  class Ping
    OK_RETURN_CODE = 'ok'.freeze
    FAIL_RETURN_CODE = 'FAIL'.freeze
    PACKAGES = %w(katello candlepin pulp foreman hammer dynflow).freeze

    class << self
      def services(capsule_id = nil)
        proxy = fetch_proxy(capsule_id)
        services = [:candlepin, :candlepin_auth, :foreman_tasks, :katello_events, :candlepin_events]
        services += [:pulp3, :pulp3_content] if proxy&.pulp3_enabled?
        if proxy.nil? || proxy.has_feature?(SmartProxy::PULP_NODE_FEATURE) || proxy.has_feature?(SmartProxy::PULP_FEATURE)
          services += [:pulp, :pulp_auth]
        end

        services
      end

      def ping(services: nil, capsule_id: nil)
        ping_services_for_capsule(services, capsule_id)
      end

      def ping!(services: nil, capsule_id: nil)
        result = ping_services_for_capsule(services, capsule_id)

        if result[:status] != OK_RETURN_CODE
          failed_names = failed_services(result).keys
          fail Katello::Errors::PingError, "The following services have not been started or are reporting errors: #{failed_names.join(', ')}"
        end

        result
      end

      def status
        {
          version: Katello::VERSION,
          timeUTC: Time.now.getutc
        }
      end

      def event_daemon_status(status, result)
        running = status&.dig(:running)

        if running
          result[:message] = "#{status[:processed_count].to_i} Processed, #{status[:failed_count].to_i} Failed"
        else
          result[:status] = FAIL_RETURN_CODE
          result[:message] = _("Not running")
        end
      end

      def ping_katello_events(result)
        exception_watch(result) do
          status = Katello::EventDaemon::Runner.service_status(:katello_events)
          event_daemon_status(status, result)
        end
      end

      def ping_candlepin_events(result)
        exception_watch(result) do
          status = Katello::EventDaemon::Runner.service_status(:candlepin_events)
          event_daemon_status(status, result)
        end
      end

      def ping_pulp3_without_auth(service_result, capsule_id)
        exception_watch(service_result) do
          Katello::Ping.pulp3_without_auth(fetch_proxy(capsule_id).pulp3_url)
        end
      end

      def ping_pulp3_content_without_auth(service_result, capsule_id)
        exception_watch(service_result) do
          Katello::Ping.pulp3_content_without_auth(fetch_proxy(capsule_id).pulp3_url)
        end
      end

      def ping_pulp_without_auth(service_result, capsule_id)
        exception_watch(service_result) do
          Katello::Ping.pulp_without_auth(pulp_url(capsule_id))
        end
      end

      def ping_candlepin_without_auth(service_result)
        url = SETTINGS[:katello][:candlepin][:url]
        exception_watch(service_result) do
          status = backend_status(url, :candlepin)
          check_candlepin_status(status)
        end
      end

      def ping_pulp_with_auth(service_result, pulp_without_auth_status, capsule_id)
        exception_watch(service_result) do
          if pulp_without_auth_status == OK_RETURN_CODE
            Katello::Pulp::Server.config(pulp_url(capsule_id), User.remote_user).resources.user.retrieve_all
          else
            fail _("Skipped pulp_auth check after failed pulp check")
          end
        end
      end

      def ping_candlepin_with_auth(service_result)
        exception_watch(service_result) do
          status = Katello::Resources::Candlepin::CandlepinPing.ping
          check_candlepin_status(status)
        end
      end

      def ping_foreman_tasks(service_result)
        exception_watch(service_result) do
          timeout   = 2
          world     = ForemanTasks.dynflow.world
          executors = world.coordinator.find_worlds(true)
          if executors.empty?
            fail _("foreman-tasks service not running or is not ready yet")
          end

          checks = executors.map { |executor| world.ping(executor.id, timeout) }
          checks.each(&:wait)
          if checks.any?(&:rejected?)
            fail _("some executors are not responding, check %{status_url}") % { :status_url => '/foreman_tasks/dynflow/status' }
          end
        end
      end

      def check_candlepin_status(status)
        if status[:mode] != 'NORMAL'
          fail _("Candlepin is not running properly")
        end
      end

      # check for exception - set the result code properly
      def exception_watch(result)
        start = Time.new
        result[:status] = OK_RETURN_CODE
        yield
        result[:duration_ms] = ((Time.new - start) * 1000).round.to_s
        result
      rescue => e
        Rails.logger.warn(e.backtrace ? [e.message, e.backtrace].join("\n") : e.message)
        result[:status] = FAIL_RETURN_CODE
        result[:message] = e.message
        result
      end

      # get package information for katello and its components
      def packages
        names = PACKAGES.join("|")
        packages = `rpm -qa | egrep "#{names}"`
        packages.split("\n").sort
      end

      def pulp_url(capsule_id)
        proxy = fetch_proxy(capsule_id)
        uri = URI.parse(proxy.pulp_url)
        "#{uri.scheme}://#{uri.host.downcase}/pulp/api/v2/"
      end

      # this checks Pulp is running and responding without need
      # for authentication. We don't use RestClient.options here
      # because it returns empty string, which is not enough to say
      # pulp is the one that responded
      def pulp_without_auth(url)
        json = backend_status(url, :pulp)

        fail _("Pulp does not appear to be running at %s.") % url if json.empty?

        if json['database_connection'] && json['database_connection']['connected'] != true
          fail _("Pulp database connection issue at %s.") % url
        end

        if json['messaging_connection'] && json['messaging_connection']['connected'] != true
          fail _("Pulp message bus connection issue at %s.") % url
        end

        unless all_pulp_workers_present?(json)
          fail _("Not all necessary pulp workers running at %s.") % url
        end

        json
      end

      def pulp3_without_auth(url)
        json = backend_status(url, :pulp)
        fail _("Pulp does not appear to be running at %s.") % url if json.empty?

        if json['database_connection'] && json['database_connection']['connected'] != true
          fail _("Pulp database connection issue at %s.") % url
        end

        if json['redis_connection'] && json['redis_connection']['connected'] != true
          fail _("Pulp redis connection issue at %s.") % url
        end

        workers = json["online_workers"] || []
        fail _("No pulpcore workers are running at %s.") % url if workers.empty?

        json
      end

      def pulp3_content_without_auth(url)
        json = backend_status(url, :pulp)
        fail _("Pulp does not appear to be running at %s.") % url if json.empty?

        content_apps = json["online_content_apps"] || []
        fail _("No pulpcore content apps are running at %s.") % url if content_apps.empty?

        json
      end

      def all_pulp_workers_present?(json)
        worker_ids = json["known_workers"].collect { |worker| worker["_id"] }
        return false unless worker_ids.any?
        scheduler = worker_ids.any? { |worker| worker.include?("scheduler@") }
        resource_manager = worker_ids.any? { |worker| worker.include?("resource_manager@") }
        reservered_resource_worker = worker_ids.any? { |worker| worker =~ /reserved_resource_worker-./ }
        scheduler && resource_manager && reservered_resource_worker
      end

      private

      def failed_services(result)
        result[:services].select do |_name, details|
          details[:status] != OK_RETURN_CODE
        end
      end

      def ping_services_for_capsule(services, capsule_id)
        services ||= self.services(capsule_id)
        result = {}
        services.each { |service| result[service] = {} }

        ping_pulp3_without_auth(result[:pulp3], capsule_id) if result.include?(:pulp3)
        ping_pulp3_content_without_auth(result[:pulp3_content], capsule_id) if result.include?(:pulp3_content)
        ping_pulp_without_auth(result[:pulp], capsule_id) if result.include?(:pulp)
        ping_candlepin_without_auth(result[:candlepin]) if result.include?(:candlepin)

        ping_pulp_with_auth(result[:pulp_auth], result[:pulp][:status], capsule_id) if result.include?(:pulp_auth)
        ping_candlepin_with_auth(result[:candlepin_auth]) if result.include?(:candlepin_auth)
        ping_foreman_tasks(result[:foreman_tasks]) if result.include?(:foreman_tasks)
        ping_katello_events(result[:katello_events]) if result.include?(:katello_events)
        ping_candlepin_events(result[:candlepin_events]) if result.include?(:candlepin_events)

        # set overall status result code
        result = {:services => result}
        result[:status] = result[:services].each_value.any? { |v| v[:status] == FAIL_RETURN_CODE } ? FAIL_RETURN_CODE : OK_RETURN_CODE
        result
      end

      def fetch_proxy(capsule_id)
        capsule_id ? SmartProxy.unscoped.find(capsule_id) : SmartProxy.pulp_primary
      end

      def backend_status(url, backend)
        ca_file = SETTINGS[:katello][backend][:ca_cert_file]
        request_id = ::Logging.mdc['request']

        options = {}
        options[:ssl_ca_file] = ca_file unless ca_file.nil?
        options[:verify_ssl] = SETTINGS[:katello][backend][:verify_ssl] if SETTINGS[:katello][backend].key?(:verify_ssl)
        options[:headers] = { 'Correlation-ID' => request_id } if request_id
        client = RestClient::Resource.new("#{url}/status/", options)

        response = client.get
        response.empty? ? {} : JSON.parse(response).with_indifferent_access
      end
    end
  end
end