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