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 [email protected].
To unsubscribe from this group, send email to [email protected].
For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en.
