Please review pull request #593: Bug/master/13284 missing env vars during provider command execution opened by (zaphod42)

Description:

Provide a mechanism for better controlling the execution of commands in providers. This is mainly to fix the macports provider losing HOME.

  • Opened: Fri Mar 23 22:10:08 UTC 2012
  • Based on: puppetlabs:master (0b900c9548f6c652f0d7271156b69c35aa232ead)
  • Requested merge: zaphod42:bug/master/13284-missing-env-vars-during-provider-command-execution (52c393c954601868ac411ef3ad7247dc7bab317d)

Diff follows:

diff --git a/lib/puppet/provider.rb b/lib/puppet/provider.rb
index d902206..0e8d9f1 100644
--- a/lib/puppet/provider.rb
+++ b/lib/puppet/provider.rb
@@ -7,6 +7,7 @@ class Puppet::Provider
   extend Puppet::Util::Warnings
 
   require 'puppet/provider/confiner'
+  require 'puppet/provider/command'
 
   extend Puppet::Provider::Confiner
 
@@ -48,8 +49,12 @@ def self.command(name)
   end
 
   # Define commands that are not optional.
-  def self.commands(hash)
-    optional_commands(hash) do |name, path|
+  #
+  # @param [Hash{String => String, Puppet::Provider::Command}] command_specs Named commands that the provider will 
+  #   be executing on the system. Each command is specified with a name and the path of the executable or a 
+  #   Puppet::Provider::Command object.
+  def self.commands(command_specs)
+    optional_commands(command_specs) do |name, path|
       confine :exists => path, :for_binary => true
     end
   end
@@ -107,19 +112,18 @@ def self.instances
   end
 
   # Create the methods for a given command.
+  #
+  # @deprecated Use {#commands} or {#optional_commands} instead. This was not meant to be part of a public API
   def self.make_command_methods(name)
+    Puppet.deprecation_warning "Provider.make_command_methods is deprecated; use Provider.commands or Provider.optional_commands instead for creating command methods"
+
     # Now define a method for that command
     unless singleton_class.method_defined?(name)
       meta_def(name) do |*args|
         raise Puppet::Error, "Command #{name} is missing" unless command(name)
-        if args.empty?
-          cmd = [command(name)]
-        else
-          cmd = [command(name)] + args
-        end
         # This might throw an ExecutionFailure, but the system above
         # will catch it, if so.
-        return execute(cmd)
+        return Puppet::Provider::Command.new(command(name)).execute(Puppet::Util::Execution, *args)
       end
 
       # And then define an instance method that just calls the class method.
@@ -161,17 +165,36 @@ def self.mk_resource_methods
 
   # Define one or more binaries we'll be using.  If a block is passed, yield the name
   # and path to the block (really only used by 'commands').
+  #
+  # (see #commands)
   def self.optional_commands(hash)
-    hash.each do |name, path|
+    hash.each do |name, target|
       name = symbolize(name)
-      @commands[name] = path
+      command = target.is_a?(String) ? Puppet::Provider::Command.new(target) : target
 
-      yield(name, path) if block_given?
+      @commands[name] = command.executable
+
+      yield(name, command.executable) if block_given?
 
       # Now define the class and instance methods.
-      make_command_methods(name)
+      create_class_and_instance_method(name) do |*args|
+        return command.execute(name, Puppet::Util, Puppet::Util::Execution, *args)
+      end
+    end
+  end
+
+  def self.create_class_and_instance_method(name, &block)
+    unless singleton_class.method_defined?(name)
+      meta_def(name, &block)
+    end
+
+    unless method_defined?(name)
+      define_method(name) do |*args|
+        self.class.send(name, *args)
+      end
     end
   end
+  private_class_method :create_class_and_instance_method
 
   # Retrieve the data source.  Defaults to the provider name.
   def self.source
