diff --git a/lib/puppet.rb b/lib/puppet.rb index 54a9f6c77..1da2ca6dd 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,395 +1,397 @@ # see the bottom of the file for further inclusions require 'singleton' require 'facter' require 'puppet/error' require 'puppet/event-loop' require 'puppet/util' require 'puppet/log' require 'puppet/autoload' require 'puppet/config' require 'puppet/feature' require 'puppet/suidmanager' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' module Puppet PUPPETVERSION = '0.20.1' def Puppet.version return PUPPETVERSION end class << self # So we can monitor signals and such. include SignalObserver include Puppet::Util # To keep a copy of arguments. Set within Config#addargs, because I'm # lazy. attr_accessor :args attr_reader :features end def self.name unless defined? @name @name = $0.gsub(/.+#{File::SEPARATOR}/,'').sub(/\.rb$/, '') end return @name end # the hash that determines how our system behaves @@config = Puppet::Config.new # The services running in this process. @services ||= [] # define helper messages for each of the message levels Puppet::Log.eachlevel { |level| define_method(level,proc { |args| if args.is_a?(Array) args = args.join(" ") end Puppet::Log.create( :level => level, :message => args ) }) module_function level } # I keep wanting to use Puppet.error # XXX this isn't actually working right now alias :error :err # The feature collection @features = Puppet::Feature.new('puppet/feature') - @features.load # Store a new default value. def self.setdefaults(section, hash) @@config.setdefaults(section, hash) end # Load all of the configuration parameters. require 'puppet/configuration' # configuration parameter access and stuff def self.[](param) case param when :debug: if Puppet::Log.level == :debug return true else return false end else return @@config[param] end end # configuration parameter access and stuff def self.[]=(param,value) @@config[param] = value end def self.clear @@config.clear end def self.debug=(value) if value Puppet::Log.level=(:debug) else Puppet::Log.level=(:notice) end end def self.config @@config end def self.genconfig if Puppet[:configprint] != "" val = Puppet[:configprint] if val == "all" hash = {} Puppet.config.each do |name, obj| val = obj.value case val when true, false, "": val = val.inspect end hash[name] = val end hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val| puts "%s = %s" % [name, val] end elsif val =~ /,/ val.split(/\s*,\s*/).sort.each do |v| puts "%s = %s" % [v, Puppet[v]] end else puts Puppet[val] end exit(0) end if Puppet[:genconfig] puts Puppet.config.to_config exit(0) end end def self.genmanifest if Puppet[:genmanifest] puts Puppet.config.to_manifest exit(0) end end # Run all threads to their ends def self.join defined? @threads and @threads.each do |t| t.join end end # Create a new service that we're supposed to run def self.newservice(service) @services ||= [] @services << service end def self.newthread(&block) @threads ||= [] @threads << Thread.new do yield end end def self.newtimer(hash, &block) timer = nil threadlock(:timers) do @timers ||= [] timer = EventLoop::Timer.new(hash) @timers << timer if block_given? observe_signal(timer, :alarm, &block) end end # In case they need it for something else. timer end # Relaunch the executable. def self.restart command = $0 + " " + self.args.join(" ") Puppet.notice "Restarting with '%s'" % command Puppet.shutdown(false) Puppet::Log.reopen exec(command) end # Trap a couple of the main signals. This should probably be handled # in a way that anyone else can register callbacks for traps, but, eh. def self.settraps [:INT, :TERM].each do |signal| trap(signal) do Puppet.notice "Caught #{signal}; shutting down" Puppet.shutdown end end # Handle restarting. trap(:HUP) do if client = @services.find { |s| s.is_a? Puppet::Client::MasterClient } and client.running? client.restart else Puppet.restart end end # Provide a hook for running clients where appropriate trap(:USR1) do done = 0 Puppet.notice "Caught USR1; triggering client run" @services.find_all { |s| s.is_a? Puppet::Client }.each do |client| if client.respond_to? :running? if client.running? Puppet.info "Ignoring running %s" % client.class else done += 1 begin client.runnow rescue => detail Puppet.err "Could not run client: %s" % detail end end else Puppet.info "Ignoring %s; cannot test whether it is running" % client.class end end unless done > 0 Puppet.notice "No clients were run" end end trap(:USR2) do Puppet::Log.reopen end end # Shutdown our server process, meaning stop all services and all threads. # Optionally, exit. def self.shutdown(leave = true) Puppet.notice "Shutting down" # Unmonitor our timers defined? @timers and @timers.each do |timer| EventLoop.current.ignore_timer timer end # This seems to exit the process, although I can't find where it does # so. Leaving it out doesn't seem to hurt anything. #if EventLoop.current.running? # EventLoop.current.quit #end # Stop our services defined? @services and @services.each do |svc| begin timeout(20) do svc.shutdown end rescue TimeoutError Puppet.err "%s could not shut down within 20 seconds" % svc.class end end # And wait for them all to die, giving a decent amount of time defined? @threads and @threads.each do |thr| begin timeout(20) do thr.join end rescue TimeoutError # Just ignore this, since we can't intelligently provide a warning end end if leave exit(0) end end # Start all of our services and optionally our event loop, which blocks, # waiting for someone, somewhere, to generate events of some kind. def self.start(block = true) # Starting everything in its own thread, fwiw defined? @services and @services.dup.each do |svc| newthread do begin svc.start rescue => detail if Puppet[:trace] puts detail.backtrace end @services.delete svc Puppet.err "Could not start %s: %s" % [svc.class, detail] end end end # We need to give the services a chance to register their timers before # we try to start monitoring them. sleep 0.5 unless @services.length > 0 Puppet.notice "No remaining services; exiting" exit(1) end if defined? @timers and ! @timers.empty? @timers.each do |timer| EventLoop.current.monitor_timer timer end end if block EventLoop.current.run end end # Create the timer that our different objects (uh, mostly the client) # check. def self.timer unless defined? @timer #Puppet.info "Interval is %s" % Puppet[:runinterval] #@timer = EventLoop::Timer.new(:interval => Puppet[:runinterval]) @timer = EventLoop::Timer.new( :interval => Puppet[:runinterval], :tolerance => 1, :start? => true ) EventLoop.current.monitor_timer @timer end @timer end # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) if FileTest.exist?(dir) return false else tmp = dir.sub(/^\//,'') path = [File::SEPARATOR] tmp.split(File::SEPARATOR).each { |dir| path.push dir if ! FileTest.exist?(File.join(path)) begin Dir.mkdir(File.join(path), mode) rescue Errno::EACCES => detail Puppet.err detail.to_s return false rescue => detail Puppet.err "Could not create %s: %s" % [path, detail.to_s] return false end elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise Puppet::Error, "Cannot create %s: basedir %s is a file" % [dir, File.join(path)] end } return true end end # Create a new type. Just proxy to the Type class. def self.newtype(name, parent = nil, &block) Puppet::Type.newtype(name, parent, &block) end # Retrieve a type by name. Just proxy to the Type class. def self.type(name) Puppet::Type.type(name) end + + # Only load all of the features once the Puppet module methods are defined. + @features.load end require 'puppet/server' require 'puppet/type' require 'puppet/storage' # $Id$ diff --git a/lib/puppet/feature.rb b/lib/puppet/feature.rb index 55fab5d3c..2d965ed47 100644 --- a/lib/puppet/feature.rb +++ b/lib/puppet/feature.rb @@ -1,64 +1,61 @@ # Created by Luke Kanies on 2006-11-07. # Copyright (c) 2006. All rights reserved. class Puppet::Feature # Create a new feature test. You have to pass the feature name, # and it must be unique. You can either provide a block that # will get executed immediately to determine if the feature # is present, or you can pass an option to determine it. # Currently, the only supported option is 'libs' (must be # passed as a symbol), which will make sure that each lib loads # successfully. def add(name, options = {}) method = name.to_s + "?" if self.class.respond_to?(method) raise ArgumentError, "Feature %s is already defined" % name end result = true if block_given? begin result = yield rescue => detail - if Puppet[:trace] - puts detail.backtrace - Puppet.err "Failed to load %s: %s" % [name, detail] - end + warn "Failed to load feature test for %s: %s" % [name, detail] result = false end end if ary = options[:libs] ary = [ary] unless ary.is_a?(Array) ary.each do |lib| unless lib.is_a?(String) raise ArgumentError, "Libraries must be passed as strings not %s" % lib.class end begin require lib rescue Exception Puppet.debug "Failed to load library '%s' for feature '%s'" % [lib, name] result = false end end end meta_def(method) do result end end # Create a new feature collection. def initialize(path) @path = path end def load loader = Puppet::Autoload.new(self, @path) loader.loadall end end -# $Id$ \ No newline at end of file +# $Id$ diff --git a/lib/puppet/feature/rails.rb b/lib/puppet/feature/rails.rb new file mode 100644 index 000000000..288f02f82 --- /dev/null +++ b/lib/puppet/feature/rails.rb @@ -0,0 +1,44 @@ +# Created by Luke Kanies on 2006-11-07. +# Copyright (c) 2006. All rights reserved. + +require 'puppet/feature' + +Puppet.features.add(:rails) do + begin + require 'active_record' + rescue LoadError => detail + if Facter["operatingsystem"].value == "Debian" and + FileTest.exists?("/usr/share/rails") + count = 0 + Dir.entries("/usr/share/rails").each do |dir| + libdir = File.join("/usr/share/rails", dir, "lib") + if FileTest.exists?(libdir) and ! $:.include?(libdir) + count += 1 + $: << libdir + end + end + + if count > 0 + retry + end + end + end + + # If we couldn't find it the normal way, try using a Gem. + unless defined? ActiveRecord + begin + require 'rubygems' + require_gem 'rails' + rescue LoadError + # Nothing + end + end + + if defined? ActiveRecord + true + else + false + end +end + +# $Id$ diff --git a/lib/puppet/parser/collector.rb b/lib/puppet/parser/collector.rb index 61807717d..71213ee6a 100644 --- a/lib/puppet/parser/collector.rb +++ b/lib/puppet/parser/collector.rb @@ -1,127 +1,153 @@ # An object that collects stored objects from the central cache and returns # them to the current host, yo. class Puppet::Parser::Collector attr_accessor :type, :scope, :vquery, :rquery, :form, :resources # Collect exported objects. def collect_exported require 'puppet/rails' + Puppet.info "collecting %s" % @type # First get everything from the export table. Just reuse our # collect_virtual method but tell it to use 'exported? for the test. resources = collect_virtual(true) - count = resources.length + count = 0 # We're going to collect objects from rails, but we don't want any # objects from this host. host = Puppet::Rails::Host.find_by_name(@scope.host) args = {} if host args[:conditions] = "host_id != #{host.id}" else #Puppet.info "Host %s is uninitialized" % @scope.host end + Puppet.info "collecting %s" % @type # Now look them up in the rails db. When we support attribute comparison # and such, we'll need to vary the conditions, but this works with no # attributes, anyway. - Puppet::Util.benchmark(:debug, "Collected #{self.type} resources") do + time = Puppet::Util.thinmark do Puppet::Rails::RailsResource.find_all_by_restype_and_exported(@type, true, args ).each do |obj| + if existing = @scope.findresource(obj.restype, obj.title) + # See if we exported it; if so, just move on + if @scope.host == obj.host.name + next + else + # Next see if we've already collected this resource + if existing.rails_id == obj.id + # This is the one we've already collected + next + else + raise Puppet::ParseError, + "Exported resource %s[%s] cannot override local resource" % + [obj.restype, obj.title] + end + end + end count += 1 + begin resource = obj.to_resource(self.scope) # XXX Because the scopes don't expect objects to return values, # we have to manually add our objects to the scope. This is # uber-lame. - scope.setresource(resource) + scope.setresource(resource) + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + raise + end + resource.exported = false resources << resource end end - scope.debug("Collected %s objects of type %s" % - [count, @convertedtype]) + scope.debug("Collected %s %s resources in %.2f seconds" % + [count, @type, time]) return resources end def collect_resources unless @resources.is_a?(Array) @resources = [@resources] end method = "collect_#{form.to_s}_resources" send(method) end def collect_exported_resources raise Puppet::ParseError, "realize() is not yet implemented for exported resources" end def collect_virtual_resources @resources.collect do |ref| if res = @scope.findresource(ref.to_s) res else raise Puppet::ParseError, "Could not find resource %s" % ref end end.each do |res| res.virtual = false end end # Collect just virtual objects, from our local configuration. def collect_virtual(exported = false) if exported method = :exported? else method = :virtual? end scope.resources.find_all do |resource| resource.type == @type and resource.send(method) and match?(resource) end end # Call the collection method, mark all of the returned objects as non-virtual, # and then delete this object from the list of collections to evaluate. def evaluate if self.resources return collect_resources end method = "collect_#{@form.to_s}" objects = send(method).each do |obj| obj.virtual = false end # And then remove us from the list of collections, since we've # now been evaluated. @scope.collections.delete(self) objects end def initialize(scope, type, equery, vquery, form) @scope = scope @type = type @equery = equery @vquery = vquery @form = form @tests = [] end # Does the resource match our tests? We don't yet support tests, # so it's always true at the moment. def match?(resource) if self.vquery return self.vquery.call(resource) else return true end end end # $Id$ diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 4480a123a..d2ec1b92c 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,802 +1,803 @@ # The interepreter's job is to convert from a parsed file to the configuration # for a given client. It really doesn't do any work on its own, it just collects # and calls out to other objects. require 'puppet' require 'timeout' +require 'puppet/rails' require 'puppet/parser/parser' require 'puppet/parser/scope' class Puppet::Parser::Interpreter include Puppet::Util Puppet.setdefaults("ldap", :ldapnodes => [false, "Whether to search for node configurations in LDAP."], :ldapssl => [false, "Whether SSL should be used when searching for nodes. Defaults to false because SSL usually requires certificates to be set up on the client side."], :ldaptls => [false, "Whether TLS should be used when searching for nodes. Defaults to false because TLS usually requires certificates to be set up on the client side."], :ldapserver => ["ldap", "The LDAP server. Only used if ``ldapnodes`` is enabled."], :ldapport => [389, "The LDAP port. Only used if ``ldapnodes`` is enabled."], :ldapstring => ["(&(objectclass=puppetClient)(cn=%s))", "The search string used to find an LDAP node."], :ldapattrs => ["puppetclass", "The LDAP attributes to use to define Puppet classes. Values should be comma-separated."], :ldapparentattr => ["parentnode", "The attribute to use to define the parent node."], :ldapuser => ["", "The user to use to connect to LDAP. Must be specified as a full DN."], :ldappassword => ["", "The password to use to connect to LDAP."], :ldapbase => ["", "The search base for LDAP searches. It's impossible to provide a meaningful default here, although the LDAP libraries might have one already set. Generally, it should be the 'ou=Hosts' branch under your main directory."] ) Puppet.setdefaults(:puppetmaster, :storeconfigs => [false, "Whether to store each client's configuration. This requires ActiveRecord from Ruby on Rails."] ) attr_accessor :usenodes class << self attr_writer :ldap end # just shorten the constant path a bit, using what amounts to an alias AST = Puppet::Parser::AST include Puppet::Util::Errors # Create an ldap connection. This is a class method so others can call # it and use the same variables and such. def self.ldap unless defined? @ldap and @ldap if Puppet[:ldapssl] @ldap = LDAP::SSLConn.new(Puppet[:ldapserver], Puppet[:ldapport]) elsif Puppet[:ldaptls] @ldap = LDAP::SSLConn.new( Puppet[:ldapserver], Puppet[:ldapport], true ) else @ldap = LDAP::Conn.new(Puppet[:ldapserver], Puppet[:ldapport]) end @ldap.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3) @ldap.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON) @ldap.simple_bind(Puppet[:ldapuser], Puppet[:ldappassword]) end return @ldap end def clear initparsevars end # Iteratively evaluate all of the objects. This finds all fo the # objects that represent definitions and evaluates the definitions appropriately. # It also adds defaults and overrides as appropriate. def evaliterate(scope) count = 0 begin timeout 300 do while ary = scope.unevaluated ary.each do |resource| resource.evaluate end end end rescue Timeout::Error raise Puppet::DevError, "Got a timeout trying to evaluate all definitions" end end # Evaluate a specific node. def evalnode(client, scope, facts) return unless self.usenodes unless client raise Puppet::Error, "Cannot evaluate nodes with a nil client" end names = [client] # Make sure both the fqdn and the short name of the # host can be used in the manifest if client =~ /\./ names << client.sub(/\..+/,'') else names << "#{client}.#{facts['domain']}" end if names.empty? raise Puppet::Error, "Cannot evaluate nodes with a nil client" end # Look up our node object. if nodeclass = nodesearch(*names) nodeclass.safeevaluate :scope => scope else raise Puppet::Error, "Could not find %s with names %s" % [client, names.join(", ")] end end # Evaluate all of the code we can find that's related to our client. def evaluate(client, facts) scope = Puppet::Parser::Scope.new(:interp => self) # no parent scope scope.name = "top" scope.type = "main" - scope.host = facts["hostname"] || Facter.value("hostname") + scope.host = client classes = @classes.dup # Okay, first things first. Set our facts. scope.setfacts(facts) # Everyone will always evaluate the top-level class, if there is one. if klass = findclass("", "") # Set the source, so objects can tell where they were defined. scope.source = klass klass.safeevaluate :scope => scope, :nosubscope => true end # Next evaluate the node evalnode(client, scope, facts) # If we were passed any classes, evaluate those. if classes classes.each do |klass| if klassobj = findclass("", klass) klassobj.safeevaluate :scope => scope end end end # That was the first pass evaluation. Now iteratively evaluate # until we've gotten rid of all of everything or thrown an error. evaliterate(scope) # Now make sure we fail if there's anything left to do failonleftovers(scope) # Now perform the collections exceptwrap do scope.collections.each do |coll| coll.evaluate end end # Now finish everything. This recursively calls finish on the # contained scopes and resources. scope.finish # Store everything. We need to do this before translation, because # it operates on resources, not transobjects. if Puppet[:storeconfigs] args = { :resources => scope.resources, :name => client, :facts => facts } unless scope.classlist.empty? args[:classes] = scope.classlist end storeconfigs(args) end # Now, finally, convert our scope tree + resources into a tree of # buckets and objects. objects = scope.translate # Add the class list unless scope.classlist.empty? objects.classes = scope.classlist end return objects end # Fail if there any overrides left to perform. def failonleftovers(scope) overrides = scope.overrides if overrides.empty? return nil else fail Puppet::ParseError, "Could not find object(s) %s" % overrides.collect { |o| o.ref }.join(", ") end end # Find a class definition, relative to the current namespace. def findclass(namespace, name) fqfind namespace, name, @classtable end # Find a component definition, relative to the current namespace. def finddefine(namespace, name) fqfind namespace, name, @definetable end # The recursive method used to actually look these objects up. def fqfind(namespace, name, table) if name =~ /^::/ or namespace == "" return table[name.sub(/^::/, '')] end ary = namespace.split("::") while ary.length > 0 newname = (ary + [name]).join("::").sub(/^::/, '') if 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] return obj end return nil end # Create a new node, just from a list of names, classes, and an optional parent. def gennode(name, hash) facts = hash[:facts] classes = hash[:classes] parent = hash[:parentnode] arghash = { :name => name, :interp => self, :fqname => name } if (classes.is_a?(Array) and classes.empty?) or classes.nil? arghash[:code] = AST::ASTArray.new(:children => []) else classes = [classes] unless classes.is_a?(Array) classcode = @parser.ast(AST::ASTArray, :children => classes.collect do |klass| @parser.ast(AST::FlatString, :value => klass) end) # Now generate a function call. code = @parser.ast(AST::Function, :name => "include", :arguments => classcode, :ftype => :statement ) arghash[:code] = code end if parent arghash[:parentclass] = parent end # Create the node return @parser.ast(AST::Node, arghash) end # create our interpreter def initialize(hash) if @code = hash[:Code] @file = nil # to avoid warnings elsif ! @file = hash[:Manifest] devfail "You must provide code or a manifest" end if hash.include?(:UseNodes) @usenodes = hash[:UseNodes] else @usenodes = true end # By default, we only search for parsed nodes. @nodesources = [] if Puppet[:ldapnodes] # Nodes in the file override nodes in ldap. @nodesources << :ldap end if hash[:NodeSources] unless hash[:NodeSources].is_a?(Array) hash[:NodeSources] = [hash[:NodeSources]] end hash[:NodeSources].each do |src| if respond_to? "nodesearch_#{src.to_s}" @nodesources << src.to_s.intern else Puppet.warning "Node source '#{src}' not supported" end end end unless @nodesources.include?(:code) @nodesources << :code end @setup = false initparsevars() # Set it to either the value or nil. This is currently only used # by the cfengine module. @classes = hash[:Classes] || [] @local = hash[:Local] || false if hash.include?(:ForkSave) @forksave = hash[:ForkSave] else # This is just too dangerous right now. Sorry, it's going # to have to be slow. @forksave = false end # The class won't always be defined during testing. - if Puppet[:storeconfigs] and defined? ActiveRecord::Base + if Puppet[:storeconfigs] and Puppet.features.rails? Puppet::Rails.init end @files = [] # Create our parser object parsefiles end # Initialize or reset the variables related to parsing. def initparsevars @classtable = {} @namespace = "main" @nodetable = {} @definetable = {} end # Find the ldap node and extra the info, returning just # the critical data. def ldapsearch(node) unless defined? @ldap and @ldap setup_ldap() unless @ldap Puppet.info "Skipping ldap source; no ldap connection" return nil, [] end end if node =~ /\./ node = node.sub(/\..+/, '') end filter = Puppet[:ldapstring] attrs = Puppet[:ldapattrs].split("\s*,\s*") sattrs = attrs.dup pattr = nil if pattr = Puppet[:ldapparentattr] if pattr == "" pattr = nil else sattrs << pattr end end if filter =~ /%s/ filter = filter.gsub(/%s/, node) end parent = nil classes = [] found = false count = 0 begin # We're always doing a sub here; oh well. @ldap.search(Puppet[:ldapbase], 2, filter, sattrs) do |entry| found = true if pattr if values = entry.vals(pattr) if values.length > 1 raise Puppet::Error, "Node %s has more than one parent: %s" % [node, values.inspect] end unless values.empty? parent = values.shift end end end attrs.each { |attr| if values = entry.vals(attr) values.each do |v| classes << v end end } end rescue => detail if count == 0 # Try reconnecting to ldap @ldap = nil setup_ldap() retry else raise Puppet::Error, "LDAP Search failed: %s" % detail end end classes.flatten! if classes.empty? classes = nil end return parent, classes 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(fqname, options = {}) if @definetable.include?(fqname) raise Puppet::ParseError, "Cannot redefine class %s as a definition" % fqname end code = options[:code] parent = options[:parent] # If the class is already defined, then add code to it. if other = @classtable[fqname] # Make sure the parents match if parent and other.parentclass and (parent != other.parentclass) @parser.error @parser.addcontext("Class %s is already defined" % fqname) + " with parent %s" % [fqname, other.parentclass] 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 = fqname if tmp == "" tmp = "main" end Puppet.debug @parser.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. ns, name = namesplit(fqname) # 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 = {:type => name, :namespace => fqname, :fqname => fqname, :interp => self} args[:code] = code if code args[:parentclass] = parent if parent @classtable[fqname] = @parser.ast AST::HostClass, args end return @classtable[fqname] end # Create a new definition. def newdefine(fqname, options = {}) if @classtable.include?(fqname) raise Puppet::ParseError, "Cannot redefine class %s as a definition" % fqname end # Make sure our definition doesn't already exist if other = @definetable[fqname] @parser.error @parser.addcontext( "%s is already defined at line %s" % [fqname, other.line], other ) end ns, name = namesplit(fqname) args = { :type => name, :namespace => ns, :arguments => options[:arguments], :code => options[:code], :fqname => fqname } [:code, :arguments].each do |param| args[param] = options[param] if options[param] end @definetable[fqname] = @parser.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| if other = @nodetable[name] @parser.error @parser.addcontext("Node %s is already defined" % [other.name], other) end name = name.to_s if name.is_a?(Symbol) args = { :name => name, } if options[:code] args[:code] = options[:code] end if options[:parent] args[:parentclass] = options[:parent] end @nodetable[name] = @parser.ast(AST::Node, args) @nodetable[name].fqname = name @nodetable[name] @nodetable[name].interp = self @nodetable[name] end end # Add a new file to be checked when we're checking to see if we should be # reparsed. def newfile(*files) files.each do |file| unless file.is_a? Puppet::LoadedFile file = Puppet::LoadedFile.new(file) end @files << file end end # Search for our node in the various locations. def nodesearch(*nodes) # At this point, stop at the first source that defines # the node @nodesources.each do |source| method = "nodesearch_%s" % source if self.respond_to? method # Do an inverse sort on the length, so the longest match always # wins nodes.sort { |a,b| b.length <=> a.length }.each do |node| node = node.to_s if node.is_a?(Symbol) if obj = self.send(method, node) nsource = obj.file || source Puppet.info "Found %s in %s" % [node, nsource] return obj end end end end # If they made it this far, we haven't found anything, so look for a # default node. unless nodes.include?("default") if defobj = self.nodesearch("default") Puppet.notice "Using default node for %s" % [nodes[0]] return defobj end end return nil end # See if our node was defined in the code. def nodesearch_code(name) @nodetable[name] end # Look for our node in ldap. def nodesearch_ldap(node) parent, classes = ldapsearch(node) if parent or classes args = {} args[:classes] = classes if classes args[:parentnode] = parent if parent return gennode(node, args) else return nil end end def parsedate parsefiles() @parsedate end # evaluate our whole tree def run(client, facts) # We have to leave this for after initialization because there # seems to be a problem keeping ldap open after a fork. unless @setup @nodesources.each { |source| method = "setup_%s" % source.to_s if respond_to? method exceptwrap :type => Puppet::Error, :message => "Could not set up node source %s" % source do self.send(method) end end } end parsefiles() # Evaluate all of the appropriate code. objects = evaluate(client, facts) # And return it all. return objects end # Connect to the LDAP Server def setup_ldap self.class.ldap = nil begin require 'ldap' rescue LoadError Puppet.notice( "Could not set up LDAP Connection: Missing ruby/ldap libraries" ) @ldap = nil return end begin @ldap = self.class.ldap() rescue => detail raise Puppet::Error, "Could not connect to LDAP: %s" % detail end end def scope return @scope end private # Check whether any of our files have changed. def checkfiles if @files.find { |f| f.changed? } @parsedate = Time.now.to_i end end # Parse the files, generating our parse tree. This automatically # reparses only if files are updated, so it's safe to call multiple # times. def parsefiles # First check whether there are updates to any non-puppet files # like templates. If we need to reparse, this will get quashed, # but it needs to be done first in case there's no reparse # but there are other file changes. checkfiles() # Check if the parser should reparse. if @file if defined? @parser if stamp = @parser.reparse? Puppet.notice "Reloading files" else return false end end unless FileTest.exists?(@file) # If we've already parsed, then we're ok. if findclass("", "") return else raise Puppet::Error, "Manifest %s must exist" % @file end end end # Reset our parse tables. clear() # Create a new parser, just to keep things fresh. @parser = Puppet::Parser::Parser.new(self) if @code @parser.string = @code else @parser.file = @file # Mark when we parsed, so we can check freshness @parsedate = File.stat(@file).ctime.to_i end # Parsing stores all classes and defines and such in their # various tables, so we don't worry about the return. if @local @parser.parse else benchmark(:info, "Parsed manifest") do @parser.parse end end @parsedate = Time.now.to_i end # Store the configs into the database. def storeconfigs(hash) unless defined? ActiveRecord require 'puppet/rails' unless defined? ActiveRecord raise LoadError, "storeconfigs is enabled but rails is unavailable" end end Puppet::Rails.init # Fork the storage, since we don't need the client waiting # on that. How do I avoid this duplication? if @forksave fork { # We store all of the objects, even the collectable ones benchmark(:info, "Stored configuration for #{hash[:name]}") do # Try to batch things a bit, by putting them into # a transaction Puppet::Rails::Host.transaction do Puppet::Rails::Host.store(hash) end end } else begin # We store all of the objects, even the collectable ones benchmark(:info, "Stored configuration for #{hash[:name]}") do Puppet::Rails::Host.transaction do Puppet::Rails::Host.store(hash) end end rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not store configs: %s" % detail.to_s end end # Now that we've stored everything, we need to strip out # the collectable objects so that they are not sent on # to the host #hash[:objects].collectstrip! end end # $Id$ diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index cb339e38d..751b9c0c7 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,347 +1,347 @@ # A resource that we're managing. This handles making sure that only subclasses # can set parameters. class Puppet::Parser::Resource require 'puppet/parser/resource/param' require 'puppet/parser/resource/reference' ResParam = Struct.new :name, :value, :source, :line, :file include Puppet::Util include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging - attr_accessor :source, :line, :file, :scope + attr_accessor :source, :line, :file, :scope, :rails_id attr_accessor :virtual, :override, :params, :translated attr_reader :exported attr_writer :tags # Proxy a few methods to our @ref object. [:builtin?, :type, :title].each do |method| define_method(method) do @ref.send(method) end end # Set up some boolean test methods [:exported, :translated, :override].each do |method| newmeth = (method.to_s + "?").intern define_method(newmeth) do self.send(method) end end def [](param) param = symbolize(param) if param == :title return self.title end if @params.has_key?(param) @params[param].value else nil end end # Add default values from our definition. def adddefaults defaults = scope.lookupdefaults(self.type) defaults.each do |name, param| unless @params.include?(param.name) self.debug "Adding default for %s" % param.name @params[param.name] = param end end end # Add any metaparams defined in our scope. This actually adds any metaparams # from any parent scope, and there's currently no way to turn that off. def addmetaparams Puppet::Type.eachmetaparam do |name| next if self[name] if val = scope.lookupvar(name.to_s, false) unless val == :undefined set Param.new(:name => name, :value => val, :source => scope.source) end end end end # Add any overrides for this object. def addoverrides overrides = scope.lookupoverrides(self) overrides.each do |over| self.merge(over) end overrides.clear end def builtin=(bool) @ref.builtin = bool end # Retrieve the associated definition and evaluate it. def evaluate if klass = @ref.definedtype finish() scope.deleteresource(self) return klass.evaluate(:scope => scope, :type => self.type, :name => self.title, :arguments => self.to_hash, :scope => self.scope, :exported => self.exported ) elsif builtin? devfail "Cannot evaluate a builtin type" else self.fail "Cannot find definition %s" % self.type end # if builtin? # devfail "Cannot evaluate a builtin type" # end # # unless klass = scope.finddefine(self.type) # self.fail "Cannot find definition %s" % self.type # end # # finish() # # scope.deleteresource(self) # # return klass.evaluate(:scope => scope, # :type => self.type, # :name => self.title, # :arguments => self.to_hash, # :scope => self.scope, # :exported => self.exported # ) ensure @evaluated = true end def exported=(value) if value @virtual = true @exported = value else @exported = value end end def evaluated? if defined? @evaluated and @evaluated true else false end end # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish addoverrides() adddefaults() addmetaparams() end def initialize(options) options = symbolize_options(options) # Collect the options necessary to make the reference. refopts = [:type, :title].inject({}) do |hash, param| hash[param] = options[param] options.delete(param) hash end @params = {} tmpparams = nil if tmpparams = options[:params] options.delete(:params) end # Now set the rest of the options. set_options(options) @ref = Reference.new(refopts) requiredopts(:scope, :source) @ref.scope = self.scope if tmpparams tmpparams.each do |param| # We use the method here, because it does type-checking. set(param) end end end # Merge an override resource in. def merge(resource) # Some of these might fail, but they'll fail in the way we want. resource.params.each do |name, param| set(param) end end # This *significantly* reduces the number of calls to Puppet.[]. def paramcheck? unless defined? @@paramcheck @@paramcheck = Puppet[:paramcheck] end @@paramcheck end # Verify that all passed parameters are valid. This throws an error if there's # a problem, so we don't have to worry about the return value. def paramcheck(param) # Now make sure it's a valid argument to our class. These checks # are organized in order of commonhood -- most types, it's a valid argument # and paramcheck is enabled. if @ref.typeclass.validattr?(param) true elsif (param == "name" or param == "title") # always allow these true elsif paramcheck? self.fail Puppet::ParseError, "Invalid parameter '%s' for type '%s'" % [param.inspect, @ref.type] end end # A temporary occasion, until I get paths in the scopes figured out. def path to_s end # Return the short version of our name. def ref @ref.to_s end # You have to pass a Resource::Param to this. def set(param) # Because definitions are now parse-time, I can paramcheck immediately. paramcheck(param.name) if current = @params[param.name] # XXX Should we ignore any settings that have the same values? if param.source.child_of?(current.source) # Replace it, keeping all of its info. @params[param.name] = param else if Puppet[:trace] puts caller end fail Puppet::ParseError, "Parameter %s is already set on %s by %s" % [param.name, self.to_s, param.source] end else if self.source == param.source or param.source.child_of?(self.source) @params[param.name] = param else fail Puppet::ParseError, "Only subclasses can set parameters" end end end # Store our object as a Rails object. We need the host object we're storing it # with. def store(host) args = {} %w{type title tags file line exported}.each do |param| if value = self.send(param) args[param] = value end end # 'type' isn't a valid column name, so we have to use something else. args = symbolize_options(args) args[:restype] = args[:type] args.delete(:type) # Let's see if the object exists #if obj = host.rails_resources.find_by_type_and_title(self.type, self.title) if obj = host.rails_resources.find_by_restype_and_title(self.type, self.title) # We exist args.each do |param, value| obj[param] = value end else # Else create it anew obj = host.rails_resources.build(args) end # Either way, now add our parameters @params.each do |name, param| param.store(obj) end return obj end def tags unless defined? @tags @tags = scope.tags @tags << self.type end @tags end def to_hash @params.inject({}) do |hash, ary| param = ary[1] hash[param.name] = param.value hash end end def to_s self.ref end # Translate our object to a transportable object. def to_trans unless builtin? devfail "Tried to translate a non-builtin resource" end return nil if virtual? # Now convert to a transobject obj = Puppet::TransObject.new(@ref.title, @ref.type) to_hash.each do |p, v| if v.is_a?(Reference) v = v.to_ref elsif v.is_a?(Array) v = v.collect { |av| if av.is_a?(Reference) av = av.to_ref end av } end obj[p.to_s] = v end obj.file = self.file obj.line = self.line obj.tags = self.tags return obj end def virtual? self.virtual end end # $Id$ diff --git a/lib/puppet/rails.rb b/lib/puppet/rails.rb index 959ea5628..77afe6be1 100644 --- a/lib/puppet/rails.rb +++ b/lib/puppet/rails.rb @@ -1,144 +1,112 @@ # Load the appropriate libraries, or set a class indicating they aren't available require 'facter' require 'puppet' -begin - require 'active_record' -rescue LoadError => detail - if Facter["operatingsystem"].value == "Debian" and - FileTest.exists?("/usr/share/rails") - count = 0 - Dir.entries("/usr/share/rails").each do |dir| - libdir = File.join("/usr/share/rails", dir, "lib") - if FileTest.exists?(libdir) and ! $:.include?(libdir) - count += 1 - $: << libdir - end - end - - if count > 0 - retry - end - end -end - -# If we couldn't find it the normal way, try using a Gem. -unless defined? ActiveRecord - begin - require 'rubygems' - require_gem 'rails' - rescue LoadError - # Nothing - end -end - module Puppet::Rails Puppet.config.setdefaults(:puppetmaster, :dblocation => { :default => "$statedir/clientconfigs.sqlite3", :mode => 0600, :owner => "$user", :group => "$group", :desc => "The database cache for client configurations. Used for querying within the language." }, :dbadapter => [ "sqlite3", "The type of database to use." ], :dbname => [ "puppet", "The name of the database to use." ], :dbserver => [ "puppet", "The database server for Client caching. Only used when networked databases are used."], :dbuser => [ "puppet", "The database user for Client caching. Only used when networked databases are used."], :dbpassword => [ "puppet", "The database password for Client caching. Only used when networked databases are used."], :railslog => {:default => "$logdir/puppetrails.log", :mode => 0600, :owner => "$user", :group => "$group", :desc => "Where Rails-specific logs are sent" } ) def self.clear @inited = false end # Set up our database connection. It'd be nice to have a "use" system # that could make callbacks. def self.init - unless defined? ActiveRecord::Base + unless Puppet.features.rails? raise Puppet::DevError, "No activerecord, cannot init Puppet::Rails" end - - # This global init does not work for testing, because we remove # the state dir on every test. #unless (defined? @inited and @inited) or defined? Test::Unit::TestCase unless (defined? @inited and @inited) Puppet.config.use(:puppet) ActiveRecord::Base.logger = Logger.new(Puppet[:railslog]) args = {:adapter => Puppet[:dbadapter]} case Puppet[:dbadapter] when "sqlite3": args[:database] = Puppet[:dblocation] #unless FileTest.exists?(Puppet[:dblocation]) # Puppet.config.use(:puppet) # Puppet.config.write(:dblocation) do |f| # f.print "" # end #end when "mysql": args[:host] = Puppet[:dbserver] args[:username] = Puppet[:dbuser] args[:password] = Puppet[:dbpassword] args[:database] = Puppet[:dbname] end begin ActiveRecord::Base.establish_connection(args) rescue => detail if Puppet[:trace] puts detail.backtrace end raise Puppet::Error, "Could not connect to database: %s" % detail end #puts "Database initialized: #{@inited.inspect} " end ActiveRecord::Base.logger = Logger.new(Puppet[:railslog]) if Puppet[:dbadapter] == "sqlite3" and ! FileTest.exists?(Puppet[:dblocation]) dbdir = nil $:.each { |d| tmp = File.join(d, "puppet/rails/database") if FileTest.directory?(tmp) dbdir = tmp end } unless dbdir raise Puppet::Error, "Could not find Puppet::Rails database dir" end begin ActiveRecord::Migrator.migrate(dbdir) rescue => detail if Puppet[:trace] puts detail.backtrace end raise Puppet::Error, "Could not initialize database: %s" % detail end end Puppet.config.use(:puppetmaster) ActiveRecord::Base.logger = Logger.new(Puppet[:railslog]) end end -if defined? ActiveRecord::Base +if Puppet.features.rails? require 'puppet/rails/host' end # $Id$ diff --git a/lib/puppet/rails/rails_resource.rb b/lib/puppet/rails/rails_resource.rb index caad1b460..0b981f963 100644 --- a/lib/puppet/rails/rails_resource.rb +++ b/lib/puppet/rails/rails_resource.rb @@ -1,34 +1,35 @@ require 'puppet' require 'puppet/rails/rails_parameter' #RailsParameter = Puppet::Rails::RailsParameter class Puppet::Rails::RailsResource < ActiveRecord::Base has_many :rails_parameters, :dependent => :delete_all serialize :tags, Array belongs_to :host # Convert our object to a resource. Do not retain whether the object # is collectable, though, since that would cause it to get stripped # from the configuration. def to_resource(scope) hash = self.attributes hash["type"] = hash["restype"] hash.delete("restype") hash.delete("host_id") hash.delete("id") hash.each do |p, v| hash.delete(p) if v.nil? end hash[:scope] = scope hash[:source] = scope.source + hash[:rails_id] = self.id obj = Puppet::Parser::Resource.new(hash) rails_parameters.each do |param| obj.set(param.to_resourceparam(scope.source)) end return obj end end # $Id$ diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index 0d409e430..6b65a4cbc 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,368 +1,368 @@ # A module to collect utility functions. require 'sync' require 'puppet/lock' module Puppet # A command failed to execute. class ExecutionFailure < Puppet::Error end module Util require 'benchmark' require 'puppet/util/posix' extend Puppet::Util::POSIX # Create a hash to store the different sync objects. @@syncresources = {} # Return the sync object associated with a given resource. def self.sync(resource) @@syncresources[resource] ||= Sync.new return @@syncresources[resource] end # Change the process to a different user def self.chuser if Facter["operatingsystem"].value == "Darwin" $stderr.puts "Ruby on darwin is broken; puppetmaster will not set its UID to 'puppet' and must run as root" return end if group = Puppet[:group] group = self.gid(group) unless group raise Puppet::Error, "No such group %s" % Puppet[:group] end unless Puppet::SUIDManager.gid == group begin Puppet::SUIDManager.egid = group Puppet::SUIDManager.gid = group rescue => detail Puppet.warning "could not change to group %s: %s" % [group.inspect, detail] $stderr.puts "could not change to group %s" % group.inspect # Don't exit on failed group changes, since it's # not fatal #exit(74) end end end if user = Puppet[:user] user = self.uid(user) unless user raise Puppet::Error, "No such user %s" % Puppet[:user] end unless Puppet::SUIDManager.uid == user begin Puppet::SUIDManager.uid = user Puppet::SUIDManager.euid = user rescue $stderr.puts "could not change to user %s" % user exit(74) end end end end # Create a shared lock for reading def self.readlock(file) self.sync(file).synchronize(Sync::SH) do File.open(file) { |f| f.lock_shared { |lf| yield lf } } end end # Create an exclusive lock for writing, and do the writing in a # tmp file. def self.writelock(file, mode = 0600) tmpfile = file + ".tmp" unless FileTest.directory?(File.dirname(tmpfile)) raise Puppet::DevError, "Cannot create %s; directory %s does not exist" % [file, File.dirname(file)] end self.sync(file).synchronize(Sync::EX) do File.open(file, "w", mode) do |rf| rf.lock_exclusive do |lrf| File.open(tmpfile, "w", mode) do |tf| yield tf end begin File.rename(tmpfile, file) rescue => detail Puppet.err "Could not rename %s to %s: %s" % [file, tmpfile, detail] end end end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| if args.is_a?(Array) args = args.join(" ") end if useself Puppet::Log.create( :level => level, :source => self, :message => args ) else Puppet::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) if FileTest.exist?(dir) return false else tmp = dir.sub(/^\//,'') path = [File::SEPARATOR] tmp.split(File::SEPARATOR).each { |dir| path.push dir if ! FileTest.exist?(File.join(path)) Dir.mkdir(File.join(path), mode) elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise "Cannot create %s: basedir %s is a file" % [dir, File.join(path)] end } return true end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? object = Puppet else object = args.pop end unless level raise Puppet::DevError, "Failed to provide level to :benchmark" end unless object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to %s" % level end # Only benchmark if our log level is high enough if level != :none and Puppet::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def binary(bin) if bin =~ /^\// if FileTest.exists? bin return true else return nil end else ENV['PATH'].split(":").each do |dir| if FileTest.exists? File.join(dir, bin) return File.join(dir, bin) end end return nil end end module_function :binary # Execute the provided command in a pipe, yielding the pipe object. def execpipe(command, failonfail = true) if respond_to? :debug debug "Executing '%s'" % command else Puppet.debug "Executing '%s'" % command end output = open("| #{command} 2>&1") do |pipe| yield pipe end if failonfail unless $? == 0 raise ExecutionFailure, output end end return output end def execfail(command, exception) begin output = execute(command) return output rescue ExecutionFailure raise exception, output end end # Execute the desired command, and return the status and output. def execute(command, failonfail = true) if respond_to? :debug debug "Executing '%s'" % command else Puppet.debug "Executing '%s'" % command end command += " 2>&1" unless command =~ />/ output = %x{#{command}} if failonfail unless $? == 0 raise ExecutionFailure, "Could not execute '%s': %s" % [command, output] end end return output end module_function :execute # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.sync(resource).synchronize(type) do yield end end # Because some modules provide their own version of this method. alias util_execute execute module_function :benchmark def memory unless defined? @pmap pmap = %x{which pmap 2>/dev/null}.chomp if $? != 0 or pmap =~ /^no/ @pmap = nil else @pmap = pmap end end if @pmap return %x{pmap #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end end def symbolizehash!(hash) hash.each do |name, val| if name.is_a? String hash[name.intern] = val hash.delete(name) end end return hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } return seconds end - module_function :memory + module_function :memory, :thinmark end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' # $Id$ diff --git a/test/language/collector.rb b/test/language/collector.rb index ceab76ccc..b3d948768 100755 --- a/test/language/collector.rb +++ b/test/language/collector.rb @@ -1,249 +1,293 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet/rails' require 'puppettest' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' require 'puppettest/railstesting' class TestCollector < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::RailsTesting Parser = Puppet::Parser AST = Parser::AST def setup super Puppet[:trace] = false @interp, @scope, @source = mkclassframing end # Test just collecting a specific resource. This is used by the 'realize' # function, and it's much faster than iterating over all of the resources. def test_collect_resource # Make a couple of virtual resources one = mkresource(:type => "file", :title => "/tmp/virtual1", :virtual => true, :params => {:owner => "root"}) two = mkresource(:type => "file", :title => "/tmp/virtual2", :virtual => true, :params => {:owner => "root"}) @scope.setresource one @scope.setresource two # Now make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :virtual) end # Now set the resource in the collector assert_nothing_raised do coll.resources = one.ref end # Now run the collector assert_nothing_raised do coll.evaluate end # And make sure the resource is no longer virtual assert(! one.virtual?, "Resource is still virtual") # But the other still is assert(two.virtual?, "Resource got realized") end def test_virtual # Make a virtual resource virtual = mkresource(:type => "file", :title => "/tmp/virtual", :virtual => true, :params => {:owner => "root"}) @scope.setresource virtual # And a non-virtual real = mkresource(:type => "file", :title => "/tmp/real", :params => {:owner => "root"}) @scope.setresource real # Now make a collector coll = nil # Make a fake query code = proc do |res| true end assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, code, :virtual) end # Set it in our scope @scope.newcollection(coll) # Make sure it's in the collections assert_equal([coll], @scope.collections) # And try to collect the virtual resources. ret = nil assert_nothing_raised do ret = coll.collect_virtual end assert_equal([virtual], ret) # Now make sure evaluate does the right thing. assert_nothing_raised do ret = coll.evaluate end # Make sure it got deleted from the collection list assert_equal([], @scope.collections) # And make sure our virtual object is no longer virtual assert(! virtual.virtual?, "Virtual object did not get realized") # Now make a new collector of a different type and make sure it # finds nothing. assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "exec", nil, nil, :virtual) end # Remark this as virtual virtual.virtual = true assert_nothing_raised do ret = coll.evaluate end assert_equal([], ret) end if defined? ActiveRecord::Base def test_collect_exported railsinit # make an exported resource exported = mkresource(:type => "file", :title => "/tmp/exported", :exported => true, :params => {:owner => "root"}) @scope.setresource exported assert(exported.exported?, "Object was not marked exported") assert(exported.virtual?, "Object was not marked virtual") # And a non-exported real = mkresource(:type => "file", :title => "/tmp/real", :params => {:owner => "root"}) @scope.setresource real # Now make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :exported) end # Set it in our scope @scope.newcollection(coll) # Make sure it's in the collections assert_equal([coll], @scope.collections) # And try to collect the virtual resources. ret = nil assert_nothing_raised do ret = coll.collect_exported end assert_equal([exported], ret) # Now make sure evaluate does the right thing. assert_nothing_raised do ret = coll.evaluate end # Make sure it got deleted from the collection list assert_equal([], @scope.collections) # And make sure our exported object is no longer exported assert(! exported.virtual?, "Virtual object did not get realized") # But it should still be marked exported. assert(exported.exported?, "Resource got un-exported") # Now make a new collector of a different type and make sure it # finds nothing. assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "exec", nil, nil, :exported) end # Remark this as virtual exported.virtual = true assert_nothing_raised do ret = coll.evaluate end assert_equal([], ret) + + # Now create a whole new scope and make sure we can actually retrieve + # the resource from the database, not just from the scope. + # First create a host object and store our resource in it. + host = Puppet::Rails::Host.find_or_create_by_name("localhost") + assert(host, "did not get rails host") + assert_nothing_raised("could not store resource") do + exported.store(host) + end + host.save + + # And make sure it's in there + newres = Puppet::Rails::RailsResource.find_by_title("/tmp/exported") + assert(newres, "Did not find resource in db") + interp, scope, source = mkclassframing + + # Now make a collector + coll = nil + assert_nothing_raised do + coll = Puppet::Parser::Collector.new(scope, "file", nil, nil, :exported) + end + + # Set it in our scope + scope.newcollection(coll) + + # Make sure it's in the collections + assert_equal([coll], scope.collections) + + # And try to collect the virtual resources. + ret = nil + assert_nothing_raised do + ret = coll.collect_exported + end + + assert_equal(["/tmp/exported"], ret.collect { |f| f.title }) + + # Make sure we can evaluate the same collection multiple times and + # that later collections do nothing + assert_nothing_raised do + ret = coll.evaluate + end + + # Make sure it got deleted from the collection list + assert_equal([], scope.collections) end def test_collection_conflicts railsinit # First make a railshost we can conflict with host = Puppet::Rails::Host.new(:name => "myhost") host.rails_resources.build(:title => "/tmp/conflicttest", :restype => "file", :exported => true) host.save # Now make a normal resource normal = mkresource(:type => "file", :title => "/tmp/conflicttest", :params => {:owner => "root"}) @scope.setresource normal # Now make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :exported) end # And try to collect the virtual resources. assert_raise(Puppet::ParseError) do ret = coll.collect_exported end end # Make sure we do not collect resources from the host we're on def test_no_resources_from_me railsinit # Make our configuration host = Puppet::Rails::Host.new(:name => "myhost") host.rails_resources.build(:title => "/tmp/hosttest", :restype => "file", :exported => true) host.save @scope.host = "myhost" # Now make a collector coll = nil assert_nothing_raised do coll = Puppet::Parser::Collector.new(@scope, "file", nil, nil, :exported) end # And make sure we get nada back ret = nil assert_nothing_raised do ret = coll.collect_exported end assert(ret.empty?, "Found exports from our own host") end end end # $Id$ diff --git a/test/other/features.rb b/test/other/features.rb index fb643c926..2ae623335 100755 --- a/test/other/features.rb +++ b/test/other/features.rb @@ -1,60 +1,60 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2006-11-07. # Copyright (c) 2006. All rights reserved. $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppet/feature' class TestFeatures < Test::Unit::TestCase include PuppetTest def setup super libdir = tempfile() @features = Puppet::Feature.new(libdir) end def test_new assert_nothing_raised do @features.add(:failer) do raise ArgumentError, "nopes" end end assert(@features.respond_to?(:failer?), "Feature method did not get added") assert_nothing_raised("failure propagated outside of feature") do assert(! @features.failer?, "failure was considered true") end # Now make one that succeeds $succeeds = nil assert_nothing_raised("Failed to add normal feature") do @features.add(:succeeds) do $succeeds = true end end assert($succeeds, "Block was not called on initialization") assert(@features.respond_to?(:succeeds?), "Did not add succeeding feature") assert_nothing_raised("Failed to call succeeds") { assert(@features.succeeds?, "Feature was not true") } end def test_libs assert_nothing_raised do @features.add(:puppet, :libs => %w{puppet}) end assert(@features.puppet?) assert_nothing_raised do @features.add(:missing, :libs => %w{puppet no/such/library/okay}) end assert(! @features.missing?, "Missing lib was considered true") end end -# $Id$ \ No newline at end of file +# $Id$