Skip to content

Commit 93602b9

Browse files
author
Kevin Reeuwijk
authored
v0.4.0 - Rewrote reboot behavior (#14)
* new reboot_if_pending type * fix bug in is_patchday() logic * tag more resources * use anchors for dependencies * use onlyif for conditional execs * refactor & deduplicate * strip cmd_opts * pre-patch reboot only when allowed * default patch windows to "reboot: ifneeded" * Update version, readme and changelog
1 parent 01979a3 commit 93602b9

File tree

13 files changed

+344
-107
lines changed

13 files changed

+344
-107
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## Release 0.4.0
6+
7+
**Features**
8+
- Completely rewrote the reboot behavior, so that pending reboot detections fully works both before patching and after patching, in the same Puppet run. There is no more dependency on the `reboots.reboot_required` portion of the `pe_patch`/`os_patching` fact, all logic is now internal and no longer requires multiple Puppet runs.
9+
- Changed the default schedules to `reboot: ifneeded` (was `reboot: always`), now that the pending reboot logic has improved so much
10+
- Ensured that pre_reboot commands will now trigger when necessary (only one scenario can happen at a time):
11+
- when an OS pending reboot is detected at the start of a run (before patching)
12+
- when an OS pending reboot is detected at the end of a run (after patching)
13+
- Forced pre_reboot commands (which are essentially Exec resources) to use the `posix` provider on Linux and the `powershell` provider on Windows, so that the pending reboot detection logic can be injected to the resource dynamically.
14+
515
## Release 0.3.0
616

717
**Features**

README.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ This module will leverage the fact data provided by either the [albatrossflavour
2828
Once available patches are known via the above facts, the module will install the patches during the configured patch window.
2929
* For Linux operating systems, this happens through the native Package resource.
3030
* For Windows operating systems, this happens through the `windows_updates::kb` class, which requires the [noma4i/windows_updates](https://forge.puppet.com/noma4i/windows_updates) module to be installed.
31-
* By default, a reboot is performed at the end of a patch run that actually installed patches. You can change this behavior though.
32-
* You can define pre-patch, post-patch and pre-reboot commands for patching runs.
31+
* By default, a reboot is only performed when necessary at the end of a patch run that actually installed patches. You can change this behavior though, to either always reboot or never reboot.
32+
* You can define pre-patch, post-patch and pre-reboot commands for patching runs. We recommend that for Windows, you use Powershell-based commands for these. Specifically for pre-reboot commands on Windows, you *must* use Powershell-based commands.
3333

3434
### Setup Requirements
3535

@@ -51,7 +51,7 @@ or
5151
class {'patching_as_code':}
5252
```
5353
This enables automatic detection of available patches, and puts all the nodes in the `primary` patch group.
54-
By default this will patch your systems on the 3rd Friday of the month, between 22:00 and midnight (00:00), and perform a reboot.
54+
By default this will patch your systems on the 3rd Friday of the month, between 22:00 and midnight (00:00), and perform a reboot if necessary.
5555
On PE 2019.8 or newer this will not automatically classify the `pe_patch` class, so that you can control this through PE's builtin "PE Patch Management" node groups.
5656

5757
To allow patching_as_code to control & declare the `pe_patch` class, change the declaration to:
@@ -60,7 +60,7 @@ class {'patching_as_code':
6060
classify_pe_patch => true
6161
}
6262
```
63-
This will change the behavior to also declare the `pe_patch` class, and match its `patch_group` parameter with this module's `patch_group` parameter. In this scenario, make sure you do not classify your nodes with `pe_patch` via the node groups or other means.
63+
This will change the behavior to also declare the `pe_patch` class, and match its `patch_group` parameter with this module's `patch_group` parameter. In this scenario, make sure you do not classify your nodes with `pe_patch` via the "PE Patch Management" node groups or other means.
6464

6565
## Usage
6666

@@ -71,15 +71,15 @@ patching_as_code::patch_group: early
7171
```
7272
The module provides 5 patch groups out of the box:
7373
```
74-
testing: patches every 2nd Thursday of the month, between 07:00 and 09:00, performs a reboot
75-
early: patches every 3rd Monday of the month, between 20:00 and 22:00, performs a reboot
76-
primary: patches every 3rd Friday of the month, between 22:00 and 00:00, performs a reboot
77-
secondary: patches every 3rd Saturday of the month, between 22:00 and 00:00, performs a reboot
78-
late: patches every 4th Saturday of the month, between 22:00 and 00:00, performs a reboot
74+
testing: patches every 2nd Thursday of the month, between 07:00 and 09:00, performs a reboot if needed
75+
early: patches every 3rd Monday of the month, between 20:00 and 22:00, performs a reboot if needed
76+
primary: patches every 3rd Friday of the month, between 22:00 and 00:00, performs a reboot if needed
77+
secondary: patches every 3rd Saturday of the month, between 22:00 and 00:00, performs a reboot if needed
78+
late: patches every 4th Saturday of the month, between 22:00 and 00:00, performs a reboot if needed
7979
```
8080
There are also 2 special built-in patch groups:
8181
```
82-
always: patches immediately when a patch is available, can patch in any agent run, performs a reboot
82+
always: patches immediately when a patch is available, can patch in any agent run, performs a reboot if needed
8383
never: never performs any patching and does not reboot
8484
```
8585

@@ -177,8 +177,8 @@ You can control additional commands that get executed at specific times, to faci
177177
2) Run pre-patching commands
178178
3) Install patches
179179
4) Run post-patching commands
180-
5) If reboots are enabled, run pre-reboot commands
181-
6) If reboots are enabled, reboot system
180+
5) If reboots are enabled, run pre-reboot commands (if a reboot is pending, or when reboots are set to `always`)
181+
6) If reboots are enabled, reboot system (if a reboot is pending, or when reboots are set to `always`)
182182

183183
To define the pre/post-patching and pre-reboot commands, you need to create hashes in Hiera. The commands will be executed as `Exec` resources, and you can use any of the [allowed attributes](https://puppet.com/docs/puppet/6.17/types/exec.html#exec-attributes) for that resource (just don't use metaparameters). There are 3 hashes you can define:
184184
```
@@ -203,6 +203,8 @@ patching_as_code::pre_patch_commands:
203203
```
204204
As you can see, it's just like defining `Exec` resources.
205205

206+
Note that specifically for `patching_as_code::pre_reboot_commands`, the `provider:`, `onlyif:` and `unless:` parameters will be ignored, as these are overwritten by the internal logic to detect pending reboots. On Linux the `provider:` is forced to `posix`, on Windows it is forced to `powershell`.
207+
206208
## Limitations
207209

208210
This solution will patching to initiate whenever an agent run occurs inside the patch window. On Windows, patch runs for Cumulative Updates can take a long time, so you may want to tune the hours of your patch windows to account for a patch run getting started near the end of the window and still taking a significant amount of time.

REFERENCE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ Options:
113113

114114
* **:command** `String`: The pre-reboot command to execute
115115
* **:path** `String`: The path for the command
116-
* **:provider** `String`: The provider for the command
116+
117+
Note: the provider for the command gets forced to `posix` on Linux and `powershell` on Windows
117118

118119
##### `use_pe_patch`
119120

data/common.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,31 @@ patching_as_code::patch_schedule:
66
count_of_week: 2
77
hours: 07:00 - 09:00
88
max_runs: 4
9-
reboot: always
9+
reboot: ifneeded
1010
early:
1111
day_of_week: Monday
1212
count_of_week: 3
1313
hours: 20:00 - 22:00
1414
max_runs: 4
15-
reboot: always
15+
reboot: ifneeded
1616
primary:
1717
day_of_week: Friday
1818
count_of_week: 3
1919
hours: 22:00 - 00:00
2020
max_runs: 4
21-
reboot: always
21+
reboot: ifneeded
2222
secondary:
2323
day_of_week: Saturday
2424
count_of_week: 3
2525
hours: 22:00 - 00:00
2626
max_runs: 4
27-
reboot: always
27+
reboot: ifneeded
2828
late:
2929
day_of_week: Saturday
3030
count_of_week: 4
3131
hours: 22:00 - 00:00
3232
max_runs: 4
33-
reboot: always
33+
reboot: ifneeded
3434

3535
patching_as_code::blocklist: []
3636
patching_as_code::allowlist: []

functions/is_patchday.pp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function patching_as_code::is_patchday(
1919
$som_weekday = Integer($startofmonth.strftime('%u'))
2020

2121
# Calculate first occurence of same weekday
22-
if $day_number - $som_weekday <= 0 {
22+
if $day_number - $som_weekday < 0 {
2323
$firstocc = 1 + 7 + $day_number - $som_weekday
2424
} else {
2525
$firstocc = 1 + $day_number - $som_weekday
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
function Get-PendingReboot {
2+
#Copied from http://ilovepowershell.com/2015/09/10/how-to-check-if-a-server-needs-a-reboot/
3+
#Adapted from https://gist.github.com/altrive/5329377
4+
#Based on <http://gallery.technet.microsoft.com/scriptcenter/Get-PendingReboot-Query-bdb79542>
5+
6+
$rebootPending = $false
7+
8+
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { $rebootPending = $true }
9+
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { $rebootPending = $true }
10+
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { $rebootPending = $true }
11+
try {
12+
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
13+
$status = $util.DetermineIfRebootPending()
14+
if (($null -ne $status) -and $status.RebootPending) {
15+
$rebootPending = $true
16+
}
17+
}
18+
catch { }
19+
20+
# return result
21+
$rebootPending
22+
}
23+
24+
Get-PendingReboot
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
if [ -f '/usr/bin/needs-restarting' ]
2+
then
3+
case $(facter os.release.major) in
4+
7|8)
5+
/usr/bin/needs-restarting -r 2>/dev/null 1>/dev/null
6+
if [ $? -eq 0 ]
7+
then
8+
echo "true"
9+
fi
10+
;;
11+
6)
12+
/usr/bin/needs-restarting 2>/dev/null 1>/dev/null
13+
if [ $? -gt 0 ]
14+
then
15+
echo "true"
16+
fi
17+
;;
18+
esac
19+
fi
20+
21+
if [ $(facter osfamily) = 'Debian' ] || [ $(facter osfamily) = 'Suse' ]
22+
then
23+
if [ -f '/var/run/reboot-required' ]
24+
then
25+
echo "true"
26+
fi
27+
fi

lib/puppet/type/patch_package.rb

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,6 @@
1212
desc 'Puppet schedule to link package resource to'
1313
end
1414

15-
newparam(:cache_clean, array_matching: :all) do
16-
desc 'Name of Exec resource to clean cache'
17-
18-
munge do |values|
19-
values = [values] unless values.is_a? Array
20-
values
21-
end
22-
end
23-
2415
newparam(:triggers, array_matching: :all) do
2516
desc 'Resources to notify after updating the package'
2617

@@ -32,7 +23,7 @@
3223

3324
# All parameters are required
3425
validate do
35-
[:name, :patch_window, :cache_clean, :triggers].each do |param|
26+
[:name, :patch_window, :triggers].each do |param|
3627
raise Puppet::Error, "Required parameter missing: #{param}" unless @parameters[param]
3728
end
3829
end
@@ -41,14 +32,6 @@
4132
# If package is found, update resource one-time for patching
4233
# If package is not found, create a one-time package resource
4334
def pre_run_check
44-
# Validate :cache_clean
45-
cache_clean = parameter(:cache_clean)
46-
cache_clean.value.each do |res|
47-
retrieve_resource_reference(res)
48-
rescue ArgumentError => e
49-
raise Puppet::Error, "Parameter cache_clean failed: #{e} at #{@file}:#{@line}"
50-
end
51-
5235
# Validate :triggers
5336
triggers = parameter(:triggers)
5437
triggers.value.each do |res|
@@ -71,20 +54,21 @@ def pre_run_check
7154
Puppet.send('notice', "#{package_res} (managed) will be updated by Patching_as_code")
7255
catalog.resource(package_res)['ensure'] = 'latest'
7356
catalog.resource(package_res)['schedule'] = self[:patch_window]
74-
catalog.resource(package_res)['require'] = Array(catalog.resource(package_res)['require']) + cache_clean.value
57+
catalog.resource(package_res)['before'] = Array(catalog.resource(package_res)['before']) + ['Anchor[patching_as_code::patchday::end]']
58+
catalog.resource(package_res)['require'] = Array(catalog.resource(package_res)['require']) + ['Anchor[patching_as_code::patchday::start]']
7559
catalog.resource(package_res)['notify'] = Array(catalog.resource(package_res)['notify']) + triggers.value
7660
else
7761
Puppet.send('notice', "#{package_res} (managed) will not be updated by Patching_as_code, due to the package enforcing a specific version")
7862
end
7963
else
8064
Puppet.send('notice', "#{package_res} (unmanaged) will be updated by Patching_as_code")
81-
catalog.add_resource(Puppet::Type.type('package').new(
82-
title: package,
83-
ensure: 'latest',
84-
schedule: self[:patch_window],
85-
require: cache_clean.value,
86-
notify: triggers.value,
87-
))
65+
catalog.create_resource('package',
66+
title: package,
67+
ensure: 'latest',
68+
schedule: self[:patch_window],
69+
before: 'Anchor[patching_as_code::patchday::end]',
70+
require: 'Anchor[patching_as_code::patchday::start]',
71+
notify: triggers.value)
8872
end
8973
end
9074

lib/puppet/type/reboot_if_pending.rb

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# rubocop:disable Style/FrozenStringLiteralComment
2+
# rubocop:enable Style/FrozenStringLiteralComment
3+
Puppet::Type.newtype(:reboot_if_pending) do
4+
@doc = 'Perform a clean reboot if it was pending before this agent run'
5+
6+
newparam(:name) do
7+
isnamevar
8+
desc 'Name of this resource (has no function)'
9+
end
10+
11+
newparam(:patch_window) do
12+
desc 'Puppet schedule to link the reboot resource to'
13+
end
14+
15+
newparam(:os) do
16+
desc 'OS type from kernel fact'
17+
end
18+
19+
# All parameters are required
20+
validate do
21+
[:name, :patch_window, :os].each do |param|
22+
raise Puppet::Error, "Required parameter missing: #{param}" unless @parameters[param]
23+
end
24+
end
25+
26+
# Add a reboot resource to the catalog if a pending reboot is detected
27+
def pre_run_check
28+
# Check for pending reboots
29+
pending_reboot = false
30+
kernel = parameter(:os).value.downcase
31+
case kernel
32+
when 'windows'
33+
sysroot = ENV['SystemRoot']
34+
powershell = "#{sysroot}\\system32\\WindowsPowerShell\\v1.0\\powershell.exe"
35+
# get the script path relative to the Puppet Type
36+
checker_script = File.join(
37+
__dir__,
38+
'..',
39+
'..',
40+
'patching_as_code',
41+
'pending_reboot.ps1',
42+
)
43+
pending_reboot = Puppet::Util::Execution.execute("#{powershell} -ExecutionPolicy Unrestricted -File #{checker_script}").chomp.to_s.downcase == 'true'
44+
when 'linux'
45+
# get the script path relative to the Puppet Type
46+
checker_script = File.join(
47+
__dir__,
48+
'..',
49+
'..',
50+
'patching_as_code',
51+
'pending_reboot.sh',
52+
)
53+
pending_reboot = Puppet::Util::Execution.execute("/bin/sh #{checker_script}").chomp.to_s.downcase == 'true'
54+
else
55+
raise Puppet::Error, "Patching as Code - Unsupported Operating System type: #{kernel}"
56+
end
57+
return unless pending_reboot
58+
59+
Puppet.send('notice', 'Patching as Code - Pending OS reboot detected, node will reboot at start of patch window today')
60+
## Reorganize dependencies for pre-patch, post-patch and pre-reboot exec resources:
61+
pre_patch_resources = []
62+
post_patch_resources = []
63+
pre_reboot_resources = []
64+
catalog.resources.each do |res|
65+
next unless res.type.to_s == 'exec'
66+
next unless res['tag'].is_a? Array
67+
next unless (res['tag'] & ['patching_as_code_pre_patching', 'patching_as_code_post_patching', 'patching_as_code_pre_reboot']).any?
68+
69+
if res['tag'].include?('patching_as_code_pre_patching')
70+
pre_patch_resources << res
71+
elsif res['tag'].include?('patching_as_code_post_patching')
72+
post_patch_resources << res
73+
elsif res['tag'].include?('patching_as_code_pre_reboot')
74+
pre_reboot_resources << res
75+
end
76+
end
77+
## pre-patch resources should gain Reboot[Patching as Code - Pending OS reboot] for require
78+
pre_patch_resources.each do |res|
79+
catalog.resource(res.to_s)['require'] = Array(catalog.resource(res.to_s)['require']) << 'Reboot[Patching as Code - Pending OS reboot]'
80+
end
81+
## post-patch resources should lose existing before dependencies
82+
post_patch_resources.each do |res|
83+
catalog.resource(res.to_s)['before'] = []
84+
end
85+
## pre-reboot resources should lose existing dependencies
86+
pre_reboot_resources.each do |res|
87+
catalog.resource(res.to_s)['require'] = []
88+
catalog.resource(res.to_s)['before'] = []
89+
end
90+
91+
catalog.add_resource(Puppet::Type.type('reboot').new(
92+
title: 'Patching as Code - Pending OS reboot',
93+
apply: 'immediately',
94+
schedule: parameter(:patch_window).value,
95+
before: "Class[patching_as_code::#{kernel}::patchday]",
96+
require: pre_reboot_resources,
97+
))
98+
99+
catalog.add_resource(Puppet::Type.type('notify').new(
100+
title: 'Patching as Code - Performing Pending OS reboot before patching...',
101+
schedule: parameter(:patch_window).value,
102+
notify: 'Reboot[Patching as Code - Pending OS reboot]',
103+
before: "Class[patching_as_code::#{kernel}::patchday]",
104+
require: pre_reboot_resources,
105+
))
106+
end
107+
108+
def retrieve_resource_reference(res)
109+
case res
110+
when Puppet::Type # rubocop:disable Lint/EmptyWhen
111+
when Puppet::Resource # rubocop:disable Lint/EmptyWhen
112+
when String
113+
begin
114+
Puppet::Resource.new(res)
115+
rescue ArgumentError
116+
raise ArgumentError, "#{res} is not a valid resource reference"
117+
end
118+
else
119+
raise ArgumentError, "#{res} is not a valid resource reference"
120+
end
121+
122+
resource = catalog.resource(res.to_s)
123+
124+
raise ArgumentError, "#{res} is not in the catalog" unless resource
125+
126+
resource
127+
end
128+
end

0 commit comments

Comments
 (0)