From 9fe76a59c1c2d6c261422ec1001b8fc7c087dac0 Mon Sep 17 00:00:00 2001 From: Robert Waffen Date: Thu, 22 Jan 2026 17:27:38 +0100 Subject: [PATCH 1/4] refactor: improve health check handling and output structure in patch_batch plan --- plans/patch_batch.pp | 84 ++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/plans/patch_batch.pp b/plans/patch_batch.pp index 464e24a..5d30b99 100644 --- a/plans/patch_batch.pp +++ b/plans/patch_batch.pp @@ -55,58 +55,58 @@ } # get nodes that are 'clean' from health check results - $nodes_to_patch = ($health_checks.filter_set |$item| { $item.value['state'] == 'clean' }).map |$n| { $n.target } - $skipped_nodes = ($health_checks.filter_set |$item| { $item.status == 'failure' }).map |$n| { $n.target } + $nodes_to_patch = ($health_checks.filter_set |$item| { $item.value['state'] == 'clean' }).map |$n| { $n.target } + $health_check_failed = ($health_checks.filter_set |$item| { $item.status == 'failure' }).map |$n| { $n.target } if $debug { out::message('patch_batch.pp: Nodes to patch after health check:') out::message($nodes_to_patch) out::message('patch_batch.pp: Skipped nodes after health check:') - out::message($skipped_nodes) + out::message($health_check_failed) } + } else { + $nodes_to_patch = $batch + $health_check_failed = [] + } - $patching_result = run_task('os_patching::patch_server', $nodes_to_patch, - _catch_errors => $catch_errors, - clean_cache => $clean_cache, - dpkg_params => $dpkg_params, - reboot => $reboot, - security_only => $security_only, - timeout => $timeout, - yum_params => $yum_params, - zypper_params => $zypper_params, - ) + $patching_result = run_task('os_patching::patch_server', $nodes_to_patch, + _catch_errors => $catch_errors, + clean_cache => $clean_cache, + dpkg_params => $dpkg_params, + reboot => $reboot, + security_only => $security_only, + timeout => $timeout, + yum_params => $yum_params, + zypper_params => $zypper_params, + ) - if $debug { - out::message('patch_batch.pp: Patching results:') - out::message($patching_result) - } - } else { - $patching_result = run_task('os_patching::patch_server', $batch, - _catch_errors => $catch_errors, - clean_cache => $clean_cache, - dpkg_params => $dpkg_params, - reboot => $reboot, - security_only => $security_only, - timeout => $timeout, - yum_params => $yum_params, - zypper_params => $zypper_params, - ) + if $debug { + out::message('patch_batch.pp: Patching results:') + out::message($patching_result) + } - if $debug { - out::message('patch_batch.pp: Patching results:') - out::message($patching_result) - } + $no_patches = $patching_result.ok_set.filter |$item| { + $item.value['packages_updated'].empty + }.map |$n| { $n.target } + + $with_patches = $patching_result.ok_set.filter |$item| { + ! $item.value['packages_updated'].empty + }.map |$n| { $n.target } - $skipped_nodes = [] # No skipped nodes if health check is not run + $reboot_required = $patching_result.ok_set.filter |$item| { + $item.value['reboot_required'] == true + }.map |$n| { $n.target } + + $output = { + failed => $patching_result.error_set.names, + health_check => $run_health_check, + health_check_failed => $health_check_failed, + no_patches => $no_patches, + reboot_pattern => $reboot, + reboot_required => $reboot_required, + targets => $batch, + with_patches => $with_patches, } - return( - { - targets => $batch, - patched => $patching_result.ok_set.names, - failed => $patching_result.error_set.names, - skipped => $skipped_nodes, - health_check => $run_health_check, - } - ) + return($output) } From ece9e2922dfd48cb5be3484bd7e5e922f08439c0 Mon Sep 17 00:00:00 2001 From: Robert Waffen Date: Thu, 22 Jan 2026 17:27:53 +0100 Subject: [PATCH 2/4] refactor: enhance result structure in patch_group plan to include additional health check and reboot information --- plans/patch_group.pp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plans/patch_group.pp b/plans/patch_group.pp index 2ded4de..caf72f4 100644 --- a/plans/patch_group.pp +++ b/plans/patch_group.pp @@ -80,11 +80,14 @@ # Merge all batch results into a single hash by combining arrays $result = { - 'targets' => $batch_results.map |$r| { $r['targets'] }.flatten, - 'patched' => $batch_results.map |$r| { $r['patched'] }.flatten, - 'failed' => $batch_results.map |$r| { $r['failed'] }.flatten, - 'skipped' => $batch_results.map |$r| { $r['skipped'] }.flatten, - 'health_check' => $batch_results[0]['health_check'], + failed => $batch_results.map |$r| { $r['failed'] }.flatten, + health_check => $batch_results[0]['health_check'], + health_check_failed => $batch_results.map |$r| { $r['health_check_failed'] }.flatten, + no_patches => $batch_results.map |$r| { $r['no_patches'] }.flatten, + reboot_pattern => $batch_results[0]['reboot_pattern'], + reboot_required => $batch_results.map |$r| { $r['reboot_required'] }.flatten, + targets => $batch_results.map |$r| { $r['targets'] }.flatten, + with_patches => $batch_results.map |$r| { $r['with_patches'] }.flatten, } } else { out::message("patch_group.pp: Patching all targets at once: ${targets}") From 62a58e751fd3492d4ae06e436667e6f2a8eea4bf Mon Sep 17 00:00:00 2001 From: Robert Waffen Date: Thu, 22 Jan 2026 17:28:00 +0100 Subject: [PATCH 3/4] refactor: enhance result structure in patch_pql plan to include additional reboot and health check details --- plans/patch_pql.pp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plans/patch_pql.pp b/plans/patch_pql.pp index 1033402..dc8e0f0 100644 --- a/plans/patch_pql.pp +++ b/plans/patch_pql.pp @@ -78,11 +78,14 @@ # Merge all batch results into a single hash by combining arrays $result = { - 'targets' => $batch_results.map |$r| { $r['targets'] }.flatten, - 'patched' => $batch_results.map |$r| { $r['patched'] }.flatten, - 'failed' => $batch_results.map |$r| { $r['failed'] }.flatten, - 'skipped' => $batch_results.map |$r| { $r['skipped'] }.flatten, - 'health_check' => $batch_results[0]['health_check'], + failed => $batch_results.map |$r| { $r['failed'] }.flatten, + health_check => $batch_results[0]['health_check'], + health_check_failed => $batch_results.map |$r| { $r['health_check_failed'] }.flatten, + no_patches => $batch_results.map |$r| { $r['no_patches'] }.flatten, + reboot_pattern => $batch_results[0]['reboot_pattern'], + reboot_required => $batch_results.map |$r| { $r['reboot_required'] }.flatten, + targets => $batch_results.map |$r| { $r['targets'] }.flatten, + with_patches => $batch_results.map |$r| { $r['with_patches'] }.flatten, } } else { out::message('patch_pql.pp: Patching in batches is disabled') From dc73e19f64899d1a51207d4adaaadc51ea8ff73c Mon Sep 17 00:00:00 2001 From: Robert Waffen Date: Thu, 22 Jan 2026 17:29:22 +0100 Subject: [PATCH 4/4] refactor: update output functions to include logging and enhance reboot detection for Linux --- tasks/patch_server.rb | 64 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/tasks/patch_server.rb b/tasks/patch_server.rb index 9ae05bb..5ce9a74 100755 --- a/tasks/patch_server.rb +++ b/tasks/patch_server.rb @@ -19,7 +19,7 @@ # set paths/commands for windows fact_generation_script = 'C:/ProgramData/os_patching/os_patching_fact_generation.ps1' fact_generation_cmd = "#{ENV['systemroot']}/system32/WindowsPowerShell/v1.0/powershell.exe -ExecutionPolicy RemoteSigned -file #{fact_generation_script}" - puppet_cmd = "#{ENV['programfiles']}/Puppet Labs/Puppet/bin/puppet" + @puppet_cmd = "#{ENV['programfiles']}/Puppet Labs/Puppet/bin/puppet" shutdown_cmd = 'shutdown /r /t 60 /c "Rebooting due to the installation of updates by os_patching" /d p:2:17' else # not windows @@ -29,7 +29,7 @@ # set paths/commands for linux fact_generation_script = '/usr/local/bin/os_patching_fact_generation.sh' fact_generation_cmd = fact_generation_script - puppet_cmd = '/opt/puppetlabs/puppet/bin/puppet' + @puppet_cmd = '/opt/puppetlabs/puppet/bin/puppet' shutdown_cmd = 'nohup /sbin/shutdown -r +1 2>/dev/null 1>/dev/null &' ENV['LC_ALL'] = 'C' @@ -134,8 +134,35 @@ def pending_reboot_win end end +def pending_reboot_linux(log, starttime) + facts = gather_facts(log, starttime) + + # check for existence of /var/run/reboot-required file + if facts['os']['family'] == 'RedHat' + if File.file?('/usr/bin/needs-restarting') + _output, _stderr, status = Open3.capture3('/usr/bin/needs-restarting -r') + if status != 0 + return true + else + return false + end + else + log.warn 'needs-restarting command not found, cannot determine if reboot is required' + log.warn 'please install the yum-util/dnf-utils package to enable this functionality' + # needs-restart doesn't exist so assume it is not needed + return false + end + end + + if File.file?('/var/run/reboot-required') + true + else + false + end +end + # Default output function -def output(returncode, reboot, security, message, packages_updated, debug, job_id, pinned_packages, starttime) +def output(returncode, reboot, security, message, packages_updated, debug, job_id, pinned_packages, starttime, log) endtime = Time.now.iso8601 json = { :return => returncode, @@ -148,7 +175,12 @@ def output(returncode, reboot, security, message, packages_updated, debug, job_i :pinned_packages => pinned_packages, :start_time => starttime, :end_time => endtime, + :duration => Time.parse(endtime) - Time.parse(starttime) } + + json[:reboot_required] = pending_reboot_win if IS_WINDOWS + json[:reboot_required] = pending_reboot_linux(log, starttime) unless IS_WINDOWS + puts JSON.pretty_generate(json) history(starttime, message, returncode, reboot, security, job_id) end @@ -235,6 +267,15 @@ def reboot_required(family, release, reboot) end end +def gather_facts(log, starttime) + # Cache the facts + log.debug 'Gathering facts' + full_facts, stderr, status = Open3.capture3(@puppet_cmd, 'facts') + err(status, 'os_patching/facter', stderr, starttime) if status != 0 + + JSON.parse(full_facts) +end + # Parse input, get params in scope params = nil begin @@ -259,10 +300,7 @@ def reboot_required(family, release, reboot) end # Cache the facts -log.debug 'Gathering facts' -full_facts, stderr, status = Open3.capture3(puppet_cmd, 'facts') -err(status, 'os_patching/facter', stderr, starttime) if status != 0 -facts = JSON.parse(full_facts) +facts = gather_facts(log, starttime) if facts['os'] os = facts['os'] @@ -450,7 +488,7 @@ def reboot_required(family, release, reboot) if updatecount.zero? if reboot == 'always' log.error 'Rebooting' - output('Success', reboot, security_only, 'No patches to apply, reboot triggered', '', '', '', pinned_pkgs, starttime) + output('Success', reboot, security_only, 'No patches to apply, reboot triggered', '', '', '', pinned_pkgs, starttime, log) $stdout.flush log.info 'No patches to apply, rebooting as requested' p1 = if IS_WINDOWS @@ -460,7 +498,7 @@ def reboot_required(family, release, reboot) end Process.detach(p1) else - output('Success', reboot, security_only, 'No patches to apply', '', '', '', pinned_pkgs, starttime) + output('Success', reboot, security_only, 'No patches to apply', '', '', '', pinned_pkgs, starttime, log) log.info 'No patches to apply, exiting' end exit(0) @@ -535,7 +573,7 @@ def reboot_required(family, release, reboot) pkg_hash = {} end - output(yum_return, reboot, security_only, 'Patching complete', pkg_hash, output, job, pinned_pkgs, starttime) + output(yum_return, reboot, security_only, 'Patching complete', pkg_hash, output, job, pinned_pkgs, starttime, log) log.info 'Patching complete' elsif os['family'] == 'Debian' # Are we doing security only patching? @@ -556,7 +594,7 @@ def reboot_required(family, release, reboot) apt_std_out, stderr, status = Open3.capture3("#{deb_front} apt-get #{dpkg_params} -y #{deb_opts} #{apt_mode}") err(status, 'os_patching/apt', stderr, starttime) if status != 0 - output('Success', reboot, security_only, 'Patching complete', pkg_list, apt_std_out, '', pinned_pkgs, starttime) + output('Success', reboot, security_only, 'Patching complete', pkg_list, apt_std_out, '', pinned_pkgs, starttime, log) log.info 'Patching complete' elsif os['family'] == 'windows' # we're on windows @@ -615,7 +653,7 @@ def reboot_required(family, release, reboot) # output results # def output(returncode, reboot, security, message, packages_updated, debug, job_id, pinned_packages, starttime) - output('Success', reboot, security_only, 'Patching complete', update_titles, win_std_out.split("\n"), '', '', starttime) + output('Success', reboot, security_only, 'Patching complete', update_titles, win_std_out.split("\n"), '', '', starttime, log) elsif os['family'] == 'Suse' zypper_required_params = '--non-interactive --no-abbrev --quiet' @@ -635,7 +673,7 @@ def reboot_required(family, release, reboot) status, output = run_with_timeout("zypper #{zypper_required_params} #{zypper_params} update -t package #{zypper_cmd_params}", timeout, 2) err(status, 'os_patching/zypper', "zypper update returned non-zero (#{status}) : #{output}", starttime) if status != 0 end - output('Success', reboot, security_only, 'Patching complete', pkg_list, output, '', pinned_pkgs, starttime) + output('Success', reboot, security_only, 'Patching complete', pkg_list, output, '', pinned_pkgs, starttime, log) log.info 'Patching complete' log.debug "Timeout value set to : #{timeout}" else