diff --git a/lib/puppet/parameter.rb b/lib/puppet/parameter.rb index ff7cab22b..29d60fc66 100644 --- a/lib/puppet/parameter.rb +++ b/lib/puppet/parameter.rb @@ -1,302 +1,304 @@ require 'puppet/util/methodhelper' require 'puppet/util/log_paths' require 'puppet/util/logging' require 'puppet/util/docs' require 'puppet/util/cacher' class Puppet::Parameter include Puppet::Util include Puppet::Util::Errors include Puppet::Util::LogPaths include Puppet::Util::Logging include Puppet::Util::MethodHelper include Puppet::Util::Cacher require 'puppet/parameter/value_collection' class << self include Puppet::Util include Puppet::Util::Docs attr_reader :validater, :munger, :name, :default, :required_features, :value_collection attr_accessor :metaparam # Define the default value for a given parameter or parameter. This # means that 'nil' is an invalid default value. This defines # the 'default' instance method. def defaultto(value = nil, &block) if block define_method(:default, &block) else if value.nil? raise Puppet::DevError, "Either a default value or block must be provided" end define_method(:default) do value end end end # Return a documentation string. If there are valid values, # then tack them onto the string. def doc @doc ||= "" unless defined?(@addeddocvals) @doc += value_collection.doc if f = self.required_features @doc += " Requires features #{f.flatten.collect { |f| f.to_s }.join(" ")}." end @addeddocvals = true end @doc end def nodefault undef_method :default if public_method_defined? :default end # Store documentation for this parameter. def desc(str) @doc = str end def initvars @value_collection = ValueCollection.new end # This is how we munge the value. Basically, this is our # opportunity to convert the value from one form into another. def munge(&block) # I need to wrap the unsafe version in begin/rescue parameterments, # but if I directly call the block then it gets bound to the # class's context, not the instance's, thus the two methods, # instead of just one. define_method(:unsafe_munge, &block) end # Does the parameter supports reverse munge? # This will be called when something wants to access the parameter # in a canonical form different to what the storage form is. def unmunge(&block) define_method(:unmunge, &block) end # Mark whether we're the namevar. def isnamevar @isnamevar = true @required = true end # Is this parameter the namevar? Defaults to false. def isnamevar? @isnamevar end # This parameter is required. def isrequired @required = true end # Specify features that are required for this parameter to work. def required_features=(*args) @required_features = args.flatten.collect { |a| a.to_s.downcase.intern } end # Is this parameter required? Defaults to false. def required? @required end # Verify that we got a good value def validate(&block) define_method(:unsafe_validate, &block) end # Define a new value for our parameter. def newvalues(*names) @value_collection.newvalues(*names) end def aliasvalue(name, other) @value_collection.aliasvalue(name, other) end end # Just a simple method to proxy instance methods to class methods def self.proxymethods(*values) values.each { |val| define_method(val) do self.class.send(val) end } end # And then define one of these proxies for each method in our # ParamHandler class. proxymethods("required?", "isnamevar?") attr_accessor :resource # LAK 2007-05-09: Keep the @parent around for backward compatibility. attr_accessor :parent [:line, :file, :version].each do |param| define_method(param) do resource.send(param) end end def devfail(msg) self.fail(Puppet::DevError, msg) end def expirer resource.catalog end def fail(*args) type = nil if args[0].is_a?(Class) type = args.shift else type = Puppet::Error end error = type.new(args.join(" ")) error.line = @resource.line if @resource and @resource.line error.file = @resource.file if @resource and @resource.file raise error end # Basic parameter initialization. def initialize(options = {}) options = symbolize_options(options) if resource = options[:resource] self.resource = resource options.delete(:resource) else raise Puppet::DevError, "No resource set for #{self.class.name}" end set_options(options) end def log(msg) send_log(resource[:loglevel], msg) end # Is this parameter a metaparam? def metaparam? self.class.metaparam end # each parameter class must define the name method, and parameter # instances do not change that name this implicitly means that a given # object can only have one parameter instance of a given parameter # class def name self.class.name end # for testing whether we should actually do anything def noop @noop ||= false tmp = @noop || self.resource.noop || Puppet[:noop] || false #debug "noop is #{tmp}" tmp end # return the full path to us, for logging and rollback; not currently # used def pathbuilder if @resource return [@resource.pathbuilder, self.name] else return [self.name] end end # If the specified value is allowed, then munge appropriately. # If the developer uses a 'munge' hook, this method will get overridden. def unsafe_munge(value) self.class.value_collection.munge(value) end # no unmunge by default def unmunge(value) value end # A wrapper around our munging that makes sure we raise useful exceptions. def munge(value) begin ret = unsafe_munge(value) rescue Puppet::Error => detail Puppet.debug "Reraising #{detail}" raise rescue => detail raise Puppet::DevError, "Munging failed for value #{value.inspect} in class #{self.name}: #{detail}", detail.backtrace end ret end # Verify that the passed value is valid. # If the developer uses a 'validate' hook, this method will get overridden. def unsafe_validate(value) self.class.value_collection.validate(value) end # A protected validation method that only ever raises useful exceptions. def validate(value) begin unsafe_validate(value) rescue ArgumentError => detail fail detail.to_s rescue Puppet::Error, TypeError raise rescue => detail raise Puppet::DevError, "Validate method failed for class #{self.name}: #{detail}", detail.backtrace end end def remove @resource = nil end def value unmunge(@value) unless @value.nil? end # Store the value provided. All of the checking should possibly be # late-binding (e.g., users might not exist when the value is assigned # but might when it is asked for). def value=(value) validate(value) @value = munge(value) end # Retrieve the resource's provider. Some types don't have providers, in which # case we return the resource object itself. def provider @resource.provider end # The properties need to return tags so that logs correctly collect them. def tags unless defined?(@tags) @tags = [] # This might not be true in testing @tags = @resource.tags if @resource.respond_to? :tags @tags << self.name.to_s end @tags end def to_s name.to_s end end + +require 'puppet/parameter/path' diff --git a/lib/puppet/parameter/path.rb b/lib/puppet/parameter/path.rb new file mode 100644 index 000000000..44886afd0 --- /dev/null +++ b/lib/puppet/parameter/path.rb @@ -0,0 +1,42 @@ +require 'puppet/parameter' + +class Puppet::Parameter::Path < Puppet::Parameter + def self.accept_arrays(bool = true) + @accept_arrays = !!bool + end + def self.arrays? + @accept_arrays + end + + def validate_path(paths) + if paths.is_a?(Array) and ! self.class.arrays? then + fail "#{name} only accepts a single path, not an array of paths" + end + + # We *always* support Unix path separators, as Win32 does now too. + absolute = "[/#{::Regexp.quote(::File::SEPARATOR)}]" + win32 = Puppet.features.microsoft_windows? + + Array(paths).each do |path| + next if path =~ %r{^#{absolute}} + next if win32 and path =~ %r{^(?:[a-zA-Z]:)?#{absolute}} + fail("#{name} must be a fully qualified path") + end + + paths + end + + # This will be overridden if someone uses the validate option, which is why + # it just delegates to the other, useful, method. + def unsafe_validate(paths) + validate_path(paths) + end + + # Likewise, this might be overridden, but by default... + def unsafe_munge(paths) + if paths.is_a?(Array) and ! self.class.arrays? then + fail "#{name} only accepts a single path, not an array of paths" + end + paths + end +end diff --git a/lib/puppet/provider/exec/posix.rb b/lib/puppet/provider/exec/posix.rb new file mode 100644 index 000000000..92dbd8c98 --- /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/provider/exec/shell.rb b/lib/puppet/provider/exec/shell.rb new file mode 100644 index 000000000..98f309e8f --- /dev/null +++ b/lib/puppet/provider/exec/shell.rb @@ -0,0 +1,17 @@ +Puppet::Type.type(:exec).provide :shell, :parent => :posix do + include Puppet::Util::Execution + + confine :feature => :posix + + desc "Execute external binaries directly, on POSIX systems. +passing through a shell so that shell built ins are available." + + def run(command, check = false) + command = %Q{/bin/sh -c "#{command.gsub(/"/,'\"')}"} + super(command, check) + end + + def validatecmd(command) + true + end +end diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb index 5ed2b104c..4458bf081 100755 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -1,651 +1,516 @@ module Puppet newtype(:exec) do include Puppet::Util::Execution require 'timeout' @doc = "Executes external commands. It is critical that all commands executed using this mechanism can be run multiple times without harm, i.e., they are *idempotent*. One useful way to create idempotent commands is to use the checks like `creates` to avoid running the command unless some condition is met. Note that you can restrict an `exec` to only run when it receives events by using the `refreshonly` parameter; this is a useful way to have your configuration respond to events with arbitrary commands. Note also that if an `exec` receives an event from another resource, it will get executed again (or execute the command specified in `refresh`, if there is one). There is a strong tendency to use `exec` to do whatever work Puppet can't already do; while this is obviously acceptable (and unavoidable) in the short term, it is highly recommended to migrate work from `exec` to native Puppet types as quickly as possible. If you find that you are doing a lot of work with `exec`, please at least notify us at Puppet Labs what you are doing, and hopefully we can work with you to get a native resource type for the work you are doing. - - **Autorequires:** If Puppet is managing an exec's cwd or the executable file used in an exec's command, the exec resource will autorequire those files. If Puppet is managing the user that an exec should run as, the exec resource will autorequire that user." - require 'open3' + **Autorequires:** If Puppet is managing an exec's cwd or the executable file used in an exec's command, the exec resource will autorequire those files. If Puppet is managing the user that an exec should run as, the exec resource will autorequire that user." # 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 def self.checks @checks.keys end newproperty(:returns, :array_matching => :all, :event => :executed_command) do |property| include Puppet::Util::Execution munge do |value| value.to_s end def event_name :executed_command end defaultto "0" attr_reader :output desc "The expected return code(s). An error will be returned if the executed command returns something else. Defaults to 0. Can be specified as an array of acceptable return codes or a single value." # Make output a bit prettier def change_to_s(currentvalue, newvalue) "executed successfully" end # 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 end end # Actually execute the command. def sync olddir = nil # We need a dir to change to, even if it's just the cwd dir = self.resource[:cwd] || Dir.pwd event = :executed_command tries = self.resource[:tries] try_sleep = self.resource[:try_sleep] begin 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") sleep try_sleep end end rescue Timeout::Error self.fail "Command exceeded timeout" % value.inspect end if log = @resource[:logoutput] case log when :true log = @resource[:loglevel] when :on_failure unless self.should.include?(@status.exitstatus.to_s) log = @resource[:loglevel] else log = :false end end unless log == :false @output.split(/\n/).each { |line| self.send(log, line) } end end unless self.should.include?(@status.exitstatus.to_s) self.fail("#{self.resource[:command]} returned #{@status.exitstatus} instead of one of [#{self.should.join(",")}]") end event end end newparam(:command) do isnamevar desc "The actual command to execute. Must either be fully qualified or a search path for the command must be provided. If the command succeeds, any output produced will be logged at the instance's normal log level (usually `notice`), but if the command fails (meaning its return code does not match the specified code) then any output is logged at the `err` log level." end 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 separated list." # Support both arrays and colon-separated fields. def value=(*values) @value = values.flatten.collect { |val| if val =~ /;/ # recognize semi-colon separated paths val.split(";") elsif val =~ /^\w:[^:]*$/ # heuristic to avoid splitting a driveletter away val else val.split(":") end }.flatten end end newparam(:user) do desc "The user to run the command as. Note that if you use this then any error output is not currently captured. This is because of a bug within Ruby. If you are using Puppet to create this user, the exec will automatically require the user, as long as it is specified by name." # Most validation is handled by the SUIDManager class. validate do |user| self.fail "Only root can execute commands as other users" unless Puppet.features.root? end end newparam(:group) do desc "The group to run the command as. This seems to work quite haphazardly on different platforms -- it is a platform issue not a Ruby or Puppet one, since the same variety exists when running commnands as different users in the shell." # 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 desc "Whether to log output. Defaults to logging output at the loglevel for the `exec` resource. Use *on_failure* to only log the output when the command reports an error. Values are **true**, *false*, *on_failure*, and any legal log level." newvalues(:true, :false, :on_failure) end newparam(:refresh) do desc "How to refresh this command. By default, the exec is just called again when it receives an event from another resource, but this parameter allows you to define a different command for refreshing." validate do |command| - @resource.validatecmd(command) + provider.validatecmd(command) end end newparam(:env) do desc "This parameter is deprecated. Use 'environment' instead." munge do |value| warning "'env' is deprecated on exec; use 'environment' instead." resource[:environment] = value end end newparam(:environment) do desc "Any additional environment variables you want to set for a command. Note that if you use this to set PATH, it will override the `path` attribute. Multiple environment variables should be specified as an array." validate do |values| values = [values] unless values.is_a? Array values.each do |value| unless value =~ /\w+=/ raise ArgumentError, "Invalid environment setting '#{value}'" end end end end newparam(:timeout) do desc "The maximum time the command should take. If the command takes longer than the timeout, the command is considered to have failed and will be stopped. Use any negative number to disable the timeout. The time is specified in seconds." munge do |value| value = value.shift if value.is_a?(Array) if value.is_a?(String) unless value =~ /^[-\d.]+$/ raise ArgumentError, "The timeout must be a number." end Float(value) else value end end defaultto 300 end newparam(:tries) do desc "The number of times execution of the command should be tried. Defaults to '1'. This many attempts will be made to execute the command until an acceptable return code is returned. Note that the timeout paramater applies to each try rather than to the complete set of tries." munge do |value| if value.is_a?(String) unless value =~ /^[\d]+$/ raise ArgumentError, "Tries must be an integer" end value = Integer(value) end raise ArgumentError, "Tries must be an integer >= 1" if value < 1 value end defaultto 1 end newparam(:try_sleep) do desc "The time to sleep in seconds between 'tries'." munge do |value| if value.is_a?(String) unless value =~ /^[-\d.]+$/ raise ArgumentError, "try_sleep must be a number" end value = Float(value) end raise ArgumentError, "try_sleep cannot be a negative number" if value < 0 value end defaultto 0 end newcheck(:refreshonly) do desc "The command should only be run as a refresh mechanism for when a dependent object is changed. It only makes sense to use this option when this command depends on some other object; it is useful for triggering an action: # Pull down the main aliases file file { \"/etc/aliases\": source => \"puppet://server/module/aliases\" } # Rebuild the database, but only when the file changes exec { newaliases: path => [\"/usr/bin\", \"/usr/sbin\"], subscribe => File[\"/etc/aliases\"], refreshonly => true } Note that only `subscribe` and `notify` can trigger actions, not `require`, so it only makes sense to use `refreshonly` with `subscribe` or `notify`." newvalues(:true, :false) # We always fail this test, because we're only supposed to run # on refresh. def check(value) # We have to invert the values. if value == :true false else true end 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: exec { \"tar xf /my/tar/file.tar\": cwd => \"/var/tmp\", creates => \"/var/tmp/myfile\", path => [\"/usr/bin\", \"/usr/sbin\"] } " - # 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 def check(value) ! FileTest.exists?(value) end end newcheck(:unless) do desc "If this parameter is set, then this `exec` will run unless the command returns 0. For example: exec { \"/bin/echo root >> /usr/lib/cron/cron.allow\": path => \"/usr/bin:/usr/sbin:/bin\", unless => \"grep root /usr/lib/cron/cron.allow 2>/dev/null\" } This would add `root` to the cron.allow file (on Solaris) unless `grep` determines it's already there. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. " 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 end status.exitstatus != 0 end end newcheck(:onlyif) do desc "If this parameter is set, then this `exec` will only run if the command returns 0. For example: exec { \"logrotate\": path => \"/usr/bin:/usr/sbin:/bin\", onlyif => \"test `du /var/log/messages | cut -f1` -gt 100000\" } This would run `logrotate` only if that test returned true. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. Also note that onlyif can take an array as its value, e.g.: onlyif => [\"test -f /tmp/file1\", \"test -f /tmp/file2\"] This will only run the exec if /all/ conditions in the array return true. " 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 end status.exitstatus == 0 end end # Exec names are not isomorphic with the objects. @isomorphic = false validate do - validatecmd(self[:command]) + provider.validatecmd(self[:command]) end # FIXME exec should autorequire any exec that 'creates' our cwd autorequire(:file) do reqs = [] # Stick the cwd in there if we have it reqs << self[:cwd] if self[:cwd] self[:command].scan(/^(#{File::SEPARATOR}\S+)/) { |str| reqs << str } self[:command].scan(/^"([^"]+)"/) { |str| reqs << str } [:onlyif, :unless].each { |param| next unless tmp = self[param] tmp = [tmp] unless tmp.is_a? Array tmp.each do |line| # And search the command line for files, adding any we # find. This will also catch the command itself if it's # fully qualified. It might not be a bad idea to add # unqualified files, but, well, that's a bit more annoying # to do. reqs += line.scan(%r{(#{File::SEPARATOR}\S+)}) end } # For some reason, the += isn't causing a flattening reqs.flatten! reqs end autorequire(:user) do # Autorequire users if they are specified by name if user = self[:user] and user !~ /^\d+$/ user end end def self.instances [] end # 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) val = @parameters[check].value val = [val] unless val.is_a? Array val.each do |value| return false unless @parameters[check].check(value) end end } 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 else return self.property(:returns).output end end # 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/integration/transaction_spec.rb b/spec/integration/transaction_spec.rb index d5478d7a7..2c12b3d5f 100755 --- a/spec/integration/transaction_spec.rb +++ b/spec/integration/transaction_spec.rb @@ -1,284 +1,278 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet_spec/files' require 'puppet/transaction' require 'puppet_spec/files' describe Puppet::Transaction do include PuppetSpec::Files before do Puppet::Util::Storage.stubs(:store) end def mk_catalog(*resources) catalog = Puppet::Resource::Catalog.new(Puppet::Node.new("mynode")) resources.each { |res| catalog.add_resource res } catalog end it "should not apply generated resources if the parent resource fails" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:file).new :path => "/foo/bar", :backup => false catalog.add_resource resource child_resource = Puppet::Type.type(:file).new :path => "/foo/bar/baz", :backup => false resource.expects(:eval_generate).returns([child_resource]) transaction = Puppet::Transaction.new(catalog) resource.expects(:retrieve).raises "this is a failure" resource.stubs(:err) child_resource.expects(:retrieve).never transaction.evaluate end it "should not apply virtual resources" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:file).new :path => "/foo/bar", :backup => false resource.virtual = true catalog.add_resource resource transaction = Puppet::Transaction.new(catalog) resource.expects(:evaluate).never transaction.evaluate end it "should apply exported resources" do catalog = Puppet::Resource::Catalog.new path = tmpfile("exported_files") resource = Puppet::Type.type(:file).new :path => path, :backup => false, :ensure => :file resource.exported = true catalog.add_resource resource catalog.apply FileTest.should be_exist(path) end it "should not apply virtual exported resources" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:file).new :path => "/foo/bar", :backup => false resource.exported = true resource.virtual = true catalog.add_resource resource transaction = Puppet::Transaction.new(catalog) resource.expects(:evaluate).never transaction.evaluate end # Verify that one component requiring another causes the contained # resources in the requiring component to get refreshed. it "should propagate events from a contained resource through its container to its dependent container's contained resources" do transaction = nil file = Puppet::Type.type(:file).new :path => tmpfile("event_propagation"), :ensure => :present execfile = File.join(tmpdir("exec_event"), "exectestingness2") exec = Puppet::Type.type(:exec).new :command => "touch #{execfile}", :path => ENV['PATH'] catalog = mk_catalog(file) fcomp = Puppet::Type.type(:component).new(:name => "Foo[file]") catalog.add_resource fcomp catalog.add_edge(fcomp, file) ecomp = Puppet::Type.type(:component).new(:name => "Foo[exec]") catalog.add_resource ecomp catalog.add_resource exec catalog.add_edge(ecomp, exec) ecomp[:subscribe] = Puppet::Resource.new(:foo, "file") exec[:refreshonly] = true exec.expects(:refresh) catalog.apply end # Make sure that multiple subscriptions get triggered. it "should propagate events to all dependent resources" do path = tmpfile("path") file1 = tmpfile("file1") file2 = tmpfile("file2") - file = Puppet::Type.type(:file).new( - - :path => path, - + file = Puppet::Type.type(:file).new( + :path => path, :ensure => "file" ) - exec1 = Puppet::Type.type(:exec).new( - - :path => ENV["PATH"], + exec1 = Puppet::Type.type(:exec).new( + :path => ENV["PATH"], :command => "touch #{file1}", :refreshonly => true, - - :subscribe => Puppet::Resource.new(:file, path) + :subscribe => Puppet::Resource.new(:file, path) ) - exec2 = Puppet::Type.type(:exec).new( - - :path => ENV["PATH"], - :command => "touch #{file2}", + exec2 = Puppet::Type.type(:exec).new( + :path => ENV["PATH"], + :command => "touch #{file2}", :refreshonly => true, - - :subscribe => Puppet::Resource.new(:file, path) + :subscribe => Puppet::Resource.new(:file, path) ) catalog = mk_catalog(file, exec1, exec2) catalog.apply FileTest.should be_exist(file1) FileTest.should be_exist(file2) end it "should not let one failed refresh result in other refreshes failing" do path = tmpfile("path") newfile = tmpfile("file") file = Puppet::Type.type(:file).new( :path => path, :ensure => "file" ) exec1 = Puppet::Type.type(:exec).new( :path => ENV["PATH"], :command => "touch /this/cannot/possibly/exist", :logoutput => true, :refreshonly => true, :subscribe => file, :title => "one" ) exec2 = Puppet::Type.type(:exec).new( :path => ENV["PATH"], :command => "touch #{newfile}", :logoutput => true, :refreshonly => true, :subscribe => [file, exec1], :title => "two" ) exec1.stubs(:err) catalog = mk_catalog(file, exec1, exec2) catalog.apply FileTest.should be_exists(newfile) end it "should still trigger skipped resources" do catalog = mk_catalog catalog.add_resource(*Puppet::Type.type(:schedule).mkdefaultschedules) Puppet[:ignoreschedules] = false file = Puppet::Type.type(:file).new( :name => tmpfile("file"), :ensure => "file", :backup => false ) fname = tmpfile("exec") exec = Puppet::Type.type(:exec).new( :name => "touch #{fname}", :path => "/usr/bin:/bin", :schedule => "monthly", :subscribe => Puppet::Resource.new("file", file.name) ) catalog.add_resource(file, exec) # Run it once catalog.apply FileTest.should be_exists(fname) # Now remove it, so it can get created again File.unlink(fname) file[:content] = "some content" catalog.apply FileTest.should be_exists(fname) # Now remove it, so it can get created again File.unlink(fname) # And tag our exec exec.tag("testrun") # And our file, so it runs file.tag("norun") Puppet[:tags] = "norun" file[:content] = "totally different content" catalog.apply FileTest.should be_exists(fname) end it "should not attempt to evaluate resources with failed dependencies" do exec = Puppet::Type.type(:exec).new( :command => "/bin/mkdir /this/path/cannot/possibly/exit", :title => "mkdir" ) file1 = Puppet::Type.type(:file).new( :title => "file1", :path => tmpfile("file1"), :require => exec, :ensure => :file ) file2 = Puppet::Type.type(:file).new( :title => "file2", :path => tmpfile("file2"), :require => file1, :ensure => :file ) catalog = mk_catalog(exec, file1, file2) catalog.apply FileTest.should_not be_exists(file1[:path]) FileTest.should_not be_exists(file2[:path]) end # #801 -- resources only checked in noop should be rescheduled immediately. it "should immediately reschedule noop resources" do Puppet::Type.type(:schedule).mkdefaultschedules resource = Puppet::Type.type(:notify).new(:name => "mymessage", :noop => true) catalog = Puppet::Resource::Catalog.new catalog.add_resource resource trans = catalog.apply trans.resource_harness.should be_scheduled(trans.resource_status(resource), resource) end end diff --git a/spec/lib/puppet_spec/verbose.rb b/spec/lib/puppet_spec/verbose.rb new file mode 100644 index 000000000..d9834f2d7 --- /dev/null +++ b/spec/lib/puppet_spec/verbose.rb @@ -0,0 +1,9 @@ +# Support code for running stuff with warnings disabled. +module Kernel + def with_verbose_disabled + verbose, $VERBOSE = $VERBOSE, nil + result = yield + $VERBOSE = verbose + return result + end +end diff --git a/spec/shared_behaviours/path_parameters.rb b/spec/shared_behaviours/path_parameters.rb new file mode 100644 index 000000000..b5a907900 --- /dev/null +++ b/spec/shared_behaviours/path_parameters.rb @@ -0,0 +1,185 @@ +# In order to use this correctly you must define a method to get an instance +# of the type being tested, so that this code can remain generic: +# +# it_should_behave_like "all path parameters", :path do +# def instance(path) +# Puppet::Type.type(:example).new( +# :name => 'foo', :require => 'bar', :path_param => path +# ) +# end +# +# That method will be invoked for each test to create the instance that we +# subsequently test through the system; you should ensure that the minimum of +# possible attributes are set to keep the tests clean. +# +# You must also pass the symbolic name of the parameter being tested to the +# block, and optionally can pass a hash of additional options to the block. +# +# The known options are: +# :array :: boolean, does this support arrays of paths, default true. + +shared_examples_for "all pathname parameters with arrays" do |win32| + path_types = { + "unix absolute" => "/foo/bar", + "unix relative" => "foo/bar", + "win32 absolute" => %q{\foo\bar}, + "win32 relative" => %q{foo\bar}, + "drive absolute" => %q{c:\foo\bar}, + "drive relative" => %q{c:foo\bar} + } + + describe "when given an array of paths" do + (1..path_types.length).each do |n| + path_types.keys.combination(n) do |set| + data = path_types.collect { |k, v| set.member?(k) ? v : nil } .compact + reject = true + only_absolute = set.find { |k| k =~ /relative/ } .nil? + only_unix = set.reject { |k| k =~ /unix/ } .length == 0 + + if only_absolute and (only_unix or win32) then + reject = false + end + + it "should #{reject ? 'reject' : 'accept'} #{set.join(", ")}" do + if reject then + expect { instance(data) }. + should raise_error Puppet::Error, /fully qualified/ + else + instance = instance(data) + instance[@param].should == data + end + end + + it "should #{reject ? 'reject' : 'accept'} #{set.join(", ")} doubled" do + if reject then + expect { instance(data + data) }. + should raise_error Puppet::Error, /fully qualified/ + else + instance = instance(data + data) + instance[@param].should == (data + data) + end + end + end + end + end +end + + +shared_examples_for "all path parameters" do |param, options| + # Extract and process options to the block. + options ||= {} + array = options[:array].nil? ? true : options.delete(:array) + if options.keys.length > 0 then + fail "unknown options for 'all path parameters': " + + options.keys.sort.join(', ') + end + + def instance(path) + fail "we didn't implement the 'instance(path)' method in the it_should_behave_like block" + end + + ######################################################################## + # The actual testing code... + before :all do + @param = param + end + + before :each do + @file_separator = File::SEPARATOR + end + after :each do + with_verbose_disabled do + verbose, $VERBOSE = $VERBOSE, nil + File::SEPARATOR = @file_separator + $VERBOSE = verbose + end + end + + describe "on a Unix-like platform it" do + before :each do + with_verbose_disabled do + File::SEPARATOR = '/' + end + Puppet.features.stubs(:microsoft_windows?).returns(false) + Puppet.features.stubs(:posix?).returns(true) + end + + if array then + it_should_behave_like "all pathname parameters with arrays", false + end + + it "should accept a fully qualified path" do + path = File.join('', 'foo') + instance = instance(path) + instance[@param].should == path + end + + it "should give a useful error when the path is not absolute" do + path = 'foo' + expect { instance(path) }. + should raise_error Puppet::Error, /fully qualified/ + end + + { "Unix" => '/', "Win32" => '\\' }.each do |style, slash| + %w{q Q a A z Z c C}.sort.each do |drive| + it "should reject drive letter '#{drive}' with #{style} path separators" do + path = "#{drive}:#{slash}Program Files" + expect { instance(path) }. + should raise_error Puppet::Error, /fully qualified/ + end + end + end + end + + describe "on a Windows-like platform it" do + before :each do + with_verbose_disabled do + File::SEPARATOR = '\\' + end + Puppet.features.stubs(:microsoft_windows?).returns(true) + Puppet.features.stubs(:posix?).returns(false) + end + + if array then + it_should_behave_like "all pathname parameters with arrays", true + end + + it "should accept a fully qualified path" do + path = File.join('', 'foo') + instance = instance(path) + instance[@param].should == path + end + + it "should give a useful error when the path is not absolute" do + path = 'foo' + expect { instance(path) }. + should raise_error Puppet::Error, /fully qualified/ + end + + it "also accepts Unix style path separators" do + path = '/Program Files' + instance = instance(path) + instance[@param].should == path + end + + { "Unix" => '/', "Win32" => '\\' }.each do |style, slash| + %w{q Q a A z Z c C}.sort.each do |drive| + it "should accept drive letter '#{drive}' with #{style} path separators " do + path = "#{drive}:#{slash}Program Files" + instance = instance(path) + instance[@param].should == path + end + end + end + + { "UNC paths" => %q{\\foo\bar}, + "unparsed local paths" => %q{\\?\c:\foo}, + "unparsed UNC paths" => %q{\\?\foo\bar} + }.each do |name, path| + it "should accept #{name} as absolute" do + instance = instance(path) + instance[@param].should == path + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ae4edb2d9..505a8f973 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,83 +1,89 @@ unless defined?(SPEC_HELPER_IS_LOADED) SPEC_HELPER_IS_LOADED = 1 dir = File.expand_path(File.dirname(__FILE__)) $LOAD_PATH.unshift("#{dir}/") $LOAD_PATH.unshift("#{dir}/lib") # a spec-specific test lib dir $LOAD_PATH.unshift("#{dir}/../lib") $LOAD_PATH.unshift("#{dir}/../test/lib") # Don't want puppet getting the command line arguments for rake or autotest ARGV.clear require 'puppet' require 'mocha' gem 'rspec', '>=2.0.0' # So everyone else doesn't have to include this base constant. module PuppetSpec FIXTURE_DIR = File.join(dir = File.expand_path(File.dirname(__FILE__)), "fixtures") unless defined?(FIXTURE_DIR) end module PuppetTest end +require 'pathname' +require 'lib/puppet_spec/verbose' require 'lib/puppet_spec/files' require 'monkey_patches/alias_should_to_must' require 'monkey_patches/publicize_methods' +Pathname.glob("#{dir}/shared_behaviours/**/*.rb") do |behaviour| + require behaviour.relative_path_from(Pathname.new(dir)) +end + RSpec.configure do |config| config.mock_with :mocha config.after :each do Puppet.settings.clear Puppet::Node::Environment.clear Puppet::Util::Storage.clear Puppet::Util::ExecutionStub.reset if defined?($tmpfiles) $tmpfiles.each do |file| file = File.expand_path(file) if Puppet.features.posix? and file !~ /^\/tmp/ and file !~ /^\/var\/folders/ puts "Not deleting tmpfile #{file} outside of /tmp or /var/folders" next elsif Puppet.features.microsoft_windows? tempdir = File.expand_path(File.join(Dir::LOCAL_APPDATA, "Temp")) if file !~ /^#{tempdir}/ puts "Not deleting tmpfile #{file} outside of #{tempdir}" next end end if FileTest.exist?(file) system("chmod -R 755 '#{file}'") system("rm -rf '#{file}'") end end $tmpfiles.clear end @logs.clear Puppet::Util::Log.close_all end config.before :each do # these globals are set by Application $puppet_application_mode = nil $puppet_application_name = nil Signal.stubs(:trap) # Set the confdir and vardir to gibberish so that tests # have to be correctly mocked. Puppet[:confdir] = "/dev/null" Puppet[:vardir] = "/dev/null" # Avoid opening ports to the outside world Puppet.settings[:bindaddress] = "127.0.0.1" @logs = [] Puppet::Util::Log.newdestination(@logs) end end end diff --git a/spec/unit/parameter/path_spec.rb b/spec/unit/parameter/path_spec.rb new file mode 100644 index 000000000..08a26de33 --- /dev/null +++ b/spec/unit/parameter/path_spec.rb @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby +require File.expand_path(File.join(File.dirname(__FILE__), '../../spec_helper')) + +require 'puppet/parameter/path' + +[false, true].each do |arrays| + describe "Puppet::Parameter::Path with arrays #{arrays}" do + it_should_behave_like "all path parameters", :path, :array => arrays do + # The new type allows us a test that is guaranteed to go direct to our + # validation code, without passing through any "real type" overrides or + # whatever on the way. + Puppet::newtype(:test_puppet_parameter_path) do + newparam(:path, :parent => Puppet::Parameter::Path, :arrays => arrays) do + isnamevar + accept_arrays arrays + end + end + + def instance(path) + Puppet::Type.type(:test_puppet_parameter_path).new(:path => path) + 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 000000000..d02099250 --- /dev/null +++ b/spec/unit/provider/exec/posix_spec.rb @@ -0,0 +1,120 @@ +#!/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 not be able to execute shell builtins" do + @provider.resource[:path] = ['/bin'] + lambda { @provider.run("cd ..") }.should raise_error(ArgumentError, "Could not find command 'cd'") + end + + it "should execute the command if the command given includes arguments or subcommands" do + @provider.resource[:path] = ['/bogus/bin'] + File.stubs(:exists?).returns(false) + File.stubs(:exists?).with("foo").returns(true) + File.stubs(:executable?).with("foo").returns(true) + + Puppet::Util.expects(:execute).with() { |command, arguments| (command == ['foo bar --sillyarg=true --blah']) && (arguments.is_a? Hash) } + @provider.run("foo bar --sillyarg=true --blah") + end + + it "should fail if quoted command doesn't exist" do + @provider.resource[:path] = ['/bogus/bin'] + File.stubs(:exists?).returns(false) + File.stubs(:exists?).with("foo").returns(true) + File.stubs(:executable?).with("foo").returns(true) + + lambda { @provider.run('"foo bar --sillyarg=true --blah"') }.should raise_error(ArgumentError, "Could not find command 'foo bar --sillyarg=true --blah'") + end + + it "should execute the command if it finds it in the path and is executable" do + @provider.resource[:path] = ['/bogus/bin'] + File.stubs(:exists?).with("foo").returns(true) + File.stubs(:executable?).with("foo").returns(true) + Puppet::Util.expects(:execute).with() { |command, arguments| (command == ['foo']) && (arguments.is_a? Hash) } + + @provider.run("foo") + end + + if feature == "microsoft_windows" + [".exe", ".ps1", ".bat", ".com", ""].each do |extension| + it "should check file extension #{extension} when it can't find the executable" do + @provider.resource[:path] = ['/bogus/bin'] + File.stubs(:exists?).returns(false) + File.stubs(:exists?).with("/bogus/bin/foo#{extension}").returns(true) + File.stubs(:executable?).with("foo").returns(true) + Puppet::Util.expects(:execute).with() { |command, arguments| (command == ['foo']) && (arguments.is_a? Hash) } + + @provider.run("foo") + end + end + end + + it "should warn if you're overriding something in environment" do + @provider.resource[:environment] = ['WHATEVER=/something/else', 'WHATEVER=/foo'] + File.stubs(:exists?).returns(false) + File.stubs(:exists?).with("foo").returns(true) + File.stubs(:executable?).with("foo").returns(true) + + Puppet::Util.expects(:execute).with() { |command, arguments| (command == ['foo']) && (arguments.is_a? Hash) } + @provider.run("foo") + @logs.map {|l| "#{l.level}: #{l.message}" }.should == ["warning: Overriding environment setting 'WHATEVER' with '/foo'"] + end + end + end + end +end diff --git a/spec/unit/provider/exec/shell_spec.rb b/spec/unit/provider/exec/shell_spec.rb new file mode 100644 index 000000000..a9b1f06ba --- /dev/null +++ b/spec/unit/provider/exec/shell_spec.rb @@ -0,0 +1,50 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/../../../spec_helper' + +provider_class = Puppet::Type.type(:exec).provider(:shell) + +describe provider_class do + before :each do + @resource = Puppet::Resource.new(:exec, 'foo') + @provider = provider_class.new(@resource) + end + + describe "#run" do + it "should be able to run builtin shell commands" do + output, status = @provider.run("if [ 1 == 1 ]; then echo 'blah'; fi") + status.exitstatus.should == 0 + output.should == "blah\n" + end + + it "should be able to run commands with single quotes in them" do + output, status = @provider.run("echo 'foo bar'") + status.exitstatus.should == 0 + output.should == "foo bar\n" + end + + it "should be able to run commands with double quotes in them" do + output, status = @provider.run('echo "foo bar"') + status.exitstatus.should == 0 + output.should == "foo bar\n" + end + + it "should be able to run multiple commands separated by a semicolon" do + output, status = @provider.run("echo 'foo' ; echo 'bar'") + status.exitstatus.should == 0 + output.should == "foo\nbar\n" + end + + it "should be able to read values from the environment parameter" do + @resource[:environment] = "FOO=bar" + output, status = @provider.run("echo $FOO") + status.exitstatus.should == 0 + output.should == "bar\n" + end + end + + describe "#validatecmd" do + it "should always return true because builtins don't need path or to be fully qualified" do + @provider.validatecmd('whateverdoesntmatter').should == true + end + end +end diff --git a/spec/unit/type/exec_spec.rb b/spec/unit/type/exec_spec.rb index e04cfc065..4d691d580 100755 --- a/spec/unit/type/exec_spec.rb +++ b/spec/unit/type/exec_spec.rb @@ -1,162 +1,670 @@ #!/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 @executable = Puppet.features.posix? ? '/bin/true' : 'C:/Program Files/something.exe' @command = Puppet.features.posix? ? '/bin/true whatever' : '"C:/Program Files/something.exe" whatever' File.stubs(:exists?).returns false File.stubs(:exists?).with(@executable).returns true @example_path = Puppet.features.posix? ? %w{/usr/bin /bin} : [ "C:/Program Files/something/bin", "C:/Ruby/bin" ] File.stubs(:exists?).with(File.join(@example_path[0],"true")).returns true File.stubs(:exists?).with(File.join(@example_path[0],"false")).returns true end it "should return :executed_command as its event" do resource = Puppet::Type.type(:exec).new :command => @command resource.parameter(:returns).event.name.should == :executed_command 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 diff --git a/spec/unit/type_spec.rb b/spec/unit/type_spec.rb index 6d9d0b234..9b1f20500 100755 --- a/spec/unit/type_spec.rb +++ b/spec/unit/type_spec.rb @@ -1,584 +1,584 @@ #!/usr/bin/env ruby -require File.dirname(__FILE__) + '/../spec_helper' +require File.expand_path(File.join(File.dirname(__FILE__), '/../spec_helper')) describe Puppet::Type do it "should include the Cacher module" do Puppet::Type.ancestors.should be_include(Puppet::Util::Cacher) end it "should consider a parameter to be valid if it is a valid parameter" do Puppet::Type.type(:mount).should be_valid_parameter(:path) end it "should consider a parameter to be valid if it is a valid property" do Puppet::Type.type(:mount).should be_valid_parameter(:fstype) end it "should consider a parameter to be valid if it is a valid metaparam" do Puppet::Type.type(:mount).should be_valid_parameter(:noop) end it "should use its catalog as its expirer" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.catalog = catalog resource.expirer.should equal(catalog) end it "should do nothing when asked to expire when it has no catalog" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) lambda { resource.expire }.should_not raise_error end it "should be able to retrieve a property by name" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.property(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype)) end it "should be able to retrieve a parameter by name" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.parameter(:name).must be_instance_of(Puppet::Type.type(:mount).attrclass(:name)) end it "should be able to retrieve a property by name using the :parameter method" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.parameter(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype)) end it "should be able to retrieve all set properties" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) props = resource.properties props.should_not be_include(nil) [:fstype, :ensure, :pass].each do |name| props.should be_include(resource.parameter(name)) end end it "should have a method for setting default values for resources" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:set_default) end it "should do nothing for attributes that have no defaults and no specified value" do Puppet::Type.type(:mount).new(:name => "foo").parameter(:noop).should be_nil end it "should have a method for adding tags" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:tags) end it "should use the tagging module" do Puppet::Type.type(:mount).ancestors.should be_include(Puppet::Util::Tagging) end it "should delegate to the tagging module when tags are added" do resource = Puppet::Type.type(:mount).new(:name => "foo") resource.stubs(:tag).with(:mount) resource.expects(:tag).with(:tag1, :tag2) resource.tags = [:tag1,:tag2] end it "should add the current type as tag" do resource = Puppet::Type.type(:mount).new(:name => "foo") resource.stubs(:tag) resource.expects(:tag).with(:mount) resource.tags = [:tag1,:tag2] end it "should have a method to know if the resource is exported" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:exported?) end it "should have a method to know if the resource is virtual" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:virtual?) end it "should consider its version to be its catalog version" do resource = Puppet::Type.type(:mount).new(:name => "foo") catalog = Puppet::Resource::Catalog.new catalog.version = 50 catalog.add_resource resource resource.version.should == 50 end it "should consider its version to be zero if it has no catalog" do Puppet::Type.type(:mount).new(:name => "foo").version.should == 0 end it "should provide source_descriptors" do resource = Puppet::Type.type(:mount).new(:name => "foo") catalog = Puppet::Resource::Catalog.new catalog.version = 50 catalog.add_resource resource resource.source_descriptors.should == {:tags=>["mount", "foo"], :path=>"/Mount[foo]"} end it "should consider its type to be the name of its class" do Puppet::Type.type(:mount).new(:name => "foo").type.should == :mount end it "should use any provided noop value" do Puppet::Type.type(:mount).new(:name => "foo", :noop => true).must be_noop end it "should use the global noop value if none is provided" do Puppet[:noop] = true Puppet::Type.type(:mount).new(:name => "foo").must be_noop end it "should not be noop if in a non-host_config catalog" do resource = Puppet::Type.type(:mount).new(:name => "foo") catalog = Puppet::Resource::Catalog.new catalog.add_resource resource resource.should_not be_noop end describe "when creating an event" do before do @resource = Puppet::Type.type(:mount).new :name => "foo" end it "should have the resource's reference as the resource" do @resource.event.resource.should == "Mount[foo]" end it "should have the resource's log level as the default log level" do @resource[:loglevel] = :warning @resource.event.default_log_level.should == :warning end {:file => "/my/file", :line => 50, :tags => %{foo bar}}.each do |attr, value| it "should set the #{attr}" do @resource.stubs(attr).returns value @resource.event.send(attr).should == value end end it "should allow specification of event attributes" do @resource.event(:status => "noop").status.should == "noop" end end describe "when choosing a default provider" do it "should choose the provider with the highest specificity" do # Make a fake type type = Puppet::Type.newtype(:defaultprovidertest) do newparam(:name) do end end basic = type.provide(:basic) {} greater = type.provide(:greater) {} basic.stubs(:specificity).returns 1 greater.stubs(:specificity).returns 2 type.defaultprovider.should equal(greater) end end describe "when initializing" do describe "and passed a TransObject" do it "should fail" do trans = Puppet::TransObject.new("/foo", :mount) lambda { Puppet::Type.type(:mount).new(trans) }.should raise_error(Puppet::DevError) end end describe "and passed a Puppet::Resource instance" do it "should set its title to the title of the resource if the resource type is equal to the current type" do resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "/other"}) Puppet::Type.type(:mount).new(resource).title.should == "/foo" end it "should set its title to the resource reference if the resource type is not equal to the current type" do resource = Puppet::Resource.new(:user, "foo") Puppet::Type.type(:mount).new(resource).title.should == "User[foo]" end [:line, :file, :catalog, :exported, :virtual].each do |param| it "should copy '#{param}' from the resource if present" do resource = Puppet::Resource.new(:mount, "/foo") resource.send(param.to_s + "=", "foo") resource.send(param.to_s + "=", "foo") Puppet::Type.type(:mount).new(resource).send(param).should == "foo" end end it "should copy any tags from the resource" do resource = Puppet::Resource.new(:mount, "/foo") resource.tag "one", "two" tags = Puppet::Type.type(:mount).new(resource).tags tags.should be_include("one") tags.should be_include("two") end it "should copy the resource's parameters as its own" do resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:atboot => true, :fstype => "boo"}) params = Puppet::Type.type(:mount).new(resource).to_hash params[:fstype].should == "boo" params[:atboot].should == true end end describe "and passed a Hash" do it "should extract the title from the hash" do Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay" end it "should work when hash keys are provided as strings" do Puppet::Type.type(:mount).new("title" => "/yay").title.should == "/yay" end it "should work when hash keys are provided as symbols" do Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay" end it "should use the name from the hash as the title if no explicit title is provided" do Puppet::Type.type(:mount).new(:name => "/yay").title.should == "/yay" end it "should use the Resource Type's namevar to determine how to find the name in the hash" do Puppet::Type.type(:file).new(:path => "/yay").title.should == "/yay" end [:catalog].each do |param| it "should extract '#{param}' from the hash if present" do Puppet::Type.type(:mount).new(:name => "/yay", param => "foo").send(param).should == "foo" end end it "should use any remaining hash keys as its parameters" do resource = Puppet::Type.type(:mount).new(:title => "/foo", :catalog => "foo", :atboot => true, :fstype => "boo") resource[:fstype].must == "boo" resource[:atboot].must == true end end it "should fail if any invalid attributes have been provided" do lambda { Puppet::Type.type(:mount).new(:title => "/foo", :nosuchattr => "whatever") }.should raise_error(Puppet::Error) end it "should set its name to the resource's title if the resource does not have a :name or namevar parameter set" do resource = Puppet::Resource.new(:mount, "/foo") Puppet::Type.type(:mount).new(resource).name.should == "/foo" end it "should fail if no title, name, or namevar are provided" do lambda { Puppet::Type.type(:file).new(:atboot => true) }.should raise_error(Puppet::Error) end it "should set the attributes in the order returned by the class's :allattrs method" do Puppet::Type.type(:mount).stubs(:allattrs).returns([:name, :atboot, :noop]) resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => "myboot", :noop => "whatever"}) set = [] Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash| set << param true end.returns(stub_everything("a property")) Puppet::Type.type(:mount).new(resource) set[-1].should == :noop set[-2].should == :atboot end it "should always set the name and then default provider before anything else" do Puppet::Type.type(:mount).stubs(:allattrs).returns([:provider, :name, :atboot]) resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => "myboot"}) set = [] Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash| set << param true end.returns(stub_everything("a property")) Puppet::Type.type(:mount).new(resource) set[0].should == :name set[1].should == :provider end # This one is really hard to test :/ it "should each default immediately if no value is provided" do defaults = [] Puppet::Type.type(:package).any_instance.stubs(:set_default).with { |value| defaults << value; true } Puppet::Type.type(:package).new :name => "whatever" defaults[0].should == :provider end it "should retain a copy of the originally provided parameters" do Puppet::Type.type(:mount).new(:name => "foo", :atboot => true, :noop => false).original_parameters.should == {:atboot => true, :noop => false} end it "should delete the name via the namevar from the originally provided parameters" do Puppet::Type.type(:file).new(:name => "/foo").original_parameters[:path].should be_nil end end it "should have a class method for converting a hash into a Puppet::Resource instance" do Puppet::Type.type(:mount).must respond_to(:hash2resource) end describe "when converting a hash to a Puppet::Resource instance" do before do @type = Puppet::Type.type(:mount) end it "should treat a :title key as the title of the resource" do @type.hash2resource(:name => "/foo", :title => "foo").title.should == "foo" end it "should use the name from the hash as the title if no explicit title is provided" do @type.hash2resource(:name => "foo").title.should == "foo" end it "should use the Resource Type's namevar to determine how to find the name in the hash" do @type.stubs(:key_attributes).returns([ :myname ]) @type.hash2resource(:myname => "foo").title.should == "foo" end [:catalog].each do |attr| it "should use any provided #{attr}" do @type.hash2resource(:name => "foo", attr => "eh").send(attr).should == "eh" end end it "should set all provided parameters on the resource" do @type.hash2resource(:name => "foo", :fstype => "boo", :boot => "fee").to_hash.should == {:name => "foo", :fstype => "boo", :boot => "fee"} end it "should not set the title as a parameter on the resource" do @type.hash2resource(:name => "foo", :title => "eh")[:title].should be_nil end it "should not set the catalog as a parameter on the resource" do @type.hash2resource(:name => "foo", :catalog => "eh")[:catalog].should be_nil end it "should treat hash keys equivalently whether provided as strings or symbols" do resource = @type.hash2resource("name" => "foo", "title" => "eh", "fstype" => "boo") resource.title.should == "eh" resource[:name].should == "foo" resource[:fstype].should == "boo" end end describe "when retrieving current property values" do before do @resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) @resource.property(:ensure).stubs(:retrieve).returns :absent end it "should fail if its provider is unsuitable" do @resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) @resource.provider.class.expects(:suitable?).returns false lambda { @resource.retrieve_resource }.should raise_error(Puppet::Error) end it "should return a Puppet::Resource instance with its type and title set appropriately" do result = @resource.retrieve_resource result.should be_instance_of(Puppet::Resource) result.type.should == "Mount" result.title.should == "foo" end it "should set the name of the returned resource if its own name and title differ" do @resource[:name] = "my name" @resource.title = "other name" @resource.retrieve_resource[:name].should == "my name" end it "should provide a value for all set properties" do values = @resource.retrieve_resource [:ensure, :fstype, :pass].each { |property| values[property].should_not be_nil } end it "should provide a value for 'ensure' even if no desired value is provided" do @resource = Puppet::Type.type(:file).new(:path => "/my/file/that/can't/exist") end it "should not call retrieve on non-ensure properties if the resource is absent and should consider the property absent" do @resource.property(:ensure).expects(:retrieve).returns :absent @resource.property(:fstype).expects(:retrieve).never @resource.retrieve_resource[:fstype].should == :absent end it "should include the result of retrieving each property's current value if the resource is present" do @resource.property(:ensure).expects(:retrieve).returns :present @resource.property(:fstype).expects(:retrieve).returns 15 @resource.retrieve_resource[:fstype] == 15 end end describe ".title_patterns" do describe "when there's one namevar" do before do @type_class = Puppet::Type.type(:notify) @type_class.stubs(:key_attributes).returns([:one]) end it "should have a default pattern for when there's one namevar" do patterns = @type_class.title_patterns patterns.length.should == 1 patterns[0].length.should == 2 end it "should have a regexp that captures the entire string" do patterns = @type_class.title_patterns string = "abc\n\tdef" patterns[0][0] =~ string $1.should == "abc\n\tdef" end end end describe "when in a catalog" do before do @catalog = Puppet::Resource::Catalog.new @container = Puppet::Type.type(:component).new(:name => "container") @one = Puppet::Type.type(:file).new(:path => "/file/one") @two = Puppet::Type.type(:file).new(:path => "/file/two") @catalog.add_resource @container @catalog.add_resource @one @catalog.add_resource @two @catalog.add_edge @container, @one @catalog.add_edge @container, @two end it "should have no parent if there is no in edge" do @container.parent.should be_nil end it "should set its parent to its in edge" do @one.parent.ref.should == @container.ref end after do @catalog.clear(true) end end it "should have a 'stage' metaparam" do Puppet::Type.metaparamclass(:stage).should be_instance_of(Class) end end describe Puppet::Type::RelationshipMetaparam do it "should be a subclass of Puppet::Parameter" do Puppet::Type::RelationshipMetaparam.superclass.should equal(Puppet::Parameter) end it "should be able to produce a list of subclasses" do Puppet::Type::RelationshipMetaparam.should respond_to(:subclasses) end describe "when munging relationships" do before do @resource = Puppet::Type.type(:mount).new :name => "/foo" @metaparam = Puppet::Type.metaparamclass(:require).new :resource => @resource end it "should accept Puppet::Resource instances" do ref = Puppet::Resource.new(:file, "/foo") @metaparam.munge(ref)[0].should equal(ref) end it "should turn any string into a Puppet::Resource" do @metaparam.munge("File[/ref]")[0].should be_instance_of(Puppet::Resource) end end it "should be able to validate relationships" do Puppet::Type.metaparamclass(:require).new(:resource => mock("resource")).should respond_to(:validate_relationship) end it "should fail if any specified resource is not found in the catalog" do catalog = mock 'catalog' resource = stub 'resource', :catalog => catalog, :ref => "resource" param = Puppet::Type.metaparamclass(:require).new(:resource => resource, :value => %w{Foo[bar] Class[test]}) catalog.expects(:resource).with("Foo[bar]").returns "something" catalog.expects(:resource).with("Class[Test]").returns nil param.expects(:fail).with { |string| string.include?("Class[Test]") } param.validate_relationship end end describe Puppet::Type.metaparamclass(:check) do it "should warn and create an instance of ':audit'" do file = Puppet::Type.type(:file).new :path => "/foo" file.expects(:warning) file[:check] = :mode file[:audit].should == [:mode] end end describe Puppet::Type.metaparamclass(:audit) do before do @resource = Puppet::Type.type(:file).new :path => "/foo" end it "should default to being nil" do @resource[:audit].should be_nil end it "should specify all possible properties when asked to audit all properties" do @resource[:audit] = :all list = @resource.class.properties.collect { |p| p.name } @resource[:audit].should == list end it "should accept the string 'all' to specify auditing all possible properties" do @resource[:audit] = 'all' list = @resource.class.properties.collect { |p| p.name } @resource[:audit].should == list end it "should fail if asked to audit an invalid property" do lambda { @resource[:audit] = :foobar }.should raise_error(Puppet::Error) end it "should create an attribute instance for each auditable property" do @resource[:audit] = :mode @resource.parameter(:mode).should_not be_nil end it "should accept properties specified as a string" do @resource[:audit] = "mode" @resource.parameter(:mode).should_not be_nil end it "should not create attribute instances for parameters, only properties" do @resource[:audit] = :noop @resource.parameter(:noop).should be_nil end describe "when generating the uniqueness key" do it "should include all of the key_attributes in alphabetical order by attribute name" do Puppet::Type.type(:file).stubs(:key_attributes).returns [:path, :mode, :owner] Puppet::Type.type(:file).stubs(:title_patterns).returns( [ [ /(.*)/, [ [:path, lambda{|x| x} ] ] ] ] ) res = Puppet::Type.type(:file).new( :title => '/my/file', :path => '/my/file', :owner => 'root', :content => 'hello' ) res.uniqueness_key.should == [ nil, 'root', '/my/file'] end end end diff --git a/test/ral/type/exec.rb b/test/ral/type/exec.rb index 5b26a98a2..dd42ae61f 100755 --- a/test/ral/type/exec.rb +++ b/test/ral/type/exec.rb @@ -1,847 +1,758 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' class TestExec < Test::Unit::TestCase include PuppetTest def test_numvsstring [0, "0"].each { |val| command = nil output = nil assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "/bin/echo", :returns => val ) } assert_events([:executed_command], command) } end def test_path_or_qualified command = nil output = nil assert_raise(Puppet::Error) { command = Puppet::Type.type(:exec).new( :command => "echo" ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( - :command => "echo", - - :path => "/usr/bin:/bin:/usr/sbin:/sbin" + :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "/bin/echo" ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( - :command => "/bin/echo", - - :path => "/usr/bin:/bin:/usr/sbin:/sbin" + :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } end def test_nonzero_returns assert_nothing_raised { command = Puppet::Type.type(:exec).new( - :command => "mkdir /this/directory/does/not/exist", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - + :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( - :command => "touch /etc", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - + :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( - :command => "thiscommanddoesnotexist", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - + :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 127 ) } end def test_cwdsettings command = nil dir = "/tmp" wd = Dir.chdir(dir) { Dir.getwd } assert_nothing_raised { command = Puppet::Type.type(:exec).new( - :command => "pwd", - :cwd => dir, - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - + :cwd => dir, + :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 0 ) } assert_events([:executed_command], command) assert_equal(wd,command.output.chomp) end def test_refreshonly_functional file = nil cmd = nil tmpfile = tempfile @@tmpfiles.push tmpfile trans = nil - file = Puppet::Type.type(:file).new( - - :path => tmpfile, - - :content => "yay" + file = Puppet::Type.type(:file).new( + :path => tmpfile, + :content => "yay" ) # Get the file in sync assert_apply(file) # Now make an exec maker = tempfile assert_nothing_raised { - cmd = Puppet::Type.type(:exec).new( - - :command => "touch #{maker}", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - :subscribe => file, - + :command => "touch #{maker}", + :path => "/usr/bin:/bin:/usr/sbin:/sbin", + :subscribe => file, :refreshonly => true ) } assert(cmd, "did not make exec") assert_nothing_raised do - assert(! cmd.check, "Check passed when refreshonly is set") + assert(! cmd.check_all_attributes, "Check passed when refreshonly is set") end assert_events([], file, cmd) assert(! FileTest.exists?(maker), "made file without refreshing") # Now change our content, so we throw a refresh file[:content] = "yayness" assert_events([:content_changed, :restarted], file, cmd) assert(FileTest.exists?(maker), "file was not made in refresh") end def test_refreshonly cmd = true assert_nothing_raised { - cmd = Puppet::Type.type(:exec).new( - - :command => "pwd", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - + :command => "pwd", + :path => "/usr/bin:/bin:/usr/sbin:/sbin", :refreshonly => true ) } # Checks should always fail when refreshonly is enabled - assert(!cmd.check, "Check passed with refreshonly true") + assert(!cmd.check_all_attributes, "Check passed with refreshonly true") # Now make sure it passes if we pass in "true" - assert(cmd.check(true), "Check failed with refreshonly true while refreshing") + assert(cmd.check_all_attributes(true), "Check failed with refreshonly true while refreshing") # Now set it to false cmd[:refreshonly] = false - assert(cmd.check, "Check failed with refreshonly false") + assert(cmd.check_all_attributes, "Check failed with refreshonly false") end def test_creates file = tempfile exec = nil assert(! FileTest.exists?(file), "File already exists") assert_nothing_raised { exec = Puppet::Type.type(:exec).new( - :command => "touch #{file}", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - + :path => "/usr/bin:/bin:/usr/sbin:/sbin", :creates => file ) } comp = mk_catalog("createstest", exec) assert_events([:executed_command], comp, "creates") assert_events([], comp, "creates") end # Verify that we can download the file that we're going to execute. def test_retrievethenmkexe exe = tempfile oexe = tempfile sh = %x{which sh} File.open(exe, "w") { |f| f.puts "#!#{sh}\necho yup" } - - file = Puppet::Type.type(:file).new( - - :path => oexe, - :source => exe, - - :mode => 0755 + file = Puppet::Type.type(:file).new( + :path => oexe, + :source => exe, + :mode => 0755 ) - - exec = Puppet::Type.type(:exec).new( - - :command => oexe, - - :require => Puppet::Resource.new(:file, oexe) + exec = Puppet::Type.type(:exec).new( + :command => oexe, + :require => Puppet::Resource.new(:file, oexe) ) comp = mk_catalog("Testing", file, exec) assert_events([:file_created, :executed_command], comp) end # Verify that we auto-require any managed scripts. def test_autorequire_files exe = tempfile oexe = tempfile sh = %x{which sh} File.open(exe, "w") { |f| f.puts "#!#{sh}\necho yup" } - file = Puppet::Type.type(:file).new( - - :path => oexe, - :source => exe, - - :mode => 755 + file = Puppet::Type.type(:file).new( + :path => oexe, + :source => exe, + :mode => 755 ) basedir = File.dirname(oexe) - baseobj = Puppet::Type.type(:file).new( - - :path => basedir, - :source => exe, - - :mode => 755 + baseobj = Puppet::Type.type(:file).new( + :path => basedir, + :source => exe, + :mode => 755 ) - ofile = Puppet::Type.type(:file).new( - - :path => exe, - - :mode => 755 + ofile = Puppet::Type.type(:file).new( + :path => exe, + :mode => 755 ) - exec = Puppet::Type.type(:exec).new( - - :command => oexe, - :path => ENV["PATH"], - - :cwd => basedir + exec = Puppet::Type.type(:exec).new( + :command => oexe, + :path => ENV["PATH"], + :cwd => basedir ) - - cat = Puppet::Type.type(:exec).new( - - :command => "cat #{exe} #{oexe}", - - :path => ENV["PATH"] + cat = Puppet::Type.type(:exec).new( + :command => "cat #{exe} #{oexe}", + :path => ENV["PATH"] ) catalog = mk_catalog(file, baseobj, ofile, exec, cat) rels = nil assert_nothing_raised do rels = exec.autorequire end # Verify we get the script itself assert(rels.detect { |r| r.source == file }, "Exec did not autorequire its command") # Verify we catch the cwd assert(rels.detect { |r| r.source == baseobj }, "Exec did not autorequire its cwd") # Verify we don't require ourselves assert(! rels.detect { |r| r.source == ofile }, "Exec incorrectly required mentioned file") # We not longer autorequire inline files assert_nothing_raised do rels = cat.autorequire end assert(! rels.detect { |r| r.source == ofile }, "Exec required second inline file") assert(! rels.detect { |r| r.source == file }, "Exec required inline file") end def test_ifonly afile = tempfile bfile = tempfile exec = nil assert_nothing_raised { exec = Puppet::Type.type(:exec).new( - :command => "touch #{bfile}", - :onlyif => "test -f #{afile}", - - :path => ENV['PATH'] + :onlyif => "test -f #{afile}", + :path => ENV['PATH'] ) } assert_events([], exec) system("touch #{afile}") assert_events([:executed_command], exec) assert_events([:executed_command], exec) system("rm #{afile}") assert_events([], exec) end def test_unless afile = tempfile bfile = tempfile exec = nil assert_nothing_raised { - exec = Puppet::Type.type(:exec).new( - :command => "touch #{bfile}", :unless => "test -f #{afile}", - :path => ENV['PATH'] ) } comp = mk_catalog(exec) assert_events([:executed_command], comp) assert_events([:executed_command], comp) system("touch #{afile}") assert_events([], comp) assert_events([], comp) system("rm #{afile}") assert_events([:executed_command], comp) assert_events([:executed_command], comp) end if Puppet.features.root? # Verify that we can execute commands as a special user def mknverify(file, user, group = nil, id = true) File.umask(0022) args = { :command => "touch #{file}", :path => "/usr/bin:/bin:/usr/sbin:/sbin", } if user #Puppet.warning "Using user #{user.name}" if id # convert to a string, because that's what the object expects args[:user] = user.uid.to_s else args[:user] = user.name end end if group #Puppet.warning "Using group #{group.name}" if id args[:group] = group.gid.to_s else args[:group] = group.name end end exec = nil assert_nothing_raised { exec = Puppet::Type.type(:exec).new(args) } comp = mk_catalog("usertest", exec) assert_events([:executed_command], comp, "usertest") assert(FileTest.exists?(file), "File does not exist") assert_equal(user.uid, File.stat(file).uid, "File UIDs do not match") if user # We can't actually test group ownership, unfortunately, because # behaviour changes wildlly based on platform. Puppet::Type.allclear end def test_userngroup file = tempfile [ [nonrootuser], # just user, by name [nonrootuser, nil, true], # user, by uid [nil, nonrootgroup], # just group [nil, nonrootgroup, true], # just group, by id [nonrootuser, nonrootgroup], # user and group, by name [nonrootuser, nonrootgroup, true], # user and group, by id ].each { |ary| mknverify(file, *ary) { } } end end def test_logoutput exec = nil assert_nothing_raised { - exec = Puppet::Type.type(:exec).new( - - :title => "logoutputesting", - :path => "/usr/bin:/bin", - :command => "echo logoutput is false", - + :title => "logoutputesting", + :path => "/usr/bin:/bin", + :command => "echo logoutput is false", :logoutput => false ) } assert_apply(exec) assert_nothing_raised { exec[:command] = "echo logoutput is true" exec[:logoutput] = true } assert_apply(exec) assert_nothing_raised { exec[:command] = "echo logoutput is on_failure" exec[:logoutput] = "on_failure" } assert_apply(exec) end def test_execthenfile exec = nil file = nil basedir = tempfile path = File.join(basedir, "subfile") assert_nothing_raised { exec = Puppet::Type.type(:exec).new( - - :title => "mkdir", - :path => "/usr/bin:/bin", + :title => "mkdir", + :path => "/usr/bin:/bin", :creates => basedir, - :command => "mkdir #{basedir}; touch #{path}" - ) } assert_nothing_raised { file = Puppet::Type.type(:file).new( - - :path => basedir, + :path => basedir, :recurse => true, - :mode => "755", - + :mode => "755", :require => Puppet::Resource.new("exec", "mkdir") ) } comp = mk_catalog(file, exec) comp.finalize assert_events([:executed_command, :mode_changed], comp) assert(FileTest.exists?(path), "Exec ran first") assert(File.stat(path).mode & 007777 == 0755) end # Make sure all checks need to be fully qualified. def test_falsevals exec = nil assert_nothing_raised do exec = Puppet::Type.type(:exec).new( :command => "/bin/touch yayness" ) end Puppet::Type.type(:exec).checks.each do |check| klass = Puppet::Type.type(:exec).paramclass(check) next if klass.value_collection.values.include? :false assert_raise(Puppet::Error, "Check '#{check}' did not fail on false") do exec[check] = false end end end def test_createcwdandexe exec1 = exec2 = nil dir = tempfile file = tempfile assert_nothing_raised { - exec1 = Puppet::Type.type(:exec).new( - - :title => "one", - :path => ENV["PATH"], - + :title => "one", + :path => ENV["PATH"], :command => "mkdir #{dir}" ) } assert_nothing_raised("Could not create exec w/out existing cwd") { exec2 = Puppet::Type.type(:exec).new( - - :title => "two", - :path => ENV["PATH"], + :title => "two", + :path => ENV["PATH"], :command => "touch #{file}", - - :cwd => dir + :cwd => dir ) } # Throw a check in there with our cwd and make sure it works assert_nothing_raised("Could not check with a missing cwd") do exec2[:unless] = "test -f /this/file/does/not/exist" exec2.retrieve end assert_raise(Puppet::Error) do exec2.property(:returns).sync end assert_nothing_raised do exec2[:require] = exec1 end assert_apply(exec1, exec2) assert(FileTest.exists?(file)) end def test_checkarrays exec = nil file = tempfile test = "test -f #{file}" assert_nothing_raised { - exec = Puppet::Type.type(:exec).new( - - :path => ENV["PATH"], - + :path => ENV["PATH"], :command => "touch #{file}" ) } assert_nothing_raised { exec[:unless] = test } assert_nothing_raised { - assert(exec.check, "Check did not pass") + assert(exec.check_all_attributes, "Check did not pass") } assert_nothing_raised { exec[:unless] = [test, test] } assert_nothing_raised { exec.finish } assert_nothing_raised { - assert(exec.check, "Check did not pass") + assert(exec.check_all_attributes, "Check did not pass") } assert_apply(exec) assert_nothing_raised { - assert(! exec.check, "Check passed") + assert(! exec.check_all_attributes, "Check passed") } end def test_missing_checks_cause_failures # Solaris's sh exits with 1 here instead of 127 return if Facter.value(:operatingsystem) == "Solaris" exec = Puppet::Type.type(:exec).new( - :command => "echo true", - :path => ENV["PATH"], - - :onlyif => "/bin/nosuchthingexists" - ) + :path => ENV["PATH"], + :onlyif => "/bin/nosuchthingexists" + ) assert_raise(ArgumentError, "Missing command did not raise error") { - exec.run("/bin/nosuchthingexists") + exec.provider.run("/bin/nosuchthingexists") } end def test_envparam exec = Puppet::Type.newexec( - :command => "echo $envtest", - :path => ENV["PATH"], - - :env => "envtest=yayness" + :path => ENV["PATH"], + :env => "envtest=yayness" ) assert(exec, "Could not make exec") output = status = nil assert_nothing_raised { - output, status = exec.run("echo $envtest") + output, status = exec.provider.run("echo $envtest") } assert_equal("yayness\n", output) # Now check whether we can do multiline settings assert_nothing_raised do exec[:env] = "envtest=a list of things and stuff" end output = status = nil assert_nothing_raised { - output, status = exec.run('echo "$envtest"') + output, status = exec.provider.run('echo "$envtest"') } assert_equal("a list of things\nand stuff\n", output) # Now test arrays assert_nothing_raised do exec[:env] = ["funtest=A", "yaytest=B"] end output = status = nil assert_nothing_raised { - output, status = exec.run('echo "$funtest" "$yaytest"') + output, status = exec.provider.run('echo "$funtest" "$yaytest"') } assert_equal("A B\n", output) end def test_environmentparam exec = Puppet::Type.newexec( - - :command => "echo $environmenttest", - :path => ENV["PATH"], - + :command => "echo $environmenttest", + :path => ENV["PATH"], :environment => "environmenttest=yayness" ) assert(exec, "Could not make exec") output = status = nil assert_nothing_raised { - output, status = exec.run("echo $environmenttest") + output, status = exec.provider.run("echo $environmenttest") } assert_equal("yayness\n", output) # Now check whether we can do multiline settings assert_nothing_raised do exec[:environment] = "environmenttest=a list of things and stuff" end output = status = nil assert_nothing_raised { - output, status = exec.run('echo "$environmenttest"') + output, status = exec.provider.run('echo "$environmenttest"') } assert_equal("a list of things\nand stuff\n", output) # Now test arrays assert_nothing_raised do exec[:environment] = ["funtest=A", "yaytest=B"] end output = status = nil assert_nothing_raised { - output, status = exec.run('echo "$funtest" "$yaytest"') + output, status = exec.provider.run('echo "$funtest" "$yaytest"') } assert_equal("A B\n", output) end - def test_timeout - exec = Puppet::Type.type(:exec).new(:command => "sleep 1", :path => ENV["PATH"], :timeout => "0.2") - time = Time.now - - assert_raise(Timeout::Error) { - exec.run("sleep 1") - } - Puppet.info "#{Time.now.to_f - time.to_f} seconds, vs a timeout of #{exec[:timeout]}" - - - assert_apply(exec) - end - # Testing #470 def test_run_as_created_user exec = nil if Process.uid == 0 user = "nosuchuser" assert_nothing_raised("Could not create exec with non-existent user") do - exec = Puppet::Type.type(:exec).new( - :command => "/bin/echo yay", - - :user => user + :user => user ) end end # Now try the group group = "nosuchgroup" assert_nothing_raised("Could not create exec with non-existent user") do exec = Puppet::Type.type(:exec).new( - :command => "/bin/echo yay", - - :group => group + :group => group ) end end # make sure paths work both as arrays and strings def test_paths_as_arrays path = %w{/usr/bin /usr/sbin /sbin} exec = nil assert_nothing_raised("Could not use an array for the path") do exec = Puppet::Type.type(:exec).new(:command => "echo yay", :path => path) end assert_equal(path, exec[:path], "array-based path did not match") assert_nothing_raised("Could not use a string for the path") do exec = Puppet::Type.type(:exec).new(:command => "echo yay", :path => path.join(":")) end assert_equal(path, exec[:path], "string-based path did not match") assert_nothing_raised("Could not use a colon-separated strings in an array for the path") do exec = Puppet::Type.type(:exec).new(:command => "echo yay", :path => ["/usr/bin", "/usr/sbin:/sbin"]) end assert_equal(path, exec[:path], "colon-separated array path did not match") end def test_checks_apply_to_refresh file = tempfile maker = tempfile - exec = Puppet::Type.type(:exec).new( - - :title => "maker", - :command => "touch #{maker}", - - :path => ENV["PATH"] + exec = Puppet::Type.type(:exec).new( + :title => "maker", + :command => "touch #{maker}", + :path => ENV["PATH"] ) # Make sure it runs normally assert_apply(exec) assert(FileTest.exists?(maker), "exec did not run") File.unlink(maker) # Now make sure it refreshes assert_nothing_raised("Failed to refresh exec") do exec.refresh end assert(FileTest.exists?(maker), "exec did not run refresh") File.unlink(maker) # Now add the checks exec[:creates] = file # Make sure it runs when the file doesn't exist assert_nothing_raised("Failed to refresh exec") do exec.refresh end assert(FileTest.exists?(maker), "exec did not refresh when checks passed") File.unlink(maker) # Now create the file and make sure it doesn't refresh File.open(file, "w") { |f| f.puts "" } assert_nothing_raised("Failed to refresh exec") do exec.refresh end assert(! FileTest.exists?(maker), "exec refreshed with failing checks") end def test_explicit_refresh refresher = tempfile maker = tempfile - exec = Puppet::Type.type(:exec).new( - - :title => "maker", - :command => "touch #{maker}", - - :path => ENV["PATH"] + exec = Puppet::Type.type(:exec).new( + :title => "maker", + :command => "touch #{maker}", + :path => ENV["PATH"] ) # Call refresh normally assert_nothing_raised do exec.refresh end # Make sure it created the normal file assert(FileTest.exists?(maker), "normal refresh did not work") File.unlink(maker) # Now reset refresh, and make sure it wins assert_nothing_raised("Could not set refresh parameter") do exec[:refresh] = "touch #{refresher}" end assert_nothing_raised do exec.refresh end # Make sure it created the normal file assert(FileTest.exists?(refresher), "refresh param was ignored") assert(! FileTest.exists?(maker), "refresh param also ran command") end if Puppet.features.root? def test_autorequire_user user = Puppet::Type.type(:user).new(:name => "yay") exec = Puppet::Type.type(:exec).new(:command => "/bin/echo fun", :user => "yay") rels = nil assert_nothing_raised("Could not evaluate autorequire") do rels = exec.autorequire end assert(rels.find { |r| r.source == user and r.target == exec }, "Exec did not autorequire user") end end end -