From: Daniel Pittman <dan...@rimspace.net> This is in preparation for allowing other new providers to handle exec commands differently.
Reviewed-by: Max Martin and Matt Robinson Signed-off-by: Max Martin <m...@puppetlabs.com> --- lib/puppet/provider/exec/posix.rb | 112 ++++++ lib/puppet/type/exec.rb | 179 ++-------- spec/unit/provider/exec/posix_spec.rb | 115 ++++++ spec/unit/type/exec_spec.rb | 674 +++++++++++++++++++++++++++++---- 4 files changed, 841 insertions(+), 239 deletions(-) create mode 100644 lib/puppet/provider/exec/posix.rb create mode 100755 spec/unit/provider/exec/posix_spec.rb diff --git a/lib/puppet/provider/exec/posix.rb b/lib/puppet/provider/exec/posix.rb new file mode 100644 index 0000000..92dbd8c --- /dev/null +++ b/lib/puppet/provider/exec/posix.rb @@ -0,0 +1,112 @@ +Puppet::Type.type(:exec).provide :posix do + include Puppet::Util::Execution + + confine :feature => :posix + defaultfor :feature => :posix + + desc "Execute external binaries directly, on POSIX systems. +This does not pass through a shell, or perform any interpolation, but +only directly calls the command with the arguments given." + + 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 + end + end + + raise ArgumentError, "Could not find command '#{exe}'" unless File.exists?(exe) + unless File.executable?(exe) + raise ArgumentError, + "'#{exe}' is not executable" + end + 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/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb index 387c592..67d40a7 100755 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -28,10 +28,10 @@ module Puppet # Create a new check mechanism. It's basically just a parameter that # provides one extra 'check' method. - def self.newcheck(name, &block) + def self.newcheck(name, options = {}, &block) @checks ||= {} - check = newparam(name, &block) + check = newparam(name, options, &block) @checks[name] = check end @@ -63,9 +63,11 @@ module Puppet # First verify that all of our checks pass. def retrieve - # Default to somethinng - - if @resource.check + # We need to return :notrun to trigger evaluation; when that isn't + # true, we *LIE* about what happened and return a "success" for the + # value, which causes us to be treated as in_sync?, which means we + # don't actually execute anything. I think. --daniel 2011-03-10 + if @resource.check_all_attributes return :notrun else return self.should @@ -87,7 +89,7 @@ module Puppet tries.times do |try| # Only add debug messages for tries > 1 to reduce log spam. debug("Exec try #{try+1}/#{tries}") if tries > 1 - @output, @status = @resource.run(self.resource[:command]) + @output, @status = provider.run(self.resource[:command]) break if self.should.include?(@status.exitstatus.to_s) if try_sleep > 0 and tries > 1 debug("Sleeping for #{try_sleep} seconds between tries") @@ -137,7 +139,7 @@ module Puppet newparam(:path) do desc "The search path used for command execution. Commands must be fully qualified if no path is specified. Paths - can be specified as an array or as a colon-separated list." + can be specified as an array or as a colon or semi-colon separated list." # Support both arrays and colon-separated fields. def value=(*values) @@ -174,21 +176,9 @@ module Puppet # Validation is handled by the SUIDManager class. end - newparam(:cwd) do + newparam(:cwd, :parent => Puppet::Parameter::Path) do desc "The directory from which to run the command. If this directory does not exist, the command will fail." - - validate do |dir| - unless dir =~ /^#{File::SEPARATOR}/ - self.fail("CWD must be a fully qualified path") - end - end - - munge do |dir| - dir = dir[0] if dir.is_a?(Array) - - dir - end end newparam(:logoutput) do @@ -207,7 +197,7 @@ module Puppet for refreshing." validate do |command| - @resource.validatecmd(command) + provider.validatecmd(command) end end @@ -331,7 +321,7 @@ module Puppet end end - newcheck(:creates) do + newcheck(:creates, :parent => Puppet::Parameter::Path) do desc "A file that this command creates. If this parameter is provided, then the command will only be run if the specified file does not exist: @@ -344,19 +334,7 @@ module Puppet " - # FIXME if they try to set this and fail, then we should probably - # fail the entire exec, right? - validate do |files| - files = [files] unless files.is_a? Array - - files.each do |file| - self.fail("'creates' must be set to a fully qualified path") unless file - - unless file =~ %r{^#{File::SEPARATOR}} - self.fail "'creates' files must be fully qualified." - end - end - end + accept_arrays # If the file exists, return false (i.e., don't run the command), # else return true @@ -384,15 +362,15 @@ module Puppet validate do |cmds| cmds = [cmds] unless cmds.is_a? Array - cmds.each do |cmd| - @resource.validatecmd(cmd) + cmds.each do |command| + provider.validatecmd(command) end end # Return true if the command does not return 0. def check(value) begin - output, status = @resource.run(value, true) + output, status = provider.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false @@ -426,15 +404,15 @@ module Puppet validate do |cmds| cmds = [cmds] unless cmds.is_a? Array - cmds.each do |cmd| - @resource.validatecmd(cmd) + cmds.each do |command| + provider.validatecmd(command) end end # Return true if the command returns 0. def check(value) begin - output, status = @resource.run(value, true) + output, status = provider.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false @@ -448,7 +426,7 @@ module Puppet @isomorphic = false validate do - validatecmd(self[:command]) + provider.validatecmd(self[:command]) end # FIXME exec should autorequire any exec that 'creates' our cwd @@ -501,7 +479,7 @@ module Puppet # Verify that we pass all of the checks. The argument determines whether # we skip the :refreshonly check, which is necessary because we now check # within refresh - def check(refreshing = false) + def check_all_attributes(refreshing = false) self.class.checks.each { |check| next if refreshing and check == :refreshonly if @parameters.include?(check) @@ -516,32 +494,6 @@ module Puppet true end - # Verify that we have the executable - def checkexe(cmd) - exe = extractexe(cmd) - - if self[:path] - if Puppet.features.posix? and !File.exists?(exe) - withenv :PATH => self[: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) - self[:path].each do |path| - [".exe", ".ps1", ".bat", ".com", ""].each do |extension| - file = File.join(path, exe+extension) - return if File.exists?(file) - end - end - end - end - - raise ArgumentError, "Could not find executable '#{exe}'" unless FileTest.exists?(exe) - unless FileTest.executable?(exe) - raise ArgumentError, - "'#{exe}' is not executable" - end - end - def output if self.property(:returns).nil? return nil @@ -552,98 +504,13 @@ module Puppet # Run the command, or optionally run a separately-specified command. def refresh - if self.check(true) + if self.check_all_attributes(true) if cmd = self[:refresh] - self.run(cmd) + provider.run(cmd) else self.property(:returns).sync end end end - - # Run a command. - def run(command, check = false) - output = nil - status = nil - - dir = nil - - checkexe(command) - - if dir = self[:cwd] - unless File.directory?(dir) - if check - dir = nil - else - self.fail "Working directory '#{dir}' does not exist" - end - end - end - - dir ||= Dir.pwd - - if check - debug "Executing check '#{command}'" - else - debug "Executing '#{command}'" - end - begin - # Do our chdir - Dir.chdir(dir) do - environment = {} - - environment[:PATH] = self[:path].join(":") if self[:path] - - if envlist = self[:environment] - envlist = [envlist] unless envlist.is_a? Array - envlist.each do |setting| - if setting =~ /^(\w+)=((.|\n)+)$/ - name = $1 - value = $2 - if environment.include? name - warning( - "Overriding environment setting '#{name}' with '#{value}'" - ) - end - environment[name] = value - else - warning "Cannot understand environment setting #{setting.inspect}" - end - end - end - - withenv environment do - Timeout::timeout(self[:timeout]) do - output, status = Puppet::Util::SUIDManager.run_and_capture( - [command], self[:user], self[: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 - - def validatecmd(cmd) - exe = extractexe(cmd) - # if we're not fully qualified, require a path - self.fail "'#{cmd}' is not qualified and no path was specified. Please qualify the command or specify a path." if File.expand_path(exe) != exe and self[:path].nil? - end - - def extractexe(cmd) - # easy case: command was quoted - if cmd =~ /^"([^"]+)"/ - $1 - else - cmd.split(/ /)[0] - end - end end end diff --git a/spec/unit/provider/exec/posix_spec.rb b/spec/unit/provider/exec/posix_spec.rb new file mode 100755 index 0000000..a9b2075 --- /dev/null +++ b/spec/unit/provider/exec/posix_spec.rb @@ -0,0 +1,115 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../../spec_helper' + +provider_class = Puppet::Type.type(:exec).provider(:posix) + +describe provider_class do + before :each do + @resource = Puppet::Resource.new(:exec, 'foo') + @provider = provider_class.new(@resource) + 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 + 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 + 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 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(['foo bar --sillyarg=true --blah'], {:uid => nil, :gid => nil, :combine => true, :failonfail => false}) + @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(['foo'], {:uid => nil, :gid => nil, :combine => true, :failonfail => false}) + + @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(['foo'], {:uid => nil, :gid => nil, :combine => true, :failonfail => false}) + + @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(['foo'], {:uid => nil, :gid => nil, :combine => true, :failonfail => false}) + @provider.run("foo") + @logs.map {|l| "#{l.level}: #{l.message}" }.should == ["warning: Overriding environment setting 'WHATEVER' with '/foo'"] + end + end + end + end +end diff --git a/spec/unit/type/exec_spec.rb b/spec/unit/type/exec_spec.rb index e04cfc0..4d691d5 100755 --- a/spec/unit/type/exec_spec.rb +++ b/spec/unit/type/exec_spec.rb @@ -1,31 +1,32 @@ #!/usr/bin/env ruby - require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:exec) do - - def create_resource(command, output, exitstatus, returns = 0) - @user_name = 'some_user_name' + def exec_tester(command, exitstatus = 0, rest = {}) + @user_name = 'some_user_name' @group_name = 'some_group_name' Puppet.features.stubs(:root?).returns(true) - @execer = Puppet::Type.type(:exec).new(:name => command, :path => @example_path, :user => @user_name, :group => @group_name, :returns => returns) - status = stub "process" - status.stubs(:exitstatus).returns(exitstatus) + output = rest.delete(:output) || '' + tries = rest[:tries] || 1 - Puppet::Util::SUIDManager.expects(:run_and_capture).with([command], @user_name, @group_name).returns([output, status]) - end + args = { + :name => command, + :path => @example_path, + :user => @user_name, + :group => @group_name, + :logoutput => false, + :loglevel => :err, + :returns => 0 + }.merge(rest) - def create_logging_resource(command, output, exitstatus, logoutput, loglevel, returns = 0) - create_resource(command, output, exitstatus, returns) - @execer[:logoutput] = logoutput - @execer[:loglevel] = loglevel - end + exec = Puppet::Type.type(:exec).new(args) - def expect_output(output, loglevel) - output.split(/\n/).each do |line| - @execer.property(:returns).expects(loglevel).with(line) - end + status = stub "process", :exitstatus => exitstatus + Puppet::Util::SUIDManager.expects(:run_and_capture).times(tries). + with([command], @user_name, @group_name).returns([output, status]) + + return exec end before do @@ -44,119 +45,626 @@ describe Puppet::Type.type(:exec) do end describe "when execing" do - it "should use the 'run_and_capture' method to exec" do - command = "true" - create_resource(command, "", 0) - - @execer.refresh.should == :executed_command + exec_tester("true").refresh.should == :executed_command end it "should report a failure" do - command = "false" - create_resource(command, "", 1) - - proc { @execer.refresh }.should raise_error(Puppet::Error) + proc { exec_tester('false', 1).refresh }. + should raise_error(Puppet::Error, /^false returned 1 instead of/) end it "should not report a failure if the exit status is specified in a returns array" do - command = "false" - create_resource(command, "", 1, [0,1]) - proc { @execer.refresh }.should_not raise_error(Puppet::Error) + proc { exec_tester("false", 1, :returns => [0, 1]).refresh }.should_not raise_error end it "should report a failure if the exit status is not specified in a returns array" do - command = "false" - create_resource(command, "", 1, [0,100]) - proc { @execer.refresh }.should raise_error(Puppet::Error) + proc { exec_tester('false', 1, :returns => [0, 100]).refresh }. + should raise_error(Puppet::Error, /^false returned 1 instead of/) end it "should log the output on success" do - #Puppet::Util::Log.newdestination :console - command = "false" output = "output1\noutput2\n" - create_logging_resource(command, output, 0, true, :err) - expect_output(output, :err) - @execer.refresh + exec_tester('false', 0, :output => output, :logoutput => true).refresh + output.split("\n").each do |line| + log = @logs.shift + log.level.should == :err + log.message.should == line + end end it "should log the output on failure" do - #Puppet::Util::Log.newdestination :console - command = "false" output = "output1\noutput2\n" - create_logging_resource(command, output, 1, true, :err) - expect_output(output, :err) + proc { exec_tester('false', 1, :output => output, :logoutput => true).refresh }. + should raise_error(Puppet::Error) - proc { @execer.refresh }.should raise_error(Puppet::Error) + output.split("\n").each do |line| + log = @logs.shift + log.level.should == :err + log.message.should == line + end end - end describe "when logoutput=>on_failure is set" do - it "should log the output on failure" do - #Puppet::Util::Log.newdestination :console - command = "false" output = "output1\noutput2\n" - create_logging_resource(command, output, 1, :on_failure, :err) - expect_output(output, :err) + proc { exec_tester('false', 1, :output => output, :logoutput => :on_failure).refresh }. + should raise_error(Puppet::Error, /^false returned 1 instead of/) - proc { @execer.refresh }.should raise_error(Puppet::Error) + output.split("\n").each do |line| + log = @logs.shift + log.level.should == :err + log.message.should == line + end end it "should log the output on failure when returns is specified as an array" do - #Puppet::Util::Log.newdestination :console - command = "false" output = "output1\noutput2\n" - create_logging_resource(command, output, 1, :on_failure, :err, [0, 100]) - expect_output(output, :err) - proc { @execer.refresh }.should raise_error(Puppet::Error) + proc { + exec_tester('false', 1, :output => output, :returns => [0, 100], + :logoutput => :on_failure).refresh + }.should raise_error(Puppet::Error, /^false returned 1 instead of/) + + output.split("\n").each do |line| + log = @logs.shift + log.level.should == :err + log.message.should == line + end end it "shouldn't log the output on success" do - #Puppet::Util::Log.newdestination :console - command = "true" - output = "output1\noutput2\n" - create_logging_resource(command, output, 0, :on_failure, :err) - @execer.property(:returns).expects(:err).never - @execer.refresh + exec_tester('true', 0, :output => "a\nb\nc\n", :logoutput => :on_failure).refresh + @logs.should == [] end end it "shouldn't log the output on success when non-zero exit status is in a returns array" do - #Puppet::Util::Log.newdestination :console - command = "true" - output = "output1\noutput2\n" - create_logging_resource(command, output, 100, :on_failure, :err, [1,100]) - @execer.property(:returns).expects(:err).never - @execer.refresh + exec_tester("true", 100, :output => "a\n", :logoutput => :on_failure, :returns => [1, 100]).refresh + @logs.should == [] end describe " when multiple tries are set," do - it "should repeat the command attempt 'tries' times on failure and produce an error" do - Puppet.features.stubs(:root?).returns(true) - command = "false" - user = "user" - group = "group" tries = 5 - retry_exec = Puppet::Type.type(:exec).new(:name => command, :path => %w{/usr/bin /bin}, :user => user, :group => group, :returns => 0, :tries => tries, :try_sleep => 0) - status = stub "process" - status.stubs(:exitstatus).returns(1) - Puppet::Util::SUIDManager.expects(:run_and_capture).with([command], user, group).times(tries).returns(["", status]) - proc { retry_exec.refresh }.should raise_error(Puppet::Error) + resource = exec_tester("false", 1, :tries => tries, :try_sleep => 0) + proc { resource.refresh }.should raise_error(Puppet::Error) end end it "should be able to autorequire files mentioned in the command" do catalog = Puppet::Resource::Catalog.new - catalog.add_resource Puppet::Type.type(:file).new(:name => @executable) - @execer = Puppet::Type.type(:exec).new(:name => @command) - catalog.add_resource @execer + tmp = Puppet::Type.type(:file).new(:name => "/bin/foo") + catalog.add_resource tmp + execer = Puppet::Type.type(:exec).new(:name => "/bin/foo") + catalog.add_resource execer + + catalog.relationship_graph.dependencies(execer).should == [tmp] + end + + describe "when handling the path parameter" do + expect = %w{one two three four} + { "an array" => expect, + "a colon separated list" => "one:two:three:four", + "a semi-colon separated list" => "one;two;three;four", + "both array and colon lists" => ["one", "two:three", "four"], + "both array and semi-colon lists" => ["one", "two;three", "four"], + "colon and semi-colon lists" => ["one:two", "three;four"] + }.each do |test, input| + it "should accept #{test}" do + type = Puppet::Type.type(:exec).new(:name => @command, :path => input) + type[:path].should == expect + end + end + end + + describe "when setting user" do + it "should fail if we are not root" do + Puppet.features.stubs(:root?).returns(false) + expect { Puppet::Type.type(:exec).new(:name => @command, :user => 'input') }. + should raise_error Puppet::Error, /Parameter user failed/ + end + + ['one', 2, 'root', 4294967295, 4294967296].each do |value| + it "should accept '#{value}' as user if we are root" do + Puppet.features.stubs(:root?).returns(true) + type = Puppet::Type.type(:exec).new(:name => @command, :user => value) + type[:user].should == value + end + end + end + + describe "when setting group" do + shared_examples_for "exec[:group]" do + ['one', 2, 'wheel', 4294967295, 4294967296].each do |value| + it "should accept '#{value}' without error or judgement" do + type = Puppet::Type.type(:exec).new(:name => @command, :group => value) + type[:group].should == value + end + end + end + + describe "when running as root" do + before :each do Puppet.features.stubs(:root?).returns(true) end + it_behaves_like "exec[:group]" + end + + describe "when not running as root" do + before :each do Puppet.features.stubs(:root?).returns(false) end + it_behaves_like "exec[:group]" + end + end + + describe "when setting cwd" do + it_should_behave_like "all path parameters", :cwd, :array => false do + def instance(path) + Puppet::Type.type(:exec).new(:name => '/bin/true', :cwd => path) + end + end + end + + shared_examples_for "all exec command parameters" do |param| + { "relative" => "example", "absolute" => "/bin/example" }.sort.each do |name, command| + describe "if command is #{name}" do + before :each do + @param = param + end + + def test(command, valid) + if @param == :name then + instance = Puppet::Type.type(:exec).new() + else + instance = Puppet::Type.type(:exec).new(:name => "/bin/true") + end + if valid then + instance.provider.expects(:validatecmd).returns(true) + else + instance.provider.expects(:validatecmd).raises(Puppet::Error, "from a stub") + end + instance[@param] = command + end + + it "should work if the provider calls the command valid" do + expect { test(command, true) }.should_not raise_error + end + + it "should fail if the provider calls the command invalid" do + expect { test(command, false) }. + should raise_error Puppet::Error, /Parameter #{@param} failed: from a stub/ + end + end + end + end + + shared_examples_for "all exec command parameters that take arrays" do |param| + describe "when given an array of inputs" do + before :each do + @test = Puppet::Type.type(:exec).new(:name => "/bin/true") + end + + it "should accept the array when all commands return valid" do + input = %w{one two three} + @test.provider.expects(:validatecmd).times(input.length).returns(true) + @test[param] = input + @test[param].should == input + end + + it "should reject the array when any commands return invalid" do + input = %w{one two three} + @test.provider.expects(:validatecmd).with(input.first).returns(false) + input[1..-1].each do |cmd| + @test.provider.expects(:validatecmd).with(cmd).returns(true) + end + @test[param] = input + @test[param].should == input + end + + it "should reject the array when all commands return invalid" do + input = %w{one two three} + @test.provider.expects(:validatecmd).times(input.length).returns(false) + @test[param] = input + @test[param].should == input + end + end + end + + describe "when setting refresh" do + it_should_behave_like "all exec command parameters", :refresh + end + + describe "for simple parameters" do + before :each do + @exec = Puppet::Type.type(:exec).new(:name => '/bin/true') + end + + describe "when setting env" do + it "should issue a deprecation warning" do + expect { @exec[:env] = 'foo=bar' }.should_not raise_error + @logs.first.message.should =~ /deprecate.*environment/ + end + + it "should update the value of env" do + data = ['foo=bar'] + @exec[:env] = data + @exec[:env].should == data + end + + it "should forward to environment" do + data = ['foo=bar'] + @exec[:env] = data + @exec[:environment].should == data + end + + it "should not override environment if both are set" do + pending "can't fix: too disruptive for 2.6, removed in 2.7" + # ...so this test is here to validate that we know about the problem. + # This ensures correct order of evaluation to trigger the bug; don't + # count on this happening in the constructor. --daniel 2011-03-01 + @exec[:environment] = 'environment=true' + @exec[:env] = 'env=true' + + @exec[:environment].should == "environment=true" + end + end + + describe "when setting environment" do + { "single values" => "foo=bar", + "multiple values" => ["foo=bar", "baz=quux"], + }.each do |name, data| + it "should accept #{name}" do + @exec[:environment] = data + @exec[:environment].should == data + end + end + + { "single values" => "foo", + "only values" => ["foo", "bar"], + "any values" => ["foo=bar", "baz"] + }.each do |name, data| + it "should reject #{name} without assignment" do + expect { @exec[:environment] = data }. + should raise_error Puppet::Error, /Invalid environment setting/ + end + end + end + + describe "when setting timeout" do + [-3.5, -1, 0, 0.1, 1, 10, 4294967295].each do |valid| + it "should accept '#{valid}' as valid" do + @exec[:timeout] = valid + @exec[:timeout].should == valid + end + + it "should accept '#{valid}' in an array as valid" do + @exec[:timeout] = [valid] + @exec[:timeout].should == valid + end + end + + ['1/2', '1_000_000', '+12', '', 'foo'].each do |invalid| + it "should reject '#{invalid}' as invalid" do + expect { @exec[:timeout] = invalid }. + should raise_error Puppet::Error, /The timeout must be a number/ + end + + it "should reject '#{invalid}' in an array as invalid" do + expect { @exec[:timeout] = [invalid] }. + should raise_error Puppet::Error, /The timeout must be a number/ + end + end + + it "should fail if timeout is exceeded" do + File.stubs(:exists?).with('/bin/sleep').returns(true) + sleep_exec = Puppet::Type.type(:exec).new(:name => 'sleep 1', :path => ['/bin'], :timeout => '0.2') + lambda { sleep_exec.refresh }.should raise_error Puppet::Error, "Command exceeded timeout" + end + end + + describe "when setting tries" do + [1, 10, 4294967295].each do |valid| + it "should accept '#{valid}' as valid" do + @exec[:tries] = valid + @exec[:tries].should == valid + end + + if "REVISIT: too much test log spam" == "a good thing" then + it "should accept '#{valid}' in an array as valid" do + pending "inconsistent, but this is not supporting arrays, unlike timeout" + @exec[:tries] = [valid] + @exec[:tries].should == valid + end + end + end + + [-3.5, -1, 0, 0.2, '1/2', '1_000_000', '+12', '', 'foo'].each do |invalid| + it "should reject '#{invalid}' as invalid" do + expect { @exec[:tries] = invalid }. + should raise_error Puppet::Error, /Tries must be an integer/ + end - rels = @execer.autorequire - rels[0].should be_instance_of(Puppet::Relationship) - rels[0].target.should equal(@execer) + if "REVISIT: too much test log spam" == "a good thing" then + it "should reject '#{invalid}' in an array as invalid" do + pending "inconsistent, but this is not supporting arrays, unlike timeout" + expect { @exec[:tries] = [invalid] }. + should raise_error Puppet::Error, /Tries must be an integer/ + end + end + end + end + + describe "when setting try_sleep" do + [0, 0.2, 1, 10, 4294967295].each do |valid| + it "should accept '#{valid}' as valid" do + @exec[:try_sleep] = valid + @exec[:try_sleep].should == valid + end + + if "REVISIT: too much test log spam" == "a good thing" then + it "should accept '#{valid}' in an array as valid" do + pending "inconsistent, but this is not supporting arrays, unlike timeout" + @exec[:try_sleep] = [valid] + @exec[:try_sleep].should == valid + end + end + end + + { -3.5 => "cannot be a negative number", + -1 => "cannot be a negative number", + '1/2' => 'must be a number', + '1_000_000' => 'must be a number', + '+12' => 'must be a number', + '' => 'must be a number', + 'foo' => 'must be a number', + }.each do |invalid, error| + it "should reject '#{invalid}' as invalid" do + expect { @exec[:try_sleep] = invalid }. + should raise_error Puppet::Error, /try_sleep #{error}/ + end + + if "REVISIT: too much test log spam" == "a good thing" then + it "should reject '#{invalid}' in an array as invalid" do + pending "inconsistent, but this is not supporting arrays, unlike timeout" + expect { @exec[:try_sleep] = [invalid] }. + should raise_error Puppet::Error, /try_sleep #{error}/ + end + end + end + end + + describe "when setting refreshonly" do + [:true, :false].each do |value| + it "should accept '#{value}'" do + @exec[:refreshonly] = value + @exec[:refreshonly].should == value + end + end + + [1, 0, "1", "0", "yes", "y", "no", "n"].each do |value| + it "should reject '#{value}'" do + expect { @exec[:refreshonly] = value }. + should raise_error(Puppet::Error, + /Invalid value #{value.inspect}\. Valid values are true, false/ + ) + end + end + end + + describe "when setting creates" do + it_should_behave_like "all path parameters", :creates, :array => true do + def instance(path) + Puppet::Type.type(:exec).new(:name => '/bin/true', :creates => path) + end + end + end + end + + describe "when setting unless" do + it_should_behave_like "all exec command parameters", :unless + it_should_behave_like "all exec command parameters that take arrays", :unless + end + + describe "when setting onlyif" do + it_should_behave_like "all exec command parameters", :onlyif + it_should_behave_like "all exec command parameters that take arrays", :onlyif + end + + describe "#check" do + before :each do + @test = Puppet::Type.type(:exec).new(:name => "/bin/true") + end + + describe ":refreshonly" do + { :true => false, :false => true }.each do |input, result| + it "should return '#{result}' when given '#{input}'" do + @test[:refreshonly] = input + @test.check_all_attributes.should == result + end + end + end + + describe ":creates" do + before :all do + @exist = "/" + @unexist = "/this/path/should/never/exist" + while FileTest.exist?(@unexist) do @unexist += "/foo" end + end + + context "with a single item" do + it "should run when the item does not exist" do + @test[:creates] = @unexist + @test.check_all_attributes.should == true + end + + it "should not run when the item exists" do + @test[:creates] = @exist + @test.check_all_attributes.should == false + end + end + + context "with an array with one item" do + it "should run when the item does not exist" do + @test[:creates] = [@unexist] + @test.check_all_attributes.should == true + end + + it "should not run when the item exists" do + @test[:creates] = [@exist] + @test.check_all_attributes.should == false + end + end + + context "with an array with multiple items" do + it "should run when all items do not exist" do + @test[:creates] = [@unexist] * 3 + @test.check_all_attributes.should == true + end + + it "should not run when one item exists" do + @test[:creates] = [@unexist, @exist, @unexist] + @test.check_all_attributes.should == false + end + + it "should not run when all items exist" do + @test[:creates] = [@exist] * 3 + end + end + end + + { :onlyif => { :pass => false, :fail => true }, + :unless => { :pass => true, :fail => false }, + }.each do |param, sense| + describe ":#{param}" do + before :each do + @pass = "/magic/pass" + @fail = "/magic/fail" + + @pass_status = stub('status', :exitstatus => sense[:pass] ? 0 : 1) + @fail_status = stub('status', :exitstatus => sense[:fail] ? 0 : 1) + + @test.provider.stubs(:checkexe).returns(true) + [true, false].each do |check| + @test.provider.stubs(:run).with(@pass, check). + returns(['test output', @pass_status]) + @test.provider.stubs(:run).with(@fail, check). + returns(['test output', @fail_status]) + end + end + + context "with a single item" do + it "should run if the command exits non-zero" do + @test[param] = @fail + @test.check_all_attributes.should == true + end + + it "should not run if the command exits zero" do + @test[param] = @pass + @test.check_all_attributes.should == false + end + end + + context "with an array with a single item" do + it "should run if the command exits non-zero" do + @test[param] = [@fail] + @test.check_all_attributes.should == true + end + + it "should not run if the command exits zero" do + @test[param] = [@pass] + @test.check_all_attributes.should == false + end + end + + context "with an array with multiple items" do + it "should run if all the commands exits non-zero" do + @test[param] = [@fail] * 3 + @test.check_all_attributes.should == true + end + + it "should not run if one command exits zero" do + @test[param] = [@pass, @fail, @pass] + @test.check_all_attributes.should == false + end + + it "should not run if all command exits zero" do + @test[param] = [@pass] * 3 + @test.check_all_attributes.should == false + end + end + end + end + end + + describe "#retrieve" do + before :each do + @exec_resource = Puppet::Type.type(:exec).new(:name => "/bogus/cmd") + end + + it "should return :notrun when check_all_attributes returns true" do + @exec_resource.stubs(:check_all_attributes).returns true + @exec_resource.retrieve[:returns].should == :notrun + end + + it "should return default exit code 0 when check_all_attributes returns false" do + @exec_resource.stubs(:check_all_attributes).returns false + @exec_resource.retrieve[:returns].should == ['0'] + end + + it "should return the specified exit code when check_all_attributes returns false" do + @exec_resource.stubs(:check_all_attributes).returns false + @exec_resource[:returns] = 42 + @exec_resource.retrieve[:returns].should == ["42"] + end + end + + describe "#output" do + before :each do + @exec_resource = Puppet::Type.type(:exec).new(:name => "/bogus/cmd") + end + + it "should return the provider's run output" do + provider = stub 'provider' + status = stubs "process_status" + status.stubs(:exitstatus).returns("0") + provider.expects(:run).returns(["silly output", status]) + @exec_resource.stubs(:provider).returns(provider) + + @exec_resource.refresh + @exec_resource.output.should == 'silly output' + end + end + + describe "#refresh" do + before :each do + @exec_resource = Puppet::Type.type(:exec).new(:name => "/bogus/cmd") + end + + it "should call provider run with the refresh parameter if it is set" do + provider = stub 'provider' + @exec_resource.stubs(:provider).returns(provider) + @exec_resource.stubs(:[]).with(:refresh).returns('/myother/bogus/cmd') + provider.expects(:run).with('/myother/bogus/cmd') + + @exec_resource.refresh + end + + it "should call provider run with the specified command if the refresh parameter is not set" do + provider = stub 'provider' + status = stubs "process_status" + status.stubs(:exitstatus).returns("0") + provider.expects(:run).with('/bogus/cmd').returns(["silly output", status]) + @exec_resource.stubs(:provider).returns(provider) + + @exec_resource.refresh + end + + it "should not run the provider if check_all_attributes is false" do + @exec_resource.stubs(:check_all_attributes).returns false + provider = stub 'provider' + provider.expects(:run).never + @exec_resource.stubs(:provider).returns(provider) + + @exec_resource.refresh + end end end -- 1.7.4 -- You received this message because you are subscribed to the Google Groups "Puppet Developers" group. To post to this group, send email to puppet-dev@googlegroups.com. To unsubscribe from this group, send email to puppet-dev+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en.