11module Katello
22 module Concerns
3+ # rubocop:disable Metrics/ModuleLength
34 module BaseTemplateScopeExtensions
45 extend ActiveSupport ::Concern
56 extend ApipieDSL ::Module
@@ -210,96 +211,26 @@ def last_checkin(host)
210211 end
211212
212213 apipie :method , 'Load errata applications' do
213- desc 'This macro returns a collection of task records relating to errata being applied .
214+ desc 'This macro returns a collection of errata application records from the database .
214215 The collection is loaded in bulk, 1000 records at a time.'
215216 keyword :filter_errata_type , String , desc : "Errata type. One of: #{ Katello ::Erratum ::TYPES . join ( ', ' ) } " , default : nil
216217 keyword :include_last_reboot , String , desc : "Set to 'yes' to include the last reboot time of each host" , default : 'yes'
217218 keyword :since , String , desc : 'Return errata applications after this date'
218219 keyword :up_to , String , desc : 'Return errata applications before this date'
219- keyword :status , String , desc : 'Task status. One of: "pending ", "success", "error", "warning"'
220+ keyword :status , String , desc : 'Application status. One of: "all ", "success", "error", "warning", "cancelled"' , default : 'all '
220221 keyword :host_filter , String , desc : 'A filter term to limit the resulting collection, using standard filter syntax' , default : nil
221- returns array_of : 'Erratum' , desc : 'The collection that can be iterated over using each_record'
222+ returns array_of : Hash , desc : 'The collection that can be iterated over using each_record'
222223 end
223- # rubocop:disable Metrics/MethodLength
224- # rubocop:disable Metrics/AbcSize
225- # rubocop:disable Metrics/CyclomaticComplexity
226- # rubocop:disable Metrics/PerceivedComplexity
227- def load_errata_applications ( filter_errata_type : nil , include_last_reboot : 'yes' , since : nil , up_to : nil , status : nil , host_filter : nil )
228- result = [ ]
229-
230- filter_errata_type = filter_errata_type . presence || 'all'
231- search_up_to = up_to . present? ? "ended_at < \" #{ up_to } \" " : nil
232- search_since = since . present? ? "ended_at > \" #{ since } \" " : nil
233- search_result = status . present? && status != 'all' ? "result = #{ status } " : nil
234- labels = 'label ^ (Actions::Katello::Host::Erratum::Install, Actions::Katello::Host::Erratum::ApplicableErrataInstall)'
235-
236- new_labels = 'label = Actions::RemoteExecution::RunHostJob AND remote_execution_feature.label ^ (katello_errata_install, katello_errata_install_by_search)'
237- labels = [ labels , new_labels ] . map { |label | "(#{ label } )" } . join ( ' OR ' )
238-
239- search = [ search_up_to , search_since , search_result , "state = stopped" , labels ] . compact . join ( ' and ' )
240-
241- tasks = load_resource ( klass : ForemanTasks ::Task ,
242- permission : 'view_foreman_tasks' ,
243- joins : [ :template_invocation ] ,
244- preload : [ :template_invocation ] ,
245- search : search )
246- only_host_ids = ::Host . search_for ( host_filter ) . pluck ( :id ) if host_filter
247-
248- # batch of 1_000 records
249- tasks . each do |batch |
250- @_tasks_input = { }
251- @_tasks_errata_cache = { }
252- seen_errata_ids = [ ]
253- seen_host_ids = [ ]
254-
255- batch . each do |task |
256- next if skip_task? ( task )
257- seen_errata_ids = ( seen_errata_ids + parse_errata ( task ) ) . uniq
258- seen_host_ids << get_task_input ( task ) [ 'host' ] [ 'id' ] . to_i if include_last_reboot == 'yes'
259- end
260- seen_host_ids &= only_host_ids if only_host_ids
261-
262- # preload errata in one query for this batch
263- preloaded_errata = Katello ::Erratum . where ( :errata_id => seen_errata_ids ) . pluck ( :errata_id , :errata_type , :issued , :severity )
264- preloaded_hosts = ::Host . where ( :id => seen_host_ids ) . includes ( :reported_data )
265-
266- batch . each do |task |
267- next if skip_task? ( task )
268- next unless only_host_ids . nil? || only_host_ids . include? ( get_task_input ( task ) [ 'host' ] [ 'id' ] . to_i )
269- parse_errata ( task ) . each do |erratum_id |
270- current_erratum = preloaded_errata . find { |k , _ | k == erratum_id }
271- next if current_erratum . nil?
272- current_erratum_errata_type = current_erratum [ 1 ]
273- current_erratum_issued = current_erratum [ 2 ]
274- current_erratum_severity = current_erratum [ 3 ]
275-
276- if filter_errata_type != 'all' && !( filter_errata_type == current_erratum_errata_type )
277- next
278- end
279-
280- hash = {
281- :date => task . ended_at ,
282- :hostname => get_task_input ( task ) [ 'host' ] [ 'name' ] ,
283- :erratum_id => erratum_id ,
284- :erratum_type => current_erratum_errata_type ,
285- :issued => current_erratum_issued ,
286- :severity => current_erratum_severity ,
287- :status => task . result ,
288- }
289-
290- if include_last_reboot == 'yes'
291- # It is possible that we can't find the host if it has been deleted.
292- hash [ :last_reboot_time ] = preloaded_hosts . find { |k , _ | k . id == get_task_input ( task ) [ 'host' ] [ 'id' ] . to_i } &.uptime_seconds &.seconds &.ago
293- end
294-
295- result << hash
296- end
297- end
298- end
299-
300- result
224+ def load_errata_applications ( filter_errata_type : nil , include_last_reboot : 'yes' , since : nil , up_to : nil , status : 'all' , host_filter : nil )
225+ load_errata_applications_from_db (
226+ filter_errata_type : filter_errata_type ,
227+ include_last_reboot : include_last_reboot ,
228+ since : since ,
229+ up_to : up_to ,
230+ status : status ,
231+ host_filter : host_filter
232+ )
301233 end
302- # rubocop:enable Metrics/MethodLength
303234
304235 apipie :method , 'Converts package version to be sortable' do
305236 required :version , String , desc : 'Version to convert'
@@ -321,47 +252,131 @@ def configure_host_for_new_content_source(host, ca_cert)
321252 prepare_ssl_cert ( ca_cert ) + configure_subman ( host . content_source )
322253 end
323254
324- private
325-
326- def host_subscription_facet ( host )
327- host . subscription_facet
255+ apipie :method , 'Load errata applications from database' do
256+ desc 'This macro returns a collection of errata application records from the database.
257+ This is the new recommended approach that uses dedicated database tracking instead of parsing tasks.'
258+ keyword :filter_errata_type , String , desc : "Errata type. One of: #{ Katello ::Erratum ::TYPES . join ( ', ' ) } " , default : nil
259+ keyword :include_last_reboot , String , desc : "Set to 'yes' to include the last reboot time of each host" , default : 'yes'
260+ keyword :since , String , desc : 'Return errata applications after this date'
261+ keyword :up_to , String , desc : 'Return errata applications before this date'
262+ keyword :status , String , desc : 'Application status. One of: "all", "success", "error", "warning", "cancelled"' , default : 'all'
263+ keyword :host_filter , String , desc : 'A filter term to limit the resulting collection, using standard filter syntax' , default : nil
264+ returns array_of : Hash , desc : 'Array of hashes containing errata application data'
328265 end
266+ # rubocop:disable Metrics/MethodLength
267+ # rubocop:disable Metrics/AbcSize
268+ # rubocop:disable Metrics/CyclomaticComplexity
269+ # rubocop:disable Metrics/PerceivedComplexity
270+ def load_errata_applications_from_db ( filter_errata_type : nil , include_last_reboot : 'yes' , since : nil , up_to : nil , status : 'all' , host_filter : nil )
271+ result = [ ]
329272
330- def skip_task? ( task )
331- # Skip task that doesn't apply errata
332- input = get_task_input ( task )
333- input . blank? || input [ 'host' ] . blank?
334- end
273+ authorized_hosts = ::Host ::Managed . authorized ( 'view_hosts' ) . select ( :id )
335274
336- def get_task_input ( task )
337- @_tasks_input [ task . id ] ||= if task . label == 'Actions::Katello::Host::Erratum::ApplicableErrataInstall'
338- task . execution_plan_action . all_planned_actions ( Actions ::Katello ::Host ::Erratum ::Install ) . first . try ( :input ) || { }
339- else
340- task . input
341- end
342- end
275+ # Build scoped search query for date and status filters
276+ search_conditions = [ ]
277+ search_conditions << "applied_at > \" #{ since } \" " if since . present?
278+ search_conditions << "applied_at < \" #{ up_to } \" " if up_to . present?
343279
344- def parse_errata ( task )
345- task_input = get_task_input ( task )
346- agent_input = task_input [ 'errata' ] || task_input [ 'content' ]
347- # agent_input retrieves past katello-agent tasks.
348- # There are multiple template inputs, such as errata, pre_script and post_script.
349- # We only need the errata input here.
350- @_tasks_errata_cache [ task . id ] ||= agent_input . presence || errata_ids_from_template_invocation ( task , task_input )
351- end
280+ if status . present? && status != 'all'
281+ return [ ] if status . to_s . casecmp ( 'pending' ) . zero?
282+ search_conditions << "status = #{ status . to_s . downcase } "
283+ end
284+
285+ applications = if search_conditions . any?
286+ Katello ::ErrataApplication . where ( host_id : authorized_hosts )
287+ . search_for ( search_conditions . join ( ' and ' ) )
288+ else
289+ Katello ::ErrataApplication . where ( host_id : authorized_hosts )
290+ end
291+
292+ if host_filter . present?
293+ applications = applications . where ( host_id : ::Host . search_for ( host_filter ) . select ( :id ) )
294+ end
352295
353- def errata_ids_from_template_invocation ( task , task_input )
354- if task_input . key? ( 'job_features' ) && task_input [ 'job_features' ] . include? ( 'katello_errata_install_by_search' )
355- # This may give wrong results if the template is not rendered yet
356- # This also will not work for jobs run before we started storing
357- # resolved ids in the template
358- script = task . execution_plan . actions [ 1 ] . try ( :input ) . try ( :[] , 'script' ) || ''
359- found = script . lines . find { |line | line . start_with? '# RESOLVED_ERRATA_IDS=' } || ''
360- ( found . chomp . split ( '=' , 2 ) . last || '' ) . split ( ',' )
296+ # Preload hosts
297+ if include_last_reboot == 'yes'
298+ applications = applications . includes ( host : :reported_data )
361299 else
362- TemplateInvocationInputValue . joins ( :template_input ) . where ( "template_invocation_id = ? AND template_inputs.name = ?" , task . template_invocation . id , 'errata' )
363- . first &.value &.split ( ',' ) || [ ]
300+ applications = applications . includes ( :host )
301+ end
302+
303+ # Fetch host_ids and errata_ids in a single query
304+ application_data = applications . pluck ( :host_id , :errata_ids )
305+ host_ids = application_data . map ( &:first ) . uniq
306+ all_errata_ids = application_data . map ( &:last ) . flatten . uniq
307+
308+ # Preload all errata to avoid N+1 queries (join on errata_id string)
309+ errata_by_errata_id = Katello ::Erratum . where ( errata_id : all_errata_ids ) . index_by ( &:errata_id )
310+
311+ # Fetch applicability only for errata that were applied
312+ applicable_data = Katello ::Host ::ContentFacet
313+ . where ( host_id : host_ids )
314+ . joins ( :applicable_errata )
315+ . where ( 'katello_errata.errata_id' => all_errata_ids )
316+ . pluck ( :host_id , 'katello_errata.errata_id' )
317+
318+ applicable_errata_map =
319+ applicable_data
320+ . group_by ( &:first )
321+ . transform_values { |pairs | pairs . map ( &:last ) }
322+
323+ # Process each application record
324+ applications . find_each ( batch_size : 1000 ) do |app |
325+ # Get applicable erratum IDs for this host (from pre-built map)
326+ applicable_erratum_ids = applicable_errata_map [ app . host_id ] || [ ]
327+
328+ # Create one output row per erratum
329+ app . errata_ids . each do |erratum_string_id |
330+ erratum = errata_by_errata_id [ erratum_string_id ]
331+
332+ # If erratum doesn't exist in DB (deleted/disabled repo), still create output with limited data
333+ hash = if erratum
334+ {
335+ date : app . applied_at ,
336+ hostname : app . host . name ,
337+ erratum_id : erratum . errata_id ,
338+ erratum_type : erratum . errata_type ,
339+ issued : erratum . issued ,
340+ severity : erratum . severity ,
341+ status : app . status ,
342+ still_applicable : applicable_erratum_ids . include? ( erratum_string_id ) ,
343+ }
344+ else
345+ # Erratum no longer in system, but we still have the application record
346+ {
347+ date : app . applied_at ,
348+ hostname : app . host . name ,
349+ erratum_id : erratum_string_id ,
350+ erratum_type : nil ,
351+ issued : nil ,
352+ severity : nil ,
353+ status : app . status ,
354+ still_applicable : false ,
355+ }
356+ end
357+
358+ # Filter by errata type if specified (skip if we don't have the erratum object or type doesn't match)
359+ next if filter_errata_type . present? && filter_errata_type != 'all' && ( erratum . nil? || erratum . errata_type != filter_errata_type )
360+
361+ if include_last_reboot == 'yes'
362+ hash [ :last_reboot_time ] = app . host &.uptime_seconds &.seconds &.ago
363+ end
364+
365+ result << hash
366+ end
364367 end
368+
369+ result
370+ end
371+ # rubocop:enable Metrics/MethodLength
372+ # rubocop:enable Metrics/AbcSize
373+ # rubocop:enable Metrics/CyclomaticComplexity
374+ # rubocop:enable Metrics/PerceivedComplexity
375+
376+ private
377+
378+ def host_subscription_facet ( host )
379+ host . subscription_facet
365380 end
366381 end
367382 end
0 commit comments