diff --git a/lib/puppet/provider/exec/posix.rb b/lib/puppet/provider/exec/posix.rb index 157d0f28d..782f1eac6 100644 --- a/lib/puppet/provider/exec/posix.rb +++ b/lib/puppet/provider/exec/posix.rb @@ -1,115 +1,115 @@ Puppet::Type.type(:exec).provide :posix do include Puppet::Util::Execution confine :feature => :posix defaultfor :feature => :posix desc <<-EOT Executes external binaries directly, without passing through a shell or performing any interpolation. This is a safer and more predictable way to execute most commands, but prevents the use of globbing and shell built-ins (including control logic like "for" and "if" statements). EOT def run(command, check = false) output = nil status = nil dir = nil checkexe(command) if dir = resource[:cwd] unless File.directory?(dir) if check dir = nil else self.fail "Working directory '#{dir}' does not exist" end end end dir ||= Dir.pwd debug "Executing#{check ? " check": ""} '#{command}'" begin # Do our chdir Dir.chdir(dir) do environment = {} environment[:PATH] = resource[:path].join(":") if resource[:path] if envlist = resource[:environment] envlist = [envlist] unless envlist.is_a? Array envlist.each do |setting| if setting =~ /^(\w+)=((.|\n)+)$/ env_name = $1 value = $2 if environment.include?(env_name) || environment.include?(env_name.to_sym) warning "Overriding environment setting '#{env_name}' with '#{value}'" end environment[env_name] = value else warning "Cannot understand environment setting #{setting.inspect}" end end end withenv environment do Timeout::timeout(resource[:timeout]) do output, status = Puppet::Util::SUIDManager. run_and_capture([command], resource[:user], resource[:group]) end # The shell returns 127 if the command is missing. if status.exitstatus == 127 raise ArgumentError, output end end end rescue Errno::ENOENT => detail self.fail detail.to_s end return output, status end # Verify that we have the executable def checkexe(command) exe = extractexe(command) - if resource[:path] - if Puppet.features.posix? and !File.exists?(exe) - withenv :PATH => resource[:path].join(File::PATH_SEPARATOR) do - exe = which(exe) || raise(ArgumentError,"Could not find command '#{exe}'") - end - elsif Puppet.features.microsoft_windows? and !File.exists?(exe) - resource[:path].each do |path| - [".exe", ".ps1", ".bat", ".com", ""].each do |extension| - file = File.join(path, exe+extension) - return if File.exists?(file) - end - end + if File.expand_path(exe) == exe + if !File.exists?(exe) + raise ArgumentError, "Could not find command '#{exe}'" + elsif !File.file?(exe) + raise ArgumentError, "'#{exe}' is a #{File.ftype(exe)}, not a file" + elsif !File.executable?(exe) + raise ArgumentError, "'#{exe}' is not executable" end + return end - raise ArgumentError, "Could not find command '#{exe}'" unless File.exists?(exe) - unless File.executable?(exe) - raise ArgumentError, - "'#{exe}' is not executable" + if resource[:path] + withenv :PATH => resource[:path].join(File::PATH_SEPARATOR) do + return if which(exe) + end end + + # 'which' will only return the command if it's executable, so we can't + # distinguish not found from not executable + raise ArgumentError, "Could not find command '#{exe}'" end def extractexe(command) # easy case: command was quoted if command =~ /^"([^"]+)"/ $1 else command.split(/ /)[0] end end def validatecmd(command) exe = extractexe(command) # if we're not fully qualified, require a path self.fail "'#{command}' is not qualified and no path was specified. Please qualify the command or specify a path." if File.expand_path(exe) != exe and resource[:path].nil? end end diff --git a/spec/unit/provider/exec/posix_spec.rb b/spec/unit/provider/exec/posix_spec.rb index 50697d826..876b9724d 100755 --- a/spec/unit/provider/exec/posix_spec.rb +++ b/spec/unit/provider/exec/posix_spec.rb @@ -1,120 +1,119 @@ #!/usr/bin/env rspec require 'spec_helper' -provider_class = Puppet::Type.type(:exec).provider(:posix) +describe Puppet::Type.type(:exec).provider(:posix) do + include PuppetSpec::Files + + def make_exe + command = tmpfile('my_command') + FileUtils.touch(command) + File.chmod(0755, command) + command + end + + let(:resource) { Puppet::Type.type(:exec).new(:title => '/foo') } + let(:provider) { described_class.new(resource) } -describe provider_class do before :each do - @resource = Puppet::Resource.new(:exec, 'foo') - @provider = provider_class.new(@resource) + Puppet.features.stubs(:posix?).returns(true) + Puppet.features.stubs(:microsoft_windows?).returns(false) + end + + describe "#validatecmd" do + it "should fail if no path is specified and the command is not fully qualified" do + expect { provider.validatecmd("foo") }.to raise_error( + Puppet::Error, + "'foo' is not qualified and no path was specified. Please qualify the command or specify a path." + ) + end + + it "should pass if a path is given" do + provider.resource[:path] = ['/bogus/bin'] + provider.validatecmd("../foo") + end + + it "should pass if command is fully qualifed" do + provider.resource[:path] = ['/bogus/bin'] + provider.validatecmd("/bin/blah/foo") + end end - ["posix", "microsoft_windows"].each do |feature| - describe "when in #{feature} environment" do - before :each do - if feature == "microsoft_windows" - Puppet.features.stubs(:microsoft_windows?).returns(true) - Puppet.features.stubs(:posix?).returns(false) - else - Puppet.features.stubs(:posix?).returns(true) - Puppet.features.stubs(:microsoft_windows?).returns(false) - end + describe "#run" do + describe "when the command is an absolute path" do + let(:command) { tmpfile('foo') } + + it "should fail if the command doesn't exist" do + expect { provider.run(command) }.to raise_error(ArgumentError, "Could not find command '#{command}'") + end + + it "should fail if the command isn't a file" do + FileUtils.mkdir(command) + FileUtils.chmod(0755, command) + + expect { provider.run(command) }.to raise_error(ArgumentError, "'#{command}' is a directory, not a file") + end + + it "should fail if the command isn't executable" do + FileUtils.touch(command) + + expect { provider.run(command) }.to raise_error(ArgumentError, "'#{command}' is not executable") + end + end + + describe "when the command is a relative path" do + it "should execute the command if it finds it in the path and is executable" do + command = make_exe + provider.resource[:path] = [File.dirname(command)] + filename = File.basename(command) + + Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == [filename]) && (arguments.is_a? Hash) } + + provider.run(filename) end - describe "#validatecmd" do - it "should fail if no path is specified and the command is not fully qualified" do - lambda { @provider.validatecmd("foo") }.should raise_error( - Puppet::Error, - "'foo' is not qualified and no path was specified. Please qualify the command or specify a path." - ) - end - - it "should pass if a path is given" do - @provider.resource[:path] = ['/bogus/bin'] - @provider.validatecmd("../foo") - end - - it "should pass if command is fully qualifed" do - @provider.resource[:path] = ['/bogus/bin'] - @provider.validatecmd("/bin/blah/foo") - end + it "should fail if the command isn't in the path" do + resource[:path] = ["/fake/path"] + + expect { provider.run('foo') }.to raise_error(ArgumentError, "Could not find command 'foo'") end - describe "#run" do - it "should fail if no path is specified and command does not exist" do - lambda { @provider.run("foo") }.should raise_error(ArgumentError, "Could not find command 'foo'") - end - - it "should fail if the command isn't in the path" do - @provider.resource[:path] = ['/bogus/bin'] - lambda { @provider.run("foo") }.should raise_error(ArgumentError, "Could not find command 'foo'") - end - - it "should fail if the command isn't executable" do - @provider.resource[:path] = ['/bogus/bin'] - File.stubs(:exists?).with("foo").returns(true) - - lambda { @provider.run("foo") }.should raise_error(ArgumentError, "'foo' is not executable") - end - - it "should not be able to execute shell builtins" do - @provider.resource[:path] = ['/bin'] - lambda { @provider.run("cd ..") }.should raise_error(ArgumentError, "Could not find command 'cd'") - end - - it "should execute the command if the command given includes arguments or subcommands" do - @provider.resource[:path] = ['/bogus/bin'] - File.stubs(:exists?).returns(false) - File.stubs(:exists?).with("foo").returns(true) - File.stubs(:executable?).with("foo").returns(true) - - Puppet::Util.expects(:execute).with() { |command, arguments| (command == ['foo bar --sillyarg=true --blah']) && (arguments.is_a? Hash) } - @provider.run("foo bar --sillyarg=true --blah") - end - - it "should fail if quoted command doesn't exist" do - @provider.resource[:path] = ['/bogus/bin'] - File.stubs(:exists?).returns(false) - File.stubs(:exists?).with("foo").returns(true) - File.stubs(:executable?).with("foo").returns(true) - - lambda { @provider.run('"foo bar --sillyarg=true --blah"') }.should raise_error(ArgumentError, "Could not find command 'foo bar --sillyarg=true --blah'") - end - - it "should execute the command if it finds it in the path and is executable" do - @provider.resource[:path] = ['/bogus/bin'] - File.stubs(:exists?).with("foo").returns(true) - File.stubs(:executable?).with("foo").returns(true) - Puppet::Util.expects(:execute).with() { |command, arguments| (command == ['foo']) && (arguments.is_a? Hash) } - - @provider.run("foo") - end - - if feature == "microsoft_windows" - [".exe", ".ps1", ".bat", ".com", ""].each do |extension| - it "should check file extension #{extension} when it can't find the executable" do - @provider.resource[:path] = ['/bogus/bin'] - File.stubs(:exists?).returns(false) - File.stubs(:exists?).with("/bogus/bin/foo#{extension}").returns(true) - File.stubs(:executable?).with("foo").returns(true) - Puppet::Util.expects(:execute).with() { |command, arguments| (command == ['foo']) && (arguments.is_a? Hash) } - - @provider.run("foo") - end - end - end - - it "should warn if you're overriding something in environment" do - @provider.resource[:environment] = ['WHATEVER=/something/else', 'WHATEVER=/foo'] - File.stubs(:exists?).returns(false) - File.stubs(:exists?).with("foo").returns(true) - File.stubs(:executable?).with("foo").returns(true) - - Puppet::Util.expects(:execute).with() { |command, arguments| (command == ['foo']) && (arguments.is_a? Hash) } - @provider.run("foo") - @logs.map {|l| "#{l.level}: #{l.message}" }.should == ["warning: Overriding environment setting 'WHATEVER' with '/foo'"] - end + it "should fail if the command is in the path but not executable" do + command = tmpfile('foo') + FileUtils.touch(command) + resource[:path] = [File.dirname(command)] + filename = File.basename(command) + + expect { provider.run(filename) }.to raise_error(ArgumentError, "Could not find command '#{filename}'") end end + + it "should not be able to execute shell builtins" do + provider.resource[:path] = ['/bin'] + expect { provider.run("cd ..") }.to raise_error(ArgumentError, "Could not find command 'cd'") + end + + it "should execute the command if the command given includes arguments or subcommands" do + provider.resource[:path] = ['/bogus/bin'] + command = make_exe + + Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == ["#{command} bar --sillyarg=true --blah"]) && (arguments.is_a? Hash) } + provider.run("#{command} bar --sillyarg=true --blah") + end + + it "should fail if quoted command doesn't exist" do + provider.resource[:path] = ['/bogus/bin'] + command = "/foo bar --sillyarg=true --blah" + + expect { provider.run(%Q["#{command}"]) }.to raise_error(ArgumentError, "Could not find command '#{command}'") + end + + it "should warn if you're overriding something in environment" do + provider.resource[:environment] = ['WHATEVER=/something/else', 'WHATEVER=/foo'] + command = make_exe + + Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline== [command]) && (arguments.is_a? Hash) } + provider.run(command) + @logs.map {|l| "#{l.level}: #{l.message}" }.should == ["warning: Overriding environment setting 'WHATEVER' with '/foo'"] + end end end