aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml2
-rw-r--r--lib/puppet/provider/cron/crontab.rb3
-rw-r--r--lib/puppet/provider/cron/filetype.rb209
-rw-r--r--spec/fixtures/unit/provider/cron/filetype/aixtab_output44
-rw-r--r--spec/fixtures/unit/provider/cron/filetype/crontab_output5
-rw-r--r--spec/fixtures/unit/provider/cron/filetype/suntab_output9
-rw-r--r--spec/unit/provider/cron/filetype_spec.rb144
-rw-r--r--spec/unit/provider/cron/parsed_spec.rb29
8 files changed, 431 insertions, 14 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 4b905f3..40b459e 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -18,7 +18,7 @@ AllCops:
- "**/Guardfile"
Metrics/LineLength:
Description: People have wide screens, use them.
- Max: 200
+ Max: 260
RSpec/BeforeAfterAll:
Description: Beware of using after(:all) as it may cause state to leak between tests.
A necessary evil in acceptance testing.
diff --git a/lib/puppet/provider/cron/crontab.rb b/lib/puppet/provider/cron/crontab.rb
index fec5511..8953109 100644
--- a/lib/puppet/provider/cron/crontab.rb
+++ b/lib/puppet/provider/cron/crontab.rb
@@ -1,3 +1,4 @@
+require_relative 'filetype'
require 'puppet/provider/parsedfile'
Puppet::Type.type(:cron).provide(:crontab, parent: Puppet::Provider::ParsedFile, default_target: ENV['USER'] || 'root') do
@@ -21,7 +22,7 @@ Puppet::Type.type(:cron).provide(:crontab, parent: Puppet::Provider::ParsedFile,
:crontab
end
- Puppet::Util::FileType.filetype(tabname)
+ Puppet::Provider::Cron::FileType.filetype(tabname)
end
self::TIME_FIELDS = [:minute, :hour, :monthday, :month, :weekday].freeze
diff --git a/lib/puppet/provider/cron/filetype.rb b/lib/puppet/provider/cron/filetype.rb
new file mode 100644
index 0000000..c381afa
--- /dev/null
+++ b/lib/puppet/provider/cron/filetype.rb
@@ -0,0 +1,209 @@
+require 'puppet/util/filetype'
+
+class Puppet::Provider::Cron
+ # This class defines the crontab filetypes
+ class FileType < Puppet::Util::FileType
+ class << self
+ define_method(:base_newfiletype, instance_method(:newfiletype))
+
+ # Puppet::Util::FileType.newfiletype will raise an exception if
+ # an already-defined filetype is re-defined. Unfortunately, there's
+ # a chance that this file could be loaded multiple times by Puppet's
+ # autoloader meaning that, without this wrapper, the crontab filetypes
+ # would be re-defined, causing Puppet to raise an exception.
+ def newfiletype(name, &block)
+ return if @filetypes && @filetypes.key?(name)
+
+ base_newfiletype(name, &block)
+ end
+ end
+
+ # Handle Linux-style cron tabs.
+ #
+ # TODO: We can possibly eliminate the "-u <username>" option in cmdbase
+ # by just running crontab under <username>'s uid (like we do for suntab
+ # and aixtab). It may be worth investigating this alternative
+ # implementation in the future. This way, we can refactor all three of
+ # our cron file types into a common crontab file type.
+ newfiletype(:crontab) do
+ def initialize(user)
+ self.path = user
+ end
+
+ def path=(user)
+ begin
+ @uid = Puppet::Util.uid(user)
+ rescue Puppet::Error => detail
+ raise FileReadError, _('Could not retrieve user %{user}: %{detail}') % { user: user, detail: detail }, detail.backtrace
+ end
+
+ # XXX We have to have the user name, not the uid, because some
+ # systems *cough*linux*cough* require it that way
+ @path = user
+ end
+
+ # Read a specific @path's cron tab.
+ def read
+ unless Puppet::Util.uid(@path)
+ Puppet.debug _('The %{path} user does not exist. Treating their crontab file as empty in case Puppet creates them in the middle of the run.') % { path: @path }
+
+ return ''
+ end
+
+ Puppet::Util::Execution.execute("#{cmdbase} -l", failonfail: true, combine: true)
+ rescue => detail
+ case detail.to_s
+ when %r{no crontab for}
+ return ''
+ when %r{are not allowed to}
+ Puppet.debug _('The %{path} user is not authorized to use cron. Their crontab file is treated as empty in case Puppet authorizes them in the middle of the run (by, for example, modifying the cron.deny or cron.allow files).') % { path: @path }
+
+ return ''
+ else
+ raise FileReadError, _('Could not read crontab for %{path}: %{detail}') % { path: @path, detail: detail }, detail.backtrace
+ end
+ end
+
+ # Remove a specific @path's cron tab.
+ def remove
+ cmd = "#{cmdbase} -r"
+ if ['Darwin', 'FreeBSD', 'DragonFly'].include?(Facter.value('operatingsystem'))
+ cmd = "/bin/echo yes | #{cmd}"
+ end
+
+ Puppet::Util::Execution.execute(cmd, failonfail: true, combine: true)
+ end
+
+ # Overwrite a specific @path's cron tab; must be passed the @path name
+ # and the text with which to create the cron tab.
+ #
+ # TODO: We should refactor this at some point to make it identical to the
+ # :aixtab and :suntab's write methods so that, at the very least, the pipe
+ # is not created and the crontab command's errors are not swallowed.
+ def write(text)
+ unless Puppet::Util.uid(@path)
+ raise Puppet::Error, _("Cannot write the %{path} user's crontab: The user does not exist") % { path: @path }
+ end
+
+ # this file is managed by the OS and should be using system encoding
+ IO.popen("#{cmdbase} -", 'w', encoding: Encoding.default_external) do |p|
+ p.print text
+ end
+ end
+
+ private
+
+ # Only add the -u flag when the @path is different. Fedora apparently
+ # does not think I should be allowed to set the @path to my own user name
+ def cmdbase
+ return 'crontab' if @uid == Puppet::Util::SUIDManager.uid || Facter.value(:operatingsystem) == 'HP-UX'
+
+ "crontab -u #{@path}"
+ end
+ end
+
+ # SunOS has completely different cron commands; this class implements
+ # its versions.
+ newfiletype(:suntab) do
+ # Read a specific @path's cron tab.
+ def read
+ unless Puppet::Util.uid(@path)
+ Puppet.debug _('The %{path} user does not exist. Treating their crontab file as empty in case Puppet creates them in the middle of the run.') % { path: @path }
+
+ return ''
+ end
+
+ Puppet::Util::Execution.execute(['crontab', '-l'], cronargs)
+ rescue => detail
+ case detail.to_s
+ when %r{can't open your crontab}
+ return ''
+ when %r{you are not authorized to use cron}
+ Puppet.debug _('The %{path} user is not authorized to use cron. Their crontab file is treated as empty in case Puppet authorizes them in the middle of the run (by, for example, modifying the cron.deny or cron.allow files).') % { path: @path }
+
+ return ''
+ else
+ raise FileReadError, _('Could not read crontab for %{path}: %{detail}') % { path: @path, detail: detail }, detail.backtrace
+ end
+ end
+
+ # Remove a specific @path's cron tab.
+ def remove
+ Puppet::Util::Execution.execute(['crontab', '-r'], cronargs)
+ rescue => detail
+ raise FileReadError, _('Could not remove crontab for %{path}: %{detail}') % { path: @path, detail: detail }, detail.backtrace
+ end
+
+ # Overwrite a specific @path's cron tab; must be passed the @path name
+ # and the text with which to create the cron tab.
+ def write(text)
+ # this file is managed by the OS and should be using system encoding
+ output_file = Tempfile.new('puppet_suntab', encoding: Encoding.default_external)
+ begin
+ output_file.print text
+ output_file.close
+ # We have to chown the stupid file to the user.
+ File.chown(Puppet::Util.uid(@path), nil, output_file.path)
+ Puppet::Util::Execution.execute(['crontab', output_file.path], cronargs)
+ rescue => detail
+ raise FileReadError, _('Could not write crontab for %{path}: %{detail}') % { path: @path, detail: detail }, detail.backtrace
+ ensure
+ output_file.close
+ output_file.unlink
+ end
+ end
+ end
+
+ # Support for AIX crontab with output different than suntab's crontab command.
+ newfiletype(:aixtab) do
+ # Read a specific @path's cron tab.
+ def read
+ unless Puppet::Util.uid(@path)
+ Puppet.debug _('The %{path} user does not exist. Treating their crontab file as empty in case Puppet creates them in the middle of the run.') % { path: @path }
+
+ return ''
+ end
+
+ Puppet::Util::Execution.execute(['crontab', '-l'], cronargs)
+ rescue => detail
+ case detail.to_s
+ when %r{open.*in.*directory}
+ return ''
+ when %r{not.*authorized.*cron}
+ Puppet.debug _('The %{path} user is not authorized to use cron. Their crontab file is treated as empty in case Puppet authorizes them in the middle of the run (by, for example, modifying the cron.deny or cron.allow files).') % { path: @path }
+
+ return ''
+ else
+ raise FileReadError, _('Could not read crontab for %{path}: %{detail}') % { path: @path, detail: detail }, detail.backtrace
+ end
+ end
+
+ # Remove a specific @path's cron tab.
+ def remove
+ Puppet::Util::Execution.execute(['crontab', '-r'], cronargs)
+ rescue => detail
+ raise FileReadError, _('Could not remove crontab for %{path}: %{detail}') % { path: @path, detail: detail }, detail.backtrace
+ end
+
+ # Overwrite a specific @path's cron tab; must be passed the @path name
+ # and the text with which to create the cron tab.
+ def write(text)
+ # this file is managed by the OS and should be using system encoding
+ output_file = Tempfile.new('puppet_aixtab', encoding: Encoding.default_external)
+
+ begin
+ output_file.print text
+ output_file.close
+ # We have to chown the stupid file to the user.
+ File.chown(Puppet::Util.uid(@path), nil, output_file.path)
+ Puppet::Util::Execution.execute(['crontab', output_file.path], cronargs)
+ rescue => detail
+ raise FileReadError, _('Could not write crontab for %{path}: %{detail}') % { path: @path, detail: detail }, detail.backtrace
+ ensure
+ output_file.close
+ output_file.unlink
+ end
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/unit/provider/cron/filetype/aixtab_output b/spec/fixtures/unit/provider/cron/filetype/aixtab_output
new file mode 100644
index 0000000..0b27645
--- /dev/null
+++ b/spec/fixtures/unit/provider/cron/filetype/aixtab_output
@@ -0,0 +1,44 @@
+# @(#)08 1.15.1.3 src/bos/usr/sbin/cron/root, cmdcntl, bos530 2/11/94 17:19:47
+# IBM_PROLOG_BEGIN_TAG
+# This is an automatically generated prolog.
+#
+# bos530 src/bos/usr/sbin/cron/root 1.15.1.3
+#
+# Licensed Materials - Property of IBM
+#
+# (C) COPYRIGHT International Business Machines Corp. 1989,1994
+# All Rights Reserved
+#
+# US Government Users Restricted Rights - Use, duplication or
+# disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
+#
+# IBM_PROLOG_END_TAG
+#
+# COMPONENT_NAME: (CMDCNTL) commands needed for basic system needs
+#
+# FUNCTIONS:
+#
+# ORIGINS: 27
+#
+# (C) COPYRIGHT International Business Machines Corp. 1989,1994
+# All Rights Reserved
+# Licensed Materials - Property of IBM
+#
+# US Government Users Restricted Rights - Use, duplication or
+# disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
+#
+#0 3 * * * /usr/sbin/skulker
+#45 2 * * 0 /usr/lib/spell/compress
+#45 23 * * * ulimit 5000; /usr/lib/smdemon.cleanu > /dev/null
+0 11 * * * /usr/bin/errclear -d S,O 30
+0 12 * * * /usr/bin/errclear -d H 90
+0 15 * * * /usr/lib/ras/dumpcheck >/dev/null 2>&1
+# SSA warning : Deleting the next two lines may cause errors in redundant
+# SSA warning : hardware to go undetected.
+01 5 * * * /usr/lpp/diagnostics/bin/run_ssa_ela 1>/dev/null 2>/dev/null
+0 * * * * /usr/lpp/diagnostics/bin/run_ssa_healthcheck 1>/dev/null 2>/dev/null
+# SSA warning : Deleting the next line may allow enclosure hardware errors to go undetected
+30 * * * * /usr/lpp/diagnostics/bin/run_ssa_encl_healthcheck 1>/dev/null 2>/dev/null
+# SSA warning : Deleting the next line may allow link speed exceptions to go undetected
+30 4 * * * /usr/lpp/diagnostics/bin/run_ssa_link_speed 1>/dev/null 2>/dev/null
+55 23 * * * /var/perf/pm/bin/pmcfg >/dev/null 2>&1 #Enable PM Data Collection
diff --git a/spec/fixtures/unit/provider/cron/filetype/crontab_output b/spec/fixtures/unit/provider/cron/filetype/crontab_output
new file mode 100644
index 0000000..da455f6
--- /dev/null
+++ b/spec/fixtures/unit/provider/cron/filetype/crontab_output
@@ -0,0 +1,5 @@
+# Puppet Name: pe-mcollective-metadata
+0,15,30,45 * * * * /opt/puppet/sbin/refresh-mcollective-metadata
+# Puppet Name: pe-puppet-console-prune-task
+* 1 * * * /opt/puppet/bin/rake -f /opt/puppet/share/puppet-dashboard/Rakefile RAILS_ENV=production reports:prune reports:prune:failed upto=30 unit=day > /dev/null
+0 2 * * * /usr/sbin/logrotate
diff --git a/spec/fixtures/unit/provider/cron/filetype/suntab_output b/spec/fixtures/unit/provider/cron/filetype/suntab_output
new file mode 100644
index 0000000..e6ca376
--- /dev/null
+++ b/spec/fixtures/unit/provider/cron/filetype/suntab_output
@@ -0,0 +1,9 @@
+#ident "@(#)root 1.19 98/07/06 SMI" /* SVr4.0 1.1.3.1 */
+#
+# The root crontab should be used to perform accounting data collection.
+#
+#
+10 3 * * * /usr/sbin/logadm
+15 3 * * 0 /usr/lib/fs/nfs/nfsfind
+30 3 * * * [ -x /usr/lib/gss/gsscred_clean ] && /usr/lib/gss/gsscred_clean
+#10 3 * * * /usr/lib/krb5/kprop_script ___slave_kdcs___
diff --git a/spec/unit/provider/cron/filetype_spec.rb b/spec/unit/provider/cron/filetype_spec.rb
new file mode 100644
index 0000000..bf96f91
--- /dev/null
+++ b/spec/unit/provider/cron/filetype_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+require 'puppet/provider/cron/filetype'
+
+# rubocop:disable RSpec/FilePath
+describe Puppet::Provider::Cron::FileType do
+ shared_examples_for 'crontab provider' do
+ let(:cron) { type.new('no_such_user') }
+ let(:crontab) { File.read(my_fixture(crontab_output)) }
+ let(:options) { { failonfail: true, combine: true } }
+ let(:uid) { 'no_such_user' }
+ let(:user_options) { options.merge(uid: uid) }
+
+ it 'exists' do
+ expect(type).not_to be_nil
+ end
+
+ # make Puppet::Util::SUIDManager return something deterministic, not the
+ # uid of the user running the tests, except where overridden below.
+ before :each do
+ Puppet::Util::SUIDManager.stubs(:uid).returns 1234
+ end
+
+ describe '#read' do
+ before(:each) do
+ Puppet::Util.stubs(:uid).with(uid).returns 9000
+ end
+
+ it 'runs crontab -l as the target user' do
+ Puppet::Util::Execution
+ .expects(:execute)
+ .with(['crontab', '-l'], user_options)
+ .returns(Puppet::Util::Execution::ProcessOutput.new(crontab, 0))
+
+ expect(cron.read).to eq(crontab)
+ end
+
+ it 'does not switch user if current user is the target user' do
+ Puppet::Util.expects(:uid).with(uid).twice.returns 9000
+ Puppet::Util::SUIDManager.expects(:uid).returns 9000
+ Puppet::Util::Execution
+ .expects(:execute).with(['crontab', '-l'], options)
+ .returns(Puppet::Util::Execution::ProcessOutput.new(crontab, 0))
+ expect(cron.read).to eq(crontab)
+ end
+
+ it 'treats an absent crontab as empty' do
+ Puppet::Util::Execution.expects(:execute).with(['crontab', '-l'], user_options).raises(Puppet::ExecutionFailure, absent_crontab)
+ expect(cron.read).to eq('')
+ end
+
+ it "treats a nonexistent user's crontab as empty" do
+ Puppet::Util.expects(:uid).with(uid).returns nil
+
+ expect(cron.read).to eq('')
+ end
+
+ it 'returns empty if the user is not authorized to use cron' do
+ Puppet::Util::Execution.expects(:execute).with(['crontab', '-l'], user_options).raises(Puppet::ExecutionFailure, unauthorized_crontab)
+ expect(cron.read).to eq('')
+ end
+ end
+
+ describe '#remove' do
+ it 'runs crontab -r as the target user' do
+ Puppet::Util::Execution.expects(:execute).with(['crontab', '-r'], user_options)
+ cron.remove
+ end
+
+ it 'does not switch user if current user is the target user' do
+ Puppet::Util.expects(:uid).with(uid).returns 9000
+ Puppet::Util::SUIDManager.expects(:uid).returns 9000
+ Puppet::Util::Execution.expects(:execute).with(['crontab', '-r'], options)
+ cron.remove
+ end
+ end
+
+ describe '#write' do
+ let!(:tmp_cron) { Tempfile.new('puppet_crontab_spec') }
+ let!(:tmp_cron_path) { tmp_cron.path }
+
+ before :each do
+ Puppet::Util.stubs(:uid).with(uid).returns 9000
+ Tempfile.expects(:new).with("puppet_#{name}", encoding: Encoding.default_external).returns tmp_cron
+ end
+
+ after :each do
+ File.unstub(:chown)
+ end
+
+ it 'runs crontab as the target user on a temporary file' do
+ File.expects(:chown).with(9000, nil, tmp_cron_path)
+ Puppet::Util::Execution.expects(:execute).with(['crontab', tmp_cron_path], user_options)
+
+ tmp_cron.expects(:print).with("foo\n")
+ cron.write "foo\n"
+
+ expect(Puppet::FileSystem).not_to exist(tmp_cron_path)
+ end
+
+ it 'does not switch user if current user is the target user' do
+ Puppet::Util::SUIDManager.expects(:uid).returns 9000
+ File.expects(:chown).with(9000, nil, tmp_cron_path)
+ Puppet::Util::Execution.expects(:execute).with(['crontab', tmp_cron_path], options)
+
+ tmp_cron.expects(:print).with("foo\n")
+ cron.write "foo\n"
+
+ expect(Puppet::FileSystem).not_to exist(tmp_cron_path)
+ end
+ end
+ end
+
+ describe 'the suntab filetype', unless: Puppet::Util::Platform.windows? do
+ let(:type) { described_class.filetype(:suntab) }
+ let(:name) { type.name }
+ let(:crontab_output) { 'suntab_output' }
+
+ # possible crontab output was taken from here:
+ # https://docs.oracle.com/cd/E19082-01/819-2380/sysrescron-60/index.html
+ let(:absent_crontab) do
+ 'crontab: can\'t open your crontab file'
+ end
+ let(:unauthorized_crontab) do
+ 'crontab: you are not authorized to use cron. Sorry.'
+ end
+
+ it_behaves_like 'crontab provider'
+ end
+
+ describe 'the aixtab filetype', unless: Puppet::Util::Platform.windows? do
+ let(:type) { described_class.filetype(:aixtab) }
+ let(:name) { type.name }
+ let(:crontab_output) { 'aixtab_output' }
+
+ let(:absent_crontab) do
+ '0481-103 Cannot open a file in the /var/spool/cron/crontabs directory.'
+ end
+ let(:unauthorized_crontab) do
+ '0481-109 You are not authorized to use the cron command.'
+ end
+
+ it_behaves_like 'crontab provider'
+ end
+end
diff --git a/spec/unit/provider/cron/parsed_spec.rb b/spec/unit/provider/cron/parsed_spec.rb
index d54129c..b115f9d 100644
--- a/spec/unit/provider/cron/parsed_spec.rb
+++ b/spec/unit/provider/cron/parsed_spec.rb
@@ -64,17 +64,17 @@ describe Puppet::Type.type(:cron).provider(:crontab) do
describe 'when determining the correct filetype' do
it 'uses the suntab filetype on Solaris' do
Facter.stubs(:value).with(:osfamily).returns 'Solaris'
- expect(described_class.filetype).to eq(Puppet::Util::FileType::FileTypeSuntab)
+ expect(described_class.filetype).to eq(Puppet::Provider::Cron::FileType::FileTypeSuntab)
end
it 'uses the aixtab filetype on AIX' do
Facter.stubs(:value).with(:osfamily).returns 'AIX'
- expect(described_class.filetype).to eq(Puppet::Util::FileType::FileTypeAixtab)
+ expect(described_class.filetype).to eq(Puppet::Provider::Cron::FileType::FileTypeAixtab)
end
it 'uses the crontab filetype on other platforms' do
Facter.stubs(:value).with(:osfamily).returns 'Not a real operating system family'
- expect(described_class.filetype).to eq(Puppet::Util::FileType::FileTypeCrontab)
+ expect(described_class.filetype).to eq(Puppet::Provider::Cron::FileType::FileTypeCrontab)
end
end
@@ -194,20 +194,25 @@ describe Puppet::Type.type(:cron).provider(:crontab) do
Facter.stubs(:value).with(:operatingsystem)
end
- it 'contains no resources for a user who has no crontab, or for a user that is absent' do
- if Puppet.version.to_f < 5.0
- described_class.target_object('foobar').expects(:`).with('crontab -u foobar -l 2>/dev/null').returns ''
- else
- Puppet::Util::Execution
- .expects(:execute)
- .with('crontab -u foobar -l', failonfail: true, combine: true)
- .returns('')
- end
+ it 'contains no resources for a user who has no crontab' do
+ Puppet::Util.stubs(:uid).returns(10)
+
+ Puppet::Util::Execution
+ .expects(:execute)
+ .with('crontab -u foobar -l', failonfail: true, combine: true)
+ .returns('')
+
expect(described_class.instances.select do |resource|
resource.get('target') == 'foobar'
end).to be_empty
end
+ it 'contains no resources for a user who is absent' do
+ Puppet::Util.stubs(:uid).returns(nil)
+
+ expect(described_class.instances).to be_empty
+ end
+
it 'is able to create records from not-managed records' do
described_class.stubs(:target_object).returns File.new(my_fixture('simple'))
parameters = described_class.instances.map do |p|