diff --git a/lib/puppet/fact_stores/yaml.rb b/lib/puppet/fact_stores/yaml.rb index b4b0c0e7c..a4b12a2e5 100644 --- a/lib/puppet/fact_stores/yaml.rb +++ b/lib/puppet/fact_stores/yaml.rb @@ -1,42 +1,40 @@ Puppet::Util::FactStore.newstore(:yaml) do desc "Store client facts as flat files, serialized using YAML." # Get a client's facts. def get(node) file = path(node) return nil unless FileTest.exists?(file) begin facts = YAML::load(File.read(file)) rescue => detail Puppet.err "Could not load facts for %s: %s" % [node, detail] end facts end def initialize Puppet.config.use(:yamlfacts) end # Store the facts to disk. def set(node, facts) File.open(path(node), "w", 0600) do |f| begin f.print YAML::dump(facts) rescue => detail Puppet.err "Could not write facts for %s: %s" % [node, detail] end end nil end private # Return the path to a given node's file. def path(node) File.join(Puppet[:yamlfactdir], node + ".yaml") end end - -# $Id$ diff --git a/lib/puppet/network/client/master.rb b/lib/puppet/network/client/master.rb index 0286ab87b..b9f77f61d 100644 --- a/lib/puppet/network/client/master.rb +++ b/lib/puppet/network/client/master.rb @@ -1,698 +1,698 @@ # The client for interacting with the puppetmaster config server. require 'sync' require 'timeout' class Puppet::Network::Client::Master < Puppet::Network::Client unless defined? @@sync @@sync = Sync.new end attr_accessor :objects attr_reader :compile_time class << self # Puppetd should only have one instance running, and we need a way # to retrieve it. attr_accessor :instance include Puppet::Util end def self.facts # Retrieve the facts from the central server. if Puppet[:factsync] self.getfacts() end down = Puppet[:downcasefacts] facts = {} Facter.each { |name,fact| if down facts[name] = fact.to_s.downcase else facts[name] = fact.to_s end } # Add our client version to the list of facts, so people can use it # in their manifests facts["clientversion"] = Puppet.version.to_s # And add our environment as a fact. facts["environment"] = Puppet[:environment] facts end # Return the list of dynamic facts as an array of symbols def self.dynamic_facts Puppet.config[:dynamicfacts].split(/\s*,\s*/).collect { |fact| fact.downcase } end # This method actually applies the configuration. def apply(tags = nil, ignoreschedules = false) unless defined? @objects raise Puppet::Error, "Cannot apply; objects not defined" end transaction = @objects.evaluate if tags transaction.tags = tags end if ignoreschedules transaction.ignoreschedules = true end transaction.addtimes :config_retrieval => @configtime begin transaction.evaluate rescue Puppet::Error => detail Puppet.err "Could not apply complete configuration: %s" % detail rescue => detail Puppet.err "Got an uncaught exception of type %s: %s" % [detail.class, detail] if Puppet[:trace] puts detail.backtrace end ensure Puppet::Util::Storage.store end if Puppet[:report] or Puppet[:summarize] report(transaction) end return transaction ensure if defined? transaction and transaction transaction.cleanup end end # Cache the config def cache(text) Puppet.info "Caching configuration at %s" % self.cachefile confdir = ::File.dirname(Puppet[:localconfig]) ::File.open(self.cachefile + ".tmp", "w", 0660) { |f| f.print text } ::File.rename(self.cachefile + ".tmp", self.cachefile) end def cachefile unless defined? @cachefile @cachefile = Puppet[:localconfig] + ".yaml" end @cachefile end def clear @objects.remove(true) if @objects Puppet::Type.allclear mkdefault_objects @objects = nil end # Initialize and load storage def dostorage begin Puppet::Util::Storage.load @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time] rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Corrupt state file %s: %s" % [Puppet[:statefile], detail] begin ::File.unlink(Puppet[:statefile]) retry rescue => detail raise Puppet::Error.new("Cannot remove %s: %s" % [Puppet[:statefile], detail]) end end end # Check whether our configuration is up to date def fresh?(facts) if Puppet[:ignorecache] Puppet.notice "Ignoring cache" return false end unless self.compile_time Puppet.debug "No cached compile time" return false end if facts_changed?(facts) Puppet.info "Facts have changed; recompiling" unless local? return false end - # We're willing to give a 2 second drift newcompile = @driver.freshness + # We're willing to give a 2 second drift if newcompile - @compile_time.to_i < 1 return true else - Puppet.debug "Server compile time is %s vs %s" % [newcompile, @compile_time] + Puppet.debug "Server compile time is %s vs %s" % [newcompile, @compile_time.to_i] return false end end # Let the daemon run again, freely in the filesystem. Frolick, little # daemon! def enable lockfile.unlock(:anonymous => true) end # Stop the daemon from making any configuration runs. def disable lockfile.lock(:anonymous => true) end # Retrieve the config from a remote server. If this fails, then # use the cached copy. def getconfig dostorage() facts = nil Puppet::Util.benchmark(:debug, "Retrieved facts") do facts = self.class.facts end if self.objects or FileTest.exists?(self.cachefile) if self.fresh?(facts) Puppet.info "Config is up to date" if self.objects return end if oldtext = self.retrievecache begin @objects = YAML.load(oldtext).to_type rescue => detail Puppet.warning "Could not load cached configuration: %s" % detail end return end end end Puppet.debug("getting config") # Retrieve the plugins. if Puppet[:pluginsync] getplugins() end unless facts.length > 0 raise Puppet::Network::ClientError.new( "Could not retrieve any facts" ) end unless objects = get_actual_config(facts) @objects = nil return end unless objects.is_a?(Puppet::TransBucket) raise NetworkClientError, "Invalid returned objects of type %s" % objects.class end self.setclasses(objects.classes) # Clear all existing objects, so we can recreate our stack. if self.objects clear() end # Now convert the objects to real Puppet objects @objects = objects.to_type if @objects.nil? raise Puppet::Error, "Configuration could not be processed" end # and perform any necessary final actions before we evaluate. @objects.finalize return @objects end # A simple proxy method, so it's easy to test. def getplugins self.class.getplugins end # Just so we can specify that we are "the" instance. def initialize(*args) Puppet.config.use(:main, :ssl, :puppetd) super # This might be nil @configtime = 0 self.class.instance = self @running = false mkdefault_objects end # Make the default objects necessary for function. def mkdefault_objects # First create the default scheduling objects Puppet::Type.type(:schedule).mkdefaultschedules # And filebuckets Puppet::Type.type(:filebucket).mkdefaultbucket end # Mark that we should restart. The Puppet module checks whether we're running, # so this only gets called if we're in the middle of a run. def restart # If we're currently running, then just mark for later Puppet.notice "Received signal to restart; waiting until run is complete" @restart = true end # Should we restart? def restart? if defined? @restart @restart else false end end # Retrieve the cached config def retrievecache if FileTest.exists?(self.cachefile) return ::File.read(self.cachefile) else return nil end end # The code that actually runs the configuration. def run(tags = nil, ignoreschedules = false) got_lock = false splay Puppet::Util.sync(:puppetrun).synchronize(Sync::EX) do if !lockfile.lock Puppet.notice "Lock file %s exists; skipping configuration run" % lockfile.lockfile else got_lock = true begin @configtime = thinmark do self.getconfig end rescue => detail Puppet.err "Could not retrieve configuration: %s" % detail end if defined? @objects and @objects unless @local Puppet.notice "Starting configuration run" end benchmark(:notice, "Finished configuration run") do self.apply(tags, ignoreschedules) end end end lockfile.unlock # Did we get HUPped during the run? If so, then restart now that we're # done with the run. if self.restart? Process.kill(:HUP, $$) end end ensure # Just make sure we remove the lock file if we set it. lockfile.unlock if got_lock and lockfile.locked? clear() end def running? lockfile.locked? end # Store the classes in the classfile, but only if we're not local. def setclasses(ary) if @local return end unless ary and ary.length > 0 Puppet.info "No classes to store" return end begin ::File.open(Puppet[:classfile], "w") { |f| f.puts ary.join("\n") } rescue => detail Puppet.err "Could not create class file %s: %s" % [Puppet[:classfile], detail] end end private # Download files from the remote server, returning a list of all # changed files. def self.download(args) objects = Puppet::Type.type(:component).create( :name => "#{args[:name]}_collector" ) hash = { :path => args[:dest], :recurse => true, :source => args[:source], :tag => "#{args[:name]}s", :owner => Process.uid, :group => Process.gid, :purge => true, :backup => false } if args[:ignore] hash[:ignore] = args[:ignore].split(/\s+/) end objects.push Puppet::Type.type(:file).create(hash) Puppet.info "Retrieving #{args[:name]}s" noop = Puppet[:noop] Puppet[:noop] = false begin trans = objects.evaluate trans.ignoretags = true Timeout::timeout(self.timeout) do trans.evaluate end rescue Puppet::Error, Timeout::Error => detail if Puppet[:debug] puts detail.backtrace end Puppet.err "Could not retrieve #{args[:name]}s: %s" % detail end # Now source all of the changed objects, but only source those # that are top-level. files = [] trans.changed?.find_all do |object| yield object if block_given? files << object[:path] end trans.cleanup # Now clean up after ourselves objects.remove files ensure # I can't imagine why this is necessary, but apparently at last one person has had problems with noop # being nil here. if noop.nil? Puppet[:noop] = false else Puppet[:noop] = noop end end # Retrieve facts from the central server. def self.getfacts # Clear all existing definitions. Facter.clear # Download the new facts path = Puppet[:factpath].split(":") files = [] download(:dest => Puppet[:factdest], :source => Puppet[:factsource], :ignore => Puppet[:factsignore], :name => "fact") do |object| next unless path.include?(::File.dirname(object[:path])) files << object[:path] end ensure # Reload everything. if Facter.respond_to? :loadfacts Facter.loadfacts elsif Facter.respond_to? :load Facter.load else raise Puppet::Error, "You must upgrade your version of Facter to use centralized facts" end # This loads all existing facts and any new ones. We have to remove and # reload because there's no way to unload specific facts. loadfacts() end # Retrieve the plugins from the central server. We only have to load the # changed plugins, because Puppet::Type loads plugins on demand. def self.getplugins download(:dest => Puppet[:plugindest], :source => Puppet[:pluginsource], :ignore => Puppet[:pluginsignore], :name => "plugin") do |object| next if FileTest.directory?(object[:path]) path = object[:path].sub(Puppet[:plugindest], '').sub(/^\/+/, '') unless Puppet::Util::Autoload.loaded?(path) next end begin Puppet.info "Reloading downloaded file %s" % path load object[:path] rescue => detail Puppet.warning "Could not reload downloaded file %s: %s" % [object[:path], detail] end end end def self.loaddir(dir, type) return unless FileTest.directory?(dir) Dir.entries(dir).find_all { |e| e =~ /\.rb$/ }.each do |file| fqfile = ::File.join(dir, file) begin Puppet.info "Loading #{type} %s" % ::File.basename(file.sub(".rb",'')) Timeout::timeout(self.timeout) do load fqfile end rescue => detail Puppet.warning "Could not load #{type} %s: %s" % [fqfile, detail] end end end def self.loadfacts Puppet[:factpath].split(":").each do |dir| loaddir(dir, "fact") end end def self.timeout timeout = Puppet[:configtimeout] case timeout when String: if timeout =~ /^\d+$/ timeout = Integer(timeout) else raise ArgumentError, "Configuration timeout must be an integer" end when Integer: # nothing else raise ArgumentError, "Configuration timeout must be an integer" end return timeout end # Send off the transaction report. def report(transaction) begin report = transaction.generate_report() rescue => detail Puppet.err "Could not generate report: %s" % detail return end if Puppet[:rrdgraph] == true report.graph() end if Puppet[:summarize] puts report.summary end if Puppet[:report] begin reportclient().report(report) rescue => detail Puppet.err "Reporting failed: %s" % detail end end end def reportclient unless defined? @reportclient @reportclient = Puppet::Network::Client.report.new( :Server => Puppet[:reportserver] ) end @reportclient end loadfacts() # Have the facts changed since we last compiled? def facts_changed?(facts) oldfacts = (Puppet::Util::Storage.cache(:configuration)[:facts] || {}).dup newfacts = facts.dup self.class.dynamic_facts.each do |fact| [oldfacts, newfacts].each do |facthash| facthash.delete(fact) if facthash.include?(fact) end end if oldfacts == newfacts return false else # unless oldfacts # puts "no old facts" # return true # end # newfacts.keys.each do |k| # unless newfacts[k] == oldfacts[k] # puts "%s: %s vs %s" % [k, newfacts[k], oldfacts[k]] # end # end return true end end # Actually retrieve the configuration, either from the server or from a # local master. def get_actual_config(facts) if @local return get_local_config(facts) else begin Timeout::timeout(self.class.timeout) do return get_remote_config(facts) end rescue Timeout::Error Puppet.err "Configuration retrieval timed out" return nil end end end # Retrieve a configuration from a local master. def get_local_config(facts) # If we're local, we don't have to do any of the conversion # stuff. objects = @driver.getconfig(facts, "yaml") @compile_time = Time.now if objects == "" raise Puppet::Error, "Could not retrieve configuration" end return objects end # Retrieve a config from a remote master. def get_remote_config(facts) textobjects = "" textfacts = CGI.escape(YAML.dump(facts)) benchmark(:debug, "Retrieved configuration") do # error handling for this is done in the network client begin textobjects = @driver.getconfig(textfacts, "yaml") begin textobjects = CGI.unescape(textobjects) rescue => detail raise Puppet::Error, "Could not CGI.unescape configuration" end rescue => detail Puppet.err "Could not retrieve configuration: %s" % detail unless Puppet[:usecacheonfailure] @objects = nil Puppet.warning "Not using cache on failed configuration" return end end end fromcache = false if textobjects == "" unless textobjects = self.retrievecache raise Puppet::Error.new( "Cannot connect to server and there is no cached configuration" ) end Puppet.warning "Could not get config; using cached copy" fromcache = true else @compile_time = Time.now Puppet::Util::Storage.cache(:configuration)[:facts] = facts Puppet::Util::Storage.cache(:configuration)[:compile_time] = @compile_time end begin objects = YAML.load(textobjects) rescue => detail raise Puppet::Error, "Could not understand configuration: %s" % detail.to_s end if @cache and ! fromcache self.cache(textobjects) end return objects end def lockfile unless defined?(@lockfile) @lockfile = Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile]) end @lockfile end # Sleep when splay is enabled; else just return. def splay return unless Puppet[:splay] limit = Integer(Puppet[:splaylimit]) # Pick a splay time and then cache it. unless time = Puppet::Util::Storage.cache(:configuration)[:splay_time] time = rand(limit) Puppet::Util::Storage.cache(:configuration)[:splay_time] = time end Puppet.info "Sleeping for %s seconds (splay is enabled)" % time sleep(time) end end # $Id$ diff --git a/lib/puppet/network/handler/configuration.rb b/lib/puppet/network/handler/configuration.rb index a1b22207e..fd1ee86ed 100644 --- a/lib/puppet/network/handler/configuration.rb +++ b/lib/puppet/network/handler/configuration.rb @@ -1,213 +1,209 @@ 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 - if node = node_handler.details(client) - update_node_check(node) - return interpreter.configuration_version(node) - else - raise Puppet::Error, "Could not find node '%s'" % client - end + 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 0 + 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. 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] 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).new + @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/network/handler/fileserver.rb b/lib/puppet/network/handler/fileserver.rb index fdf515d6e..a429412d2 100755 --- a/lib/puppet/network/handler/fileserver.rb +++ b/lib/puppet/network/handler/fileserver.rb @@ -1,660 +1,684 @@ require 'puppet' require 'puppet/network/authstore' require 'webrick/httpstatus' require 'cgi' require 'delegate' require 'sync' class Puppet::Network::Handler AuthStoreError = Puppet::AuthStoreError class FileServerError < Puppet::Error; end class FileServer < Handler desc "The interface to Puppet's fileserving abilities." attr_accessor :local CHECKPARAMS = [:mode, :type, :owner, :group, :checksum] # Special filserver module for puppet's module system MODULES = "modules" @interface = XMLRPC::Service::Interface.new("fileserver") { |iface| iface.add_method("string describe(string, string)") iface.add_method("string list(string, string, boolean, array)") iface.add_method("string retrieve(string, string)") } def self.params CHECKPARAMS.dup end # Describe a given file. This returns all of the manageable aspects # of that file. def describe(url, links = :ignore, client = nil, clientip = nil) links = links.intern if links.is_a? String if links == :manage raise Puppet::Network::Handler::FileServerError, "Cannot currently copy links" end mount, path = convert(url, client, clientip) if client mount.debug "Describing %s for %s" % [url, client] end obj = nil unless obj = mount.getfileobject(path, links) return "" end currentvalues = mount.check(obj) desc = [] CHECKPARAMS.each { |check| if value = currentvalues[check] desc << value else if check == "checksum" and currentvalues[:type] == "file" mount.notice "File %s does not have data for %s" % [obj.name, check] end desc << nil end } return desc.join("\t") end # Create a new fileserving module. def initialize(hash = {}) @mounts = {} @files = {} if hash[:Local] @local = hash[:Local] else @local = false end if hash[:Config] == false @noreadconfig = true else @config = Puppet::Util::LoadedFile.new( hash[:Config] || Puppet[:fileserverconfig] ) @noreadconfig = false end if hash.include?(:Mount) @passedconfig = true unless hash[:Mount].is_a?(Hash) raise Puppet::DevError, "Invalid mount hash %s" % hash[:Mount].inspect end hash[:Mount].each { |dir, name| if FileTest.exists?(dir) self.mount(dir, name) end } self.mount(nil, MODULES) else @passedconfig = false readconfig(false) # don't check the file the first time. end end # List a specific directory's contents. def list(url, links = :ignore, recurse = false, ignore = false, client = nil, clientip = nil) mount, path = convert(url, client, clientip) if client mount.debug "Listing %s for %s" % [url, client] end obj = nil unless FileTest.exists?(path) return "" end # We pass two paths here, but reclist internally changes one # of the arguments when called internally. desc = reclist(mount, path, path, recurse, ignore) if desc.length == 0 mount.notice "Got no information on //%s/%s" % [mount, path] return "" end desc.collect { |sub| sub.join("\t") }.join("\n") end def local? self.local end # Mount a new directory with a name. def mount(path, name) if @mounts.include?(name) if @mounts[name] != path raise FileServerError, "%s is already mounted at %s" % [@mounts[name].path, name] else # it's already mounted; no problem return end end # Let the mounts do their own error-checking. @mounts[name] = Mount.new(name, path) @mounts[name].info "Mounted %s" % path return @mounts[name] end # Retrieve a file from the local disk and pass it to the remote # client. def retrieve(url, links = :ignore, client = nil, clientip = nil) links = links.intern if links.is_a? String mount, path = convert(url, client, clientip) if client mount.info "Sending %s to %s" % [url, client] end unless FileTest.exists?(path) return "" end links = links.intern if links.is_a? String if links == :ignore and FileTest.symlink?(path) return "" end str = nil if links == :manage raise Puppet::Error, "Cannot copy links yet." else str = File.read(path) end if @local return str else return CGI.escape(str) end end def umount(name) @mounts.delete(name) if @mounts.include? name end private def authcheck(file, mount, client, clientip) # If we're local, don't bother passing in information. if local? client = nil clientip = nil end unless mount.allowed?(client, clientip) mount.warning "%s cannot access %s" % [client, file] raise Puppet::AuthorizationError, "Cannot access %s" % mount end end def convert(url, client, clientip) readconfig url = URI.unescape(url) mount, stub = splitpath(url, client) authcheck(url, mount, client, clientip) path = nil unless path = mount.subdir(stub, client) mount.notice "Could not find subdirectory %s" % "//%s/%s" % [mount, stub] return "" end return mount, path end # Deal with ignore parameters. def handleignore(children, path, ignore) ignore.each { |ignore| Dir.glob(File.join(path,ignore), File::FNM_DOTMATCH) { |match| children.delete(File.basename(match)) } } return children end + # Return the mount for the Puppet modules; allows file copying from + # the modules. + def modules_mount(module_name, client) + # Find our environment, if we have one. + if node = node_handler.details(client || Facter.value("hostname")) + env = node.environment + else + env = nil + end + + # And use the environment to look up the module. + mod = Puppet::Module::find(module_name, env) + if mod + return @mounts[MODULES].copy(mod.name, mod.files) + else + return nil + end + 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 + # Read the configuration file. def readconfig(check = true) return if @noreadconfig if check and ! @config.changed? return end newmounts = {} begin File.open(@config.file) { |f| mount = nil count = 1 f.each { |line| case line when /^\s*#/: next # skip comments when /^\s*$/: next # skip blank lines when /\[([-\w]+)\]/: name = $1 if newmounts.include?(name) raise FileServerError, "%s is already mounted at %s" % [newmounts[name], name], count, @config.file end mount = Mount.new(name) newmounts[name] = mount when /^\s*(\w+)\s+(.+)$/: var = $1 value = $2 case var when "path": if mount.name == MODULES Puppet.warning "The '#{MODULES}' module can not have a path. Ignoring attempt to set it" else begin mount.path = value rescue FileServerError => detail Puppet.err "Removing mount %s: %s" % [mount.name, detail] newmounts.delete(mount.name) end end when "allow": value.split(/\s*,\s*/).each { |val| begin mount.info "allowing %s access" % val mount.allow(val) rescue AuthStoreError => detail raise FileServerError.new(detail.to_s, count, @config.file) end } when "deny": value.split(/\s*,\s*/).each { |val| begin mount.info "denying %s access" % val mount.deny(val) rescue AuthStoreError => detail raise FileServerError.new(detail.to_s, count, @config.file) end } else raise FileServerError.new("Invalid argument '%s'" % var, count, @config.file) end else raise FileServerError.new("Invalid line '%s'" % line.chomp, count, @config.file) end count += 1 } } rescue Errno::EACCES => detail Puppet.err "FileServer error: Cannot read %s; cannot serve" % @config #raise Puppet::Error, "Cannot read %s" % @config rescue Errno::ENOENT => detail Puppet.err "FileServer error: '%s' does not exist; cannot serve" % @config #raise Puppet::Error, "%s does not exit" % @config #rescue FileServerError => detail # Puppet.err "FileServer error: %s" % detail end unless newmounts[MODULES] mount = Mount.new(MODULES) mount.allow("*") newmounts[MODULES] = mount end # Verify each of the mounts are valid. # We let the check raise an error, so that it can raise an error # pointing to the specific problem. newmounts.each { |name, mount| unless mount.valid? raise FileServerError, "No path specified for mount %s" % name end } @mounts = newmounts end # Recursively list the directory. FIXME This should be using # puppet objects, not directly listing. def reclist(mount, root, path, recurse, ignore) # Take out the root of the path. name = path.sub(root, '') if name == "" name = "/" end if name == path raise FileServerError, "Could not match %s in %s" % [root, path] end desc = [name] ftype = File.stat(path).ftype desc << ftype if recurse.is_a?(Integer) recurse -= 1 end ary = [desc] if recurse == true or (recurse.is_a?(Integer) and recurse > -1) if ftype == "directory" children = Dir.entries(path) if ignore children = handleignore(children, path, ignore) end children.each { |child| next if child =~ /^\.\.?$/ reclist(mount, root, File.join(path, child), recurse, ignore).each { |cobj| ary << cobj } } end end return ary.reject { |c| c.nil? } end # Split the path into the separate mount point and path. def splitpath(dir, client) # the dir is based on one of the mounts # so first retrieve the mount path mount = nil path = nil if dir =~ %r{/([-\w]+)/?} - tmp = $1 - path = dir.sub(%r{/#{tmp}/?}, '') + # Strip off the mount name. + mount_name, path = dir.sub(%r{^/}, '').split(File::Separator, 2) - mod = Puppet::Module::find(tmp) - if mod - mount = @mounts[MODULES].copy(mod.name, mod.files) - else - unless mount = @mounts[tmp] - raise FileServerError, "Fileserver module '%s' not mounted" % tmp + unless mount = modules_mount(mount_name, client) + unless mount = @mounts[mount_name] + raise FileServerError, "Fileserver module '%s' not mounted" % mount_name end end else raise FileServerError, "Fileserver error: Invalid path '%s'" % dir end if path == "" path = nil - else + elsif path # Remove any double slashes that might have occurred path = URI.unescape(path.gsub(/\/\//, "/")) end return mount, path end def to_s "fileserver" end # A simple class for wrapping mount points. Instances of this class # don't know about the enclosing object; they're mainly just used for # authorization. class Mount < Puppet::Network::AuthStore attr_reader :name @@syncs = {} @@files = {} Puppet::Util.logmethods(self, true) def getfileobject(dir, links) unless FileTest.exists?(dir) self.notice "File source %s does not exist" % dir return nil end return fileobj(dir, links) end # Run 'retrieve' on a file. This gets the actual parameters, so # we can pass them to the client. def check(obj) # Retrieval is enough here, because we don't want to cache # any information in the state file, and we don't want to generate # any state changes or anything. We don't even need to sync # the checksum, because we're always going to hit the disk # directly. # We're now caching file data, using the LoadedFile to check the # disk no more frequently than the :filetimeout. path = obj[:path] sync = sync(path) unless data = @@files[path] data = {} sync.synchronize(Sync::EX) do @@files[path] = data data[:loaded_obj] = Puppet::Util::LoadedFile.new(path) data[:values] = properties(obj) return data[:values] end end changed = nil sync.synchronize(Sync::SH) do changed = data[:loaded_obj].changed? end if changed sync.synchronize(Sync::EX) do data[:values] = properties(obj) return data[:values] end else sync.synchronize(Sync::SH) do return data[:values] end end end # Create a map for a specific client. def clientmap(client) { "h" => client.sub(/\..*$/, ""), "H" => client, "d" => client.sub(/[^.]+\./, "") # domain name } end # Replace % patterns as appropriate. def expand(path, client = nil) # This map should probably be moved into a method. map = nil if client map = clientmap(client) else Puppet.notice "No client; expanding '%s' with local host" % path # Else, use the local information map = localmap() end path.gsub(/%(.)/) do |v| key = $1 if key == "%" "%" else map[key] || v end end end # Do we have any patterns in our path, yo? def expandable? if defined? @expandable @expandable else false end end # Create out object. It must have a name. def initialize(name, path = nil) unless name =~ %r{^[-\w]+$} raise FileServerError, "Invalid name format '%s'" % name end @name = name if path self.path = path else @path = nil end super() end def fileobj(path, links) obj = nil if obj = Puppet.type(:file)[path] # This can only happen in local fileserving, but it's an # important one. It'd be nice if we didn't just set # the check params every time, but I'm not sure it's worth # the effort. obj[:check] = CHECKPARAMS else obj = Puppet.type(:file).create( :name => path, :check => CHECKPARAMS ) end if links == :manage links = :follow end # This, ah, might be completely redundant unless obj[:links] == links obj[:links] = links end return obj end # Cache this manufactured map, since if it's used it's likely # to get used a lot. def localmap unless defined? @@localmap @@localmap = { "h" => Facter.value("hostname"), "H" => [Facter.value("hostname"), Facter.value("domain")].join("."), "d" => Facter.value("domain") } end @@localmap end # Return the path as appropriate, expanding as necessary. def path(client = nil) if expandable? return expand(@path, client) else return @path end end # Set the path. def path=(path) # FIXME: For now, just don't validate paths with replacement # patterns in them. if path =~ /%./ # Mark that we're expandable. @expandable = true else unless FileTest.exists?(path) raise FileServerError, "%s does not exist" % path end unless FileTest.directory?(path) raise FileServerError, "%s is not a directory" % path end unless FileTest.readable?(path) raise FileServerError, "%s is not readable" % path end @expandable = false end @path = path end # Return the current values for the object. def properties(obj) obj.retrieve.inject({}) { |props, ary| props[ary[0].name] = ary[1]; props } end # Retrieve a specific directory relative to a mount point. # If they pass in a client, then expand as necessary. def subdir(dir = nil, client = nil) basedir = self.path(client) dirname = if dir File.join(basedir, dir.split("/").join(File::SEPARATOR)) else basedir end dirname end def sync(path) @@syncs[path] ||= Sync.new @@syncs[path] end def to_s "mount[%s]" % @name end # Verify our configuration is valid. This should really check to # make sure at least someone will be allowed, but, eh. def valid? if name == MODULES return @path.nil? else return ! @path.nil? end end # Return a new mount with the same properties as +self+, except # with a different name and path. def copy(name, path) result = self.clone result.path = path result.instance_variable_set(:@name, name) return result end end end end # $Id$ diff --git a/lib/puppet/network/handler/master.rb b/lib/puppet/network/handler/master.rb index 0cab94f69..e5bfa8122 100644 --- a/lib/puppet/network/handler/master.rb +++ b/lib/puppet/network/handler/master.rb @@ -1,144 +1,145 @@ require 'openssl' require 'puppet' require 'puppet/parser/interpreter' require 'puppet/sslcertificates' require 'xmlrpc/server' require 'yaml' class Puppet::Network::Handler class MasterError < Puppet::Error; end class Master < Handler desc "Puppet's configuration interface. Used for all interactions related to generating client configurations." include Puppet::Util attr_accessor :ast attr_reader :ca @interface = XMLRPC::Service::Interface.new("puppetmaster") { |iface| iface.add_method("string getconfig(string)") iface.add_method("int freshness()") } # Tell a client whether there's a fresh config for it def freshness(client = nil, clientip = nil) + client ||= Facter.value("hostname") config_handler.version(client, clientip) end def initialize(hash = {}) args = {} # Allow specification of a code snippet or of a file if code = hash[:Code] args[:Code] = code elsif man = hash[:Manifest] args[:Manifest] = man end if hash[:Local] @local = hash[:Local] else @local = false end args[:Local] = local? if hash.include?(:CA) and hash[:CA] @ca = Puppet::SSLCertificates::CA.new() else @ca = nil end Puppet.debug("Creating interpreter") if hash.include?(:UseNodes) args[:UseNodes] = hash[:UseNodes] elsif @local args[:UseNodes] = false end # This is only used by the cfengine module, or if --loadclasses was # specified in +puppet+. if hash.include?(:Classes) args[:Classes] = hash[:Classes] end @config_handler = Puppet::Network::Handler.handler(:configuration).new(args) end # Call our various handlers; this handler is getting deprecated. def getconfig(facts, format = "marshal", client = nil, clientip = nil) facts = decode_facts(facts) client, clientip = clientname(client, clientip, facts) # Pass the facts to the fact handler fact_handler.set(client, facts) # And get the configuration from the config handler return config_handler.configuration(client) end def local=(val) @local = val config_handler.local = val fact_handler.local = val end private # Manipulate the client name as appropriate. def clientname(name, ip, facts) # Always use the hostname from Facter. client = facts["hostname"] clientip = facts["ipaddress"] if Puppet[:node_name] == 'cert' if name client = name end if ip clientip = ip end end return client, clientip end def config_handler unless defined? @config_handler @config_handler = Puppet::Network::Handler.handler(:config).new :local => local? end @config_handler end # def decode_facts(facts) if @local # we don't need to do anything, since we should already # have raw objects Puppet.debug "Our client is local" else Puppet.debug "Our client is remote" begin facts = YAML.load(CGI.unescape(facts)) rescue => detail raise XMLRPC::FaultException.new( 1, "Could not rebuild facts" ) end end return facts end def fact_handler unless defined? @fact_handler @fact_handler = Puppet::Network::Handler.handler(:facts).new :local => local? end @fact_handler end end end # $Id$ diff --git a/lib/puppet/network/handler/node.rb b/lib/puppet/network/handler/node.rb index 2c4d3e1b5..bf2424b18 100644 --- a/lib/puppet/network/handler/node.rb +++ b/lib/puppet/network/handler/node.rb @@ -1,227 +1,242 @@ # Created by Luke A. Kanies on 2007-08-13. # Copyright (c) 2007. All rights reserved. require 'puppet/util' require 'puppet/node' require 'puppet/util/classgen' require 'puppet/util/instance_loader' # Look up a node, along with all the details about it. class Puppet::Network::Handler::Node < Puppet::Network::Handler desc "Retrieve information about nodes." + # Create a singleton node handler + def self.create + unless @handler + @handler = new + end + @handler + end + # Add a new node source. def self.newnode_source(name, options = {}, &block) name = symbolize(name) fact_merge = options[:fact_merge] mod = genmodule(name, :extend => SourceBase, :hash => instance_hash(:node_source), :block => block) mod.send(:define_method, :fact_merge?) do fact_merge end mod end # Collect the docs for all of our node sources. def self.node_source_docs docs = "" # Use this method so they all get loaded instance_loader(:node_source).loadall loaded_instances(:node_source).sort { |a,b| a.to_s <=> b.to_s }.each do |name| mod = self.node_source(name) docs += "%s\n%s\n" % [name, "-" * name.to_s.length] docs += Puppet::Util::Docs.scrub(mod.doc) + "\n\n" end docs end # List each of the node sources. def self.node_sources instance_loader(:node_source).loadall loaded_instances(:node_source) end # Remove a defined node source; basically only used for testing. def self.rm_node_source(name) rmclass(name, :hash => instance_hash(:node_source)) end extend Puppet::Util::ClassGen extend Puppet::Util::InstanceLoader # A simple base module we can use for modifying how our node sources work. module SourceBase include Puppet::Util::Docs end @interface = XMLRPC::Service::Interface.new("nodes") { |iface| iface.add_method("string details(key)") iface.add_method("string parameters(key)") iface.add_method("string environment(key)") iface.add_method("string classes(key)") } # Set up autoloading and retrieving of reports. autoload :node_source, 'puppet/node_source' attr_reader :source # Return a given node's classes. def classes(key) if node = details(key) node.classes else nil end end # Return an entire node configuration. This uses the 'nodesearch' method # defined in the node_source to look for the node. def details(key, client = nil, clientip = nil) + return nil unless key + if node = cached?(key) + return node + end facts = node_facts(key) node = nil names = node_names(key, facts) names.each do |name| name = name.to_s if name.is_a?(Symbol) if node = nodesearch(name) Puppet.info "Found %s in %s" % [name, @source] break end end # If they made it this far, we haven't found anything, so look for a # default node. unless node or names.include?("default") if node = nodesearch("default") Puppet.notice "Using default node for %s" % key end end if node node.source = @source node.names = names # Merge the facts into the parameters. if fact_merge? node.fact_merge(facts) end + + cache(node) + return node else return nil end end # Return a given node's environment. def environment(key, client = nil, clientip = nil) if node = details(key) node.environment else nil end end # Create our node lookup tool. def initialize(hash = {}) @source = hash[:Source] || Puppet[:node_source] unless mod = self.class.node_source(@source) raise ArgumentError, "Unknown node source '%s'" % @source end extend(mod) super # We cache node info for speed @node_cache = {} end # Try to retrieve a given node's parameters. def parameters(key, client = nil, clientip = nil) if node = details(key) node.parameters else nil end end private # Store the node to make things a bit faster. def cache(node) @node_cache[node.name] = node end # If the node is cached, return it. def cached?(name) # Don't use cache when the filetimeout is set to 0 return false if [0, "0"].include?(Puppet[:filetimeout]) if node = @node_cache[name] and Time.now - node.time < Puppet[:filetimeout] return node else return false end end # Create/cache a fact handler. def fact_handler unless defined?(@fact_handler) @fact_handler = Puppet::Network::Handler.handler(:facts).new end @fact_handler end # Short-hand for creating a new node, so the node sources don't need to # specify the constant. def newnode(options) Puppet::Node.new(options) end # Look up the node facts from our fact handler. def node_facts(key) if facts = fact_handler.get(key) facts else {} end end # Calculate the list of node names we should use for looking # up our node. def node_names(key, facts = nil) facts ||= node_facts(key) names = [] if hostname = facts["hostname"] unless hostname == key names << hostname end else hostname = key end if fqdn = facts["fqdn"] hostname = fqdn names << fqdn end # Make sure both the fqdn and the short name of the # host can be used in the manifest if hostname =~ /\./ names << hostname.sub(/\..+/,'') elsif domain = facts['domain'] names << hostname + "." + domain end # Sort the names inversely by name length. names.sort! { |a,b| b.length <=> a.length } # And make sure the key is first, since that's the most # likely usage. ([key] + names).uniq end end diff --git a/test/network/handler/configuration.rb b/test/network/handler/configuration.rb index 98c3bdcde..a34952208 100755 --- a/test/network/handler/configuration.rb +++ b/test/network/handler/configuration.rb @@ -1,189 +1,187 @@ #!/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) 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) interp = Object.new 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) interp = Object.new interp.expects(:compile).with(node).returns(:config) 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 - interp.expects(:parsedate).returns(:version) - config.expects(:interpreter).returns(interp) - assert_equal(:version, config.version(:client), "Did not return configuration version") + 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(:parsedate).returns(:version) + 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/master.rb b/test/network/handler/master.rb index 5ac8cbbbc..a976726ef 100755 --- a/test/network/handler/master.rb +++ b/test/network/handler/master.rb @@ -1,156 +1,156 @@ #!/usr/bin/env ruby $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppet/network/handler/master' class TestMaster < Test::Unit::TestCase include PuppetTest::ServerTest def test_defaultmanifest textfiles { |file| Puppet[:manifest] = file client = nil master = nil assert_nothing_raised() { # this is the default server setup master = Puppet::Network::Handler.master.new( :Manifest => file, :UseNodes => false, :Local => true ) } assert_nothing_raised() { client = Puppet::Network::Client.master.new( :Master => master ) } # pull our configuration assert_nothing_raised() { client.getconfig stopservices Puppet::Type.allclear } break } end def test_filereread # Start with a normal setting Puppet[:filetimeout] = 15 manifest = mktestmanifest() facts = Puppet::Network::Client.master.facts # Store them, so we don't determine frshness based on facts. Puppet::Util::Storage.cache(:configuration)[:facts] = facts file2 = @createdfile + "2" @@tmpfiles << file2 client = master = nil assert_nothing_raised() { # this is the default server setup master = Puppet::Network::Handler.master.new( :Manifest => manifest, :UseNodes => false, :Local => true ) } assert_nothing_raised() { client = Puppet::Network::Client.master.new( :Master => master ) } assert(client, "did not create master client") # The client doesn't have a config, so it can't be up to date assert(! client.fresh?(facts), "Client is incorrectly up to date") Puppet.config.use(:main) assert_nothing_raised { client.getconfig client.apply } # Now it should be up to date assert(client.fresh?(facts), "Client is not up to date") # Cache this value for later - parse1 = master.freshness + parse1 = master.freshness("mynode") # Verify the config got applied assert(FileTest.exists?(@createdfile), "Created file %s does not exist" % @createdfile) Puppet::Type.allclear sleep 1.5 # Create a new manifest File.open(manifest, "w") { |f| f.puts "file { \"%s\": ensure => file }\n" % file2 } # Verify that the master doesn't immediately reparse the file; we # want to wait through the timeout - assert_equal(parse1, master.freshness, "Master did not wait through timeout") + assert_equal(parse1, master.freshness("mynode"), "Master did not wait through timeout") assert(client.fresh?(facts), "Client is not up to date") # Then eliminate it Puppet[:filetimeout] = 0 # Now make sure the master does reparse #Puppet.notice "%s vs %s" % [parse1, master.freshness] - assert(parse1 != master.freshness, "Master did not reparse file") + assert(parse1 != master.freshness("mynode"), "Master did not reparse file") assert(! client.fresh?(facts), "Client is incorrectly up to date") # Retrieve and apply the new config assert_nothing_raised { client.getconfig client.apply } assert(client.fresh?(facts), "Client is not up to date") assert(FileTest.exists?(file2), "Second file %s does not exist" % file2) end # Make sure we're correctly doing clientname manipulations. # Testing to make sure we always get a hostname and IP address. def test_clientname # create our master master = Puppet::Network::Handler.master.new( :Manifest => tempfile, :UseNodes => true, :Local => true ) # First check that 'cert' works Puppet[:node_name] = "cert" # Make sure we get the fact data back when nothing is set facts = {"hostname" => "fact_hostname", "ipaddress" => "fact_ip"} certname = "cert_hostname" certip = "cert_ip" resname, resip = master.send(:clientname, nil, nil, facts) assert_equal(facts["hostname"], resname, "Did not use fact hostname when no certname was present") assert_equal(facts["ipaddress"], resip, "Did not use fact ip when no certname was present") # Now try it with the cert stuff present resname, resip = master.send(:clientname, certname, certip, facts) assert_equal(certname, resname, "Did not use cert hostname when certname was present") assert_equal(certip, resip, "Did not use cert ip when certname was present") # And reset the node_name stuff and make sure we use it. Puppet[:node_name] = :facter resname, resip = master.send(:clientname, certname, certip, facts) assert_equal(facts["hostname"], resname, "Did not use fact hostname when nodename was set to facter") assert_equal(facts["ipaddress"], resip, "Did not use fact ip when nodename was set to facter") end end # $Id$ diff --git a/test/network/handler/node.rb b/test/network/handler/node.rb index d5c98fec6..f7cbf6017 100755 --- a/test/network/handler/node.rb +++ b/test/network/handler/node.rb @@ -1,637 +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) 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