diff --git a/lib/puppet/provider.rb b/lib/puppet/provider.rb index 578d4a972..0589b1a3e 100644 --- a/lib/puppet/provider.rb +++ b/lib/puppet/provider.rb @@ -1,369 +1,375 @@ # The container class for implementations. class Puppet::Provider include Puppet::Util::Execution include Puppet::Util include Puppet::Util::Errors include Puppet::Util::Warnings extend Puppet::Util::Warnings require 'puppet/provider/confiner' require 'puppet/provider/command' extend Puppet::Provider::Confiner Puppet::Util.logmethods(self, true) class << self # Include the util module so we have access to things like 'which' include Puppet::Util, Puppet::Util::Docs include Puppet::Util::Logging attr_accessor :name # The source parameter exists so that providers using the same # source can specify this, so reading doesn't attempt to read the # same package multiple times. attr_writer :source # LAK 2007-05-09: Keep the model stuff around for backward compatibility attr_reader :model attr_accessor :resource_type attr_writer :doc end # LAK 2007-05-09: Keep the model stuff around for backward compatibility attr_reader :model attr_accessor :resource def self.command(name) name = symbolize(name) if defined?(@commands) and command = @commands[name] # nothing elsif superclass.respond_to? :command and command = superclass.command(name) # nothing else raise Puppet::DevError, "No command #{name} defined for provider #{self.name}" end which(command) end # Define commands that are not optional. # # @param [Hash{String => String}] 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. # (@see #has_command) def self.commands(command_specs) command_specs.each do |name, path| has_command(name, path) end end # Define commands that are optional. # # @param [Hash{String => String}] 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. # (@see #has_command) def self.optional_commands(hash) hash.each do |name, target| has_command(name, target) do is_optional end end end # Define a single command # # A method will be generated on the provider that allows easy execution of the command. The generated # method can take arguments that will be passed through to the executable as the command line arguments # when it is run. # # has_command(:echo, "/bin/echo") # def some_method # echo("arg 1", "arg 2") # end # # # or # # has_command(:echo, "/bin/echo") do # is_optional # environment :HOME => "/var/tmp", :PWD => "/tmp" # end # # @param [Symbol] name Name of the command (will be the name of the generated method to call the command) # @param [String] path The path to the executable for the command # @yield A block that configures the command (@see Puppet::Provider::Command) def self.has_command(name, path, &block) name = symbolize(name) configuration = block_given? ? block : Proc.new {} - command = CommandDefiner.new(name, path, self, &configuration).command + command = CommandDefiner.define(name, path, self, &configuration) @commands[name] = command.executable # Now define the class and instance methods. create_class_and_instance_method(name) do |*args| - return command.execute(name, Puppet::Util, Puppet::Util::Execution, *args) + return command.execute(*args) end end class CommandDefiner - def initialize(name, path, confiner, &block) + private_class_method :new + + def self.define(name, path, confiner, &block) + definer = new(name, path, confiner) + definer.instance_eval &block + definer.command + end + + def initialize(name, path, confiner) @name = name @path = path @optional = false @confiner = confiner @custom_environment = {} - - instance_eval &block end def is_optional @optional = true end def environment(env) @custom_environment = @custom_environment.merge(env) end def command if not @optional @confiner.confine :exists => @path, :for_binary => true end - Puppet::Provider::Command.new(@path, { :custom_environment => @custom_environment }) + Puppet::Provider::Command.new(@name, @path, Puppet::Util, Puppet::Util::Execution, { :custom_environment => @custom_environment }) end end # Is the provided feature a declared feature? def self.declared_feature?(name) defined?(@declared_features) and @declared_features.include?(name) end # Does this implementation match all of the default requirements? If # defaults are empty, we return false. def self.default? return false if @defaults.empty? if @defaults.find do |fact, values| values = [values] unless values.is_a? Array if fval = Facter.value(fact).to_s and fval != "" fval = fval.to_s.downcase.intern else return false end # If any of the values match, we're a default. if values.find do |value| fval == value.to_s.downcase.intern end false else true end end return false else return true end end # Store how to determine defaults. def self.defaultfor(hash) hash.each do |d,v| @defaults[d] = v end end def self.specificity (@defaults.length * 100) + ancestors.select { |a| a.is_a? Class }.length end def self.initvars @defaults = {} @commands = {} end # The method for returning a list of provider instances. Note that it returns providers, preferably with values already # filled in, not resources. def self.instances raise Puppet::DevError, "Provider #{self.name} has not defined the 'instances' class method" 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| # This might throw an ExecutionFailure, but the system above # will catch it, if so. - command = Puppet::Provider::Command.new(command(name)) - return command.execute(name, Puppet::Util, Puppet::Util::Execution, *args) + command = Puppet::Provider::Command.new(name, command(name), Puppet::Util, Puppet::Util::Execution) + return command.execute(*args) end # And then define an instance method that just calls the class method. # We need both, so both instances and classes can easily run the commands. unless method_defined?(name) define_method(name) do |*args| self.class.send(name, *args) end end end end # Create getter/setter methods for each property our resource type supports. # They all get stored in @property_hash. This method is useful # for those providers that use prefetch and flush. def self.mkmodelmethods Puppet.deprecation_warning "Provider.mkmodelmethods is deprecated; use Provider.mk_resource_methods" mk_resource_methods end # Create getter/setter methods for each property our resource type supports. # They all get stored in @property_hash. This method is useful # for those providers that use prefetch and flush. def self.mk_resource_methods [resource_type.validproperties, resource_type.parameters].flatten.each do |attr| attr = symbolize(attr) next if attr == :name define_method(attr) do @property_hash[attr] || :absent end define_method(attr.to_s + "=") do |val| @property_hash[attr] = val end end end self.initvars 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 @source ||= self.name end # Does this provider support the specified parameter? def self.supports_parameter?(param) if param.is_a?(Class) klass = param else unless klass = resource_type.attrclass(param) raise Puppet::DevError, "'#{param}' is not a valid parameter for #{resource_type.name}" end end return true unless features = klass.required_features !!satisfies?(*features) end # def self.to_s # unless defined?(@str) # if self.resource_type # @str = "#{resource_type.name} provider #{self.name}" # else # @str = "unattached provider #{self.name}" # end # end # @str # end dochook(:defaults) do if @defaults.length > 0 return "Default for " + @defaults.collect do |f, v| "`#{f}` == `#{[v].flatten.join(', ')}`" end.join(" and ") + "." end end dochook(:commands) do if @commands.length > 0 return "Required binaries: " + @commands.collect do |n, c| "`#{c}`" end.join(", ") + "." end end dochook(:features) do if features.length > 0 return "Supported features: " + features.collect do |f| "`#{f}`" end.join(", ") + "." end end # Remove the reference to the resource, so GC can clean up. def clear @resource = nil @model = nil end # Retrieve a named command. def command(name) self.class.command(name) end # Get a parameter value. def get(param) @property_hash[symbolize(param)] || :absent end def initialize(resource = nil) if resource.is_a?(Hash) # We don't use a duplicate here, because some providers (ParsedFile, at least) # use the hash here for later events. @property_hash = resource elsif resource @resource = resource # LAK 2007-05-09: Keep the model stuff around for backward compatibility @model = resource @property_hash = {} else @property_hash = {} end end def name if n = @property_hash[:name] return n elsif self.resource resource.name else raise Puppet::DevError, "No resource and no name in property hash in #{self.class.name} instance" end end # Set passed params as the current values. def set(params) params.each do |param, value| @property_hash[symbolize(param)] = value end end def to_s "#{@resource}(provider=#{self.class.name})" end # Make providers comparable. include Comparable def <=>(other) # We can only have ordering against other providers. return nil unless other.is_a? Puppet::Provider # Otherwise, order by the providers class name. return self.class.name <=> other.class.name end end diff --git a/lib/puppet/provider/command.rb b/lib/puppet/provider/command.rb index 74f83453a..b3b377674 100644 --- a/lib/puppet/provider/command.rb +++ b/lib/puppet/provider/command.rb @@ -1,21 +1,25 @@ # A command that can be executed on the system class Puppet::Provider::Command attr_reader :executable + attr_reader :name + # @param [String] name A way of referencing the name # @param [String] executable The path to the executable file + # @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 [Hash] options Extra options to be used when executing (see Puppet::Util::Execution#execute) - def initialize(executable, options = {}) + def initialize(name, executable, resolver, executor, options = {}) + @name = name @executable = executable + @resolver = resolver + @executor = executor @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] 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) + def execute(*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/spec/unit/provider/command_spec.rb b/spec/unit/provider/command_spec.rb index 68c6691b7..1afe019ee 100755 --- a/spec/unit/provider/command_spec.rb +++ b/spec/unit/provider/command_spec.rb @@ -1,62 +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) + command = Puppet::Provider::Command.new(name, executable, path_resolves_to_itself, executor) + command.execute() 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) + command = Puppet::Provider::Command.new(name, executable, path_resolves_to_itself, executor, the_options) + command.execute() 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") + command = Puppet::Provider::Command.new(name, executable, path_resolves_to_itself, executor) + command.execute("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) + command = Puppet::Provider::Command.new(name, executable, resolver, executor) + command.execute() 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) + command = Puppet::Provider::Command.new(name, executable, resolver, executor) - lambda { command.execute(name, resolver, executor) }.should raise_error(Puppet::Error, "Command #{name} is missing") + lambda { command.execute() }.should raise_error(Puppet::Error, "Command #{name} is missing") end end diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb index 9f697725b..eb061c9dd 100755 --- a/spec/unit/provider_spec.rb +++ b/spec/unit/provider_spec.rb @@ -1,212 +1,212 @@ require 'spec_helper' describe Puppet::Provider do after do Puppet::Type.rmtype(:dummy) end describe "has command" do it "installs a method to run the command specified by the path" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") allow_creation_of(echo_command) provider = provider_of do has_command(:echo, "/bin/echo") end provider.echo("an argument") end it "installs a command that is run with a given environment" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") allow_creation_of(echo_command, { :EV => "value", :OTHER => "different" }) provider = provider_of do has_command(:echo, "/bin/echo") do environment :EV => "value", :OTHER => "different" end end provider.echo("an argument") end it "is required by default" do provider = provider_of do has_command(:does_not_exist, "/does/not/exist") end provider.should_not be_suitable end it "is required by default" do provider = provider_of do has_command(:does_exist, "/exists/somewhere") end file_exists_and_is_executable("/exists/somewhere") provider.should be_suitable end it "can be specified as optional" do provider = provider_of do has_command(:does_not_exist, "/does/not/exist") do is_optional end end provider.should be_suitable end end describe "has required commands" do it "installs methods to run executables by path" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") ls_command = expect_command_executed(:ls, "/bin/ls") allow_creation_of(echo_command) allow_creation_of(ls_command) provider = provider_of do commands :echo => "/bin/echo", :ls => "/bin/ls" end provider.echo("an argument") provider.ls end it "allows the provider to be suitable if the executable is present" do provider = provider_of do commands :always_exists => "/this/command/exists" end file_exists_and_is_executable("/this/command/exists") provider.should be_suitable end it "does not allow the provider to be suitable if the executable is not present" do provider = provider_of do commands :does_not_exist => "/this/command/does/not/exist" end provider.should_not be_suitable end end describe "has optional commands" do it "installs methods to run executables" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") ls_command = expect_command_executed(:ls, "/bin/ls") allow_creation_of(echo_command) allow_creation_of(ls_command) provider = provider_of do optional_commands :echo => "/bin/echo", :ls => "/bin/ls" end provider.echo("an argument") provider.ls end it "allows the provider to be suitable even if the executable is not present" do provider = provider_of do optional_commands :does_not_exist => "/this/command/does/not/exist" end provider.should be_suitable end end it "makes command methods on demand (deprecated)" do Puppet::Util.expects(:which).with("/not/a/command").returns("/not/a/command") Puppet::Util::Execution.expects(:execute).with(["/not/a/command"], {}) provider = provider_of do @commands[:echo] = "/not/a/command" end provider.stubs(:which).with("/not/a/command").returns("/not/a/command") provider.make_command_methods(:echo) provider.echo end it "should consider two defaults to be higher specificity than one default" do one = provider_of do defaultfor :operatingsystem => "solaris" end two = provider_of do defaultfor :operatingsystem => "solaris", :operatingsystemrelease => "5.10" end two.specificity.should > one.specificity end it "should consider a subclass more specific than its parent class" do one = provider_of {} two = provider_of({ :parent => one }) {} two.specificity.should > one.specificity end it "should be Comparable" do res = Puppet::Type.type(:notify).new(:name => "res") # Normally I wouldn't like the stubs, but the only way to name a class # otherwise is to assign it to a constant, and that hurts more here in # testing world. --daniel 2012-01-29 a = Class.new(Puppet::Provider).new(res) a.class.stubs(:name).returns "Puppet::Provider::Notify::A" b = Class.new(Puppet::Provider).new(res) b.class.stubs(:name).returns "Puppet::Provider::Notify::B" c = Class.new(Puppet::Provider).new(res) c.class.stubs(:name).returns "Puppet::Provider::Notify::C" [[a, b, c], [a, c, b], [b, a, c], [b, c, a], [c, a, b], [c, b, a]].each do |this| this.sort.should == [a, b, c] end a.should be < b a.should be < c b.should be > a b.should be < c c.should be > a c.should be > b [a, b, c].each {|x| a.should be <= x } [a, b, c].each {|x| c.should be >= x } b.should be_between(a, c) end def provider_of(options = {}, &block) type = Puppet::Type.newtype(:dummy) do provide(:dummy, options, &block) end type.provider(:dummy) end def expect_command_executed(name, path, *args) - command = Puppet::Provider::Command.new(path) - command.expects(:execute).with(name, Puppet::Util, Puppet::Util::Execution, *args) + command = Puppet::Provider::Command.new(name, path, Puppet::Util, Puppet::Util::Execution) + command.expects(:execute).with(*args) command end def allow_creation_of(command, environment = {}) - Puppet::Provider::Command.stubs(:new).with(command.executable, { :custom_environment => environment }).returns(command) + Puppet::Provider::Command.stubs(:new).with(command.name, command.executable, Puppet::Util, Puppet::Util::Execution, { :custom_environment => environment }).returns(command) end def file_exists_and_is_executable(path) FileTest.expects(:file?).with(path).returns(true) FileTest.expects(:executable?).with(path).returns(true) end end