diff --git a/lib/puppet/provider/command.rb b/lib/puppet/provider/command.rb
new file mode 100644
index 0000000..74f8345
--- /dev/null
+++ b/lib/puppet/provider/command.rb
@@ -0,0 +1,21 @@
+# A command that can be executed on the system
+class Puppet::Provider::Command
+  attr_reader :executable
+
+  # @param [String] executable The path to the executable file
+  # @param [Hash] options Extra options to be used when executing (see Puppet::Util::Execution#execute)
+  def initialize(executable, options = {})
+    @executable = executable
+    @options = options
+  end
+
+  # @param [String] name A way of referencing the name
+  # @param resolver An object for resolving the executable to an absolute path (usually Puppet::Util)
+  # @param executor An object for performing the actual execution of the command (usually Puppet::Util::Execution)
+  # @param [Array<String>] Any command line arguments to pass to the executable
+  # @returns The output from the command
+  def execute(name, resolver, executor, *args)
+    resolved_executable = resolver.which(@executable) or raise Puppet::Error, "Command #{name} is missing"
+    executor.execute([resolved_executable] + args, @options)
+  end
+end
diff --git a/lib/puppet/provider/package/macports.rb b/lib/puppet/provider/package/macports.rb
index 3df29af..43e26d1 100755
--- a/lib/puppet/provider/package/macports.rb
+++ b/lib/puppet/provider/package/macports.rb
@@ -1,4 +1,5 @@
 require 'puppet/provider/package'
+require 'puppet/provider/command'
 
 Puppet::Type.type(:package).provide :macports, :parent => Puppet::Provider::Package do
   desc "Package management using MacPorts on OS X.
@@ -12,7 +13,7 @@
   "
 
   confine :operatingsystem => :darwin
-  commands :port => "/opt/local/bin/port"
+  commands :port => Puppet::Provider::Command.new("/opt/local/bin/port", { :custom_environment => { :HOME => ENV['HOME'] } })
 
   has_feature :installable
   has_feature :uninstallable
diff --git a/lib/puppet/util/execution.rb b/lib/puppet/util/execution.rb
index 17247ba..7313b1a 100644
--- a/lib/puppet/util/execution.rb
+++ b/lib/puppet/util/execution.rb
@@ -54,8 +54,11 @@ def self.execfail(command, exception)
 
 
   # Execute the desired command, and return the status and output.
-  # def execute(command, arguments)
-  # [arguments] a Hash optionally containing any of the following keys:
+  # def execute(command, options)
+  # [command] an Array or String representing the command to execute. If it is
+  #   an Array the first element should be the executable and the rest of the
+  #   elements should be the individual arguments to that executable.
+  # [options] a Hash optionally containing any of the following keys:
   #   :failonfail (default true) -- if this value is set to true, then this method will raise an error if the
   #      command is not executed successfully.
   #   :uid (default nil) -- the user id of the user that the process should be run as
@@ -70,11 +73,11 @@ def self.execfail(command, exception)
   #     Passing in a value of false for this option will allow the command to be executed using the user/system locale.
   #   :custom_environment (default {}) -- a hash of key/value pairs to set as environment variables for the duration
   #     of the command
-  def self.execute(command, arguments = {})
+  def self.execute(command, options = {})
 
     # specifying these here rather than in the method signature to allow callers to pass in a partial
     # set of overrides without affecting the default values for options that they don't pass in
-    default_arguments = {
+    default_options = {
         :failonfail => true,
         :uid => nil,
         :gid => nil,
@@ -85,7 +88,7 @@ def self.execute(command, arguments = {})
         :custom_environment => {},
     }
 
-    arguments = default_arguments.merge(arguments)
+    options = default_options.merge(options)
 
     if command.is_a?(Array)
       command = command.flatten.map(&:to_s)
@@ -102,11 +105,11 @@ def self.execute(command, arguments = {})
 
     null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null'
 
-    stdin = File.open(arguments[:stdinfile] || null_file, 'r')
-    stdout = arguments[:squelch] ? File.open(null_file, 'w') : Tempfile.new('puppet')
-    stderr = arguments[:combine] ? stdout : File.open(null_file, 'w')
+    stdin = File.open(options[:stdinfile] || null_file, 'r')
+    stdout = options[:squelch] ? File.open(null_file, 'w') : Tempfile.new('puppet')
+    stderr = options[:combine] ? stdout : File.open(null_file, 'w')
 
-    exec_args = [command, arguments, stdin, stdout, stderr]
+    exec_args = [command, options, stdin, stdout, stderr]
 
     if execution_stub = Puppet::Util::ExecutionStub.current_value
       return execution_stub.call(*exec_args)
@@ -126,12 +129,12 @@ def self.execute(command, arguments = {})
     [stdin, stdout, stderr].each {|io| io.close rescue nil}
 
     # read output in if required
-    unless arguments[:squelch]
+    unless options[:squelch]
       output = wait_for_output(stdout)
       Puppet.warning "Could not get output" unless output
     end
 
-    if arguments[:failonfail] and exit_status != 0
+    if options[:failonfail] and exit_status != 0
       raise ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output}"
     end
 
