diff --git a/cookbooks/fb_cron/README.md b/cookbooks/fb_cron/README.md index b7960fe..e409cd4 100644 --- a/cookbooks/fb_cron/README.md +++ b/cookbooks/fb_cron/README.md @@ -12,12 +12,15 @@ Attributes ---------- * node['fb_cron']['environment'][$NAME][$VALUE] * node['fb_cron']['jobs'][$NAME]['command'] +* node['fb_cron']['jobs'][$NAME]['comment'] * node['fb_cron']['jobs'][$NAME]['time'] * node['fb_cron']['jobs'][$NAME]['user'] * node['fb_cron']['jobs'][$NAME]['only_if'] * node['fb_cron']['jobs'][$NAME]['splaysecs'] * node['fb_cron']['jobs'][$NAME]['exclusive'] * node['fb_cron']['anacrontab']['environment']['$SETTING'] +* node['fb_cron']['cron_allow'] +* node['fb_cron']['cron_deny'] Usage ----- @@ -25,7 +28,7 @@ Usage ### Adding Jobs `node['fb_cron']['jobs']` is a hash of crons. To add a job, simply do: -``` +```ruby node.default['fb_cron']['jobs']['do_this_thing'] = { 'time' => '4 5 * * *', 'user' => 'apache', @@ -50,7 +53,7 @@ Any cron entry can include an `only_if` that *must* be a `proc`. It will be evaluated at runtime and the job will not be included if the only_if does not evaluate to true. For example: -``` +```ruby node.default['fb_cron']['jobs']['do_this_thing'] = { 'only_if' => proc { node['fb_bla']['enabled'] } 'time' => '4 5 * * *', @@ -59,6 +62,9 @@ node.default['fb_cron']['jobs']['do_this_thing'] = { } ``` +#### comment +The comment entry of the job, which defaults to the job name. + ### splaysecs Defaults to false/none. Please set a splay time for your cronjob, or explicitly set this to false to indicate that your job can't tolerate a splay. @@ -77,7 +83,7 @@ job, it'll be removed from any systems it was on. A bunch of default crons we want everywhere are set in the attributes file, if you need to exempt yourself from one, you can simply remove it from the hash: -``` +```ruby node.default['fb_cron']['jobs'].delete('do_this_thing') ``` @@ -93,7 +99,7 @@ affect the environment of the init script. On Redhat-like systems these variables go into `/etc/sysconfig/crond`, on Debian-like systems these go to `/etc/default/cron`. For example: -``` +```ruby # For RH node.default['fb_cron']['environment']['CRONDARGS'] = "-s" # For Debian @@ -108,8 +114,20 @@ execution. This can be configured using the to modify the start time of anacron jobs from the default 3-22 o'clock to 6-8 o'clock (server time): -``` +```ruby node.default['fb_cron']['anacrontab']['environment']['start_hours_range'] = '6-8' ``` NOTE: This is currently only implemented on Redhat-like OSes. + +### configuring who can run crontab command +Use the `node['fb_cron']['cron_allow']` and `node['fb_cron']['cron_deny']` +attributes to control the content of the `/etc/cron.allow` and `/etc/cron.deny` +files. The attributes default to empty arrays, and the files will be removed if +they remain empty arrays. Simply append to them: + +```ruby +node.default['fb_cron']['cron_allow'] << 'user1' +``` + +This can be used for compliance with security benchmarks. diff --git a/cookbooks/fb_cron/attributes/default.rb b/cookbooks/fb_cron/attributes/default.rb index 3dbbba7..7ca566f 100644 --- a/cookbooks/fb_cron/attributes/default.rb +++ b/cookbooks/fb_cron/attributes/default.rb @@ -27,6 +27,8 @@ 'start_hours_range' => '3-22', }, }, + 'cron_deny' => [], + 'cron_allow' => [], # Path for the crontab that contains all the fb_cron job entries. # This is a hidden attribute because people shouldn't change this unless diff --git a/cookbooks/fb_cron/metadata.rb b/cookbooks/fb_cron/metadata.rb index 813e42b..9a9ff3e 100644 --- a/cookbooks/fb_cron/metadata.rb +++ b/cookbooks/fb_cron/metadata.rb @@ -5,7 +5,6 @@ license 'Apache-2.0' description 'Installs/Configures cron' source_url 'https://github.com/facebook/chef-cookbooks/' -long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) version '0.0.1' supports 'centos' supports 'debian' diff --git a/cookbooks/fb_cron/recipes/default.rb b/cookbooks/fb_cron/recipes/default.rb index 560380c..4346beb 100644 --- a/cookbooks/fb_cron/recipes/default.rb +++ b/cookbooks/fb_cron/recipes/default.rb @@ -39,9 +39,10 @@ block do node['fb_cron']['jobs'].to_hash.each do |name, data| if data['only_if'] - unless data['only_if'].class == Proc + unless data['only_if'].instance_of?(Proc) fail 'fb_cron\'s only_if requires a Proc' end + unless data['only_if'].call Chef::Log.debug("fb_cron: Not including #{name} due to only_if") node.rm('fb_cron', 'jobs', name) @@ -71,6 +72,7 @@ if Integer(data['splaysecs']) <= 0 || Integer(data['splaysecs']) > 9600 fail "unreasonable splaysecs #{data['splaysecs']} in #{name} cron" end + sleepnum = node.get_seeded_flexible_shard(Integer(data['splaysecs']), data['command']) node.default['fb_cron']['jobs'][name]['splaycmd'] = @@ -78,6 +80,11 @@ else node.default['fb_cron']['jobs'][name]['splaycmd'] = '' end + + # Populate comment field + unless data['comment'] + node.default['fb_cron']['jobs'][name]['comment'] = name + end end end end @@ -124,7 +131,7 @@ # Make sure we nuke all crons from the cron resource. root_crontab = value_for_platform_family( ['rhel', 'fedora', 'suse'] => '/var/spool/cron/root', - ['debian', 'ubuntu'] => '/var/spool/cron/crontabs/root', + ['debian'] => '/var/spool/cron/crontabs/root', ) if root_crontab file 'clean out root crontab' do @@ -152,3 +159,25 @@ command '/usr/local/bin/osx_make_crond.sh' end end + +{ + 'cron_deny' => '/etc/cron.deny', + 'cron_allow' => '/etc/cron.allow', +}.each do |key, cronfile| + file cronfile do # this is an absolute path: ~FB031 + only_if { node['fb_cron'][key].empty? } + action :delete + end + + template cronfile do # this is an absolute path: ~FB031 + not_if { node['fb_cron'][key].empty? } + source 'fb_cron_allow_deny.erb' + owner node.root_user + group node.root_group + mode '0600' + variables( + :config => key, + ) + action :create + end +end diff --git a/cookbooks/fb_cron/recipes/packages.rb b/cookbooks/fb_cron/recipes/packages.rb index e3390b8..3b40efd 100644 --- a/cookbooks/fb_cron/recipes/packages.rb +++ b/cookbooks/fb_cron/recipes/packages.rb @@ -24,6 +24,8 @@ if node['platform'] == 'amazon' || node['platform_version'].to_i >= 6 package_name = 'cronie' end +when 'debian' + package_name = 'cron' end if package_name # ~FC023 diff --git a/cookbooks/fb_cron/spec/default_spec.rb b/cookbooks/fb_cron/spec/default_spec.rb new file mode 100644 index 0000000..9c59e07 --- /dev/null +++ b/cookbooks/fb_cron/spec/default_spec.rb @@ -0,0 +1,97 @@ +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +# +# Copyright (c) 2016-present, Facebook, Inc. +# All rights reserved. +# +# 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/spec_helper' + +recipe 'fb_cron::default' do |tc| + let(:chef_run) { tc.chef_run } + + it 'should render basic crontab' do + chef_run.converge(described_recipe) do |node| + node.default['fb_cron']['jobs']['do_this_thing'] = { + 'time' => '1 2 3 4 5', + 'user' => 'apache', + 'command' => '/usr/local/bin/foo.php', + } + node.default['fb_cron']['jobs']['comment_special'] = { + 'time' => '1 2 3 4 5', + 'user' => 'apache', + 'command' => '/usr/local/bin/foo.php', + 'comment' => 'a very useful comment', + } + end + + expect(chef_run).to render_file('/etc/cron.d/fb_crontab').with_content( + tc.fixture('fb_crontab'), + ) + end + + it 'should render crontab with mailto' do + chef_run.converge(described_recipe) do |node| + node.default['fb_cron']['jobs']['do_this_thing'] = { + 'time' => '2 1 5 4 3', + 'user' => 'root', + 'command' => '/usr/local/bin/foo.php', + 'mailto' => 'noreply@fb.com', + } + end + + expect(chef_run).to render_file('/etc/cron.d/fb_crontab').with_content( + tc.fixture('fb_crontab_mailto'), + ) + end + + it 'should render crontab with more than one job' do + chef_run.converge(described_recipe) do |node| + node.default['fb_cron']['jobs']['do_this_thing'] = { + 'time' => '* 1 * 2 *', + 'user' => 'hank', + 'command' => '/usr/local/bin/foo.php', + } + node.default['fb_cron']['jobs']['do_this_other_thing'] = { + 'time' => '1 * 3 * 5', + 'user' => 'fred', + 'command' => '/usr/local/bin/bar.php', + 'mailto' => 'noreply@fb.com', + } + end + + expect(chef_run).to render_file('/etc/cron.d/fb_crontab').with_content( + tc.fixture('fb_crontab_several'), + ) + end + + it 'should render anacrontab on appropriate platforms' do + chef_run.converge(described_recipe) do |node| + node.default['fb_cron']['anacrontab']['environment'] = { + 'shell' => '/bin/bash', + 'path' => '/sbin:/bin:/usr/sbin:/usr/bin:/usr/fake', + 'mailto' => 'noreply@fb.com', + 'random_delay' => '8', + 'start_hours_range' => '2-3', + } + end + + if tc.platform.to_s.start_with?('centos') + expect(chef_run).to render_file('/etc/anacrontab').with_content( + tc.fixture('anacrontab'), + ) + else + expect(chef_run).to_not render_file('/etc/anacrontab') + end + end +end diff --git a/cookbooks/fb_cron/spec/fixtures/default/anacrontab b/cookbooks/fb_cron/spec/fixtures/default/anacrontab new file mode 100644 index 0000000..7c50fd0 --- /dev/null +++ b/cookbooks/fb_cron/spec/fixtures/default/anacrontab @@ -0,0 +1,16 @@ +# +# THIS FILE IS CONTROLLED BY CHEF, DO NOT EDIT! +# +# You may add cronjobs following the instructions in +# fb_cron/README.md +# +SHELL=/bin/bash +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/fake +MAILTO=noreply@fb.com +RANDOM_DELAY=8 +START_HOURS_RANGE=2-3 + +#period in days delay in minutes job-identifier command +1 5 cron.daily nice run-parts /etc/cron.daily +7 25 cron.weekly nice run-parts /etc/cron.weekly +@monthly 45 cron.monthly nice run-parts /etc/cron.monthly diff --git a/cookbooks/fb_cron/spec/fixtures/default/fb_crontab b/cookbooks/fb_cron/spec/fixtures/default/fb_crontab new file mode 100644 index 0000000..b296f33 --- /dev/null +++ b/cookbooks/fb_cron/spec/fixtures/default/fb_crontab @@ -0,0 +1,11 @@ +# +# THIS FILE IS CONTROLLED BY CHEF, DO NOT EDIT! +# +# You may add cronjobs following the instructions in +# fb_cron/README.md +# +# do_this_thing +1 2 3 4 5 apache /usr/local/bin/foo.php + +# a very useful comment +1 2 3 4 5 apache /usr/local/bin/foo.php diff --git a/cookbooks/fb_cron/spec/fixtures/default/fb_crontab_mailto b/cookbooks/fb_cron/spec/fixtures/default/fb_crontab_mailto new file mode 100644 index 0000000..c29e011 --- /dev/null +++ b/cookbooks/fb_cron/spec/fixtures/default/fb_crontab_mailto @@ -0,0 +1,10 @@ +# +# THIS FILE IS CONTROLLED BY CHEF, DO NOT EDIT! +# +# You may add cronjobs following the instructions in +# fb_cron/README.md +# +# do_this_thing +MAILTO=noreply@fb.com +2 1 5 4 3 root /usr/local/bin/foo.php +MAILTO=root diff --git a/cookbooks/fb_cron/spec/fixtures/default/fb_crontab_several b/cookbooks/fb_cron/spec/fixtures/default/fb_crontab_several new file mode 100644 index 0000000..674fb53 --- /dev/null +++ b/cookbooks/fb_cron/spec/fixtures/default/fb_crontab_several @@ -0,0 +1,13 @@ +# +# THIS FILE IS CONTROLLED BY CHEF, DO NOT EDIT! +# +# You may add cronjobs following the instructions in +# fb_cron/README.md +# +# do_this_thing +* 1 * 2 * hank /usr/local/bin/foo.php + +# do_this_other_thing +MAILTO=noreply@fb.com +1 * 3 * 5 fred /usr/local/bin/bar.php +MAILTO=root diff --git a/cookbooks/fb_cron/templates/default/fb_cron_allow_deny.erb b/cookbooks/fb_cron/templates/default/fb_cron_allow_deny.erb new file mode 100644 index 0000000..0c8a387 --- /dev/null +++ b/cookbooks/fb_cron/templates/default/fb_cron_allow_deny.erb @@ -0,0 +1,4 @@ +# this file generated by Chef. See fb_cron/README.md +<% node['fb_cron'][@config].to_a.each do |user| %> +<%= user %> +<% end %> diff --git a/cookbooks/fb_cron/templates/default/fb_crontab.erb b/cookbooks/fb_cron/templates/default/fb_crontab.erb index 7e30c4e..421daec 100644 --- a/cookbooks/fb_cron/templates/default/fb_crontab.erb +++ b/cookbooks/fb_cron/templates/default/fb_crontab.erb @@ -6,7 +6,7 @@ # <% node['fb_cron']['jobs'].to_hash.each do |name, job| %> <% job['user'] = 'root' unless job['user'] -%> -# <%= name %> +# <%= job['comment'] %> <% if job['mailto'] -%> MAILTO=<%= job['mailto'] %> <% end -%>