Skip to content

Commit

Permalink
[chef-client] Enable chef-client scheduled task to behave like cron, …
Browse files Browse the repository at this point in the history
…with predictable splay and start time

Signed-off-by: George Holt <[email protected]>
  • Loading branch information
George Holt authored and George Holt committed Jul 21, 2021
1 parent 8556de0 commit 4cd29c2
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 36 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ jobs:
- name: Check out cookbook code
uses: actions/checkout@master
- name: Install Chef Infra Client
uses: actionshub/chef-install@master
uses: actionshub/chef-install@main
- name: Dokken
uses: actionshub/kitchen-dokken@master
uses: actionshub/kitchen-dokken@main
env:
CHEF_LICENSE: accept-no-persist
KITCHEN_LOCAL_YAML: kitchen.dokken.yml
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ The chef_client_scheduled_task resource setups up Chef Infra Client to run as a
- `frequency_modifier` Numeric value to go with the scheduled task frequency - default: '30'
- `start_time` The start time for the task in HH:mm format (ex: 14:00). If the `frequency` is `minute` default start time will be `Time.now` plus the `frequency_modifier` number of minutes.
- `start_date` - The start date for the task in `m:d:Y` format (ex: 12/17/2017). nil by default and isn't necessary if you're running a regular interval.
- `splay` - A random number of seconds between 0 and X to add to interval. default: '300'
- `splay` - A random number of seconds between 0 and X to add to interval. Note splay is applied differently when use_consistent_splay is set to true. default: '300'
- `config_directory` - The path to the Chef config directory. default: 'C:/chef'
- `log_file_name` - The name of the log file. default: 'client.log'
- `log_directory` - The path to the Chef log directory. default: 'CONFIG_DIRECTORY/log'
- `chef_binary_path` - The path to the chef-client binary. default: 'C:/opscode/chef/bin/chef-client'
- `daemon_options` - An optional array of extra options to pass to the chef-client
- `task_name` - The name of the scheduled task. This allows for multiple chef_client_scheduled_task resources when it is used directly like in a wrapper cookbook. default: 'chef-client'
- `use_consistent_splay` - Indicates that the randomly computed splay should remain consistent for a given node, similar to how it functions in cron resource. default: false
- `snap_time_to_frequency` - Indicates that the start day and time for the task should be snapped to start at the next frequency cycle after the previous top of the hour. For example if the current time is 14:07 and the frequency_modifier is 30, the next task start time should be 14:30. Only applicable when frequency = 'minute'. default: false

### chef_client_cron

Expand Down Expand Up @@ -244,7 +246,7 @@ Use this recipe to run chef-client as a cron job rather than as a service. The c

### task

Use this recipe to run chef-client on Windows nodes as a scheduled task. Without modifying attributes the scheduled task will run 30 minutes after the recipe runs, with each chef run rescheduling the run 30 minutes in the future. By default the job runs as the system user. The time period between runs can be modified with the `default['chef_client']['task']['frequency_modifier']` attribute and the user can be changed with the `default['chef_client']['task']['user']` and `default['chef_client']['task']['password']` attributes.
Use this recipe to run chef-client on Windows nodes as a scheduled task. Without modifying attributes the scheduled task will run 30 minutes after the recipe runs, with each chef run rescheduling the run 30 minutes in the future. By default the job runs as the system user. The time period between runs can be modified with the `default['chef_client']['task']['frequency_modifier']` attribute and the user can be changed with the `default['chef_client']['task']['user']` and `default['chef_client']['task']['password']` attributes. For a scheduled task that behaves more like the chef-client cron job, the snap_time_to_frequency and use_consistent_splay properties can be set to true.

## Usage

Expand Down
6 changes: 4 additions & 2 deletions attributes/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
'weekday' => '*',
'path' => nil,
'environment_variables' => nil,
'log_directory' => nil,
'log_file' => '/dev/null',
'log_directory' => '/dev',
'log_file' => 'null',
'append_log' => false,
'use_cron_d' => false,
'mailto' => nil,
Expand All @@ -79,6 +79,8 @@
default['chef_client']['task']['start_time'] = nil
default['chef_client']['task']['start_date'] = nil
default['chef_client']['task']['name'] = 'chef-client'
default['chef_client']['task']['use_consistent_splay'] = false
default['chef_client']['task']['snap_time_to_frequency'] = false

