From 1b78260023924ed3af203e4d66cae2127ecd411d Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Tue, 24 Mar 2020 13:59:16 -0700 Subject: Add chef_client_windows_task resource from chef-client cookbook Allow setting up chef-client to run as a scheduled task out of the box. Signed-off-by: Tim Smith --- lib/chef/dist.rb | 4 + lib/chef/resource/chef_client_scheduled_task.rb | 178 +++++++++++++++++++++ lib/chef/resources.rb | 1 + .../resource/chef_client_scheduled_task_spec.rb | 70 ++++++++ 4 files changed, 253 insertions(+) create mode 100644 lib/chef/resource/chef_client_scheduled_task.rb create mode 100644 spec/unit/resource/chef_client_scheduled_task_spec.rb diff --git a/lib/chef/dist.rb b/lib/chef/dist.rb index 9221591b9b..7e6b221864 100644 --- a/lib/chef/dist.rb +++ b/lib/chef/dist.rb @@ -58,6 +58,10 @@ class Chef # "cinc" => /etc/cinc, /var/cinc, C:\\cinc DIR_SUFFIX = ChefConfig::Dist::DIR_SUFFIX.freeze + # The legacy conf folder: C:/opscode/chef. Specifically the "opscode" part + # DIR_SUFFIX is appended to it in code where relevant + LEGACY_CONF_DIR = ChefConfig::Dist::LEGACY_CONF_DIR.freeze + # The server's configuration directory SERVER_CONF_DIR = "/etc/chef-server".freeze end diff --git a/lib/chef/resource/chef_client_scheduled_task.rb b/lib/chef/resource/chef_client_scheduled_task.rb new file mode 100644 index 0000000000..d8cdabe76f --- /dev/null +++ b/lib/chef/resource/chef_client_scheduled_task.rb @@ -0,0 +1,178 @@ +# +# Copyright:: 2017-2020, Chef Software Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require_relative "../resource" +require_relative "../dist" + +class Chef + class Resource + class ChefClientScheduledTask < Chef::Resource + unified_mode true + + provides :chef_client_scheduled_task + + description "Use the chef_client_cron resource to setup the #{Chef::Dist::PRODUCT} to run as a Windows scheduled task. This resource will also create the specified log directory if it doesn't already exist." + introduced "16.0" + examples <<~DOC + Setup #{Chef::Dist::PRODUCT} to run using the default 30 minute cadence + ```ruby + chef_client_scheduled_task "Run chef-client as a scheduled task" + ``` + + Run #{Chef::Dist::PRODUCT} on system start + ```ruby + chef_client_scheduled_task 'Chef Client on start' do + frequency 'onstart' + end + ``` + + Run #{Chef::Dist::PRODUCT} with extra options passed to the client + ```ruby + chef_client_scheduled_task "Run an override recipe" do + daemon_options ["--override-runlist mycorp_base::default"] + end + ``` + DOC + + resource_name :chef_client_scheduled_task + + property :user, String, + description: "The name of the user that #{Chef::Dist::PRODUCT} runs as.", + default: "System", sensitive: true + + property :password, String, sensitive: true, + description: "The password for the user that #{Chef::Dist::PRODUCT} runs as." + + property :frequency, String, + description: "Frequency with which to run the task.", + default: "minute", + equal_to: %w{minute hourly daily monthly once on_logon onstart on_idle} + + property :frequency_modifier, [Integer, String], + coerce: proc { |x| Integer(x) }, + callbacks: { "should be a positive number" => proc { |v| v > 0 } }, + description: "Numeric value to go with the scheduled task frequency", + default: 30 + + property :start_date, String, + description: "The start date for the task in m:d:Y format (ex: 12/17/2020).", + regex: [%r{^[0-1][0-9]\/[0-3][0-9]\/\d{4}$}] + + property :start_time, String, + description: "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.", + regex: [/^\d{2}:\d{2}$/] + + property :splay, [Integer, String], + coerce: proc { |x| Integer(x) }, + callbacks: { "should be a positive number" => proc { |v| v > 0 } }, + description: "A random number of seconds between 0 and X to add to interval so that all #{Chef::Dist::CLIENT} commands don't execute at the same time.", + default: 300 + + property :config_directory, String, + description: "The path of the config directory.", + default: Chef::Dist::CONF_DIR + + property :log_directory, String, + description: "The path of the directory to create the log file in.", + default: lazy { |r| "#{r.config_directory}/log" } + + property :log_file_name, String, + description: "The name of the log file to use.", + default: "client.log" + + property :chef_binary_path, String, + description: "The path to the #{Chef::Dist::CLIENT} binary.", + default: "C:/#{Chef::Dist::LEGACY_CONF_DIR}/#{Chef::Dist::DIR_SUFFIX}/bin/#{Chef::Dist::CLIENT}" + + property :daemon_options, Array, + description: "An array of options to pass to the #{Chef::Dist::CLIENT} command.", + default: lazy { [] } + + property :task_name, String, + description: "The name of the scheduled task to create.", + default: Chef::Dist::CLIENT + + action :add do + # TODO: Replace this with a :create_if_missing action on directory when that exists + unless Dir.exist?(new_resource.log_directory) + directory new_resource.log_directory do + inherits true + recursive true + action :create + end + end + + # Fetch path of cmd.exe through environment variable comspec + cmd_path = ENV["COMSPEC"] + + # According to https://docs.microsoft.com/en-us/windows/desktop/taskschd/schtasks, + # the :once, :onstart, :onlogon, and :onidle schedules don't accept schedule modifiers + windows_task new_resource.task_name do + run_level :highest + command "#{cmd_path} /c \"#{client_cmd}\"" + user new_resource.user + password new_resource.password + frequency new_resource.frequency.to_sym + frequency_modifier new_resource.frequency_modifier if frequency_supports_frequency_modifier? + start_time new_resource.start_time + start_day new_resource.start_date unless new_resource.start_date.nil? + random_delay new_resource.splay if frequency_supports_random_delay? + action %i{create enable} + end + end + + action :remove do + windows_task new_resource.task_name do + action :delete + end + end + + action_class do + # Build command line to pass to cmd.exe + # + # @return [String] + def client_cmd + cmd = new_resource.chef_binary_path.dup + cmd << " -L #{::File.join(new_resource.log_directory, new_resource.log_file_name)}" + cmd << " -c #{::File.join(new_resource.config_directory, "client.rb")}" + + # Add custom options + cmd << " #{new_resource.daemon_options.join(" ")}" if new_resource.daemon_options.any? + cmd + end + + # + # not all frequencies in the windows_task resource support random_delay + # + # @return [boolean] + # + def frequency_supports_random_delay? + %w{once minute hourly daily weekly monthly}.include?(new_resource.frequency) + end + + # + # not all frequencies in the windows_task resource support frequency_modifier + # + # @return [boolean] + # + def frequency_supports_frequency_modifier? + # these are the only ones that don't + !%w{once on_logon onstart on_idle}.include?(new_resource.frequency) + end + end + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 2256804eb7..66bec89658 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -28,6 +28,7 @@ require_relative "resource/breakpoint" require_relative "resource/build_essential" require_relative "resource/cookbook_file" require_relative "resource/chef_client_cron" +require_relative "resource/chef_client_scheduled_task" require_relative "resource/chef_gem" require_relative "resource/chef_handler" require_relative "resource/chef_sleep" diff --git a/spec/unit/resource/chef_client_scheduled_task_spec.rb b/spec/unit/resource/chef_client_scheduled_task_spec.rb new file mode 100644 index 0000000000..6ddda0c8ce --- /dev/null +++ b/spec/unit/resource/chef_client_scheduled_task_spec.rb @@ -0,0 +1,70 @@ +# +# Author:: Tim Smith () +# Copyright:: 2020, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" + +describe Chef::Resource::ChefClientScheduledTask do + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:resource) { Chef::Resource::ChefClientScheduledTask.new("fakey_fakerton", run_context) } + let(:provider) { resource.provider_for_action(:add) } + + it "sets the default action as :add" do + expect(resource.action).to eql([:add]) + end + + it "coerces splay to an Integer" do + resource.splay "10" + expect(resource.splay).to eql(10) + end + + it "raises an error if splay is not a positive number" do + expect { resource.splay("-10") }.to raise_error(Chef::Exceptions::ValidationFailed) + end + + it "coerces frequency_modifier to an Integer" do + resource.frequency_modifier "10" + expect(resource.frequency_modifier).to eql(10) + end + + it "validates the start_time property input" do + expect { resource.start_time("8:00 am") }.to raise_error(Chef::Exceptions::ValidationFailed) + expect { resource.start_time("8:00") }.to raise_error(Chef::Exceptions::ValidationFailed) + expect { resource.start_time("08:00") }.not_to raise_error(Chef::Exceptions::ValidationFailed) + end + + it "validates the start_date property input" do + expect { resource.start_date("2/1/20") }.to raise_error(Chef::Exceptions::ValidationFailed) + expect { resource.start_date("02/01/20") }.to raise_error(Chef::Exceptions::ValidationFailed) + expect { resource.start_date("02/01/2020") }.not_to raise_error(Chef::Exceptions::ValidationFailed) + end + + it "raises an error if frequency_modifier is not a positive number" do + expect { resource.frequency_modifier("-10") }.to raise_error(Chef::Exceptions::ValidationFailed) + end + + it "builds a default value for chef_binary_path dist values" do + expect(resource.chef_binary_path).to eql("C:/opscode/chef/bin/chef-client") + end + + it "supports :add and :remove actions" do + expect { resource.action :add }.not_to raise_error + expect { resource.action :remove }.not_to raise_error + end +end -- cgit v1.2.1