diff --git a/lib/puppet/transportable.rb b/lib/puppet/transportable.rb index f686fbb78..3ad084b3b 100644 --- a/lib/puppet/transportable.rb +++ b/lib/puppet/transportable.rb @@ -1,247 +1,255 @@ require 'puppet' require 'puppet/resource_reference' require 'yaml' module Puppet # The transportable objects themselves. Basically just a hash with some # metadata and a few extra methods. I used to have the object actually # be a subclass of Hash, but I could never correctly dump them using # YAML. class TransObject include Enumerable attr_accessor :type, :name, :file, :line, :catalog attr_writer :tags %w{has_key? include? length delete empty? << [] []=}.each { |method| define_method(method) do |*args| @params.send(method, *args) end } def each @params.each { |p,v| yield p, v } end def initialize(name,type) @type = type.to_s.downcase @name = name @params = {} @tags = [] end def longname return [@type,@name].join('--') end def ref unless defined? @ref @ref = Puppet::ResourceReference.new(@type, @name) end @ref.to_s end def tags return @tags end # Convert a defined type into a component. def to_component trans = TransObject.new(ref, :component) @params.each { |param,value| next unless Puppet::Type::Component.validattr?(param) Puppet.debug "Defining %s on %s" % [param, ref] trans[param] = value } Puppet::Type::Component.create(trans) end def to_hash @params.dup end def to_s return "%s(%s) => %s" % [@type,@name,super] end def to_manifest "#{self.type.to_s} { \'#{self.name}\':\n%s\n}" % @params.collect { |p, v| if v.is_a? Array " #{p} => [\'#{v.join("','")}\']" else " #{p} => \'#{v}\'" end }.join(",\n") end def to_yaml_properties instance_variables.reject { |v| %w{@ref}.include?(v) } end def to_ref ref end def to_type - retobj = nil if typeklass = Puppet::Type.type(self.type) return typeklass.create(self) else return to_component end - - return retobj end end # Just a linear container for objects. Behaves mostly like an array, except # that YAML will correctly dump them even with their instance variables. class TransBucket include Enumerable attr_accessor :name, :type, :file, :line, :classes, :keyword, :top, :catalog %w{delete shift include? length empty? << []}.each { |method| define_method(method) do |*args| #Puppet.warning "Calling %s with %s" % [method, args.inspect] @children.send(method, *args) #Puppet.warning @params.inspect end } # Recursively yield everything. def delve(&block) @children.each do |obj| block.call(obj) if obj.is_a? self.class obj.delve(&block) else obj end end end def each @children.each { |c| yield c } end # Turn our heirarchy into a flat list def flatten @children.collect do |obj| if obj.is_a? Puppet::TransBucket obj.flatten else obj end end.flatten end def initialize(children = []) @children = children end def push(*args) args.each { |arg| case arg when Puppet::TransBucket, Puppet::TransObject # nada else raise Puppet::DevError, "TransBuckets cannot handle objects of type %s" % arg.class end } @children += args end # Convert to a parseable manifest def to_manifest unless self.top unless defined? @keyword and @keyword raise Puppet::DevError, "No keyword; cannot convert to manifest" end end str = "#{@keyword} #{@name} {\n%s\n}" str % @children.collect { |child| child.to_manifest }.collect { |str| if self.top str else str.gsub(/^/, " ") # indent everything once end }.join("\n\n") # and throw in a blank line end def to_yaml_properties instance_variables end # Create a resource graph from our structure. - def to_catalog - catalog = Puppet::Node::Catalog.new(Facter.value("hostname")) do |config| - delver = proc do |obj| - obj.catalog = config - unless container = config.resource(obj.to_ref) - container = obj.to_type - config.add_resource container + def to_catalog(clear_on_failure = true) + catalog = Puppet::Node::Catalog.new(Facter.value("hostname")) + + # This should really use the 'delve' method, but this + # whole class is going away relatively soon, hopefully, + # so it's not worth it. + delver = proc do |obj| + obj.catalog = catalog + unless container = catalog.resource(obj.to_ref) + container = obj.to_type + catalog.add_resource container + end + obj.each do |child| + child.catalog = catalog + unless resource = catalog.resource(child.to_ref) + resource = child.to_type + catalog.add_resource resource end - obj.each do |child| - child.catalog = config - unless resource = config.resource(child.to_ref) - next unless resource = child.to_type - config.add_resource resource - end - config.add_edge(container, resource) - if child.is_a?(self.class) - delver.call(child) - end + + catalog.add_edge(container, resource) + if child.is_a?(self.class) + delver.call(child) end end + end + begin delver.call(self) + catalog.finalize + rescue => detail + # This is important until we lose the global resource references. + catalog.clear if (clear_on_failure) + raise end return catalog end def to_ref unless defined? @ref if self.type and self.name @ref = Puppet::ResourceReference.new(self.type, self.name) elsif self.type and ! self.name # This is old-school node types @ref = Puppet::ResourceReference.new("node", self.type) elsif ! self.type and self.name @ref = Puppet::ResourceReference.new("component", self.name) else @ref = nil end end @ref.to_s if @ref end def to_type Puppet.debug("TransBucket '%s' has no type" % @name) unless defined? @type # Nodes have the same name and type trans = TransObject.new(to_ref, :component) if defined? @parameters @parameters.each { |param,value| Puppet.debug "Defining %s on %s" % [param, to_ref] trans[param] = value } end return Puppet::Type::Component.create(trans) end def param(param,value) unless defined? @parameters @parameters = {} end @parameters[param] = value end end end diff --git a/lib/puppet/util/settings.rb b/lib/puppet/util/settings.rb index c6797a767..d27406d6d 100644 --- a/lib/puppet/util/settings.rb +++ b/lib/puppet/util/settings.rb @@ -1,1252 +1,1248 @@ require 'puppet' require 'sync' require 'puppet/transportable' require 'getoptlong' # The class for handling configuration files. class Puppet::Util::Settings include Enumerable include Puppet::Util @@sync = Sync.new attr_accessor :file attr_reader :timer # Retrieve a config value def [](param) value(param) end # Set a config value. This doesn't set the defaults, it sets the value itself. def []=(param, value) @@sync.synchronize do # yay, thread-safe param = symbolize(param) unless element = @config[param] raise ArgumentError, "Attempt to assign a value to unknown configuration parameter %s" % param.inspect end if element.respond_to?(:munge) value = element.munge(value) end if element.respond_to?(:handle) element.handle(value) end # Reset the name, so it's looked up again. if param == :name @name = nil end @values[:memory][param] = value @cache.clear end return value end - # A simplified equality operator. - # LAK: For some reason, this causes mocha to not be able to mock - # the 'value' method, and it's not used anywhere. -# def ==(other) -# self.each { |myname, myobj| -# unless other[myname] == value(myname) -# return false -# end -# } -# -# return true -# end - # Generate the list of valid arguments, in a format that GetoptLong can # understand, and add them to the passed option list. def addargs(options) # Hackish, but acceptable. Copy the current ARGV for restarting. Puppet.args = ARGV.dup # Add all of the config parameters as valid options. self.each { |name, element| element.getopt_args.each { |args| options << args } } return options end # Turn the config into a Puppet configuration and apply it def apply trans = self.to_transportable begin config = trans.to_catalog config.store_state = false config.apply config.clear rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not configure myself: %s" % detail end end # Is our parameter a boolean parameter? def boolean?(param) param = symbolize(param) if @config.include?(param) and @config[param].kind_of? CBoolean return true else return false end end # Remove all set values, potentially skipping cli values. def clear(exceptcli = false) @config.each { |name, obj| unless exceptcli and obj.setbycli obj.clear end } @values.each do |name, values| next if name == :cli and exceptcli @values.delete(name) end # Don't clear the 'used' in this case, since it's a config file reparse, # and we want to retain this info. unless exceptcli @used = [] end @cache.clear @name = nil end # This is mostly just used for testing. def clearused @cache.clear @used = [] end # Do variable interpolation on the value. def convert(value) return value unless value return value unless value.is_a? String newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value| varname = $2 || $1 if pval = self.value(varname) pval else raise Puppet::DevError, "Could not find value for %s" % parent end end return newval end # Return a value's description. def description(name) if obj = @config[symbolize(name)] obj.desc else nil end end def each @config.each { |name, object| yield name, object } end # Iterate over each section name. def eachsection yielded = [] @config.each do |name, object| section = object.section unless yielded.include? section yield section yielded << section end end end # Return an object by name. def element(param) param = symbolize(param) @config[param] end # Handle a command-line argument. def handlearg(opt, value = nil) @cache.clear value = munge_value(value) if value str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end str = str.intern if self.valid?(str) if self.boolean?(str) @values[:cli][str] = bool else @values[:cli][str] = value end else raise ArgumentError, "Invalid argument %s" % opt end end def include?(name) name = name.intern if name.is_a? String @config.include?(name) end # check to see if a short name is already defined def shortinclude?(short) short = short.intern if name.is_a? String @shortnames.include?(short) end # Create a new config object def initialize @config = {} @shortnames = {} @created = [] @searchpath = nil # Keep track of set values. @values = Hash.new { |hash, key| hash[key] = {} } # And keep a per-environment cache @cache = Hash.new { |hash, key| hash[key] = {} } # A central concept of a name. @name = nil # The list of sections we've used. @used = [] end # Return a given object's file metadata. def metadata(param) if obj = @config[symbolize(param)] and obj.is_a?(CFile) return [:owner, :group, :mode].inject({}) do |meta, p| if v = obj.send(p) meta[p] = v end meta end else nil end end # Make a directory with the appropriate user, group, and mode def mkdir(default) obj = get_config_file_default(default) Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end # Figure out our name. def name unless @name unless @config[:name] return nil end searchpath.each do |source| next if source == :name break if @name = @values[source][:name] end unless @name @name = convert(@config[:name].default).intern end end @name end # Return all of the parameters associated with a given section. def params(section = nil) if section section = section.intern if section.is_a? String @config.find_all { |name, obj| obj.section == section }.collect { |name, obj| name } else @config.keys end end # Parse the configuration file. def parse(file) clear(true) parse_file(file).each do |area, values| @values[area] = values end # Determine our environment, if we have one. if @config[:environment] env = self.value(:environment).to_sym else env = "none" end # Call any hooks we should be calling. settings_with_hooks.each do |setting| each_source(env) do |source| if value = @values[source][setting.name] # We still have to use value() to retrieve the value, since # we want the fully interpolated value, not $vardir/lib or whatever. # This results in extra work, but so few of the settings # will have associated hooks that it ends up being less work this # way overall. setting.handle(self.value(setting.name, env)) break end end end # We have to do it in the reverse of the search path, # because multiple sections could set the same value # and I'm too lazy to only set the metadata once. searchpath.reverse.each do |source| if meta = @values[source][:_meta] set_metadata(meta) end end end # Parse the configuration file. As of May 2007, this is a backward-compatibility method and # will be deprecated soon. def old_parse(file) text = nil if file.is_a? Puppet::Util::LoadedFile @file = file else @file = Puppet::Util::LoadedFile.new(file) end # Don't create a timer for the old style parsing. # settimer() begin text = File.read(@file.file) rescue Errno::ENOENT raise Puppet::Error, "No such file %s" % file rescue Errno::EACCES raise Puppet::Error, "Permission denied to file %s" % file end @values = Hash.new { |names, name| names[name] = {} } # Get rid of the values set by the file, keeping cli values. self.clear(true) section = "puppet" metas = %w{owner group mode} values = Hash.new { |hash, key| hash[key] = {} } text.split(/\n/).each { |line| case line when /^\[(\w+)\]$/: section = $1 # Section names when /^\s*#/: next # Skip comments when /^\s*$/: next # Skip blanks when /^\s*(\w+)\s*=\s*(.+)$/: # settings var = $1.intern if var == :mode value = $2 else value = munge_value($2) end # Only warn if we don't know what this config var is. This # prevents exceptions later on. unless @config.include?(var) or metas.include?(var.to_s) Puppet.warning "Discarded unknown configuration parameter %s" % var.inspect next # Skip this line. end # Mmm, "special" attributes if metas.include?(var.to_s) unless values.include?(section) values[section] = {} end values[section][var.to_s] = value # If the parameter is valid, then set it. if section == Puppet[:name] and @config.include?(var) #@config[var].value = value @values[:main][var] = value end next end # Don't override set parameters, since the file is parsed # after cli arguments are handled. unless @config.include?(var) and @config[var].setbycli Puppet.debug "%s: Setting %s to '%s'" % [section, var, value] @values[:main][var] = value end @config[var].section = symbolize(section) metas.each { |meta| if values[section][meta] if @config[var].respond_to?(meta + "=") @config[var].send(meta + "=", values[section][meta]) end end } else raise Puppet::Error, "Could not match line %s" % line end } end # Create a new element. The value is passed in because it's used to determine # what kind of element we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. def newelement(hash) value = hash[:value] || hash[:default] klass = nil if hash[:section] hash[:section] = symbolize(hash[:section]) end case value when true, false, "true", "false": klass = CBoolean when /^\$\w+\//, /^\//: klass = CFile when String, Integer, Float: # nothing klass = CElement else raise Puppet::Error, "Invalid value '%s' for %s" % [value.inspect, hash[:name]] end hash[:parent] = self element = klass.new(hash) return element end # This has to be private, because it doesn't add the elements to @config private :newelement # Iterate across all of the objects in a given section. def persection(section) section = symbolize(section) self.each { |name, obj| if obj.section == section yield obj end } end # Reparse our config file, if necessary. def reparse if defined? @file and @file.changed? Puppet.notice "Reparsing %s" % @file.file @@sync.synchronize do parse(@file) end reuse() end end def reuse return unless defined? @used @@sync.synchronize do # yay, thread-safe @used.each do |section| @used.delete(section) self.use(section) end end end # The order in which to search for values. def searchpath(environment = nil) if environment [:cli, :memory, environment, :name, :main] else [:cli, :memory, :name, :main] end end # Get a list of objects per section def sectionlist sectionlist = [] self.each { |name, obj| section = obj.section || "puppet" sections[section] ||= [] unless sectionlist.include?(section) sectionlist << section end sections[section] << obj } return sectionlist, sections end # Convert a single section into transportable objects. def section_to_transportable(section, done = nil) done ||= Hash.new { |hash, key| hash[key] = {} } objects = [] persection(section) do |obj| if @config[:mkusers] and value(:mkusers) objects += add_user_resources(section, obj, done) end value = obj.value # Only files are convertable to transportable resources. next unless obj.respond_to? :to_transportable and transobjects = obj.to_transportable transobjects = [transobjects] unless transobjects.is_a? Array transobjects.each do |trans| # transportable could return nil next unless trans unless done[:file].include? trans.name @created << trans.name objects << trans done[:file][trans.name] = trans end end end bucket = Puppet::TransBucket.new bucket.type = "Settings" bucket.name = section bucket.push(*objects) bucket.keyword = "class" return bucket end # Set a bunch of defaults in a given section. The sections are actually pretty # pointless, but they help break things up a bit, anyway. def setdefaults(section, defs) section = symbolize(section) call = [] defs.each { |name, hash| if hash.is_a? Array unless hash.length == 2 raise ArgumentError, "Defaults specified as an array must contain only the default value and the decription" end tmp = hash hash = {} [:default, :desc].zip(tmp).each { |p,v| hash[p] = v } end name = symbolize(name) hash[:name] = name hash[:section] = section name = hash[:name] if @config.include?(name) raise ArgumentError, "Parameter %s is already defined" % name end tryconfig = newelement(hash) if short = tryconfig.short if other = @shortnames[short] raise ArgumentError, "Parameter %s is already using short name '%s'" % [other.name, short] end @shortnames[short] = tryconfig end @config[name] = tryconfig # Collect the settings that need to have their hooks called immediately. # We have to collect them so that we can be sure we're fully initialized before # the hook is called. call << tryconfig if tryconfig.call_on_define } call.each { |setting| setting.handle(self.value(setting.name)) } end # Create a timer to check whether the file should be reparsed. def settimer if Puppet[:filetimeout] > 0 @timer = Puppet.newtimer( :interval => Puppet[:filetimeout], :tolerance => 1, :start? => true ) do self.reparse() end end end # Convert our list of objects into a component that can be applied. def to_configuration transport = self.to_transportable return transport.to_catalog end # Convert our list of config elements into a configuration file. def to_config str = %{The configuration file for #{Puppet[:name]}. Note that this file is likely to have unused configuration parameters in it; any parameter that's valid anywhere in Puppet can be in any config file, even if it's not used. Every section can specify three special parameters: owner, group, and mode. These parameters affect the required permissions of any files specified after their specification. Puppet will sometimes use these parameters to check its own configured state, so they can be used to make Puppet a bit more self-managing. Note also that the section names are entirely for human-level organizational purposes; they don't provide separate namespaces. All parameters are in a single namespace. Generated on #{Time.now}. }.gsub(/^/, "# ") # Add a section heading that matches our name. if @config.include?(:name) str += "[%s]\n" % self[:name] end eachsection do |section| persection(section) do |obj| str += obj.to_config + "\n" end end return str end # Convert our configuration into a list of transportable objects. def to_transportable(*sections) done = Hash.new { |hash, key| hash[key] = {} } topbucket = Puppet::TransBucket.new if defined? @file.file and @file.file topbucket.name = @file.file else topbucket.name = "top" end topbucket.type = "Settings" topbucket.top = true # Now iterate over each section if sections.empty? eachsection do |section| sections << section end end sections.each do |section| obj = section_to_transportable(section, done) topbucket.push obj end topbucket end # Convert to a parseable manifest def to_manifest transport = self.to_transportable manifest = transport.to_manifest + "\n" eachsection { |section| manifest += "include #{section}\n" } return manifest end # Create the necessary objects to use a section. This is idempotent; # you can 'use' a section as many times as you want. def use(*sections) @@sync.synchronize do # yay, thread-safe sections = sections.reject { |s| @used.include?(s.to_sym) } return if sections.empty? bucket = to_transportable(*sections) begin catalog = bucket.to_catalog + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not create resources for managing Puppet's files and directories: %s" % detail + + # We need some way to get rid of any resources created during the catalog creation + # but not cleaned up. + return + end + + begin catalog.host_config = false catalog.apply do |transaction| if failures = transaction.any_failed? raise "Could not configure for running; got %s failure(s)" % failures end end ensure - # The catalog won't exist if there was an error creating it. - catalog.clear if catalog + catalog.clear end sections.each { |s| @used << s } @used.uniq! end end def valid?(param) param = symbolize(param) @config.has_key?(param) end # Find the correct value using our search path. Optionally accept an environment # in which to search before the other configuration sections. def value(param, environment = nil) param = symbolize(param) environment = symbolize(environment) if environment # Short circuit to nil for undefined parameters. return nil unless @config.include?(param) # Yay, recursion. self.reparse() unless param == :filetimeout # Check the cache first. It needs to be a per-environment # cache so that we don't spread values from one env # to another. if cached = @cache[environment||"none"][param] return cached end # See if we can find it within our searchable list of values val = nil each_source(environment) do |source| # Look for the value. We have to test the hash for whether # it exists, because the value might be false. if @values[source].include?(param) val = @values[source][param] break end end # If we didn't get a value, use the default val = @config[param].default if val.nil? # Convert it if necessary val = convert(val) # And cache it @cache[environment||"none"][param] = val return val end # Open a file with the appropriate user, group, and mode def write(default, *args, &bloc) obj = get_config_file_default(default) writesub(default, value(obj.name), *args, &bloc) end # Open a non-default file under a default dir with the appropriate user, # group, and mode def writesub(default, file, *args, &bloc) obj = get_config_file_default(default) chown = nil if Puppet::Util::SUIDManager.uid == 0 chown = [obj.owner, obj.group] else chown = [nil, nil] end Puppet::Util::SUIDManager.asuser(*chown) do mode = obj.mode || 0640 if args.empty? args << "w" end args << mode # Update the umask to make non-executable files Puppet::Util.withumask(File.umask ^ 0111) do File.open(file, *args) do |file| yield file end end end end def readwritelock(default, *args, &bloc) file = value(get_config_file_default(default).name) tmpfile = file + ".tmp" sync = Sync.new unless FileTest.directory?(File.dirname(tmpfile)) raise Puppet::DevError, "Cannot create %s; directory %s does not exist" % [file, File.dirname(file)] end sync.synchronize(Sync::EX) do File.open(file, "r+", 0600) do |rf| rf.lock_exclusive do if File.exist?(tmpfile) raise Puppet::Error, ".tmp file already exists for %s; Aborting locked write. Check the .tmp file and delete if appropriate" % [file] end writesub(default, tmpfile, *args, &bloc) begin File.rename(tmpfile, file) rescue => detail Puppet.err "Could not rename %s to %s: %s" % [file, tmpfile, detail] end end end end end private def get_config_file_default(default) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default %s" % default end unless obj.is_a? CFile raise ArgumentError, "Default %s is not a file" % default end return obj end # Create the transportable objects for users and groups. def add_user_resources(section, obj, done) resources = [] [:owner, :group].each do |attr| type = nil if attr == :owner type = :user else type = attr end # If a user and/or group is set, then make sure we're # managing that object if obj.respond_to? attr and name = obj.send(attr) # Skip root or wheel next if %w{root wheel}.include?(name.to_s) # Skip owners and groups we've already done, but tag # them with our section if necessary if done[type].include?(name) tags = done[type][name].tags unless tags.include?(section) done[type][name].tags = tags << section end else newobj = Puppet::TransObject.new(name, type.to_s) newobj.tags = ["puppet", "configuration", section] newobj[:ensure] = :present if type == :user newobj[:comment] ||= "%s user" % name end # Set the group appropriately for the user if type == :user newobj[:gid] = Puppet[:group] end done[type][name] = newobj resources << newobj end end end resources end # Yield each search source in turn. def each_source(environment) searchpath(environment).each do |source| # Modify the source as necessary. source = self.name if source == :name yield source end end # Return all elements that have associated hooks; this is so # we can call them after parsing the configuration file. def settings_with_hooks @config.values.find_all { |setting| setting.respond_to?(:handle) } end # Extract extra setting information for files. def extract_fileinfo(string) result = {} value = string.sub(/\{\s*([^}]+)\s*\}/) do params = $1 params.split(/\s*,\s*/).each do |str| if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/ param, value = $1.intern, $2 result[param] = value unless [:owner, :mode, :group].include?(param) raise ArgumentError, "Invalid file option '%s'" % param end if param == :mode and value !~ /^\d+$/ raise ArgumentError, "File modes must be numbers" end else raise ArgumentError, "Could not parse '%s'" % string end end '' end result[:value] = value.sub(/\s*$/, '') return result return nil end # Convert arguments into booleans, integers, or whatever. def munge_value(value) # Handle different data types correctly return case value when /^false$/i: false when /^true$/i: true when /^\d+$/i: Integer(value) else value.gsub(/^["']|["']$/,'').sub(/\s+$/, '') end end # This is an abstract method that just turns a file in to a hash of hashes. # We mostly need this for backward compatibility -- as of May 2007 we need to # support parsing old files with any section, or new files with just two # valid sections. def parse_file(file) text = read_file(file) # Create a timer so that this file will get checked automatically # and reparsed if necessary. settimer() result = Hash.new { |names, name| names[name] = {} } count = 0 # Default to 'main' for the section. section = :main result[section][:_meta] = {} text.split(/\n/).each { |line| count += 1 case line when /^\s*\[(\w+)\]$/: section = $1.intern # Section names # Add a meta section result[section][:_meta] ||= {} when /^\s*#/: next # Skip comments when /^\s*$/: next # Skip blanks when /^\s*(\w+)\s*=\s*(.*)$/: # settings var = $1.intern # We don't want to munge modes, because they're specified in octal, so we'll # just leave them as a String, since Puppet handles that case correctly. if var == :mode value = $2 else value = munge_value($2) end # Check to see if this is a file argument and it has extra options begin if value.is_a?(String) and options = extract_fileinfo(value) value = options[:value] options.delete(:value) result[section][:_meta][var] = options end result[section][var] = value rescue Puppet::Error => detail detail.file = file detail.line = line raise end else error = Puppet::Error.new("Could not match line %s" % line) error.file = file error.line = line raise error end } return result end # Read the file in. def read_file(file) if file.is_a? Puppet::Util::LoadedFile @file = file else @file = Puppet::Util::LoadedFile.new(file) end begin return File.read(@file.file) rescue Errno::ENOENT raise ArgumentError, "No such file %s" % file rescue Errno::EACCES raise ArgumentError, "Permission denied to file %s" % file end end # Set file metadata. def set_metadata(meta) meta.each do |var, values| values.each do |param, value| @config[var].send(param.to_s + "=", value) end end end # The base element type. class CElement attr_accessor :name, :section, :default, :parent, :setbycli, :call_on_define attr_reader :desc, :short # Unset any set value. def clear @value = nil end def desc=(value) @desc = value.gsub(/^\s*/, '') end # get the arguments in getopt format def getopt_args if short [["--#{name}", "-#{short}", GetoptLong::REQUIRED_ARGUMENT]] else [["--#{name}", GetoptLong::REQUIRED_ARGUMENT]] end end def hook=(block) meta_def :handle, &block end # Create the new element. Pretty much just sets the name. def initialize(args = {}) if args.include?(:parent) self.parent = args[:parent] args.delete(:parent) end args.each do |param, value| method = param.to_s + "=" unless self.respond_to? method raise ArgumentError, "%s does not accept %s" % [self.class, param] end self.send(method, value) end unless self.desc raise ArgumentError, "You must provide a description for the %s config option" % self.name end end def iscreated @iscreated = true end def iscreated? if defined? @iscreated return @iscreated else return false end end def set? if defined? @value and ! @value.nil? return true else return false end end # short name for the celement def short=(value) if value.to_s.length != 1 raise ArgumentError, "Short names can only be one character." end @short = value.to_s end # Convert the object to a config statement. def to_config str = @desc.gsub(/^/, "# ") + "\n" # Add in a statement about the default. if defined? @default and @default str += "# The default value is '%s'.\n" % @default end # If the value has not been overridden, then print it out commented # and unconverted, so it's clear that that's the default and how it # works. value = @parent.value(self.name) if value != @default line = "%s = %s" % [@name, value] else line = "# %s = %s" % [@name, @default] end str += line + "\n" str.gsub(/^/, " ") end # Retrieves the value, or if it's not set, retrieves the default. def value @parent.value(self.name) end end # A file. class CFile < CElement attr_writer :owner, :group attr_accessor :mode, :create def group if defined? @group return @parent.convert(@group) else return nil end end def owner if defined? @owner return @parent.convert(@owner) else return nil end end # Set the type appropriately. Yep, a hack. This supports either naming # the variable 'dir', or adding a slash at the end. def munge(value) # If it's not a fully qualified path... if value.is_a?(String) and value !~ /^\$/ and value !~ /^\// and value != 'false' # Make it one value = File.join(Dir.getwd, value) end if value.to_s =~ /\/$/ @type = :directory return value.sub(/\/$/, '') end return value end # Return the appropriate type. def type value = @parent.value(self.name) if @name.to_s =~ /dir/ return :directory elsif value.to_s =~ /\/$/ return :directory elsif value.is_a? String return :file else return nil end end # Convert the object to a TransObject instance. def to_transportable type = self.type return nil unless type path = self.value return nil unless path.is_a?(String) return nil if path =~ /^\/dev/ return nil if Puppet::Type.type(:file)[path] # skip files that are in our global resource list. objects = [] # Skip plain files that don't exist, since we won't be managing them anyway. return nil unless self.name.to_s =~ /dir$/ or File.exist?(path) or self.create obj = Puppet::TransObject.new(path, "file") # Only create directories, or files that are specifically marked to # create. if type == :directory or self.create obj[:ensure] = type end [:mode].each { |var| if value = self.send(var) # Don't bother converting the mode, since the file type # can handle it any old way. obj[var] = value end } # Only chown or chgrp when root if Puppet.features.root? [:group, :owner].each { |var| if value = self.send(var) obj[var] = value end } end # And set the loglevel to debug for everything obj[:loglevel] = "debug" # We're not actually modifying any files here, and if we allow a # filebucket to get used here we get into an infinite recursion # trying to set the filebucket up. obj[:backup] = false if self.section obj.tags += ["puppet", "configuration", self.section, self.name] end objects << obj objects end # Make sure any provided variables look up to something. def validate(value) return true unless value.is_a? String value.scan(/\$(\w+)/) { |name| name = $1 unless @parent.include?(name) raise ArgumentError, "Settings parameter '%s' is undefined" % name end } end end # A simple boolean. class CBoolean < CElement # get the arguments in getopt format def getopt_args if short [["--#{name}", "-#{short}", GetoptLong::NO_ARGUMENT], ["--no-#{name}", GetoptLong::NO_ARGUMENT]] else [["--#{name}", GetoptLong::NO_ARGUMENT], ["--no-#{name}", GetoptLong::NO_ARGUMENT]] end end def munge(value) case value when true, "true": return true when false, "false": return false else raise ArgumentError, "Invalid value '%s' for %s" % [value.inspect, @name] end end end end diff --git a/spec/unit/other/transbucket.rb b/spec/unit/other/transbucket.rb index 3904e39fe..4494f2abb 100755 --- a/spec/unit/other/transbucket.rb +++ b/spec/unit/other/transbucket.rb @@ -1,165 +1,172 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::TransBucket do before do @bucket = Puppet::TransBucket.new end it "should be able to produce a RAL component" do @bucket.name = "luke" @bucket.type = "user" resource = nil proc { resource = @bucket.to_type }.should_not raise_error resource.should be_instance_of(Puppet::Type::Component) resource.title.should == "User[luke]" end it "should accept TransObjects into its children list" do object = Puppet::TransObject.new("luke", "user") proc { @bucket.push(object) }.should_not raise_error @bucket.each do |o| o.should equal(object) end end it "should accept TransBuckets into its children list" do object = Puppet::TransBucket.new() proc { @bucket.push(object) }.should_not raise_error @bucket.each do |o| o.should equal(object) end end it "should refuse to accept any children that are not TransObjects or TransBuckets" do proc { @bucket.push "a test" }.should raise_error end it "should return use 'node' as the type and the provided name as the title if only a type is provided" do @bucket.type = "mystuff" @bucket.to_ref.should == "Node[mystuff]" end it "should return use 'component' as the type and the provided type as the title if only a name is provided" do @bucket.name = "mystuff" @bucket.to_ref.should == "Class[mystuff]" end it "should return nil as its reference when type and name are missing" do @bucket.to_ref.should be_nil end it "should return the title as its reference" do @bucket.name = "luke" @bucket.type = "user" @bucket.to_ref.should == "User[luke]" end it "should canonize resource references when the type is 'component'" do @bucket.name = 'something' @bucket.type = 'foo::bar' @bucket.to_ref.should == "Foo::Bar[something]" end end describe Puppet::TransBucket, " when generating a catalog" do before do @bottom = Puppet::TransBucket.new @bottom.type = "fake" @bottom.name = "bottom" @bottomobj = Puppet::TransObject.new("bottom", "user") @bottom.push @bottomobj @middle = Puppet::TransBucket.new @middle.type = "fake" @middle.name = "middle" @middleobj = Puppet::TransObject.new("middle", "user") @middle.push(@middleobj) @middle.push(@bottom) @top = Puppet::TransBucket.new @top.type = "fake" @top.name = "top" @topobj = Puppet::TransObject.new("top", "user") @top.push(@topobj) @top.push(@middle) - @config = @top.to_catalog - @users = %w{top middle bottom} @fakes = %w{Fake[bottom] Fake[middle] Fake[top]} end + after do + Puppet::Type.allclear + end + it "should convert all transportable objects to RAL resources" do + @catalog = @top.to_catalog @users.each do |name| - @config.vertices.find { |r| r.class.name == :user and r.title == name }.should be_instance_of(Puppet::Type.type(:user)) + @catalog.vertices.find { |r| r.class.name == :user and r.title == name }.should be_instance_of(Puppet::Type.type(:user)) end end + it "should fail if any transportable resources fail to convert to RAL resources" do + @bottomobj.expects(:to_type).raises ArgumentError + lambda { @bottom.to_catalog }.should raise_error(ArgumentError) + end + it "should convert all transportable buckets to RAL components" do + @catalog = @top.to_catalog @fakes.each do |name| - @config.vertices.find { |r| r.class.name == :component and r.title == name }.should be_instance_of(Puppet::Type.type(:component)) + @catalog.vertices.find { |r| r.class.name == :component and r.title == name }.should be_instance_of(Puppet::Type.type(:component)) end end it "should add all resources to the graph's resource table" do - @config.resource("fake[top]").should equal(@top) + @catalog = @top.to_catalog + @catalog.resource("fake[top]").should equal(@top) end it "should finalize all resources" do - @config.vertices.each do |vertex| vertex.should be_finalized end + @catalog = @top.to_catalog + @catalog.vertices.each do |vertex| vertex.should be_finalized end end it "should only call to_type on each resource once" do - @topobj.expects(:to_type) - @bottomobj.expects(:to_type) - Puppet::Type.allclear + # We just raise exceptions here because we're not interested in + # what happens with the result, only that the method only + # gets called once. + resource = @topobj.to_type + @topobj.expects(:to_type).once.returns resource @top.to_catalog end it "should set each TransObject's catalog before converting to a RAL resource" do @middleobj.expects(:catalog=).with { |c| c.is_a?(Puppet::Node::Catalog) } - Puppet::Type.allclear @top.to_catalog end it "should set each TransBucket's catalog before converting to a RAL resource" do # each bucket is seen twice in the loop, so we have to handle the case where the config # is set twice @bottom.expects(:catalog=).with { |c| c.is_a?(Puppet::Node::Catalog) }.at_least_once - Puppet::Type.allclear @top.to_catalog end - - after do - Puppet::Type.allclear - end end describe Puppet::TransBucket, " when serializing" do before do @bucket = Puppet::TransBucket.new(%w{one two}) @bucket.name = "one" @bucket.type = "two" end it "should be able to be dumped to yaml" do proc { YAML.dump(@bucket) }.should_not raise_error end it "should dump YAML that produces an equivalent object" do result = YAML.dump(@bucket) newobj = YAML.load(result) newobj.name.should == "one" newobj.type.should == "two" children = [] newobj.each do |o| children << o end children.should == %w{one two} end end diff --git a/spec/unit/util/settings.rb b/spec/unit/util/settings.rb index fbd638663..a6b358462 100755 --- a/spec/unit/util/settings.rb +++ b/spec/unit/util/settings.rb @@ -1,695 +1,706 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Util::Settings, " when specifying defaults" do before do @settings = Puppet::Util::Settings.new end it "should start with no defined parameters" do @settings.params.length.should == 0 end it "should allow specification of default values associated with a section as an array" do @settings.setdefaults(:section, :myvalue => ["defaultval", "my description"]) end it "should not allow duplicate parameter specifications" do @settings.setdefaults(:section, :myvalue => ["a", "b"]) lambda { @settings.setdefaults(:section, :myvalue => ["c", "d"]) }.should raise_error(ArgumentError) end it "should allow specification of default values associated with a section as a hash" do @settings.setdefaults(:section, :myvalue => {:default => "defaultval", :desc => "my description"}) end it "should consider defined parameters to be valid" do @settings.setdefaults(:section, :myvalue => ["defaultval", "my description"]) @settings.valid?(:myvalue).should be_true end it "should require a description when defaults are specified with an array" do lambda { @settings.setdefaults(:section, :myvalue => ["a value"]) }.should raise_error(ArgumentError) end it "should require a description when defaults are specified with a hash" do lambda { @settings.setdefaults(:section, :myvalue => {:default => "a value"}) }.should raise_error(ArgumentError) end it "should support specifying owner, group, and mode when specifying files" do @settings.setdefaults(:section, :myvalue => {:default => "/some/file", :owner => "blah", :mode => "boo", :group => "yay", :desc => "whatever"}) end it "should support specifying a short name" do @settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) end it "should fail when short names conflict" do @settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) lambda { @settings.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) }.should raise_error(ArgumentError) end end describe Puppet::Util::Settings, " when setting values" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :main, :myval => ["val", "desc"] @settings.setdefaults :main, :bool => [true, "desc"] end it "should provide a method for setting values from other objects" do @settings[:myval] = "something else" @settings[:myval].should == "something else" end it "should support a getopt-specific mechanism for setting values" do @settings.handlearg("--myval", "newval") @settings[:myval].should == "newval" end it "should support a getopt-specific mechanism for turning booleans off" do @settings.handlearg("--no-bool") @settings[:bool].should == false end it "should support a getopt-specific mechanism for turning booleans on" do # Turn it off first @settings[:bool] = false @settings.handlearg("--bool") @settings[:bool].should == true end it "should clear the cache when setting getopt-specific values" do @settings.setdefaults :mysection, :one => ["whah", "yay"], :two => ["$one yay", "bah"] @settings[:two].should == "whah yay" @settings.handlearg("--one", "else") @settings[:two].should == "else yay" end it "should not clear other values when setting getopt-specific values" do @settings[:myval] = "yay" @settings.handlearg("--no-bool") @settings[:myval].should == "yay" end it "should call passed blocks when values are set" do values = [] @settings.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }}) values.should == [] @settings[:hooker] = "something" values.should == %w{something} end it "should provide an option to call passed blocks during definition" do values = [] @settings.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :call_on_define => true, :hook => lambda { |v| values << v }}) values.should == %w{yay} end it "should pass the fully interpolated value to the hook when called on definition" do values = [] @settings.setdefaults(:section, :one => ["test", "a"]) @settings.setdefaults(:section, :hooker => {:default => "$one/yay", :desc => "boo", :call_on_define => true, :hook => lambda { |v| values << v }}) values.should == %w{test/yay} end it "should munge values using the element-specific methods" do @settings[:bool] = "false" @settings[:bool].should == false end it "should prefer cli values to values set in Ruby code" do @settings.handlearg("--myval", "cliarg") @settings[:myval] = "memarg" @settings[:myval].should == "cliarg" end end describe Puppet::Util::Settings, " when returning values" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :section, :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"], :four => ["$two $three FOUR", "d"] end it "should provide a mechanism for returning set values" do @settings[:one] = "other" @settings[:one].should == "other" end it "should interpolate default values for other parameters into returned parameter values" do @settings[:one].should == "ONE" @settings[:two].should == "ONE TWO" @settings[:three].should == "ONE ONE TWO THREE" end it "should interpolate default values that themselves need to be interpolated" do @settings[:four].should == "ONE TWO ONE ONE TWO THREE FOUR" end it "should interpolate set values for other parameters into returned parameter values" do @settings[:one] = "on3" @settings[:two] = "$one tw0" @settings[:three] = "$one $two thr33" @settings[:four] = "$one $two $three f0ur" @settings[:one].should == "on3" @settings[:two].should == "on3 tw0" @settings[:three].should == "on3 on3 tw0 thr33" @settings[:four].should == "on3 on3 tw0 on3 on3 tw0 thr33 f0ur" end it "should not cache interpolated values such that stale information is returned" do @settings[:two].should == "ONE TWO" @settings[:one] = "one" @settings[:two].should == "one TWO" end it "should not cache values such that information from one environment is returned for another environment" do text = "[env1]\none = oneval\n[env2]\none = twoval\n" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/whatever") @settings.stubs(:read_file).with(file).returns(text) @settings.parse(file) @settings.value(:one, "env1").should == "oneval" @settings.value(:one, "env2").should == "twoval" end it "should have a name determined by the 'name' parameter" do @settings.setdefaults(:whatever, :name => ["something", "yayness"]) @settings.name.should == :something @settings[:name] = :other @settings.name.should == :other end end describe Puppet::Util::Settings, " when choosing which value to return" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :section, :one => ["ONE", "a"], :name => ["myname", "w"] end it "should return default values if no values have been set" do @settings[:one].should == "ONE" end it "should return values set on the cli before values set in the configuration file" do text = "[main]\none = fileval\n" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/whatever") @settings.stubs(:parse_file).returns(text) @settings.handlearg("--one", "clival") @settings.parse(file) @settings[:one].should == "clival" end it "should return values set on the cli before values set in Ruby" do @settings[:one] = "rubyval" @settings.handlearg("--one", "clival") @settings[:one].should == "clival" end it "should return values set in the executable-specific section before values set in the main section" do text = "[main]\none = mainval\n[myname]\none = nameval\n" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/whatever") @settings.stubs(:read_file).with(file).returns(text) @settings.parse(file) @settings[:one].should == "nameval" end it "should not return values outside of its search path" do text = "[other]\none = oval\n" file = "/some/file" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/whatever") @settings.stubs(:read_file).with(file).returns(text) @settings.parse(file) @settings[:one].should == "ONE" end it "should return values in a specified environment" do text = "[env]\none = envval\n" file = "/some/file" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/whatever") @settings.stubs(:read_file).with(file).returns(text) @settings.parse(file) @settings.value(:one, "env").should == "envval" end it "should return values in a specified environment before values in the main or name sections" do text = "[env]\none = envval\n[main]\none = mainval\n[myname]\none = nameval\n" file = "/some/file" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/whatever") @settings.stubs(:read_file).with(file).returns(text) @settings.parse(file) @settings.value(:one, "env").should == "envval" end end describe Puppet::Util::Settings, " when parsing its configuration" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :section, :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"] end it "should return values set in the configuration file" do text = "[main] one = fileval " file = "/some/file" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) @settings[:one].should == "fileval" end #484 - this should probably be in the regression area it "should not throw an exception on unknown parameters" do text = "[main]\nnosuchparam = mval\n" file = "/some/file" @settings.expects(:read_file).with(file).returns(text) lambda { @settings.parse(file) }.should_not raise_error end it "should support an old parse method when per-executable configuration files still exist" do # I'm not going to bother testing this method. @settings.should respond_to(:old_parse) end it "should convert booleans in the configuration file into Ruby booleans" do text = "[main] one = true two = false " file = "/some/file" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) @settings[:one].should == true @settings[:two].should == false end it "should convert integers in the configuration file into Ruby Integers" do text = "[main] one = 65 " file = "/some/file" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) @settings[:one].should == 65 end it "should support specifying all metadata (owner, group, mode) in the configuration file" do @settings.setdefaults :section, :myfile => ["/myfile", "a"] text = "[main] myfile = /other/file {owner = luke, group = luke, mode = 644} " file = "/some/file" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) @settings[:myfile].should == "/other/file" @settings.metadata(:myfile).should == {:owner => "luke", :group => "luke", :mode => "644"} end it "should support specifying a single piece of metadata (owner, group, or mode) in the configuration file" do @settings.setdefaults :section, :myfile => ["/myfile", "a"] text = "[main] myfile = /other/file {owner = luke} " file = "/some/file" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) @settings[:myfile].should == "/other/file" @settings.metadata(:myfile).should == {:owner => "luke"} end it "should call hooks associated with values set in the configuration file" do values = [] @settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} text = "[main] mysetting = setval " file = "/some/file" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) values.should == ["setval"] end it "should not call the same hook for values set multiple times in the configuration file" do values = [] @settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} text = "[main] mysetting = setval [puppet] mysetting = other " file = "/some/file" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) values.should == ["setval"] end it "should pass the environment-specific value to the hook when one is available" do values = [] @settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} @settings.setdefaults :section, :environment => ["yay", "a"] @settings.setdefaults :section, :environments => ["yay,foo", "a"] text = "[main] mysetting = setval [yay] mysetting = other " file = "/some/file" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) values.should == ["other"] end it "should pass the interpolated value to the hook when one is available" do values = [] @settings.setdefaults :section, :base => {:default => "yay", :desc => "a", :hook => proc { |v| values << v }} @settings.setdefaults :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} text = "[main] mysetting = $base/setval " file = "/some/file" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) values.should == ["yay/setval"] end it "should allow empty values" do @settings.setdefaults :section, :myarg => ["myfile", "a"] text = "[main] myarg = " @settings.stubs(:read_file).returns(text) @settings.parse("/some/file") @settings[:myarg].should == "" end end describe Puppet::Util::Settings, " when reparsing its configuration" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :section, :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"] end it "should replace in-memory values with on-file values" do # Init the value text = "[main]\none = disk-init\n" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/test/file") @settings[:one] = "init" @settings.file = file # Now replace the value text = "[main]\none = disk-replace\n" # This is kinda ridiculous - the reason it parses twice is that # it goes to parse again when we ask for the value, because the # mock always says it should get reparsed. @settings.expects(:read_file).with(file).returns(text).times(2) @settings.reparse @settings[:one].should == "disk-replace" end it "should retain parameters set by cli when configuration files are reparsed" do @settings.handlearg("--one", "clival") text = "[main]\none = on-disk\n" file = mock 'file' file.stubs(:file).returns("/test/file") @settings.stubs(:read_file).with(file).returns(text) @settings.parse(file) @settings[:one].should == "clival" end it "should remove in-memory values that are no longer set in the file" do # Init the value text = "[main]\none = disk-init\n" file = mock 'file' file.stubs(:changed?).returns(true) file.stubs(:file).returns("/test/file") @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) @settings[:one].should == "disk-init" # Now replace the value text = "[main]\ntwo = disk-replace\n" @settings.expects(:read_file).with(file).returns(text) @settings.parse(file) #@settings.reparse # The originally-overridden value should be replaced with the default @settings[:one].should == "ONE" # and we should now have the new value in memory @settings[:two].should == "disk-replace" end end describe Puppet::Util::Settings, " when being used to manage the host machine" do before do @settings = Puppet::Util::Settings.new @settings.setdefaults :main, :maindir => ["/maindir", "a"], :seconddir => ["/seconddir", "a"] @settings.setdefaults :other, :otherdir => {:default => "/otherdir", :desc => "a", :owner => "luke", :group => "johnny", :mode => 0755} @settings.setdefaults :third, :thirddir => ["/thirddir", "b"] @settings.setdefaults :files, :myfile => {:default => "/myfile", :desc => "a", :mode => 0755} end def stub_transaction @bucket = mock 'bucket' @config = mock 'config', :clear => nil @trans = mock 'transaction' @settings.expects(:to_transportable).with(:whatever).returns(@bucket) @bucket.expects(:to_catalog).returns(@config) @config.expects(:apply).yields(@trans) @config.stubs(:host_config=) end it "should provide a method that writes files with the correct modes" do pending "Not converted from test/unit yet" end it "should provide a method that creates directories with the correct modes" do Puppet::Util::SUIDManager.expects(:asuser).with("luke", "johnny").yields Dir.expects(:mkdir).with("/otherdir", 0755) @settings.mkdir(:otherdir) end it "should be able to create needed directories in a single section" do Dir.expects(:mkdir).with("/maindir") Dir.expects(:mkdir).with("/seconddir") @settings.use(:main) end it "should be able to create needed directories in multiple sections" do Dir.expects(:mkdir).with("/maindir") Dir.expects(:mkdir).with("/seconddir") Dir.expects(:mkdir).with("/thirddir") @settings.use(:main, :third) end it "should provide a method to trigger enforcing of file modes on existing files and directories" do pending "Not converted from test/unit yet" end it "should provide a method to convert the file mode enforcement into a Puppet manifest" do pending "Not converted from test/unit yet" end it "should create files when configured to do so with the :create parameter" it "should provide a method to convert the file mode enforcement into transportable resources" do # Make it think we're root so it tries to manage user and group. Puppet.features.stubs(:root?).returns(true) File.stubs(:exist?).with("/myfile").returns(true) trans = nil trans = @settings.to_transportable resources = [] trans.delve { |obj| resources << obj if obj.is_a? Puppet::TransObject } %w{/maindir /seconddir /otherdir /myfile}.each do |path| obj = resources.find { |r| r.type == "file" and r.name == path } if path.include?("dir") obj[:ensure].should == :directory else # Do not create the file, just manage mode obj[:ensure].should be_nil end obj.should be_instance_of(Puppet::TransObject) case path when "/otherdir": obj[:owner].should == "luke" obj[:group].should == "johnny" obj[:mode].should == 0755 when "/myfile": obj[:mode].should == 0755 end end end it "should not try to manage user or group when not running as root" do Puppet.features.stubs(:root?).returns(false) trans = nil trans = @settings.to_transportable(:other) trans.delve do |obj| next unless obj.is_a?(Puppet::TransObject) obj[:owner].should be_nil obj[:group].should be_nil end end it "should add needed users and groups to the manifest when asked" do # This is how we enable user/group management @settings.setdefaults :main, :mkusers => [true, "w"] Puppet.features.stubs(:root?).returns(false) trans = nil trans = @settings.to_transportable(:other) resources = [] trans.delve { |obj| resources << obj if obj.is_a? Puppet::TransObject and obj.type != "file" } user = resources.find { |r| r.type == "user" } user.should be_instance_of(Puppet::TransObject) user.name.should == "luke" user[:ensure].should == :present # This should maybe be a separate test, but... group = resources.find { |r| r.type == "group" } group.should be_instance_of(Puppet::TransObject) group.name.should == "johnny" group[:ensure].should == :present end it "should ignore tags and schedules when creating files and directories" it "should apply all resources in debug mode to reduce logging" it "should not try to manage absent files" do # Make it think we're root so it tries to manage user and group. Puppet.features.stubs(:root?).returns(true) trans = nil trans = @settings.to_transportable file = nil trans.delve { |obj| file = obj if obj.name == "/myfile" } file.should be_nil end it "should not try to manage files in memory" do main = Puppet::Type.type(:file).create(:path => "/maindir") trans = @settings.to_transportable lambda { trans.to_catalog }.should_not raise_error end + it "should do nothing if a catalog cannot be created" do + bucket = mock 'bucket' + catalog = mock 'catalog' + + @settings.expects(:to_transportable).returns bucket + bucket.expects(:to_catalog).raises RuntimeError + catalog.expects(:apply).never + + @settings.use(:mysection) + end + it "should clear the catalog after applying" do bucket = mock 'bucket' catalog = mock 'catalog' @settings.expects(:to_transportable).returns bucket bucket.expects(:to_catalog).returns catalog catalog.stubs(:host_config=) catalog.stubs(:apply) catalog.expects(:clear) @settings.use(:mysection) end it "should clear the catalog even if there is an exception during applying" do bucket = mock 'bucket' catalog = mock 'catalog' @settings.expects(:to_transportable).returns bucket bucket.expects(:to_catalog).returns catalog catalog.stubs(:host_config=) catalog.expects(:apply).raises(ArgumentError) catalog.expects(:clear) # We don't care about the raised exception, we just care that # we clear the catalog even with the exception lambda { @settings.use(:mysection) }.should raise_error end it "should do nothing if all specified sections have already been used" do bucket = mock 'bucket' catalog = mock 'catalog' @settings.expects(:to_transportable).once.returns(bucket) bucket.expects(:to_catalog).returns catalog catalog.stub_everything @settings.use(:whatever) @settings.use(:whatever) end it "should ignore file settings whose values are not strings" do @settings[:maindir] = false lambda { trans = @settings.to_transportable }.should_not raise_error end it "should be able to turn the current configuration into a parseable manifest" it "should convert octal numbers correctly when producing a manifest" it "should be able to provide all of its parameters in a format compatible with GetOpt::Long" do pending "Not converted from test/unit yet" end it "should not attempt to manage files within /dev" do pending "Not converted from test/unit yet" end it "should not modify the stored state database when managing resources" do Puppet::Util::Storage.expects(:store).never Puppet::Util::Storage.expects(:load).never Dir.expects(:mkdir).with("/maindir") Dir.expects(:mkdir).with("/seconddir") @settings.use(:main) end it "should convert all relative paths to fully-qualified paths (#795)" do @settings[:myfile] = "unqualified" dir = Dir.getwd @settings[:myfile].should == File.join(dir, "unqualified") end it "should support a method for re-using all currently used sections" do Dir.expects(:mkdir).with("/thirddir").times(2) @settings.use(:third) @settings.reuse end it "should fail if any resources fail" do stub_transaction @trans.expects(:any_failed?).returns(true) proc { @settings.use(:whatever) }.should raise_error(RuntimeError) end after { Puppet::Type.allclear } end