summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith84@gmail.com>2020-03-24 13:59:16 -0700
committerTim Smith <tsmith84@gmail.com>2020-03-24 15:18:32 -0700
commit1b78260023924ed3af203e4d66cae2127ecd411d (patch)
tree086fdac7a22a2dabe02e05c1cd0c52560bea480c
parentb4c2005bd98e56e4806013be8bdfcf102420c79e (diff)
downloadchef_client_windows_task.tar.gz
Add chef_client_windows_task resource from chef-client cookbookchef_client_windows_task
Allow setting up chef-client to run as a scheduled task out of the box. Signed-off-by: Tim Smith <tsmith@chef.io>
-rw-r--r--lib/chef/dist.rb4
-rw-r--r--lib/chef/resource/chef_client_scheduled_task.rb178
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--spec/unit/resource/chef_client_scheduled_task_spec.rb70
4 files changed, 253 insertions, 0 deletions
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 (<tsmith@chef.io>)
+# 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