diff --git a/lib/puppet/network/handler/configuration.rb b/lib/puppet/network/handler/configuration.rb index fd1ee86ed..b2b16d022 100644 --- a/lib/puppet/network/handler/configuration.rb +++ b/lib/puppet/network/handler/configuration.rb @@ -1,209 +1,213 @@ require 'openssl' require 'puppet' require 'puppet/parser/interpreter' require 'puppet/sslcertificates' require 'xmlrpc/server' require 'yaml' class Puppet::Network::Handler class Configuration < Handler desc "Puppet's configuration compilation interface. Passed a node name or other key, retrieves information about the node (using the ``node_source``) and returns a compiled configuration." include Puppet::Util attr_accessor :local @interface = XMLRPC::Service::Interface.new("configuration") { |iface| iface.add_method("string configuration(string)") iface.add_method("string version()") } # Compile a node's configuration. def configuration(key, client = nil, clientip = nil) # Note that this is reasonable, because either their node source should actually # know about the node, or they should be using the ``none`` node source, which # will always return data. unless node = node_handler.details(key) raise Puppet::Error, "Could not find node '%s'" % key end # Add any external data to the node. add_node_data(node) return translate(compile(node)) end def initialize(options = {}) if options[:Local] @local = options[:Local] else @local = false end # Just store the options, rather than creating the interpreter # immediately. Mostly, this is so we can create the interpreter # on-demand, which is easier for testing. @options = options set_server_facts end # Are we running locally, or are our clients networked? def local? self.local end # Return the configuration version. def version(client = nil, clientip = nil) if client and node = node_handler.details(client) update_node_check(node) return interpreter.configuration_version(node) else # Just return something that will always result in a recompile, because # this is local. return (Time.now + 1000).to_i end end private # Add any extra data necessary to the node. def add_node_data(node) # Merge in our server-side facts, so they can be used during compilation. node.fact_merge(@server_facts) # Add any specified classes to the node's class list. if classes = @options[:Classes] classes.each do |klass| node.classes << klass end end end # Compile the actual configuration. def compile(node) # Pick the benchmark level. if local? level = :none else level = :notice end # Ask the interpreter to compile the configuration. + str = "Compiled configuration for %s" % node.name + if node.environment + str += " in environment %s" % node.environment + end config = nil benchmark(level, "Compiled configuration for %s" % node.name) do begin config = interpreter.compile(node) rescue Puppet::Error => detail if Puppet[:trace] puts detail.backtrace end Puppet.err detail raise XMLRPC::FaultException.new( 1, detail.to_s ) end end return config end # Create our interpreter object. def create_interpreter(options) args = {} # Allow specification of a code snippet or of a file if code = options[:Code] args[:Code] = code - else - args[:Manifest] = options[:Manifest] || Puppet[:manifest] + elsif options[:Manifest] + args[:Manifest] = options[:Manifest] end args[:Local] = local? if options.include?(:UseNodes) args[:UseNodes] = options[:UseNodes] elsif @local args[:UseNodes] = false end # This is only used by the cfengine module, or if --loadclasses was # specified in +puppet+. if options.include?(:Classes) args[:Classes] = options[:Classes] end return Puppet::Parser::Interpreter.new(args) end # Create/return our interpreter. def interpreter unless defined?(@interpreter) and @interpreter @interpreter = create_interpreter(@options) end @interpreter end # Create a node handler instance for looking up our nodes. def node_handler unless defined?(@node_handler) @node_handler = Puppet::Network::Handler.handler(:node).create end @node_handler end # Initialize our server fact hash; we add these to each client, and they # won't change while we're running, so it's safe to cache the values. def set_server_facts @server_facts = {} # Add our server version to the fact list @server_facts["serverversion"] = Puppet.version.to_s # And then add the server name and IP {"servername" => "fqdn", "serverip" => "ipaddress" }.each do |var, fact| if value = Facter.value(fact) @server_facts[var] = value else Puppet.warning "Could not retrieve fact %s" % fact end end if @server_facts["servername"].nil? host = Facter.value(:hostname) if domain = Facter.value(:domain) @server_facts["servername"] = [host, domain].join(".") else @server_facts["servername"] = host end end end # Translate our configuration appropriately for sending back to a client. def translate(config) if local? config else CGI.escape(config.to_yaml(:UseBlock => true)) end end # Mark that the node has checked in. FIXME this needs to be moved into # the Node class, or somewhere that's got abstract backends. def update_node_check(node) if Puppet.features.rails? and Puppet[:storeconfigs] Puppet::Rails.connect host = Puppet::Rails::Host.find_or_create_by_name(node.name) host.last_freshcheck = Time.now host.save end end end end # $Id$ diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index b6c61d202..93a4bc170 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,95 +1,98 @@ require 'puppet' require 'timeout' require 'puppet/rails' require 'puppet/util/methodhelper' require 'puppet/parser/parser' require 'puppet/parser/compile' 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::Compile.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 => environment) if self.code parser.string = self.code elsif self.file parser.file = self.file + else + file = Puppet.config.value(:manifest, environment) + parser.file = 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/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index 401b5b1c0..660fa8169 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -1,450 +1,454 @@ # I pulled this into a separate file, because I got # tired of rebuilding the parser.rb file all the time. class Puppet::Parser::Parser require 'puppet/parser/functions' ASTSet = Struct.new(:classes, :definitions, :nodes) # Define an accessor method for each table. We hide the existence of # the struct. [:classes, :definitions, :nodes].each do |name| define_method(name) do @astset.send(name) end end AST = Puppet::Parser::AST - attr_reader :file, :version + attr_reader :version, :environment attr_accessor :files # Add context to a message; useful for error messages and such. def addcontext(message, obj = nil) obj ||= @lexer message += " on line %s" % obj.line if file = obj.file message += " in file %s" % file end return message end # Create an AST array out of all of the args def aryfy(*args) if args[0].instance_of?(AST::ASTArray) result = args.shift args.each { |arg| result.push arg } else result = ast AST::ASTArray, :children => args end return result end # Create an AST object, and automatically add the file and line information if # available. def ast(klass, hash = nil) hash ||= {} unless hash.include?(:line) hash[:line] = @lexer.line end unless hash.include?(:file) if file = @lexer.file hash[:file] = file end end return klass.new(hash) end # The fully qualifed name, with the full namespace. def classname(name) [@lexer.namespace, name].join("::").sub(/^::/, '') end def clear initvars end # Raise a Parse error. def error(message) if brace = @lexer.expected message += "; expected '%s'" end except = Puppet::ParseError.new(message) except.line = @lexer.line if @lexer.file except.file = @lexer.file end raise except end + def file + @lexer.file + end + def file=(file) unless FileTest.exists?(file) unless file =~ /\.pp$/ file = file + ".pp" end unless FileTest.exists?(file) raise Puppet::Error, "Could not find file %s" % file end end if @files.detect { |f| f.file == file } raise Puppet::AlreadyImportedError.new("Import loop detected") else @files << Puppet::Util::LoadedFile.new(file) @lexer.file = file end end # Find a class definition, relative to the current namespace. def findclass(namespace, name) fqfind namespace, name, classes end # Find a component definition, relative to the current namespace. def finddefine(namespace, name) fqfind namespace, name, definitions end # This is only used when nodes are looking up the code for their # parent nodes. def findnode(name) fqfind "", name, nodes end # The recursive method used to actually look these objects up. def fqfind(namespace, name, table) namespace = namespace.downcase name = name.downcase if name =~ /^::/ or namespace == "" classname = name.sub(/^::/, '') unless table[classname] self.load(classname) end return table[classname] end ary = namespace.split("::") while ary.length > 0 newname = (ary + [name]).join("::").sub(/^::/, '') if obj = table[newname] or (self.load(newname) and obj = table[newname]) return obj end # Delete the second to last object, which reduces our namespace by one. ary.pop end # If we've gotten to this point without finding it, see if the name # exists at the top namespace if obj = table[name] or (self.load(name) and obj = table[name]) return obj end return nil end # Import our files. def import(file) if Puppet[:ignoreimport] return AST::ASTArray.new(:children => []) end # use a path relative to the file doing the importing if @lexer.file dir = @lexer.file.sub(%r{[^/]+$},'').sub(/\/$/, '') else dir = "." end if dir == "" dir = "." end result = ast AST::ASTArray # We can't interpolate at this point since we don't have any # scopes set up. Warn the user if they use a variable reference pat = file if pat.index("$") Puppet.warning( "The import of #{pat} contains a variable reference;" + " variables are not interpolated for imports " + "in file #{@lexer.file} at line #{@lexer.line}" ) end files = Puppet::Module::find_manifests(pat, :cwd => dir) if files.size == 0 raise Puppet::ImportError.new("No file(s) found for import " + "of '#{pat}'") end files.collect { |file| parser = Puppet::Parser::Parser.new(:astset => @astset, :environment => @environment) parser.files = self.files Puppet.debug("importing '%s'" % file) unless file =~ /^#{File::SEPARATOR}/ file = File.join(dir, file) end begin parser.file = file rescue Puppet::AlreadyImportedError # This file has already been imported to just move on next end # This will normally add code to the 'main' class. parser.parse } end def initialize(options = {}) @astset = options[:astset] || ASTSet.new({}, {}, {}) @environment = options[:environment] initvars() end # Initialize or reset all of our variables. def initvars @lexer = Puppet::Parser::Lexer.new() @files = [] @loaded = [] end # Try to load a class, since we could not find it. def load(classname) return false if classname == "" filename = classname.gsub("::", File::SEPARATOR) loaded = false # First try to load the top-level module mod = filename.scan(/^[\w-]+/).shift unless @loaded.include?(mod) @loaded << mod begin import(mod) Puppet.info "Autoloaded module %s" % mod loaded = true rescue Puppet::ImportError => detail # We couldn't load the module end end unless filename == mod and ! @loaded.include?(mod) @loaded << mod # Then the individual file begin import(filename) Puppet.info "Autoloaded file %s from module %s" % [filename, mod] loaded = true rescue Puppet::ImportError => detail # We couldn't load the file end end return loaded end # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") n = ary.pop || "" ns = ary.join("::") return ns, n end # Create a new class, or merge with an existing class. def newclass(name, options = {}) name = name.downcase if definitions.include?(name) raise Puppet::ParseError, "Cannot redefine class %s as a definition" % name end code = options[:code] parent = options[:parent] # If the class is already defined, then add code to it. if other = @astset.classes[name] # Make sure the parents match if parent and other.parentclass and (parent != other.parentclass) error("Class %s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line]) end # This might be dangerous... if parent and ! other.parentclass other.parentclass = parent end # This might just be an empty, stub class. if code tmp = name if tmp == "" tmp = "main" end Puppet.debug addcontext("Adding code to %s" % tmp) # Else, add our code to it. if other.code and code other.code.children += code.children else other.code ||= code end end else # Define it anew. # Note we're doing something somewhat weird here -- we're setting # the class's namespace to its fully qualified name. This means # anything inside that class starts looking in that namespace first. args = {:namespace => name, :classname => name, :parser => self} args[:code] = code if code args[:parentclass] = parent if parent @astset.classes[name] = ast AST::HostClass, args end return @astset.classes[name] end # Create a new definition. def newdefine(name, options = {}) name = name.downcase if @astset.classes.include?(name) raise Puppet::ParseError, "Cannot redefine class %s as a definition" % name end # Make sure our definition doesn't already exist if other = @astset.definitions[name] error("%s is already defined at %s:%s; cannot redefine" % [name, other.file, other.line]) end ns, whatever = namesplit(name) args = { :namespace => ns, :arguments => options[:arguments], :code => options[:code], :parser => self, :classname => name } [:code, :arguments].each do |param| args[param] = options[param] if options[param] end @astset.definitions[name] = ast AST::Component, args end # Create a new node. Nodes are special, because they're stored in a global # table, not according to namespaces. def newnode(names, options = {}) names = [names] unless names.instance_of?(Array) names.collect do |name| name = name.to_s.downcase if other = @astset.nodes[name] error("Node %s is already defined at %s:%s; cannot redefine" % [other.name, other.file, other.line]) end name = name.to_s if name.is_a?(Symbol) args = { :name => name, :parser => self } if options[:code] args[:code] = options[:code] end if options[:parent] args[:parentclass] = options[:parent] end @astset.nodes[name] = ast(AST::Node, args) @astset.nodes[name].classname = name @astset.nodes[name] end end def on_error(token,value,stack) #on '%s' at '%s' in\n'%s'" % [token,value,stack] #error = "line %s: parse error after '%s'" % # [@lexer.line,@lexer.last] error = "Syntax error at '%s'" % [value] if brace = @lexer.expected error += "; expected '%s'" % brace end except = Puppet::ParseError.new(error) except.line = @lexer.line if @lexer.file except.file = @lexer.file end raise except end # how should I do error handling here? def parse(string = nil) if string self.string = string end begin main = yyparse(@lexer,:scan) rescue Racc::ParseError => except error = Puppet::ParseError.new(except) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error rescue Puppet::ParseError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::Error => except # and this is a framework error except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::DevError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue => except error = Puppet::DevError.new(except.message) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error end if main # Store the results as the top-level class. newclass("", :code => main) end @version = Time.now.to_i return @astset ensure @lexer.clear end # See if any of the files have changed. def reparse? if file = @files.detect { |file| file.changed? } return file.stamp else return false end end def string=(string) @lexer.string = string end # Add a new file to be checked when we're checking to see if we should be # reparsed. This is basically only used by the TemplateWrapper to let the # parser know about templates that should be parsed. def watch_file(*files) files.each do |file| unless file.is_a? Puppet::Util::LoadedFile file = Puppet::Util::LoadedFile.new(file) end @files << file end end end diff --git a/lib/puppet/util/config.rb b/lib/puppet/util/config.rb index b6831ba9b..fb1c01d56 100644 --- a/lib/puppet/util/config.rb +++ b/lib/puppet/util/config.rb @@ -1,1201 +1,1218 @@ 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_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 - @values[:cache].clear + @cache.clear end return value end # A simplified equality operator. 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 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 } @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 - @values[:cache].clear + @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) + clear(true) 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 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 # 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 # We have to do it in the reverse of the search path, # because multiple sections could set the same value. 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 - [:cache, :cli, :memory, environment, :name, :main] + [:cli, :memory, environment, :name, :main] else - [:cache, :cli, :memory, :name, :main] + [: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, includefiles = true) done ||= Hash.new { |hash, key| hash[key] = {} } objects = [] persection(section) do |obj| if @config[:mkusers] and value(:mkusers) [: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 value(obj.name) =~ /^\/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 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 # 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 @cache[environment||"none"].include?(param) + return @cache[environment||"none"][param] + end + # See if we can find it within our searchable list of values + val = nil searchpath(environment).each do |source| # Modify the source as necessary. source = case source when :name: self.name else source end - # Look for the value. + # 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] - # Cache the value, because we do so many parameter lookups. - unless source == :cache - val = convert(val) - @values[:cache][param] = val - end - return val + break end end - # No normal source, so get the default and cache it - val = convert(@config[param].default) - @values[:cache][param] = val + # If we didn't get a value, use the default + if val.nil? + val = @config[param].default + end + + # 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) 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(value(obj.name), *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 # 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 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 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. # 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 = @parent.value(self.name).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 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::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 ArgumentError, "Invalid value '%s' for %s" % [value.inspect, @name] end end end end # $Id$ diff --git a/spec/unit/parser/interpreter.rb b/spec/unit/parser/interpreter.rb index 7328e2651..ebb7d4cbf 100755 --- a/spec/unit/parser/interpreter.rb +++ b/spec/unit/parser/interpreter.rb @@ -1,170 +1,192 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Parser::Interpreter, " when initializing" do it "should default to neither code nor file" do interp = Puppet::Parser::Interpreter.new interp.code.should be_nil interp.file.should be_nil end it "should set the code to parse" do interp = Puppet::Parser::Interpreter.new :Code => :code interp.code.should equal(:code) end it "should set the file to parse" do interp = Puppet::Parser::Interpreter.new :Manifest => :file interp.file.should equal(:file) end it "should set code and ignore manifest when both are present" do interp = Puppet::Parser::Interpreter.new :Code => :code, :Manifest => :file interp.code.should equal(:code) interp.file.should be_nil end it "should default to usenodes" do interp = Puppet::Parser::Interpreter.new interp.usenodes?.should be_true end it "should allow disabling of usenodes" do interp = Puppet::Parser::Interpreter.new :UseNodes => false interp.usenodes?.should be_false end end describe Puppet::Parser::Interpreter, " when creating parser instances" do before do @interp = Puppet::Parser::Interpreter.new @parser = mock('parser') end it "should create a parser with code passed in at initialization time" do @interp.code = :some_code - @parser.expects(:code=).with(:some_code) + @parser.expects(:string=).with(:some_code) @parser.expects(:parse) - Puppet::Parser::Parser.expects(:new).with(:environment).returns(@parser) - @interp.send(:create_parser, :environment).object_id.should equal(@parser.object_id) + Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).returns(@parser) + @interp.send(:create_parser, :myenv).object_id.should equal(@parser.object_id) end it "should create a parser with a file passed in at initialization time" do @interp.file = :a_file @parser.expects(:file=).with(:a_file) @parser.expects(:parse) - Puppet::Parser::Parser.expects(:new).with(:environment).returns(@parser) - @interp.send(:create_parser, :environment).should equal(@parser) + Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).returns(@parser) + @interp.send(:create_parser, :myenv).should equal(@parser) end - it "should create a parser when passed neither code nor file" do + it "should create a parser with the main manifest when passed neither code nor file" do @parser.expects(:parse) - Puppet::Parser::Parser.expects(:new).with(:environment).returns(@parser) - @interp.send(:create_parser, :environment).should equal(@parser) + @parser.expects(:file=).with(Puppet[:manifest]) + Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).returns(@parser) + @interp.send(:create_parser, :myenv).should equal(@parser) end it "should return nothing when new parsers fail" do - Puppet::Parser::Parser.expects(:new).with(:environment).raises(ArgumentError) - @interp.send(:create_parser, :environment).should be_nil + Puppet::Parser::Parser.expects(:new).with(:environment => :myenv).raises(ArgumentError) + @interp.send(:create_parser, :myenv).should be_nil + end + + it "should create parsers with environment-appropriate manifests" do + # Set our per-environment values. We can't just stub :value, because + # it's called by too much of the rest of the code. + text = "[env1]\nmanifest = /t/env1.pp\n[env2]\nmanifest = /t/env2.pp" + file = mock 'file' + file.stubs(:changed?).returns(true) + file.stubs(:file).returns("/whatever") + Puppet.config.stubs(:read_file).with(file).returns(text) + Puppet.config.parse(file) + + parser1 = mock 'parser1' + Puppet::Parser::Parser.expects(:new).with(:environment => :env1).returns(parser1) + parser1.expects(:file=).with("/t/env1.pp") + @interp.send(:create_parser, :env1) + + parser2 = mock 'parser2' + Puppet::Parser::Parser.expects(:new).with(:environment => :env2).returns(parser2) + parser2.expects(:file=).with("/t/env2.pp") + @interp.send(:create_parser, :env2) end end describe Puppet::Parser::Interpreter, " when managing parser instances" do before do @interp = Puppet::Parser::Interpreter.new @parser = mock('parser') end it "it should an exception when nothing is there and nil is returned" do - @interp.expects(:create_parser).with(:environment).returns(nil) - lambda { @interp.send(:parser, :environment) }.should raise_error(Puppet::Error) + @interp.expects(:create_parser).with(:myenv).returns(nil) + lambda { @interp.send(:parser, :myenv) }.should raise_error(Puppet::Error) end it "should create and return a new parser and use the same parser when the parser does not need reparsing" do - @interp.expects(:create_parser).with(:environment).returns(@parser) - @interp.send(:parser, :environment).should equal(@parser) + @interp.expects(:create_parser).with(:myenv).returns(@parser) + @interp.send(:parser, :myenv).should equal(@parser) @parser.expects(:reparse?).returns(false) - @interp.send(:parser, :environment).should equal(@parser) + @interp.send(:parser, :myenv).should equal(@parser) end it "should create a new parser when reparse is true" do oldparser = mock('oldparser') newparser = mock('newparser') oldparser.expects(:reparse?).returns(true) oldparser.expects(:clear) - @interp.expects(:create_parser).with(:environment).returns(oldparser) - @interp.send(:parser, :environment).should equal(oldparser) - @interp.expects(:create_parser).with(:environment).returns(newparser) - @interp.send(:parser, :environment).should equal(newparser) + @interp.expects(:create_parser).with(:myenv).returns(oldparser) + @interp.send(:parser, :myenv).should equal(oldparser) + @interp.expects(:create_parser).with(:myenv).returns(newparser) + @interp.send(:parser, :myenv).should equal(newparser) end it "should keep the old parser if create_parser doesn't return anything." do # Get the first parser in the hash. - @interp.expects(:create_parser).with(:environment).returns(@parser) - @interp.send(:parser, :environment).should equal(@parser) + @interp.expects(:create_parser).with(:myenv).returns(@parser) + @interp.send(:parser, :myenv).should equal(@parser) # Have it indicate something has changed @parser.expects(:reparse?).returns(true) # But fail to create a new parser - @interp.expects(:create_parser).with(:environment).returns(nil) + @interp.expects(:create_parser).with(:myenv).returns(nil) # And make sure we still get the old valid parser - @interp.send(:parser, :environment).should equal(@parser) + @interp.send(:parser, :myenv).should equal(@parser) end it "should use different parsers for different environments" do # get one for the first env @interp.expects(:create_parser).with(:first_env).returns(@parser) @interp.send(:parser, :first_env).should equal(@parser) other_parser = mock('otherparser') @interp.expects(:create_parser).with(:second_env).returns(other_parser) @interp.send(:parser, :second_env).should equal(other_parser) end end describe Puppet::Parser::Interpreter, " when compiling configurations" do before do @interp = Puppet::Parser::Interpreter.new end it "should create a configuration with the node, parser, and whether to use ast nodes" do node = mock('node') node.expects(:environment).returns(:myenv) compile = mock 'compile' compile.expects(:compile).returns(:config) parser = mock 'parser' @interp.expects(:parser).with(:myenv).returns(parser) @interp.expects(:usenodes?).returns(true) - Puppet::Parser::Configuration.expects(:new).with(node, parser, :ast_nodes => true).returns(compile) + Puppet::Parser::Compile.expects(:new).with(node, parser, :ast_nodes => true).returns(compile) @interp.compile(node) # Now try it when usenodes is true @interp = Puppet::Parser::Interpreter.new :UseNodes => false node.expects(:environment).returns(:myenv) compile.expects(:compile).returns(:config) @interp.expects(:parser).with(:myenv).returns(parser) @interp.expects(:usenodes?).returns(false) - Puppet::Parser::Configuration.expects(:new).with(node, parser, :ast_nodes => false).returns(compile) + Puppet::Parser::Compile.expects(:new).with(node, parser, :ast_nodes => false).returns(compile) @interp.compile(node).should equal(:config) end end describe Puppet::Parser::Interpreter, " when returning configuration version" do before do @interp = Puppet::Parser::Interpreter.new end it "should ask the appropriate parser for the configuration version" do node = mock 'node' node.expects(:environment).returns(:myenv) parser = mock 'parser' parser.expects(:version).returns(:myvers) @interp.expects(:parser).with(:myenv).returns(parser) @interp.configuration_version(node).should equal(:myvers) end end diff --git a/spec/unit/util/config.rb b/spec/unit/util/config.rb index 7f9b64c94..0b3b65c28 100755 --- a/spec/unit/util/config.rb +++ b/spec/unit/util/config.rb @@ -1,383 +1,395 @@ #!/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 not allow duplicate parameter specifications" 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 call passed blocks when values are set" do values = [] @config.setdefaults(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }}) values.should == [] @config[:hooker] = "something" values.should == %w{something} end it "should munge values using the element-specific methods" do @config[:bool] = "false" @config[:bool].should == false end it "should prefer cli values to values set in Ruby code" do @config.handlearg("--myval", "cliarg") @config[:myval] = "memarg" @config[:myval].should == "cliarg" 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"], :four => ["$two $three FOUR", "d"] end it "should provide a mechanism for returning set values" do @config[:one] = "other" @config[:one].should == "other" end it "should interpolate default values for 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 interpolate default values that themselves need to be interpolated" do @config[:four].should == "ONE TWO ONE ONE TWO THREE FOUR" end it "should interpolate set values for other parameters into returned parameter values" do @config[:one] = "on3" @config[:two] = "$one tw0" @config[:three] = "$one $two thr33" @config[:four] = "$one $two $three f0ur" @config[:one].should == "on3" @config[:two].should == "on3 tw0" @config[:three].should == "on3 on3 tw0 thr33" @config[:four].should == "on3 on3 tw0 on3 on3 tw0 thr33 f0ur" 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 + 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") + @config.stubs(:read_file).with(file).returns(text) + @config.parse(file) + + @config.value(:one, "env1").should == "oneval" + @config.value(:one, "env2").should == "twoval" + end + it "should have a name determined by the 'name' parameter" do @config.setdefaults(:whatever, :name => ["something", "yayness"]) @config.name.should == :something @config[:name] = :other @config.name.should == :other end end describe Puppet::Util::Config, " when choosing which value to return" do before do @config = Puppet::Util::Config.new @config.setdefaults :section, :one => ["ONE", "a"], :name => ["myname", "w"] end it "should return default values if no values have been set" do @config[: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") @config.stubs(:parse_file).returns(text) @config.handlearg("--one", "clival") @config.parse(file) @config[:one].should == "clival" end it "should return values set on the cli before values set in Ruby" do @config[:one] = "rubyval" @config.handlearg("--one", "clival") @config[: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") @config.stubs(:read_file).with(file).returns(text) @config.parse(file) @config[: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") @config.stubs(:read_file).with(file).returns(text) @config.parse(file) @config[: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") @config.stubs(:read_file).with(file).returns(text) @config.parse(file) @config.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") @config.stubs(:read_file).with(file).returns(text) @config.parse(file) @config.value(:one, "env").should == "envval" 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 return values set in the configuration file" do text = "[main] one = fileval " file = "/some/file" @config.expects(:read_file).with(file).returns(text) @config.parse(file) @config[: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" @config.expects(:read_file).with(file).returns(text) lambda { @config.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. @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", "clival") text = "[main]\none = on-disk\n" file = mock 'file' file.stubs(:file).returns("/test/file") @config.stubs(:read_file).with(file).returns(text) @config.parse(file) @config[: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") @config.expects(:read_file).with(file).returns(text) @config.parse(file) @config[:one].should == "disk-init" # Now replace the value text = "[main]\ntwo = disk-replace\n" @config.expects(:read_file).with(file).returns(text) @config.parse(file) #@config.reparse # The originally-overridden value should be replaced with the default @config[:one].should == "ONE" # and we should now have the new value in memory @config[:two].should == "disk-replace" 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/network/handler/configuration.rb b/test/network/handler/configuration.rb index a34952208..072fdc053 100755 --- a/test/network/handler/configuration.rb +++ b/test/network/handler/configuration.rb @@ -1,187 +1,191 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppet/network/handler/configuration' class TestHandlerConfiguration < Test::Unit::TestCase include PuppetTest Config = Puppet::Network::Handler.handler(:configuration) # Check all of the setup stuff. def test_initialize config = nil assert_nothing_raised("Could not create local config") do config = Config.new(:Local => true) end assert(config.local?, "Config is not considered local after being started that way") end # Make sure we create the node handler when necessary. def test_node_handler config = Config.new handler = nil assert_nothing_raised("Could not create node handler") do handler = config.send(:node_handler) end assert_instance_of(Puppet::Network::Handler.handler(:node), handler, "Did not create node handler") # Now make sure we get the same object back again assert_equal(handler.object_id, config.send(:node_handler).object_id, "Did not cache node handler") end # Test creation/returning of the interpreter def test_interpreter config = Config.new # First test the defaults args = {} config.instance_variable_set("@options", args) config.expects(:create_interpreter).with(args).returns(:interp) assert_equal(:interp, config.send(:interpreter), "Did not return the interpreter") # Now run it again and make sure we get the same thing assert_equal(:interp, config.send(:interpreter), "Did not cache the interpreter") end def test_create_interpreter config = Config.new(:Local => false) args = {} # Try it first with defaults. - Puppet::Parser::Interpreter.expects(:new).with(:Local => config.local?, :Manifest => Puppet[:manifest]).returns(:interp) + Puppet::Parser::Interpreter.expects(:new).with(:Local => config.local?).returns(:interp) assert_equal(:interp, config.send(:create_interpreter, args), "Did not return the interpreter") # Now reset it and make sure a specified manifest passes through file = tempfile args[:Manifest] = file Puppet::Parser::Interpreter.expects(:new).with(:Local => config.local?, :Manifest => file).returns(:interp) assert_equal(:interp, config.send(:create_interpreter, args), "Did not return the interpreter") # And make sure the code does, too args.delete(:Manifest) args[:Code] = "yay" Puppet::Parser::Interpreter.expects(:new).with(:Local => config.local?, :Code => "yay").returns(:interp) assert_equal(:interp, config.send(:create_interpreter, args), "Did not return the interpreter") end # Make sure node objects get appropriate data added to them. def test_add_node_data # First with no classes config = Config.new fakenode = Object.new # Set the server facts to something config.instance_variable_set("@server_facts", :facts) fakenode.expects(:fact_merge).with(:facts) config.send(:add_node_data, fakenode) # Now try it with classes. config.instance_variable_set("@options", {:Classes => %w{a b}}) list = [] fakenode = Object.new fakenode.expects(:fact_merge).with(:facts) fakenode.expects(:classes).returns(list).times(2) config.send(:add_node_data, fakenode) assert_equal(%w{a b}, list, "Did not add classes to node") end def test_compile config = Config.new # First do a local - node = Object.new - node.expects(:name).returns(:mynode) + node = mock 'node' + node.stubs(:name).returns(:mynode) + node.stubs(:environment).returns(:myenv) - interp = Object.new + interp = mock 'interpreter' + interp.stubs(:environment) interp.expects(:compile).with(node).returns(:config) config.expects(:interpreter).returns(interp) Puppet.expects(:notice) # The log message from benchmarking assert_equal(:config, config.send(:compile, node), "Did not return config") # Now try it non-local - config = Config.new(:Local => true) - - node = Object.new - node.expects(:name).returns(:mynode) + node = mock 'node' + node.stubs(:name).returns(:mynode) + node.stubs(:environment).returns(:myenv) - interp = Object.new + interp = mock 'interpreter' + interp.stubs(:environment) interp.expects(:compile).with(node).returns(:config) + + config = Config.new(:Local => true) config.expects(:interpreter).returns(interp) assert_equal(:config, config.send(:compile, node), "Did not return config") end def test_set_server_facts config = Config.new assert_nothing_raised("Could not call :set_server_facts") do config.send(:set_server_facts) end facts = config.instance_variable_get("@server_facts") %w{servername serverversion serverip}.each do |fact| assert(facts.include?(fact), "Config did not set %s fact" % fact) end end def test_translate # First do a local config config = Config.new(:Local => true) assert_equal(:plain, config.send(:translate, :plain), "Attempted to translate local config") # Now a non-local config = Config.new(:Local => false) obj = Object.new yamld = Object.new obj.expects(:to_yaml).with(:UseBlock => true).returns(yamld) CGI.expects(:escape).with(yamld).returns(:translated) assert_equal(:translated, config.send(:translate, obj), "Did not return translated config") end # Check that we're storing the node freshness into the rails db. Hackilicious. def test_update_node_check # This is stupid. config = Config.new node = Object.new node.expects(:name).returns(:hostname) now = Object.new Time.expects(:now).returns(now) host = Object.new host.expects(:last_freshcheck=).with(now) host.expects(:save) # Only test the case where rails is there Puppet[:storeconfigs] = true Puppet.features.expects(:rails?).returns(true) Puppet::Rails.expects(:connect) Puppet::Rails::Host.expects(:find_or_create_by_name).with(:hostname).returns(host) config.send(:update_node_check, node) end def test_version # First try the case where we can't look up the node config = Config.new handler = Object.new handler.expects(:details).with(:client).returns(false) config.expects(:node_handler).returns(handler) interp = Object.new assert_instance_of(Bignum, config.version(:client), "Did not return configuration version") # And then when we find the node. config = Config.new node = Object.new handler = Object.new handler.expects(:details).with(:client).returns(node) config.expects(:update_node_check).with(node) config.expects(:node_handler).returns(handler) interp = Object.new interp.expects(:configuration_version).returns(:version) config.expects(:interpreter).returns(interp) assert_equal(:version, config.version(:client), "Did not return configuration version") end end diff --git a/test/network/handler/node.rb b/test/network/handler/node.rb index f7cbf6017..6b8ab9290 100755 --- a/test/network/handler/node.rb +++ b/test/network/handler/node.rb @@ -1,640 +1,640 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'mocha' require 'puppettest' require 'puppettest/resourcetesting' require 'puppettest/parsertesting' require 'puppettest/servertest' require 'puppet/network/handler/node' module NodeTesting include PuppetTest Node = Puppet::Network::Handler::Node SimpleNode = Puppet::Node def mk_node_mapper # First, make sure our nodesearch command works as we expect # Make a nodemapper mapper = tempfile() ruby = %x{which ruby}.chomp File.open(mapper, "w") { |f| f.puts "#!#{ruby} require 'yaml' name = ARGV.last.chomp result = {} if name =~ /a/ result[:parameters] = {'one' => ARGV.last + '1', 'two' => ARGV.last + '2'} end if name =~ /p/ result['classes'] = [1,2,3].collect { |n| ARGV.last + n.to_s } end puts YAML.dump(result) " } File.chmod(0755, mapper) mapper end def mk_searcher(name) searcher = Object.new searcher.extend(Node.node_source(name)) searcher.meta_def(:newnode) do |name, *args| SimpleNode.new(name, *args) end searcher end def mk_node_source @node_info = {} @node_source = Node.newnode_source(:testing, :fact_merge => true) do def nodesearch(key) if info = @node_info[key] SimpleNode.new(info) else nil end end end Puppet[:node_source] = "testing" cleanup { Node.rm_node_source(:testing) } end end class TestNodeHandler < Test::Unit::TestCase include NodeTesting def setup super mk_node_source end # Make sure that the handler includes the appropriate # node source. def test_initialize # First try it when passing in the node source handler = nil assert_nothing_raised("Could not specify a node source") do handler = Node.new(:Source => :testing) end assert(handler.metaclass.included_modules.include?(@node_source), "Handler did not include node source") # Now use the Puppet[:node_source] Puppet[:node_source] = "testing" assert_nothing_raised("Could not specify a node source") do handler = Node.new() end assert(handler.metaclass.included_modules.include?(@node_source), "Handler did not include node source") # And make sure we throw an exception when an invalid node source is used assert_raise(ArgumentError, "Accepted an invalid node source") do handler = Node.new(:Source => "invalid") end end # Make sure we can find and we cache a fact handler. def test_fact_handler handler = Node.new fhandler = nil assert_nothing_raised("Could not retrieve the fact handler") do fhandler = handler.send(:fact_handler) end assert_instance_of(Puppet::Network::Handler::Facts, fhandler, "Did not get a fact handler back") # Now call it again, making sure we're caching the value. fhandler2 = nil assert_nothing_raised("Could not retrieve the fact handler") do fhandler2 = handler.send(:fact_handler) end assert_instance_of(Puppet::Network::Handler::Facts, fhandler2, "Did not get a fact handler on the second run") assert_equal(fhandler.object_id, fhandler2.object_id, "Did not cache fact handler") end # Make sure we can get node facts from the fact handler. def test_node_facts # Check the case where we find the node. handler = Node.new fhandler = handler.send(:fact_handler) fhandler.expects(:get).with("present").returns("a" => "b") result = nil assert_nothing_raised("Could not get facts from fact handler") do result = handler.send(:node_facts, "present") end assert_equal({"a" => "b"}, result, "Did not get correct facts back") # Now try the case where the fact handler knows nothing about our host fhandler.expects(:get).with('missing').returns(nil) result = nil assert_nothing_raised("Could not get facts from fact handler when host is missing") do result = handler.send(:node_facts, "missing") end assert_equal({}, result, "Did not get empty hash when no facts are known") end # Test our simple shorthand def test_newnode SimpleNode.expects(:new).with("stuff") handler = Node.new handler.send(:newnode, "stuff") end # Make sure we can build up the correct node names to search for def test_node_names handler = Node.new # Verify that the handler asks for the facts if we don't pass them in handler.expects(:node_facts).with("testing").returns({}) handler.send(:node_names, "testing") handler = Node.new # Test it first with no parameters assert_equal(%w{testing}, handler.send(:node_names, "testing"), "Node names did not default to an array including just the node name") # Now test it with a fully qualified name assert_equal(%w{testing.domain.com testing}, handler.send(:node_names, "testing.domain.com"), "Fully qualified names did not get turned into multiple names, longest first") # And try it with a short name + domain fact assert_equal(%w{testing host.domain.com host}, handler.send(:node_names, "testing", "domain" => "domain.com", "hostname" => "host"), "The domain fact was not used to build up an fqdn") # And with an fqdn assert_equal(%w{testing host.domain.com host}, handler.send(:node_names, "testing", "fqdn" => "host.domain.com"), "The fqdn was not used") # And make sure the fqdn beats the domain assert_equal(%w{testing host.other.com host}, handler.send(:node_names, "testing", "domain" => "domain.com", "fqdn" => "host.other.com"), "The domain was used in preference to the fqdn") end # Make sure we can retrieve a whole node by name. def test_details_when_we_find_nodes handler = Node.new # Make sure we get the facts first handler.expects(:node_facts).with("host").returns(:facts) # Find the node names handler.expects(:node_names).with("host", :facts).returns(%w{a b c}) # Iterate across them handler.expects(:nodesearch).with("a").returns(nil) handler.expects(:nodesearch).with("b").returns(nil) # Create an example node to return node = SimpleNode.new("host") # Make sure its source is set node.expects(:source=).with(handler.source) # And that the names are retained node.expects(:names=).with(%w{a b c}) # And make sure we actually get it back handler.expects(:nodesearch).with("c").returns(node) handler.expects(:fact_merge?).returns(true) # Make sure we merge the facts with the node's parameters. node.expects(:fact_merge).with(:facts) # Now call the method result = nil assert_nothing_raised("could not call 'details'") do result = handler.details("host") end assert_equal(node, result, "Did not get correct node back") end # But make sure we pass through to creating default nodes when appropriate. def test_details_using_default_node handler = Node.new # Make sure we get the facts first handler.expects(:node_facts).with("host").returns(:facts) # Find the node names handler.expects(:node_names).with("host", :facts).returns([]) # Create an example node to return node = SimpleNode.new("host") # Make sure its source is set node.expects(:source=).with(handler.source) # And make sure we actually get it back handler.expects(:nodesearch).with("default").returns(node) # This time, have it return false handler.expects(:fact_merge?).returns(false) # And because fact_merge was false, we don't merge them. node.expects(:fact_merge).never # Now call the method result = nil assert_nothing_raised("could not call 'details'") do result = handler.details("host") end assert_equal(node, result, "Did not get correct node back") end # Make sure our handler behaves rationally when it comes to getting environment data. def test_environment # What happens when we can't find the node handler = Node.new handler.expects(:details).with("fake").returns(nil) result = nil assert_nothing_raised("Could not call 'Node.environment'") do result = handler.environment("fake") end assert_nil(result, "Got an environment for a node we could not find") # Now for nodes we can find handler = Node.new node = SimpleNode.new("fake") handler.expects(:details).with("fake").returns(node) node.expects(:environment).returns("dev") result = nil assert_nothing_raised("Could not call 'Node.environment'") do result = handler.environment("fake") end assert_equal("dev", result, "Did not get environment back") end # Make sure our handler behaves rationally when it comes to getting parameter data. def test_parameters # What happens when we can't find the node handler = Node.new handler.expects(:details).with("fake").returns(nil) result = nil assert_nothing_raised("Could not call 'Node.parameters'") do result = handler.parameters("fake") end assert_nil(result, "Got parameters for a node we could not find") # Now for nodes we can find handler = Node.new node = SimpleNode.new("fake") handler.expects(:details).with("fake").returns(node) node.expects(:parameters).returns({"a" => "b"}) result = nil assert_nothing_raised("Could not call 'Node.parameters'") do result = handler.parameters("fake") end assert_equal({"a" => "b"}, result, "Did not get parameters back") end def test_classes # What happens when we can't find the node handler = Node.new handler.expects(:details).with("fake").returns(nil) result = nil assert_nothing_raised("Could not call 'Node.classes'") do result = handler.classes("fake") end assert_nil(result, "Got classes for a node we could not find") # Now for nodes we can find handler = Node.new node = SimpleNode.new("fake") handler.expects(:details).with("fake").returns(node) node.expects(:classes).returns(%w{yay foo}) result = nil assert_nothing_raised("Could not call 'Node.classes'") do result = handler.classes("fake") end assert_equal(%w{yay foo}, result, "Did not get classes back") end # We reuse the filetimeout for the node caching timeout. def test_node_caching handler = Node.new node = Object.new node.metaclass.instance_eval do attr_accessor :time, :name end node.time = Time.now node.name = "yay" # Make sure caching works normally assert_nothing_raised("Could not cache node") do handler.send(:cache, node) end assert_equal(node.object_id, handler.send(:cached?, "yay").object_id, "Did not get node back from the cache") # And that it's returned if we ask for it, instead of creating a new node. assert_equal(node.object_id, handler.details("yay").object_id, "Did not use cached node") # Now set the node's time to be a long time ago node.time = Time.now - 50000 assert(! handler.send(:cached?, "yay"), "Timed-out node was returned from cache") end end # Test our configuration object. class TestNodeSources < Test::Unit::TestCase include NodeTesting def test_node_sources mod = nil assert_nothing_raised("Could not add new search type") do mod = Node.newnode_source(:testing) do def nodesearch(name) end end end assert_equal(mod, Node.node_source(:testing), "Did not get node_source back") cleanup do Node.rm_node_source(:testing) assert(! Node.const_defined?("Testing"), "Did not remove constant") end end def test_external_node_source source = Node.node_source(:external) assert(source, "Could not find external node source") mapper = mk_node_mapper searcher = mk_searcher(:external) assert(searcher.fact_merge?, "External node source does not merge facts") # Make sure it gives the right response assert_equal({'classes' => %w{apple1 apple2 apple3}, :parameters => {"one" => "apple1", "two" => "apple2"}}, YAML.load(%x{#{mapper} apple})) # First make sure we get nil back by default assert_nothing_raised { assert_nil(searcher.nodesearch("apple"), "Interp#nodesearch_external defaulted to a non-nil response") } assert_nothing_raised { Puppet[:external_nodes] = mapper } node = nil # Both 'a' and 'p', so we get classes and parameters assert_nothing_raised { node = searcher.nodesearch("apple") } assert_equal("apple", node.name, "node name was not set correctly for apple") assert_equal(%w{apple1 apple2 apple3}, node.classes, "node classes were not set correctly for apple") assert_equal( {"one" => "apple1", "two" => "apple2"}, node.parameters, "node parameters were not set correctly for apple") # A 'p' but no 'a', so we only get classes assert_nothing_raised { node = searcher.nodesearch("plum") } assert_equal("plum", node.name, "node name was not set correctly for plum") assert_equal(%w{plum1 plum2 plum3}, node.classes, "node classes were not set correctly for plum") assert_equal({}, node.parameters, "node parameters were not set correctly for plum") # An 'a' but no 'p', so we only get parameters. assert_nothing_raised { node = searcher.nodesearch("guava")} # no p's, thus no classes assert_equal("guava", node.name, "node name was not set correctly for guava") assert_equal([], node.classes, "node classes were not set correctly for guava") assert_equal({"one" => "guava1", "two" => "guava2"}, node.parameters, "node parameters were not set correctly for guava") assert_nothing_raised { node = searcher.nodesearch("honeydew")} # neither, thus nil assert_nil(node) end # Make sure a nodesearch with arguments works def test_nodesearch_external_arguments mapper = mk_node_mapper Puppet[:external_nodes] = "#{mapper} -s something -p somethingelse" searcher = mk_searcher(:external) node = nil assert_nothing_raised do node = searcher.nodesearch("apple") end assert_instance_of(SimpleNode, node, "did not create node") end # A wrapper test, to make sure we're correctly calling the external search method. def test_nodesearch_external_functional mapper = mk_node_mapper searcher = mk_searcher(:external) Puppet[:external_nodes] = mapper node = nil assert_nothing_raised do node = searcher.nodesearch("apple") end assert_instance_of(SimpleNode, node, "did not create node") end # This can stay in the main test suite because it doesn't actually use ldapsearch, # it just overrides the method so it behaves as though it were hitting ldap. def test_ldap_nodesearch source = Node.node_source(:ldap) assert(source, "Could not find ldap node source") searcher = mk_searcher(:ldap) assert(searcher.fact_merge?, "LDAP node source does not merge facts") nodetable = {} # Override the ldapsearch definition, so we don't have to actually set it up. searcher.meta_def(:ldapsearch) do |name| nodetable[name] end # Make sure we get nothing for nonexistent hosts node = nil assert_nothing_raised do node = searcher.nodesearch("nosuchhost") end assert_nil(node, "Got a node for a non-existent host") # Now add a base node with some classes and parameters nodetable["base"] = [nil, %w{one two}, {"base" => "true"}] assert_nothing_raised do node = searcher.nodesearch("base") end assert_instance_of(SimpleNode, node, "Did not get node from ldap nodesearch") assert_equal("base", node.name, "node name was not set") assert_equal(%w{one two}, node.classes, "node classes were not set") assert_equal({"base" => "true"}, node.parameters, "node parameters were not set") # Now use a different with this as the base nodetable["middle"] = ["base", %w{three}, {"center" => "boo"}] assert_nothing_raised do node = searcher.nodesearch("middle") end assert_instance_of(SimpleNode, node, "Did not get node from ldap nodesearch") assert_equal("middle", node.name, "node name was not set") assert_equal(%w{one two three}.sort, node.classes.sort, "node classes were not set correctly with a parent node") assert_equal({"base" => "true", "center" => "boo"}, node.parameters, "node parameters were not set correctly with a parent node") # And one further, to make sure we fully recurse nodetable["top"] = ["middle", %w{four five}, {"master" => "far"}] assert_nothing_raised do node = searcher.nodesearch("top") end assert_instance_of(SimpleNode, node, "Did not get node from ldap nodesearch") assert_equal("top", node.name, "node name was not set") assert_equal(%w{one two three four five}.sort, node.classes.sort, "node classes were not set correctly with the top node") assert_equal({"base" => "true", "center" => "boo", "master" => "far"}, node.parameters, "node parameters were not set correctly with the top node") end # Make sure we always get a node back from the 'none' nodesource. def test_nodesource_none source = Node.node_source(:none) assert(source, "Could not find 'none' node source") searcher = mk_searcher(:none) assert(searcher.fact_merge?, "'none' node source does not merge facts") # Run a couple of node names through it node = nil %w{192.168.0.1 0:0:0:3:a:f host host.domain.com}.each do |name| assert_nothing_raised("Could not create an empty node with name '%s'" % name) do node = searcher.nodesearch(name) end assert_instance_of(SimpleNode, node, "Did not get a simple node back for %s" % name) assert_equal(name, node.name, "Name was not set correctly") end end end class LdapNodeTest < PuppetTest::TestCase include NodeTesting include PuppetTest::ServerTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting AST = Puppet::Parser::AST confine "LDAP is not available" => Puppet.features.ldap? confine "No LDAP test data for networks other than Luke's" => Facter.value(:domain) == "madstop.com" def ldapconnect @ldap = LDAP::Conn.new("ldap", 389) @ldap.set_option( LDAP::LDAP_OPT_PROTOCOL_VERSION, 3 ) @ldap.simple_bind("", "") return @ldap end def ldaphost(name) - node = NodeDef.new(:name => name) + node = Puppet::Node.new(name) parent = nil found = false @ldap.search( "ou=hosts, dc=madstop, dc=com", 2, "(&(objectclass=puppetclient)(cn=%s))" % name ) do |entry| node.classes = entry.vals("puppetclass") || [] node.parameters = entry.to_hash.inject({}) do |hash, ary| if ary[1].length == 1 hash[ary[0]] = ary[1].shift else hash[ary[0]] = ary[1] end hash end parent = node.parameters["parentnode"] found = true end raise "Could not find node %s" % name unless found return node, parent end def test_ldapsearch Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com" Puppet[:ldapnodes] = true searcher = Object.new searcher.extend(Node.node_source(:ldap)) ldapconnect() # Make sure we get nil and nil back when we search for something missing parent, classes, parameters = nil assert_nothing_raised do parent, classes, parameters = searcher.ldapsearch("nosuchhost") end assert_nil(parent, "Got a parent for a non-existent host") assert_nil(classes, "Got classes for a non-existent host") # Make sure we can find 'culain' in ldap assert_nothing_raised do parent, classes, parameters = searcher.ldapsearch("culain") end node, realparent = ldaphost("culain") assert_equal(realparent, parent, "did not get correct parent node from ldap") assert_equal(node.classes, classes, "did not get correct ldap classes from ldap") assert_equal(node.parameters, parameters, "did not get correct ldap parameters from ldap") # Now compare when we specify the attributes to get. Puppet[:ldapattrs] = "cn" assert_nothing_raised do parent, classes, parameters = searcher.ldapsearch("culain") end assert_equal(realparent, parent, "did not get correct parent node from ldap") assert_equal(node.classes, classes, "did not get correct ldap classes from ldap") list = %w{cn puppetclass parentnode dn} should = node.parameters.inject({}) { |h, a| h[a[0]] = a[1] if list.include?(a[0]); h } assert_equal(should, parameters, "did not get correct ldap parameters from ldap") end end class LdapReconnectTests < PuppetTest::TestCase include NodeTesting include PuppetTest::ServerTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting AST = Puppet::Parser::AST confine "Not running on culain as root" => (Puppet::Util::SUIDManager.uid == 0 and Facter.value("hostname") == "culain") def test_ldapreconnect Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com" Puppet[:ldapnodes] = true searcher = Object.new searcher.extend(Node.node_source(:ldap)) hostname = "culain.madstop.com" # look for our host assert_nothing_raised { parent, classes = searcher.nodesearch(hostname) } # Now restart ldap system("/etc/init.d/slapd restart 2>/dev/null >/dev/null") sleep(1) # and look again assert_nothing_raised { parent, classes = searcher.nodesearch(hostname) } # Now stop ldap system("/etc/init.d/slapd stop 2>/dev/null >/dev/null") cleanup do system("/etc/init.d/slapd start 2>/dev/null >/dev/null") end # And make sure we actually fail here assert_raise(Puppet::Error) { parent, classes = searcher.nodesearch(hostname) } end end