default['chef_client']['load_gems'] = {}

Expand Down
5 changes: 5 additions & 0 deletions kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,8 @@ suites:
run_list:
- recipe[test::task]
includes: ["windows-2012r2-13", "windows-2012r2-14", "windows-2016", "windows-2019"]

- name: emulate-cron-task
run_list:
- recipe[test::emulate_cron_task]
includes: ["windows-2012r2-13", "windows-2012r2-14", "windows-2016", "windows-2019"]
14 changes: 14 additions & 0 deletions libraries/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ module Helpers
include Chef::Mixin::Which
require 'digest/md5'

#
# Snaps the start time of the scheduled task to the next start of the frequency cycle, where the frequency cycle begins
# at the top of the hour and increments by frequency_modifier in minutes
#
# @param [Integer] frequency_modifier - The frequency modifier in minutes
# @return [Time] The next snap-to time on the frequency cycle computed from top of the previous hour
#
def snap_time(frequency_modifier)
ref_time = Time.now
snap = Time.new(ref_time.year, ref_time.mon, ref_time.day, ref_time.hour, 0, 0)
minutes = (ref_time.min + frequency_modifier) - (ref_time.min % frequency_modifier)
snap + (minutes * 60)
end

def wmi_property_from_query(wmi_property, wmi_query)
@wmi = ::WIN32OLE.connect('winmgmts://')
result = @wmi.ExecQuery(wmi_query)
Expand Down
2 changes: 2 additions & 0 deletions recipes/task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class ::Chef::Recipe
chef_binary_path node['chef_client']['bin']
daemon_options node['chef_client']['daemon_options']
task_name node['chef_client']['task']['name']
use_consistent_splay node['chef_client']['task']['use_consistent_splay']
snap_time_to_frequency node['chef_client']['task']['snap_time_to_frequency']
end

windows_service 'chef-client' do
Expand Down
1 change: 1 addition & 0 deletions resources/cron.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
chef_version_for_provides '< 16.0' if respond_to?(:chef_version_for_provides)

provides :chef_client_cron
unified_mode true
resource_name :chef_client_cron

property :job_name, String, default: 'chef-client'
Expand Down
27 changes: 24 additions & 3 deletions resources/scheduled_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
# limitations under the License.
#

chef_version_for_provides '< 16.0' if respond_to?(:chef_version_for_provides)
chef_version_for_provides '< 18.0' if respond_to?(:chef_version_for_provides)

provides :chef_client_scheduled_task
unified_mode true
resource_name :chef_client_scheduled_task

property :task_name, String,
Expand All @@ -40,6 +41,9 @@
callbacks: { 'should be a positive number' => proc { |v| v > 0 } },
default: lazy { frequency == 'minute' ? 30 : 1 }

property :snap_time_to_frequency, [true, false],
default: false

property :accept_chef_license, [true, false],
default: false

Expand All @@ -54,6 +58,9 @@
callbacks: { 'should be a positive number' => proc { |v| v > 0 } },
default: 300

property :use_consistent_splay, [true, false],
default: false

property :run_on_battery, [true, false],
default: true

Expand Down Expand Up @@ -93,7 +100,7 @@
frequency_modifier new_resource.frequency_modifier if frequency_supports_frequency_modifier?
start_time start_time_value
start_day new_resource.start_date unless new_resource.start_date.nil?
random_delay new_resource.splay if frequency_supports_random_delay?
random_delay new_resource.splay if frequency_supports_random_delay? && !new_resource.use_consistent_splay
disallow_start_if_on_batteries new_resource.splay unless new_resource.run_on_battery || Gem::Requirement.new('< 14.4').satisfied_by?(Gem::Version.new(Chef::VERSION))
action [ :create, :enable ]
end
Expand All @@ -115,7 +122,17 @@ def full_command
# Fetch path of cmd.exe through environment variable comspec
cmd_path = ENV['COMSPEC']

"#{cmd_path} /c \"#{client_cmd}\""
"#{cmd_path} /c \"#{consistent_splay_command}#{client_cmd}\""
end

