diff options
author | Tim Smith <tsmith@chef.io> | 2020-03-24 13:56:28 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-24 13:56:28 -0700 |
commit | b4c2005bd98e56e4806013be8bdfcf102420c79e (patch) | |
tree | f81f0ab7f3813f857b858b441bacc4bc7cb9f3c6 | |
parent | 984b43786e32ebed1a0e0f78ae51870aa4ecc215 (diff) | |
parent | 44a3d003e789ea0a605bba59647272ebe3390637 (diff) | |
download | chef-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.rb | 11 | ||||
-rw-r--r-- | lib/chef/dist.rb | 4 | ||||
-rw-r--r-- | lib/chef/resource/chef_client_cron.rb | 205 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/unit/resource/chef_client_cron_spec.rb | 109 |
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 |