diff --git a/lib/puppet/util/filetype.rb b/lib/puppet/util/filetype.rb index 51b0d9175..5711aa5cd 100755 --- a/lib/puppet/util/filetype.rb +++ b/lib/puppet/util/filetype.rb @@ -1,281 +1,296 @@ # Basic classes for reading, writing, and emptying files. Not much # to see here. require 'puppet/util/selinux' require 'tempfile' require 'fileutils' class Puppet::Util::FileType attr_accessor :loaded, :path, :synced include Puppet::Util::SELinux class << self attr_accessor :name include Puppet::Util::ClassGen end # Create a new filetype. def self.newfiletype(name, &block) @filetypes ||= {} - - klass = genclass( - name, + klass = genclass( + name, :block => block, :prefix => "FileType", - :hash => @filetypes ) # Rename the read and write methods, so that we're sure they # maintain the stats. klass.class_eval do # Rename the read method define_method(:real_read, instance_method(:read)) define_method(:read) do begin val = real_read @loaded = Time.now if val return val.gsub(/# HEADER.*\n/,'') else return "" end rescue Puppet::Error => detail raise rescue => detail puts detail.backtrace if Puppet[:trace] raise Puppet::Error, "#{self.class} could not read #{@path}: #{detail}" end end # And then the write method define_method(:real_write, instance_method(:write)) define_method(:write) do |text| begin val = real_write(text) @synced = Time.now return val rescue Puppet::Error => detail raise rescue => detail puts detail.backtrace if Puppet[:debug] raise Puppet::Error, "#{self.class} could not write #{@path}: #{detail}" end end end end def self.filetype(type) @filetypes[type] end # Pick or create a filebucket to use. def bucket @bucket ||= Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end def initialize(path) raise ArgumentError.new("Path is nil") if path.nil? @path = path end + # Arguments that will be passed to the execute method. Will set the uid + # to the target user if the target user and the current user are not + # the same + def cronargs + if uid = Puppet::Util.uid(@path) and uid == Puppet::Util::SUIDManager.uid + {:failonfail => true, :combine => true} + else + {:failonfail => true, :combine => true, :uid => @path} + end + end + # Operate on plain files. newfiletype(:flat) do # Back the file up before replacing it. def backup bucket.backup(@path) if File.exists?(@path) end # Read the file. def read if File.exist?(@path) File.read(@path) else return nil end end # Remove the file. def remove File.unlink(@path) if File.exist?(@path) end # Overwrite the file. def write(text) tf = Tempfile.new("puppet") tf.print text; tf.flush FileUtils.cp(tf.path, @path) tf.close # If SELinux is present, we need to ensure the file has its expected context set_selinux_default_context(@path) end end # Operate on plain files. newfiletype(:ram) do @@tabs = {} def self.clear @@tabs.clear end def initialize(path) super @@tabs[@path] ||= "" end # Read the file. def read Puppet.info "Reading #{@path} from RAM" @@tabs[@path] end # Remove the file. def remove Puppet.info "Removing #{@path} from RAM" @@tabs[@path] = "" end # Overwrite the file. def write(text) Puppet.info "Writing #{@path} to RAM" @@tabs[@path] = text end end # Handle Linux-style cron tabs. newfiletype(:crontab) do def initialize(user) self.path = user end def path=(user) begin @uid = Puppet::Util.uid(user) rescue Puppet::Error => detail - raise Puppet::Error, "Could not retrieve user #{user}" + raise Puppet::Error, "Could not retrieve user #{user}: #{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 %x{#{cmdbase} -l 2>/dev/null} end # Remove a specific @path's cron tab. def remove if %w{Darwin FreeBSD}.include?(Facter.value("operatingsystem")) %x{/bin/echo yes | #{cmdbase} -r 2>/dev/null} else %x{#{cmdbase} -r 2>/dev/null} end 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) IO.popen("#{cmdbase()} -", "w") { |p| p.print text } 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 cmd = nil if @uid == Puppet::Util::SUIDManager.uid || Facter.value(:operatingsystem) == "HP-UX" return "crontab" else return "crontab -u #{@path}" end 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 - output = Puppet::Util.execute(%w{crontab -l}, :uid => @path) - return "" if output.include?("can't open your crontab") - raise Puppet::Error, "User #{@path} not authorized to use cron" if output.include?("you are not authorized to use cron") - return output + Puppet::Util.execute(%w{crontab -l}, cronargs) rescue => detail - raise Puppet::Error, "Could not read crontab for #{@path}: #{detail}" + case detail.to_s + when /can't open your crontab/ + return "" + when /you are not authorized to use cron/ + raise Puppet::Error, "User #{@path} not authorized to use cron", detail.backtrace + else + raise Puppet::Error, "Could not read crontab for #{@path}: #{detail}", detail.backtrace + end end # Remove a specific @path's cron tab. def remove - Puppet::Util.execute(%w{crontab -r}, :uid => @path) + Puppet::Util.execute(%w{crontab -r}, cronargs) rescue => detail - raise Puppet::Error, "Could not remove crontab for #{@path}: #{detail}" + raise Puppet::Error, "Could not remove crontab for #{@path}: #{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) - puts text - output_file = Tempfile.new("puppet") - fh = output_file.open - fh.print text - fh.close - - # We have to chown the stupid file to the user. - File.chown(Puppet::Util.uid(@path), nil, output_file.path) - + output_file = Tempfile.new("puppet_suntab") begin - Puppet::Util.execute(["crontab", output_file.path], :uid => @path) + 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.execute(["crontab", output_file.path], cronargs) rescue => detail - raise Puppet::Error, "Could not write crontab for #{@path}: #{detail}" + raise Puppet::Error, "Could not write crontab for #{@path}: #{detail}", detail.backtrace + ensure + output_file.close + output_file.unlink end - output_file.delete 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 - output = Puppet::Util.execute(%w{crontab -l}, :uid => @path) - raise Puppet::Error, "User #{@path} not authorized to use cron" if output.include?("You are not authorized to use the cron command") - return output + Puppet::Util.execute(%w{crontab -l}, cronargs) rescue => detail - raise Puppet::Error, "Could not read crontab for #{@path}: #{detail}" + case detail.to_s + when /Cannot open a file in the .* directory/ + return "" + when /You are not authorized to use the cron command/ + raise Puppet::Error, "User #{@path} not authorized to use cron", detail.backtrace + else + raise Puppet::Error, "Could not read crontab for #{@path}: #{detail}", detail.backtrace + end end # Remove a specific @path's cron tab. def remove - Puppet::Util.execute(%w{crontab -r}, :uid => @path) + Puppet::Util.execute(%w{crontab -r}, cronargs) rescue => detail - raise Puppet::Error, "Could not remove crontab for #{@path}: #{detail}" + raise Puppet::Error, "Could not remove crontab for #{@path}: #{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) - output_file = Tempfile.new("puppet") - fh = output_file.open - fh.print text - fh.close - - # We have to chown the stupid file to the user. - File.chown(Puppet::Util.uid(@path), nil, output_file.path) + output_file = Tempfile.new("puppet_aixtab") begin - Puppet::Util.execute(["crontab", output_file.path], :uid => @path) + 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.execute(["crontab", output_file.path], cronargs) rescue => detail - raise Puppet::Error, "Could not write crontab for #{@path}: #{detail}" + raise Puppet::Error, "Could not write crontab for #{@path}: #{detail}", detail.backtrace ensure - output_file.delete + output_file.close + output_file.unlink end end end end diff --git a/spec/fixtures/unit/util/filetype/aixtab_output b/spec/fixtures/unit/util/filetype/aixtab_output new file mode 100644 index 000000000..0b276454f --- /dev/null +++ b/spec/fixtures/unit/util/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/util/filetype/suntab_output b/spec/fixtures/unit/util/filetype/suntab_output new file mode 100644 index 000000000..e6ca376b5 --- /dev/null +++ b/spec/fixtures/unit/util/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/util/filetype_spec.rb b/spec/unit/util/filetype_spec.rb index a2c0da660..67d6fbdae 100755 --- a/spec/unit/util/filetype_spec.rb +++ b/spec/unit/util/filetype_spec.rb @@ -1,99 +1,205 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/util/filetype' # XXX Import all of the tests into this file. describe Puppet::Util::FileType do - describe "when backing up a file" do - before do - @file = Puppet::Util::FileType.filetype(:flat).new("/my/file") - end - - it "should do nothing if the file does not exist" do - File.expects(:exists?).with("/my/file").returns false - @file.expects(:bucket).never - @file.backup - end - - it "should use its filebucket to backup the file if it exists" do - File.expects(:exists?).with("/my/file").returns true - - bucket = mock 'bucket' - bucket.expects(:backup).with("/my/file") - - @file.expects(:bucket).returns bucket - @file.backup - end - - it "should use the default filebucket" do - bucket = mock 'bucket' - bucket.expects(:bucket).returns "mybucket" - - Puppet::Type.type(:filebucket).expects(:mkdefaultbucket).returns bucket - - @file.bucket.should == "mybucket" - end - end - describe "the flat filetype" do - before do - @type = Puppet::Util::FileType.filetype(:flat) - end + let(:path) { '/my/file' } + let(:type) { Puppet::Util::FileType.filetype(:flat) } + let(:file) { type.new(path) } + it "should exist" do - @type.should_not be_nil + type.should_not be_nil end describe "when the file already exists" do it "should return the file's contents when asked to read it" do - file = @type.new("/my/file") - File.expects(:exist?).with("/my/file").returns true - File.expects(:read).with("/my/file").returns "my text" + File.expects(:exist?).with(path).returns true + File.expects(:read).with(path).returns "my text" file.read.should == "my text" end it "should unlink the file when asked to remove it" do - file = @type.new("/my/file") - File.expects(:exist?).with("/my/file").returns true - File.expects(:unlink).with("/my/file") + File.expects(:exist?).with(path).returns true + File.expects(:unlink).with(path) file.remove end end describe "when the file does not exist" do it "should return an empty string when asked to read the file" do - file = @type.new("/my/file") - File.expects(:exist?).with("/my/file").returns false + File.expects(:exist?).with(path).returns false file.read.should == "" end end describe "when writing the file" do + let(:tempfile) { stub 'tempfile', :print => nil, :close => nil, :flush => nil, :path => "/other/file" } before do - @file = @type.new("/my/file") FileUtils.stubs(:cp) - - @tempfile = stub 'tempfile', :print => nil, :close => nil, :flush => nil, :path => "/other/file" - Tempfile.stubs(:new).returns @tempfile + Tempfile.stubs(:new).returns tempfile end it "should first create a temp file and copy its contents over to the file location" do - Tempfile.expects(:new).with("puppet").returns @tempfile - @tempfile.expects(:print).with("my text") - @tempfile.expects(:flush) - @tempfile.expects(:close) - FileUtils.expects(:cp).with(@tempfile.path, "/my/file") + Tempfile.expects(:new).with("puppet").returns tempfile + tempfile.expects(:print).with("my text") + tempfile.expects(:flush) + tempfile.expects(:close) + FileUtils.expects(:cp).with(tempfile.path, path) - @file.write "my text" + file.write "my text" end it "should set the selinux default context on the file" do - @file.expects(:set_selinux_default_context).with("/my/file") - @file.write "eh" + file.expects(:set_selinux_default_context).with(path) + file.write "eh" end end + + describe "when backing up a file" do + it "should do nothing if the file does not exist" do + File.expects(:exists?).with(path).returns false + file.expects(:bucket).never + file.backup + end + + it "should use its filebucket to backup the file if it exists" do + File.expects(:exists?).with(path).returns true + + bucket = mock 'bucket' + bucket.expects(:backup).with(path) + + file.expects(:bucket).returns bucket + file.backup + end + + it "should use the default filebucket" do + bucket = mock 'bucket' + bucket.expects(:bucket).returns "mybucket" + + Puppet::Type.type(:filebucket).expects(:mkdefaultbucket).returns bucket + + file.bucket.should == "mybucket" + end + end + end + + 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 "should exist" do + type.should_not be_nil + end + + describe "#read" do + it "should run crontab -l as the target user" do + Puppet::Util.expects(:execute).with(['crontab', '-l'], user_options).returns crontab + cron.read.should == crontab + end + + it "should 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.expects(:execute).with(['crontab', '-l'], options).returns crontab + cron.read.should == crontab + end + + it "should treat an absent crontab as empty" do + Puppet::Util.expects(:execute).with(['crontab', '-l'], user_options).raises(Puppet::ExecutionFailure, absent_crontab) + cron.read.should == '' + end + + it "should raise an error if the user is not authorized to use cron" do + Puppet::Util.expects(:execute).with(['crontab', '-l'], user_options).raises(Puppet::ExecutionFailure, unauthorized_crontab) + expect { + cron.read + }.to raise_error Puppet::Error, /User #{uid} not authorized to use cron/ + end + end + + describe "#remove" do + it "should run crontab -r as the target user" do + Puppet::Util.expects(:execute).with(['crontab', '-r'], user_options) + cron.remove + end + + it "should 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.expects(:execute).with(['crontab','-r'], options) + cron.remove + end + end + + describe "#write" do + before :each do + @tmp_cron = Tempfile.new("puppet_crontab_spec") + @tmp_cron_path = @tmp_cron.path + Puppet::Util.stubs(:uid).with(uid).returns 9000 + Tempfile.expects(:new).with("puppet_#{name}").returns @tmp_cron + end + + after :each do + File.should_not be_exist @tmp_cron_path + end + + it "should run crontab as the target user on a temporary file" do + File.expects(:chown).with(9000, nil, @tmp_cron_path) + Puppet::Util.expects(:execute).with(["crontab", @tmp_cron_path], user_options) + + @tmp_cron.expects(:print).with("foo\n") + cron.write "foo\n" + end + + it "should 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.expects(:execute).with(["crontab", @tmp_cron_path], options) + + @tmp_cron.expects(:print).with("foo\n") + cron.write "foo\n" + end + end + end + + describe "the suntab filetype" do + let(:type) { Puppet::Util::FileType.filetype(:suntab) } + let(:name) { type.name } + let(:crontab_output) { 'suntab_output' } + + # possible crontab output was taken from here: + # http://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_should_behave_like "crontab provider" + end + + describe "the aixtab filetype" do + let(:type) { Puppet::Util::FileType.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_should_behave_like "crontab provider" end end