diff --git a/REFERENCE.md b/REFERENCE.md
index 471910e..c8289e9 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -21,6 +21,9 @@ the `os_patching` fact.
[puppet health check](https://forge.puppet.com/albatrossflavour/puppet_health_check)
module to perform a pre-check on the nodes you're planning to patch. If the nodes pass the
check, they get patched
+* [`os_patching::patch_batch`](#os_patching--patch_batch): Patch nodes in a batch, is called from patch_group plan or patch_pql plan
+* [`os_patching::patch_group`](#os_patching--patch_group): Patch nodes collected by a fact group
+* [`os_patching::patch_pql`](#os_patching--patch_pql): Patch nodes collected by a PQL query
## Classes
@@ -43,6 +46,7 @@ class { 'os_patching':
'end' => '2019-01-15T23:59:59+10:00',
},
},
+ group => 'patching01',
}
```
@@ -65,6 +69,7 @@ class profiles::soe::patching (
patch_window => $patch_window,
reboot_override => $reboot_override,
blackout_windows => $full_blackout_windows,
+ group => 'patching01',
}
}
```
@@ -119,6 +124,7 @@ The following parameters are available in the `os_patching` class:
* [`windows_update_interval_mins`](#-os_patching--windows_update_interval_mins)
* [`fact_mode`](#-os_patching--fact_mode)
* [`ensure`](#-os_patching--ensure)
+* [`group`](#-os_patching--group)
##### `puppet_binary`
@@ -212,7 +218,7 @@ This overrides the setting in the task
##### `patch_window`
-Data type: `Optional[String]`
+Data type: `Optional[Pattern[/^[A-Za-z0-9\-_ ]+$/]]`
A freeform text entry used to allocate a node to a specific patch window (Optional)
@@ -296,6 +302,14 @@ Data type: `Enum['present', 'absent']`
`present` to install scripts, cronjobs, files, etc, `absent` to cleanup a system that previously hosted us
+##### `group`
+
+Data type: `Optional[Pattern[/^[A-Za-z0-9\-_ ]+$/]]`
+
+The group to assign the node for patching purposes.
+
+Default value: `undef`
+
## Tasks
### `clean_cache`
@@ -399,3 +413,197 @@ Data type: `Optional[Integer]`
Default value: `1800`
+### `os_patching::patch_batch`
+
+Patch nodes in a batch, is called from patch_group plan or patch_pql plan
+
+#### Parameters
+
+The following parameters are available in the `os_patching::patch_batch` plan:
+
+* [`batch`](#-os_patching--patch_batch--batch)
+* [`catch_errors`](#-os_patching--patch_batch--catch_errors)
+* [`noop_state`](#-os_patching--patch_batch--noop_state)
+* [`run_health_check`](#-os_patching--patch_batch--run_health_check)
+* [`service_enabled`](#-os_patching--patch_batch--service_enabled)
+* [`service_running`](#-os_patching--patch_batch--service_running)
+* [`runinterval`](#-os_patching--patch_batch--runinterval)
+* [`debug`](#-os_patching--patch_batch--debug)
+
+##### `batch`
+
+Data type: `TargetSpec`
+
+The batch of nodes to patch
+
+##### `catch_errors`
+
+Data type: `Boolean`
+
+Whether to catch errors during task execution
+
+Default value: `true`
+
+##### `noop_state`
+
+Data type: `Boolean`
+
+Whether to consider noop state during health check
+
+Default value: `false`
+
+##### `run_health_check`
+
+Data type: `Boolean`
+
+Whether to run a health check before patching
+
+Default value: `false`
+
+##### `service_enabled`
+
+Data type: `Boolean`
+
+Whether the puppet service should be enabled during health check
+
+Default value: `true`
+
+##### `service_running`
+
+Data type: `Boolean`
+
+Whether the puppet service should be running during health check
+
+Default value: `true`
+
+##### `runinterval`
+
+Data type: `Integer[0]`
+
+The runinterval to use during health check
+
+Default value: `1800`
+
+##### `debug`
+
+Data type: `Boolean`
+
+Whether to enable debug output
+
+Default value: `false`
+
+### `os_patching::patch_group`
+
+Patch nodes collected by a fact group
+
+#### Parameters
+
+The following parameters are available in the `os_patching::patch_group` plan:
+
+* [`group`](#-os_patching--patch_group--group)
+* [`patch_in_batches`](#-os_patching--patch_group--patch_in_batches)
+* [`batch_size`](#-os_patching--patch_group--batch_size)
+* [`run_health_check`](#-os_patching--patch_group--run_health_check)
+* [`debug`](#-os_patching--patch_group--debug)
+* [`pql_query`](#-os_patching--patch_group--pql_query)
+
+##### `group`
+
+Data type: `String[1]`
+
+The fact group name to patch
+
+##### `patch_in_batches`
+
+Data type: `Boolean`
+
+Whether to patch nodes in batches
+
+Default value: `true`
+
+##### `batch_size`
+
+Data type: `Integer[0]`
+
+The size of each batch if patching in batches
+
+Default value: `15`
+
+##### `run_health_check`
+
+Data type: `Boolean`
+
+Whether to run a health check after patching
+
+Default value: `true`
+
+##### `debug`
+
+Data type: `Boolean`
+
+Whether to enable debug output
+
+Default value: `false`
+
+##### `pql_query`
+
+Data type: `String[1]`
+
+The PQL query to retrieve nodes in the group
+
+Default value: `"inventory[certname] { facts.os_patching.group = '${group}'}"`
+
+### `os_patching::patch_pql`
+
+Patch nodes collected by a PQL query
+
+#### Parameters
+
+The following parameters are available in the `os_patching::patch_pql` plan:
+
+* [`pql_query`](#-os_patching--patch_pql--pql_query)
+* [`patch_in_batches`](#-os_patching--patch_pql--patch_in_batches)
+* [`batch_size`](#-os_patching--patch_pql--batch_size)
+* [`run_health_check`](#-os_patching--patch_pql--run_health_check)
+* [`debug`](#-os_patching--patch_pql--debug)
+
+##### `pql_query`
+
+Data type: `String[1]`
+
+The PQL query to retrieve nodes to patch
+
+Default value: `'inventory[certname] { facts.os.family = "redhat" }'`
+
+##### `patch_in_batches`
+
+Data type: `Boolean`
+
+Whether to patch nodes in batches
+
+Default value: `true`
+
+##### `batch_size`
+
+Data type: `Integer[0]`
+
+The size of each batch if patching in batches
+
+Default value: `15`
+
+##### `run_health_check`
+
+Data type: `Boolean`
+
+Whether to run a health check after patching
+
+Default value: `true`
+
+##### `debug`
+
+Data type: `Boolean`
+
+Whether to enable debug output
+
+Default value: `false`
+
diff --git a/lib/facter/os_patching.rb b/lib/facter/os_patching.rb
index 16e94a9..2659233 100644
--- a/lib/facter/os_patching.rb
+++ b/lib/facter/os_patching.rb
@@ -302,5 +302,21 @@
data['blocked_reasons'] = blocked_reasons
data
end
+
+ chunk(:group) do
+ data = {}
+ groupfile = os_patching_dir + '/group'
+ if File.file?(groupfile)
+ group = File.open(groupfile, 'r').to_a
+ line = group.last.chomp
+ matchdata = line.match(/^(.*)$/)
+ if matchdata[0]
+ data['group'] = matchdata[0]
+ end
+ else
+ data['group'] = 'default'
+ end
+ data
+ end
end
end
diff --git a/manifests/init.pp b/manifests/init.pp
index 0d08d6c..87d6187 100644
--- a/manifests/init.pp
+++ b/manifests/init.pp
@@ -96,6 +96,9 @@
# @param ensure
# `present` to install scripts, cronjobs, files, etc, `absent` to cleanup a system that previously hosted us
#
+# @param group
+# The group to assign the node for patching purposes.
+#
# @example assign node to 'Week3' patching window, force a reboot and create a blackout window for the end of the year
# class { 'os_patching':
# patch_window => 'Week3',
@@ -106,6 +109,7 @@
# 'end' => '2019-01-15T23:59:59+10:00',
# },
# },
+# group => 'patching01',
# }
#
# @example An example profile to setup patching, sourcing blackout windows from hiera
@@ -125,6 +129,7 @@
# patch_window => $patch_window,
# reboot_override => $reboot_override,
# blackout_windows => $full_blackout_windows,
+# group => 'patching01',
# }
# }
#
@@ -163,8 +168,9 @@
Variant[Enum['absent'], Integer[1,31]] $patch_cron_monthday,
Variant[Enum['absent'], Integer[0,7]] $patch_cron_weekday,
Integer[0,59] $patch_cron_min = fqdn_rand(59),
- Optional[String] $patch_window = undef,
+ Optional[Pattern[/^[A-Za-z0-9\-_ ]+$/]] $patch_window = undef,
Optional[Hash] $blackout_windows = undef,
+ Optional[Pattern[/^[A-Za-z0-9\-_ ]+$/]] $group = undef,
) {
# None tunable
$cache_dir = lookup('os_patching::cache_dir',Stdlib::Absolutepath,first,undef)
@@ -207,10 +213,6 @@
default => 'absent',
}
- if ($patch_window and $patch_window !~ /[A-Za-z0-9\-_ ]+/ ) {
- fail('The patch window can only contain alphanumerics, space, underscore and dash')
- }
-
file { $cache_dir:
ensure => $ensure_dir,
force => true,
@@ -243,6 +245,11 @@
default => 'absent'
}
+ $group_ensure = ($ensure == 'present' and $group) ? {
+ true => 'file',
+ default => 'absent',
+ }
+
file { "${cache_dir}/patch_window":
ensure => $patch_window_ensure,
content => $patch_window,
@@ -258,6 +265,11 @@
notify => Exec[$fact_exec],
}
+ file { "${cache_dir}/group":
+ ensure => $group_ensure,
+ content => $group,
+ }
+
$reboot_override_ensure = ($ensure == 'present' and $reboot_override) ? {
true => 'file',
default => 'absent',
@@ -314,6 +326,7 @@
"${cache_dir}/patch_window",
"${cache_dir}/reboot_override",
"${cache_dir}/blackout_windows",
+ "${cache_dir}/group",
],
}
}
diff --git a/plans/patch_after_healthcheck.pp b/plans/patch_after_healthcheck.pp
index 7439ca4..4fb9730 100644
--- a/plans/patch_after_healthcheck.pp
+++ b/plans/patch_after_healthcheck.pp
@@ -9,13 +9,12 @@
) {
# Run an initial health check to make sure the target nodes are ready
- $health_checks = run_task('puppet_health_check::agent_health',
- $nodes,
+ $health_checks = run_task('puppet_health_check::agent_health', $nodes,
target_noop_state => $noop_state,
target_service_enabled => true,
target_service_running => true,
target_runinterval => $runinterval,
- '_catch_errors' => true,
+ _catch_errors => true,
)
$nodes_to_patch = $health_checks.filter | $items | { $items.value['state'] == 'clean' }
diff --git a/plans/patch_batch.pp b/plans/patch_batch.pp
new file mode 100644
index 0000000..52d962e
--- /dev/null
+++ b/plans/patch_batch.pp
@@ -0,0 +1,84 @@
+# @summary Patch nodes in a batch, is called from patch_group plan or patch_pql plan
+#
+# @param batch The batch of nodes to patch
+# @param catch_errors Whether to catch errors during task execution
+# @param noop_state Whether to consider noop state during health check
+# @param run_health_check Whether to run a health check before patching
+# @param service_enabled Whether the puppet service should be enabled during health check
+# @param service_running Whether the puppet service should be running during health check
+# @param runinterval The runinterval to use during health check
+# @param debug Whether to enable debug output
+#
+# @return A hash containing the results of the patching operation
+#
+plan os_patching::patch_batch (
+ TargetSpec $batch,
+ Boolean $catch_errors = true,
+ Boolean $noop_state = false,
+ Boolean $run_health_check = false,
+ Boolean $service_enabled = true,
+ Boolean $service_running = true,
+ Integer[0] $runinterval = 1800,
+ Boolean $debug = false,
+) {
+ out::message("patch_batch.pp: Patching batch of nodes: ${batch}")
+ out::message("patch_batch.pp: Health check is ${run_health_check}.")
+
+ if $run_health_check {
+ out::message('patch_batch.pp: Running health check before patching')
+ # this comes from https://github.com/voxpupuli/puppet_health_check
+ $health_checks = run_task('puppet_health_check::agent_health', $batch,
+ _catch_errors => $catch_errors,
+ target_noop_state => $noop_state,
+ target_runinterval => $runinterval,
+ target_service_enabled => $service_enabled,
+ target_service_running => $service_running,
+ )
+
+ if $debug {
+ out::message('patch_batch.pp: Health check results:')
+ out::message($health_checks)
+ }
+
+ # 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 }
+
+ 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)
+ }
+
+ $patching_result = run_task('os_patching::patch_server', $nodes_to_patch,
+ _catch_errors => $catch_errors,
+ )
+
+ 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,
+ )
+
+ if $debug {
+ out::message('patch_batch.pp: Patching results:')
+ out::message($patching_result)
+ }
+
+ $skipped_nodes = [] # No skipped nodes if health check is not run
+ }
+
+ return(
+ {
+ targets => $batch,
+ patched => $patching_result.ok_set.names,
+ failed => $patching_result.error_set.names,
+ skipped => $skipped_nodes,
+ health_check => $run_health_check,
+ }
+ )
+}
diff --git a/plans/patch_group.pp b/plans/patch_group.pp
new file mode 100644
index 0000000..8cd2646
--- /dev/null
+++ b/plans/patch_group.pp
@@ -0,0 +1,65 @@
+# @summary Patch nodes collected by a fact group
+#
+# @param group The fact group name to patch
+# @param patch_in_batches Whether to patch nodes in batches
+# @param batch_size The size of each batch if patching in batches
+# @param run_health_check Whether to run a health check after patching
+# @param debug Whether to enable debug output
+# @param pql_query The PQL query to retrieve nodes in the group
+#
+# @return A hash containing the results of the patching operation
+#
+plan os_patching::patch_group (
+ String[1] $group,
+ String[1] $pql_query = "inventory[certname] { facts.os_patching.group = '${group}'}",
+ Boolean $patch_in_batches = true,
+ Integer[0] $batch_size = 15,
+ Boolean $run_health_check = true,
+ Boolean $debug = false,
+) {
+ $pql_data = puppetdb_query($pql_query)
+ $certnames = $pql_data.map |$item| { $item['certname'] }
+ $targets = get_targets($certnames)
+
+ out::message("patch_group.pp: Patching group: ${group}")
+ out::message("patch_group.pp: Targets in group: ${targets}")
+ out::message("patch_group.pp: Patching in batches is ${patch_in_batches}")
+
+ if $patch_in_batches {
+ $batches = slice($targets, $batch_size)
+
+ out::message("patch_group.pp: Patching in batches of size: ${batch_size}")
+ out::message("patch_group.pp: Patching batches created: ${batches}")
+
+ $batch_results = $batches.map |$batch| {
+ # out::message("patch_group.pp: Patching with nodes: ${batch}")
+ run_plan('os_patching::patch_batch',
+ {
+ batch => $batch,
+ run_health_check => $run_health_check,
+ debug => $debug,
+ }
+ )
+ }
+
+ # 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'],
+ }
+ } else {
+ out::message("patch_group.pp: Patching all targets at once: ${targets}")
+ $result = run_plan('os_patching::patch_batch',
+ {
+ batch => $targets,
+ run_health_check => $run_health_check,
+ debug => $debug,
+ }
+ )
+ }
+
+ return $result
+}
diff --git a/plans/patch_pql.pp b/plans/patch_pql.pp
new file mode 100644
index 0000000..4872f7c
--- /dev/null
+++ b/plans/patch_pql.pp
@@ -0,0 +1,64 @@
+# @summary Patch nodes collected by a PQL query
+#
+# @param pql_query The PQL query to retrieve nodes to patch
+# @param patch_in_batches Whether to patch nodes in batches
+# @param batch_size The size of each batch if patching in batches
+# @param run_health_check Whether to run a health check after patching
+# @param debug Whether to enable debug output
+#
+# @return A hash containing the results of the patching operation
+#
+plan os_patching::patch_pql (
+ String[1] $pql_query = 'inventory[certname] { facts.os.family = "redhat" }',
+ Boolean $patch_in_batches = true,
+ Integer[0] $batch_size = 15,
+ Boolean $run_health_check = true,
+ Boolean $debug = false,
+) {
+ $pql_data = puppetdb_query($pql_query)
+ $certnames = $pql_data.map |$item| { $item['certname'] }
+ $targets = get_targets($certnames)
+
+ out::message("patch_pql.pp: Patching PQL query: ${pql_query}")
+ out::message("patch_pql.pp: Targets in group: ${targets}")
+ out::message("patch_pql.pp: Patching in batches is ${patch_in_batches}")
+
+ if $patch_in_batches {
+ $batches = slice($targets, $batch_size)
+
+ out::message("patch_pql.pp: Patching in batches of size: ${batch_size}")
+ out::message("patch_pql.pp: Patching batches created: ${batches}")
+
+ $batch_results = $batches.map |$batch| {
+ # out::message("patch_pql.pp: Patching with nodes: ${batch}")
+ run_plan('os_patching::patch_batch',
+ {
+ batch => $batch,
+ run_health_check => $run_health_check,
+ debug => $debug,
+ }
+ )
+ }
+
+ # 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'],
+ }
+ } else {
+ out::message('patch_pql.pp: Patching in batches is disabled')
+ out::message("patch_pql.pp: Patching all targets at once: ${targets}")
+ $result = run_plan('os_patching::patch_batch',
+ {
+ batch => $targets,
+ run_health_check => $run_health_check,
+ debug => $debug,
+ }
+ )
+ }
+
+ return $result
+}
diff --git a/spec/classes/os_patching_spec.rb b/spec/classes/os_patching_spec.rb
index 9c8e1e4..b86227d 100644
--- a/spec/classes/os_patching_spec.rb
+++ b/spec/classes/os_patching_spec.rb
@@ -141,7 +141,7 @@
context 'with patch_window => $#&!RYYQ!' do
let(:params) { {'patch_window' => '(((((##(@(!$#&!RYYQ!'} }
- it { is_expected.to compile }
+ it { is_expected.to raise_error(Puppet::Error, /patch_window/) }
end
context 'with patch_window => Week3' do