diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 0398115de..291f122a7 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,95 +1,95 @@ require 'puppet' require 'timeout' require 'puppet/rails' require 'puppet/util/methodhelper' require 'puppet/parser/parser' require 'puppet/parser/configuration' require 'puppet/parser/scope' # The interpreter is a very simple entry-point class that # manages the existence of the parser (e.g., replacing it # when files are reparsed). You can feed it a node and # get the node's configuration back. class Puppet::Parser::Interpreter include Puppet::Util attr_accessor :usenodes attr_accessor :code, :file include Puppet::Util::Errors # Determine the configuration version for a given node's environment. def configuration_version(node) parser(node.environment).version end # evaluate our whole tree def compile(node) return Puppet::Parser::Configuration.new(node, parser(node.environment), :ast_nodes => usenodes?).compile end # create our interpreter def initialize(options = {}) if @code = options[:Code] elsif @file = options[:Manifest] end if options.include?(:UseNodes) @usenodes = options[:UseNodes] else @usenodes = true end # The class won't always be defined during testing. if Puppet[:storeconfigs] if Puppet.features.rails? Puppet::Rails.init else raise Puppet::Error, "Rails is missing; cannot store configurations" end end @parsers = {} end # Should we parse ast nodes? def usenodes? defined?(@usenodes) and @usenodes end private # Create a new parser object and pre-parse the configuration. def create_parser(environment) begin parser = Puppet::Parser::Parser.new(environment) if self.code - parser.code = self.code + parser.string = self.code elsif self.file parser.file = self.file end parser.parse return parser rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not parse for environment %s: %s" % [environment, detail] return nil end end # Return the parser for a specific environment. def parser(environment) if ! @parsers[environment] or @parsers[environment].reparse? if tmp = create_parser(environment) @parsers[environment].clear if @parsers[environment] @parsers[environment] = tmp end unless @parsers[environment] raise Puppet::Error, "Could not parse any configurations" end end @parsers[environment] end end diff --git a/lib/puppet/util/config.rb b/lib/puppet/util/config.rb index 932314215..9ec777e99 100644 --- a/lib/puppet/util/config.rb +++ b/lib/puppet/util/config.rb @@ -1,1153 +1,1196 @@ require 'puppet' require 'sync' require 'puppet/transportable' require 'getoptlong' # The class for handling configuration files. class Puppet::Util::Config include Enumerable include Puppet::Util @@sync = Sync.new - attr_reader :file, :timer + attr_accessor :file + attr_reader :timer # Retrieve a config value def [](param) param = symbolize(param) # Yay, recursion. self.reparse() unless param == :filetimeout # Cache the returned values; this method was taking close to # 10% of the compile time. unless @returned[param] if @config.include?(param) if @config[param] @returned[param] = @config[param].value end else raise ArgumentError, "Undefined configuration parameter '%s'" % param end end return @returned[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 @config.include?(param) - raise Puppet::Error, + raise ArgumentError, "Attempt to assign a value to unknown configuration parameter %s" % param.inspect end unless @order.include?(param) @order << param end @config[param].value = value - if @returned.include?(param) - @returned.delete(param) - end + @returned.clear end return value end # A simplified equality operator. def ==(other) self.each { |myname, myobj| unless other[myname] == myobj.value 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 transaction and apply it def apply trans = self.to_transportable begin comp = trans.to_type trans = comp.evaluate trans.evaluate comp.remove 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 } @returned.clear # 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 end # This is mostly just used for testing. def clearused @returned.clear @used = [] end + # Return a value's description. + def description(name) + if obj = @config[symbolize(name)] + obj.desc + else + nil + end + end + def each @order.each { |name| if @config.include?(name) yield name, @config[name] else raise Puppet::DevError, "%s is in the order but does not exist" % name end } end # Iterate over each section name. def eachsection yielded = [] @order.each { |name| if @config.include?(name) section = @config[name].section unless yielded.include? section yield section yielded << section end else raise Puppet::DevError, "%s is in the order but does not exist" % name 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) value = munge_value(value) if value str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end if self.valid?(str) if self.boolean?(str) self[str] = bool else self[str] = value end # Mark that this was set on the cli, so it's not overridden if the # config gets reread. @config[str.intern].setbycli = true 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 @order = [] @config = {} @shortnames = {} @created = [] @returned = {} 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 = 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 Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end # Return all of the parameters associated with a given section. - def params(section) - section = section.intern if section.is_a? String - @config.find_all { |name, obj| - obj.section == section - }.collect { |name, obj| - name - } + 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) configmap = parse_file(file) # We know we want the 'main' section if main = configmap[:main] set_parameter_hash(main) end # Otherwise, we only want our named section if @config.include?(:name) and named = configmap[symbolize(self[:name])] set_parameter_hash(named) 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 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] self[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) @order << element.name return element end # 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 # 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, includefiles = true) done ||= Hash.new { |hash, key| hash[key] = {} } objects = [] persection(section) do |obj| if @config[:mkusers] and @config[:mkusers].value [: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 elsif newobj = Puppet::Type.type(type)[name] unless newobj.property(:ensure) newobj[:ensure] = "present" end newobj.tag(section) if type == :user newobj[:comment] ||= "%s user" % name 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 objects << newobj end end end end if obj.respond_to? :to_transportable next if obj.value =~ /^\/dev/ 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 end bucket = Puppet::TransBucket.new bucket.type = 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) 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 Puppet::Error, "Parameter %s is already defined" % 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 } 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_component transport = self.to_transportable return transport.to_type end # Convert our list of objects 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 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 = "configtop" end topbucket.type = "puppetconfig" topbucket.top = true # Now iterate over each section eachsection do |section| topbucket.push section_to_transportable(section, done) 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 unless defined? @used @used = [] end runners = sections.collect { |s| symbolize(s) }.find_all { |s| ! @used.include? s } return if runners.empty? bucket = Puppet::TransBucket.new bucket.type = "puppetconfig" bucket.top = true # Create a hash to keep track of what we've done so far. @done = Hash.new { |hash, key| hash[key] = {} } runners.each do |section| bucket.push section_to_transportable(section, @done, false) end objects = bucket.to_type objects.finalize tags = nil if Puppet[:tags] tags = Puppet[:tags] Puppet[:tags] = "" end trans = objects.evaluate trans.ignoretags = true trans.configurator = true trans.evaluate if tags Puppet[:tags] = tags end # Remove is a recursive process, so it's sufficient to just call # it on the component. objects.remove(true) objects = nil runners.each { |s| @used << s } end end def valid?(param) param = symbolize(param) @config.has_key?(param) end + def value(param) + param = symbolize(param) + if obj = @config[param] + obj.value + else + nil + end + end + # Open a file with the appropriate user, group, and mode def write(default, *args) 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 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 File.open(obj.value, *args) do |file| yield file end end end # Open a non-default file under a default dir with the appropriate user, # group, and mode def writesub(default, file, *args) 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 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 private - # Extra extra setting information for files. + # Extract extra setting information for files. def extract_fileinfo(string) - paramregex = %r{(\w+)\s*=\s*([\w\d]+)} result = {} - string.scan(/\{\s*([^}]+)\s*\}/) do + value = string.sub(/\{\s*([^}]+)\s*\}/) do params = $1 params.split(/\s*,\s*/).each do |str| - if str =~ /^\s*(\w+)\s*=\s*([\w\w]+)\s*$/ + if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/ param, value = $1.intern, $2 result[param] = value unless [:owner, :mode, :group].include?(param) - raise Puppet::Error, "Invalid file option '%s'" % param + raise ArgumentError, "Invalid file option '%s'" % param end if param == :mode and value !~ /^\d+$/ - raise Puppet::Error, "File modes must be numbers" + raise ArgumentError, "File modes must be numbers" end else - raise Puppet::Error, "Could not parse '%s'" % string + raise ArgumentError, "Could not parse '%s'" % string end end - - return result + '' 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 = nil - - if file.is_a? Puppet::Util::LoadedFile - @file = file - else - @file = Puppet::Util::LoadedFile.new(file) - end + text = read_file(file) # Create a timer so that this file will get checked automatically # and reparsed if necessary. 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 - 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 /^\[(\w+)\]$/: + 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 + # Take all members of a hash and assign their values appropriately. def set_parameter_hash(params) params.each do |param, value| next if param == :_meta unless @config.include?(param) Puppet.warning "Discarded unknown configuration parameter %s" % param next end if @config[param].setbycli Puppet.debug "Ignoring %s set by config file; overridden by cli" % param else self[param] = value end end if meta = params[:_meta] meta.each do |var, values| values.each do |param, value| @config[var].send(param.to_s + "=", value) end end end end # The base element type. class CElement attr_accessor :name, :section, :default, :parent, :setbycli attr_reader :desc, :short # Unset any set value. def clear @value = nil 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 = @parent[varname] pval else raise Puppet::DevError, "Could not find value for %s" % parent end end return newval 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 line = "%s = %s" % [@name, self.value] # 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. if defined? @value and ! @value.nil? line = "%s = %s" % [@name, self.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 retval = nil if defined? @value and ! @value.nil? retval = @value elsif defined? @default retval = @default else return nil end if retval.is_a? String return convert(retval) else return retval end end # Set the value. def value=(value) if respond_to?(:validate) validate(value) end if respond_to?(:munge) @value = munge(value) else @value = value end if respond_to?(:handle) handle(@value) end end end # A file. class CFile < CElement attr_writer :owner, :group attr_accessor :mode, :create def group if defined? @group return convert(@group) else return nil end end def owner if defined? @owner return 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 value.to_s =~ /\/$/ @type = :directory return value.sub(/\/$/, '') end return value end # Return the appropriate type. def type value = self.value 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. # FIXME There's no dependency system in place right now; if you use # a section that requires another section, there's nothing done to # correct that for you, at the moment. def to_transportable type = self.type return nil unless type path = self.value.split(File::SEPARATOR) path.shift # remove the leading nil objects = [] obj = Puppet::TransObject.new(self.value, "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 both 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::Util::SUIDManager.uid == 0 [: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, "Configuration 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 Puppet::Error, "Invalid value '%s' for %s" % + raise ArgumentError, "Invalid value '%s' for %s" % [value.inspect, @name] end end end end # $Id$ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d8f326924..aa56fd93e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,13 +1,13 @@ dir = File.dirname(__FILE__) $:.unshift("#{dir}/lib").unshift("#{dir}/../lib") # Add the old test dir, so that we can still find mocha and spec $:.unshift("#{dir}/../test/lib") require 'mocha' require 'spec' -require 'puppet' +require 'puppettest' Spec::Runner.configure do |config| config.mock_with :mocha end diff --git a/spec/unit/util/config.rb b/spec/unit/util/config.rb new file mode 100755 index 000000000..ef0c00262 --- /dev/null +++ b/spec/unit/util/config.rb @@ -0,0 +1,249 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Puppet::Util::Config, " when specifying defaults" do + before do + @config = Puppet::Util::Config.new + end + + it "should start with no defined parameters" do + @config.params.length.should == 0 + end + + it "should allow specification of default values associated with a section as an array" do + @config.setdefaults(:section, :myvalue => ["defaultval", "my description"]) + end + + it "should fail when a parameter has already been defined" do + @config.setdefaults(:section, :myvalue => ["a", "b"]) + lambda { @config.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 + @config.setdefaults(:section, :myvalue => {:default => "defaultval", :desc => "my description"}) + end + + it "should consider defined parameters to be valid" do + @config.setdefaults(:section, :myvalue => ["defaultval", "my description"]) + @config.valid?(:myvalue).should be_true + end + + it "should require a description when defaults are specified with an array" do + lambda { @config.setdefaults(:section, :myvalue => ["a value"]) }.should raise_error(ArgumentError) + end + + it "should require a description when defaults are specified with a hash" do + lambda { @config.setdefaults(:section, :myvalue => {:default => "a value"}) }.should raise_error(ArgumentError) + end + + it "should support specifying owner, group, and mode when specifying files" do + @config.setdefaults(:section, :myvalue => {:default => "/some/file", :owner => "blah", :mode => "boo", :group => "yay", :desc => "whatever"}) + end + + it "should support specifying a short name" do + @config.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) + end + + it "should fail when short names conflict" do + @config.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) + lambda { @config.setdefaults(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) }.should raise_error(ArgumentError) + end +end + +describe Puppet::Util::Config, " when setting values" do + before do + @config = Puppet::Util::Config.new + @config.setdefaults :main, :myval => ["val", "desc"] + @config.setdefaults :main, :bool => [true, "desc"] + end + + it "should provide a method for setting values from other objects" do + @config[:myval] = "something else" + @config[:myval].should == "something else" + end + + it "should support a getopt-specific mechanism for setting values" do + @config.handlearg("--myval", "newval") + @config[:myval].should == "newval" + end + + it "should support a getopt-specific mechanism for turning booleans off" do + @config.handlearg("--no-bool") + @config[:bool].should == false + end + + it "should support a getopt-specific mechanism for turning booleans on" do + # Turn it off first + @config[:bool] = false + @config.handlearg("--bool") + @config[:bool].should == true + end + + it "should support a mechanism for setting values in a specific search section" do + pending "This code requires the search path functionality" + #@config.set(:myval, "new value", :cli) + #@config[:myval].should == "new value" + end +end + +describe Puppet::Util::Config, " when returning values" do + before do + @config = Puppet::Util::Config.new + @config.setdefaults :section, :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"] + end + + it "should provide a mechanism for returning set values" do + @config[:one] = "other" + @config[:one].should == "other" + end + + it "should return default values if no values have been set" do + @config[:one].should == "ONE" + end + + it "should support a search path for finding values" do + pending "I have no idea how this will work yet" + end + + it "should return set values in the order defined in the search path" do + pending "Still no clear idea how this will work" + end + + it "should interpolate other parameters into returned parameter values" do + @config[:one].should == "ONE" + @config[:two].should == "ONE TWO" + @config[:three].should == "ONE ONE TWO THREE" + end + + it "should not cache interpolated values such that stale information is returned" do + @config[:two].should == "ONE TWO" + @config[:one] = "one" + @config[:two].should == "one TWO" + end +end + +describe Puppet::Util::Config, " when parsing its configuration" do + before do + @config = Puppet::Util::Config.new + @config.setdefaults :section, :one => ["ONE", "a"], :two => ["$one TWO", "b"], :three => ["$one $two THREE", "c"] + end + + it "should not return values outside of its search path" do + text = "[main] + one = mval + [other] + two = oval + " + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:one].should == "mval" + @config[:two].should == "mval TWO" + 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. + @config.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" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:one].should == true + @config[:two].should == false + end + + it "should convert integers in the configuration file into Ruby Integers" do + text = "[main] + one = 65 + " + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:one].should == 65 + end + + it "should support specifying file all metadata (owner, group, mode) in the configuration file" do + @config.setdefaults :section, :myfile => ["/my/file", "a"] + + text = "[main] + myfile = /other/file {owner = luke, group = luke, mode = 644} + " + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:myfile].should == "/other/file" + @config.metadata(:myfile).should == {:owner => "luke", :group => "luke", :mode => "644"} + end + + it "should support specifying file a single piece of metadata (owner, group, or mode) in the configuration file" do + @config.setdefaults :section, :myfile => ["/my/file", "a"] + + text = "[main] + myfile = /other/file {owner = luke} + " + file = "/some/file" + @config.expects(:read_file).with(file).returns(text) + @config.parse(file) + @config[:myfile].should == "/other/file" + @config.metadata(:myfile).should == {:owner => "luke"} + end +end + +describe Puppet::Util::Config, " when reparsing its configuration" do + before do + @config = Puppet::Util::Config.new + @config.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") + @config[:one] = "init" + @config.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. + @config.expects(:read_file).with(file).returns(text).times(2) + @config.reparse + @config[:one].should == "disk-replace" + end + + it "should retain parameters set by cli when configuration files are reparsed" do + @config.handlearg("--one", "myval") + @config[:two] = "otherval" + end +end + +describe Puppet::Util::Config, " when being used to manage the host machine" do + it "should provide a method that writes files with the correct modes" + + it "should provide a method that creates directories with the correct modes" + + it "should provide a method to declare what directories should exist" + + it "should provide a method to trigger enforcing of file modes on existing files and directories" + + it "should provide a method to convert the file mode enforcement into a Puppet manifest" + + it "should provide an option to create needed users and groups" + + it "should provide a method to print out the current configuration" + + it "should be able to provide all of its parameters in a format compatible with GetOpt::Long" + + it "should not attempt to manage files within /dev" +end diff --git a/test/util/config.rb b/test/util/config.rb index 9a1017058..b75f8e73d 100755 --- a/test/util/config.rb +++ b/test/util/config.rb @@ -1,1311 +1,1317 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'mocha' require 'puppettest' require 'puppet/util/config' require 'puppettest/parsertesting' class TestConfig < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting CElement = Puppet::Util::Config::CElement CBoolean = Puppet::Util::Config::CBoolean def setup super @config = mkconfig end def set_configs(config = nil) config ||= @config config.setdefaults("main", :one => ["a", "one"], :two => ["a", "two"], :yay => ["/default/path", "boo"], :mkusers => [true, "uh, yeah"], :name => ["testing", "a"] ) config.setdefaults("section1", :attr => ["a", "one"], :attrdir => ["/another/dir", "two"], :attr3 => ["$attrdir/maybe", "boo"] ) end def check_for_users count = Puppet::Type.type(:user).inject(0) { |c,o| c + 1 } assert(count > 0, "Found no users") end def test_to_transportable set_configs trans = nil assert_nothing_raised("Could not convert to a transportable") { trans = @config.to_transportable } comp = nil assert_nothing_raised("Could not convert transportable to component") { comp = trans.to_type } assert_nothing_raised("Could not retrieve transported config") { comp.retrieve } end def test_to_manifest set_configs manifest = nil assert_nothing_raised("Could not convert to a manifest") { manifest = @config.to_manifest } Puppet[:parseonly] = true interp = nil assert_nothing_raised do interp = mkinterp :Code => manifest, :UseNodes => false end trans = nil + node = Puppet::Node.new("node") assert_nothing_raised do - trans = interp.evaluate(nil, {}) + trans = interp.compile(node) end assert_nothing_raised("Could not instantiate objects") { trans.to_type } end def test_to_comp set_configs comp = nil assert_nothing_raised("Could not convert to a component") { comp = @config.to_component } assert_nothing_raised("Could not retrieve component") { comp.retrieve } end def test_to_config set_configs newc = mkconfig set_configs(newc) # Reset all of the values, so we know they're changing. newc.each do |name, obj| next if name == :name newc[name] = true end newfile = tempfile() File.open(newfile, "w") { |f| @config.to_config.split("\n").each do |line| # Uncomment the settings, so they actually take. if line =~ / = / f.puts line.sub(/^\s*#/, '') else f.puts line end end } assert_nothing_raised("Could not parse generated configuration") { newc.parse(newfile) } @config.each do |name, object| assert_equal(@config[name], newc[name], "Parameter %s is not the same" % name) end end def mkconfig c = nil assert_nothing_raised { c = Puppet::Util::Config.new } return c end def test_addbools assert_nothing_raised { @config.setdefaults(:testing, :booltest => [true, "testing"]) } assert(@config[:booltest]) @config = mkconfig assert_nothing_raised { @config.setdefaults(:testing, :booltest => ["true", "testing"]) } assert(@config[:booltest]) assert_nothing_raised { @config[:booltest] = false } assert(! @config[:booltest], "Booltest is not false") assert_nothing_raised { @config[:booltest] = "false" } assert(! @config[:booltest], "Booltest is not false") - assert_raise(Puppet::Error) { + assert_raise(ArgumentError) { @config[:booltest] = "yayness" } - assert_raise(Puppet::Error) { + assert_raise(ArgumentError) { @config[:booltest] = "/some/file" } end def test_strings val = "this is a string" assert_nothing_raised { @config.setdefaults(:testing, :strtest => [val, "testing"]) } assert_equal(val, @config[:strtest]) # Verify that variables are interpolated assert_nothing_raised { @config.setdefaults(:testing, :another => ["another $strtest", "testing"]) } assert_equal("another #{val}", @config[:another]) end def test_files c = mkconfig parent = "/puppet" assert_nothing_raised { @config.setdefaults(:testing, :parentdir => [parent, "booh"]) } assert_nothing_raised { @config.setdefaults(:testing, :child => ["$parent/child", "rah"]) } assert_equal(parent, @config[:parentdir]) assert_equal("/puppet/child", File.join(@config[:parentdir], "child")) end def test_getset initial = "an initial value" - assert_raise(Puppet::Error) { + assert_raise(ArgumentError) { @config[:yayness] = initial } default = "this is a default" assert_nothing_raised { @config.setdefaults(:testing, :yayness => [default, "rah"]) } assert_equal(default, @config[:yayness]) assert_nothing_raised { @config[:yayness] = initial } assert_equal(initial, @config[:yayness]) assert_nothing_raised { @config.clear } assert_equal(default, @config[:yayness]) assert_nothing_raised { @config[:yayness] = "not default" } assert_equal("not default", @config[:yayness]) end def test_parse_file text = %{ one = this is a test two = another test owner = root group = root yay = /a/path [main] four = five six = seven [section1] attr = value owner = puppet group = puppet attrdir = /some/dir attr3 = $attrdir/other } file = tempfile() File.open(file, "w") { |f| f.puts text } @config.expects(:settimer) result = nil assert_nothing_raised { result = @config.send(:parse_file, file) } main = result[:main] assert(main, "Did not get section for main") { :one => "this is a test", :two => "another test", :owner => "root", :group => "root", :yay => "/a/path", :four => "five", :six => "seven" }.each do |param, value| assert_equal(value, main[param], "Param %s was not set correctly in main" % param) end section1 = result[:section1] assert(section1, "Did not get section1") { :attr => "value", :owner => "puppet", :group => "puppet", :attrdir => "/some/dir", :attr3 => "$attrdir/other" }.each do |param, value| assert_equal(value, section1[param], "Param %s was not set correctly in section1" % param) end end def test_old_parse text = %{ one = this is a test two = another test owner = root group = root yay = /a/path [section1] attr = value owner = puppet group = puppet attrdir = /some/dir attr3 = $attrdir/other } file = tempfile() File.open(file, "w") { |f| f.puts text } assert_nothing_raised { @config.setdefaults("puppet", :one => ["a", "one"], :two => ["a", "two"], :yay => ["/default/path", "boo"], :mkusers => [true, "uh, yeah"] ) } assert_nothing_raised { @config.setdefaults("section1", :attr => ["a", "one"], :attrdir => ["/another/dir", "two"], :attr3 => ["$attrdir/maybe", "boo"] ) } assert_nothing_raised { @config.old_parse(file) } assert_equal("value", @config[:attr]) assert_equal("/some/dir", @config[:attrdir]) assert_equal(:directory, @config.element(:attrdir).type) assert_equal("/some/dir/other", @config[:attr3]) elem = nil assert_nothing_raised { elem = @config.element(:attr3) } assert(elem) assert_equal("puppet", elem.owner) config = nil assert_nothing_raised { config = @config.to_config } assert_nothing_raised("Could not create transportable config") { @config.to_transportable } end def test_parse result = { :main => {:main => "main", :bad => "invalid", :cliparam => "reset"}, :puppet => {:other => "puppet", :cliparam => "reset"}, :puppetd => {:other => "puppetd", :cliparam => "reset"} } # Set our defaults, so they're valid. Don't define 'bad', since we want to test for failures. @config.setdefaults(:main, :main => ["whatever", "a"], :cliparam => ["default", "y"], :other => ["a", "b"], :name => ["puppet", "b"] # our default name ) @config.setdefaults(:other, :one => ["whatever", "a"], :two => ["default", "y"], :apple => ["a", "b"], :shoe => ["puppet", "b"] # our default name ) @config.handlearg("--cliparam", "changed") @config.expects(:parse_file).returns(result).times(2) # First do it with our name being 'puppet' assert_nothing_raised("Could not handle parse results") do @config.parse(tempfile) end assert_logged(:warning, /unknown configuration parameter bad/, "Did not log invalid config param") assert_equal("main", @config[:main], "Did not get main value") assert_equal("puppet", @config[:other], "Did not get name value") assert_equal("changed", @config[:cliparam], "CLI values were overridden by config") # Now switch names and make sure the parsing switches, too. @config.clear(true) @config[:name] = :puppetd assert_nothing_raised("Could not handle parse results") do @config.parse(tempfile) end assert_logged(:warning, /unknown configuration parameter bad/, "Did not log invalid config param") assert_equal("main", @config[:main], "Did not get main value") assert_equal("puppetd", @config[:other], "Did not get name value") assert_equal("changed", @config[:cliparam], "CLI values were overridden by config") end # Make sure we can extract file options correctly. def test_parsing_file_options @config.setdefaults(:whev, :file => { :desc => "whev", :default => "/default", :owner => "me", :group => "me", :mode => "755" } ) file = tempfile + count = 0 { :pass => { " {owner = you}" => {:owner => "you"}, " {owner = you, group = you}" => {:owner => "you", :group => "you"}, " {owner = you, group = you, mode = 755}" => {:owner => "you", :group => "you", :mode => "755"}, " { owner = you, group = you } " => {:owner => "you", :group => "you"}, "{owner=you,group=you} " => {:owner => "you", :group => "you"}, "{owner=you,} " => {:owner => "you"} }, :fail => [ %{{owner = you group = you}}, %{{owner => you, group => you}}, %{{user => you}}, %{{random => you}}, %{{mode => you}}, # make sure modes are numbers %{{owner => you}} ] }.each do |type, list| + count += 1 list.each do |value| if type == :pass value, should = value[0], value[1] end + path = "/other%s" % count # Write our file out File.open(file, "w") do |f| - f.puts %{[main]\nfile = /other%s} % value + f.puts %{[main]\nfile = #{path}#{value}} end if type == :fail - assert_raise(Puppet::Error, "Did not fail on %s" % value.inspect) do + assert_raise(ArgumentError, "Did not fail on %s" % value.inspect) do @config.send(:parse_file, file) end else result = nil assert_nothing_raised("Failed to parse %s" % value.inspect) do result = @config.send(:parse_file, file) end assert_equal(should, result[:main][:_meta][:file], "Got incorrect return for %s" % value.inspect) + assert_equal(path, result[:main][:file], "Got incorrect value for %s" % value.inspect) end end end end # Make sure file options returned from parse_file are handled correctly. def test_parsed_file_options @config.setdefaults(:whev, :file => { :desc => "whev", :default => "/default", :owner => "me", :group => "me", :mode => "755" } ) result = { :main => { :file => "/other", :_meta => { :file => { :owner => "you", :group => "you", :mode => "644" } } } } @config.expects(:parse_file).returns(result) assert_nothing_raised("Could not handle file options") do @config.parse("/whatever") end # Get the actual object, so we can verify metadata file = @config.element(:file) + assert_equal("/other", file.value, "Did not get correct value") assert_equal("you", file.owner, "Did not pass on user") assert_equal("you", file.group, "Did not pass on group") assert_equal("644", file.mode, "Did not pass on mode") end def test_arghandling c = mkconfig assert_nothing_raised { @config.setdefaults("testing", :onboolean => [true, "An on bool"], :offboolean => [false, "An off bool"], :string => ["a string", "A string arg"], :file => ["/path/to/file", "A file arg"] ) } data = { :onboolean => [true, false], :offboolean => [true, false], :string => ["one string", "another string"], :file => %w{/a/file /another/file} } data.each { |param, values| values.each { |val| opt = nil arg = nil if @config.boolean?(param) if val opt = "--%s" % param else opt = "--no-%s" % param end else opt = "--%s" % param arg = val end assert_nothing_raised("Could not handle arg %s with value %s" % [opt, val]) { @config.handlearg(opt, arg) } } } end def test_addargs @config.setdefaults("testing", :onboolean => [true, "An on bool"], :offboolean => [false, "An off bool"], :string => ["a string", "A string arg"], :file => ["/path/to/file", "A file arg"] ) should = [] @config.each { |name, element| element.expects(:getopt_args).returns([name]) should << name } result = [] assert_nothing_raised("Add args failed") do @config.addargs(result) end assert_equal(should, result, "Did not call addargs correctly.") end def test_addargs_functional @config.setdefaults("testing", :onboolean => [true, "An on bool"], :string => ["a string", "A string arg"] ) result = [] should = [] assert_nothing_raised("Add args failed") do @config.addargs(result) end @config.each do |name, element| if name == :onboolean should << ["--onboolean", GetoptLong::NO_ARGUMENT] should << ["--no-onboolean", GetoptLong::NO_ARGUMENT] elsif name == :string should << ["--string", GetoptLong::REQUIRED_ARGUMENT] end end assert_equal(should, result, "Add args functional test failed") end def test_usesection # We want to make sure that config processes do not result in graphing. Puppet[:graphdir] = tempfile() Puppet[:graph] = true Dir.mkdir(Puppet[:graphdir]) c = mkconfig dir = tempfile() file = "$mydir/myfile" realfile = File.join(dir, "myfile") otherfile = File.join(dir, "otherfile") section = "testing" assert_nothing_raised { @config.setdefaults(section, :mydir => [dir, "A dir arg"], :otherfile => { :default => "$mydir/otherfile", :create => true, :desc => "A file arg" }, :myfile => [file, "A file arg"] ) } assert_nothing_raised("Could not use a section") { @config.use(section) } assert_nothing_raised("Could not reuse a section") { @config.use(section) } # Make sure it didn't graph anything, which is the only real way # to test that the transaction was marked as a configurator. assert(Dir.entries(Puppet[:graphdir]).reject { |f| f =~ /^\.\.?$/ }.empty?, "Graphed config process") assert(FileTest.directory?(dir), "Did not create directory") assert(FileTest.exists?(otherfile), "Did not create file") assert(!FileTest.exists?(realfile), "Created file") end def test_setdefaultsarray c = mkconfig assert_nothing_raised { @config.setdefaults("yay", :a => [false, "some value"], :b => ["/my/file", "a file"] ) } assert_equal(false, @config[:a], "Values are not equal") assert_equal("/my/file", @config[:b], "Values are not equal") end def test_setdefaultshash c = mkconfig assert_nothing_raised { @config.setdefaults("yay", :a => {:default => false, :desc => "some value"}, :b => {:default => "/my/file", :desc => "a file"} ) } assert_equal(false, @config[:a], "Values are not equal") assert_equal("/my/file", @config[:b], "Values are not equal") end def test_reuse c = mkconfig file = tempfile() section = "testing" assert_nothing_raised { @config.setdefaults(section, :myfile => {:default => file, :create => true, :desc => "yay"} ) } assert_nothing_raised("Could not use a section") { @config.use(section) } assert(FileTest.exists?(file), "Did not create file") assert(! Puppet::Type.type(:file)[file], "File obj still exists") File.unlink(file) @config.reuse assert(FileTest.exists?(file), "Did not create file") end def test_mkusers c = mkconfig file = tempfile() section = "testing" assert_nothing_raised { @config.setdefaults(section, :mkusers => [false, "yay"], :myfile => { :default => file, :owner => "pptest", :group => "pptest", :desc => "yay", :create => true } ) } comp = nil assert_nothing_raised { comp = @config.to_component } [:user, :group].each do |type| # The objects might get created internally by Puppet::Util; just # make sure they're not being managed if obj = Puppet.type(type)["pptest"] assert(! obj.managed?, "%s objectis managed" % type) end end comp.each { |o| o.remove } @config[:mkusers] = true assert_nothing_raised { @config.to_component } user = Puppet.type(:user)["pptest"] assert(user, "User object did not get created") assert(user.managed?, "User object is not managed.") assert(user.should(:comment), "user does not have a comment set") group = Puppet.type(:group)["pptest"] assert(group, "Group object did not get created") assert(group.managed?, "Group object is not managed." ) if Process.uid == 0 cleanup do user[:ensure] = :absent group[:ensure] = :absent assert_apply(user, group) end assert_apply(user, group) end end def test_notmanagingdev c = mkconfig path = "/dev/testing" @config.setdefaults(:test, :file => { :default => path, :mode => 0640, :desc => 'yay' } ) assert_nothing_raised { @config.to_component } assert(! Puppet.type(:file)["/dev/testing"], "Created dev file") end def test_groupsetting cfile = tempfile() group = "yayness" File.open(cfile, "w") do |f| f.puts "[main] group = #{group} " end config = mkconfig config.setdefaults(Puppet[:name], :group => ["puppet", "a group"]) assert_nothing_raised { config.parse(cfile) } assert_equal(group, config[:group], "Group did not take") end # provide a method to modify and create files w/out specifying the info # already stored in a config def test_writingfiles File.umask(0022) path = tempfile() mode = 0644 config = mkconfig args = { :default => path, :mode => mode, :desc => "yay" } user = nonrootuser() group = nonrootgroup() if Puppet::Util::SUIDManager.uid == 0 args[:owner] = user.name args[:group] = group.name end config.setdefaults(:testing, :myfile => args) assert_nothing_raised { config.write(:myfile) do |file| file.puts "yay" end } assert_equal(mode, filemode(path), "Modes are not equal") # OS X is broken in how it chgrps files if Puppet::Util::SUIDManager.uid == 0 assert_equal(user.uid, File.stat(path).uid, "UIDS are not equal") case Facter["operatingsystem"].value when /BSD/, "Darwin": # nothing else assert_equal(group.gid, File.stat(path).gid, "GIDS are not equal") end end end def test_mkdir File.umask(0022) path = tempfile() mode = 0755 config = mkconfig args = { :default => path, :mode => mode, :desc => "a file" } user = nonrootuser() group = nonrootgroup() if Puppet::Util::SUIDManager.uid == 0 args[:owner] = user.name args[:group] = group.name end config.setdefaults(:testing, :mydir => args) assert_nothing_raised { config.mkdir(:mydir) } assert_equal(mode, filemode(path), "Modes are not equal") # OS X and *BSD is broken in how it chgrps files if Puppet::Util::SUIDManager.uid == 0 assert_equal(user.uid, File.stat(path).uid, "UIDS are not equal") case Facter["operatingsystem"].value when /BSD/, "Darwin": # nothing else assert_equal(group.gid, File.stat(path).gid, "GIDS are not equal") end end end def test_booleans_and_integers config = mkconfig config.setdefaults(:mysection, :booltest => [false, "yay"], :inttest => [14, "yay"] ) file = tempfile() File.open(file, "w") do |f| f.puts %{ [main] booltest = true inttest = 27 } end assert_nothing_raised { config.parse(file) } assert_equal(true, config[:booltest], "Boolean was not converted") assert_equal(27, config[:inttest], "Integer was not converted") # Now make sure that they get converted through handlearg config.handlearg("--inttest", "true") assert_equal(true, config[:inttest], "Boolean was not converted") config.handlearg("--no-booltest", "false") assert_equal(false, config[:booltest], "Boolean was not converted") end # Make sure that tags are ignored when configuring def test_configs_ignore_tags config = mkconfig file = tempfile() config.setdefaults(:mysection, :mydir => [file, "a file"] ) Puppet[:tags] = "yayness" assert_nothing_raised { config.use(:mysection) } assert(FileTest.directory?(file), "Directory did not get created") assert_equal("yayness", Puppet[:tags], "Tags got changed during config") end def test_configs_replace_in_url config = mkconfig config.setdefaults(:mysection, :name => ["yayness", "yay"]) config.setdefaults(:mysection, :url => ["http://$name/rahness", "yay"]) val = nil assert_nothing_raised { val = config[:url] } assert_equal("http://yayness/rahness", val, "Config got messed up") end def test_correct_type_assumptions config = mkconfig file = Puppet::Util::Config::CFile element = Puppet::Util::Config::CElement bool = Puppet::Util::Config::CBoolean # We have to keep these ordered, unfortunately. [ ["/this/is/a/file", file], ["true", bool], [true, bool], ["false", bool], ["server", element], ["http://$server/yay", element], ["$server/yayness", file], ["$server/yayness.conf", file] ].each do |ary| value, type = ary elem = nil assert_nothing_raised { elem = config.newelement( :name => value, :default => value, :desc => name.to_s, :section => :yayness ) } assert_instance_of(type, elem, "%s got created as wrong type" % value.inspect) end end # Make sure we correctly reparse our config files but don't lose CLI values. def test_reparse Puppet[:filetimeout] = 0 config = mkconfig() config.setdefaults(:mysection, :default => ["default", "yay"]) config.setdefaults(:mysection, :clichange => ["clichange", "yay"]) config.setdefaults(:mysection, :filechange => ["filechange", "yay"]) file = tempfile() # Set one parameter in the file File.open(file, "w") { |f| f.puts %{[main]\nfilechange = filevalue} } assert_nothing_raised { config.parse(file) } # Set another "from the cli" assert_nothing_raised { config.handlearg("clichange", "clivalue") } # And leave the other unset assert_equal("default", config[:default]) assert_equal("filevalue", config[:filechange]) assert_equal("clivalue", config[:clichange]) # Now rewrite the file File.open(file, "w") { |f| f.puts %{[main]\nfilechange = newvalue} } cfile = config.file cfile.send("tstamp=".intern, Time.now - 50) # And check all of the values assert_equal("default", config[:default]) assert_equal("clivalue", config[:clichange]) assert_equal("newvalue", config[:filechange]) end def test_parse_removes_quotes config = mkconfig() config.setdefaults(:mysection, :singleq => ["single", "yay"]) config.setdefaults(:mysection, :doubleq => ["double", "yay"]) config.setdefaults(:mysection, :none => ["noquote", "yay"]) config.setdefaults(:mysection, :middle => ["midquote", "yay"]) file = tempfile() # Set one parameter in the file File.open(file, "w") { |f| f.puts %{[main]\n singleq = 'one' doubleq = "one" none = one middle = mid"quote } } assert_nothing_raised { config.parse(file) } %w{singleq doubleq none}.each do |p| assert_equal("one", config[p], "%s did not match" % p) end assert_equal('mid"quote', config["middle"], "middle did not match") end def test_timer Puppet[:filetimeout] = 0.1 origpath = tempfile() config = mkconfig() config.setdefaults(:mysection, :paramdir => [tempfile(), "yay"]) file = tempfile() # Set one parameter in the file File.open(file, "w") { |f| f.puts %{[main]\n paramdir = #{origpath} } } assert_nothing_raised { config.parse(file) config.use(:mysection) } assert(FileTest.directory?(origpath), "dir did not get created") # Now start the timer assert_nothing_raised { EventLoop.current.monitor_timer config.timer } newpath = tempfile() File.open(file, "w") { |f| f.puts %{[main]\n paramdir = #{newpath} } } config.file.send("tstamp=".intern, Time.now - 50) sleep 1 assert_equal(newpath, config["paramdir"], "File did not get reparsed from timer") assert(FileTest.directory?(newpath), "new dir did not get created") end # Test that config parameters correctly call passed-in blocks when the value # is set. def test_paramblocks config = mkconfig() testing = nil elem = nil assert_nothing_raised do elem = config.newelement :default => "yay", :name => :blocktest, :desc => "boo", :section => :test, :hook => proc { |value| testing = value } end assert_nothing_raised do assert_equal("yay", elem.value) end assert_nothing_raised do elem.value = "yaytest" end assert_nothing_raised do assert_equal("yaytest", elem.value) end assert_equal("yaytest", testing) assert_nothing_raised do elem.value = "another" end assert_nothing_raised do assert_equal("another", elem.value) end assert_equal("another", testing) # Now verify it works from setdefault assert_nothing_raised do config.setdefaults :test, :blocktest2 => { :default => "yay", :desc => "yay", :hook => proc { |v| testing = v } } end assert_equal("yay", config[:blocktest2]) assert_nothing_raised do config[:blocktest2] = "footest" end assert_equal("footest", config[:blocktest2]) assert_equal("footest", testing) end def test_no_modify_root config = mkconfig config.setdefaults(:yay, :mydir => {:default => tempfile(), :mode => 0644, :owner => "root", :group => "root", :desc => "yay" }, :mkusers => [false, "yay"] ) assert_nothing_raised do config.use(:yay) end # Now enable it so they'll be added config[:mkusers] = true comp = config.to_component Puppet::Type.type(:user).each do |u| assert(u.name != "root", "Tried to manage root user") end Puppet::Type.type(:group).each do |u| assert(u.name != "root", "Tried to manage root group") assert(u.name != "wheel", "Tried to manage wheel group") end # assert(yay, "Did not find yay component") # yay.each do |c| # puts @config.ref # end # assert(! yay.find { |o| o.class.name == :user and o.name == "root" }, # "Found root user") # assert(! yay.find { |o| o.class.name == :group and o.name == "root" }, # "Found root group") end # #415 def test_remove_trailing_spaces config = mkconfig() config.setdefaults(:yay, :rah => ["testing", "a desc"]) file = tempfile() File.open(file, "w") { |f| f.puts "rah = something " } assert_nothing_raised { config.parse(file) } assert_equal("something", config[:rah], "did not remove trailing whitespace in parsing") end # #484 def test_parsing_unknown_variables logstore() config = mkconfig() config.setdefaults(:mysection, :one => ["yay", "yay"]) file = tempfile() File.open(file, "w") { |f| f.puts %{[main]\n one = one two = yay } } assert_nothing_raised("Unknown parameter threw an exception") do config.parse(file) end assert(@logs.detect { |l| l.message =~ /unknown configuration/ and l.level == :warning }, "Did not generate warning message") end def test_multiple_interpolations @config.setdefaults(:section, :one => ["oneval", "yay"], :two => ["twoval", "yay"], :three => ["$one/$two", "yay"] ) assert_equal("oneval/twoval", @config[:three], "Did not interpolate multiple variables") end # Make sure we can replace ${style} var names def test_curly_replacements @config.setdefaults(:section, :one => ["oneval", "yay"], :two => ["twoval", "yay"], :three => ["$one/${two}/${one}/$two", "yay"] ) assert_equal("oneval/twoval/oneval/twoval", @config[:three], "Did not interpolate curlied variables") end # Discovered from #734 def test_set_parameter_hash @config.setdefaults(:section, :unchanged => ["unval", "yay"], :normal => ["normalval", "yay"], :cliparam => ["clival", "yay"], :file => ["/my/file", "yay"] ) # Set the cli param using the cli method @config.handlearg("--cliparam", "other") # Make sure missing params just throw warnings, not errors assert_nothing_raised("Could not call set_parameter_hash with an invalid option") do @config.send(:set_parameter_hash, :missing => "something") end # Make sure normal values get set assert_nothing_raised("Could not call set_parameter_hash with a normal value") do @config.send(:set_parameter_hash, :normal => "abnormal") end assert_equal("abnormal", @config[:normal], "Value did not get set") # Make sure cli-set values don't get overridden assert_nothing_raised("Could not call set_parameter_hash with an override of a cli value") do @config.send(:set_parameter_hash, :cliparam => "something else") end assert_equal("other", @config[:cliparam], "CLI value was overridden by config value") # Make sure the meta stuff works assert_nothing_raised("Could not call set_parameter_hash with meta info") do @config.send(:set_parameter_hash, :file => "/other/file", :_meta => {:file => {:mode => "0755"}}) end assert_equal("/other/file", @config[:file], "value with meta info was overridden by config value") assert_equal("0755", @config.element(:file).mode, "Did not set mode from meta info") # And make sure other params are unchanged assert_equal("unval", @config[:unchanged], "Unchanged value has somehow changed") end # Test to make sure that we can set and get a short name def test_celement_short_name element = nil assert_nothing_raised("Could not create celement") do element = CElement.new :short => "n", :desc => "anything" end assert_equal("n", element.short, "Short value is not retained") assert_raise(ArgumentError,"Allowed multicharactered short names.") do element = CElement.new :short => "no", :desc => "anything" end end # Test to make sure that no two celements have the same short name def test_celement_short_name_not_duplicated config = mkconfig assert_nothing_raised("Could not create celement with short name.") do config.setdefaults(:main, :one => { :default => "blah", :desc => "anything", :short => "o" }) end assert_nothing_raised("Could not create second celement with short name.") do config.setdefaults(:main, :two => { :default => "blah", :desc => "anything", :short => "i" }) end assert_raise(ArgumentError, "Could create second celement with duplicate short name.") do config.setdefaults(:main, :three => { :default => "blah", :desc => "anything", :short => "i" }) end # make sure that when the above raises an expection that the config is not included assert(!config.include?(:three), "Invalid configuration item was retained") end # Tell getopt which arguments are valid def test_get_getopt_args element = CElement.new :name => "foo", :desc => "anything" assert_equal([["--foo", GetoptLong::REQUIRED_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") element.short = "n" assert_equal([["--foo", "-n", GetoptLong::REQUIRED_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") element = CBoolean.new :name => "foo", :desc => "anything" assert_equal([["--foo", GetoptLong::NO_ARGUMENT], ["--no-foo", GetoptLong::NO_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") element.short = "n" assert_equal([["--foo", "-n", GetoptLong::NO_ARGUMENT],["--no-foo", GetoptLong::NO_ARGUMENT]], element.getopt_args, "Did not produce appropriate getopt args") end end # $Id$