#
# The consistent splay sleep time when use_consistent_splay is true
#
# @return [NilClass,String] The prepended sleep command to run prior to executing the full command.
#
def consistent_splay_command
return unless new_resource.use_consistent_splay
"C:/windows/system32/windowspowershell/v1.0/powershell.exe Start-Sleep -s #{splay_sleep_time(new_resource.splay)} && "
end

#
Expand Down Expand Up @@ -157,6 +174,10 @@ def frequency_supports_frequency_modifier?
def start_time_value
if new_resource.start_time
new_resource.start_time
elsif new_resource.snap_time_to_frequency && new_resource.frequency == 'minute'
snap_time = snap_time(new_resource.frequency_modifier)
new_resource.start_date = snap_time.strftime('%m/%d/%Y')
snap_time.strftime('%H:%M')
elsif Gem::Requirement.new('< 13.7.0').satisfied_by?(Gem::Version.new(Chef::VERSION))
new_resource.frequency == 'minute' ? (Time.now + 60 * new_resource.frequency_modifier.to_f).strftime('%H:%M') : nil
end
Expand Down
1 change: 1 addition & 0 deletions resources/systemd_timer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
chef_version_for_provides '< 16.0' if respond_to?(:chef_version_for_provides)

provides :chef_client_systemd_timer
unified_mode true
resource_name :chef_client_systemd_timer

property :job_name, String, default: 'chef-client'
Expand Down
11 changes: 11 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,15 @@
config.color = true # Use color in STDOUT
config.formatter = :documentation # Use the specified formatter
config.log_level = :error # Avoid deprecation notice SPAM

config.before do
unless RUBY_PLATFORM =~ /mswin|mingw32|windows/
# This enables spec testing of windows recipes on nix OSes. This is used
# to address the use of Win32::Service in recipe chef-client::task
Win32 = Module.new unless defined?(Win32)
Win32::Service = Class.new unless defined?(Win32::Service)
allow(::Win32::Service).to receive(:exists?).with(anything).and_return(false)
allow(::Win32::Service).to receive(:exists?).with('chef-client').and_return(true)
end
end
end
80 changes: 54 additions & 26 deletions spec/unit/scheduled_task_spec.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,57 @@
# Chefspec and windows aren't the best of friends. Running this on a non-windows
# host results in win32ole load errors.

# require 'spec_helper'
#
# describe 'chef-client::task' do
# context 'when given override attributes' do
# let(:chef_run) do
# ChefSpec::ServerRunner.new(platform: 'windows', version: '2012R2', step_into: ['chef_client_scheduled_task']) do |node|
# node.override['chef_client']['task']['start_time'] = 'Tue Sep 13 15:46:33 EDT 2016'
# node.override['chef_client']['task']['user'] = 'system'
# node.override['chef_client']['task']['password'] = 'secret'
# node.override['chef_client']['task']['frequency'] = 'hourly'
# node.override['chef_client']['task']['frequency_modifier'] = 60
# end.converge(described_recipe)
# end
#
# it 'creates the windows_task resource with desired settings' do
# expect(chef_run).to create_windows_task('chef-client').with(
# command: 'cmd /c "C:/opscode/chef/bin/chef-client -L C:/chef/log/client.log -c C:/chef/client.rb -s 300 ^> NUL 2^>^&1"',
# user: 'system',
# password: 'secret',
# frequency: :hourly,
# frequency_modifier: 60,
# start_time: 'Tue Sep 13 15:46:33 EDT 2016'
# )
# end
# end
# end
require 'spec_helper'

describe 'chef-client::task' do
let(:local_system) { instance_double('LocalSystem', account_simple_name: 'system') }
let(:sid_class) { class_double('Chef::ReservedNames::Win32::Security::SID', LocalSystem: local_system, system_user?: true) }
let(:node) { runner.node }
let(:chef_run) { runner.converge(described_recipe) }
let(:runner) { ChefSpec::SoloRunner.new(platform: 'windows', version: '2012R2', step_into: ['chef_client_scheduled_task']) }

before do
# Mock up the environment to behave like Windows
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with('COMSPEC').and_return('cmd')
stub_const('Chef::ReservedNames::Win32::Security::SID', sid_class)
node.automatic['chef_client']['bin'] = 'C:/opscode/chef/bin/chef-client'
end

