diff --git a/site-modules/profile/functions/cron/validate_field.pp b/site-modules/profile/functions/cron/validate_field.pp new file mode 100644 index 00000000..015c6377 --- /dev/null +++ b/site-modules/profile/functions/cron/validate_field.pp @@ -0,0 +1,157 @@ +function profile::cron::validate_field( + String $field, + Variant[Integer, String, Array[Variant[Integer, String]]] $value, + Optional[Array[String]] $valid_strings, + Optional[Tuple[Integer, Integer]] $int_range, + Boolean $arrays_valid = true, +) >> Tuple[Optional[String], Array[String]] { + + if $value =~ Array and !$arrays_valid { + return [undef, ["Cannot nest Arrays in value for ${field}"]] + } + + $_valid_strings = pick_default($valid_strings, []) + + case $value { + Array: { + $ret = $value.map |$_value| { + profile::cron::validate_field( + $field, + $_value, + $valid_strings, + $int_range, + false, + ) + } + + $_failed_values = $ret.filter |$_value| { + $_value[0] == undef + } + + if empty($_failed_values) { + return [$ret.map |$_value| {$_value[0]}.join(','), []] + } else { + return [undef, $_failed_values.map |$_value| { $_value[1] }.flatten] + } + } + + *($_valid_strings + ['*']): { + return [$value, []] + } + + /^\d+$/: { + return profile::cron::validate_field( + $field, + Integer($value, 10), + $valid_strings, + $int_range, + $arrays_valid, + ) + } + + /[ ,]/: { + return profile::cron::validate_field( + $field, + $value.split('[ ,]'), + $valid_strings, + $int_range, + $arrays_valid, + ) + } + + /^([0-9a-z]+)-([0-9a-z]+)$/: { + $min_valid = profile::cron::validate_field( + $field, + $1, + $valid_strings, + $int_range, + false, + ) + + $max_valid = profile::cron::validate_field( + $field, + $2, + $valid_strings, + $int_range, + false, + ) + + $_errors = $min_valid[1] + $max_valid[1] + if empty($_errors) { + $_parsed_min = $min_valid[0] + $_parsed_max = $max_valid[0] + return ["${_parsed_min}-${_parsed_max}", []] + } else { + return [undef, $_errors] + } + } + + /^([0-9a-z]+)-([0-9a-z]+)\/(\d+)$/: { + $min_valid = profile::cron::validate_field( + $field, + $1, + $valid_strings, + $int_range, + false, + ) + + $max_valid = profile::cron::validate_field( + $field, + $2, + $valid_strings, + $int_range, + false, + ) + + $interval_valid = profile::cron::validate_field( + $field, + Integer($3, 10), + [], + $int_range, + false, + ) + $_errors = $min_valid[1] + $max_valid[1] + $interval_valid[1] + if empty($_errors) { + $_parsed_min = $min_valid[0] + $_parsed_max = $max_valid[0] + $_parsed_interval = $interval_valid[0] + return ["${_parsed_min}-${_parsed_max}/${_parsed_interval}", []] + } else { + return [undef, $_errors] + } + } + + /^\*\/(\d+)$/: { + $interval_valid = profile::cron::validate_field( + $field, + Integer($1, 10), + [], + $int_range, + false, + ) + if empty($interval_valid[1]) { + $_parsed_interval = $interval_valid[0] + return ["*/${_parsed_interval}", []] + } else { + return $interval_valid + } + } + + String: { + return [undef, ["Could not parse value ${value} for field ${field}"]] + } + + Integer: { + [$_min, $_max] = pick_default($int_range, [0, 0]) + if $_min <= $value and $value <= $_max { + return [String($value), []] + } else { + return [undef, ["Value ${value} out of range ${_min}-${_max} for field ${field}"]] + } + } + + default: { + return [undef, ["Unknown type ${value} for field ${field}"]] + } + } +} diff --git a/site-modules/profile/manifests/cron.pp b/site-modules/profile/manifests/cron.pp new file mode 100644 index 00000000..317c3016 --- /dev/null +++ b/site-modules/profile/manifests/cron.pp @@ -0,0 +1,22 @@ +# Handle a /etc/puppet-cron.d directory with properly managed cron.d snippets + +class profile::cron { + $directory = '/etc/puppet-cron.d' + + file {$directory: + ensure => directory, + mode => '0755', + owner => 'root', + group => 'root', + recurse => true, + purge => true, + notify => Exec['clean-cron.d-symlinks'], + } + + exec {'clean-cron.d-symlinks': + path => ['/bin', '/usr/bin'], + command => 'find /etc/cron.d -type l ! -exec test -e {} \; -delete', + refreshonly => true, + require => File[$directory], + } +} diff --git a/site-modules/profile/manifests/cron/d.pp b/site-modules/profile/manifests/cron/d.pp new file mode 100644 index 00000000..4d6b54cd --- /dev/null +++ b/site-modules/profile/manifests/cron/d.pp @@ -0,0 +1,74 @@ +# Add a cron.d snippet to the /etc/puppet-cron.d directory + +define profile::cron::d( + String $command, + String $unique_tag = $title, + String $target = 'default', + Optional[Variant[Integer, String, Array[Variant[Integer, String]]]] $minute = undef, + Optional[Variant[Integer, String, Array[Variant[Integer, String]]]] $hour = undef, + Optional[Variant[Integer, String, Array[Variant[Integer, String]]]] $monthday = undef, + Optional[Variant[Integer, String, Array[Variant[Integer, String]]]] $month = undef, + Optional[Variant[Integer, String, Array[Variant[Integer, String]]]] $weekday = undef, + Optional[Enum['@reboot', '@yearly', '@annually', '@monthly', '@weekly', '@daily', '@midnight', '@hourly']] $special = undef, + String $user = 'root', + Optional[String] $random_seed = undef, +) { + include profile::cron + + $_params = { + 'minute' => $minute, + 'hour' => $hour, + 'monthday' => $monthday, + 'month' => $month, + 'weekday' => $weekday, + } + + $_int_limits = { + 'minute' => [0, 59], + 'hour' => [0, 23], + 'monthday' => [1, 31], + 'month' => [1, 12], + 'weekday' => [0, 7], + } + + $_str_values = { + 'month' => ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'], + 'weekday' => ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], + } + + if $special != undef { + $_defined_params = $_params.filter |$field, $value| { $value == undef } + + unless empty($_defined_params) { + $_defined_fields = keys($_defined_params).each |$field| {"'${field}'"}.join(', ') + fail("profile::cron::d parameter 'special' is exclusive with ${_defined_fields}.") + } + } + + $_parsed_params = $_params.map |$field, $value| { + [$field] + profile::cron::validate_field( + $field, + pick_default($value, '*'), + $_str_values[$field], + $_int_limits[$field], + ) + } + + $_parse_errors = $_parsed_params.filter |$value| { $value[1] == undef }.map |$value| { $value[2] }.flatten + unless empty($_parse_errors) { + $_str_parse_errors = $_parse_errors.join(', ') + fail("Parse errors in profile::cron::d: ${_str_parse_errors}") + } + + $_params_hash = $_parsed_params.map |$value| { $value[0,2] }.hash + + if !defined(Profile::Cron::File[$target]) { + profile::cron::file {$target:} + } + + concat_fragment {"profile::cron::${unique_tag}": + order => '10', + content => template('profile/cron/snippet.erb'), + tag => "profile::cron::${target}", + } +} diff --git a/site-modules/profile/manifests/cron/file.pp b/site-modules/profile/manifests/cron/file.pp new file mode 100644 index 00000000..282312f4 --- /dev/null +++ b/site-modules/profile/manifests/cron/file.pp @@ -0,0 +1,27 @@ +# Base definition of a /etc/puppet-cron.d + +define profile::cron::file ( + String $target = $title, +) { + include profile::cron + + $file = "${profile::cron::directory}/${target}" + concat_file {"profile::cron::${target}": + path => $file, + owner => 'root', + group => 'root', + mode => '0644', + tag => "profile::cron::${target}", + } + -> file {"/etc/cron.d/puppet-${target}": + ensure => 'link', + target => $file, + } + -> Exec['clean-cron.d-symlinks'] + + concat_fragment {"profile::cron::${target}::_header": + target => "profile::cron::${target}", + order => '00', + content => "# Managed by puppet (module profile::cron), manual changes will be lost\n\n", + } +} diff --git a/site-modules/profile/templates/cron/snippet.erb b/site-modules/profile/templates/cron/snippet.erb new file mode 100644 index 00000000..ad084fd0 --- /dev/null +++ b/site-modules/profile/templates/cron/snippet.erb @@ -0,0 +1,6 @@ +# Cron snippet <%= @unique_tag %> +<% if @special -%> +<%= @special %> <%= @user %> <%= @command %> +<% else -%> +<%= @_params_hash['minute'] %> <%= @_params_hash['hour'] %> <%= @_params_hash['monthday'] %> <%= @_params_hash['month'] %> <%= @_params_hash['weekday'] %> <%= @user %> <%= @command %> +<% end -%>