@@ -152,7 +155,7 @@ class << self
 
 
   # this is private method, see call to private_class_method after method definition
-  def self.execute_posix(command, arguments, stdin, stdout, stderr)
+  def self.execute_posix(command, options, stdin, stdout, stderr)
     child_pid = Kernel.fork do
       # We can't just call Array(command), and rely on it returning
       # things like ['foo'], when passed ['foo'], because
@@ -172,10 +175,10 @@ def self.execute_posix(command, arguments, stdin, stdout, stderr)
         # (assumes that there are only 256 file descriptors used)
         3.upto(256){|fd| IO::new(fd).close rescue nil}
 
-        Puppet::Util::SUIDManager.change_privileges(arguments[:uid], arguments[:gid], true)
+        Puppet::Util::SUIDManager.change_privileges(options[:uid], options[:gid], true)
 
         # if the caller has requested that we override locale environment variables,
-        if (arguments[:override_locale]) then
+        if (options[:override_locale]) then
           # loop over them and clear them
           Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |name| ENV.delete(name) }
           # set LANG and LC_ALL to 'C' so that the command will have consistent, predictable output
@@ -192,8 +195,8 @@ def self.execute_posix(command, arguments, stdin, stdout, stderr)
         # a forked process.
         Puppet::Util::POSIX::USER_ENV_VARS.each { |name| ENV.delete(name) }
 
-        arguments[:custom_environment] ||= {}
-        Puppet::Util.withenv(arguments[:custom_environment]) do
+        options[:custom_environment] ||= {}
+        Puppet::Util.withenv(options[:custom_environment]) do
           Kernel.exec(*command)
         end
       rescue => detail
@@ -207,14 +210,14 @@ def self.execute_posix(command, arguments, stdin, stdout, stderr)
 
 
   # this is private method, see call to private_class_method after method definition
-  def self.execute_windows(command, arguments, stdin, stdout, stderr)
+  def self.execute_windows(command, options, stdin, stdout, stderr)
     command = command.map do |part|
       part.include?(' ') ? %Q["#{part.gsub(/"/, '\"')}"] : part
     end.join(" ") if command.is_a?(Array)
 
-    arguments[:custom_environment] ||= {}
-    Puppet::Util.withenv(arguments[:custom_environment]) do
-      Puppet::Util::Windows::Process.execute(command, arguments, stdin, stdout, stderr)
+    options[:custom_environment] ||= {}
+    Puppet::Util.withenv(options[:custom_environment]) do
+      Puppet::Util::Windows::Process.execute(command, options, stdin, stdout, stderr)
     end
   end
   private_class_method :execute_windows
