Katello/katello

View on GitHub
app/lib/katello/concerns/base_template_scope_extensions.rb

Summary

Maintainability
D
1 day
Test Coverage
module Katello
  module Concerns
    module BaseTemplateScopeExtensions
      extend ActiveSupport::Concern
      extend ApipieDSL::Module

      apipie :class, 'Base macros related to content hosts to use within a template' do
        name 'Base Content'
        sections only: %w[all reports provisioning jobs partition_tables]
      end

      apipie :method, 'Returns an erratum by ID' do
        required :id, Integer, desc: 'Errata ID'
        returns Object, desc: 'The erratum object'
      end
      def errata(id)
        Katello::Erratum.in_repositories(Katello::Repository.readable).with_identifiers(id).map(&:attributes).first.slice!('created_at', 'updated_at')
      end

      apipie :method, 'Returns subscriptions for the host' do
        required :host, 'Host::Managed', desc: 'Host object to get subscriptions for'
        returns array_of: 'Subscription', desc: 'Array with subscription object'
      end
      def host_subscriptions(host)
        host.subscriptions
      end

      apipie :method, 'Returns subscription names for the host' do
        required :host, 'Host::Managed', desc: 'Host object to get subscriptions for'
        returns array_of: String, desc: "Array with names of host's subscriptions"
      end
      def host_subscriptions_names(host)
        host.subscriptions.map(&:name)
      end

      apipie :method, 'Returns Red Hat subscription names for the host' do
        required :host, 'Host::Managed', desc: 'Host object to get subscriptions for'
        returns array_of: String, desc: "Array with names of host's subscriptions"
      end
      def host_redhat_subscription_names(host)
        host.subscriptions.redhat.pluck(:name)
      end

      apipie :method, 'Returns the count of Red Hat subscriptions consumed for the host' do
        required :host, 'Host::Managed', desc: 'Host object to get subscriptions for'
        returns Integer, desc: "Count of consumed Red Hat subscriptions"
      end
      def host_redhat_subscriptions_consumed(host)
        presenter = ::Katello::HostSubscriptionsPresenter.new(host)
        presenter.subscriptions.select(&:redhat?).sum(&:quantity_consumed)
      end

      apipie :method, 'Returns content facet for the host' do
        desc "Content facet is an object containing the host's content-related metadata and associations"
        required :host, 'Host::Managed', desc: 'Host object to get content facet for'
        returns 'ContentFacet'
      end
      def host_content_facet(host)
        host.content_facet
      end

      apipie :method, 'Returns service level for the host' do
        required :host, 'Host::Managed', desc: 'Host object to get service level for'
        returns String, example: 'host_sla(host) #=> "Standard"'
      end
      def host_sla(host)
        host_subscription_facet(host)&.service_level
      end

      apipie :method, 'Returns installed products for the host' do
        required :host, 'Host::Managed', desc: 'Host object to get products for'
        returns array_of: 'Product', desc: "Array of installed product objects on the host"
      end
      def host_products(host)
        host_subscription_facet(host)&.installed_products
      end

      apipie :method, 'Returns installed product names for the host' do
        required :host, 'Host::Managed', desc: 'Host object to get products for'
        returns array_of: String, desc: 'Array with names of installed products on the host'
      end
      def host_products_names(host)
        products = host_products(host)
        if products
          products.map(&:name)
        else
          []
        end
      end

      apipie :method, 'Returns installed product names for the host with CP IDs' do
        required :host, 'Host::Managed', desc: 'Host object to get products for'
        returns array_of: String, desc: 'Array with names and CP IDs of installed products on the host'
      end
      def host_products_names_and_ids(host)
        products = host_products(host)
        if products
          products.collect { |product| "#{product.name} (#{product.cp_product_id})" }
        else
          []
        end
      end

      apipie :method, 'Returns the host collections the host belongs to' do
        required :host, 'Host::Managed', desc: 'Host object to get the host collections for'
        returns array_of: 'HostCollection', desc: "Array of the host collection objects the host belongs to"
      end
      def host_collections(host)
        host.host_collections
      end

      apipie :method, 'Returns names of the host collections the host belongs to' do
        required :host, 'Host::Managed', desc: 'Host object to get the host collections for'
        returns array_of: String, desc: 'Array with names of the host collections the host belongs to'
      end
      def host_collections_names(host)
        host.host_collections.map(&:name)
      end

      apipie :method, 'Returns subscription name of the pool' do
        required :pool, 'Katello::Pool', desc: 'Pool object to get subscription name of'
        returns String, desc: 'Name of the subscription'
      end
      def sub_name(pool)
        return unless pool
        pool.subscription&.name
      end

      apipie :method, 'Returns subscription SKU' do
        required :pool, 'Katello::Pool', desc: 'Pool object to get subscription SKU of'
        returns String, desc: "SKU's ID of the subscription"
      end
      def sub_sku(pool)
        return unless pool
        pool.subscription&.cp_id
      end

      apipie :method, 'Returns the smart proxy that the host was registered through' do
        required :host, 'Host::Managed', desc: 'Host object to get smart proxy of'
        returns String, desc: 'Hostname of the smart proxy'
      end
      def registered_through(host)
        host_subscription_facet(host)&.registered_through
      end

      apipie :method, 'Returns time the host was registered at' do
        required :host, 'Host::Managed', desc: 'Host object to get registration time of'
        returns 'ActiveSupport::TimeWithZone', desc: 'Registration time of the host'
      end
      def registered_at(host)
        host_subscription_facet(host)&.registered_at
      end

      apipie :method, 'Returns IDs of the applicable errata on the host' do
        required :host, 'Host::Managed', desc: 'Host object to get the applicable errata for'
        returns array_of: Integer, desc: 'Array with applicable errata IDs of the host'
      end
      def host_applicable_errata_ids(host)
        host.applicable_errata.pluck(:errata_id)
      end

      apipie :method, 'Returns IDs of the installable errata on the host' do
        required :host, 'Host::Managed', desc: 'Host object to get the installable errata for'
        returns array_of: Integer, desc: 'Array with installable errata IDs of the host'
      end
      def host_installable_errata_ids(host)
        return [] if host.content_facet.nil?
        host.installable_errata.pluck(:errata_id)
      end

      apipie :method, 'Returns filtered applicable errata for the host' do
        required :host, 'Host::Managed', desc: 'Host object to get the applicable errata for'
        optional :filter, String, desc: 'Filter to apply on applicable errata', default: ''
        returns array_of: 'Erratum', desc: 'Filtered applicable errata for the host'
      end
      def host_applicable_errata_filtered(host, filter = '')
        host.applicable_errata.includes(:cves).search_for(filter)
      end

      apipie :method, 'Returns filtered installable errata for the host' do
        required :host, 'Host::Managed', desc: 'Host object to get the installable errata for'
        optional :filter, String, desc: 'Filter to apply on installable errata', default: ''
        returns array_of: 'Erratum', desc: 'Filtered installable errata for the host'
      end
      def host_installable_errata_filtered(host, filter = '')
        return [] if host.content_facet.nil?
        host.installable_errata.includes(:cves).search_for(filter)
      end

      apipie :method, 'Returns version of the latest applicable RPM package' do
        required :host, 'Host::Managed', desc: 'Host object to get the applicable RPM package version on'
        required :package, String, desc: 'Name of the package'
        returns String, desc: 'Package version'
      end
      def host_latest_applicable_rpm_version(host, package)
        return [] if host.content_facet.nil?
        applicable = ::Katello::Rpm.latest(host.applicable_rpms.where(name: package))
        applicable.present? ? applicable.first.nvra : []
      end

      apipie :method, 'Returns version of the latest installable RPM package' do
        required :host, 'Host::Managed', desc: 'Host object to get the installable RPM package version on'
        required :package, String, desc: 'Name of the package'
        returns String, desc: 'Package version'
      end
      def host_latest_installable_rpm_version(host, package)
        return [] if host.content_facet.nil?
        installable = ::Katello::Rpm.latest(host.installable_rpms.where(name: package))
        installable.present? ? installable.first.nvra : []
      end

      apipie :method, 'Loads Pool objects' do
        desc 'This macro returns a collection of Pools matching search criteria.
          The collection is loaded in bulk, 1000 records at a time.'
        keyword :search, String, desc: 'A search term to limit the resulting collection, using standard search syntax', default: ''
        keyword :includes, Array, of: [String, Symbol], desc: 'An array of associations represented by strings or symbols, to be included in the SQL query. The list can be extended
          from plugins and can not be fully documented here. Most used associations are :subscription, :products, :organization', default: nil
        returns array_of: 'Pool', desc: 'The collection that can be iterated over using each_record'
        keyword :expiring_in_days, String, desc: "Return subscriptions expiring in the given number of days. Leave blank to return all subscriptions.", default: nil
      end
      def load_pools(search: '', includes: nil, expiring_in_days: nil)
        pools = Pool.readable
        if expiring_in_days
          pools = pools.expiring_in_days(expiring_in_days)
        end
        load_resource(klass: pools, search: search, permission: nil, includes: includes)
      end

      apipie :method, 'Returns the last time the host checked in via RHSM' do
        required :host, 'Host::Managed', desc: 'Host object to get the last time the host checked in'
        returns 'ActiveSupport::TimeWithZone', desc: 'The last time the host checked in via RHSM'
      end
      def last_checkin(host)
        host&.subscription_facet&.last_checkin
      end

      apipie :method, 'Load errata applications' do
        desc 'This macro returns a collection of task records relating to errata being applied.
          The collection is loaded in bulk, 1000 records at a time.'
        keyword :filter_errata_type, String, desc: "Errata type. One of: #{Katello::Erratum::TYPES.join(', ')}", default: nil
        keyword :include_last_reboot, String, desc: "Set to 'yes' to include the last reboot time of each host", default: 'yes'
        keyword :since, String, desc: 'Return errata applications after this date'
        keyword :up_to, String, desc: 'Return errata applications before this date'
        keyword :status, String, desc: 'Task status. One of: "pending", "success", "error", "warning"'
        keyword :host_filter, String, desc: 'A filter term to limit the resulting collection, using standard filter syntax', default: nil
        returns array_of: 'Erratum', desc: 'The collection that can be iterated over using each_record'
      end
      # rubocop:disable Metrics/MethodLength
      # rubocop:disable Metrics/AbcSize
      # rubocop:disable Metrics/CyclomaticComplexity
      # rubocop:disable Metrics/PerceivedComplexity
      def load_errata_applications(filter_errata_type: nil, include_last_reboot: 'yes', since: nil, up_to: nil, status: nil, host_filter: nil)
        result = []

        filter_errata_type = filter_errata_type.presence || 'all'
        search_up_to = up_to.present? ? "ended_at < \"#{up_to}\"" : nil
        search_since = since.present? ? "ended_at > \"#{since}\"" : nil
        search_result = status.present? && status != 'all' ? "result = #{status}" : nil
        labels = 'label ^ (Actions::Katello::Host::Erratum::Install, Actions::Katello::Host::Erratum::ApplicableErrataInstall)'
        select = 'foreman_tasks_tasks.*'

        if Katello.with_remote_execution?
          new_labels = 'label = Actions::RemoteExecution::RunHostJob AND remote_execution_feature.label ^ (katello_errata_install, katello_errata_install_by_search)'
          labels = [labels, new_labels].map { |label| "(#{label})" }.join(' OR ')
          select += ',template_invocations.id AS template_invocation_id'
        else
          select += ',NULL AS template_invocation_id'
        end

        search = [search_up_to, search_since, search_result, "state = stopped", labels].compact.join(' and ')

        tasks = load_resource(klass: ForemanTasks::Task,
                              permission: 'view_foreman_tasks',
                              select: select,
                              search: search)
        only_host_ids = ::Host.search_for(host_filter).pluck(:id) if host_filter

        # batch of 1_000 records
        tasks.each do |batch|
          @_tasks_input = {}
          @_tasks_errata_cache = {}
          seen_errata_ids = []
          seen_host_ids = []

          batch.each do |task|
            next if skip_task?(task)
            seen_errata_ids = (seen_errata_ids + parse_errata(task)).uniq
            seen_host_ids << get_task_input(task)['host']['id'].to_i if include_last_reboot == 'yes'
          end
          seen_host_ids &= only_host_ids if only_host_ids

          # preload errata in one query for this batch
          preloaded_errata = Katello::Erratum.where(:errata_id => seen_errata_ids).pluck(:errata_id, :errata_type, :issued)
          preloaded_hosts = ::Host.where(:id => seen_host_ids).includes(:reported_data)

          batch.each do |task|
            next if skip_task?(task)
            next unless only_host_ids.nil? || only_host_ids.include?(get_task_input(task)['host']['id'].to_i)
            parse_errata(task).each do |erratum_id|
              current_erratum = preloaded_errata.find { |k, _| k == erratum_id }
              next if current_erratum.nil?
              current_erratum_errata_type = current_erratum[1]
              current_erratum_issued = current_erratum.last

              if filter_errata_type != 'all'
                next unless filter_errata_type == current_erratum_errata_type
              end

              hash = {
                :date => task.ended_at,
                :hostname => get_task_input(task)['host']['name'],
                :erratum_id => erratum_id,
                :erratum_type => current_erratum_errata_type,
                :issued => current_erratum_issued,
                :status => task.result
              }

              if include_last_reboot == 'yes'
                # It is possible that we can't find the host if it has been deleted.
                hash[:last_reboot_time] = preloaded_hosts.find { |k, _| k.id == get_task_input(task)['host']['id'].to_i }&.uptime_seconds&.seconds&.ago
              end

              result << hash
            end
          end
        end

        result
      end
      # rubocop:enable Metrics/MethodLength

      apipie :method, 'Converts package version to be sortable' do
        required :version, String, desc: 'Version to convert'
        returns String, desc: 'Sortable version of a package'
        example 'For usage example please refer to **Host - compare content hosts packages** report template'
      end
      def sortable_version(version)
        Util::Package.sortable_version(version)
      end

      include Katello::ContentSourceHelper

      apipie :method, "Generate script to change a host's content source" do
        returns String
      end
      def configure_host_for_new_content_source(host, ca_cert)
        return missing_content_source(host) unless host.content_source

        prepare_ssl_cert(ca_cert) + configure_subman(host.content_source)
      end

      private

      def host_subscription_facet(host)
        host.subscription_facet
      end

      def skip_task?(task)
        # Skip task that doesn't apply errata
        input = get_task_input(task)
        input.blank? || input['host'].blank?
      end

      def get_task_input(task)
        @_tasks_input[task.id] ||= if task.label == 'Actions::Katello::Host::Erratum::ApplicableErrataInstall'
                                     task.execution_plan_action.all_planned_actions(Actions::Katello::Host::Erratum::Install).first.try(:input) || {}
                                   else
                                     task.input
                                   end
      end

      def parse_errata(task)
        task_input = get_task_input(task)
        agent_input = task_input['errata'] || task_input['content']
        # agent_input retrieves past katello-agent tasks.
        # There are multiple template inputs, such as errata, pre_script and post_script.
        # We only need the errata input here.
        @_tasks_errata_cache[task.id] ||= agent_input.presence || errata_ids_from_template_invocation(task, task_input)
      end

      def errata_ids_from_template_invocation(task, task_input)
        if task_input.key?('job_features') && task_input['job_features'].include?('katello_errata_install_by_search')
          # This may give wrong results if the template is not rendered yet
          # This also will not work for jobs run before we started storing
          #   resolved ids in the template
          script = task.execution_plan.actions[1].try(:input).try(:[], 'script') || ''
          found = script.lines.find { |line| line.start_with? '# RESOLVED_ERRATA_IDS=' } || ''
          (found.chomp.split('=', 2).last || '').split(',')
        else
          TemplateInvocationInputValue.joins(:template_input).where("template_invocation_id = ? AND template_inputs.name = ?", task.template_invocation_id, 'errata')
            .first&.value&.split(',') || []
        end
      end
    end
  end
end