summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2020-03-24 13:56:28 -0700
committerGitHub <noreply@github.com>2020-03-24 13:56:28 -0700
commitb4c2005bd98e56e4806013be8bdfcf102420c79e (patch)
treef81f0ab7f3813f857b858b441bacc4bc7cb9f3c6
parent984b43786e32ebed1a0e0f78ae51870aa4ecc215 (diff)
parent44a3d003e789ea0a605bba59647272ebe3390637 (diff)
downloadchef-b4c2005bd98e56e4806013be8bdfcf102420c79e.tar.gz
Merge pull request #9518 from chef/chef_client_cron
Add chef_client_cron resource from chef-client cookbook
-rw-r--r--kitchen-tests/cookbooks/end_to_end/recipes/default.rb11
-rw-r--r--lib/chef/dist.rb4
-rw-r--r--lib/chef/resource/chef_client_cron.rb205
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--spec/unit/resource/chef_client_cron_spec.rb109
5 files changed, 330 insertions, 0 deletions
diff --git a/kitchen-tests/cookbooks/end_to_end/recipes/default.rb b/kitchen-tests/cookbooks/end_to_end/recipes/default.rb
index 276223ff8d..c1a335d98c 100644
--- a/kitchen-tests/cookbooks/end_to_end/recipes/default.rb
+++ b/kitchen-tests/cookbooks/end_to_end/recipes/default.rb
@@ -138,6 +138,17 @@ user_ulimit "tomcat" do
rtprio_hard_limit 60
end
+chef_client_cron "Run chef-client as a cron job"
+
+chef_client_cron "Run chef-client with base recipe" do
+ minute 0
+ hour "0,12"
+ job_name "chef-client-base"
+ log_directory "/var/log/custom_chef_client_dir/"
+ log_file_name "chef-client-base.log"
+ daemon_options ["--override-runlist mycorp_base::default"]
+end
+
include_recipe "::chef-vault" unless includes_recipe?("end_to_end::chef-vault")
include_recipe "::alternatives"
include_recipe "::tests"
diff --git a/lib/chef/dist.rb b/lib/chef/dist.rb
index f5f63103fa..9221591b9b 100644
--- a/lib/chef/dist.rb
+++ b/lib/chef/dist.rb
@@ -54,6 +54,10 @@ class Chef
# The user's configuration directory
USER_CONF_DIR = ChefConfig::Dist::USER_CONF_DIR.freeze
+ # The suffix for Chef's /etc/chef, /var/chef and C:\\Chef directories
+ # "cinc" => /etc/cinc, /var/cinc, C:\\cinc
+ DIR_SUFFIX = ChefConfig::Dist::DIR_SUFFIX.freeze
+
# The server's configuration directory
SERVER_CONF_DIR = "/etc/chef-server".freeze
end
diff --git a/lib/chef/resource/chef_client_cron.rb b/lib/chef/resource/chef_client_cron.rb
new file mode 100644
index 0000000000..a6d0c61f57
--- /dev/null
+++ b/lib/chef/resource/chef_client_cron.rb
@@ -0,0 +1,205 @@
+#
+# Copyright:: 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"
+require_relative "helpers/cron_validations"
+require "digest/md5"
+
+class Chef
+ class Resource
+ class ChefClientCron < Chef::Resource
+ unified_mode true
+
+ provides :chef_client_cron
+
+ description "Use the chef_client_cron resource to setup the #{Chef::Dist::PRODUCT} to run as a cron job. 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_cron "Run chef-client as a cron job"
+ ```
+
+ Run #{Chef::Dist::PRODUCT} twice a day
+ ```ruby
+ chef_client_cron "Run chef-client every 12 hours" do
+ minute 0
+ hour "0,12"
+ end
+ ```
+
+ Run #{Chef::Dist::PRODUCT} with extra options passed to the client
+ ```ruby
+ chef_client_cron "Run an override recipe" do
+ daemon_options ["--override-runlist mycorp_base::default"]
+ end
+ ```
+ DOC
+
+ property :user, String,
+ description: "The name of the user that #{Chef::Dist::PRODUCT} runs as.",
+ default: "root"
+
+ property :minute, [Integer, String],
+ description: "The minute at which #{Chef::Dist::PRODUCT} is to run (0 - 59) or a cron pattern such as '0,30'.",
+ default: "0,30", callbacks: {
+ "should be a valid minute spec" => ->(spec) { Chef::ResourceHelpers::CronValidations.validate_numeric(spec, 0, 59) },
+ }
+
+ property :hour, [Integer, String],
+ description: "The hour at which #{Chef::Dist::PRODUCT} is to run (0 - 23) or a cron pattern such as '0,12'.",
+ default: "*", callbacks: {
+ "should be a valid hour spec" => ->(spec) { Chef::ResourceHelpers::CronValidations.validate_numeric(spec, 0, 23) },
+ }
+
+ property :day, [Integer, String],
+ description: "The day of month at which #{Chef::Dist::PRODUCT} is to run (1 - 31) or a cron pattern such as '1,7,14,21,28'.",
+ default: "*", callbacks: {
+ "should be a valid day spec" => ->(spec) { Chef::ResourceHelpers::CronValidations.validate_numeric(spec, 1, 31) },
+ }
+
+ property :month, [Integer, String],
+ description: "The month in the year on which #{Chef::Dist::PRODUCT} is to run (1 - 12, jan-dec, or *).",
+ default: "*", callbacks: {
+ "should be a valid month spec" => ->(spec) { Chef::ResourceHelpers::CronValidations.validate_month(spec) },
+ }
+
+ property :weekday, [Integer, String],
+ description: "The day of the week on which #{Chef::Dist::PRODUCT} is to run (0-7, mon-sun, or *), where Sunday is both 0 and 7.",
+ default: "*", callbacks: {
+ "should be a valid weekday spec" => ->(spec) { Chef::ResourceHelpers::CronValidations.validate_dow(spec) },
+ }
+
+ property :mailto, String,
+ description: "The e-mail address to e-mail any cron task failures to."
+
+ property :job_name, String,
+ default: Chef::Dist::CLIENT,
+ description: "The name of the cron job to create."
+
+ property :splay, [Integer, String],
+ default: 300,
+ 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."
+
+ property :environment, Hash,
+ default: lazy { {} },
+ description: "A Hash containing additional arbitrary environment variables under which the cron job will be run in the form of ``({'ENV_VARIABLE' => 'VALUE'})``."
+
+ property :comment, String,
+ description: "A comment to place in the cron.d file."
+
+ property :config_directory, String,
+ default: Chef::Dist::CONF_DIR,
+ description: "The path of the config directory."
+
+ property :log_directory, String,
+ default: lazy { platform?("mac_os_x") ? "/Library/Logs/#{Chef::Dist::DIR_SUFFIX.capitalize}" : "/var/log/#{Chef::Dist::DIR_SUFFIX}" },
+ description: "The path of the directory to create the log file in."
+
+ property :log_file_name, String,
+ default: "client.log",
+ description: "The name of the log file to use."
+
+ property :append_log_file, [true, false],
+ default: true,
+ description: "Append to the log file instead of overwriting the log file on each run."
+
+ property :chef_binary_path, String,
+ default: "/opt/#{Chef::Dist::DIR_SUFFIX}/bin/#{Chef::Dist::CLIENT}",
+ description: "The path to the #{Chef::Dist::CLIENT} binary."
+
+ property :daemon_options, Array,
+ default: lazy { [] },
+ description: "An array of options to pass to the #{Chef::Dist::CLIENT} command."
+
+ 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
+ owner new_resource.user
+ mode "0640"
+ recursive true
+ end
+ end
+
+ cron_d new_resource.job_name do
+ minute new_resource.minute
+ hour new_resource.hour
+ day new_resource.day
+ weekday new_resource.weekday
+ month new_resource.month
+ environment new_resource.environment
+ mailto new_resource.mailto if new_resource.mailto
+ user new_resource.user
+ comment new_resource.comment if new_resource.comment
+ command cron_command
+ end
+ end
+
+ action :remove do
+ cron_d new_resource.job_name do
+ action :delete
+ end
+ end
+
+ action_class do
+ #
+ # Generate a uniformly distributed unique number to sleep from 0 to the splay time
+ #
+ # @param [Integer] splay The number of seconds to splay
+ #
+ # @return [Integer]
+ #
+ def splay_sleep_time(splay)
+ seed = node["shard_seed"] || Digest::MD5.hexdigest(node.name).to_s.hex
+ random = Random.new(seed.to_i)
+ random.rand(splay)
+ end
+
+ #
+ # The complete cron command to run
+ #
+ # @return [String]
+ #
+ def cron_command
+ cmd = ""
+ cmd << "/bin/sleep #{splay_sleep_time(new_resource.splay)}; "
+ cmd << "#{new_resource.chef_binary_path} "
+ cmd << "#{new_resource.daemon_options.join(" ")} " unless new_resource.daemon_options.empty?
+ cmd << log_command
+ cmd << " || echo \"#{Chef::Dist::PRODUCT} execution failed\"" if new_resource.mailto
+ cmd
+ end
+
+ #
+ # The portion of the overall cron job that handles logging based on the append_log_file property
+ #
+ # @return [String]
+ #
+ def log_command
+ if new_resource.append_log_file
+ "-L #{::File.join(new_resource.log_directory, new_resource.log_file_name)}"
+ else
+ "> #{::File.join(new_resource.log_directory, new_resource.log_file_name)} 2>&1"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index 1fc707f642..2256804eb7 100644
--- a/lib/chef/resources.rb
+++ b/lib/chef/resources.rb
@@ -27,6 +27,7 @@ require_relative "resource/batch"
require_relative "resource/breakpoint"
require_relative "resource/build_essential"
require_relative "resource/cookbook_file"
+require_relative "resource/chef_client_cron"
require_relative "resource/chef_gem"
require_relative "resource/chef_handler"
require_relative "resource/chef_sleep"
diff --git a/spec/unit/resource/chef_client_cron_spec.rb b/spec/unit/resource/chef_client_cron_spec.rb
new file mode 100644
index 0000000000..85472d6f09
--- /dev/null
+++ b/spec/unit/resource/chef_client_cron_spec.rb
@@ -0,0 +1,109 @@
+#
+# 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::ChefClientCron do
+ let(:node) { Chef::Node.new }
+ let(:events) { Chef::EventDispatch::Dispatcher.new }
+ let(:run_context) { Chef::RunContext.new(node, {}, events) }
+ let(:resource) { Chef::Resource::ChefClientCron.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 "builds a default value for chef_binary_path dist values" do
+ expect(resource.chef_binary_path).to eql("/opt/chef/bin/chef-client")
+ end
+
+ it "log_directory is /Library/Logs/Chef on macOS systems" do
+ node.automatic_attrs[:platform_family] = "mac_os_x"
+ node.automatic_attrs[:platform] = "mac_os_x"
+ expect(resource.log_directory).to eql("/Library/Logs/Chef")
+ end
+
+ it "log_directory is /var/log/chef on non-macOS systems" do
+ node.automatic_attrs[:platform_family] = "ubuntu"
+ expect(resource.log_directory).to eql("/var/log/chef")
+ 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
+
+ describe "#splay_sleep_time" do
+ it "uses shard_seed attribute if present" do
+ node.automatic_attrs[:shard_seed] = "73399073"
+ expect(provider.splay_sleep_time(300)).to satisfy { |v| v >= 0 && v <= 300 }
+ end
+
+ it "uses a hex conversion of a md5 hash of the splay if present" do
+ node.automatic_attrs[:shard_seed] = nil
+ allow(node).to receive(:name).and_return("test_node")
+ expect(provider.splay_sleep_time(300)).to satisfy { |v| v >= 0 && v <= 300 }
+ end
+ end
+
+ describe "#cron_command" do
+ before do
+ allow(provider).to receive(:splay_sleep_time).and_return("123")
+ end
+
+ it "creates a valid command if using all default properties" do
+ expect(provider.cron_command).to eql("/bin/sleep 123; /opt/chef/bin/chef-client -L /var/log/chef/client.log")
+ end
+
+ it "uses daemon_options if set" do
+ resource.daemon_options ["--foo 1", "--bar 2"]
+ expect(provider.cron_command).to eql("/bin/sleep 123; /opt/chef/bin/chef-client --foo 1 --bar 2 -L /var/log/chef/client.log")
+ end
+
+ it "uses custom log files / paths if set" do
+ resource.log_file_name "my-client.log"
+ resource.log_directory "/var/log/my-chef/"
+ expect(provider.cron_command).to eql("/bin/sleep 123; /opt/chef/bin/chef-client -L /var/log/my-chef/my-client.log")
+ end
+
+ it "uses mailto if set" do
+ resource.mailto "bob@example.com"
+ expect(provider.cron_command).to eql("/bin/sleep 123; /opt/chef/bin/chef-client -L /var/log/chef/client.log || echo \"Chef Infra Client execution failed\"")
+ end
+
+ it "uses custom chef-client binary if set" do
+ resource.chef_binary_path "/usr/local/bin/chef-client"
+ expect(provider.cron_command).to eql("/bin/sleep 123; /usr/local/bin/chef-client -L /var/log/chef/client.log")
+ end
+
+ it "appends to the log file appending if set to false" do
+ resource.append_log_file false
+ expect(provider.cron_command).to eql("/bin/sleep 123; /opt/chef/bin/chef-client > /var/log/chef/client.log 2>&1")
+ end
+ end
+end