diff --git a/spec/unit/property/command_spec.rb b/spec/unit/property/command_spec.rb
new file mode 100755
index 0000000..68c6691
--- /dev/null
+++ b/spec/unit/property/command_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+require 'puppet/provider/command'
+
+describe Puppet::Provider::Command do
+  let(:name) { "the name" }
+  let(:the_options) { { :option => 1 } }
+  let(:no_options) { {} }
+  let(:executable) { "foo" }
+  let(:executable_absolute_path) { "/foo/bar" }
+
+  let(:executor) { mock('executor') }
+  let(:resolver) { mock('resolver') }
+
+  let(:path_resolves_to_itself) do
+    resolves = Object.new
+    class << resolves
+      def which(path)
+        path
+      end
+    end
+    resolves
+  end
+
+  it "executes a simple command" do
+    executor.expects(:execute).with([executable], no_options)
+
+    command = Puppet::Provider::Command.new(executable)
+    command.execute(name, path_resolves_to_itself, executor)
+  end
+
+  it "executes a command with extra options" do
+    executor.expects(:execute).with([executable], the_options)
+
+    command = Puppet::Provider::Command.new(executable, the_options)
+    command.execute(name, path_resolves_to_itself, executor)
+  end
+
+  it "executes a command with arguments" do
+    executor.expects(:execute).with([executable, "arg1", "arg2"], no_options)
+
+    command = Puppet::Provider::Command.new(executable)
+    command.execute(name, path_resolves_to_itself, executor, "arg1", "arg2")
+  end
+
+  it "resolves to an absolute path for better execution" do
+    resolver.expects(:which).with(executable).returns(executable_absolute_path)
+    executor.expects(:execute).with([executable_absolute_path], no_options)
+
+    command = Puppet::Provider::Command.new(executable)
+    command.execute(name, resolver, executor)
+  end
+
+  it "errors when the executable resolves to nothing" do
+    resolver.expects(:which).with(executable).returns(nil)
+    executor.expects(:execute).never
+
+    command = Puppet::Provider::Command.new(executable)
+
+    lambda { command.execute(name, resolver, executor) }.should raise_error(Puppet::Error, "Command #{name} is missing")
+  end
+end
diff --git a/spec/unit/provider/package/pacman_spec.rb b/spec/unit/provider/package/pacman_spec.rb
index e1e5f14..42458ff 100755
--- a/spec/unit/provider/package/pacman_spec.rb
+++ b/spec/unit/provider/package/pacman_spec.rb
@@ -4,8 +4,13 @@
 provider = Puppet::Type.type(:package).provider(:pacman)
 
 describe provider do
+  let(:no_extra_options) { {} }
+  let(:executor) { Puppet::Util::Execution }
+  let(:resolver) { Puppet::Util }
+
   before do
-    provider.stubs(:command).with(:pacman).returns('/usr/bin/pacman')
+    resolver.stubs(:which).with('/usr/bin/pacman').returns('/usr/bin/pacman')
+    provider.stubs(:which).with('/usr/bin/pacman').returns('/usr/bin/pacman')
     @resource = Puppet::Type.type(:package).new(:name => 'package')
     @provider = provider.new(@resource)
   end
@@ -17,42 +22,18 @@
       })
     end
 
-    it "should call pacman" do
-      provider.
+    it "should call pacman to install the right package quietly" do
+      executor.
         expects(:execute).
         at_least_once.
-        with { |args|
-          args[0] == "/usr/bin/pacman"
-        }.
+        with(["/usr/bin/pacman", "--noconfirm", "--noprogressbar", "-Sy", @resource[:name]], no_extra_options).
         returns ""
 
       @provider.install
     end
 
-    it "should be quiet" do
-      provider.
-        expects(:execute).
-        with { |args|
-          args[1,2] == ["--noconfirm", "--noprogressbar"]
-        }.
-        returns("")
-
-      @provider.install
-    end
-
-    it "should install the right package" do
-      provider.
-        expects(:execute).
-        with { |args|
-          args[3,4] == ["-Sy", @resource[:name]]
-        }.
-        returns("")
-
-      @provider.install
-    end
-
     it "should raise an ExecutionFailure if the installation failed" do
-      provider.stubs(:execute).returns("")
+      executor.stubs(:execute).returns("")
       @provider.expects(:query).returns(nil)
 
       lambda { @provider.install }.should raise_exception(Puppet::ExecutionFailure)
@@ -72,13 +53,13 @@
           it "should install #{source} directly" do
             @resource[:source] = source
 
-            provider.expects(:execute).
-              with(all_of(includes("-Sy"), includes("--noprogressbar"))).
+            executor.expects(:execute).
+              with(all_of(includes("-Sy"), includes("--noprogressbar")), no_extra_options).
               in_sequence(@install).
               returns("")
 
-            provider.expects(:execute).
-              with(all_of(includes("-U"), includes(source))).
+            executor.expects(:execute).
+              with(all_of(includes("-U"), includes(source)), no_extra_options).
               in_sequence(@install).
               returns("")
 