context 'when given override attributes' do
before do
node.override['chef_client']['task']['start_time'] = '16:10'
node.override['chef_client']['task']['frequency'] = 'hourly'
node.override['chef_client']['task']['frequency_modifier'] = 1
node.override['chef_client']['daemon_options'] = ['-s', '300', '^>', 'NUL', '2^>^&1']
end

it 'creates the windows_task resource with desired settings' do
expect(chef_run).to create_windows_task('chef-client').with(
command: 'cmd /c "C:/opscode/chef/bin/chef-client -L C:/chef/log/client.log -c C:/chef/client.rb -s 300 ^> NUL 2^>^&1"',
user: 'SYSTEM',
frequency: :hourly,
frequency_modifier: 1,
start_time: '16:10'
)
end
end

context 'when configured to use a consistent splay and snap frequency time' do
let(:now) { Time.new('2021', '4', '15', '16', '7', '12') }
before do
node.override['chef_client']['task']['use_consistent_splay'] = true
node.override['chef_client']['task']['snap_time_to_frequency'] = true
allow(Time).to receive(:now).and_return(now)
allow_any_instance_of(Chef::Resource::ChefClientScheduledTask).to receive(:splay_sleep_time).and_return(222)
end

it 'creates the windows_task resource with desired settings' do
expect(chef_run).to create_windows_task('chef-client').with(
command: 'cmd /c "C:/windows/system32/windowspowershell/v1.0/powershell.exe Start-Sleep -s 222 && C:/opscode/chef/bin/chef-client -L C:/chef/log/client.log -c C:/chef/client.rb"',
start_day: '04/15/2021',
start_time: '16:30'
)
end
end
end
1 change: 0 additions & 1 deletion test/cookbooks/test/recipes/cron.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
apt_update 'update'
include_recipe 'test::config'
include_recipe 'cron::default'
package 'crontabs' if platform?('fedora') # ensures we actually have the /etc/cron.d dir
include_recipe 'chef-client::cron'
include_recipe 'chef-client::delete_validation'
19 changes: 19 additions & 0 deletions test/cookbooks/test/recipes/emulate_cron_task.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
node.override['chef_client']['interval'] = 900
node.override['chef_client']['task']['snap_time_to_frequency'] = true
node.override['chef_client']['task']['use_consistent_splay'] = true

include_recipe 'test::config'
include_recipe 'chef-client::task'
include_recipe 'chef-client::delete_validation'

chef_client_scheduled_task 'Chef Client on start' do
user node['chef_client']['task']['user']
password node['chef_client']['task']['password']
frequency 'onstart'
config_directory node['chef_client']['conf_dir']
log_directory node['chef_client']['log_dir']
log_file_name node['chef_client']['log_file']
chef_binary_path node['chef_client']['bin']
daemon_options node['chef_client']['daemon_options']
task_name "#{node['chef_client']['task']['name']}-onstart"
end
22 changes: 22 additions & 0 deletions test/integration/emulate_cron_task/task_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
describe command('C:/opscode/chef/embedded/bin/ohai virtualization -c C:/chef/client.rb') do
its('exit_status') { should eq 0 }
end

describe file('C:/chef/client.rb') do
its('content') { should match(/ohai.disabled_plugins = \["Mdadm"\]/) }
its('content') { should match(/ohai.optional_plugins = \["Passwd"\]/) }
its('content') { should match(%r{ohai.plugin_path << "/tmp/kitchen/ohai/plugins"}) }
end

# the inspec resource requires PS 3.0+ and 2k8r2 only has PS 2.0 by default
unless os.release.to_f == 6.1
describe windows_task('chef-client') do
it { should be_enabled }
its('run_as_user') { should eq 'SYSTEM' }
its('task_to_run') { should match %r{cmd.exe /c C:/windows/system32/windowspowershell/v1.0/powershell.exe Start-Sleep -s ([0-9]|[1-9][0-9]|[1-9][0-9][0-9]) C:/opscode/chef/bin/chef-client -L C:/chef/log/client.log -c C:/chef/client.rb -s 300} }
end

describe windows_task('chef-client-onstart') do
it { should be_enabled }
end
end

0 comments on commit 4cd29c2

Please sign in to comment.