@@ -95,14 +76,16 @@
         end
 
         it "should install from the path segment of the URL" do
-          provider.expects(:execute).
-            with(all_of(includes("-Sy"), includes("--noprogressbar"),
-                        includes("--noconfirm"))).
+          executor.expects(:execute).
+            with(all_of(includes("-Sy"),
+                        includes("--noprogressbar"),
+                        includes("--noconfirm")),
+                 no_extra_options).
             in_sequence(@install).
             returns("")
 
-          provider.expects(:execute).
-            with(all_of(includes("-U"), includes(@actual_file_path))).
+          executor.expects(:execute).
+            with(all_of(includes("-U"), includes(@actual_file_path)), no_extra_options).
             in_sequence(@install).
             returns("")
 
@@ -140,45 +123,21 @@
   end
 
   describe "when uninstalling" do
-    it "should call pacman" do
-      provider.
+    it "should call pacman to remove the right package quietly" do
+      executor.
         expects(:execute).
-        with { |args|
-          args[0] == "/usr/bin/pacman"
-        }.
+        with(["/usr/bin/pacman", "--noconfirm", "--noprogressbar", "-R", @resource[:name]], no_extra_options).
         returns ""
 
       @provider.uninstall
     end
-
-    it "should be quiet" do
-      provider.
-        expects(:execute).
-        with { |args|
-          args[1,2] == ["--noconfirm", "--noprogressbar"]
-        }.
-        returns("")
-
-      @provider.uninstall
-    end
-
-    it "should remove the right package" do
-      provider.
-        expects(:execute).
-        with { |args|
-          args[3,4] == ["-R", @resource[:name]]
-        }.
-        returns("")
-
-      @provider.uninstall
-    end
   end
 
   describe "when querying" do
     it "should query pacman" do
-      provider.
+      executor.
         expects(:execute).
-        with(["/usr/bin/pacman", "-Qi", @resource[:name]])
+        with(["/usr/bin/pacman", "-Qi", @resource[:name]], no_extra_options)
       @provider.query
     end
 
@@ -206,17 +165,17 @@
 Description    : A library-based package manager with dependency support
 EOF
 
-      provider.expects(:execute).returns(query_output)
+      executor.expects(:execute).returns(query_output)
       @provider.query.should == {:ensure => "1.01.3-2"}
     end
 
     it "should return a nil if the package isn't found" do
-      provider.expects(:execute).returns("")
+      executor.expects(:execute).returns("")
       @provider.query.should be_nil
     end
 
     it "should return a hash indicating that the package is missing on error" do
-      provider.expects(:execute).raises(Puppet::ExecutionFailure.new("ERROR!"))
+      executor.expects(:execute).raises(Puppet::ExecutionFailure.new("ERROR!"))
       @provider.query.should == {
         :ensure => :purged,
         :status => 'missing',
@@ -266,12 +225,12 @@
   describe "when determining the latest version" do
     it "should refresh package list" do
       get_latest_version = sequence("get_latest_version")
-      provider.
+      executor.
         expects(:execute).
         in_sequence(get_latest_version).
-        with(['/usr/bin/pacman', '-Sy'])
+        with(['/usr/bin/pacman', '-Sy'], no_extra_options)
 
-      provider.
+      executor.
         stubs(:execute).
         in_sequence(get_latest_version).
         returns("")
@@ -281,21 +240,21 @@
 
     it "should get query pacman for the latest version" do
       get_latest_version = sequence("get_latest_version")
-      provider.
+      executor.
         stubs(:execute).
         in_sequence(get_latest_version)
 
-      provider.
+      executor.
         expects(:execute).
         in_sequence(get_latest_version).
-        with(['/usr/bin/pacman', '-Sp', '--print-format', '%v', @resource[:name]]).
+        with(['/usr/bin/pacman', '-Sp', '--print-format', '%v', @resource[:name]], no_extra_options).
         returns("")
 
       @provider.latest
     end
 
     it "should return the version number from pacman" do
-      provider.
+      executor.
         expects(:execute).
         at_least_once().
         returns("1.00.2-3\n")

    

--
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.

Reply via email to