diff --git a/lib/puppet.rb b/lib/puppet.rb index a802688e8..b0b3bbea9 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,535 +1,536 @@ # 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/config' 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.19.3' 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 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 # Store a new default value. def self.setdefaults(section, hash) @@config.setdefaults(section, hash) end # If we're running the standalone puppet process as a non-root user, # use basedirs that are in the user's home directory. conf = nil var = nil if self.name == "puppet" and Puppet::SUIDManager.uid != 0 conf = File.expand_path("~/.puppet") var = File.expand_path("~/.puppet/var") else # Else, use system-wide directories. conf = "/etc/puppet" var = "/var/puppet" end self.setdefaults(:puppet, :confdir => [conf, "The main Puppet configuration directory."], :vardir => [var, "Where Puppet stores dynamic and growing data."] ) if self.name == "puppetmasterd" self.setdefaults(:puppetmasterd, :logdir => {:default => "$vardir/log", :mode => 0750, :owner => "$user", :group => "$group", :desc => "The Puppet log directory." } ) else self.setdefaults(:puppet, :logdir => ["$vardir/log", "The Puppet log directory."] ) end self.setdefaults(:puppet, :trace => [false, "Whether to print stack traces on some errors"], :syslogfacility => ["daemon", "What syslog facility to use when logging to syslog. Syslog has a fixed list of valid facilities, and you must choose one of those; you cannot just make one up."], :statedir => { :default => "$vardir/state", :mode => 01777, :desc => "The directory where Puppet state is stored. Generally, this directory can be removed without causing harm (although it might result in spurious service restarts)." }, :rundir => { :default => "$vardir/run", :mode => 01777, :desc => "Where Puppet PID files are kept." }, :lockdir => { :default => "$vardir/locks", :mode => 01777, :desc => "Where lock files are kept." }, :statefile => { :default => "$statedir/state.yaml", :mode => 0660, :desc => "Where puppetd and puppetmasterd store state associated with the running configuration. In the case of puppetmasterd, this file reflects the state discovered through interacting with clients." }, :ssldir => { :default => "$confdir/ssl", :mode => 0771, :owner => "root", :desc => "Where SSL certificates are kept." }, :genconfig => [false, "Whether to just print a configuration to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :genmanifest => [false, "Whether to just print a manifest to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :configprint => ["", "Print the value of a specific configuration parameter. If a parameter is provided for this, then the value is printed and puppet exits. Comma-separate multiple values. For a list of all values, specify 'all'. This feature is only available in Puppet versions higher than 0.18.4."], :color => [true, "Whether to use ANSI colors when logging to the console."], :mkusers => [false, "Whether to create the necessary user and group that puppetd will run as."] ) # Define the config default. self.setdefaults(self.name, :config => ["$confdir/#{self.name}.conf", "The configuration file for #{self.name}."] ) self.setdefaults("puppetmasterd", :user => ["puppet", "The user puppetmasterd should run as."], :group => ["puppet", "The group puppetmasterd should run as."], :manifestdir => ["$confdir/manifests", "Where puppetmasterd looks for its manifests."], :manifest => ["$manifestdir/site.pp", "The entry-point manifest for puppetmasterd."], :masterlog => { :default => "$logdir/puppetmaster.log", :owner => "$user", :group => "$group", :mode => 0660, :desc => "Where puppetmasterd logs. This is generally not used, since syslog is the default log destination." }, :masterhttplog => { :default => "$logdir/masterhttp.log", :owner => "$user", :group => "$group", :mode => 0660, :create => true, :desc => "Where the puppetmasterd web server logs." }, :masterport => [8140, "Which port puppetmasterd listens on."], :parseonly => [false, "Just check the syntax of the manifests."], :node_name => ["cert", "How the puppetmaster determines the client's identity and sets the 'hostname' fact for use in the manifest, in particular for determining which 'node' statement applies to the client. Possible values are 'cert' (use the subject's CN in the client's certificate) and 'facter' (use the hostname that the client reported in its facts)"] ) self.setdefaults("puppetd", :localconfig => { :default => "$confdir/localconfig", :owner => "root", :mode => 0660, :desc => "Where puppetd caches the local configuration. An extension indicating the cache format is added automatically."}, :classfile => { :default => "$confdir/classes.txt", :owner => "root", :mode => 0644, :desc => "The file in which puppetd stores a list of the classes associated with the retrieved configuratiion. Can be loaded in the separate ``puppet`` executable using the ``--loadclasses`` option."}, :puppetdlog => { :default => "$logdir/puppetd.log", :owner => "root", :mode => 0640, :desc => "The log file for puppetd. This is generally not used." }, :httplog => { :default => "$logdir/http.log", :owner => "root", :mode => 0640, :desc => "Where the puppetd web server logs." }, :server => ["puppet", "The server to which server puppetd should connect"], :ignoreschedules => [false, "Boolean; whether puppetd should ignore schedules. This is useful for initial puppetd runs."], :puppetport => [8139, "Which port puppetd listens on."], :noop => [false, "Whether puppetd should be run in noop mode."], :runinterval => [1800, # 30 minutes "How often puppetd applies the client configuration; in seconds"] ) # 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) 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 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.each do |svc| newthread do begin svc.start rescue => detail if Puppet[:debug] puts detail.backtrace end @services.delete svc Puppet.err "Could not start %s: %s" % [svc.class, detail] end end end unless @services.length > 0 Puppet.notice "No remaining services; exiting" exit(1) end # We need to give the services a chance to register their timers before # we try to start monitoring them. sleep 0.5 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 end require 'puppet/server' require 'puppet/type' require 'puppet/storage' # $Id$ diff --git a/lib/puppet/element.rb b/lib/puppet/element.rb index cc682f2e4..934fafeb9 100644 --- a/lib/puppet/element.rb +++ b/lib/puppet/element.rb @@ -1,71 +1,72 @@ # included so we can test object types require 'puppet' # the base class for both types and states # very little functionality; basically just defines the interface # and provides a few simple across-the-board functions like 'noop' class Puppet::Element include Puppet include Puppet::Util + include Puppet::Util::Errors attr_writer :noop class << self attr_accessor :doc, :nodoc include Puppet::Util end # all of our subclasses must respond to each of these methods... @@interface_methods = [ :retrieve, :insync?, :sync, :evaluate ] # so raise an error if a method that isn't overridden gets called @@interface_methods.each { |method| self.send(:define_method,method) { raise Puppet::DevError, "%s has not overridden %s" % [self.class.name,method] } } Puppet::Util.logmethods(self, true) # for testing whether we should actually do anything def noop unless defined? @noop @noop = false end return @noop || Puppet[:noop] || false end # return the full path to us, for logging and rollback # some classes (e.g., FileTypeRecords) will have to override this def path unless defined? @path if defined? @parent and @parent if self.is_a?(Puppet.type(:component)) @path = [@parent.path, self.name] else @path = [@parent.path, self.class.name.to_s + "=" + self.name] end else - # The top-level name is always puppet[top], so we don't bother with + # The top-level name is always main[top], so we don't bother with # that. And we don't add the hostname here, it gets added # in the log server thingy. - if self.name == "puppet[top]" + if self.name == "main[top]" @path = ["/"] else if self.is_a?(Puppet.type(:component)) @path = [self.name] else @path = [self.class.name.to_s + "=" + self.name.to_s] end end end end return @path.join("/") end end # $Id$ diff --git a/lib/puppet/loadedfile.rb b/lib/puppet/loadedfile.rb index b0e408475..c14bcc195 100755 --- a/lib/puppet/loadedfile.rb +++ b/lib/puppet/loadedfile.rb @@ -1,66 +1,67 @@ # A simple class that tells us when a file has changed and thus whether we # should reload it require 'puppet' module Puppet class NoSuchFile < Puppet::Error; end class LoadedFile attr_reader :file, :statted # Provide a hook for setting the timestamp during testing, so we don't # have to depend on the granularity of the filesystem. attr_writer :tstamp Puppet.config.setdefaults(:puppet, :filetimeout => [ 15, "The minimum time to wait between checking for updates in configuration files." ] ) # Determine whether the file has changed and thus whether it should # be reparsed. def changed? tmp = stamp() # We use a different internal variable than the stamp method # because it doesn't keep historical state and we do -- that is, # we will always be comparing two timestamps, whereas # stamp() just always wants the latest one. if tmp == @tstamp return false else @tstamp = tmp return @tstamp end end # Create the file. Must be passed the file path. def initialize(file) @file = file unless FileTest.exists?(@file) raise Puppet::NoSuchFile, "Can not use a non-existent file for parsing" end @statted = 0 + @stamp = nil @tstamp = stamp() end # Retrieve the filestamp, but only refresh it if we're beyond our # filetimeout def stamp if @stamp.nil? or (Time.now.to_i - @statted >= Puppet[:filetimeout]) @statted = Time.now.to_i @stamp = File.stat(@file).ctime end return @stamp end def to_s @file end end end # $Id$ diff --git a/lib/puppet/log.rb b/lib/puppet/log.rb index 0659042ce..c8d92e649 100644 --- a/lib/puppet/log.rb +++ b/lib/puppet/log.rb @@ -1,523 +1,521 @@ require 'syslog' module Puppet # Pass feedback to the user. Log levels are modeled after syslog's, and it is # expected that that will be the most common log destination. Supports # multiple destinations, one of which is a remote server. class Log include Puppet::Util PINK="" GREEN="" YELLOW="" SLATE="" ORANGE="" BLUE="" RESET="" @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] @loglevel = 2 @desttypes = {} # A type of log destination. class Destination class << self attr_accessor :name end def self.initvars @matches = [] end # Mark the things we're supposed to match. def self.match(obj) @matches ||= [] @matches << obj end # See whether we match a given thing. def self.match?(obj) # Convert single-word strings into symbols like :console and :syslog if obj.is_a? String and obj =~ /^\w+$/ obj = obj.downcase.intern end @matches.each do |thing| # Search for direct matches or class matches return true if thing === obj or thing == obj.class.to_s end return false end def name if defined? @name return @name else return self.class.name end end # Set how to handle a message. def self.sethandler(&block) define_method(:handle, &block) end # Mark how to initialize our object. def self.setinit(&block) define_method(:initialize, &block) end end # Create a new destination type. def self.newdesttype(name, options = {}, &block) dest = genclass(name, :parent => Destination, :prefix => "Dest", :block => block, :hash => @desttypes, :attributes => options ) dest.match(dest.name) return dest end @destinations = {} class << self include Puppet::Util include Puppet::Util::ClassGen end # Reset all logs to basics. Basically just closes all files and undefs # all of the other objects. def Log.close(dest = nil) if dest if @destinations.include?(dest) if @destinations.respond_to?(:close) @destinations[dest].close end @destinations.delete(dest) end else @destinations.each { |name, dest| if dest.respond_to?(:flush) dest.flush end if dest.respond_to?(:close) dest.close end } @destinations = {} end end # Flush any log destinations that support such operations. def Log.flush @destinations.each { |type, dest| if dest.respond_to?(:flush) dest.flush end } end # Create a new log message. The primary role of this method is to # avoid creating log messages below the loglevel. def Log.create(hash) unless hash.include?(:level) raise Puppet::DevError, "Logs require a level" end unless @levels.index(hash[:level]) raise Puppet::DevError, "Invalid log level %s" % hash[:level] end if @levels.index(hash[:level]) >= @loglevel return Puppet::Log.new(hash) else return nil end end def Log.destinations return @destinations.keys end # Yield each valid level in turn def Log.eachlevel @levels.each { |level| yield level } end # Return the current log level. def Log.level return @levels[@loglevel] end # Set the current log level. def Log.level=(level) unless level.is_a?(Symbol) level = level.intern end unless @levels.include?(level) raise Puppet::DevError, "Invalid loglevel %s" % level end @loglevel = @levels.index(level) end def Log.levels @levels.dup end newdesttype :syslog do def close Syslog.close end def initialize if Syslog.opened? Syslog.close end name = Puppet.name name = "puppet-#{name}" unless name =~ /puppet/ options = Syslog::LOG_PID | Syslog::LOG_NDELAY # XXX This should really be configurable. str = Puppet[:syslogfacility] begin facility = Syslog.const_get("LOG_#{str.upcase}") rescue NameError raise Puppet::Error, "Invalid syslog facility %s" % str end @syslog = Syslog.open(name, options, facility) end def handle(msg) # XXX Syslog currently has a bug that makes it so you # cannot log a message with a '%' in it. So, we get rid # of them. if msg.source == "Puppet" @syslog.send(msg.level, msg.to_s.gsub("%", '%%')) else @syslog.send(msg.level, "(%s) %s" % [msg.source.to_s.gsub("%", ""), msg.to_s.gsub("%", '%%') ] ) end end end newdesttype :file do match(/^\//) def close if defined? @file @file.close @file = nil end end def flush if defined? @file @file.flush end end def initialize(path) @name = path # first make sure the directory exists # We can't just use 'Config.use' here, because they've # specified a "special" destination. unless FileTest.exist?(File.dirname(path)) Puppet.recmkdir(File.dirname(path)) Puppet.info "Creating log directory %s" % File.dirname(path) end # create the log file, if it doesn't already exist file = File.open(path, File::WRONLY|File::CREAT|File::APPEND) @file = file end def handle(msg) @file.puts("%s %s (%s): %s" % [msg.time, msg.source, msg.level, msg.to_s]) end end newdesttype :console do @@colors = { :debug => SLATE, :info => GREEN, :notice => PINK, :warning => ORANGE, :err => YELLOW, :alert => BLUE, :emerg => RESET, :crit => RESET } def initialize # Flush output immediately. $stdout.sync = true end def handle(msg) color = "" reset = "" if Puppet[:color] color = @@colors[msg.level] reset = RESET end if msg.source == "Puppet" puts color + "%s: %s" % [ msg.level, msg.to_s ] + reset else puts color + "%s: %s: %s" % [ msg.level, msg.source, msg.to_s ] + reset end end end newdesttype :host do def initialize(host) Puppet.info "Treating %s as a hostname" % host args = {} if host =~ /:(\d+)/ args[:Port] = $1 args[:Server] = host.sub(/:\d+/, '') else args[:Server] = host end @name = host @driver = Puppet::Client::LogClient.new(args) end def handle(msg) unless msg.is_a?(String) or msg.remote unless defined? @hostname @hostname = Facter["hostname"].value end unless defined? @domain @domain = Facter["domain"].value if @domain @hostname += "." + @domain end end if msg.source =~ /^\// msg.source = @hostname + ":" + msg.source elsif msg.source == "Puppet" msg.source = @hostname + " " + msg.source else msg.source = @hostname + " " + msg.source end begin #puts "would have sent %s" % msg #puts "would have sent %s" % # CGI.escape(YAML.dump(msg)) begin tmp = CGI.escape(YAML.dump(msg)) rescue => detail puts "Could not dump: %s" % detail.to_s return end # Add the hostname to the source @driver.addlog(tmp) rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err detail Puppet::Log.close(self) end end end end # Log to a transaction report. newdesttype :report do match "Puppet::Transaction::Report" def initialize(report) @report = report end def handle(msg) # Only add messages from objects, since anything else is # probably unrelated to this run. if msg.objectsource? @report.newlog(msg) end end end # Create a new log destination. def Log.newdestination(dest) # Each destination can only occur once. if @destinations.find { |name, obj| obj.name == dest } return end name, type = @desttypes.find do |name, klass| klass.match?(dest) end unless type raise Puppet::DevError, "Unknown destination type %s" % dest end begin if type.instance_method(:initialize).arity == 1 @destinations[dest] = type.new(dest) else @destinations[dest] = type.new() end rescue => detail if Puppet[:debug] puts detail.backtrace end # If this was our only destination, then add the console back in. if @destinations.empty? and (dest != :console and dest != "console") newdestination(:console) end end end # Route the actual message. FIXME There are lots of things this method # should do, like caching, storing messages when there are not yet # destinations, a bit more. It's worth noting that there's a potential # for a loop here, if the machine somehow gets the destination set as # itself. def Log.newmessage(msg) if @levels.index(msg.level) < @loglevel return end @destinations.each do |name, dest| threadlock(dest) do dest.handle(msg) end end end def Log.sendlevel?(level) @levels.index(level) >= @loglevel end # Reopen all of our logs. def Log.reopen types = @destinations.keys @destinations.each { |type, dest| if dest.respond_to?(:close) dest.close end } @destinations.clear # We need to make sure we always end up with some kind of destination begin types.each { |type| Log.newdestination(type) } rescue => detail if @destinations.empty? Log.newdestination(:syslog) Puppet.err detail.to_s end end end # Is the passed level a valid log level? def self.validlevel?(level) @levels.include?(level) end attr_accessor :level, :message, :time, :tags, :remote attr_reader :source def initialize(args) unless args.include?(:level) && args.include?(:message) raise Puppet::DevError, "Puppet::Log called incorrectly" end if args[:level].class == String @level = args[:level].intern elsif args[:level].class == Symbol @level = args[:level] else raise Puppet::DevError, "Level is not a string or symbol: #{args[:level].class}" end # Just return unless we're actually at a level we should send #return unless self.class.sendlevel?(@level) @message = args[:message].to_s @time = Time.now # this should include the host name, and probly lots of other # stuff, at some point unless self.class.validlevel?(level) raise Puppet::DevError, "Invalid message level #{level}" end if args.include?(:tags) @tags = args[:tags] end if args.include?(:source) self.source = args[:source] else @source = "Puppet" end Log.newmessage(self) end # Was the source of this log an object? def objectsource? if defined? @objectsource and @objectsource @objectsource else false end end # If they pass a source in to us, we make sure it is a string, and # we retrieve any tags we can. def source=(source) - # We can't store the actual source, we just store the path. This - # is a bit of a stupid hack, specifically testing for elements, but - # eh. - if source.is_a?(Puppet::Element) and source.respond_to?(:path) + # We can't store the actual source, we just store the path. + if source.respond_to?(:path) @objectsource = true @source = source.path else @objectsource = false @source = source.to_s end unless defined? @tags and @tags if source.respond_to?(:tags) @tags = source.tags end end end def tagged?(tag) @tags.detect { |t| t.to_s == tag.to_s } end def to_report "%s %s (%s): %s" % [self.time, self.source, self.level, self.to_s] end def to_s return @message end end end # $Id$ diff --git a/lib/puppet/parser/ast.rb b/lib/puppet/parser/ast.rb index ccde7928b..6f9c9492c 100644 --- a/lib/puppet/parser/ast.rb +++ b/lib/puppet/parser/ast.rb @@ -1,160 +1,128 @@ # the parent class for all of our syntactical objects require 'puppet' require 'puppet/autoload' -module Puppet - module Parser - # The base class for all of the objects that make up the parse trees. - # Handles things like file name, line #, and also does the initialization - # for all of the parameters of all of the child objects. - class AST - # Do this so I don't have to type the full path in all of the subclasses - AST = Puppet::Parser::AST - - Puppet.setdefaults("ast", - :typecheck => [true, "Whether to validate types during parsing."], - :paramcheck => [true, "Whether to validate parameters during parsing."] - ) - attr_accessor :line, :file, :parent, :scope - - # Just used for 'tree', which is only used in debugging. - @@pink = "" - @@green = "" - @@yellow = "" - @@slate = "" - @@reset = "" - - # Just used for 'tree', which is only used in debugging. - @@indent = " " * 4 - @@indline = @@pink + ("-" * 4) + @@reset - @@midline = @@slate + ("-" * 4) + @@reset - - @@settypes = {} - - # Just used for 'tree', which is only used in debugging. - def AST.indention - return @@indent * @@indention - end - - # Just used for 'tree', which is only used in debugging. - def AST.midline - return @@midline - end - - # Evaluate the current object. Basically just iterates across all - # of the contained children and evaluates them in turn, returning a - # list of all of the collected values, rejecting nil values - def evaluate(args) - #Puppet.debug("Evaluating ast %s" % @name) - value = self.collect { |obj| - obj.safeevaluate(args) - }.reject { |obj| - obj.nil? - } - end - - # The version of the evaluate method that should be called, because it - # correctly handles errors. It is critical to use this method because - # it can enable you to catch the error where it happens, rather than - # much higher up the stack. - def safeevaluate(*args) - begin - self.evaluate(*args) - rescue Puppet::DevError => except - except.line ||= @line - except.file ||= @file - raise - rescue Puppet::ParseError => except - except.line ||= @line - except.file ||= @file - raise - rescue => detail - error = Puppet::DevError.new( - "Child of type %s failed with error %s: %s" % - [self.class, detail.class, detail.to_s] - ) - error.line ||= @line - error.file ||= @file - error.set_backtrace detail.backtrace - raise error - end - end - - # Again, just used for printing out the parse tree. - def typewrap(string) - #return self.class.to_s.sub(/.+::/,'') + - #"(" + @@green + string.to_s + @@reset + ")" - return @@green + string.to_s + @@reset + - "(" + self.class.to_s.sub(/.+::/,'') + ")" - end - - # Initialize the object. Requires a hash as the argument, and - # takes each of the parameters of the hash and calls the settor - # method for them. This is probably pretty inefficient and should - # likely be changed at some point. - def initialize(args) - @file = nil - @line = nil - args.each { |param,value| - method = param.to_s + "=" - unless self.respond_to?(method) - error = Puppet::ParseError.new( - "Invalid parameter %s to object class %s" % - [param,self.class.to_s] - ) - error.line = self.line - error.file = self.file - raise error - end - - begin - #Puppet.debug("sending %s to %s" % [method, self.class]) - self.send(method,value) - rescue => detail - error = Puppet::DevError.new( - "Could not set parameter %s on class %s: %s" % - [method,self.class.to_s,detail] - ) - error.line ||= self.line - error.file ||= self.file - raise error - end - } - end - #--------------------------------------------------------------- - # Now autoload everything. - # XXX We can't do this, because it causes multiple loads of some - # things. - #@autoloader = Puppet::Autoload.new(self, - # "puppet/parser/ast" - #) - #@autoloader.loadall +# The base class for all of the objects that make up the parse trees. +# Handles things like file name, line #, and also does the initialization +# for all of the parameters of all of the child objects. +class Puppet::Parser::AST + # Do this so I don't have to type the full path in all of the subclasses + AST = Puppet::Parser::AST + + include Puppet::Util::Errors + include Puppet::Util::MethodHelper + + Puppet.setdefaults("ast", + :typecheck => [true, "Whether to validate types during parsing."], + :paramcheck => [true, "Whether to validate parameters during parsing."] + ) + attr_accessor :line, :file, :parent, :scope + + # Just used for 'tree', which is only used in debugging. + @@pink = "" + @@green = "" + @@yellow = "" + @@slate = "" + @@reset = "" + + # Just used for 'tree', which is only used in debugging. + @@indent = " " * 4 + @@indline = @@pink + ("-" * 4) + @@reset + @@midline = @@slate + ("-" * 4) + @@reset + + @@settypes = {} + + # Just used for 'tree', which is only used in debugging. + def AST.indention + return @@indent * @@indention + end + + # Just used for 'tree', which is only used in debugging. + def AST.midline + return @@midline + end + + # Evaluate the current object. Basically just iterates across all + # of the contained children and evaluates them in turn, returning a + # list of all of the collected values, rejecting nil values + def evaluate(args) + #Puppet.debug("Evaluating ast %s" % @name) + value = self.collect { |obj| + obj.safeevaluate(args) + }.reject { |obj| + obj.nil? + } + end + + # Throw a parse error. + def parsefail(message) + self.fail(Puppet::ParseError, message) + end + + # Wrap a statemp in a reusable way so we always throw a parse error. + def parsewrap + exceptwrap :type => Puppet::ParseError do + yield + end + end + + # The version of the evaluate method that should be called, because it + # correctly handles errors. It is critical to use this method because + # it can enable you to catch the error where it happens, rather than + # much higher up the stack. + def safeevaluate(*args) + exceptwrap do + self.evaluate(*args) end end + + # Again, just used for printing out the parse tree. + def typewrap(string) + #return self.class.to_s.sub(/.+::/,'') + + #"(" + @@green + string.to_s + @@reset + ")" + return @@green + string.to_s + @@reset + + "(" + self.class.to_s.sub(/.+::/,'') + ")" + end + + # Initialize the object. Requires a hash as the argument, and + # takes each of the parameters of the hash and calls the settor + # method for them. This is probably pretty inefficient and should + # likely be changed at some point. + def initialize(args) + @file = nil + @line = nil + set_options(args) + end + #--------------------------------------------------------------- + # Now autoload everything. + # XXX We can't do this, because it causes multiple loads of some + # things. + @autoloader = Puppet::Autoload.new(self, + "puppet/parser/ast" + ) + @autoloader.loadall end -require 'puppet/parser/ast/astarray' -require 'puppet/parser/ast/branch' -require 'puppet/parser/ast/collection' -require 'puppet/parser/ast/caseopt' -require 'puppet/parser/ast/casestatement' -require 'puppet/parser/ast/classdef' -require 'puppet/parser/ast/compdef' -require 'puppet/parser/ast/component' -require 'puppet/parser/ast/else' -require 'puppet/parser/ast/hostclass' -require 'puppet/parser/ast/ifstatement' -require 'puppet/parser/ast/leaf' -require 'puppet/parser/ast/node' -require 'puppet/parser/ast/nodedef' -require 'puppet/parser/ast/objectdef' -require 'puppet/parser/ast/objectparam' -require 'puppet/parser/ast/objectref' -require 'puppet/parser/ast/selector' -require 'puppet/parser/ast/typedefaults' -require 'puppet/parser/ast/vardef' -require 'puppet/parser/ast/tag' -require 'puppet/parser/ast/function' +#require 'puppet/parser/ast/astarray' +#require 'puppet/parser/ast/branch' +#require 'puppet/parser/ast/collection' +#require 'puppet/parser/ast/caseopt' +#require 'puppet/parser/ast/casestatement' +#require 'puppet/parser/ast/component' +#require 'puppet/parser/ast/else' +#require 'puppet/parser/ast/hostclass' +#require 'puppet/parser/ast/ifstatement' +#require 'puppet/parser/ast/leaf' +#require 'puppet/parser/ast/node' +#require 'puppet/parser/ast/resourcedef' +#require 'puppet/parser/ast/resourceparam' +#require 'puppet/parser/ast/resourceref' +#require 'puppet/parser/ast/resourceoverride' +#require 'puppet/parser/ast/selector' +#require 'puppet/parser/ast/resourcedefaults' +#require 'puppet/parser/ast/vardef' +#require 'puppet/parser/ast/tag' +#require 'puppet/parser/ast/function' # $Id$ diff --git a/lib/puppet/parser/ast/astarray.rb b/lib/puppet/parser/ast/astarray.rb index c42f658fd..fb0a3f671 100644 --- a/lib/puppet/parser/ast/astarray.rb +++ b/lib/puppet/parser/ast/astarray.rb @@ -1,143 +1,107 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # The basic container class. This object behaves almost identically # to a normal array except at initialization time. Note that its name # is 'AST::ASTArray', rather than plain 'AST::Array'; I had too many # bugs when it was just 'AST::Array', because things like # 'object.is_a?(Array)' never behaved as I expected. class ASTArray < Branch include Enumerable # Return a child by index. Probably never used. def [](index) @children[index] end # Evaluate our children. def evaluate(hash) scope = hash[:scope] rets = nil # We basically always operate declaratively, and when we # do we need to evaluate the settor-like statements first. This # is basically variable and type-default declarations. if scope.declarative? # This is such a stupid hack. I've no real idea how to make a # "real" declarative language, so I hack it so it looks like # one, yay. - definelist = [ - AST::CompDef, AST::NodeDef, AST::ClassDef - ] setlist = [ - AST::VarDef, AST::TypeDefaults, AST::Function + AST::VarDef, AST::ResourceDefaults, AST::Function ] - definers = [] settors = [] others = [] # Make a new array, so we don't have to deal with the details of # flattening and such items = [] # First clean out any AST::ASTArrays @children.each { |child| if child.instance_of?(AST::ASTArray) child.each do |ac| items << ac end else items << child end } # Now sort them all according to the type of action items.each { |child| - if definelist.include?(child.class) - definers << child - elsif setlist.include?(child.class) + if setlist.include?(child.class) settors << child else others << child end } - rets = [definers, settors, others].flatten.collect { |child| + rets = [settors, others].flatten.collect { |child| child.safeevaluate(:scope => scope) } else # If we're not declarative, just do everything in order. rets = @children.collect { |item| item.safeevaluate(:scope => scope) } end rets = rets.reject { |obj| obj.nil? } return rets end def push(*ary) ary.each { |child| #Puppet.debug "adding %s(%s) of type %s to %s" % # [child, child.object_id, child.class.to_s.sub(/.+::/,''), # self.object_id] @children.push(child) } return self end # Convert to a string. Only used for printing the parse tree. def to_s return "[" + @children.collect { |child| child.to_s }.join(", ") + "]" end # Print the parse tree. def tree(indent = 0) #puts((AST.indent * indent) + self.pin) self.collect { |child| child.tree(indent) }.join("\n" + (AST.midline * (indent+1)) + "\n") end end # A simple container class, containing the parameters for an object. # Used for abstracting the grammar declarations. Basically unnecessary # except that I kept finding bugs because I had too many arrays that # meant completely different things. - class ObjectInst < ASTArray; end - - # Another simple container class to make sure we can correctly arrayfy - # things. - class CompArgument < ASTArray - @@warnings = {} - def initialize(hash) - super - name = @children[0].value - - # If it's not a metaparamer, we're fine. - return unless Puppet::Type.metaparamclass(name) - - if @children[1] - if @children[1].value == false - raise Puppet::ParseError, - "%s is a metaparameter; please choose another name" % - name - else - unless @@warnings[name] - @@warnings[name] = true - Puppet.warning "%s is a metaparam; this value will inherit to all contained elements" % name - end - end - else - raise Puppet::ParseError, - "%s is a metaparameter; please choose another name" % - name - end - end - end + class ResourceInst < ASTArray; end end # $Id$ diff --git a/lib/puppet/parser/ast/classdef.rb b/lib/puppet/parser/ast/classdef.rb deleted file mode 100644 index e09db985f..000000000 --- a/lib/puppet/parser/ast/classdef.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'puppet/parser/ast/compdef' - -class Puppet::Parser::AST - # Define a new class. Syntactically similar to component definitions, - # but classes are always singletons -- only one can exist on a given - # host. - class ClassDef < AST::CompDef - @keyword = "class" - - def self.genclass - AST::HostClass - end - - def each - if @parentclass - #[@type,@args,@parentclass,@code].each { |child| yield child } - [@type,@parentclass,@code].each { |child| yield child } - else - #[@type,@args,@code].each { |child| yield child } - [@type,@code].each { |child| yield child } - end - end - - # Store our parse tree according to type. - def disabled_evaluate(hash) - scope = hash[:scope] - type = @type.safeevaluate(:scope => scope) - #args = @args.safeevaluate(:scope => scope) - - #:args => args, - arghash = { - :type => type, - :code => @code - } - - if @parentclass - arghash[:parentclass] = @parentclass.safeevaluate(:scope => scope) - end - - #Puppet.debug("defining hostclass '%s' with arguments [%s]" % - # [type,args]) - - begin - hclass = HostClass.new(arghash) - hclass.keyword = self.keyword - scope.settype(type, hclass) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - error.set_backtrace detail.backtrace - raise error - end - end - - def tree(indent = 0) - #@args.tree(indent + 1), - return [ - @type.tree(indent + 1), - ((@@indline * 4 * indent) + self.typewrap("class")), - @parentclass ? @parentclass.tree(indent + 1) : "", - @code.tree(indent + 1), - ].join("\n") - end - - def to_s - return "class %s(%s) inherits %s {\n%s }" % - [@type, @parentclass, @code] - #[@type, @args, @parentclass, @code] - end - end - -end diff --git a/lib/puppet/parser/ast/collection.rb b/lib/puppet/parser/ast/collection.rb index 8e9acce57..fa480b179 100644 --- a/lib/puppet/parser/ast/collection.rb +++ b/lib/puppet/parser/ast/collection.rb @@ -1,91 +1,30 @@ -require 'puppet/rails' +require 'puppet' +require 'puppet/parser/ast/branch' +require 'puppet/parser/collector' +# An object that collects stored objects from the central cache and returns +# them to the current host, yo. class Puppet::Parser::AST - # An object that collects stored objects from the central cache and returns - # them to the current host, yo. - class Collection < AST::Branch - attr_accessor :type +class Collection < AST::Branch + attr_accessor :type, :query, :form - # We cannot evaluate directly here; instead we need to store a - # CollectType object, which will do the collection. This is - # the only way to find certain exported types in the current - # configuration. - def evaluate(hash) - scope = hash[:scope] + # We return an object that does a late-binding evaluation. + def evaluate(hash) + scope = hash[:scope] - @convertedtype = @type.safeevaluate(:scope => scope) - - scope.newcollection(self) + if self.query + q = self.query.safeevaluate :scope => scope + else + q = nil end - # Now perform the actual collection, yo. - def perform(scope) - # First get everything from the export table. - - # FIXME This will only find objects that are before us in the tree, - # which is a problem. - objects = scope.exported(@convertedtype) - - # We want to return all of these objects, and then whichever objects - # we find in the db. - array = objects.values - - # Mark all of these objects as collected, so that they also get - # returned to the client. We don't store them in our scope - # or anything, which is a little odd, but eh. - array.each do |obj| - obj.collected = true - end - - count = array.length - - # Now we also have to see if there are any exported objects - # in our own scope. - scope.lookupexported(@convertedtype).each do |obj| - objects[obj.name] = obj - obj.collected = true - end - - bucket = Puppet::TransBucket.new - - Puppet::Rails::RailsObject.find_all_by_collectable(true).each do |obj| - # FIXME This should check that the source of the object is the - # host we're running on, else it's a bad conflict. - if objects.include?(obj.name) - scope.debug("%s[%s] is already exported" % [@convertedtype, obj.name]) - next - end - count += 1 - trans = obj.to_trans - bucket.push(trans) + newcoll = Puppet::Parser::Collector.new(scope, @type, q, self.form) - args = { - :name => trans.name, - :type => trans.type, - } + scope.newcollection(newcoll) - [:file, :line].each do |param| - if val = trans.send(param) - args[param] = val - end - end - - args[:arguments] = {} - trans.each do |p,v| args[:arguments][p] = v end - - - # 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.setobject(args) - end - - scope.debug("Collected %s objects of type %s" % - [count, @convertedtype]) - - return bucket - end + newcoll end end +end # $Id$ diff --git a/lib/puppet/parser/ast/collexpr.rb b/lib/puppet/parser/ast/collexpr.rb new file mode 100644 index 000000000..f69b2e92e --- /dev/null +++ b/lib/puppet/parser/ast/collexpr.rb @@ -0,0 +1,81 @@ +require 'puppet' +require 'puppet/parser/ast/branch' +require 'puppet/parser/collector' + +# An object that collects stored objects from the central cache and returns +# them to the current host, yo. +class Puppet::Parser::AST +class CollExpr < AST::Branch + attr_accessor :test1, :test2, :oper, :form, :type, :parens + + # We return an object that does a late-binding evaluation. + def evaluate(hash) + scope = hash[:scope] + + # Make sure our contained expressions have all the info they need. + [@test1, @test2].each do |t| + if t.is_a?(self.class) + t.form ||= self.form + t.type ||= self.type + end + end + + # The code is only used for virtual lookups + str1, code1 = @test1.safeevaluate :scope => scope + str2, code2 = @test2.safeevaluate :scope => scope + + # First build up the virtual code. + # If we're a conjunction operator, then we're calling code. I did + # some speed comparisons, and it's at least twice as fast doing these + # case statements as doing an eval here. + code = proc do |resource| + case @oper + when "and": code1.call(resource) and code2.call(resource) + when "or": code1.call(resource) or code2.call(resource) + when "==": resource[str1] == str2 + when "!=": resource[str1] != str2 + end + end + + # Now build up the rails conditions code + if self.parens and self.form == :exported + Puppet.warning "Parentheses are ignored in Rails searches" + end + + case @oper + when "and", "or": oper = @oper.upcase + when "==": oper = "=" + else + oper = @oper + end + + if oper == "=" or oper == "!=" + # Add the rails association info where necessary + if str1 == "title" + str = "title #{oper} '#{str2}'" + else + unless self.form == :virtual or str1 == "title" + parsefail "Collection from the database only supports " + + "title matching currently" + end + str = "rails_parameters.name = '#{str1}' and " + + "rails_parameters.value #{oper} '#{str2}'" + end + else + str = [str1, oper, str2].join(" ") + end + + return str, code + end + + def initialize(hash = {}) + super + + unless %w{== != and or}.include?(@oper) + raise ArgumentError, "Invalid operator %s" % @oper + end + end +end +end + +# $Id$ diff --git a/lib/puppet/parser/ast/compdef.rb b/lib/puppet/parser/ast/compdef.rb deleted file mode 100644 index 17ee60181..000000000 --- a/lib/puppet/parser/ast/compdef.rb +++ /dev/null @@ -1,98 +0,0 @@ -class Puppet::Parser::AST - # Define a new component. This basically just stores the - # associated parse tree by name in our current scope. Note that - # there is currently a mismatch in how we look up components -- it - # usually uses scopes, but sometimes uses '@@settypes'. - # FIXME This class should verify that each of its direct children - # has an abstractable name -- i.e., if a file does not include a - # variable in its name, then the user is essentially guaranteed to - # encounter an error if the component is instantiated more than - # once. - class CompDef < AST::Branch - attr_accessor :type, :args, :code, :scope, :parentclass - attr_writer :keyword - - @keyword = "define" - - class << self - attr_reader :keyword - end - - - def self.genclass - AST::Component - end - - def each - [@type,@args,@code].each { |child| yield child } - end - - # Store the parse tree. - def evaluate(hash) - scope = hash[:scope] - arghash = {:code => @code} - arghash[:type] = @type.safeevaluate(:scope => scope) - - if @args - arghash[:args] = @args.safeevaluate(:scope => scope) - end - - if @parentclass - arghash[:parentclass] = @parentclass.safeevaluate(:scope => scope) - end - - - begin - comp = self.class.genclass.new(arghash) - comp.keyword = self.keyword - scope.settype(arghash[:type], comp) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - error.set_backtrace detail.backtrace - raise error - end - end - - def initialize(hash) - @parentclass = nil - @args = nil - super - - #if @parentclass - # Puppet.notice "Parent class of %s is %s" % - # [@type.value, @parentclass.value] - #end - - #Puppet.debug "Defining type %s" % @type.value - end - - def keyword - if defined? @keyword - @keyword - else - self.class.keyword - end - end - - def tree(indent = 0) - return [ - @type.tree(indent + 1), - ((@@indline * 4 * indent) + self.typewrap("define")), - @args.tree(indent + 1), - @code.tree(indent + 1), - ].join("\n") - end - - def to_s - return "define %s(%s) {\n%s }" % [@type, @args, @code] - end - end -end - -# $Id$ diff --git a/lib/puppet/parser/ast/component.rb b/lib/puppet/parser/ast/component.rb index ebbf21959..312b11d99 100644 --- a/lib/puppet/parser/ast/component.rb +++ b/lib/puppet/parser/ast/component.rb @@ -1,161 +1,211 @@ +require 'puppet/parser/ast/branch' + class Puppet::Parser::AST # Evaluate the stored parse tree for a given component. This will # receive the arguments passed to the component and also the type and # name of the component. class Component < AST::Branch + include Puppet::Util + include Puppet::Util::Warnings + include Puppet::Util::MethodHelper class << self attr_accessor :name end # The class name - @name = :component + @name = :definition - attr_accessor :type, :args, :code, :scope, :keyword - attr_accessor :collectable, :parentclass + attr_accessor :type, :arguments, :code, :scope, :keyword + attr_accessor :exported, :namespace, :fqname, :interp # These are retrieved when looking up the superclass - attr_accessor :name, :arguments + attr_accessor :name def evaluate(hash) origscope = hash[:scope] objtype = hash[:type] - @name = hash[:name] - @arguments = hash[:arguments] || {} + name = hash[:name] + args = symbolize_options(hash[:arguments] || {}) - @collectable = hash[:collectable] + exported = hash[:exported] pscope = origscope - #pscope = if ! Puppet[:lexical] or hash[:asparent] == false - # origscope - #else - # @scope - #end - scope = pscope.newscope( - :type => @type, - :name => @name, - :keyword => self.keyword - ) - newcontext = hash[:newcontext] + scope = subscope(pscope, name) - if @collectable or origscope.collectable - scope.collectable = true + if exported or origscope.exported? + scope.exported = true end - - unless self.is_a? AST::HostClass and ! newcontext - #scope.warning "Setting context to %s" % self.object_id - scope.context = self.object_id - end - @scope = scope - + scope = scope # Additionally, add a tag for whatever kind of class # we are - scope.tag(@type) + if @type != "" + scope.tag(@type) + end - unless @name.nil? - scope.tag(@name) + unless name.nil? or name =~ /[^\w]/ + scope.tag(name) end # define all of the arguments in our local scope - if self.args + if self.arguments # Verify that all required arguments are either present or # have been provided with defaults. # FIXME This should probably also require each parent # class's arguments... - self.args.each { |arg, default| - unless @arguments.include?(arg) + self.arguments.each { |arg, default| + arg = symbolize(arg) + unless args.include?(arg) if defined? default and ! default.nil? - @arguments[arg] = default + default = default.safeevaluate :scope => scope + args[arg] = default #Puppet.debug "Got default %s for %s in %s" % # [default.inspect, arg.inspect, @name.inspect] else - error = Puppet::ParseError.new( - "Must pass %s to %s of type %s" % - [arg.inspect,@name,@type] - ) - error.line = self.line - error.file = self.file - raise error + parsefail "Must pass %s to %s of type %s" % + [arg,name,@type] end end } end # Set each of the provided arguments as variables in the # component's scope. - @arguments.each { |arg,value| - begin - scope.setvar(arg,@arguments[arg]) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => except - error = Puppet::ParseError.new(except.message) - error.line = self.line - error.file = self.file - error.set_backtrace except.backtrace - raise error + args.each { |arg,value| + unless validattr?(arg) + parsefail "%s does not accept attribute %s" % [@type, arg] + end + + exceptwrap do + scope.setvar(arg.to_s,args[arg]) end } - unless @arguments.include? "name" - scope.setvar("name",@name) + unless args.include? "name" + scope.setvar("name",name) end - # Now just evaluate the code with our new bindings. - #scope.inside(self) do # part of definition inheritance - self.code.safeevaluate(:scope => scope) - #end + if self.code + return self.code.safeevaluate(:scope => scope) + else + return nil + end + end + + def initialize(hash = {}) + @arguments = nil + @parentclass = nil + super + + # Deal with metaparams in the argument list. + if @arguments + @arguments.each do |arg, defvalue| + next unless Puppet::Type.metaparamclass(arg) + if defvalue + warnonce "%s is a metaparam; this value will inherit to all contained elements" % arg + else + raise Puppet::ParseError, + "%s is a metaparameter; please choose another name" % + name + end + end + end + end + + def parentclass + parentobj do |name| + @interp.findclass(namespace, name) + end + end - # If we're being evaluated as a parent class, we want to return the - # scope, so it can be overridden and such, but if not, we want to - # return a TransBucket of our objects. - if hash.include?(:asparent) - return scope + # Set our parent class, with a little check to avoid some potential + # weirdness. + def parentclass=(name) + if name == @type + parsefail "Parent classes must have dissimilar names" + end + + @parentclass = name + end + + # Hunt down our class object. + def parentobj + if @parentclass + # Cache our result, since it should never change. + unless @parentclass.is_a?(AST::HostClass) + unless tmp = yield(@parentclass) + parsefail "Could not find %s %s" % [self.class.name, @parentclass] + end + + if tmp == self + parsefail "Parent classes must have dissimilar names" + end + + @parentclass = tmp + end + @parentclass else - return scope.to_trans + nil end end + # Create a new subscope in which to evaluate our code. + def subscope(scope, name = nil) + args = { + :type => @type, + :keyword => self.keyword, + :namespace => self.namespace + } + + args[:name] = name if name + args[:type] = self.type if self.type + scope = scope.newscope(args) + scope.source = self + + return scope + end + + def to_s + fqname + end + # Check whether a given argument is valid. Searches up through # any parent classes that might exist. - def validarg?(param) + def validattr?(param) + return true if Puppet::Type.metaparam?(param) + + param = param.to_s found = false - unless @args.is_a? Array - @args = [@args] + unless @arguments.is_a? Array + @arguments = [@arguments] end - found = @args.detect { |arg| + found = @arguments.detect { |arg| if arg.is_a? Array arg[0] == param else arg == param end } if found # It's a valid arg for us return true elsif defined? @parentclass and @parentclass # Else, check any existing parent if parent = @scope.lookuptype(@parentclass) and parent != [] return parent.validarg?(param) elsif builtin = Puppet::Type.type(@parentclass) return builtin.validattr?(param) else raise Puppet::Error, "Could not find parent class %s" % @parentclass end else # Or just return false return false end end end end # $Id$ diff --git a/lib/puppet/parser/ast/function.rb b/lib/puppet/parser/ast/function.rb index 2c63c8b76..f67531ae3 100644 --- a/lib/puppet/parser/ast/function.rb +++ b/lib/puppet/parser/ast/function.rb @@ -1,50 +1,53 @@ class Puppet::Parser::AST # An AST object to call a function. class Function < AST::Branch attr_accessor :name, :arguments def evaluate(hash) # We don't need to evaluate the name, because it's plaintext # Just evaluate the arguments scope = hash[:scope] args = @arguments.safeevaluate(:scope => scope) - return scope.send("function_" + @name, args) + #exceptwrap :message => "Failed to execute %s" % @name, + # :type => Puppet::ParseError do + return scope.send("function_" + @name, args) + #end end def initialize(hash) @ftype = hash[:ftype] || :rvalue hash.delete(:ftype) if hash.include? :ftype super(hash) # Make sure it's a defined function unless @fname = Puppet::Parser::Functions.function(@name) raise Puppet::ParseError, "Unknown function %s" % @name end # Now check that it's been used correctly case @ftype when :rvalue: unless Puppet::Parser::Functions.rvalue?(@name) raise Puppet::ParseError, "Function '%s' does not return a value" % @name end when :statement: if Puppet::Parser::Functions.rvalue?(@name) raise Puppet::ParseError, "Function '%s' must be the value of a statement" % @name end else raise Puppet::DevError, "Invalid function type %s" % @ftype.inspect end # Lastly, check the arity end end end # $Id$ diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb index 44077983d..4a4664f0f 100644 --- a/lib/puppet/parser/ast/hostclass.rb +++ b/lib/puppet/parser/ast/hostclass.rb @@ -1,164 +1,64 @@ +require 'puppet/parser/ast/component' + class Puppet::Parser::AST # The code associated with a class. This is different from components # in that each class is a singleton -- only one will exist for a given # node. class HostClass < AST::Component @name = :class + # Are we a child of the passed class? Do a recursive search up our + # parentage tree to figure it out. + def child_of?(klass) + return false unless self.parentclass + + if klass == self.parentclass + return true + else + return self.parentclass.child_of?(klass) + end + end + + # Evaluate the code associated with this class. def evaluate(hash) scope = hash[:scope] - objname = hash[:name] args = hash[:arguments] + # Verify that we haven't already been evaluated - # FIXME The second subclass won't evaluate the parent class - # code at all, and any overrides will throw an error. - if myscope = scope.lookupclass(self.object_id) + if scope.setclass?(self) Puppet.debug "%s class already evaluated" % @type - - # Not used, but will eventually be used to fix #140. - if myscope.is_a? Puppet::Parser::Scope - unless scope.object_id == myscope.object_id - #scope.parent = myscope - end - end return nil end - # Set the class before we do anything else, so that it's set - # during the evaluation and can be inspected. - scope.setclass(self.object_id, @type) - - origscope = scope - - # Default to creating a new context - newcontext = true - - # If we've got a parent, then we pass it the original scope we - # received. It will get passed all the way up to the top class, - # which will create a subscope and pass that subscope to its - # subclass. - if @parentscope = self.evalparent( - :scope => scope, :arguments => args, :name => objname - ) - if @parentscope.is_a? Puppet::TransBucket - raise Puppet::DevError, "Got a bucket instead of a scope" - end - - # Override our scope binding with the parent scope - # binding. - scope = @parentscope - - # But don't create a new context if our parent created one - newcontext = false - end - - # Just use the Component evaluate method, but change the type - # to our own type. - result = super( - :scope => scope, - :arguments => args, - :type => @type, - :name => objname, # might be nil - :newcontext => newcontext, - :asparent => hash[:asparent] || false # might be nil - ) - - # Now set the class again, this time using the scope. This way - # we can look up the parent scope of this class later, so we - # can hook the children together. - scope.setscope(self.object_id, result) - - # This is important but painfully difficult. If we're the top-level - # class, that is, we have no parent classes, then the transscope - # is our own scope, but if there are parent classes, then the topmost - # parent's scope is the transscope, since it contains its code and - # all of the subclass's code. - transscope ||= result - - if hash[:asparent] - # If we're a parent class, then return the scope object itself. - return result - else - transscope = nil - if @parentscope - transscope = @parentscope - until transscope.parent.object_id == origscope.object_id - transscope = transscope.parent - end + if @parentclass + if pklass = self.parentclass + pklass.safeevaluate :scope => scope else - transscope = result + parsefail "Could not find class %s" % @parentclass end - - # But if we're the final subclass, translate the whole scope tree - # into TransObjects and TransBuckets. - return transscope.to_trans end - end - # Evaluate our parent class. Parent classes are evaluated in the - # exact same scope as the children. This is maybe not a good idea - # but, eh. - #def evalparent(scope, args, name) - def evalparent(hash) - scope = hash[:scope] - args = hash[:arguments] - name = hash[:name] - if @parentclass - #scope.warning "parent class of %s is %s" % - # [@type, @parentclass.inspect] - parentobj = nil + # Set the class before we do anything else, so that it's set + # during the evaluation and can be inspected. + scope.setclass(self) - begin - parentobj = scope.lookuptype(@parentclass) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - raise error - end - unless parentobj - error = Puppet::ParseError.new( - "Could not find parent '%s' of '%s'" % - [@parentclass,@name]) - error.line = self.line - error.file = self.file - raise error - end + unless hash[:nosubscope] + scope = subscope(scope) + end - # Verify that the parent and child are of the same type - unless parentobj.class == self.class - error = Puppet::ParseError.new( - "Class %s has incompatible parent type, %s vs %s" % - [@type, parentobj.class, self.class] - ) - error.file = self.file - error.line = self.line - raise error - end - # We don't need to pass the type, because the parent will just - # use its own type. Specify that it's being evaluated as a parent, - # so that it returns the scope, not a transbucket. - return parentobj.safeevaluate( - :scope => scope, - :arguments => args, - :name => name, - :asparent => true, - :collectable => self.collectable - ) + # Now evaluate our code, yo. + if self.code + return self.code.evaluate(:scope => scope) else - return false + return nil end end def initialize(hash) @parentclass = nil super end - end - end + +# $Id$ diff --git a/lib/puppet/parser/ast/leaf.rb b/lib/puppet/parser/ast/leaf.rb index 7b9a283d7..298c0168f 100644 --- a/lib/puppet/parser/ast/leaf.rb +++ b/lib/puppet/parser/ast/leaf.rb @@ -1,108 +1,97 @@ class Puppet::Parser::AST # The base class for all of the leaves of the parse trees. These # basically just have types and values. Both of these parameters # are simple values, not AST objects. class Leaf < AST attr_accessor :value, :type # Return our value. def evaluate(hash) return @value end # Print the value in parse tree context. def tree(indent = 0) return ((@@indent * indent) + self.typewrap(self.value)) end def to_s return @value end end # The boolean class. True or false. Converts the string it receives # to a Ruby boolean. class Boolean < AST::Leaf # Use the parent method, but then convert to a real boolean. def initialize(hash) super - unless @value == 'true' or @value == 'false' + unless @value == true or @value == false raise Puppet::DevError, "'%s' is not a boolean" % @value end - if @value == 'true' - @value = true - else - @value = false - end + @value end end # The base string class. class String < AST::Leaf # Interpolate the string looking for variables, and then return # the result. def evaluate(hash) return hash[:scope].strinterp(@value) end end # The base string class. class FlatString < AST::Leaf # Interpolate the string looking for variables, and then return # the result. def evaluate(hash) return @value end end # The 'default' option on case statements and selectors. class Default < AST::Leaf; end # Capitalized words; used mostly for type-defaults, but also # get returned by the lexer any other time an unquoted capitalized # word is found. class Type < AST::Leaf; end # Lower-case words. class Name < AST::Leaf; end + # double-colon separated class names + class ClassName < AST::Leaf; end + # Host names, either fully qualified or just the short name class HostName < AST::Leaf def initialize(hash) super unless @value =~ %r{^[0-9a-zA-Z\-]+(\.[0-9a-zA-Z\-]+)*$} raise Puppet::DevError, "'%s' is not a valid hostname" % @value end end end # A simple variable. This object is only used during interpolation; # the VarDef class is used for assignment. class Variable < Name # Looks up the value of the object in the scope tree (does # not include syntactical constructs, like '$' and '{}'). def evaluate(hash) - begin + parsewrap do return hash[:scope].lookupvar(@value) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::DevError.new(detail) - error.line = self.line - error.file = self.file - error.set_backtrace detail.backtrace - raise error end end end end # $Id$ diff --git a/lib/puppet/parser/ast/node.rb b/lib/puppet/parser/ast/node.rb index e4e69bed9..3f508b2bf 100644 --- a/lib/puppet/parser/ast/node.rb +++ b/lib/puppet/parser/ast/node.rb @@ -1,113 +1,70 @@ +require 'puppet/parser/ast/hostclass' + class Puppet::Parser::AST # The specific code associated with a host. Nodes are annoyingly unlike # other objects. That's just the way it is, at least for now. class Node < AST::HostClass @name = :node - attr_accessor :type, :args, :code, :parentclass + attr_accessor :name #def evaluate(scope, facts = {}) def evaluate(hash) - origscope = hash[:scope] - facts = hash[:facts] || {} - - # nodes are never instantiated like a normal object, - # but we need the type to be the name users would use for - # instantiation, otherwise tags don't work out + scope = hash[:scope] - pscope = origscope #pscope = if ! Puppet[:lexical] or hash[:asparent] # @scope #else # origscope #end - scope = pscope.newscope( - :type => self.type, - :keyword => @keyword - ) - scope.context = self.object_id + Puppet.warning "%s => %s" % [scope.type, self.name] - # Now set all of the facts inside this scope - facts.each { |var, value| - scope.setvar(var, value) - } - - if tmp = self.evalparent(scope) - # Again, override our scope with the parent scope, if - # there is one. - scope = tmp + # We don't have to worry about the declarativeness of node parentage, + # because the entry point is always a single node definition. + if parent = self.parentclass + scope = parent.safeevaluate :scope => scope end + Puppet.notice "%s => %s" % [scope.type, self.name] - #scope.tag(@name) - - # We never pass the facts to the parent class, because they've - # already been defined at this top-level scope. - #super(scope, facts, @name, @name) + scope = scope.newscope( + :type => self.name, + :keyword => @keyword, + :source => self, + :namespace => "" # nodes are always in "" + ) + Puppet.info "%s => %s" % [scope.type, self.name] # Mark our node name as a class, too, but strip it of the domain # name. Make the mark before we evaluate the code, so that it is # marked within the code itself. - scope.setclass(self.object_id, @type.sub(/\..+/, '')) + scope.setclass(self) - # And then evaluate our code. - @code.safeevaluate(:scope => scope) + # And then evaluate our code if we have any + if self.code + @code.safeevaluate(:scope => scope) + end return scope end - # Evaluate our parent class. - def evalparent(scope) - if @parentclass - # This is pretty messed up. I don't know if this will - # work in the long term, but we need to evaluate the node - # in our own scope, even though our parent node has - # a scope associated with it, because otherwise we 1) won't - # get our facts defined, and 2) we won't actually get the - # objects returned, based on how nodes work. - - # We also can't just evaluate the node itself, because - # it would create a node scope within this scope, - # and that would cause mass havoc. - node = nil - - # The 'node' method just returns a hash of the node - # code and name. It's used here, and in 'evalnode'. - unless hash = scope.node(@parentclass) - raise Puppet::ParseError, - "Could not find parent node %s" % - @parentclass - end - - node = hash[:node] - type = nil - if type = node.type - scope.tag(node.type) - end - - begin - code = node.code - code.safeevaluate(:scope => scope, :asparent => true) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - raise error - end + def initialize(hash) + @parentclass = nil + super - if node.parentclass - node.evalparent(scope) - end + # Do some validation on the node name + if @name =~ /[^-\w.]/ + raise Puppet::ParseError, "Invalid node name %s" % @name end end - def initialize(hash) - @parentclass = nil - super + def parentclass + parentobj do |name| + @interp.nodesearch(name) + end + @parentclass end end end + +# $Id$ diff --git a/lib/puppet/parser/ast/nodedef.rb b/lib/puppet/parser/ast/nodedef.rb deleted file mode 100644 index 06b104828..000000000 --- a/lib/puppet/parser/ast/nodedef.rb +++ /dev/null @@ -1,73 +0,0 @@ -class Puppet::Parser::AST - # Define a node. The node definition stores a parse tree for each - # specified node, and this parse tree is only ever looked up when - # a client connects. - class NodeDef < AST::Branch - attr_accessor :names, :code, :parentclass, :keyword, :scope - - def each - [@names,@code].each { |child| yield child } - end - - # Do implicit iteration over each of the names passed. - def evaluate(hash) - scope = hash[:scope] - names = @names.safeevaluate(:scope => scope) - - unless names.is_a?(Array) - names = [names] - end - - names.each { |name| - #Puppet.debug("defining host '%s' in scope %s" % - # [name, scope.object_id]) - # We use 'type' here instead of name, because every component - # type supports both 'type' and 'name', and 'type' is a more - # appropriate description of the syntactic role that this term - # plays. - arghash = { - :type => name, - :code => @code - } - - if @parentclass - arghash[:parentclass] = @parentclass.safeevaluate(:scope => scope) - end - - begin - node = Node.new(arghash) - node.keyword = true - scope.setnode(name, node) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - raise error - end - } - end - - def initialize(hash) - @parentclass = nil - @keyword = "node" - super - end - - def tree(indent = 0) - return [ - @names.tree(indent + 1), - ((@@indline * 4 * indent) + self.typewrap("node")), - @code.tree(indent + 1), - ].join("\n") - end - - def to_s - return "node %s {\n%s }" % [@name, @code] - end - end - -end diff --git a/lib/puppet/parser/ast/objectdef.rb b/lib/puppet/parser/ast/objectdef.rb deleted file mode 100644 index ac5fe3070..000000000 --- a/lib/puppet/parser/ast/objectdef.rb +++ /dev/null @@ -1,353 +0,0 @@ -class Puppet::Parser::AST - # Any normal puppet object declaration. Can result in a class or a - # component, in addition to builtin types. - class ObjectDef < AST::Branch - attr_accessor :name, :type, :collectable - attr_reader :params - - # probably not used at all - def []=(index,obj) - @params[index] = obj - end - - # probably not used at all - def [](index) - return @params[index] - end - - # Iterate across all of our children. - def each - [@type,@name,@params].flatten.each { |param| - #Puppet.debug("yielding param %s" % param) - yield param - } - end - - # Does not actually return an object; instead sets an object - # in the current scope. - def evaluate(hash) - scope = hash[:scope] - @scope = scope - hash = {} - - # Get our type and name. - objtype = @type.safeevaluate(:scope => scope) - - # Disable definition inheritance, for now. 8/27/06, luke - #if objtype == "super" - # objtype = supertype() - # @subtype = true - #else - @subtype = false - #end - - # If the type was a variable, we wouldn't have typechecked yet. - # Do it now, if so. - unless @checked - self.typecheck(objtype) - end - - # See if our object type was defined. If not, we know it's - # builtin because we already typechecked. - begin - object = scope.lookuptype(objtype) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - error.set_backtrace detail.backtrace - raise error - end - - hash = {} - # Evaluate all of the specified params. - @params.each { |param| - ary = param.safeevaluate(:scope => scope) - hash[ary[0]] = ary[1] - } - - # Now collect info from our parent. - parentname = nil - if @subtype - parentname = supersetup(hash) - end - - objnames = [nil] - # Determine our name if we have one. - if self.name - objnames = @name.safeevaluate(:scope => scope) - # it's easier to always use an array, even for only one name - unless objnames.is_a?(Array) - objnames = [objnames] - end - else - if parentname - objnames = [parentname] - else - # See if they specified the name as a parameter instead of - # as a normal name (i.e., before the colon). - unless object # we're a builtin - if objclass = Puppet::Type.type(objtype) - namevar = objclass.namevar - - tmp = hash["name"] || hash[namevar.to_s] - - if tmp - objnames = [tmp] - end - else - # this should never happen, because we've already - # typechecked, but it's no real problem if it does - # happen. We just end up with an object with no - # name. - end - end - end - end - - # this is where our implicit iteration takes place; - # if someone passed an array as the name, then we act - # just like the called us many times - objnames.collect { |objname| - # If the object is a class, that means it's a builtin type, so - # we just store it in the scope - begin - #Puppet.debug( - # ("Setting object '%s' " + - # "in scope %s " + - # "with arguments %s") % - # [objname, scope.object_id, hash.inspect] - #) - obj = scope.setobject( - :type => objtype, - :name => objname, - :arguments => hash, - :file => @file, - :line => @line, - :collectable => self.collectable - ) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - error.set_backtrace detail.backtrace - raise error - end - }.reject { |obj| obj.nil? } - end - - # Create our ObjectDef. Handles type checking for us. - def initialize(hash) - @checked = false - super - - #self.typecheck(@type.value) - end - - # Verify that all passed parameters are valid - def paramcheck(builtin, objtype) - # This defaults to true - unless Puppet[:paramcheck] - return - end - - @params.each { |param| - if builtin - self.parambuiltincheck(builtin, param) - else - self.paramdefinedcheck(objtype, param) - end - } - - # Mark that we've made it all the way through. - @checked = true - end - - def parambuiltincheck(type, param) - unless param.is_a?(AST::ObjectParam) - raise Puppet::DevError, - "Got something other than param" - end - begin - pname = param.param.value - rescue => detail - raise Puppet::DevError, detail.to_s - end - - return if pname == "name" # always allow these - unless type.validattr?(pname) - error = Puppet::ParseError.new( - "Invalid parameter '%s' for type '%s'" % - [pname, type.name] - ) - error.line = self.line - error.file = self.file - raise error - end - end - - def paramdefinedcheck(objtype, param) - # FIXME We might need to do more here eventually. Metaparams - # behave strangely on containers. - if Puppet::Type.metaparam?(param.param.value.intern) - return - end - - begin - pname = param.param.value - rescue => detail - raise Puppet::DevError, detail.to_s - end - - unless objtype.validarg?(pname) - error = Puppet::ParseError.new( - "Invalid parameter '%s' for type '%s'" % - [pname,objtype.type] - ) - error.line = self.line - error.file = self.file - raise error - end - end - - # Set the parameters for our object. - def params=(params) - if params.is_a?(AST::ASTArray) - @params = params - else - @params = AST::ASTArray.new( - :line => params.line, - :file => params.file, - :children => [params] - ) - end - end - - def supercomp - unless defined? @supercomp - if @scope and comp = @scope.inside - @supercomp = comp - else - error = Puppet::ParseError.new( - "'super' is only valid within definitions" - ) - error.line = self.line - error.file = self.file - raise error - end - end - @supercomp - end - - # Take all of the arguments of our parent and add them into our own, - # without overriding anything. - def supersetup(hash) - comp = supercomp() - - # Now check each of the arguments from the parent. - comp.arguments.each do |name, value| - unless hash.has_key? name - hash[name] = value - end - end - - # Return the parent name, so it can be used if appropriate. - return comp.name - end - - # Retrieve our supertype. - def supertype - unless defined? @supertype - if parent = supercomp.parentclass - @supertype = parent - else - error = Puppet::ParseError.new( - "%s does not have a parent class" % comp.type - ) - error.line = self.line - error.file = self.file - raise error - end - end - @supertype - end - - # Print this object out. - def tree(indent = 0) - return [ - @type.tree(indent + 1), - @name.tree(indent + 1), - ((@@indline * indent) + self.typewrap(self.pin)), - @params.collect { |param| - begin - param.tree(indent + 1) - rescue NoMethodError => detail - Puppet.err @params.inspect - error = Puppet::DevError.new( - "failed to tree a %s" % self.class - ) - error.set_backtrace detail.backtrace - raise error - end - }.join("\n") - ].join("\n") - end - - # Verify that the type is valid. This throws an error if there's - # a problem, so the return value doesn't matter - def typecheck(objtype) - # This will basically always be on, but I wanted to make it at - # least simple to turn off if it came to that - unless Puppet[:typecheck] - return - end - - builtin = false - begin - builtin = Puppet::Type.type(objtype) - rescue TypeError - # nothing; we've already set builtin to false - end - - typeobj = nil - if builtin - self.paramcheck(builtin, objtype) - else - # If there's no set scope, then we're in initialize, not - # evaluate, so we can't test defined types. - return true unless defined? @scope and @scope - - # Unless we can look up the type, throw an error - unless typeobj = @scope.lookuptype(objtype) - error = Puppet::ParseError.new( - "Unknown type '%s'" % objtype - ) - error.line = self.line - error.file = self.file - raise error - end - - # Now that we have the type, verify all of the parameters. - # Note that we're now passing an AST Class object or whatever - # as the type, not a simple string. - self.paramcheck(builtin, typeobj) - end - end - - def to_s - return "%s => { %s }" % [@name, - @params.collect { |param| - param.to_s - }.join("\n") - ] - end - end -end diff --git a/lib/puppet/parser/ast/objectref.rb b/lib/puppet/parser/ast/objectref.rb deleted file mode 100644 index f9a63e222..000000000 --- a/lib/puppet/parser/ast/objectref.rb +++ /dev/null @@ -1,77 +0,0 @@ -class Puppet::Parser::AST - # A reference to an object. Only valid as an rvalue. - class ObjectRef < AST::Branch - attr_accessor :name, :type - - def each - [@type,@name].flatten.each { |param| - #Puppet.debug("yielding param %s" % param) - yield param - } - end - - # Evaluate our object, but just return a simple array of the type - # and name. - def evaluate(hash) - scope = hash[:scope] - objtype = @type.safeevaluate(:scope => scope) - objnames = @name.safeevaluate(:scope => scope) - - # it's easier to always use an array, even for only one name - unless objnames.is_a?(Array) - objnames = [objnames] - end - - # See if we can look the object up in our scope tree. - begin - object = scope.lookuptype(objtype) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - error.set_backtrace detail.backtrace - raise error - end - - # If the type isn't defined, verify that it's builtin - unless object or Puppet::Type.type(objtype) - error = Puppet::ParseError.new("Could not find type %s" % - objtype.inspect) - error.line = self.line - error.file = self.file - raise error - end - - # should we implicitly iterate here? - # yes, i believe that we essentially have to... - ret = objnames.collect { |objname| - if object.is_a?(AST::Component) - objname = "%s[%s]" % [objtype,objname] - objtype = "component" - end - [objtype,objname] - }.reject { |obj| obj.nil? } - - # Return a flattened array, since we know that we've created an - # array - return *ret - end - - def tree(indent = 0) - return [ - @type.tree(indent + 1), - @name.tree(indent + 1), - ((@@indline * indent) + self.typewrap(self.pin)) - ].join("\n") - end - - def to_s - return "%s[%s]" % [@name,@type] - end - end - -end diff --git a/lib/puppet/parser/ast/resourcedef.rb b/lib/puppet/parser/ast/resourcedef.rb new file mode 100644 index 000000000..1c9333030 --- /dev/null +++ b/lib/puppet/parser/ast/resourcedef.rb @@ -0,0 +1,215 @@ +require 'puppet/parser/ast/branch' + +# Any normal puppet object declaration. Can result in a class or a +# component, in addition to builtin types. +class Puppet::Parser::AST +class ResourceDef < AST::Branch + attr_accessor :title, :type, :exported, :virtual + attr_reader :params + + # probably not used at all + def []=(index,obj) + @params[index] = obj + end + + # probably not used at all + def [](index) + return @params[index] + end + + # Iterate across all of our children. + def each + [@type,@title,@params].flatten.each { |param| + #Puppet.debug("yielding param %s" % param) + yield param + } + end + + # Does not actually return an object; instead sets an object + # in the current scope. + def evaluate(hash) + scope = hash[:scope] + @scope = scope + hash = {} + + # Get our type and name. + objtype = @type + + # Disable definition inheritance, for now. 8/27/06, luke + #if objtype == "super" + # objtype = supertype() + # @subtype = true + #else + @subtype = false + #end + + # Evaluate all of the specified params. + paramobjects = @params.collect { |param| + param.safeevaluate(:scope => scope) + } + + # Now collect info from our parent. + parentname = nil + if @subtype + parentname = supersetup(hash) + end + + objtitles = nil + # Determine our name if we have one. + if self.title + objtitles = @title.safeevaluate(:scope => scope) + # it's easier to always use an array, even for only one name + unless objtitles.is_a?(Array) + objtitles = [objtitles] + end + else + if parentname + objtitles = [parentname] + else + # See if they specified the name as a parameter instead of + # as a normal name (i.e., before the colon). + unless object # we're a builtin + if objclass = Puppet::Type.type(objtype) + namevar = objclass.namevar + + tmp = hash["name"] || hash[namevar.to_s] + + if tmp + objtitles = [tmp] + end + else + # This isn't grammatically legal. + raise Puppet::ParseError, "Got a resource with no title" + end + end + end + end + + # This is where our implicit iteration takes place; if someone + # passed an array as the name, then we act just like the called us + # many times. + objtitles.collect { |objtitle| + exceptwrap :type => Puppet::ParseError do + obj = Puppet::Parser::Resource.new( + :type => objtype, + :title => objtitle, + :params => paramobjects, + :file => @file, + :line => @line, + :exported => self.exported || scope.exported, + :virtual => self.virtual, + :source => scope.source, + :scope => scope + ) + + # And then store the resource in the scope. + # XXX At some point, we need to switch all of this to return + # objects instead of storing them like this. + scope.setresource(obj) + obj + end + }.reject { |obj| obj.nil? } + end + + # Create our ResourceDef. Handles type checking for us. + def initialize(hash) + @checked = false + super + + #self.typecheck(@type.value) + end + + # Set the parameters for our object. + def params=(params) + if params.is_a?(AST::ASTArray) + @params = params + else + @params = AST::ASTArray.new( + :line => params.line, + :file => params.file, + :children => [params] + ) + end + end + + def supercomp + unless defined? @supercomp + if @scope and comp = @scope.inside + @supercomp = comp + else + error = Puppet::ParseError.new( + "'super' is only valid within definitions" + ) + error.line = self.line + error.file = self.file + raise error + end + end + @supercomp + end + + # Take all of the arguments of our parent and add them into our own, + # without overriding anything. + def supersetup(hash) + comp = supercomp() + + # Now check each of the arguments from the parent. + comp.arguments.each do |name, value| + unless hash.has_key? name + hash[name] = value + end + end + + # Return the parent name, so it can be used if appropriate. + return comp.name + end + + # Retrieve our supertype. + def supertype + unless defined? @supertype + if parent = supercomp.parentclass + @supertype = parent + else + error = Puppet::ParseError.new( + "%s does not have a parent class" % comp.type + ) + error.line = self.line + error.file = self.file + raise error + end + end + @supertype + end + + # Print this object out. + def tree(indent = 0) + return [ + @type.tree(indent + 1), + @title.tree(indent + 1), + ((@@indline * indent) + self.typewrap(self.pin)), + @params.collect { |param| + begin + param.tree(indent + 1) + rescue NoMethodError => detail + Puppet.err @params.inspect + error = Puppet::DevError.new( + "failed to tree a %s" % self.class + ) + error.set_backtrace detail.backtrace + raise error + end + }.join("\n") + ].join("\n") + end + + def to_s + return "%s => { %s }" % [@title, + @params.collect { |param| + param.to_s + }.join("\n") + ] + end +end +end + +# $Id$ diff --git a/lib/puppet/parser/ast/typedefaults.rb b/lib/puppet/parser/ast/resourcedefaults.rb similarity index 50% rename from lib/puppet/parser/ast/typedefaults.rb rename to lib/puppet/parser/ast/resourcedefaults.rb index 2e11a1b94..44db4d465 100644 --- a/lib/puppet/parser/ast/typedefaults.rb +++ b/lib/puppet/parser/ast/resourcedefaults.rb @@ -1,46 +1,38 @@ class Puppet::Parser::AST - # A statement syntactically similar to an ObjectDef, but uses a + # A statement syntactically similar to an ResourceDef, but uses a # capitalized object type and cannot have a name. - class TypeDefaults < AST::Branch + class ResourceDefaults < AST::Branch attr_accessor :type, :params def each [@type,@params].each { |child| yield child } end - # As opposed to ObjectDef, this stores each default for the given + # As opposed to ResourceDef, this stores each default for the given # object type. def evaluate(hash) scope = hash[:scope] - type = @type.safeevaluate(:scope => scope) + type = @type.downcase params = @params.safeevaluate(:scope => scope) - begin - scope.setdefaults(type.downcase,params) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - error.set_backtrace detail.backtrace - raise error + parsewrap do + scope.setdefaults(type, params) end end def tree(indent = 0) return [ @type.tree(indent + 1), ((@@indline * 4 * indent) + self.typewrap(self.pin)), @params.tree(indent + 1) ].join("\n") end def to_s return "%s { %s }" % [@type,@params] end end end + +# $Id$ diff --git a/lib/puppet/parser/ast/resourceoverride.rb b/lib/puppet/parser/ast/resourceoverride.rb new file mode 100644 index 000000000..26d69ae97 --- /dev/null +++ b/lib/puppet/parser/ast/resourceoverride.rb @@ -0,0 +1,62 @@ +require 'puppet/parser/ast/resourcedef' + +class Puppet::Parser::AST + # Set a parameter on a resource specification created somewhere else in the + # configuration. The object is responsible for verifying that this is allowed. + class ResourceOverride < ResourceDef + attr_accessor :object + attr_reader :params + + # Iterate across all of our children. + def each + [@object,@params].flatten.each { |param| + #Puppet.debug("yielding param %s" % param) + yield param + } + end + + # Does not actually return an object; instead sets an object + # in the current scope. + def evaluate(hash) + scope = hash[:scope] + + # Get our object reference. + object = @object.safeevaluate(:scope => scope) + + hash = {} + + # Evaluate all of the specified params. + params = @params.collect { |param| + param.safeevaluate(:scope => scope) + } + + # Now we just create a normal resource, but we call a very different + # method on the scope. + obj = Puppet::Parser::Resource.new( + :type => object.type, + :title => object.title, + :params => params, + :file => @file, + :line => @line, + :source => scope.source, + :scope => scope + ) + + # Now we tell the scope that it's an override, and it behaves as + # necessary. + scope.setoverride(obj) + + obj + end + + # Create our ResourceDef. Handles type checking for us. + def initialize(hash) + @checked = false + super + + #self.typecheck(@type.value) + end + end +end + +# $Id$ diff --git a/lib/puppet/parser/ast/objectparam.rb b/lib/puppet/parser/ast/resourceparam.rb similarity index 58% rename from lib/puppet/parser/ast/objectparam.rb rename to lib/puppet/parser/ast/resourceparam.rb index 87c3e5e8e..248e91b32 100644 --- a/lib/puppet/parser/ast/objectparam.rb +++ b/lib/puppet/parser/ast/resourceparam.rb @@ -1,31 +1,42 @@ +require 'puppet/parser/ast/branch' + class Puppet::Parser::AST - # The AST object for the parameters inside ObjectDefs and Selectors. - class ObjectParam < AST::Branch + # The AST object for the parameters inside ResourceDefs and Selectors. + class ResourceParam < AST::Branch attr_accessor :value, :param def each [@param,@value].each { |child| yield child } end # Return the parameter and the value. def evaluate(hash) scope = hash[:scope] - param = @param.safeevaluate(:scope => scope) + param = @param value = @value.safeevaluate(:scope => scope) - return [param, value] + + args = {:name => param, :value => value, :source => scope.source} + [:line, :file].each do |p| + if v = self.send(p) + args[p] = v + end + end + + return Puppet::Parser::Resource::Param.new(args) end def tree(indent = 0) return [ @param.tree(indent + 1), ((@@indline * indent) + self.typewrap(self.pin)), @value.tree(indent + 1) ].join("\n") end def to_s return "%s => %s" % [@param,@value] end end - end + +# $Id$ diff --git a/lib/puppet/parser/ast/resourceref.rb b/lib/puppet/parser/ast/resourceref.rb new file mode 100644 index 000000000..b0fe5f6d7 --- /dev/null +++ b/lib/puppet/parser/ast/resourceref.rb @@ -0,0 +1,44 @@ +require 'puppet/parser/ast/branch' + +class Puppet::Parser::AST + # A reference to an object. Only valid as an rvalue. + class ResourceRef < AST::Branch + attr_accessor :title, :type + + def each + [@type,@title].flatten.each { |param| + #Puppet.debug("yielding param %s" % param) + yield param + } + end + + # Evaluate our object, but just return a simple array of the type + # and name. + def evaluate(hash) + scope = hash[:scope] + + # We want a lower-case type. + objtype = @type.downcase + + title = @title.safeevaluate(:scope => scope) + + return Puppet::Parser::Resource::Reference.new( + :type => objtype, :title => title + ) + end + + def tree(indent = 0) + return [ + @type.tree(indent + 1), + @title.tree(indent + 1), + ((@@indline * indent) + self.typewrap(self.pin)) + ].join("\n") + end + + def to_s + return "%s[%s]" % [@type,@title] + end + end +end + +# $Id$ diff --git a/lib/puppet/parser/ast/selector.rb b/lib/puppet/parser/ast/selector.rb index 5d1c41494..fe3fe9eaf 100644 --- a/lib/puppet/parser/ast/selector.rb +++ b/lib/puppet/parser/ast/selector.rb @@ -1,66 +1,62 @@ class Puppet::Parser::AST # The inline conditional operator. Unlike CaseStatement, which executes # code, we just return a value. class Selector < AST::Branch attr_accessor :param, :values def each [@param,@values].each { |child| yield child } end # Find the value that corresponds with the test. def evaluate(hash) scope = hash[:scope] retvalue = nil found = nil # Get our parameter. paramvalue = @param.safeevaluate(:scope => scope) default = nil #@values = [@values] unless @values.instance_of? AST::ASTArray unless @values.instance_of? AST::ASTArray or @values.instance_of? Array @values = [@values] end # Then look for a match in the options. @values.each { |obj| param = obj.param.safeevaluate(:scope => scope) if param == paramvalue # we found a matching option retvalue = obj.value.safeevaluate(:scope => scope) found = true break elsif obj.param.is_a?(Default) default = obj end } # Unless we found something, look for the default. unless found if default retvalue = default.value.safeevaluate(:scope => scope) else - error = Puppet::ParseError.new( + self.fail Puppet::ParseError, "No value for selector param '%s'" % paramvalue - ) - error.line = self.line - error.file = self.file - raise error end end return retvalue end def tree(indent = 0) return [ @param.tree(indent + 1), ((@@indline * indent) + self.typewrap(self.pin)), @values.tree(indent + 1) ].join("\n") end end end diff --git a/lib/puppet/parser/ast/vardef.rb b/lib/puppet/parser/ast/vardef.rb index 79129f31a..30eef204a 100644 --- a/lib/puppet/parser/ast/vardef.rb +++ b/lib/puppet/parser/ast/vardef.rb @@ -1,45 +1,37 @@ class Puppet::Parser::AST # Define a variable. Stores the value in the current scope. class VarDef < AST::Branch attr_accessor :name, :value # Look up our name and value, and store them appropriately. The # lexer strips off the syntax stuff like '$'. def evaluate(hash) scope = hash[:scope] name = @name.safeevaluate(:scope => scope) value = @value.safeevaluate(:scope => scope) - begin + parsewrap do scope.setvar(name,value) - rescue Puppet::ParseError => except - except.line = self.line - except.file = self.file - raise except - rescue => detail - error = Puppet::ParseError.new(detail) - error.line = self.line - error.file = self.file - error.set_backtrace detail.backtrace - raise error end end def each [@name,@value].each { |child| yield child } end def tree(indent = 0) return [ @name.tree(indent + 1), ((@@indline * 4 * indent) + self.typewrap(self.pin)), @value.tree(indent + 1) ].join("\n") end def to_s return "%s => %s" % [@name,@value] end end end + +# $Id$ diff --git a/lib/puppet/parser/collector.rb b/lib/puppet/parser/collector.rb new file mode 100644 index 000000000..f3c7949de --- /dev/null +++ b/lib/puppet/parser/collector.rb @@ -0,0 +1,97 @@ +# 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, :query, :form + + # Collect exported objects. + def collect_exported + require 'puppet/rails' + # 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 + + # 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 + + # 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 + Puppet::Rails::RailsResource.find_all_by_restype_and_exported(@type, true, + args + ).each do |obj| + count += 1 + 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) + + resources << resource + end + end + + scope.debug("Collected %s objects of type %s" % + [count, @convertedtype]) + + return resources + 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 + 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, query, form) + @scope = scope + @type = type + @query = query + @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.query + return self.query.call(resource) + else + return true + end + end +end + +# $Id$ diff --git a/lib/puppet/parser/functions.rb b/lib/puppet/parser/functions.rb index 9562441bc..96fccd7cd 100644 --- a/lib/puppet/parser/functions.rb +++ b/lib/puppet/parser/functions.rb @@ -1,168 +1,160 @@ # Grr require 'puppet/autoload' require 'puppet/parser/scope' module Puppet::Parser module Functions # A module for managing parser functions. Each specified function # becomes an instance method on the Scope class. class << self include Puppet::Util end # Create a new function type. def self.newfunction(name, ftype = :statement, &block) @functions ||= {} name = symbolize(name) if @functions.include? name raise Puppet::DevError, "Function %s already defined" % name end # We want to use a separate, hidden module, because we don't want # people to be able to call them directly. unless defined? FCollection eval("module FCollection; end") end unless ftype == :statement or ftype == :rvalue raise Puppet::DevError, "Invalid statement type %s" % ftype.inspect end fname = "function_" + name.to_s Puppet::Parser::Scope.send(:define_method, fname, &block) # Someday we'll support specifying an arity, but for now, nope #@functions[name] = {:arity => arity, :type => ftype} @functions[name] = {:type => ftype, :name => fname} end # Determine if a given name is a function def self.function(name) name = symbolize(name) unless defined? @autoloader @autoloader = Puppet::Autoload.new(self, "puppet/parser/functions", :wrap => false ) end unless @functions.include? name @autoloader.load(name) end if @functions.include? name return @functions[name][:name] else return false end end # Determine if a given function returns a value or not. def self.rvalue?(name) name = symbolize(name) if @functions.include? name case @functions[name][:type] when :statement: return false when :rvalue: return true end else return false end end # Include the specified classes newfunction(:include) do |vals| - vals.each do |val| - if objecttype = lookuptype(val) - # It's a defined type, so set it into the scope so it can - # be evaluated. - setobject( - :type => val, - :arguments => {} - ) - else - raise Puppet::ParseError, "Unknown class %s" % val - end + klasses = evalclasses(*vals) + + missing = vals.find_all do |klass| + ! klass.include?(klass) + end + + # Throw an error if we didn't evaluate all of the classes. + if missing.length == 1 + self.fail Puppet::ParseError, + "Could not find class %s" % missing + elsif missing.length > 1 + self.fail Puppet::ParseError, + "Could not find classes %s" % missing.join(", ") end end # Tag the current scope with each passed name newfunction(:tag) do |vals| - vals.each do |val| - # Some hackery, because the tags are stored by object id - # for singletonness. - self.setclass(val.object_id, val) - end - - # Also add them as tags self.tag(*vals) end # Test whether a given tag is set. This functions as a big OR -- if any of the # specified tags are unset, we return false. newfunction(:tagged, :rvalue) do |vals| classlist = self.classlist retval = true vals.each do |val| unless classlist.include?(val) or self.tags.include?(val) retval = false break end end return retval end # Test whether a given class or definition is defined newfunction(:defined, :rvalue) do |vals| - retval = true - - vals.each do |val| - unless builtintype?(val) or lookuptype(val) - retval = false - break - end + # For some reason, it doesn't want me to return from here. + if vals.detect do |val| Puppet::Type.type(val) or finddefine(val) end + true + else + false end - return retval end newfunction(:fail, :statement) do |vals| vals = vals.collect { |s| s.to_s }.join(" ") if vals.is_a? Array raise Puppet::ParseError, vals.to_s end # Runs a newfunction to create a function for each of the log levels Puppet::Log.levels.each do |level| newfunction(level, :statement) do |vals| send(level, vals.join(" ")) end end newfunction(:template, :rvalue) do |vals| require 'erb' vals.collect do |file| # Use a wrapper, so the template can't get access to the full # Scope object. debug "Retrieving template %s" % file - wrapper = Puppet::Parser::Scope::TemplateWrapper.new(self, file) + wrapper = Puppet::Parser::TemplateWrapper.new(self, file) begin wrapper.result() rescue => detail raise Puppet::ParseError, "Failed to parse template %s: %s" % [file, detail] end end.join("") end end end # $Id$ diff --git a/lib/puppet/parser/grammar.ra b/lib/puppet/parser/grammar.ra index 3a1a78e50..5c91aabdc 100644 --- a/lib/puppet/parser/grammar.ra +++ b/lib/puppet/parser/grammar.ra @@ -1,744 +1,789 @@ # vim: syntax=ruby # the parser class Puppet::Parser::Parser token LBRACK DQTEXT SQTEXT RBRACK LBRACE RBRACE SYMBOL FARROW COMMA TRUE -token FALSE EQUALS LESSEQUAL NOTEQUAL DOT COLON TYPE +token FALSE EQUALS LESSEQUAL NOTEQUAL DOT COLON TYPE LLCOLLECT RRCOLLECT token QMARK LPAREN RPAREN ISEQUAL GREATEREQUAL GREATERTHAN LESSTHAN token IF ELSE IMPORT DEFINE ELSIF VARIABLE CLASS INHERITS NODE BOOLEAN -token NAME SEMIC CASE DEFAULT AT LCOLLECT RCOLLECT +token NAME SEMIC CASE DEFAULT AT LCOLLECT RCOLLECT CLASSNAME CLASSREF +token NOT OR AND # We have 2 shift/reduce conflicts #expect 2 rule program: statements { - # Make sure we always return an array. - if val[0].is_a?(AST::ASTArray) - result = val[0] + if val[0] + # Make sure we always return an array. + if val[0].is_a?(AST::ASTArray) + if val[0].children.empty? + result = nil + else + result = val[0] + end + else + result = aryfy(val[0]) + end else - result = aryfy(val[0]) + result = nil end } - | nothing + | nil -statements: statement +statements: statement | statements statement { - if val[0].instance_of?(AST::ASTArray) - val[0].push(val[1]) - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0],val[1]] + if val[0] and val[1] + if val[0].instance_of?(AST::ASTArray) + val[0].push(val[1]) + result = val[0] + else + result = ast AST::ASTArray, :children => [val[0],val[1]] + end + elsif obj = (val[0] || val[1]) + result = obj + else result = nil end } # The main list of valid statements -statement: object - | collectable +statement: resource + | virtualresource | collection | assignment | casestatement | ifstatement | import | fstatement | definition | hostclass | nodedef + | resourceoverride -fstatement: NAME LPAREN classnames RPAREN { +fstatement: NAME LPAREN namestrings RPAREN { args = aryfy(val[2]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement } - | NAME classnames { + | NAME namestrings { args = aryfy(val[1]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement } -# Includes are just syntactic sugar for classes with no names and -# no arguments. -#include: INCLUDE classnames { -# result = function_include(val[1]) -#} - -# Define a new tag. Both of these functions should really be done generically, -# but I'm not in a position to do that just yet. :/ -#tag: TAG classnames { -# result = function_tag(val[1]) -#} - -classnames: classname - | classnames COMMA classname { +namestrings: namestring + | namestrings COMMA namestring { result = aryfy(val[0], val[2]) result.line = @lexer.line result.file = @lexer.file } -classname: name +namestring: name | variable | quotedtext -#object: name LBRACE objectname COLON params endcomma RBRACE { -object: name LBRACE objectinstances endsemi RBRACE { +resource: NAME LBRACE resourceinstances endsemi RBRACE { if val[0].instance_of?(AST::ASTArray) - raise Puppet::ParseError, "Invalid name" + error "Invalid name" end array = val[2] - if array.instance_of?(AST::ObjectInst) + if array.instance_of?(AST::ResourceInst) array = [array] end result = ast AST::ASTArray - # this iterates across each specified objectinstance + # this iterates across each specified resourceinstance array.each { |instance| - unless instance.instance_of?(AST::ObjectInst) + unless instance.instance_of?(AST::ResourceInst) raise Puppet::Dev, "Got something that isn't an instance" end # now, i need to somehow differentiate between those things with # arrays in their names, and normal things - result.push ast(AST::ObjectDef, + result.push ast(AST::ResourceDef, :type => val[0], - :name => instance[0], + :title => instance[0], :params => instance[1]) } -} | name LBRACE params endcomma RBRACE { - if val[0].instance_of?(AST::ASTArray) - Puppet.notice "invalid name" - raise Puppet::ParseError, "Invalid name" - end - # an object but without a name - # this cannot be an instance of a library type - result = ast AST::ObjectDef, :type => val[0], :params => val[2] - -} | type LBRACE params endcomma RBRACE { +} | NAME LBRACE params endcomma RBRACE { + # This is a deprecated syntax. + error "All resource specifications require names" +} | TYPE LBRACE params endcomma RBRACE { # a template setting for a type if val[0].instance_of?(AST::ASTArray) - raise Puppet::ParseError, "Invalid type" + error "Invalid type" end - result = ast(AST::TypeDefaults, :type => val[0], :params => val[2]) + result = ast(AST::ResourceDefaults, :type => val[0], :params => val[2]) +} + +# Override a value set elsewhere in the configuration. +resourceoverride: resourceref LBRACE params RBRACE { + result = ast AST::ResourceOverride, :object => val[0], :params => val[2] } -# Collectable objects; these get stored in the database, instead of -# being passed to the client. -collectable: AT object { - unless Puppet[:storeconfigs] - raise Puppet::ParseError, "You cannot collect without storeconfigs being set" +# Exported and virtual resources; these don't get sent to the client +# unless they get collected elsewhere in the db. +virtualresource: at resource { + type = val[0] + + if type == :exported and ! Puppet[:storeconfigs] + error "You cannot collect without storeconfigs being set" end - if val[1].is_a? AST::TypeDefaults - raise Puppet::ParseError, "Defaults are not collectable" + if val[1].is_a? AST::ResourceDefaults + error "Defaults are not virtualizable" end - # Just mark our objects as collectable and pass them through. + method = type.to_s + "=" + + # Just mark our resources as exported and pass them through. if val[1].instance_of?(AST::ASTArray) val[1].each do |obj| - obj.collectable = true + obj.send(method, true) end else - val[1].collectable = true + val[1].send(method, true) end result = val[1] } +at: AT { result = :virtual } + | AT AT { result = :exported } + # A collection statement. Currently supports no arguments at all, but eventually # will, I assume. -collection: name LCOLLECT RCOLLECT { - unless Puppet[:storeconfigs] - raise Puppet::ParseError, "You cannot collect without storeconfigs being set" +collection: collectname collectrhand { + if val[0] =~ /^[a-z]/ + Puppet.warning addcontext("Collection names must now be capitalized") + end + type = val[0].downcase + args = {:type => type} + + if val[1].is_a?(AST::CollExpr) + args[:query] = val[1] + args[:query].type = type + args[:form] = args[:query].form + else + args[:form] = val[1] + end + if args[:form] == :exported and ! Puppet[:storeconfigs] + error "You cannot collect exported resources without storeconfigs being set" end - result = ast AST::Collection, :type => val[0] + result = ast AST::Collection, args } -objectinst: objectname COLON params endcomma { - result = ast AST::ObjectInst, :children => [val[0],val[2]] +collectname: TYPE | NAME + +collectrhand: LCOLLECT collstatements RCOLLECT { + if val[1] + result = val[1] + result.form = :virtual + else + result = :virtual + end } + | LLCOLLECT collstatements RRCOLLECT { + if val[1] + result = val[1] + result.form = :exported + else + result = :exported + end +} + +# A mini-language for handling collection comparisons. This is organized +# to avoid the need for precedence indications. +collstatements: nil + | collstatement + | collstatements colljoin collstatement { + result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] +} + +collstatement: collexpr + | LPAREN collstatements RPAREN { + result = val[1] + result.parens = true +} + +colljoin: AND | OR -objectinstances: objectinst - | objectinstances SEMIC objectinst { - if val[0].instance_of?(AST::ObjectInst) +collexpr: colllval ISEQUAL simplervalue { + result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] + #result = ast AST::CollExpr + #result.push *val +} + | colllval NOTEQUAL simplervalue { + result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] + #result = ast AST::CollExpr + #result.push *val +} + +colllval: variable + | name + +resourceinst: resourcename COLON params endcomma { + result = ast AST::ResourceInst, :children => [val[0],val[2]] +} + +resourceinstances: resourceinst + | resourceinstances SEMIC resourceinst { + if val[0].instance_of?(AST::ResourceInst) result = ast AST::ASTArray, :children => [val[0],val[2]] else val[0].push val[2] result = val[0] end } endsemi: # nothing | SEMIC name: NAME { result = ast AST::Name, :value => val[0] } type: TYPE { result = ast AST::Type, :value => val[0] } -objectname: quotedtext +resourcename: quotedtext | name | type | selector | variable | array assignment: VARIABLE EQUALS rvalue { # this is distinct from referencing a variable - variable = ast AST::Name, :value => val[0].sub(/^\$/,'') + variable = ast AST::Name, :value => val[0] result = ast AST::VarDef, :name => variable, :value => val[2] } params: # nothing { result = ast AST::ASTArray } | param { result = val[0] } | params COMMA param { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } param: NAME FARROW rvalue { - leaf = ast AST::String, :value => val[0] - result = ast AST::ObjectParam, :param => leaf, :value => val[2] + result = ast AST::ResourceParam, :param => val[0], :value => val[2] } rvalues: rvalue | rvalues comma rvalue { if val[0].instance_of?(AST::ASTArray) result = val[0].push(val[2]) else result = ast AST::ASTArray, :children => [val[0],val[2]] end } +simplervalue: quotedtext + | name + | type + | boolean + | selector + | variable + rvalue: quotedtext | name | type | boolean | selector | variable | array - | objectref + | resourceref | funcrvalue # We currently require arguments in these functions. -funcrvalue: NAME LPAREN classnames RPAREN { +funcrvalue: NAME LPAREN namestrings RPAREN { args = aryfy(val[2]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :rvalue } quotedtext: DQTEXT { result = ast AST::String, :value => val[0] } | SQTEXT { result = ast AST::FlatString, :value => val[0] } boolean: BOOLEAN { result = ast AST::Boolean, :value => val[0] } -objectref: name LBRACK rvalue RBRACK { - result = ast AST::ObjectRef, :type => val[0], :name => val[2] +resourceref: NAME LBRACK rvalue RBRACK { + Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized") + result = ast AST::ResourceRef, :type => val[0], :title => val[2] +} | TYPE LBRACK rvalue RBRACK { + result = ast AST::ResourceRef, :type => val[0], :title => val[2] } ifstatement: IF iftest LBRACE statements RBRACE else { args = { :test => val[1], :statements => val[3] } if val[5] args[:else] = val[5] end result = ast AST::IfStatement, args } else: # nothing | ELSE LBRACE statements RBRACE { result = ast AST::Else, :statements => val[2] } # Currently we only support a single value, but eventually one assumes # we'll support operators and such. iftest: rvalue casestatement: CASE rvalue LBRACE caseopts RBRACE { options = val[3] unless options.instance_of?(AST::ASTArray) options = ast AST::ASTArray, :children => [val[3]] end result = ast AST::CaseStatement, :test => val[1], :options => options } caseopts: caseopt | caseopts caseopt { if val[0].instance_of?(AST::ASTArray) val[0].push val[1] result = val[0] else result = ast AST::ASTArray, :children => [val[0], val[1]] end } caseopt: casevalues COLON LBRACE statements RBRACE { result = ast AST::CaseOpt, :value => val[0], :statements => val[3] } | casevalues COLON LBRACE RBRACE { result = ast(AST::CaseOpt, :value => val[0], :statements => ast(AST::ASTArray) ) } casevalues: selectlhand | casevalues COMMA selectlhand { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } selector: selectlhand QMARK svalues { result = ast AST::Selector, :param => val[0], :values => val[2] } svalues: selectval | LBRACE sintvalues RBRACE { result = val[1] } sintvalues: selectval | sintvalues comma selectval { if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end } selectval: selectlhand FARROW rvalue { - result = ast AST::ObjectParam, :param => val[0], :value => val[2] + result = ast AST::ResourceParam, :param => val[0], :value => val[2] } selectlhand: name | type | quotedtext | variable | funcrvalue | boolean | DEFAULT { result = ast AST::Default, :value => val[0] } import: IMPORT quotedtext { # importing files # yuk, i hate keywords # we'll probably have to have some kind of search path eventually # but for now, just use a path relative to the file doing the importing dir = @lexer.file.sub(%r{[^/]+$},'').sub(/\/$/, '') if dir == "" dir = "." end result = ast AST::ASTArray Dir.chdir(dir) { # We can't interpolate at this point since we don't have any # scopes set up. Warn the user if they use a variable reference pat = val[1].value if pat.index("$") Puppet.warning( "The import of #{pat} contains a variable reference;" + " variables are not interpolated for imports " + "in file #{@lexer.file} at line #{@lexer.line}" ) end files = Dir.glob(pat) if files.size == 0 files = Dir.glob(pat + ".pp") if files.size == 0 raise Puppet::ImportError.new("No file(s) found for import " + "of '#{pat}'") end end files.each { |file| - parser = Puppet::Parser::Parser.new() + parser = Puppet::Parser::Parser.new(interp) parser.files = self.files Puppet.debug("importing '%s'" % file) unless file =~ /^#{File::SEPARATOR}/ file = File.join(dir, file) end begin parser.file = file rescue Puppet::ImportError Puppet.warning( "Importing %s would result in an import loop" % File.join(dir, file) ) next end # push the results into the main result array # We always return an array when we parse. - parser.parse.each do |child| - result.push child + ast = parser.parse + + # Things that just get added to the classtable or whatever return nil + if ast + ast.each do |child| + result.push child + end end } } } # Disable definition inheritance for now. 8/27/06, luke #definition: DEFINE NAME argumentlist parent LBRACE statements RBRACE { -definition: DEFINE NAME argumentlist LBRACE statements RBRACE { - args = { - :type => ast(AST::Name, :value => val[1]), - :args => val[2], - :code => val[4] # Switch to 5 for parents - } +definition: DEFINE fqname argumentlist LBRACE statements RBRACE { + interp.newdefine fqname(val[1]), :arguments => val[2], :code => val[4] + @lexer.indefine = false + result = nil - if val[3].instance_of?(AST::Name) - args[:parentclass] = val[3] - end - result = ast AST::CompDef, args #} | DEFINE NAME argumentlist parent LBRACE RBRACE { -} | DEFINE NAME argumentlist LBRACE RBRACE { - args = { - :type => ast(AST::Name, :value => val[1]), - :args => val[2], - :code => ast(AST::ASTArray) - } - - if val[3].instance_of?(AST::Name) - args[:parentclass] = val[3] - end - - result = ast AST::CompDef, args +} | DEFINE fqname argumentlist LBRACE RBRACE { + interp.newdefine fqname(val[1]), :arguments => val[2] + @lexer.indefine = false + result = nil } #hostclass: CLASS NAME argumentlist parent LBRACE statements RBRACE { -hostclass: CLASS NAME parent LBRACE statements RBRACE { - #:args => val[2], - args = { - :type => ast(AST::Name, :value => val[1]), - :code => val[4] - } - # It'll be an ASTArray if we didn't get a parent - if val[2].instance_of?(AST::Name) - args[:parentclass] = val[2] - end - result = ast AST::ClassDef, args -} | CLASS NAME parent LBRACE RBRACE { - args = { - :type => ast(AST::Name, :value => val[1]), - :code => ast(AST::ASTArray, :children => []) - } - # It'll be an ASTArray if we didn't get a parent - if val[2].instance_of?(AST::Name) - args[:parentclass] = val[2] - end - result = ast AST::ClassDef, args +hostclass: CLASS fqname parent LBRACE statements RBRACE { + # Our class gets defined in the parent namespace, not our own. + @lexer.namepop + interp.newclass fqname(val[1]), :code => val[4], :parent => val[2] + result = nil +} | CLASS fqname parent LBRACE RBRACE { + # Our class gets defined in the parent namespace, not our own. + @lexer.namepop + interp.newclass fqname(val[1]), :parent => val[2] + result = nil } nodedef: NODE hostnames parent LBRACE statements RBRACE { - unless val[1].instance_of?(AST::ASTArray) - val[1] = ast AST::ASTArray, - :line => val[1].line, - :file => val[1].file, - :children => [val[1]] - end - args = { - :names => val[1], - :code => val[4] - } - if val[2].instance_of?(AST::Name) - args[:parentclass] = val[2] - end - result = ast AST::NodeDef, args + interp.newnode val[1], :parent => val[2], :code => val[4] + result = nil } | NODE hostnames parent LBRACE RBRACE { - unless val[1].instance_of?(AST::ASTArray) - val[1] = ast AST::ASTArray, - :line => val[1].line, - :file => val[1].file, - :children => [val[1]] - end - args = { - :names => val[1], - :code => ast(AST::ASTArray, :children => []) - } - if val[2].instance_of?(AST::Name) - args[:parentclass] = val[2] - end - result = ast AST::NodeDef,args + interp.newnode val[1], :parent => val[2] + result = nil } -# Multiple hostnames, as used for node names. +fqname: NAME + | CLASSNAME + +# Multiple hostnames, as used for node names. These are all literal +# strings, not AST objects. hostnames: hostname | hostnames COMMA hostname { - if val[0].instance_of?(AST::ASTArray) - result = val[0] - result.push val[2] - else - result = ast AST::ASTArray, :children => [val[0], val[2]] - end + result = val[0] + result = [result] unless result.is_a?(Array) + result << val[2] } -hostname: NAME { - result = ast AST::HostName, :value => val[0] -} | SQTEXT { - result = ast AST::HostName, :value => val[0] -} | DEFAULT { - result = ast AST::Default, :value => val[0] +hostname: NAME + | SQTEXT + | DEFAULT + +nil: { + result = nil } nothing: { result = ast AST::ASTArray, :children => [] } -argumentlist: nothing +argumentlist: nil | LPAREN nothing RPAREN { - result = val[1] + result = nil } | LPAREN arguments RPAREN { - if val[1].instance_of?(AST::ASTArray) - result = val[1] - else - result = ast AST::ASTArray, :children => [val[1]] - end + result = val[1] + result = [result] unless result[0].is_a?(Array) } arguments: argument | arguments COMMA argument { - if val[0].instance_of?(AST::ASTArray) - val[0].push(val[2]) - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0],val[2]] - end + result = val[0] + result = [result] unless result[0].is_a?(Array) + result << val[2] } -argument: name EQUALS rvalue { - msg = "Deprecation notice: #{val[0].value} must now include '$' in prototype" - msg += " at line %s" % @lexer.line - msg += " in file %s" % @lexer.file if @lexer.file - Puppet.warning msg - result = ast AST::CompArgument, :children => [val[0],val[2]] +argument: NAME EQUALS rvalue { + Puppet.warning addcontext("Deprecation notice: #{val[0].value} must now include '$' in prototype") + result = [val[0], val[2]] } - | name { - msg = "Deprecation notice: #{val[0].value} must now include '$' in prototype" - msg += " at line %s" % @lexer.line - msg += " in file %s" % @lexer.file if @lexer.file - Puppet.warning msg - result = ast AST::CompArgument, :children => [val[0]] -} | lvariable EQUALS rvalue { - result = ast AST::CompArgument, :children => [val[0],val[2]] -} | lvariable { - result = ast AST::CompArgument, :children => [val[0]] + | NAME { + Puppet.warning addcontext("Deprecation notice: #{val[0].value} must now include '$' in prototype") + result = [val[0]] +} | VARIABLE EQUALS rvalue { + result = [val[0], val[2]] +} | VARIABLE { + result = [val[0]] } -parent: nothing +parent: nil | INHERITS NAME { - result = ast AST::Name, :value => val[1] + result = val[1] } variable: VARIABLE { - name = val[0].sub(/^\$/,'') - result = ast AST::Variable, :value => name -} - -# This is variables as lvalues; we're assigning them, not deferencing them. -lvariable: VARIABLE { - result = ast AST::Name, :value => val[0].sub(/^\$/,'') + result = ast AST::Variable, :value => val[0] } array: LBRACK rvalues RBRACK { if val[1].instance_of?(AST::ASTArray) result = val[1] else result = ast AST::ASTArray, :children => [val[1]] end } | LBRACK RBRACK { result = ast AST::ASTArray } comma: FARROW | COMMA endcomma: # nothing | COMMA { result = nil } end ---- header ---- require 'puppet' require 'puppet/loadedfile' require 'puppet/parser/lexer' require 'puppet/parser/ast' #require 'puppet/parser/interpreter' module Puppet class ParseError < Puppet::Error; end class ImportError < Racc::ParseError; end end Puppet[:typecheck] = true Puppet[:paramcheck] = true ---- inner ---- require 'puppet/parser/functions' -attr_reader :file +attr_reader :file, :interp attr_accessor :files +# Add context to a message; useful for error messages and such. +def addcontext(message, obj = nil) + obj ||= @lexer + + message += " on line %s" % obj.line + if file = obj.file + message += " in file %s" % file + end + + return message +end + # Create an AST array out of all of the args def aryfy(*args) if args[0].instance_of?(AST::ASTArray) result = args.shift args.each { |arg| result.push arg } else result = ast AST::ASTArray, :children => args end return result end # Create an AST object, and automatically add the file and line information if # available. def ast(klass, hash = nil) hash ||= {} unless hash[:line] hash[:line] = @lexer.line end unless hash[:file] if file = @lexer.file hash[:file] = file end end return klass.new(hash) end +# Raise a Parse error. +def error(message) + except = Puppet::ParseError.new(message) + except.line = @lexer.line + if @lexer.file + except.file = @lexer.file + end + + raise except +end + def file=(file) unless FileTest.exists?(file) unless file =~ /\.pp$/ file = file + ".pp" end unless FileTest.exists?(file) raise Puppet::Error, "Could not find file %s" % file end end if @files.detect { |f| f.file == file } raise Puppet::ImportError.new("Import loop detected") else @files << Puppet::LoadedFile.new(file) @lexer.file = file end end -def initialize +def initialize(interpreter) + @interp = interpreter + initvars() +end + +# Initialize or reset all of our variables. +def initvars @lexer = Puppet::Parser::Lexer.new() @files = [] - #if Puppet[:debug] - # @yydebug = true - #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 +# The fully qualifed name, with the full namespace. +def fqname(name) + [@lexer.namespace, name].join("::").sub(/^::/, '') end def on_error(token,value,stack) #on '%s' at '%s' in\n'%s'" % [token,value,stack] #error = "line %s: parse error after '%s'" % # [@lexer.line,@lexer.last] error = "Syntax error at '%s'" % [value] except = Puppet::ParseError.new(error) except.line = @lexer.line if @lexer.file except.file = @lexer.file end raise except end # how should I do error handling here? def parse(string = nil) if string self.string = string end begin - yyparse(@lexer,:scan) + main = yyparse(@lexer,:scan) rescue Racc::ParseError => except error = Puppet::ParseError.new(except) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error rescue Puppet::ParseError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::Error => except # and this is a framework error except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::DevError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue => except error = Puppet::DevError.new(except.message) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error end + if main + # Store the results as the top-level class. + interp.newclass("", :code => main) + return main + end +ensure + @lexer.clear end # See if any of the files have changed. def reparse? if file = @files.detect { |file| file.changed? } return file.stamp else return false end end def string=(string) @lexer.string = string end # Make emacs happy # Local Variables: # mode: ruby # End: # $Id$ diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 26bf8104e..111fe2ed3 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,466 +1,777 @@ # 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/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 -module Puppet - module Parser - class 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."] - ) + return @ldap + end - attr_accessor :ast + def clear + initparsevars + end - class << self - attr_writer :ldap - end - # just shorten the constant path a bit, using what amounts to an alias - AST = Puppet::Parser::AST - - # 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]) + # 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 - @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 + rescue Timeout::Error + raise Puppet::DevError, "Got a timeout trying to evaluate all definitions" + end + end - # create our interpreter - def initialize(hash) - if @code = hash[:Code] - @file = nil # to avoid warnings - elsif ! @file = hash[:Manifest] - raise Puppet::DevError, "You must provide code or a manifest" - end + # Evaluate a specific node. + def evalnode(client, scope, facts) + return unless self.usenodes - if hash.include?(:UseNodes) - @usenodes = hash[:UseNodes] - else - @usenodes = true - end + 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 - # By default, we only search the parse tree. - @nodesources = [] + if names.empty? + raise Puppet::Error, + "Cannot evaluate nodes with a nil client" + end - if Puppet[:ldapnodes] - @nodesources << :ldap - 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 - if hash[:NodeSources] - 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 + # Evaluate all of the code we can find that's related to our client. + def evaluate(client, facts) - @setup = false + scope = Puppet::Parser::Scope.new(:interp => self) # no parent scope + scope.name = "top" + scope.type = "main" - # Set it to either the value or nil. This is currently only used - # by the cfengine module. - @classes = hash[:Classes] || [] + scope.host = facts["hostname"] || Facter.value("hostname") - @local = hash[:Local] || false + classes = @classes.dup - 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 + # Okay, first things first. Set our facts. + scope.setfacts(facts) - if Puppet[:storeconfigs] - Puppet::Rails.init - end + # 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 - @files = [] + # Next evaluate the node + evalnode(client, scope, facts) - # Create our parser object - parsefiles + # 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 - # Search for our node in the various locations. This only searches - # locations external to the files; the scope is responsible for - # searching the parse tree. - def nodesearch(*nodes) - # At this point, stop at the first source that defines - # the node - @nodesources.each do |source| - method = "nodesearch_%s" % source - parent = nil - nodeclasses = nil - if self.respond_to? method - nodes.each do |node| - parent, nodeclasses = self.send(method, node) - - if parent or (nodeclasses and !nodeclasses.empty?) - Puppet.info "Found %s in %s" % [node, source] - return parent, nodeclasses - else - # Look for a default node. - parent, nodeclasses = self.send(method, "default") - if parent or (nodeclasses and !nodeclasses.empty?) - Puppet.info "Found default node for %s in %s" % - [node, source] - return parent, nodeclasses - end - end - 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) - return nil, nil + # Now perform the collections + scope.collections.each do |coll| + coll.evaluate + 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 - # Find the ldap node and extra the info, returning just - # the critical data. - def nodesearch_ldap(node) - unless defined? @ldap and @ldap - setup_ldap() - unless @ldap - Puppet.info "Skipping ldap source; no ldap connection" - return nil, [] - end - end + storeconfigs(args) + end - if node =~ /\./ - node = node.sub(/\..+/, '') - end + # Now, finally, convert our scope tree + resources into a tree of + # buckets and objects. + objects = scope.translate - 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 + # Add the class list + unless scope.classlist.empty? + objects.classes = scope.classlist + end - if filter =~ /%s/ - filter = filter.gsub(/%s/, node) - end + return objects + 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 + # 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 - 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 + # Find a class definition, relative to the current namespace. + def findclass(namespace, name) + fqfind namespace, name, @classtable + end - classes.flatten! + # Find a component definition, relative to the current namespace. + def finddefine(namespace, name) + fqfind namespace, name, @definetable + end - return parent, classes - 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("::") - def parsedate - parsefiles() - @parsedate + while ary.length > 0 + newname = (ary + [name]).join("::").sub(/^::/, '') + if obj = table[newname] + return obj end - # Add a new file to check for updateness. - def newfile(file) - unless @files.find { |f| f.file == file } - @files << Puppet::LoadedFile.new(file) - end - end + # Delete the second to last object, which reduces our namespace by one. + ary.pop + 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 - begin - self.send(method) - rescue => detail - raise Puppet::Error, - "Could not set up node source %s" % source - end - end - } - end - parsefiles() + # 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, + :code => AST::ASTArray.new(:pin => "[]"), + :interp => self, + :fqname => name + } + 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 + ) + + 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 = [:code] - # Really, we should stick multiple names in here - # but for now just make a simple array - names = [client] + if Puppet[:ldapnodes] + # Nodes in the file override nodes in ldap. + @nodesources << :ldap + end - # Make sure both the fqdn and the short name of the - # host can be used in the manifest - if client =~ /\./ - names << client.sub(/\..+/,'') + 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 - names << "#{client}.#{facts['domain']}" + Puppet.warning "Node source '#{src}' not supported" end + end + end - scope = Puppet::Parser::Scope.new() # no parent scope - scope.name = "top" - scope.type = "puppet" - scope.interp = self + @setup = false - classes = @classes.dup + initparsevars() - args = {:ast => @ast, :facts => facts, :classes => classes} + # Set it to either the value or nil. This is currently only used + # by the cfengine module. + @classes = hash[:Classes] || [] - if @usenodes - unless client - raise Puppet::Error, - "Cannot evaluate nodes with a nil client" - end + @local = hash[:Local] || false - args[:names] = names + 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 + 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 - parent, nodeclasses = nodesearch(*names) + if node =~ /\./ + node = node.sub(/\..+/, '') + end - args[:classes] += nodeclasses if nodeclasses + 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 - args[:parentnode] = parent if parent + if filter =~ /%s/ + filter = filter.gsub(/%s/, node) + end - if nodeclasses or parent - args[:searched] = true + 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 - begin - objects = scope.evaluate(args) - rescue Puppet::DevError, Puppet::Error, Puppet::ParseError => except - raise - rescue => except - error = Puppet::DevError.new("%s: %s" % - [except.class, except.message]) - error.set_backtrace except.backtrace - raise error - 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 - if Puppet[:storeconfigs] - storeconfigs( - :objects => objects, - :host => client, - :facts => facts - ) - end + classes.flatten! + + 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 - return objects + # 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 - # 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 + # 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 - begin - @ldap = self.class.ldap() - rescue => detail - raise Puppet::Error, "Could not connect to LDAP: %s" % detail + + 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) + args = {:type => name, :namespace => ns, :fqname => fqname, :interp => self} + args[:code] = code if code + args[:parentclass] = parent if parent + @classtable[fqname] = @parser.ast AST::HostClass, args + end - def scope - return @scope - end + return @classtable[fqname] + end - private + # 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 - # Check whether any of our files have changed. - def checkfiles - if @files.find { |f| f.changed? } - @parsedate = Time.now.to_i - 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 - # 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 + # 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 + else + # Look for a default node. + if defobj = self.send(method, "default") + Puppet.info "Found default node for %s in %s" % + [node, source] + return defobj end end + end + end + end - unless FileTest.exists?(@file) - if @ast - return - else - raise Puppet::Error, "Manifest %s must exist" % @file - 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 and ! classes.empty? + 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 + + # Iteratively make sure that every object in the scope tree is translated. + def translate(scope) + end + + private - # should i be creating a new parser each time...? - @parser = Puppet::Parser::Parser.new() - if @code - @parser.string = @code + # 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 - @parser.file = @file - # Mark when we parsed, so we can check freshness - @parsedate = File.stat(@file).ctime.to_i + return false end + end - if @local - @ast = @parser.parse + unless FileTest.exists?(@file) + # If we've already parsed, then we're ok. + if findclass("", "") + return else - benchmark(:info, "Parsed manifest") do - @ast = @parser.parse - end + raise Puppet::Error, "Manifest %s must exist" % @file end - @parsedate = Time.now.to_i end + 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 + # 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 - 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[:client]}") 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 - # We store all of the objects, even the collectable ones - benchmark(:info, "Stored configuration for #{hash[:client]}") do - Puppet::Rails::Host.transaction do - Puppet::Rails::Host.store(hash) - 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 - - # 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! + } + else + # 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 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/lexer.rb b/lib/puppet/parser/lexer.rb index 80a8715ba..39fef8668 100644 --- a/lib/puppet/parser/lexer.rb +++ b/lib/puppet/parser/lexer.rb @@ -1,236 +1,298 @@ # the scanner/lexer require 'strscan' require 'puppet' module Puppet class LexError < RuntimeError; end module Parser #--------------------------------------------------------------- class Lexer attr_reader :line, :last, :file + attr_accessor :indefine + #%r{\w+} => :WORD, @@tokens = { %r{#.*} => :COMMENT, %r{\[} => :LBRACK, %r{\]} => :RBRACK, %r{\{} => :LBRACE, %r{\}} => :RBRACE, %r{\(} => :LPAREN, %r{\)} => :RPAREN, %r{\"} => :DQUOTE, %r{\n} => :RETURN, %r{\'} => :SQUOTE, %r{=} => :EQUALS, %r{==} => :ISEQUAL, %r{>=} => :GREATEREQUAL, %r{>} => :GREATERTHAN, %r{<} => :LESSTHAN, %r{<=} => :LESSEQUAL, %r{!=} => :NOTEQUAL, + %r{!} => :NOT, %r{,} => :COMMA, %r{\.} => :DOT, %r{:} => :COLON, %r{@} => :AT, + %r{<<\|} => :LLCOLLECT, + %r{\|>>} => :RRCOLLECT, %r{<\|} => :LCOLLECT, %r{\|>} => :RCOLLECT, %r{;} => :SEMIC, %r{\?} => :QMARK, %r{\\} => :BACKSLASH, %r{=>} => :FARROW, %r{[a-z][-\w]*} => :NAME, + %r{([a-z][-\w]*::)+[a-z][-\w]*} => :CLASSNAME, + %r{([A-Z][-\w]*::)+[A-Z][-\w]*} => :CLASSREF, %r{[A-Z][-\w]*} => :TYPE, %r{[0-9]+} => :NUMBER, %r{\$\w+} => :VARIABLE } @@keywords = { "case" => :CASE, "class" => :CLASS, "default" => :DEFAULT, "define" => :DEFINE, "false" => :BOOLEAN, "import" => :IMPORT, "if" => :IF, "elsif" => :ELSIF, "else" => :ELSE, "inherits" => :INHERITS, "node" => :NODE, - "true" => :BOOLEAN + "true" => :BOOLEAN, + "and" => :AND, + "or" => :OR } + def clear + initvars + end + # scan the whole file # basically just used for testing def fullscan array = [] self.scan { |token,str| #Puppet.debug("got token '%s' => '%s'" % [token,str]) if token.nil? return array else array.push([token,str]) end } return array end # this is probably pretty damned inefficient... # it'd be nice not to have to load the whole file first... def file=(file) @file = file @line = 1 File.open(file) { |of| str = "" of.each { |line| str += line } @scanner = StringScanner.new(str) } end + def indefine? + if defined? @indefine + @indefine + else + false + end + end + def initialize + initvars() + end + + def initvars @line = 1 @last = "" + @lasttoken = nil @scanner = nil @file = nil # AAARRGGGG! okay, regexes in ruby are bloody annoying # no one else has "\n" =~ /\s/ @skip = %r{[ \t]+} + + @namestack = [] + @indefine = false + end + + # Go up one in the namespace. + def namepop + @namestack.pop + end + + # Collect the current namespace. + def namespace + @namestack.join("::") + end + + # This value might have :: in it, but we don't care -- it'll be + # handled normally when joining, and when popping we want to pop + # this full value, however long the namespace is. + def namestack(value) + @namestack << value end def rest @scanner.rest end # this is the heart of the lexer def scan #Puppet.debug("entering scan") if @scanner.nil? raise TypeError.new("Invalid or empty string") end @scanner.skip(@skip) until @scanner.eos? do yielded = false sendbreak = false # gah, this is a nasty hack stoken = nil sregex = nil value = "" # first find out which type of token we've got @@tokens.each { |regex,token| # we're just checking, which doesn't advance the scan # pointer tmp = @scanner.check(regex) if tmp.nil? #puppet.debug("did not match %s to '%s'" % # [regex,@scanner.rest]) next end # find the longest match if tmp.length > value.length value = tmp stoken = token sregex = regex else # we've already got a longer match next end } # error out if we didn't match anything at all if stoken.nil? nword = nil if @scanner.rest =~ /^(\S+)/ nword = $1 elsif@scanner.rest =~ /^(\s+)/ nword = $1 else nword = @scanner.rest end raise "Could not match '%s'" % nword end value = @scanner.scan(sregex) if value == "" raise "Didn't match regex on token %s" % stoken end # token-specific operations # if this gets much more complicated, it should # be moved up to where the tokens themselves are defined # which will get me about 75% of the way to a lexer generator + ptoken = stoken case stoken when :NAME then wtoken = stoken # we're looking for keywords here if @@keywords.include?(value) wtoken = @@keywords[value] #Puppet.debug("token '%s'" % wtoken) + if wtoken == :BOOLEAN + value = eval(value) + end end - yield [wtoken,value] - @last = value + ptoken = wtoken when :NUMBER then - yield [:NAME,value] - # just throw comments away + ptoken = :NAME when :COMMENT then # just throw comments away + next when :RETURN then @line += 1 @scanner.skip(@skip) + next when :SQUOTE then #Puppet.debug("searching '%s' after '%s'" % [self.rest,value]) value = self.slurpstring(value) - yield [:SQTEXT,value] - @last = value - #stoken = :DQTEXT + ptoken = :SQTEXT #Puppet.debug("got string '%s' => '%s'" % [:DQTEXT,value]) when :DQUOTE then - #Puppet.debug("searching '%s' after '%s'" % [self.rest,value]) value = self.slurpstring(value) - yield [:DQTEXT,value] - @last = value - #stoken = :DQTEXT - #Puppet.debug("got string '%s' => '%s'" % [:DQTEXT,value]) - else - #Puppet.debug("got token '%s' => '%s'" % [stoken,value]) - yield [stoken,value] - @last = value + ptoken = :DQTEXT + when :VARIABLE then + value = value.sub(/^\$/, '') + end + + yield [ptoken, value] + + if @lasttoken == :CLASS + namestack(value) end + + if @lasttoken == :DEFINE + if indefine? + self.indefine = false + raise Puppet::ParseError, + "Definitions cannot nest" + end + + @indefine = true + end + + @last = value + @lasttoken = ptoken + @scanner.skip(@skip) end @scanner = nil yield [false,false] end # we've encountered an opening quote... # slurp in the rest of the string and return it def slurpstring(quote) # we search for the next quote that isn't preceded by a # backslash; the caret is there to match empty strings str = @scanner.scan_until(/([^\\]|^)#{quote}/) if str.nil? raise Puppet::LexError.new("Unclosed quote after '%s' in '%s'" % [self.last,self.rest]) else str.sub!(/#{quote}\Z/,"") str.gsub!(/\\#{quote}/,quote) end return str end # just parse a string, not a whole file def string=(string) @scanner = StringScanner.new(string) end end #--------------------------------------------------------------- end end # $Id$ diff --git a/lib/puppet/parser/parser.rb b/lib/puppet/parser/parser.rb index 3cda43ee0..047dace3a 100644 --- a/lib/puppet/parser/parser.rb +++ b/lib/puppet/parser/parser.rb @@ -1,1519 +1,1692 @@ # # DO NOT MODIFY!!!! # This file is automatically generated by racc 1.4.5 # from racc grammer file "grammar.ra". # require 'racc/parser' require 'puppet' require 'puppet/loadedfile' require 'puppet/parser/lexer' require 'puppet/parser/ast' #require 'puppet/parser/interpreter' module Puppet class ParseError < Puppet::Error; end class ImportError < Racc::ParseError; end end Puppet[:typecheck] = true Puppet[:paramcheck] = true module Puppet module Parser class Parser < Racc::Parser -module_eval <<'..end grammar.ra modeval..idc4b6d943e3', 'grammar.ra', 603 +module_eval <<'..end grammar.ra modeval..ide506d3a623', 'grammar.ra', 621 require 'puppet/parser/functions' -attr_reader :file +attr_reader :file, :interp attr_accessor :files +# Add context to a message; useful for error messages and such. +def addcontext(message, obj = nil) + obj ||= @lexer + + message += " on line %s" % obj.line + if file = obj.file + message += " in file %s" % file + end + + return message +end + # Create an AST array out of all of the args def aryfy(*args) if args[0].instance_of?(AST::ASTArray) result = args.shift args.each { |arg| result.push arg } else result = ast AST::ASTArray, :children => args end return result end # Create an AST object, and automatically add the file and line information if # available. def ast(klass, hash = nil) hash ||= {} unless hash[:line] hash[:line] = @lexer.line end unless hash[:file] if file = @lexer.file hash[:file] = file end end return klass.new(hash) end +# Raise a Parse error. +def error(message) + except = Puppet::ParseError.new(message) + except.line = @lexer.line + if @lexer.file + except.file = @lexer.file + end + + raise except +end + def file=(file) unless FileTest.exists?(file) unless file =~ /\.pp$/ file = file + ".pp" end unless FileTest.exists?(file) raise Puppet::Error, "Could not find file %s" % file end end if @files.detect { |f| f.file == file } raise Puppet::ImportError.new("Import loop detected") else @files << Puppet::LoadedFile.new(file) @lexer.file = file end end -def initialize +def initialize(interpreter) + @interp = interpreter + initvars() +end + +# Initialize or reset all of our variables. +def initvars @lexer = Puppet::Parser::Lexer.new() @files = [] - #if Puppet[:debug] - # @yydebug = true - #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 +# The fully qualifed name, with the full namespace. +def fqname(name) + [@lexer.namespace, name].join("::").sub(/^::/, '') end def on_error(token,value,stack) #on '%s' at '%s' in\n'%s'" % [token,value,stack] #error = "line %s: parse error after '%s'" % # [@lexer.line,@lexer.last] error = "Syntax error at '%s'" % [value] except = Puppet::ParseError.new(error) except.line = @lexer.line if @lexer.file except.file = @lexer.file end raise except end # how should I do error handling here? def parse(string = nil) if string self.string = string end begin - yyparse(@lexer,:scan) + main = yyparse(@lexer,:scan) rescue Racc::ParseError => except error = Puppet::ParseError.new(except) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error rescue Puppet::ParseError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::Error => except # and this is a framework error except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::DevError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue => except error = Puppet::DevError.new(except.message) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error end + if main + # Store the results as the top-level class. + interp.newclass("", :code => main) + return main + end +ensure + @lexer.clear end # See if any of the files have changed. def reparse? if file = @files.detect { |file| file.changed? } return file.stamp else return false end end def string=(string) @lexer.string = string end # Make emacs happy # Local Variables: # mode: ruby # End: # $Id$ -..end grammar.ra modeval..idc4b6d943e3 +..end grammar.ra modeval..ide506d3a623 ##### racc 1.4.5 generates ### racc_reduce_table = [ 0, 0, :racc_error, - 1, 44, :_reduce_1, - 1, 44, :_reduce_none, - 1, 45, :_reduce_none, - 2, 45, :_reduce_4, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 1, 47, :_reduce_none, - 4, 55, :_reduce_16, - 2, 55, :_reduce_17, - 1, 59, :_reduce_none, - 3, 59, :_reduce_19, - 1, 60, :_reduce_none, - 1, 60, :_reduce_none, - 1, 60, :_reduce_none, - 5, 48, :_reduce_23, - 5, 48, :_reduce_24, - 5, 48, :_reduce_25, - 2, 49, :_reduce_26, - 3, 50, :_reduce_27, - 4, 69, :_reduce_28, - 1, 64, :_reduce_none, - 3, 64, :_reduce_30, - 0, 65, :_reduce_none, - 1, 65, :_reduce_none, - 1, 61, :_reduce_33, - 1, 68, :_reduce_34, - 1, 70, :_reduce_none, - 1, 70, :_reduce_none, - 1, 70, :_reduce_none, - 1, 70, :_reduce_none, - 1, 70, :_reduce_none, - 1, 70, :_reduce_none, - 3, 51, :_reduce_41, - 0, 66, :_reduce_42, - 1, 66, :_reduce_43, - 3, 66, :_reduce_44, - 3, 74, :_reduce_45, - 1, 75, :_reduce_none, - 3, 75, :_reduce_47, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 1, 73, :_reduce_none, - 4, 79, :_reduce_57, - 1, 63, :_reduce_58, - 1, 63, :_reduce_59, - 1, 77, :_reduce_60, - 4, 78, :_reduce_61, - 6, 53, :_reduce_62, - 0, 81, :_reduce_none, - 4, 81, :_reduce_64, + 1, 51, :_reduce_1, + 1, 51, :_reduce_none, + 1, 52, :_reduce_none, + 2, 52, :_reduce_4, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 1, 54, :_reduce_none, + 4, 62, :_reduce_17, + 2, 62, :_reduce_18, + 1, 67, :_reduce_none, + 3, 67, :_reduce_20, + 1, 68, :_reduce_none, + 1, 68, :_reduce_none, + 1, 68, :_reduce_none, + 5, 55, :_reduce_24, + 5, 55, :_reduce_25, + 5, 55, :_reduce_26, + 4, 66, :_reduce_27, + 2, 56, :_reduce_28, + 1, 77, :_reduce_29, + 2, 77, :_reduce_30, + 2, 57, :_reduce_31, + 1, 78, :_reduce_none, + 1, 78, :_reduce_none, + 3, 79, :_reduce_34, + 3, 79, :_reduce_35, + 1, 80, :_reduce_none, 1, 80, :_reduce_none, - 5, 52, :_reduce_66, + 3, 80, :_reduce_38, + 1, 81, :_reduce_none, + 3, 81, :_reduce_40, 1, 82, :_reduce_none, - 2, 82, :_reduce_68, - 5, 83, :_reduce_69, - 4, 83, :_reduce_70, + 1, 82, :_reduce_none, + 3, 83, :_reduce_43, + 3, 83, :_reduce_44, + 1, 84, :_reduce_none, 1, 84, :_reduce_none, - 3, 84, :_reduce_72, - 3, 71, :_reduce_73, - 1, 86, :_reduce_none, - 3, 86, :_reduce_75, - 1, 88, :_reduce_none, - 3, 88, :_reduce_77, - 3, 87, :_reduce_78, + 4, 86, :_reduce_47, + 1, 72, :_reduce_none, + 3, 72, :_reduce_49, + 0, 73, :_reduce_none, + 1, 73, :_reduce_none, + 1, 69, :_reduce_52, + 1, 88, :_reduce_53, + 1, 87, :_reduce_none, + 1, 87, :_reduce_none, + 1, 87, :_reduce_none, + 1, 87, :_reduce_none, + 1, 87, :_reduce_none, + 1, 87, :_reduce_none, + 3, 58, :_reduce_60, + 0, 74, :_reduce_61, + 1, 74, :_reduce_62, + 3, 74, :_reduce_63, + 3, 92, :_reduce_64, + 1, 93, :_reduce_none, + 3, 93, :_reduce_66, 1, 85, :_reduce_none, 1, 85, :_reduce_none, 1, 85, :_reduce_none, 1, 85, :_reduce_none, 1, 85, :_reduce_none, 1, 85, :_reduce_none, - 1, 85, :_reduce_85, - 2, 54, :_reduce_86, - 6, 56, :_reduce_87, - 5, 56, :_reduce_88, - 6, 57, :_reduce_89, - 5, 57, :_reduce_90, - 6, 58, :_reduce_91, - 5, 58, :_reduce_92, 1, 91, :_reduce_none, - 3, 91, :_reduce_94, - 1, 92, :_reduce_95, - 1, 92, :_reduce_96, - 1, 92, :_reduce_97, - 0, 46, :_reduce_98, - 1, 89, :_reduce_none, - 3, 89, :_reduce_100, - 3, 89, :_reduce_101, - 1, 93, :_reduce_none, - 3, 93, :_reduce_103, - 3, 94, :_reduce_104, - 1, 94, :_reduce_105, - 3, 94, :_reduce_106, - 1, 94, :_reduce_107, - 1, 90, :_reduce_none, - 2, 90, :_reduce_109, - 1, 62, :_reduce_110, - 1, 95, :_reduce_111, - 3, 72, :_reduce_112, - 2, 72, :_reduce_113, - 1, 76, :_reduce_none, - 1, 76, :_reduce_none, - 0, 67, :_reduce_none, - 1, 67, :_reduce_117 ] - -racc_reduce_n = 118 - -racc_shift_n = 197 + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 4, 96, :_reduce_82, + 1, 71, :_reduce_83, + 1, 71, :_reduce_84, + 1, 95, :_reduce_85, + 4, 76, :_reduce_86, + 4, 76, :_reduce_87, + 6, 60, :_reduce_88, + 0, 98, :_reduce_none, + 4, 98, :_reduce_90, + 1, 97, :_reduce_none, + 5, 59, :_reduce_92, + 1, 99, :_reduce_none, + 2, 99, :_reduce_94, + 5, 100, :_reduce_95, + 4, 100, :_reduce_96, + 1, 101, :_reduce_none, + 3, 101, :_reduce_98, + 3, 89, :_reduce_99, + 1, 103, :_reduce_none, + 3, 103, :_reduce_101, + 1, 105, :_reduce_none, + 3, 105, :_reduce_103, + 3, 104, :_reduce_104, + 1, 102, :_reduce_none, + 1, 102, :_reduce_none, + 1, 102, :_reduce_none, + 1, 102, :_reduce_none, + 1, 102, :_reduce_none, + 1, 102, :_reduce_none, + 1, 102, :_reduce_111, + 2, 61, :_reduce_112, + 6, 63, :_reduce_113, + 5, 63, :_reduce_114, + 6, 64, :_reduce_115, + 5, 64, :_reduce_116, + 6, 65, :_reduce_117, + 5, 65, :_reduce_118, + 1, 106, :_reduce_none, + 1, 106, :_reduce_none, + 1, 109, :_reduce_none, + 3, 109, :_reduce_122, + 1, 110, :_reduce_none, + 1, 110, :_reduce_none, + 1, 110, :_reduce_none, + 0, 53, :_reduce_126, + 0, 111, :_reduce_127, + 1, 107, :_reduce_none, + 3, 107, :_reduce_129, + 3, 107, :_reduce_130, + 1, 112, :_reduce_none, + 3, 112, :_reduce_132, + 3, 113, :_reduce_133, + 1, 113, :_reduce_134, + 3, 113, :_reduce_135, + 1, 113, :_reduce_136, + 1, 108, :_reduce_none, + 2, 108, :_reduce_138, + 1, 70, :_reduce_139, + 3, 90, :_reduce_140, + 2, 90, :_reduce_141, + 1, 94, :_reduce_none, + 1, 94, :_reduce_none, + 0, 75, :_reduce_none, + 1, 75, :_reduce_145 ] + +racc_reduce_n = 146 + +racc_shift_n = 240 racc_action_table = [ - 48, 36, 38, 81, 162, 20, 48, 36, 38, 64, - 79, 161, 48, 36, 38, 86, 20, 36, 38, 75, - 36, 38, 20, 37, -79, 145, 36, 38, 20, 44, - 37, -82, -79, 49, 53, 44, 31, 55, 31, 49, - 53, 44, 72, 55, 65, 49, 53, -81, 44, 55, - 48, 36, 38, 37, 44, 145, 48, 36, 38, 37, - 37, 137, 48, 36, 38, 113, 20, 139, 29, 79, - 29, 33, 20, 33, -80, 79, 85, 170, 20, 44, - 154, 146, 102, 49, 53, 44, 115, 55, 171, 49, - 53, 44, 149, 55, 77, 49, 53, 36, 38, 55, - 48, 36, 38, 36, 38, 116, 48, 36, 38, 117, - 118, 88, 48, 36, 38, 179, 20, 117, 118, -81, - 45, 87, 20, 112, 155, 44, -82, 158, 20, 44, - 37, 44, 105, 49, 53, 44, 37, 55, 64, 49, - 53, 44, 163, 55, 77, 49, 53, 85, 84, 55, - 48, 36, 38, -80, 169, 72, 48, 36, 38, 172, - 173, -83, 48, 36, 38, -84, 20, 178, 108, 136, - 182, 77, 20, 36, 38, 112, 71, 160, 20, 44, - 109, 70, 69, 49, 53, 44, 113, 55, 20, 49, - 53, 44, 190, 55, 66, 49, 93, 36, 38, 55, - 112, 44, 35, 28, 166, 49, 53, 36, 38, 55, - 125, nil, 20, 36, 38, nil, nil, nil, nil, 36, - 38, nil, 20, nil, nil, 44, nil, nil, 20, 49, - 53, nil, nil, 55, 20, 44, nil, nil, nil, 49, - 53, 44, nil, 55, nil, 49, 53, 44, nil, 55, - nil, 49, 53, 36, 38, 55, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 20, 176, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 20, 44, nil, nil, 168, 49, 53, nil, 12, 55, - 16, 19, nil, 24, 26, 20, 3, nil, 9, 175, - 14, nil, 21, 12, nil, 16, 19, nil, 24, 26, - 20, 3, nil, 9, 196, 14, nil, 21, 12, nil, - 16, 19, nil, 24, 26, 20, 3, nil, 9, 153, - 14, nil, 21, 12, nil, 16, 19, nil, 24, 26, - 20, 3, nil, 9, 185, 14, nil, 21, 12, nil, - 16, 19, nil, 24, 26, 20, 3, nil, 9, 189, - 14, nil, 21, 12, nil, 16, 19, nil, 24, 26, - 20, 3, nil, 9, 195, 14, nil, 21, 12, nil, - 16, 19, nil, 24, 26, 20, 3, nil, 9, 148, - 14, nil, 21, 12, nil, 16, 19, nil, 24, 26, - 20, 3, nil, 9, 193, 14, nil, 21, 12, nil, - 16, 19, nil, 24, 26, 20, 3, nil, 9, nil, - 14, nil, 21, 12, nil, 16, 19, nil, 24, 26, - 20, 3, nil, 9, nil, 14, nil, 21, 12, nil, - 16, 19, nil, 24, 26, 20, 3, nil, 9, nil, - 14, nil, 21, 12, nil, 16, 19, nil, 24, 26, - 20, 3, nil, 9, nil, 14, nil, 21, 12, nil, - 16, 19, nil, 24, 26, 20, 3, nil, 9, nil, - 14, nil, 21, 12, nil, 16, 19, nil, 24, 26, - nil, 3, nil, 9, nil, 14, nil, 21 ] + 59, 50, 52, 50, 52, 97, 59, 50, 52, 33, + 146, 75, 59, 50, 52, 76, 69, -105, 59, 50, + 52, 171, 111, 120, 59, 50, 52, 117, 69, 32, + 84, 45, 38, 45, 69, 64, 67, 45, 54, 70, + 69, 64, 108, 45, 146, 70, 137, 64, 67, 45, + 150, 70, 121, 64, 67, 45, 39, 70, 178, 64, + 67, 50, 52, 70, 59, 50, 52, 50, 52, 122, + 59, 50, 52, 48, 134, 135, 59, 50, 52, 75, + 69, 99, 59, 50, 52, -110, 69, 124, 59, 50, + 52, 45, 69, 120, 143, 45, 54, 45, 69, 64, + 67, 45, 54, 70, 69, 64, 67, 45, 126, 70, + -107, 64, 67, 45, -108, 70, 139, 64, 67, 45, + 183, 70, 87, 64, 67, 138, 99, 70, 59, 50, + 52, -105, 173, 45, 59, 50, 52, 177, 54, 205, + 59, 50, 52, 119, 69, 134, 135, 50, 52, 197, + 69, 208, 173, 50, 52, 179, 111, 177, 180, 45, + 41, 215, 111, 64, 67, 45, 198, 70, 111, 64, + 67, 45, 41, 70, 214, 64, 163, 45, 124, 70, + 124, 64, 163, 45, -106, 70, 210, 64, 163, 50, + 52, 70, 35, 209, 42, 50, 52, 43, 202, 34, + 35, 120, 50, 52, 111, 164, 42, 34, 99, 43, + 111, 50, 52, 50, 52, 50, 52, 111, 150, 45, + 50, 52, 147, 64, 163, 45, 111, 70, 111, 64, + 163, -107, 45, 70, 152, 111, 64, 163, 154, 155, + 70, 45, -108, 45, 211, 64, 163, 64, 163, 70, + 45, 70, 212, 87, 64, 163, 87, 87, 70, 48, + 50, 52, 213, 56, 45, 219, 140, 45, 45, 54, + 134, 135, 54, 54, -105, 224, 17, 154, 155, 46, + 216, 84, 206, 124, 76, -106, 14, -109, 20, 23, + 45, 1, 5, 17, 9, 54, 12, -108, 16, 217, + 24, -107, -110, 14, 56, 20, 23, 150, 1, 5, + 17, 9, 82, 12, -106, 16, 220, 24, 225, 227, + 14, 81, 20, 23, 145, 1, 5, 17, 9, 127, + 12, 124, 16, 232, 24, 77, 141, 14, 234, 20, + 23, 131, 1, 5, 17, 9, 132, 12, 30, 16, + 235, 24, 151, nil, 14, nil, 20, 23, nil, 1, + 5, 17, 9, nil, 12, nil, 16, 193, 24, nil, + nil, 14, nil, 20, 23, nil, 1, 5, 17, 9, + nil, 12, nil, 16, 238, 24, nil, nil, 14, nil, + 20, 23, nil, 1, 5, 17, 9, nil, 12, nil, + 16, 181, 24, nil, nil, 14, nil, 20, 23, nil, + 1, 5, 17, 9, nil, 12, nil, 16, 239, 24, + nil, nil, 14, nil, 20, 23, nil, 1, 5, 17, + 9, nil, 12, nil, 16, nil, 24, nil, nil, 14, + nil, 20, 23, nil, 1, 5, 17, 9, nil, 12, + nil, 16, nil, 24, nil, nil, 14, nil, 20, 23, + nil, 1, 5, 17, 9, nil, 12, nil, 16, nil, + 24, nil, nil, 14, nil, 20, 23, nil, 1, 5, + 17, 9, nil, 12, nil, 16, nil, 24, nil, nil, + 14, nil, 20, 23, nil, 1, 5, 17, 9, nil, + 12, nil, 16, nil, 24, nil, nil, 14, nil, 20, + 23, nil, 1, 5, nil, 9, nil, 12, nil, 16, + nil, 24 ] racc_action_check = [ - 48, 48, 48, 48, 133, 21, 86, 86, 86, 17, - 39, 133, 137, 137, 137, 57, 48, 16, 16, 30, - 79, 79, 86, 21, 98, 170, 45, 45, 137, 48, - 170, 99, 57, 48, 48, 86, 3, 48, 75, 86, - 86, 137, 30, 86, 17, 137, 137, 100, 79, 137, - 69, 69, 69, 79, 45, 105, 119, 119, 119, 45, - 105, 101, 172, 172, 172, 93, 69, 103, 3, 121, - 75, 3, 119, 75, 92, 80, 93, 141, 172, 69, - 121, 107, 65, 69, 69, 119, 80, 69, 141, 119, - 119, 172, 111, 119, 112, 172, 172, 85, 85, 172, - 12, 12, 12, 9, 9, 83, 14, 14, 14, 83, - 83, 62, 113, 113, 113, 156, 12, 156, 156, 61, - 9, 60, 14, 90, 122, 85, 59, 128, 113, 12, - 85, 9, 66, 12, 12, 14, 9, 12, 68, 14, - 14, 113, 135, 14, 136, 113, 113, 53, 52, 113, - 173, 173, 173, 51, 140, 70, 158, 158, 158, 142, - 144, 50, 64, 64, 64, 46, 173, 153, 72, 95, - 161, 35, 158, 131, 131, 164, 28, 131, 64, 173, - 74, 26, 24, 173, 173, 158, 77, 173, 131, 158, - 158, 64, 178, 158, 19, 64, 64, 88, 88, 64, - 76, 131, 6, 2, 138, 131, 131, 87, 87, 131, - 87, nil, 88, 125, 125, nil, nil, nil, nil, 162, - 162, nil, 87, nil, nil, 88, nil, nil, 125, 88, - 88, nil, nil, 88, 162, 87, nil, nil, nil, 87, - 87, 125, nil, 87, nil, 125, 125, 162, nil, 125, - nil, 162, 162, 180, 180, 162, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 180, 147, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - 147, 180, nil, nil, 139, 180, 180, nil, 147, 180, - 147, 147, nil, 147, 147, 139, 147, nil, 147, 146, - 147, nil, 147, 139, nil, 139, 139, nil, 139, 139, - 146, 139, nil, 139, 194, 139, nil, 139, 146, nil, - 146, 146, nil, 146, 146, 194, 146, nil, 146, 120, - 146, nil, 146, 194, nil, 194, 194, nil, 194, 194, - 120, 194, nil, 194, 167, 194, nil, 194, 120, nil, - 120, 120, nil, 120, 120, 167, 120, nil, 120, 174, - 120, nil, 120, 167, nil, 167, 167, nil, 167, 167, - 174, 167, nil, 167, 192, 167, nil, 167, 174, nil, - 174, 174, nil, 174, 174, 192, 174, nil, 174, 109, - 174, nil, 174, 192, nil, 192, 192, nil, 192, 192, - 109, 192, nil, 192, 182, 192, nil, 192, 109, nil, - 109, 109, nil, 109, 109, 182, 109, nil, 109, nil, - 109, nil, 109, 182, nil, 182, 182, nil, 182, 182, - 84, 182, nil, 182, nil, 182, nil, 182, 84, nil, - 84, 84, nil, 84, 84, 190, 84, nil, 84, nil, - 84, nil, 84, 190, nil, 190, 190, nil, 190, 190, - 5, 190, nil, 190, nil, 190, nil, 190, 5, nil, - 5, 5, nil, 5, 5, 0, 5, nil, 5, nil, - 5, nil, 5, 0, nil, 0, 0, nil, 0, 0, - nil, 0, nil, 0, nil, 0, nil, 0 ] + 75, 75, 75, 46, 46, 44, 56, 56, 56, 2, + 108, 17, 153, 153, 153, 17, 75, 187, 146, 146, + 146, 123, 56, 108, 59, 59, 59, 59, 153, 2, + 44, 75, 6, 46, 146, 75, 75, 56, 46, 75, + 59, 56, 56, 153, 124, 56, 89, 153, 153, 146, + 125, 153, 72, 146, 146, 59, 6, 146, 128, 59, + 59, 99, 99, 59, 30, 30, 30, 120, 120, 74, + 48, 48, 48, 67, 89, 89, 14, 14, 14, 69, + 30, 98, 16, 16, 16, 68, 48, 76, 202, 202, + 202, 99, 14, 67, 98, 30, 99, 120, 16, 30, + 30, 48, 120, 30, 202, 48, 48, 14, 77, 48, + 66, 14, 14, 16, 65, 14, 93, 16, 16, 202, + 133, 16, 136, 202, 202, 93, 165, 202, 212, 212, + 212, 63, 215, 136, 216, 216, 216, 215, 136, 165, + 147, 147, 147, 61, 212, 133, 133, 167, 167, 148, + 216, 167, 127, 210, 210, 130, 147, 127, 130, 212, + 97, 175, 167, 212, 212, 216, 149, 212, 210, 216, + 216, 147, 9, 216, 175, 147, 147, 167, 150, 147, + 151, 167, 167, 210, 57, 167, 169, 210, 210, 223, + 223, 210, 23, 169, 97, 122, 122, 97, 158, 23, + 5, 163, 119, 119, 223, 119, 9, 5, 47, 9, + 122, 139, 139, 138, 138, 20, 20, 119, 113, 223, + 164, 164, 109, 223, 223, 122, 139, 223, 138, 122, + 122, 107, 119, 122, 118, 164, 119, 119, 118, 118, + 119, 139, 106, 138, 172, 139, 139, 138, 138, 139, + 164, 138, 173, 87, 164, 164, 39, 38, 164, 12, + 12, 12, 174, 12, 87, 182, 95, 39, 38, 87, + 95, 95, 39, 38, 104, 204, 182, 204, 204, 12, + 177, 36, 166, 180, 33, 185, 182, 71, 182, 182, + 12, 182, 182, 166, 182, 12, 182, 188, 182, 178, + 182, 189, 190, 166, 32, 166, 166, 200, 166, 166, + 178, 166, 27, 166, 101, 166, 194, 166, 206, 209, + 178, 24, 178, 178, 100, 178, 178, 194, 178, 80, + 178, 82, 178, 218, 178, 19, 96, 194, 225, 194, + 194, 84, 194, 194, 218, 194, 86, 194, 1, 194, + 227, 194, 115, nil, 218, nil, 218, 218, nil, 218, + 218, 227, 218, nil, 218, nil, 218, 141, 218, nil, + nil, 227, nil, 227, 227, nil, 227, 227, 141, 227, + nil, 227, nil, 227, 236, 227, nil, nil, 141, nil, + 141, 141, nil, 141, 141, 236, 141, nil, 141, nil, + 141, 132, 141, nil, nil, 236, nil, 236, 236, nil, + 236, 236, 132, 236, nil, 236, nil, 236, 237, 236, + nil, nil, 132, nil, 132, 132, nil, 132, 132, 237, + 132, nil, 132, nil, 132, nil, 132, nil, nil, 237, + nil, 237, 237, nil, 237, 237, 22, 237, nil, 237, + nil, 237, nil, 237, nil, nil, 22, nil, 22, 22, + nil, 22, 22, 234, 22, nil, 22, nil, 22, nil, + 22, nil, nil, 234, nil, 234, 234, nil, 234, 234, + 121, 234, nil, 234, nil, 234, nil, 234, nil, nil, + 121, nil, 121, 121, nil, 121, 121, 0, 121, nil, + 121, nil, 121, nil, 121, nil, nil, 0, nil, 0, + 0, nil, 0, 0, nil, 0, nil, 0, nil, 0, + nil, 0 ] racc_action_pointer = [ - 457, nil, 203, 32, nil, 442, 196, nil, nil, 100, - nil, nil, 98, nil, 104, nil, 14, 3, nil, 158, - nil, -13, nil, nil, 169, nil, 145, nil, 176, nil, - 9, nil, nil, nil, nil, 135, nil, nil, nil, 0, - nil, nil, nil, nil, nil, 23, 146, nil, -2, nil, - 142, 134, 142, 127, nil, nil, nil, 13, nil, 107, - 102, 100, 105, nil, 160, 40, 112, nil, 132, 48, - 122, nil, 132, nil, 174, 34, 190, 177, nil, 17, - 65, nil, nil, 100, 412, 94, 4, 204, 194, nil, - 113, nil, 55, 56, nil, 152, nil, nil, 5, 12, - 28, 24, nil, 61, nil, 24, nil, 75, nil, 382, - nil, 85, 58, 110, nil, nil, nil, nil, nil, 54, - 322, 59, 119, nil, nil, 210, nil, nil, 118, nil, - nil, 170, nil, -6, nil, 135, 108, 10, 197, 277, - 133, 67, 146, nil, 147, nil, 292, 262, nil, nil, - nil, nil, nil, 140, nil, nil, 108, nil, 154, nil, - nil, 164, 216, nil, 165, nil, nil, 337, nil, nil, - -6, nil, 60, 148, 352, nil, nil, nil, 186, nil, - 250, nil, 397, nil, nil, nil, nil, nil, nil, nil, - 427, nil, 367, nil, 307, nil, nil ] + 479, 335, -9, nil, nil, 162, 13, nil, nil, 168, + nil, nil, 257, nil, 74, nil, 80, 9, nil, 335, + 212, nil, 428, 154, 279, nil, nil, 306, nil, nil, + 62, nil, 298, 278, nil, nil, 246, nil, 235, 234, + nil, nil, nil, nil, -5, nil, 0, 198, 68, nil, + nil, nil, nil, nil, nil, nil, 4, 163, nil, 22, + nil, 122, nil, 110, nil, 93, 89, 71, 64, 77, + nil, 266, 46, nil, 63, -2, 49, 108, nil, nil, + 307, nil, 293, nil, 303, nil, 340, 231, nil, 26, + nil, nil, nil, 101, nil, 222, 330, 156, 71, 58, + 319, 293, nil, nil, 253, nil, 221, 210, 1, 183, + nil, nil, nil, 208, nil, 335, nil, nil, 229, 199, + 64, 462, 192, 16, 35, 40, nil, 119, 52, nil, + 148, nil, 394, 97, nil, nil, 100, nil, 210, 208, + nil, 360, nil, nil, nil, nil, 16, 138, 142, 159, + 140, 142, nil, 10, nil, nil, nil, nil, 189, nil, + nil, nil, nil, 179, 217, 116, 275, 144, nil, 176, + nil, nil, 237, 239, 239, 151, nil, 267, 292, nil, + 245, nil, 258, nil, nil, 264, nil, -4, 276, 280, + 281, nil, nil, nil, 309, nil, nil, nil, nil, nil, + 297, nil, 86, nil, 268, nil, 289, nil, nil, 313, + 150, nil, 126, nil, nil, 99, 132, nil, 326, nil, + nil, nil, nil, 186, nil, 332, nil, 343, nil, nil, + nil, nil, nil, nil, 445, nil, 377, 411, nil, nil ] racc_action_default = [ - -98, -12, -118, -118, -13, -1, -118, -14, -2, -33, - -15, -3, -118, -5, -118, -6, -118, -118, -7, -118, - -34, -118, -8, -9, -118, -10, -118, -11, -118, -95, - -98, -96, -93, -97, -4, -42, -58, -33, -59, -17, - -18, -20, -21, -22, -110, -118, -51, -55, -118, -60, - -56, -50, -118, -33, -52, -85, -54, -49, -65, -53, - -118, -48, -118, -86, -42, -118, -98, -26, -118, -118, - -98, 197, -118, -108, -118, -118, -116, -118, -43, -118, - -118, -113, -46, -118, -118, -118, -118, -118, -118, -84, - -116, -83, -37, -33, -29, -118, -38, -40, -36, -39, - -35, -31, -27, -118, -99, -98, -41, -118, -109, -118, - -94, -118, -117, -118, -19, -16, -112, -114, -115, -118, - -118, -118, -118, -80, -79, -118, -82, -81, -118, -73, - -74, -118, -67, -118, -71, -118, -42, -32, -118, -118, - -118, -118, -105, -102, -107, -111, -118, -118, -92, -25, - -44, -45, -47, -63, -57, -61, -118, -76, -118, -68, - -66, -118, -118, -24, -116, -30, -23, -118, -88, -100, - -118, -101, -118, -118, -118, -90, -91, -62, -118, -75, - -118, -78, -118, -72, -28, -87, -103, -104, -106, -89, - -118, -77, -118, -70, -118, -69, -64 ] + -126, -146, -146, -16, -5, -146, -146, -6, -7, -146, + -8, -9, -33, -10, -146, -11, -146, -32, -12, -146, + -146, -13, -1, -146, -29, -14, -2, -146, -15, -3, + -146, -28, -146, -146, -120, -119, -126, -31, -126, -126, + -121, -124, -123, -125, -126, -139, -146, -18, -146, -19, + -83, -21, -84, -22, -52, -23, -61, -75, -77, -146, + -79, -146, -91, -74, -85, -78, -73, -52, -76, -53, + -111, -81, -146, -80, -146, -146, -61, -146, -112, -4, + -126, -30, -61, -60, -146, -137, -146, -126, -46, -146, + -45, -37, -39, -146, -36, -146, -146, -146, -146, -146, + -146, -56, -57, -59, -55, -62, -58, -54, -52, -50, + -110, -53, -109, -144, -48, -146, -65, -141, -146, -146, + -146, -146, -146, -146, -146, -144, 240, -127, -146, -128, + -146, -138, -146, -146, -42, -41, -146, -35, -146, -146, + -34, -146, -122, -17, -20, -86, -146, -51, -146, -146, + -145, -61, -140, -146, -142, -143, -106, -105, -146, -108, + -99, -107, -100, -52, -146, -146, -146, -146, -93, -146, + -97, -87, -146, -136, -146, -146, -131, -134, -146, -27, + -146, -116, -146, -40, -38, -69, -71, -68, -72, -67, + -70, -43, -44, -118, -146, -64, -49, -24, -25, -63, + -144, -66, -146, -102, -146, -82, -89, -94, -92, -146, + -146, -26, -146, -129, -130, -146, -146, -114, -146, -115, + -117, -47, -104, -146, -101, -146, -88, -146, -98, -135, + -132, -133, -113, -103, -146, -96, -146, -146, -95, -90 ] racc_goto_table = [ - 5, 76, 34, 43, 32, 8, 58, 42, 62, 111, - 63, 132, 39, 143, 150, 51, 52, 51, 130, 94, - 74, 128, 134, 135, 177, 96, 89, 119, 97, 131, - 90, 83, 138, 129, 101, 156, 103, 114, 30, 43, - 67, 141, 82, 42, 2, nil, nil, nil, 80, 89, - 89, 51, nil, nil, 159, nil, 157, nil, 100, 128, - 107, nil, 99, 106, nil, 134, nil, 92, nil, nil, - nil, 104, 51, 43, nil, nil, 110, 42, 186, 43, - 122, 127, 127, 42, 120, 126, 126, 89, 121, 51, - 123, 123, 165, 89, nil, nil, 183, 184, 96, 89, - 180, 97, 164, nil, nil, nil, nil, 151, nil, 147, - 140, 191, nil, 152, 128, nil, 51, 34, nil, 127, - nil, nil, 51, 126, 89, 127, nil, nil, 123, 126, - nil, 100, nil, 41, 123, 99, 57, nil, 57, 167, - 92, nil, 89, nil, 34, 68, 174, nil, nil, nil, - nil, nil, 181, nil, nil, nil, 127, nil, nil, nil, - 126, 51, nil, nil, 34, 123, 187, 188, nil, 41, - nil, 34, 57, 91, 127, 51, 51, nil, 126, nil, - nil, nil, 192, 123, nil, nil, nil, nil, 98, 34, - 194, 34, nil, 57, nil, nil, 91, 91, nil, nil, - nil, nil, nil, 41, nil, nil, nil, nil, nil, 41, - 57, 124, 124, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, 142, - nil, nil, nil, nil, 91, nil, nil, 57, nil, nil, - 91, nil, nil, 57, nil, nil, 91, nil, nil, 124, - nil, nil, nil, nil, nil, 124, nil, nil, nil, nil, - nil, 98, nil, nil, nil, nil, nil, nil, nil, nil, - nil, 91, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, 57, nil, nil, nil, 124, nil, nil, 91, - nil, nil, nil, nil, 142, nil, 57, 57, nil, nil, - nil, nil, nil, nil, 124 ] + 22, 79, 51, 162, 102, 47, 113, 55, 62, 40, + 74, 114, 103, 153, 158, 78, 176, 170, 109, 149, + 168, 89, 95, 72, 83, 199, 125, 36, 88, 88, + 86, 172, 130, 191, 192, 226, 51, 167, 96, 98, + 26, 55, 100, 148, 144, 80, 104, 73, 203, 73, + 160, 107, 31, 116, 204, 199, 53, 118, 128, 158, + 37, 44, 170, 73, 184, 207, 174, 175, 19, 123, + 133, nil, nil, nil, nil, nil, 85, 88, nil, nil, + nil, 73, 90, 90, 85, nil, 186, 186, nil, 51, + 53, nil, 73, nil, 55, 102, nil, 142, nil, 223, + 106, 200, 196, 103, 230, 228, 221, 233, 73, 157, + 51, 112, 157, 165, 161, 55, nil, 161, 158, nil, + 129, 166, nil, nil, nil, nil, 88, nil, 187, 187, + nil, 90, 182, 189, 189, nil, nil, 104, nil, nil, + 195, 194, 107, 53, nil, 79, nil, 201, nil, nil, + nil, nil, nil, nil, 157, nil, nil, 157, nil, 161, + 110, 79, 161, 159, 53, 101, 159, nil, nil, nil, + nil, nil, nil, 79, 112, nil, nil, 112, 218, 73, + 90, nil, 188, 188, nil, nil, 73, nil, nil, nil, + nil, 106, nil, 112, 112, nil, 222, 79, nil, nil, + 157, nil, 112, nil, nil, 161, 229, nil, 159, nil, + 231, 159, nil, 157, nil, 79, 79, nil, 161, 112, + nil, nil, 112, 110, nil, nil, 110, 236, 156, nil, + nil, 156, nil, nil, 237, 73, nil, nil, nil, nil, + nil, nil, 190, 190, nil, 73, nil, 185, 185, 73, + nil, 110, nil, nil, 159, nil, 101, nil, nil, nil, + nil, nil, nil, nil, nil, 112, nil, 159, 110, nil, + nil, 110, nil, 156, nil, nil, 156, nil, 112, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, 110, nil, nil, nil, nil, 156, + nil, nil, nil, nil, nil, nil, nil, 110, nil, nil, + nil, nil, 156 ] racc_goto_check = [ - 2, 23, 4, 20, 49, 3, 30, 19, 30, 24, - 20, 40, 16, 51, 31, 25, 37, 25, 44, 26, - 47, 42, 42, 24, 38, 28, 34, 33, 29, 39, - 23, 32, 22, 43, 21, 45, 46, 17, 48, 20, - 5, 50, 30, 19, 1, nil, nil, nil, 16, 34, - 34, 25, nil, nil, 40, nil, 44, nil, 20, 42, - 47, nil, 19, 30, nil, 42, nil, 25, nil, nil, - nil, 3, 25, 20, nil, nil, 49, 19, 51, 20, - 30, 20, 20, 19, 2, 19, 19, 34, 16, 25, - 25, 25, 26, 34, nil, nil, 42, 24, 28, 34, - 33, 29, 23, nil, nil, nil, nil, 30, nil, 2, - 3, 44, nil, 30, 42, nil, 25, 4, nil, 20, - nil, nil, 25, 19, 34, 20, nil, nil, 25, 19, - nil, 20, nil, 18, 25, 19, 18, nil, 18, 2, - 25, nil, 34, nil, 4, 18, 2, nil, nil, nil, - nil, nil, 30, nil, nil, nil, 20, nil, nil, nil, - 19, 25, nil, nil, 4, 25, 30, 30, nil, 18, - nil, 4, 18, 36, 20, 25, 25, nil, 19, nil, - nil, nil, 2, 25, nil, nil, nil, nil, 18, 4, - 2, 4, nil, 18, nil, nil, 36, 36, nil, nil, - nil, nil, nil, 18, nil, nil, nil, nil, nil, 18, - 18, 18, 18, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, 18, - nil, nil, nil, nil, 36, nil, nil, 18, nil, nil, - 36, nil, nil, 18, nil, nil, 36, nil, nil, 18, - nil, nil, nil, nil, nil, 18, nil, nil, nil, nil, - nil, 18, nil, nil, nil, nil, nil, nil, nil, nil, - nil, 36, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, 18, nil, nil, nil, 18, nil, nil, 36, - nil, nil, nil, nil, 18, nil, 18, 18, nil, nil, - nil, nil, nil, nil, 18 ] + 2, 4, 19, 54, 39, 17, 24, 21, 41, 60, + 41, 36, 40, 44, 52, 21, 63, 52, 22, 25, + 50, 30, 30, 47, 41, 42, 24, 56, 19, 19, + 58, 25, 24, 35, 35, 48, 19, 49, 58, 17, + 3, 21, 41, 23, 18, 56, 19, 26, 54, 26, + 53, 21, 5, 41, 55, 42, 20, 43, 57, 52, + 29, 59, 52, 26, 31, 50, 61, 62, 1, 41, + 30, nil, nil, nil, nil, nil, 3, 19, nil, nil, + nil, 26, 20, 20, 3, nil, 39, 39, nil, 19, + 20, nil, 26, nil, 21, 39, nil, 60, nil, 44, + 20, 24, 36, 40, 63, 52, 25, 54, 26, 19, + 19, 46, 19, 17, 21, 21, nil, 21, 52, nil, + 3, 2, nil, nil, nil, nil, 19, nil, 19, 19, + nil, 20, 2, 21, 21, nil, nil, 19, nil, nil, + 41, 2, 21, 20, nil, 4, nil, 41, nil, nil, + nil, nil, nil, nil, 19, nil, nil, 19, nil, 21, + 45, 4, 21, 20, 20, 38, 20, nil, nil, nil, + nil, nil, nil, 4, 46, nil, nil, 46, 2, 26, + 20, nil, 20, 20, nil, nil, 26, nil, nil, nil, + nil, 20, nil, 46, 46, nil, 41, 4, nil, nil, + 19, nil, 46, nil, nil, 21, 41, nil, 20, nil, + 41, 20, nil, 19, nil, 4, 4, nil, 21, 46, + nil, nil, 46, 45, nil, nil, 45, 2, 38, nil, + nil, 38, nil, nil, 2, 26, nil, nil, nil, nil, + nil, nil, 45, 45, nil, 26, nil, 38, 38, 26, + nil, 45, nil, nil, 20, nil, 38, nil, nil, nil, + nil, nil, nil, nil, nil, 46, nil, 20, 45, nil, + nil, 45, nil, 38, nil, nil, 38, nil, 46, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, 45, nil, nil, nil, nil, 38, + nil, nil, nil, nil, nil, nil, nil, 45, nil, nil, + nil, nil, 38 ] racc_goto_pointer = [ - nil, 44, 0, 5, -3, 19, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, 3, -42, 124, -2, - -6, -30, -69, -34, -67, 3, -45, nil, -39, -36, - -6, -98, -17, -56, -38, nil, 109, 4, -129, -59, - -77, nil, -66, -54, -69, -90, -30, -10, 35, 1, - -64, -92, nil ] + nil, 68, 0, 40, -21, 50, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, -7, -55, -10, + 44, -5, -38, -66, -50, -94, 33, nil, nil, 54, + -17, -72, nil, nil, nil, -105, -45, nil, 109, -52, + -44, -6, -125, -2, -105, 104, 55, 9, -171, -85, + -102, nil, -105, -69, -116, -110, 22, -22, -6, 52, + 0, -61, -60, -111 ] racc_goto_default = [ - nil, nil, nil, 73, 11, 13, 15, 18, 22, 23, - 25, 27, 1, 4, 7, 10, nil, 40, 17, 59, - 61, nil, nil, nil, nil, 6, nil, 95, 54, 56, - nil, 78, nil, nil, 46, 47, 50, nil, nil, nil, - nil, 133, 60, nil, nil, nil, nil, nil, nil, nil, - nil, nil, 144 ] + nil, nil, nil, 94, 29, 4, 7, 8, 10, 11, + 13, 15, 18, 21, 25, 28, 3, nil, 49, 63, + 65, 66, nil, nil, nil, nil, 27, 2, 6, nil, + nil, 91, 136, 92, 93, nil, nil, 115, 57, 58, + 60, nil, 105, nil, nil, 68, 71, nil, nil, nil, + nil, 169, 61, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil ] racc_token_table = { false => 0, Object.new => 1, :LBRACK => 2, :DQTEXT => 3, :SQTEXT => 4, :RBRACK => 5, :LBRACE => 6, :RBRACE => 7, :SYMBOL => 8, :FARROW => 9, :COMMA => 10, :TRUE => 11, :FALSE => 12, :EQUALS => 13, :LESSEQUAL => 14, :NOTEQUAL => 15, :DOT => 16, :COLON => 17, :TYPE => 18, - :QMARK => 19, - :LPAREN => 20, - :RPAREN => 21, - :ISEQUAL => 22, - :GREATEREQUAL => 23, - :GREATERTHAN => 24, - :LESSTHAN => 25, - :IF => 26, - :ELSE => 27, - :IMPORT => 28, - :DEFINE => 29, - :ELSIF => 30, - :VARIABLE => 31, - :CLASS => 32, - :INHERITS => 33, - :NODE => 34, - :BOOLEAN => 35, - :NAME => 36, - :SEMIC => 37, - :CASE => 38, - :DEFAULT => 39, - :AT => 40, - :LCOLLECT => 41, - :RCOLLECT => 42 } + :LLCOLLECT => 19, + :RRCOLLECT => 20, + :QMARK => 21, + :LPAREN => 22, + :RPAREN => 23, + :ISEQUAL => 24, + :GREATEREQUAL => 25, + :GREATERTHAN => 26, + :LESSTHAN => 27, + :IF => 28, + :ELSE => 29, + :IMPORT => 30, + :DEFINE => 31, + :ELSIF => 32, + :VARIABLE => 33, + :CLASS => 34, + :INHERITS => 35, + :NODE => 36, + :BOOLEAN => 37, + :NAME => 38, + :SEMIC => 39, + :CASE => 40, + :DEFAULT => 41, + :AT => 42, + :LCOLLECT => 43, + :RCOLLECT => 44, + :CLASSNAME => 45, + :CLASSREF => 46, + :NOT => 47, + :OR => 48, + :AND => 49 } racc_use_result_var = true -racc_nt_base = 43 +racc_nt_base = 50 Racc_arg = [ racc_action_table, racc_action_check, racc_action_default, racc_action_pointer, racc_goto_table, racc_goto_check, racc_goto_default, racc_goto_pointer, racc_nt_base, racc_reduce_table, racc_token_table, racc_shift_n, racc_reduce_n, racc_use_result_var ] Racc_token_to_s_table = [ '$end', 'error', 'LBRACK', 'DQTEXT', 'SQTEXT', 'RBRACK', 'LBRACE', 'RBRACE', 'SYMBOL', 'FARROW', 'COMMA', 'TRUE', 'FALSE', 'EQUALS', 'LESSEQUAL', 'NOTEQUAL', 'DOT', 'COLON', 'TYPE', +'LLCOLLECT', +'RRCOLLECT', 'QMARK', 'LPAREN', 'RPAREN', 'ISEQUAL', 'GREATEREQUAL', 'GREATERTHAN', 'LESSTHAN', 'IF', 'ELSE', 'IMPORT', 'DEFINE', 'ELSIF', 'VARIABLE', 'CLASS', 'INHERITS', 'NODE', 'BOOLEAN', 'NAME', 'SEMIC', 'CASE', 'DEFAULT', 'AT', 'LCOLLECT', 'RCOLLECT', +'CLASSNAME', +'CLASSREF', +'NOT', +'OR', +'AND', '$start', 'program', 'statements', -'nothing', +'nil', 'statement', -'object', -'collectable', +'resource', +'virtualresource', 'collection', 'assignment', 'casestatement', 'ifstatement', 'import', 'fstatement', 'definition', 'hostclass', 'nodedef', -'classnames', -'classname', +'resourceoverride', +'namestrings', +'namestring', 'name', 'variable', 'quotedtext', -'objectinstances', +'resourceinstances', 'endsemi', 'params', 'endcomma', +'resourceref', +'at', +'collectname', +'collectrhand', +'collstatements', +'collstatement', +'colljoin', +'collexpr', +'colllval', +'simplervalue', +'resourceinst', +'resourcename', 'type', -'objectinst', -'objectname', 'selector', 'array', 'rvalue', 'param', 'rvalues', 'comma', 'boolean', -'objectref', 'funcrvalue', 'iftest', 'else', 'caseopts', 'caseopt', 'casevalues', 'selectlhand', 'svalues', 'selectval', 'sintvalues', +'fqname', 'argumentlist', 'parent', 'hostnames', 'hostname', +'nothing', 'arguments', -'argument', -'lvariable'] +'argument'] Racc_debug_parser = false ##### racc system variables end ##### # reduce 0 omitted -module_eval <<'.,.,', 'grammar.ra', 24 +module_eval <<'.,.,', 'grammar.ra', 33 def _reduce_1( val, _values, result ) - # Make sure we always return an array. - if val[0].is_a?(AST::ASTArray) - result = val[0] + if val[0] + # Make sure we always return an array. + if val[0].is_a?(AST::ASTArray) + if val[0].children.empty? + result = nil + else + result = val[0] + end + else + result = aryfy(val[0]) + end else - result = aryfy(val[0]) + result = nil end result end .,., # reduce 2 omitted # reduce 3 omitted -module_eval <<'.,.,', 'grammar.ra', 35 +module_eval <<'.,.,', 'grammar.ra', 49 def _reduce_4( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - val[0].push(val[1]) - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0],val[1]] + if val[0] and val[1] + if val[0].instance_of?(AST::ASTArray) + val[0].push(val[1]) + result = val[0] + else + result = ast AST::ASTArray, :children => [val[0],val[1]] + end + elsif obj = (val[0] || val[1]) + result = obj + else result = nil end result end .,., # reduce 5 omitted # reduce 6 omitted # reduce 7 omitted # reduce 8 omitted # reduce 9 omitted # reduce 10 omitted # reduce 11 omitted # reduce 12 omitted # reduce 13 omitted # reduce 14 omitted # reduce 15 omitted -module_eval <<'.,.,', 'grammar.ra', 56 - def _reduce_16( val, _values, result ) + # reduce 16 omitted + +module_eval <<'.,.,', 'grammar.ra', 71 + def _reduce_17( val, _values, result ) args = aryfy(val[2]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement result end .,., -module_eval <<'.,.,', 'grammar.ra', 63 - def _reduce_17( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 78 + def _reduce_18( val, _values, result ) args = aryfy(val[1]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :statement result end .,., - # reduce 18 omitted + # reduce 19 omitted -module_eval <<'.,.,', 'grammar.ra', 82 - def _reduce_19( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 85 + def _reduce_20( val, _values, result ) result = aryfy(val[0], val[2]) result.line = @lexer.line result.file = @lexer.file result end .,., - # reduce 20 omitted - # reduce 21 omitted # reduce 22 omitted -module_eval <<'.,.,', 'grammar.ra', 111 - def _reduce_23( val, _values, result ) + # reduce 23 omitted + +module_eval <<'.,.,', 'grammar.ra', 113 + def _reduce_24( val, _values, result ) if val[0].instance_of?(AST::ASTArray) - raise Puppet::ParseError, "Invalid name" + error "Invalid name" end array = val[2] - if array.instance_of?(AST::ObjectInst) + if array.instance_of?(AST::ResourceInst) array = [array] end result = ast AST::ASTArray - # this iterates across each specified objectinstance + # this iterates across each specified resourceinstance array.each { |instance| - unless instance.instance_of?(AST::ObjectInst) + unless instance.instance_of?(AST::ResourceInst) raise Puppet::Dev, "Got something that isn't an instance" end # now, i need to somehow differentiate between those things with # arrays in their names, and normal things - result.push ast(AST::ObjectDef, + result.push ast(AST::ResourceDef, :type => val[0], - :name => instance[0], + :title => instance[0], :params => instance[1]) } result end .,., -module_eval <<'.,.,', 'grammar.ra', 120 - def _reduce_24( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - Puppet.notice "invalid name" - raise Puppet::ParseError, "Invalid name" - end - # an object but without a name - # this cannot be an instance of a library type - result = ast AST::ObjectDef, :type => val[0], :params => val[2] +module_eval <<'.,.,', 'grammar.ra', 116 + def _reduce_25( val, _values, result ) + # This is a deprecated syntax. + error "All resource specifications require names" result end .,., -module_eval <<'.,.,', 'grammar.ra', 126 - def _reduce_25( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 122 + def _reduce_26( val, _values, result ) # a template setting for a type if val[0].instance_of?(AST::ASTArray) - raise Puppet::ParseError, "Invalid type" + error "Invalid type" end - result = ast(AST::TypeDefaults, :type => val[0], :params => val[2]) + result = ast(AST::ResourceDefaults, :type => val[0], :params => val[2]) result end .,., -module_eval <<'.,.,', 'grammar.ra', 149 - def _reduce_26( val, _values, result ) - unless Puppet[:storeconfigs] - raise Puppet::ParseError, "You cannot collect without storeconfigs being set" +module_eval <<'.,.,', 'grammar.ra', 127 + def _reduce_27( val, _values, result ) + result = ast AST::ResourceOverride, :object => val[0], :params => val[2] + result + end +.,., + +module_eval <<'.,.,', 'grammar.ra', 154 + def _reduce_28( val, _values, result ) + type = val[0] + + if type == :exported and ! Puppet[:storeconfigs] + error "You cannot collect without storeconfigs being set" end - if val[1].is_a? AST::TypeDefaults - raise Puppet::ParseError, "Defaults are not collectable" + if val[1].is_a? AST::ResourceDefaults + error "Defaults are not virtualizable" end - # Just mark our objects as collectable and pass them through. + method = type.to_s + "=" + + # Just mark our resources as exported and pass them through. if val[1].instance_of?(AST::ASTArray) val[1].each do |obj| - obj.collectable = true + obj.send(method, true) end else - val[1].collectable = true + val[1].send(method, true) end result = val[1] result end .,., -module_eval <<'.,.,', 'grammar.ra', 158 - def _reduce_27( val, _values, result ) - unless Puppet[:storeconfigs] - raise Puppet::ParseError, "You cannot collect without storeconfigs being set" +module_eval <<'.,.,', 'grammar.ra', 155 + def _reduce_29( val, _values, result ) + result = :virtual + result + end +.,., + +module_eval <<'.,.,', 'grammar.ra', 156 + def _reduce_30( val, _values, result ) + result = :exported + result + end +.,., + +module_eval <<'.,.,', 'grammar.ra', 179 + def _reduce_31( val, _values, result ) + if val[0] =~ /^[a-z]/ + Puppet.warning addcontext("Collection names must now be capitalized") + end + type = val[0].downcase + args = {:type => type} + + if val[1].is_a?(AST::CollExpr) + args[:query] = val[1] + args[:query].type = type + args[:form] = args[:query].form + else + args[:form] = val[1] + end + if args[:form] == :exported and ! Puppet[:storeconfigs] + error "You cannot collect exported resources without storeconfigs being set" end - result = ast AST::Collection, :type => val[0] + result = ast AST::Collection, args result end .,., -module_eval <<'.,.,', 'grammar.ra', 162 - def _reduce_28( val, _values, result ) - result = ast AST::ObjectInst, :children => [val[0],val[2]] + # reduce 32 omitted + + # reduce 33 omitted + +module_eval <<'.,.,', 'grammar.ra', 190 + def _reduce_34( val, _values, result ) + if val[1] + result = val[1] + result.form = :virtual + else + result = :virtual + end result end .,., - # reduce 29 omitted +module_eval <<'.,.,', 'grammar.ra', 198 + def _reduce_35( val, _values, result ) + if val[1] + result = val[1] + result.form = :exported + else + result = :exported + end + result + end +.,., -module_eval <<'.,.,', 'grammar.ra', 172 - def _reduce_30( val, _values, result ) - if val[0].instance_of?(AST::ObjectInst) + # reduce 36 omitted + + # reduce 37 omitted + +module_eval <<'.,.,', 'grammar.ra', 206 + def _reduce_38( val, _values, result ) + result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] + result + end +.,., + + # reduce 39 omitted + +module_eval <<'.,.,', 'grammar.ra', 212 + def _reduce_40( val, _values, result ) + result = val[1] + result.parens = true + result + end +.,., + + # reduce 41 omitted + + # reduce 42 omitted + +module_eval <<'.,.,', 'grammar.ra', 220 + def _reduce_43( val, _values, result ) + result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] + #result = ast AST::CollExpr + #result.push *val + result + end +.,., + +module_eval <<'.,.,', 'grammar.ra', 225 + def _reduce_44( val, _values, result ) + result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2] + #result = ast AST::CollExpr + #result.push *val + result + end +.,., + + # reduce 45 omitted + + # reduce 46 omitted + +module_eval <<'.,.,', 'grammar.ra', 232 + def _reduce_47( val, _values, result ) + result = ast AST::ResourceInst, :children => [val[0],val[2]] + result + end +.,., + + # reduce 48 omitted + +module_eval <<'.,.,', 'grammar.ra', 242 + def _reduce_49( val, _values, result ) + if val[0].instance_of?(AST::ResourceInst) result = ast AST::ASTArray, :children => [val[0],val[2]] else val[0].push val[2] result = val[0] end result end .,., - # reduce 31 omitted + # reduce 50 omitted - # reduce 32 omitted + # reduce 51 omitted -module_eval <<'.,.,', 'grammar.ra', 179 - def _reduce_33( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 249 + def _reduce_52( val, _values, result ) result = ast AST::Name, :value => val[0] result end .,., -module_eval <<'.,.,', 'grammar.ra', 183 - def _reduce_34( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 253 + def _reduce_53( val, _values, result ) result = ast AST::Type, :value => val[0] result end .,., - # reduce 35 omitted + # reduce 54 omitted - # reduce 36 omitted + # reduce 55 omitted - # reduce 37 omitted + # reduce 56 omitted - # reduce 38 omitted + # reduce 57 omitted - # reduce 39 omitted + # reduce 58 omitted - # reduce 40 omitted + # reduce 59 omitted -module_eval <<'.,.,', 'grammar.ra', 196 - def _reduce_41( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 266 + def _reduce_60( val, _values, result ) # this is distinct from referencing a variable - variable = ast AST::Name, :value => val[0].sub(/^\$/,'') + variable = ast AST::Name, :value => val[0] result = ast AST::VarDef, :name => variable, :value => val[2] result end .,., -module_eval <<'.,.,', 'grammar.ra', 201 - def _reduce_42( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 271 + def _reduce_61( val, _values, result ) result = ast AST::ASTArray result end .,., -module_eval <<'.,.,', 'grammar.ra', 201 - def _reduce_43( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 271 + def _reduce_62( val, _values, result ) result = val[0] result end .,., -module_eval <<'.,.,', 'grammar.ra', 210 - def _reduce_44( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 280 + def _reduce_63( val, _values, result ) if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end result end .,., -module_eval <<'.,.,', 'grammar.ra', 215 - def _reduce_45( val, _values, result ) - leaf = ast AST::String, :value => val[0] - result = ast AST::ObjectParam, :param => leaf, :value => val[2] +module_eval <<'.,.,', 'grammar.ra', 284 + def _reduce_64( val, _values, result ) + result = ast AST::ResourceParam, :param => val[0], :value => val[2] result end .,., - # reduce 46 omitted + # reduce 65 omitted -module_eval <<'.,.,', 'grammar.ra', 224 - def _reduce_47( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 293 + def _reduce_66( val, _values, result ) if val[0].instance_of?(AST::ASTArray) result = val[0].push(val[2]) else result = ast AST::ASTArray, :children => [val[0],val[2]] end result end .,., - # reduce 48 omitted + # reduce 67 omitted - # reduce 49 omitted + # reduce 68 omitted - # reduce 50 omitted + # reduce 69 omitted - # reduce 51 omitted + # reduce 70 omitted - # reduce 52 omitted + # reduce 71 omitted - # reduce 53 omitted + # reduce 72 omitted - # reduce 54 omitted + # reduce 73 omitted - # reduce 55 omitted + # reduce 74 omitted - # reduce 56 omitted + # reduce 75 omitted + + # reduce 76 omitted + + # reduce 77 omitted + + # reduce 78 omitted + + # reduce 79 omitted -module_eval <<'.,.,', 'grammar.ra', 243 - def _reduce_57( val, _values, result ) + # reduce 80 omitted + + # reduce 81 omitted + +module_eval <<'.,.,', 'grammar.ra', 319 + def _reduce_82( val, _values, result ) args = aryfy(val[2]) result = ast AST::Function, :name => val[0], :arguments => args, :ftype => :rvalue result end .,., -module_eval <<'.,.,', 'grammar.ra', 247 - def _reduce_58( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 323 + def _reduce_83( val, _values, result ) result = ast AST::String, :value => val[0] result end .,., -module_eval <<'.,.,', 'grammar.ra', 249 - def _reduce_59( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 325 + def _reduce_84( val, _values, result ) result = ast AST::FlatString, :value => val[0] result end .,., -module_eval <<'.,.,', 'grammar.ra', 253 - def _reduce_60( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 329 + def _reduce_85( val, _values, result ) result = ast AST::Boolean, :value => val[0] result end .,., -module_eval <<'.,.,', 'grammar.ra', 257 - def _reduce_61( val, _values, result ) - result = ast AST::ObjectRef, :type => val[0], :name => val[2] +module_eval <<'.,.,', 'grammar.ra', 334 + def _reduce_86( val, _values, result ) + Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized") + result = ast AST::ResourceRef, :type => val[0], :title => val[2] result end .,., -module_eval <<'.,.,', 'grammar.ra', 270 - def _reduce_62( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 336 + def _reduce_87( val, _values, result ) + result = ast AST::ResourceRef, :type => val[0], :title => val[2] + result + end +.,., + +module_eval <<'.,.,', 'grammar.ra', 349 + def _reduce_88( val, _values, result ) args = { :test => val[1], :statements => val[3] } if val[5] args[:else] = val[5] end result = ast AST::IfStatement, args result end .,., - # reduce 63 omitted + # reduce 89 omitted -module_eval <<'.,.,', 'grammar.ra', 275 - def _reduce_64( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 354 + def _reduce_90( val, _values, result ) result = ast AST::Else, :statements => val[2] result end .,., - # reduce 65 omitted + # reduce 91 omitted -module_eval <<'.,.,', 'grammar.ra', 287 - def _reduce_66( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 366 + def _reduce_92( val, _values, result ) options = val[3] unless options.instance_of?(AST::ASTArray) options = ast AST::ASTArray, :children => [val[3]] end result = ast AST::CaseStatement, :test => val[1], :options => options result end .,., - # reduce 67 omitted + # reduce 93 omitted -module_eval <<'.,.,', 'grammar.ra', 297 - def _reduce_68( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 376 + def _reduce_94( val, _values, result ) if val[0].instance_of?(AST::ASTArray) val[0].push val[1] result = val[0] else result = ast AST::ASTArray, :children => [val[0], val[1]] end result end .,., -module_eval <<'.,.,', 'grammar.ra', 301 - def _reduce_69( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 380 + def _reduce_95( val, _values, result ) result = ast AST::CaseOpt, :value => val[0], :statements => val[3] result end .,., -module_eval <<'.,.,', 'grammar.ra', 306 - def _reduce_70( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 385 + def _reduce_96( val, _values, result ) result = ast(AST::CaseOpt, :value => val[0], :statements => ast(AST::ASTArray) ) result end .,., - # reduce 71 omitted + # reduce 97 omitted -module_eval <<'.,.,', 'grammar.ra', 316 - def _reduce_72( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 395 + def _reduce_98( val, _values, result ) if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end result end .,., -module_eval <<'.,.,', 'grammar.ra', 320 - def _reduce_73( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 399 + def _reduce_99( val, _values, result ) result = ast AST::Selector, :param => val[0], :values => val[2] result end .,., - # reduce 74 omitted + # reduce 100 omitted -module_eval <<'.,.,', 'grammar.ra', 322 - def _reduce_75( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 401 + def _reduce_101( val, _values, result ) result = val[1] result end .,., - # reduce 76 omitted + # reduce 102 omitted -module_eval <<'.,.,', 'grammar.ra', 333 - def _reduce_77( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 412 + def _reduce_103( val, _values, result ) if val[0].instance_of?(AST::ASTArray) val[0].push(val[2]) result = val[0] else result = ast AST::ASTArray, :children => [val[0],val[2]] end result end .,., -module_eval <<'.,.,', 'grammar.ra', 337 - def _reduce_78( val, _values, result ) - result = ast AST::ObjectParam, :param => val[0], :value => val[2] +module_eval <<'.,.,', 'grammar.ra', 416 + def _reduce_104( val, _values, result ) + result = ast AST::ResourceParam, :param => val[0], :value => val[2] result end .,., - # reduce 79 omitted + # reduce 105 omitted - # reduce 80 omitted + # reduce 106 omitted - # reduce 81 omitted + # reduce 107 omitted - # reduce 82 omitted + # reduce 108 omitted - # reduce 83 omitted + # reduce 109 omitted - # reduce 84 omitted + # reduce 110 omitted -module_eval <<'.,.,', 'grammar.ra', 347 - def _reduce_85( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 426 + def _reduce_111( val, _values, result ) result = ast AST::Default, :value => val[0] result end .,., -module_eval <<'.,.,', 'grammar.ra', 404 - def _reduce_86( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 488 + def _reduce_112( val, _values, result ) # importing files # yuk, i hate keywords # we'll probably have to have some kind of search path eventually # but for now, just use a path relative to the file doing the importing dir = @lexer.file.sub(%r{[^/]+$},'').sub(/\/$/, '') if dir == "" dir = "." end result = ast AST::ASTArray Dir.chdir(dir) { # We can't interpolate at this point since we don't have any # scopes set up. Warn the user if they use a variable reference pat = val[1].value if pat.index("$") Puppet.warning( "The import of #{pat} contains a variable reference;" + " variables are not interpolated for imports " + "in file #{@lexer.file} at line #{@lexer.line}" ) end files = Dir.glob(pat) if files.size == 0 files = Dir.glob(pat + ".pp") if files.size == 0 raise Puppet::ImportError.new("No file(s) found for import " + "of '#{pat}'") end end files.each { |file| - parser = Puppet::Parser::Parser.new() + parser = Puppet::Parser::Parser.new(interp) parser.files = self.files Puppet.debug("importing '%s'" % file) unless file =~ /^#{File::SEPARATOR}/ file = File.join(dir, file) end begin parser.file = file rescue Puppet::ImportError Puppet.warning( "Importing %s would result in an import loop" % File.join(dir, file) ) next end # push the results into the main result array # We always return an array when we parse. - parser.parse.each do |child| - result.push child + ast = parser.parse + + # Things that just get added to the classtable or whatever return nil + if ast + ast.each do |child| + result.push child + end end } } result end .,., -module_eval <<'.,.,', 'grammar.ra', 420 - def _reduce_87( val, _values, result ) - args = { - :type => ast(AST::Name, :value => val[1]), - :args => val[2], - :code => val[4] # Switch to 5 for parents - } +module_eval <<'.,.,', 'grammar.ra', 498 + def _reduce_113( val, _values, result ) + interp.newdefine fqname(val[1]), :arguments => val[2], :code => val[4] + @lexer.indefine = false + result = nil - if val[3].instance_of?(AST::Name) - args[:parentclass] = val[3] - end - result = ast AST::CompDef, args #} | DEFINE NAME argumentlist parent LBRACE RBRACE { result end .,., -module_eval <<'.,.,', 'grammar.ra', 432 - def _reduce_88( val, _values, result ) - args = { - :type => ast(AST::Name, :value => val[1]), - :args => val[2], - :code => ast(AST::ASTArray) - } - - if val[3].instance_of?(AST::Name) - args[:parentclass] = val[3] - end - - result = ast AST::CompDef, args +module_eval <<'.,.,', 'grammar.ra', 502 + def _reduce_114( val, _values, result ) + interp.newdefine fqname(val[1]), :arguments => val[2] + @lexer.indefine = false + result = nil result end .,., -module_eval <<'.,.,', 'grammar.ra', 446 - def _reduce_89( val, _values, result ) - #:args => val[2], - args = { - :type => ast(AST::Name, :value => val[1]), - :code => val[4] - } - # It'll be an ASTArray if we didn't get a parent - if val[2].instance_of?(AST::Name) - args[:parentclass] = val[2] - end - result = ast AST::ClassDef, args +module_eval <<'.,.,', 'grammar.ra', 510 + def _reduce_115( val, _values, result ) + # Our class gets defined in the parent namespace, not our own. + @lexer.namepop + interp.newclass fqname(val[1]), :code => val[4], :parent => val[2] + result = nil result end .,., -module_eval <<'.,.,', 'grammar.ra', 456 - def _reduce_90( val, _values, result ) - args = { - :type => ast(AST::Name, :value => val[1]), - :code => ast(AST::ASTArray, :children => []) - } - # It'll be an ASTArray if we didn't get a parent - if val[2].instance_of?(AST::Name) - args[:parentclass] = val[2] - end - result = ast AST::ClassDef, args +module_eval <<'.,.,', 'grammar.ra', 515 + def _reduce_116( val, _values, result ) + # Our class gets defined in the parent namespace, not our own. + @lexer.namepop + interp.newclass fqname(val[1]), :parent => val[2] + result = nil result end .,., -module_eval <<'.,.,', 'grammar.ra', 473 - def _reduce_91( val, _values, result ) - unless val[1].instance_of?(AST::ASTArray) - val[1] = ast AST::ASTArray, - :line => val[1].line, - :file => val[1].file, - :children => [val[1]] - end - args = { - :names => val[1], - :code => val[4] - } - if val[2].instance_of?(AST::Name) - args[:parentclass] = val[2] - end - result = ast AST::NodeDef, args +module_eval <<'.,.,', 'grammar.ra', 520 + def _reduce_117( val, _values, result ) + interp.newnode val[1], :parent => val[2], :code => val[4] + result = nil result end .,., -module_eval <<'.,.,', 'grammar.ra', 488 - def _reduce_92( val, _values, result ) - unless val[1].instance_of?(AST::ASTArray) - val[1] = ast AST::ASTArray, - :line => val[1].line, - :file => val[1].file, - :children => [val[1]] - end - args = { - :names => val[1], - :code => ast(AST::ASTArray, :children => []) - } - if val[2].instance_of?(AST::Name) - args[:parentclass] = val[2] - end - result = ast AST::NodeDef,args +module_eval <<'.,.,', 'grammar.ra', 523 + def _reduce_118( val, _values, result ) + interp.newnode val[1], :parent => val[2] + result = nil result end .,., - # reduce 93 omitted + # reduce 119 omitted -module_eval <<'.,.,', 'grammar.ra', 499 - def _reduce_94( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - result = val[0] - result.push val[2] - else - result = ast AST::ASTArray, :children => [val[0], val[2]] - end - result - end -.,., + # reduce 120 omitted -module_eval <<'.,.,', 'grammar.ra', 503 - def _reduce_95( val, _values, result ) - result = ast AST::HostName, :value => val[0] - result - end -.,., + # reduce 121 omitted -module_eval <<'.,.,', 'grammar.ra', 505 - def _reduce_96( val, _values, result ) - result = ast AST::HostName, :value => val[0] +module_eval <<'.,.,', 'grammar.ra', 535 + def _reduce_122( val, _values, result ) + result = val[0] + result = [result] unless result.is_a?(Array) + result << val[2] result end .,., -module_eval <<'.,.,', 'grammar.ra', 507 - def _reduce_97( val, _values, result ) - result = ast AST::Default, :value => val[0] + # reduce 123 omitted + + # reduce 124 omitted + + # reduce 125 omitted + +module_eval <<'.,.,', 'grammar.ra', 543 + def _reduce_126( val, _values, result ) + result = nil result end .,., -module_eval <<'.,.,', 'grammar.ra', 511 - def _reduce_98( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 547 + def _reduce_127( val, _values, result ) result = ast AST::ASTArray, :children => [] result end .,., - # reduce 99 omitted + # reduce 128 omitted -module_eval <<'.,.,', 'grammar.ra', 516 - def _reduce_100( val, _values, result ) - result = val[1] +module_eval <<'.,.,', 'grammar.ra', 552 + def _reduce_129( val, _values, result ) + result = nil result end .,., -module_eval <<'.,.,', 'grammar.ra', 523 - def _reduce_101( val, _values, result ) - if val[1].instance_of?(AST::ASTArray) - result = val[1] - else - result = ast AST::ASTArray, :children => [val[1]] - end +module_eval <<'.,.,', 'grammar.ra', 556 + def _reduce_130( val, _values, result ) + result = val[1] + result = [result] unless result[0].is_a?(Array) result end .,., - # reduce 102 omitted + # reduce 131 omitted -module_eval <<'.,.,', 'grammar.ra', 533 - def _reduce_103( val, _values, result ) - if val[0].instance_of?(AST::ASTArray) - val[0].push(val[2]) - result = val[0] - else - result = ast AST::ASTArray, :children => [val[0],val[2]] - end +module_eval <<'.,.,', 'grammar.ra', 563 + def _reduce_132( val, _values, result ) + result = val[0] + result = [result] unless result[0].is_a?(Array) + result << val[2] result end .,., -module_eval <<'.,.,', 'grammar.ra', 541 - def _reduce_104( val, _values, result ) - msg = "Deprecation notice: #{val[0].value} must now include '$' in prototype" - msg += " at line %s" % @lexer.line - msg += " in file %s" % @lexer.file if @lexer.file - Puppet.warning msg - result = ast AST::CompArgument, :children => [val[0],val[2]] +module_eval <<'.,.,', 'grammar.ra', 568 + def _reduce_133( val, _values, result ) + Puppet.warning addcontext("Deprecation notice: #{val[0].value} must now include '$' in prototype") + result = [val[0], val[2]] result end .,., -module_eval <<'.,.,', 'grammar.ra', 548 - def _reduce_105( val, _values, result ) - msg = "Deprecation notice: #{val[0].value} must now include '$' in prototype" - msg += " at line %s" % @lexer.line - msg += " in file %s" % @lexer.file if @lexer.file - Puppet.warning msg - result = ast AST::CompArgument, :children => [val[0]] +module_eval <<'.,.,', 'grammar.ra', 572 + def _reduce_134( val, _values, result ) + Puppet.warning addcontext("Deprecation notice: #{val[0].value} must now include '$' in prototype") + result = [val[0]] result end .,., -module_eval <<'.,.,', 'grammar.ra', 550 - def _reduce_106( val, _values, result ) - result = ast AST::CompArgument, :children => [val[0],val[2]] +module_eval <<'.,.,', 'grammar.ra', 574 + def _reduce_135( val, _values, result ) + result = [val[0], val[2]] result end .,., -module_eval <<'.,.,', 'grammar.ra', 552 - def _reduce_107( val, _values, result ) - result = ast AST::CompArgument, :children => [val[0]] +module_eval <<'.,.,', 'grammar.ra', 576 + def _reduce_136( val, _values, result ) + result = [val[0]] result end .,., - # reduce 108 omitted + # reduce 137 omitted -module_eval <<'.,.,', 'grammar.ra', 557 - def _reduce_109( val, _values, result ) - result = ast AST::Name, :value => val[1] +module_eval <<'.,.,', 'grammar.ra', 581 + def _reduce_138( val, _values, result ) + result = val[1] result end .,., -module_eval <<'.,.,', 'grammar.ra', 562 - def _reduce_110( val, _values, result ) - name = val[0].sub(/^\$/,'') - result = ast AST::Variable, :value => name +module_eval <<'.,.,', 'grammar.ra', 585 + def _reduce_139( val, _values, result ) + result = ast AST::Variable, :value => val[0] result end .,., -module_eval <<'.,.,', 'grammar.ra', 567 - def _reduce_111( val, _values, result ) - result = ast AST::Name, :value => val[0].sub(/^\$/,'') - result - end -.,., - -module_eval <<'.,.,', 'grammar.ra', 575 - def _reduce_112( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 593 + def _reduce_140( val, _values, result ) if val[1].instance_of?(AST::ASTArray) result = val[1] else result = ast AST::ASTArray, :children => [val[1]] end result end .,., -module_eval <<'.,.,', 'grammar.ra', 577 - def _reduce_113( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 595 + def _reduce_141( val, _values, result ) result = ast AST::ASTArray result end .,., - # reduce 114 omitted + # reduce 142 omitted - # reduce 115 omitted + # reduce 143 omitted - # reduce 116 omitted + # reduce 144 omitted -module_eval <<'.,.,', 'grammar.ra', 582 - def _reduce_117( val, _values, result ) +module_eval <<'.,.,', 'grammar.ra', 600 + def _reduce_145( val, _values, result ) result = nil result end .,., def _reduce_none( val, _values, result ) result end end # class Parser end # module Parser end # module Puppet diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb new file mode 100644 index 000000000..5d09eaa9b --- /dev/null +++ b/lib/puppet/parser/resource.rb @@ -0,0 +1,324 @@ +# 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 :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| + 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 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] || + devfail("%s must be passed to Resources" % 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 + + # 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) + # This defaults to true + unless Puppet[:paramcheck] + return + end + + return if param == "name" or param == "title" # always allow these + + # FIXME We might need to do more here eventually. Metaparams + # behave strangely on containers. + return if Puppet::Type.metaparam?(param) + + # Now make sure it's a valid argument to our class. + unless @ref.typeclass.validattr?(param) + 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 + 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/parser/resource/param.rb b/lib/puppet/parser/resource/param.rb new file mode 100644 index 000000000..259e935d1 --- /dev/null +++ b/lib/puppet/parser/resource/param.rb @@ -0,0 +1,44 @@ +# The parameters we stick in Resources. +class Puppet::Parser::Resource::Param + attr_accessor :name, :value, :source, :line, :file + include Puppet::Util::Errors + include Puppet::Util::MethodHelper + + def initialize(hash) + set_options(hash) + requiredopts(:name, :value, :source) + @name = @name.intern if @name.is_a?(String) + end + + def inspect + "#<#{self.class} @name => #{self.name}, @value => #{self.value}, @source => #{self.source.type}>" + end + + # Store this parameter in a Rails db. + def store(resource) + args = {} + [:name, :value, :line, :file].each do |var| + if val = self.send(var) + args[var] = val + end + end + args[:name] = args[:name].to_s + if obj = resource.rails_parameters.find_by_name(self.name) + # We exist + args.each do |p, v| + obj[p] = v + end + else + # Else create it anew + obj = resource.rails_parameters.build(args) + end + + return obj + end + + def to_s + "%s => %s" % [self.name, self.value] + end +end + +# $Id$ diff --git a/lib/puppet/parser/resource/reference.rb b/lib/puppet/parser/resource/reference.rb new file mode 100644 index 000000000..2210b71c2 --- /dev/null +++ b/lib/puppet/parser/resource/reference.rb @@ -0,0 +1,68 @@ +# A reference to a resource. Mostly just the type and title. +class Puppet::Parser::Resource::Reference + include Puppet::Util::MethodHelper + include Puppet::Util::Errors + + attr_accessor :type, :title, :builtin, :file, :line, :scope + + # Are we a builtin type? + def builtin? + unless defined? @builtin + if builtintype() + @builtin = true + else + @builtin = false + end + end + + self.builtin + end + + def builtintype + if t = Puppet::Type.type(self.type) and t.name != :component + t + else + nil + end + end + + # Return the defined type for our obj. + def definedtype + unless defined? @definedtype + if tmp = @scope.finddefine(self.type) + @definedtype = tmp + else + fail Puppet::ParseError, "Could not find definition %s" % self.type + end + end + + @definedtype + end + + def initialize(hash) + set_options(hash) + requiredopts(:type, :title) + end + + def to_ref + return [type.to_s,title.to_s] + end + + def to_s + "%s[%s]" % [type, title] + end + + def typeclass + unless defined? @typeclass + if tmp = builtintype || definedtype + @typeclass = tmp + else + fail Puppet::ParseError, "Could not find type %s" % self.type + end + end + + @typeclass + end +end + +# $Id$ diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index a26ec938d..9b59ebd64 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,1194 +1,648 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'puppet/parser/parser' +require 'puppet/parser/templatewrapper' require 'puppet/transportable' -module Puppet::Parser - class Scope - class ScopeObj < Hash - attr_accessor :file, :line, :type, :name - end - - # A simple wrapper for templates, so they don't have full access to - # the scope objects. - class TemplateWrapper - attr_accessor :scope, :file - include Puppet::Util - Puppet::Util.logmethods(self) - - def initialize(scope, file) - @scope = scope - if file =~ /^#{File::SEPARATOR}/ - @file = file - else - @file = File.join(Puppet[:templatedir], file) - end +class Puppet::Parser::Scope + require 'puppet/parser/resource' - unless FileTest.exists?(@file) - raise Puppet::ParseError, - "Could not find template %s" % file - end + AST = Puppet::Parser::AST - # We'll only ever not have an interpreter in testing, but, eh. - if @scope.interp - @scope.interp.newfile(@file) - end - end + # This doesn't actually work right now. + Puppet.config.setdefaults(:puppet, + :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], + :templatedir => ["$vardir/templates", + "Where Puppet looks for template files." + ] + ) - # Ruby treats variables like methods, so we can cheat here and - # trap missing vars like they were missing methods. - def method_missing(name, *args) - # We have to tell lookupvar to return :undefined to us when - # appropriate; otherwise it converts to "". - value = @scope.lookupvar(name.to_s, false) - if value != :undefined - return value - else - # Just throw an error immediately, instead of searching for - # other missingmethod things or whatever. - raise Puppet::ParseError, - "Could not find value for '%s'" % name - end - end - - def result - result = nil - benchmark(:debug, "Interpolated template #{@file}") do - template = ERB.new(File.read(@file), 0, "-") - result = template.result(binding) - end - - result - end - - def to_s - "template[%s]" % @file - end - end + Puppet::Util.logmethods(self) - # This doesn't actually work right now. - Puppet.config.setdefaults(:puppet, - :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], - :templatedir => ["$vardir/templates", - "Where Puppet looks for template files." - ] - ) + include Enumerable + include Puppet::Util::Errors + attr_accessor :parent, :level, :interp, :source, :host + attr_accessor :name, :type, :topscope, :base, :keyword, :namespace + attr_accessor :top, :context, :translated, :exported - Puppet::Util.logmethods(self) + # Whether we behave declaratively. Note that it's a class variable, + # so all scopes behave the same. + @@declarative = true - include Enumerable - attr_accessor :parent, :level, :interp - attr_accessor :name, :type, :topscope, :base, :keyword + # Retrieve and set the declarative setting. + def self.declarative + return @@declarative + end - attr_accessor :top, :context, :translated, :collectable + def self.declarative=(val) + @@declarative = val + end - # This is probably not all that good of an idea, but... - # This way a parent can share its tables with all of its children. - attr_writer :nodetable, :classtable, :definedtable, :exportable + # This handles the shared tables that all scopes have. They're effectively + # global tables, except that they're only global for a single scope tree, + # which is why I can't use class variables for them. + def self.sharedtable(*names) + attr_accessor(*names) + @@sharedtables ||= [] + @@sharedtables += names + end - # Whether we behave declaratively. Note that it's a class variable, - # so all scopes behave the same. - @@declarative = true + # This is probably not all that good of an idea, but... + # This way a parent can share its tables with all of its children. + sharedtable :classtable, :definedtable, :exportable, :overridetable, :collecttable - # Retrieve and set the declarative setting. - def self.declarative - return @@declarative + # Is the value true? This allows us to control the definition of truth + # in one place. + def self.true?(value) + if value == false or value == "" + return false + else + return true end + end - def self.declarative=(val) - @@declarative = val + # Is the type a builtin type? + def builtintype?(type) + if typeklass = Puppet::Type.type(type) + return typeklass + else + return false end + end - # Is the value true? This allows us to control the definition of truth - # in one place. - def self.true?(value) - if value == false or value == "" - return false - else - return true - end + # Create a new child scope. + def child=(scope) + @children.push(scope) + + # Copy all of the shared tables over to the child. + @@sharedtables.each do |name| + scope.send(name.to_s + "=", self.send(name)) end + end - # Add all of the defaults for a given object to that object. - def adddefaults(obj) - defaults = lookupdefaults(obj.type) + # Verify that the given object isn't defined elsewhere. + def chkobjectclosure(obj) + if @definedtable.include?(obj.ref) + typeklass = Puppet::Type.type(obj.type) + if typeklass and ! typeklass.isomorphic? + Puppet.info "Allowing duplicate %s" % type + else + exobj = @definedtable[obj.ref] - defaults.each do |var, value| - unless obj[var] - self.debug "Adding default %s for %s" % - [var, obj.type] + # Either it's a defined type, which are never + # isomorphic, or it's a non-isomorphic type. + msg = "Duplicate definition: %s is already defined" % obj.ref - obj[var] = value + if exobj.file and exobj.line + msg << " in file %s at line %s" % + [exobj.file, exobj.line] end - end - end - # Add a single object's tags to the global list of tags for - # that object. - def addtags(obj) - unless defined? @tagtable - raise Puppet::DevError, "Told to add tags, but no tag table" - end - list = @tagtable[obj.type][obj.name] - - obj.tags.each { |tag| - unless list.include?(tag) - if tag.nil? or tag == "" - Puppet.debug "Got tag %s from %s(%s)" % - [tag.inspect, obj.type, obj.name] - else - list << tag - end + if obj.line or obj.file + msg << "; cannot redefine" end - } - end - # Is the type a builtin type? - def builtintype?(type) - if typeklass = Puppet::Type.type(type) - return typeklass - else - return false + raise Puppet::ParseError.new(msg) end end - # Verify that the given object isn't defined elsewhere. - def chkobjectclosure(hash) - type = hash[:type] - name = hash[:name] - unless name - return true - end - if @definedtable[type].include?(name) - typeklass = Puppet::Type.type(type) - if typeklass and ! typeklass.isomorphic? - Puppet.info "Allowing duplicate %s" % type - else - exobj = @definedtable[type][name] - - # Either it's a defined type, which are never - # isomorphic, or it's a non-isomorphic type. - msg = "Duplicate definition: %s[%s] is already defined" % - [type, name] - - if exobj.file and exobj.line - msg << " in file %s at line %s" % - [exobj.file, exobj.line] - end - - if hash[:line] or hash[:file] - msg << "; cannot redefine" - end - - error = Puppet::ParseError.new(msg) - raise error - end - end - - return true - end - - def declarative=(val) - self.class.declarative = val - end - - def declarative - self.class.declarative - end - - # Log the existing tags. At some point this should be in a better - # place, but eh. - def logtags - @tagtable.sort { |a, b| - a[0] <=> b[0] - }.each { |type, names| - names.sort { |a, b| - a[0] <=> b[0] - }.each { |name, tags| - Puppet.info "%s(%s): '%s'" % [type, name, tags.join("' '")] - } - } - end + return true + end - # Create a new child scope. - def child=(scope) - @children.push(scope) + # Return the list of collections. + def collections + @collecttable + end - if defined? @nodetable - scope.nodetable = @nodetable - else - raise Puppet::DevError, "No nodetable has been defined" - end + def declarative=(val) + self.class.declarative = val + end - if defined? @classtable - scope.classtable = @classtable - else - raise Puppet::DevError, "No classtable has been defined" - end + def declarative + self.class.declarative + end - if defined? @exportable - scope.exportable = @exportable - else - raise Puppet::DevError, "No exportable has been defined" - end + # Test whether a given scope is declarative. Even though it's + # a global value, the calling objects don't need to know that. + def declarative? + @@declarative + end - if defined? @definedtable - scope.definedtable = @definedtable - else - raise Puppet::DevError, "No definedtable has been defined" - end - end + # Remove a specific child. + def delete(child) + @children.delete(child) + end - # Test whether a given scope is declarative. Even though it's - # a global value, the calling objects don't need to know that. - def declarative? - @@declarative + # Remove a resource from the various tables. This is only used when + # a resource maps to a definition and gets evaluated. + def deleteresource(resource) + if @definedtable[resource.ref] + @definedtable.delete(resource.ref) end - # Remove a specific child. - def delete(child) - @children.delete(child) + if @children.include?(resource) + @children.delete(resource) end + end - # Are we the top scope? - def topscope? - @level == 1 - end + # Are we the top scope? + def topscope? + @level == 1 + end - # Return a list of all of the defined classes. - def classlist - unless defined? @classtable - raise Puppet::DevError, "Scope did not receive class table" - end - return @classtable.collect { |id, klass| - # The class table can contain scopes or strings as its values - # so support them accordingly. - if klass.is_a? Scope - klass.type - else - klass - end - } + # Return a list of all of the defined classes. + def classlist + unless defined? @classtable + raise Puppet::DevError, "Scope did not receive class table" end + return @classtable.keys.reject { |k| k == "" } + end - # Yield each child scope in turn - def each - @children.each { |child| - yield child - } - end + # Yield each child scope in turn + def each + @children.each { |child| + yield child + } + end - # Evaluate a list of classes. - def evalclasses(classes) - return unless classes - classes.each do |klass| - if code = lookuptype(klass) - # Just reuse the 'include' function, since that's the equivalent - # of what we're doing here. - function_include(klass) - end + # Evaluate a list of classes. + def evalclasses(*classes) + retval = [] + classes.each do |klass| + if obj = findclass(klass) + obj.safeevaluate :scope => self + retval << klass end end + end - # Evaluate a specific node's code. This method will normally be called - # on the top-level scope, but it actually evaluates the node at the - # appropriate scope. - def evalnode(hash) - objects = hash[:ast] - names = hash[:names] or - raise Puppet::DevError, "Node names must be provided to evalnode" - facts = hash[:facts] - classes = hash[:classes] - parent = hash[:parent] - - # Always add "default" to our name list, so we're always searching - # for a default node. - names << "default" - - scope = code = nil - # Find a node that matches one of our names - names.each { |node| - if nodehash = @nodetable[node] - code = nodehash[:node] - scope = nodehash[:scope] - - if node == "default" - Puppet.info "Using default node" - end - break - end - } - - # And fail if we don't find one. - unless scope and code - raise Puppet::Error, "Could not find configuration for %s" % - names.join(" or ") - end + def exported? + self.exported + end - # We need to do a little skullduggery here. We want a - # temporary scope, because we don't want this scope to - # show up permanently in the scope tree -- otherwise we could - # not evaluate the node multiple times. We could conceivably - # cache the results, but it's not worth it at this stage. + def findclass(name) + interp.findclass(namespace, name) + end - # Note that we evaluate the node code with its containing - # scope, not with the top scope. We also retrieve the created - # scope so that we can get any classes set within it - nodescope = code.safeevaluate(:scope => scope, :facts => facts) + def finddefine(name) + interp.finddefine(namespace, name) + end - scope.evalclasses(classes) + def findresource(string, name = nil) + if name + string = "%s[%s]" % [string, name] end - # The top-level evaluate, used to evaluate a whole AST tree. This is - # a strange method, in that it turns around and calls evaluate() on its - # :ast argument. - def evaluate(hash) - objects = hash[:ast] - facts = hash[:facts] || {} - - @@done = [] - - unless objects - raise Puppet::DevError, "Evaluation requires an AST tree" - end - - # Set all of our facts in the top-level scope. - facts.each { |var, value| - self.setvar(var, value) - } - - # Evaluate all of our configuration. This does not evaluate any - # node definitions. - result = objects.safeevaluate(:scope => self) - - # If they've provided a name or a parent, we assume they're looking - # for nodes. - if hash[:searched] - # Specifying a parent node takes precedence, because it is assumed - # that this node was found in a remote repository like ldap. - gennode(hash) - elsif hash.include? :names # else, look for it in the config - evalnode(hash) - else - # Else we're not using nodes at all, so just evaluate any passed-in - # classes. - classes = hash[:classes] || [] - evalclasses(classes) - - # These classes would be passed in manually, via something like - # a cfengine module - end - - bucket = self.to_trans - - # Add our class list - unless self.classlist.empty? - bucket.classes = self.classlist - end - - # Now clean up after ourselves - [@@done].each do |table| - table.clear - end + @definedtable[string] + end - return bucket + # Recursively complete the whole tree, in preparation for + # translation or storage. + def finish + self.each do |obj| + obj.finish end + end - # Return the hash of objects that we specifically exported. We return - # a hash to make it easy for the caller to deduplicate based on name. - def exported(type) - if @exportable.include?(type) - return @exportable[type].dup + # Initialize our new scope. Defaults to having no parent and to + # being declarative. + def initialize(hash = {}) + @parent = nil + @type = nil + @name = nil + @finished = false + hash.each { |name, val| + method = name.to_s + "=" + if self.respond_to? method + self.send(method, val) else - return {} - end - end - - # Store our object in the central export table. - def exportobject(obj) - if @exportable.include?(obj.type) and - @exportable[obj.type].include?(obj.name) - raise Puppet::ParseError, "Object %s[%s] is already exported" % - [obj.type, obj.name] + raise Puppet::DevError, "Invalid scope argument %s" % name end + } - debug "Exporting %s[%s]" % [obj.type, obj.name] + @tags = [] - @exportable[obj.type][obj.name] = obj + if @parent.nil? + unless hash.include?(:declarative) + hash[:declarative] = true + end + self.istop(hash[:declarative]) + @inside = nil + else + # This is here, rather than in newchild(), so that all + # of the later variable initialization works. + @parent.child = self - return obj + @level = @parent.level + 1 + @interp = @parent.interp + @source = hash[:source] || @parent.source + @topscope = @parent.topscope + @context = @parent.context + @inside = @parent.inside + @host = @parent.host + @type ||= @parent.type end - # Pull in all of the appropriate classes and evaluate them. It'd - # be nice if this didn't know quite so much about how AST::Node - # operated internally. This is used when a list of classes is passed in, - # instead of a node definition, such as from the cfengine module. - def gennode(hash) - names = hash[:names] or - raise Puppet::DevError, "Node names must be provided to gennode" - facts = hash[:facts] - classes = hash[:classes] - parent = hash[:parentnode] - name = names.shift - arghash = { - :type => name, - :code => AST::ASTArray.new(:pin => "[]") - } + # Our child scopes and objects + @children = [] - #Puppet.notice "hash is %s" % - # hash.inspect - #Puppet.notice "Classes are %s, parent is %s" % - # [classes.inspect, parent.inspect] + # The symbol table for this scope + @symtable = Hash.new(nil) - if parent - arghash[:parentclass] = parent - end + # All of the defaults set for types. It's a hash of hashes, + # with the first key being the type, then the second key being + # the parameter. + @defaultstable = Hash.new { |dhash,type| + dhash[type] = Hash.new(nil) + } - # Create the node - node = AST::Node.new(arghash) - node.keyword = "node" + # Map the names to the tables. + @map = { + "variable" => @symtable, + "defaults" => @defaultstable + } - # Now evaluate it, which evaluates the parent and nothing else - # but does return the nodescope. - scope = node.safeevaluate(:scope => self) - - # Finally evaluate our list of classes in this new scope. - scope.evalclasses(classes) + unless @interp + raise Puppet::DevError, "Scopes require an interpreter" end + end - # Initialize our new scope. Defaults to having no parent and to - # being declarative. - def initialize(hash = {}) - @parent = nil - @type = nil - @name = nil - @finished = false - hash.each { |name, val| - method = name.to_s + "=" - if self.respond_to? method - self.send(method, val) - else - raise Puppet::DevError, "Invalid scope argument %s" % name - end - } - - @tags = [] - - if @parent.nil? - unless hash.include?(:declarative) - hash[:declarative] = true - end - self.istop(hash[:declarative]) - @inside = nil - else - # This is here, rather than in newchild(), so that all - # of the later variable initialization works. - @parent.child = self - - @level = @parent.level + 1 - @interp = @parent.interp - @topscope = @parent.topscope - @context = @parent.context - @inside = @parent.inside - end - - # Our child scopes and objects - @children = [] - - # The symbol table for this scope - @symtable = Hash.new(nil) + # Associate the object directly with the scope, so that contained objects + # can look up what container they're running within. + def inside(arg = nil) + return @inside unless arg + + old = @inside + @inside = arg + yield + ensure + #Puppet.warning "exiting %s" % @inside.name + @inside = old + end - # The type table for this scope - @typetable = Hash.new(nil) + # Mark that we're the top scope, and set some hard-coded info. + def istop(declarative = true) + # the level is mostly used for debugging + @level = 1 - # All of the defaults set for types. It's a hash of hashes, - # with the first key being the type, then the second key being - # the parameter. - @defaultstable = Hash.new { |dhash,type| - dhash[type] = Hash.new(nil) - } + # The table for storing class singletons. This will only actually + # be used by top scopes and node scopes. + @classtable = Hash.new(nil) - # The object table is similar, but it is actually a hash of hashes - # where the innermost objects are TransObject instances. - @objectable = Hash.new { |typehash,typekey| - # See #newobject for how to create the actual objects - typehash[typekey] = Hash.new(nil) - } + self.class.declarative = declarative - # This is just for collecting statements locally, so we can - # verify that there is no overlap within this specific scope - @localobjectable = Hash.new { |typehash,typekey| - typehash[typekey] = Hash.new(nil) - } + # The table for all defined objects. + @definedtable = {} - # Map the names to the tables. - @map = { - "variable" => @symtable, - "type" => @typetable, - "node" => @nodetable, - "object" => @objectable, - "defaults" => @defaultstable - } - end + # The list of objects that will available for export. + @exportable = {} - # Associate the object directly with the scope, so that contained objects - # can look up what container they're running within. - def inside(arg = nil) - return @inside unless arg - - old = @inside - @inside = arg - yield - ensure - #Puppet.warning "exiting %s" % @inside.name - @inside = old + # The list of overrides. This is used to cache overrides on objects + # that don't exist yet. We store an array of each override. + @overridetable = Hash.new do |overs, ref| + overs[ref] = [] end - # Mark that we're the top scope, and set some hard-coded info. - def istop(declarative = true) - # the level is mostly used for debugging - @level = 1 - - # The table for storing class singletons. This will only actually - # be used by top scopes and node scopes. - @classtable = Hash.new(nil) + # Eventually, if we support sites, this will allow definitions + # of nodes with the same name in different sites. For now + # the top-level scope is always the only site scope. + @sitescope = true - self.class.declarative = declarative + @namespace = "" - # The table for all defined objects. - @definedtable = Hash.new { |types, type| - types[type] = {} - } - - # A table for storing nodes. - @nodetable = Hash.new(nil) + # The list of collections that have been created. This is a global list, + # but they each refer back to the scope that created them. + @collecttable = [] - # The list of objects that will available for export. - @exportable = Hash.new { |types, type| - types[type] = {} - } + @context = nil + @topscope = self + @type = "puppet" + @name = "top" + end - # Eventually, if we support sites, this will allow definitions - # of nodes with the same name in different sites. For now - # the top-level scope is always the only site scope. - @sitescope = true - - # And create a tag table, so we can collect all of the tags - # associated with any objects created in this scope tree - @tagtable = Hash.new { |types, type| - types[type] = Hash.new { |names, name| - names[name] = [] - } + # Collect all of the defaults set at any higher scopes. + # This is a different type of lookup because it's additive -- + # it collects all of the defaults, with defaults in closer scopes + # overriding those in later scopes. + def lookupdefaults(type) + values = {} + + # first collect the values from the parents + unless @parent.nil? + @parent.lookupdefaults(type).each { |var,value| + values[var] = value } - - @context = nil - @topscope = self - @type = "puppet" - @name = "top" - end - - # Look up a given class. This enables us to make sure classes are - # singletons - def lookupclass(klassid) - unless defined? @classtable - raise Puppet::DevError, "Scope did not receive class table" - end - return @classtable[klassid] end - # Collect all of the defaults set at any higher scopes. - # This is a different type of lookup because it's additive -- - # it collects all of the defaults, with defaults in closer scopes - # overriding those in later scopes. - def lookupdefaults(type) - values = {} - - # first collect the values from the parents - unless @parent.nil? - @parent.lookupdefaults(type).each { |var,value| - values[var] = value - } - end - - # then override them with any current values - # this should probably be done differently - if @defaultstable.include?(type) - @defaultstable[type].each { |var,value| - values[var] = value - } - end - #Puppet.debug "Got defaults for %s: %s" % - # [type,values.inspect] - return values - end - - # Look up all of the exported objects of a given type. Just like - # lookupobject, this only searches up through parent classes, not - # the whole scope tree. - def lookupexported(type) - found = [] - sub = proc { |table| - # We always return nil so that it will search all the way - # up the scope tree. - if table.has_key?(type) - table[type].each do |name, obj| - found << obj - end - nil - else - info table.keys.inspect - nil - end + # then override them with any current values + # this should probably be done differently + if @defaultstable.include?(type) + @defaultstable[type].each { |var,value| + values[var] = value } - - value = lookup("object",sub, false) - - return found end - # Look up a node by name - def lookupnode(name) - #Puppet.debug "Looking up type %s" % name - value = lookup("type",name) - if value == :undefined - return nil - else - #Puppet.debug "Found node %s" % name - return value - end - end + #Puppet.debug "Got defaults for %s: %s" % + # [type,values.inspect] + return values + end - # Look up a defined type. - def lookuptype(name) - #Puppet.debug "Looking up type %s" % name - value = lookup("type",name) - if value == :undefined - return nil - else - #Puppet.debug "Found type %s" % name - return value - end + # Look up all of the exported objects of a given type. Just like + # lookupobject, this only searches up through parent classes, not + # the whole scope tree. + def lookupexported(type) + @definedtable.find_all do |name, r| + r.type == type and r.exported? end + end - # Look up an object by name and type. This should only look up objects - # within a class structure, not within the entire scope structure. - def lookupobject(hash) - type = hash[:type] - name = hash[:name] - #Puppet.debug "Looking up object %s of type %s in level %s" % - # [name, type, @level] - sub = proc { |table| - if table.include?(type) - if table[type].include?(name) - table[type][name] - end - else - nil - end - } - value = lookup("object",sub, true) - if value == :undefined - return nil - else - return value - end - end + def lookupoverrides(obj) + @overridetable[obj.ref] + end - # Look up a variable. The simplest value search we do. Default to returning - # an empty string for missing values, but support returning a constant. - def lookupvar(name, usestring = true) - value = lookup("variable", name) - if value == :undefined - if usestring - return "" - else - return :undefined - end + # Look up a defined type. + def lookuptype(name) + finddefine(name) || findclass(name) + end + + # Look up a variable. The simplest value search we do. Default to returning + # an empty string for missing values, but support returning a constant. + def lookupvar(name, usestring = true) + value = lookup("variable", name) + if value == :undefined + if usestring + return "" else - return value + return :undefined end + else + return value end + end - def newcollection(coll) - @children << coll - end - - # Add a new object to our object table. - def newobject(hash) - if @objectable[hash[:type]].include?(hash[:name]) - raise Puppet::DevError, "Object %s[%s] is already defined" % - [hash[:type], hash[:name]] - end - - self.chkobjectclosure(hash) - - obj = nil - - obj = Puppet::TransObject.new(hash[:name], hash[:type]) - - @children << obj + # Add a collection to the global list. + def newcollection(coll) + @collecttable << coll + end - @objectable[hash[:type]][hash[:name]] = obj + # Create a new scope. + def newscope(hash = {}) + hash[:parent] = self + #debug "Creating new scope, level %s" % [self.level + 1] + return Puppet::Parser::Scope.new(hash) + end - @definedtable[hash[:type]][hash[:name]] = obj + # Return the list of remaining overrides. + def overrides + @overridetable.collect { |name, overs| overs }.flatten + end - return obj - end + def resources + @definedtable.values + end - # Create a new scope. - def newscope(hash = {}) - hash[:parent] = self - #debug "Creating new scope, level %s" % [self.level + 1] - return Puppet::Parser::Scope.new(hash) + def setclass?(obj) + if obj.respond_to?(:fqname) + @classtable.has_key?(obj.fqname) + else + @classtable[obj] end + end - # Retrieve a specific node. This is used in ast.rb to find a - # parent node and in findnode to retrieve and evaluate a node. - def node(name) - @nodetable[name] + # Store the fact that we've evaluated a given class. We use a hash + # that gets inherited from the top scope down, rather than a global + # hash. We store the object ID, not class name, so that we + # can support multiple unrelated classes with the same name. + def setclass(obj) + if obj.is_a?(AST::HostClass) + unless obj.fqname + raise Puppet::DevError, "Got a %s with no fully qualified name" % + obj.class + end + @classtable[obj.fqname] = obj + else + raise Puppet::DevError, "Invalid class %s" % obj.inspect end + end - # Store the fact that we've evaluated a given class. We use a hash - # that gets inherited from the top scope down, rather than a global - # hash. We store the object ID, not class name, so that we - # can support multiple unrelated classes with the same name. - def setclass(id, name) - unless name =~ /^[a-z][\w-]*$/ - raise Puppet::ParseError, "Invalid class name '%s'" % name - end + # Set all of our facts in the top-level scope. + def setfacts(facts) + facts.each { |var, value| + self.setvar(var, value) + } + end - @classtable[id] = name - end + # Add a new object to our object table and the global list, and do any necessary + # checks. + def setresource(obj) + self.chkobjectclosure(obj) - # Store the scope for each class, so that other subclasses can look - # them up. - def setscope(id, scope) - @classtable[id] = scope - end + @children << obj - # Set defaults for a type. The typename should already be downcased, - # so that the syntax is isolated. - def setdefaults(type,params) - table = @defaultstable[type] + # The global table + @definedtable[obj.ref] = obj - # if we got a single param, it'll be in its own array - unless params[0].is_a?(Array) - params = [params] - end + return obj + end - params.each { |ary| - #Puppet.debug "Default for %s is %s => %s" % - # [type,ary[0].inspect,ary[1].inspect] - if @@declarative - if table.include?(ary[0]) - error = Puppet::ParseError.new( - "Default already defined for %s { %s }" % - [type,ary[0]] - ) - raise error - end - else - if table.include?(ary[0]) - # we should maybe allow this warning to be turned off... - Puppet.warning "Replacing default for %s { %s }" % - [type,ary[0]] - end - end - table[ary[0]] = ary[1] - } + # Override a parameter in an existing object. If the object does not yet + # exist, then cache the override in a global table, so it can be flushed + # at the end. + def setoverride(resource) + resource.override = true + if obj = @definedtable[resource.ref] + obj.merge(resource) + else + @overridetable[resource.ref] << resource end + end - # Store a host in the site node table. - def setnode(name,code) - unless defined? @nodetable - raise Puppet::DevError, "No node table defined" - end - if @nodetable.include?(name) - raise Puppet::ParseError, "Host %s is already defined" % name + # Set defaults for a type. The typename should already be downcased, + # so that the syntax is isolated. We don't do any kind of type-checking + # here; instead we let the resource do it when the defaults are used. + def setdefaults(type, params) + table = @defaultstable[type] + + # if we got a single param, it'll be in its own array + params = [params] unless params.is_a?(Array) + + params.each { |param| + #Puppet.debug "Default for %s is %s => %s" % + # [type,ary[0].inspect,ary[1].inspect] + if @@declarative + if table.include?(param.name) + self.fail "Default already defined for %s { %s }" % + [type,param.name] + end else - #Puppet.warning "Setting node %s at level %s" % [name, @level] - - # We have to store both the scope that's setting the node and - # the node itself, so that the node gets evaluated in the correct - # scope. - code.scope = self - @nodetable[name] = { - :scope => self, - :node => code - } + if table.include?(param.name) + # we should maybe allow this warning to be turned off... + Puppet.warning "Replacing default for %s { %s }" % + [type,param.name] + end end - end + table[param.name] = param + } + end - # Define our type. - def settype(name,ltype) - unless name - raise Puppet::DevError, "Got told to set type with a nil type" - end - unless ltype - raise Puppet::DevError, "Got told to set type with a nil object" - end - # Don't let them redefine the class in this scope. - if @typetable.include?(name) - raise Puppet::ParseError, - "%s is already defined" % name - else - ltype.scope = self - @typetable[name] = ltype + # Set a variable in the current scope. This will override settings + # in scopes above, but will not allow variables in the current scope + # to be reassigned if we're declarative (which is the default). + def setvar(name,value) + #Puppet.debug "Setting %s to '%s' at level %s" % + # [name.inspect,value,self.level] + if @@declarative and @symtable.include?(name) + raise Puppet::ParseError, "Cannot reassign variable %s" % name + else + if @symtable.include?(name) + Puppet.warning "Reassigning %s to %s" % [name,value] end + @symtable[name] = value end + end - # This method will fail if the named object is already defined anywhere - # in the scope tree, which is what provides some minimal closure-like - # behaviour. - def setobject(hash) - # FIXME This objectlookup stuff should be looking up using both - # the name and the namevar. - - # First see if we can look the object up using normal scope - # rules, i.e., one of our parent classes has defined the - # object or something - - name = hash[:name] - type = hash[:type] - params = hash[:arguments] - file = hash[:file] - line = hash[:line] - - collectable = hash[:collectable] || self.collectable - - # Verify that we're not overriding any already-set parameters. - if localobj = @localobjectable[type][name] - params.each { |var, value| - if localobj.include?(var) - msg = "Cannot reassign attribute %s on %s[%s]" % - [var, type, name] - - error = Puppet::ParseError.new(msg) - error.line = line - error.file = file - raise error - end - } + # Return an interpolated string. + def strinterp(string) + newstring = string.gsub(/\\\$|\$\{(\w+)\}|\$(\w+)/) do |value| + # If it matches the backslash, then just retun the dollar sign. + if value == '\\$' + '$' + else # look the variable up + var = $1 || $2 + lookupvar($1 || $2) end + end - # First look for it in a parent scope - obj = lookupobject(:name => name, :type => type) - - if obj - unless collectable == obj.collectable - msg = nil - if collectable - msg = "Exported %s[%s] cannot override local objects" - [type, name] - else - msg = "Local %s[%s] cannot override exported objects" - [type, name] - end - - error = Puppet::ParseError.new(msg) - error.line = line - error.file = file - raise error - end - end + return newstring.gsub(/\\t/, "\t").gsub(/\\n/, "\n").gsub(/\\s/, "\s") + end - unless obj and obj != :undefined - unless obj = @objectable[type][name] - obj = self.newobject( - :type => type, - :name => name, - :line => line, - :file => file - ) - - obj.collectable = collectable - - # only set these if we've created the object, - # which is the most common case - # FIXME we eventually need to store the file - # and line with each param, not the object - # itself. - obj.file = file - obj.line = line - end + # Add a tag to our current list. These tags will be added to all + # of the objects contained in this scope. + def tag(*ary) + ary.each { |tag| + unless tag =~ /^\w[-\w]+$/ + fail Puppet::ParseError, "Invalid tag %s" % tag end - - # Now add our parameters. This has the function of overriding - # existing values, which might have been defined in a higher - # scope. - params.each { |var,value| - # Add it to our found object - obj[var] = value - } - - # This is only used for override verification -- the local object - # table does not have transobjects or whatever in it, it just has - # simple hashes. This is necessary because setobject can modify - # our object table or a parent class's object table, and we - # still need to make sure param settings cannot be duplicated - # within our scope. - @localobjectable[type][name] ||= {} - params.each { |var,value| - # And add it to the local table; mmm, hack - @localobjectable[type][name][var] = value - } - - return obj - end - - # Set a variable in the current scope. This will override settings - # in scopes above, but will not allow variables in the current scope - # to be reassigned if we're declarative (which is the default). - def setvar(name,value) - #Puppet.debug "Setting %s to '%s' at level %s" % - # [name.inspect,value,self.level] - if @@declarative and @symtable.include?(name) - raise Puppet::ParseError, "Cannot reassign variable %s" % name - else - if @symtable.include?(name) - Puppet.warning "Reassigning %s to %s" % [name,value] - end - @symtable[name] = value + if tag.nil? or tag == "" + puts caller + Puppet.debug "got told to tag with %s" % tag.inspect + next end - end - - # Return an interpolated string. - def strinterp(string) - newstring = string.gsub(/\\\$|\$\{(\w+)\}|\$(\w+)/) do |value| - # If it matches the backslash, then just retun the dollar sign. - if value == '\\$' - '$' - else # look the variable up - var = $1 || $2 - lookupvar($1 || $2) - end + tag = tag.to_s + unless @tags.include?(tag) + #Puppet.info "Tagging scope %s with %s" % [self.object_id, tag] + @tags << tag end + } + end - return newstring.gsub(/\\t/, "\t").gsub(/\\n/, "\n").gsub(/\\s/, "\s") - end - - # Add a tag to our current list. These tags will be added to all - # of the objects contained in this scope. - def tag(*ary) - ary.each { |tag| + # Return the tags associated with this scope. It's basically + # just our parents' tags, plus our type. We don't cache this value + # because our parent tags might change between calls. + def tags + tmp = [] + @tags + unless ! defined? @type or @type.nil? or @type == "" + tmp << @type.to_s + end + if @parent + #info "Looking for tags in %s" % @parent.type + @parent.tags.each { |tag| if tag.nil? or tag == "" - Puppet.debug "got told to tag with %s" % tag.inspect + Puppet.debug "parent returned tag %s" % tag.inspect next end - unless @tags.include?(tag) - #Puppet.info "Tagging scope %s with %s" % [self.object_id, tag] - @tags << tag.to_s + unless tmp.include?(tag) + tmp << tag end } end + return tmp.sort.uniq + end - # Return the tags associated with this scope. It's basically - # just our parents' tags, plus our type. - def tags - tmp = [] + @tags - unless ! defined? @type or @type.nil? or @type == "" - tmp << @type.to_s - end - if @parent - #info "Looking for tags in %s" % @parent.type - @parent.tags.each { |tag| - if tag.nil? or tag == "" - Puppet.debug "parent returned tag %s" % tag.inspect - next - end - unless tmp.include?(tag) - tmp << tag - end - } - end - return tmp + # Used mainly for logging + def to_s + if self.name + return "%s[%s]" % [@type, @name] + else + return self.type.to_s end + end - # Used mainly for logging - def to_s - if @name - return "%s[%s]" % [@type, @name] + # Convert all of our objects as necessary. + def translate + ret = @children.collect do |child| + case child + when self.class + child.translate + when Puppet::Parser::Resource + child.to_trans else - return @type.to_s + devfail "Got %s for translation" % child.class end + end.reject { |o| o.nil? } + bucket = Puppet::TransBucket.new ret + unless self.type + devfail "A Scope with no type" end + if @type == "" + bucket.type = "main" + else + bucket.type = @type + end + if self.name + bucket.name = self.name + end + return bucket + end - # Convert our scope to a TransBucket. Everything in our @localobjecttable - # gets converted to either an evaluated definition, or a TransObject - def to_trans - results = [] - - # Set this on entry, just in case someone tries to get all weird - @translated = true - - @children.dup.each do |child| - if @@done.include?(child) - raise Puppet::DevError, "Already translated %s" % - child.object_id - else - @@done << child - end - #warning "Working on %s of type %s with id %s" % - # [child.type, child.class, child.object_id] - - # If it's a scope, then it can only be a subclass's scope, so - # convert it to a transbucket and store it in our results list - result = nil - case child - when Scope - result = child.to_trans - when Puppet::TransObject - # These objects can map to defined types or builtin types. - # Builtin types should be passed out as they are, but defined - # types need to be evaluated. We have to wait until this - # point so that subclass overrides can happen. - - # Wait until the last minute to set tags, although this - # probably should not matter - child.tags = self.tags - - # Add any defaults. - self.adddefaults(child) - - # Then make sure this child's tags are stored in the - # central table. This should maybe be in the evaluate - # methods, but, eh. - @topscope.addtags(child) - - # Now that all that is done, check to see what kind of object - # it is. - if objecttype = lookuptype(child.type) - # It's a defined type, so evaluate it. Retain whether - # the object is collectable. If the object is collectable, - # then it will store all of its contents into the - # @exportable table, rather than returning them. - result = objecttype.safeevaluate( - :name => child.name, - :type => child.type, - :arguments => child.to_hash, - :scope => self, - :collectable => child.collectable - ) - else - # If it's collectable, then store it. It will be - # stripped out in the interpreter using the collectstrip - # method. If we don't do this, then these objects - # don't get stored in the DB. - if child.collectable - exportobject(child) - end - result = child - end - # This is pretty hackish, but the collection has to actually - # be performed after all of the classes and definitions are - # evaluated, otherwise we won't catch objects that are exported - # in them. I think this will still be pretty limited in some - # cases, especially those where you are both exporting and - # collecting, but it's the best I can do for now. - when Puppet::Parser::AST::Collection - child.perform(self).each do |obj| - results << obj - end - else - raise Puppet::DevError, - "Puppet::Parse::Scope cannot handle objects of type %s" % - child.class - end - - # Skip nil objects or empty transbuckets - if result - unless result.is_a? Puppet::TransBucket and result.empty? - results << result - end - end - end - - # Get rid of any nil objects. - results.reject! { |child| - child.nil? - } + # Undefine a variable; only used for testing. + def unsetvar(var) + if @symtable.include?(var) + @symtable.delete(var) + end + end - # If we have a name and type, then make a TransBucket, which - # becomes a component. - # Else, just stack all of the objects into the current bucket. - if @type - bucket = Puppet::TransBucket.new + # Return an array of all of the unevaluated objects + def unevaluated + ary = @definedtable.find_all do |name, object| + ! object.builtin? and ! object.evaluated? + end.collect { |name, object| object } - if defined? @name and @name - bucket.name = @name - end - # it'd be nice not to have to do this... - results.each { |result| - #Puppet.warning "Result type is %s" % result.class - bucket.push(result) - } - bucket.type = @type - - if defined? @keyword - bucket.keyword = @keyword - end - #Puppet.debug( - # "TransBucket with name %s and type %s in scope %s" % - # [@name,@type,self.object_id] - #) - - # now find metaparams - @symtable.each { |var,value| - if Puppet::Type.metaparam?(var.intern) - #Puppet.debug("Adding metaparam %s" % var) - bucket.param(var,value) - else - #Puppet.debug("%s is not a metaparam" % var) - end - } - #Puppet.debug "Returning bucket %s from scope %s" % - # [bucket.name,self.object_id] - return bucket - else - Puppet.debug "typeless scope; just returning a list" - return results - end + if ary.empty? + return nil + else + return ary end + end - # Undefine a variable; only used for testing. - def unsetvar(var) - if @symtable.include?(var) - @symtable.delete(var) - end - end + protected - protected - - # This method abstracts recursive searching. It accepts the type - # of search being done and then either a literal key to search for or - # a Proc instance to do the searching. - def lookup(type,sub, usecontext = false) - table = @map[type] - if table.nil? - error = Puppet::ParseError.new( - "Could not retrieve %s table at level %s" % - [type,self.level] - ) - raise error - end + # This method abstracts recursive searching. It accepts the type + # of search being done and then either a literal key to search for or + # a Proc instance to do the searching. + def lookup(type,sub, usecontext = false) + table = @map[type] + if table.nil? + self.fail "Could not retrieve %s table at level %s" % + [type,self.level] + end - if sub.is_a?(Proc) and obj = sub.call(table) - return obj - elsif table.include?(sub) - return table[sub] - elsif ! @parent.nil? - # Context is used for retricting overrides. - if usecontext and self.context != @parent.context - return :undefined - else - return @parent.lookup(type,sub, usecontext) - end - else + if sub.is_a?(Proc) and obj = sub.call(table) + return obj + elsif table.include?(sub) + return table[sub] + elsif ! @parent.nil? + # Context is used for retricting overrides. + if usecontext and self.context != @parent.context return :undefined + else + return @parent.lookup(type,sub, usecontext) end + else + return :undefined end end end # $Id$ diff --git a/lib/puppet/parser/templatewrapper.rb b/lib/puppet/parser/templatewrapper.rb new file mode 100644 index 000000000..62b45852b --- /dev/null +++ b/lib/puppet/parser/templatewrapper.rb @@ -0,0 +1,58 @@ +# A simple wrapper for templates, so they don't have full access to +# the scope objects. +class Puppet::Parser::TemplateWrapper + attr_accessor :scope, :file + include Puppet::Util + Puppet::Util.logmethods(self) + + def initialize(scope, file) + @scope = scope + if file =~ /^#{File::SEPARATOR}/ + @file = file + else + @file = File.join(Puppet[:templatedir], file) + end + + unless FileTest.exists?(@file) + raise Puppet::ParseError, + "Could not find template %s" % file + end + + # We'll only ever not have an interpreter in testing, but, eh. + if @scope.interp + @scope.interp.newfile(@file) + end + end + + # Ruby treats variables like methods, so we can cheat here and + # trap missing vars like they were missing methods. + def method_missing(name, *args) + # We have to tell lookupvar to return :undefined to us when + # appropriate; otherwise it converts to "". + value = @scope.lookupvar(name.to_s, false) + if value != :undefined + return value + else + # Just throw an error immediately, instead of searching for + # other missingmethod things or whatever. + raise Puppet::ParseError, + "Could not find value for '%s'" % name + end + end + + def result + result = nil + benchmark(:debug, "Interpolated template #{@file}") do + template = ERB.new(File.read(@file), 0, "-") + result = template.result(binding) + end + + result + end + + def to_s + "template[%s]" % @file + end +end + +# $Id$ diff --git a/lib/puppet/rails/database.rb b/lib/puppet/rails/database.rb index caf87cbb8..8be05bd88 100644 --- a/lib/puppet/rails/database.rb +++ b/lib/puppet/rails/database.rb @@ -1,40 +1,45 @@ class Puppet::Rails::Database < ActiveRecord::Migration require 'sqlite3' def self.up if ActiveRecord::Migration.respond_to?(:verbose) ActiveRecord::Migration.verbose = false end - create_table :rails_objects do |table| - table.column :name, :string, :null => false - table.column :ptype, :string, :null => false + # 'type' cannot be a column name, apparently + create_table :rails_resources do |table| + table.column :title, :string, :null => false + table.column :restype, :string, :null => false table.column :tags, :string table.column :file, :string table.column :line, :integer table.column :host_id, :integer - table.column :collectable, :boolean + table.column :exported, :boolean end create_table :rails_parameters do |table| table.column :name, :string, :null => false table.column :value, :string, :null => false - table.column :rails_object_id, :integer + table.column :file, :string + table.column :line, :integer + table.column :rails_resource_id, :integer end create_table :hosts do |table| table.column :name, :string, :null => false table.column :ip, :string table.column :facts, :string table.column :connect, :date table.column :success, :date table.column :classes, :string end end def self.down - drop_table :rails_objects + drop_table :rails_resources drop_table :rails_parameters drop_table :hosts end end + +# $Id$ diff --git a/lib/puppet/rails/host.rb b/lib/puppet/rails/host.rb index 77123c871..ccda1af64 100644 --- a/lib/puppet/rails/host.rb +++ b/lib/puppet/rails/host.rb @@ -1,83 +1,87 @@ -require 'puppet/rails/rails_object' +require 'puppet/rails/rails_resource' #RailsObject = Puppet::Rails::RailsObject class Puppet::Rails::Host < ActiveRecord::Base serialize :facts, Hash serialize :classes, Array - has_many :rails_objects, :dependent => :delete_all + has_many :rails_resources, :dependent => :delete_all, + :include => :rails_parameters # If the host already exists, get rid of its objects def self.clean(host) if obj = self.find_by_name(host) obj.rails_objects.clear return obj else return nil end end # Store our host in the database. def self.store(hash) - name = hash[:host] || "localhost" - ip = hash[:ip] || "127.0.0.1" - facts = hash[:facts] || {} - objects = hash[:objects] + unless hash[:name] + raise ArgumentError, "You must specify the hostname for storage" + end - unless objects - raise ArgumentError, "You must pass objects" + args = {} + [:name, :facts, :classes].each do |param| + if hash[param] + args[param] = hash[param] + end end - hostargs = { - :name => name, - :ip => ip, - :facts => facts, - :classes => objects.classes - } + if hash[:facts].include?("ipaddress") + args[:ip] = hash[:facts]["ipaddress"] + end - objects = objects.flatten + unless hash[:resources] + raise ArgumentError, "You must pass resources" + end - host = nil - if host = clean(name) - [:name, :facts, :classes].each do |param| - unless host[param] == hostargs[param] - host[param] = hostargs[param] + if host = self.find_by_name(hash[:name]) + args.each do |param, value| + unless host[param] == args[param] + host[param] = args[param] end end - host.addobjects(objects) else - host = self.new(hostargs) do |hostobj| - hostobj.addobjects(objects) - end + # Create it anew + host = self.new(args) end + hash[:resources].each do |res| + res.store(host) + end - host.save + Puppet::Util.benchmark(:info, "Saved host to database") do + host.save + end return host end # Add all of our RailsObjects def addobjects(objects) objects.each do |tobj| params = {} tobj.each do |p,v| params[p] = v end args = {:ptype => tobj.type, :name => tobj.name} [:tags, :file, :line].each do |param| if val = tobj.send(param) args[param] = val end end robj = rails_objects.build(args) robj.addparams(params) if tobj.collectable robj.toggle(:collectable) end end end end # $Id$ diff --git a/lib/puppet/rails/rails_object.rb b/lib/puppet/rails/rails_object.rb deleted file mode 100644 index d1e58e453..000000000 --- a/lib/puppet/rails/rails_object.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'puppet' -require 'puppet/rails/rails_parameter' - -#RailsParameter = Puppet::Rails::RailsParameter -class Puppet::Rails::RailsObject < ActiveRecord::Base - has_many :rails_parameters, :dependent => :delete_all - serialize :tags, Array - - belongs_to :host - - # Add a set of parameters. - def addparams(params) - params.each do |pname, pvalue| - rails_parameters.build( - :name => pname, - :value => pvalue - ) - - #self.rails_parameters << pobj - end - end - - # Convert our object to a trans_object. Do not retain whether the object - # is collectable, though, since that would cause it to get stripped - # from the configuration. - def to_trans - obj = Puppet::TransObject.new(name(), ptype()) - - [:file, :line, :tags].each do |method| - if val = send(method) - obj.send(method.to_s + "=", val) - end - end - rails_parameters.each do |param| - obj[param.name] = param.value - end - - return obj - end -end - -# $Id$ diff --git a/lib/puppet/rails/rails_parameter.rb b/lib/puppet/rails/rails_parameter.rb index a1966e3dc..295662146 100644 --- a/lib/puppet/rails/rails_parameter.rb +++ b/lib/puppet/rails/rails_parameter.rb @@ -1,5 +1,13 @@ class Puppet::Rails::RailsParameter < ActiveRecord::Base - belongs_to :rails_objects + belongs_to :rails_resources + + def to_resourceparam(source) + hash = self.attributes + hash[:source] = source + hash.delete("rails_resource_id") + hash.delete("id") + Puppet::Parser::Resource::Param.new hash + end end # $Id$ diff --git a/lib/puppet/rails/rails_resource.rb b/lib/puppet/rails/rails_resource.rb new file mode 100644 index 000000000..caad1b460 --- /dev/null +++ b/lib/puppet/rails/rails_resource.rb @@ -0,0 +1,34 @@ +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 + 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/transportable.rb b/lib/puppet/transportable.rb index 90bc70fc2..4b43b9825 100644 --- a/lib/puppet/transportable.rb +++ b/lib/puppet/transportable.rb @@ -1,291 +1,293 @@ require 'puppet' require 'yaml' module Puppet # The transportable objects themselves. Basically just a hash with some # metadata and a few extra methods. I used to have the object actually # be a subclass of Hash, but I could never correctly dump them using # YAML. class TransObject include Enumerable attr_accessor :type, :name, :file, :line, :collectable, :collected attr_writer :tags %w{has_key? include? length delete empty? << [] []=}.each { |method| define_method(method) do |*args| @params.send(method, *args) end } def each @params.each { |p,v| yield p, v } end def initialize(name,type) @type = type @name = name @collectable = false @params = {} @tags = [] end def longname return [@type,@name].join('--') end def tags return @tags end def to_hash @params.dup end def to_s return "%s(%s) => %s" % [@type,@name,super] end def to_manifest "#{self.type.to_s} { \"#{self.name}\":\n%s\n}" % @params.collect { |p, v| if v.is_a? Array " #{p} => [\"#{v.join('","')}\"]" else " #{p} => \"#{v}\"" end }.join(",\n") end def to_yaml_properties instance_variables end def to_type(parent = nil) retobj = nil if typeklass = Puppet::Type.type(self.type) # FIXME This should really be done differently, but... if retobj = typeklass[self.name] self.each do |param, val| retobj[param] = val end else unless retobj = typeklass.create(self) return nil end end else raise Puppet::Error.new("Could not find object type %s" % self.type) end if parent parent.push retobj end return retobj end end # Just a linear container for objects. Behaves mostly like an array, except # that YAML will correctly dump them even with their instance variables. class TransBucket include Enumerable attr_accessor :name, :type, :file, :line, :classes, :keyword, :top %w{delete shift include? length empty? << []}.each { |method| define_method(method) do |*args| #Puppet.warning "Calling %s with %s" % [method, args.inspect] @children.send(method, *args) #Puppet.warning @params.inspect end } # Remove all collectable objects from our tree, since the client # should not see them. def collectstrip! @children.dup.each do |child| if child.is_a? self.class child.collectstrip! else if child.collectable and ! child.collected @children.delete(child) end end end end # Recursively yield everything. def delve(&block) @children.each do |obj| block.call(obj) if obj.is_a? self.class obj.delve(&block) else obj end end end def each @children.each { |c| yield c } end # Turn our heirarchy into a flat list def flatten @children.collect do |obj| if obj.is_a? Puppet::TransBucket obj.flatten else obj end end.flatten end - def initialize - @children = [] + def initialize(children = []) + @children = children end def push(*args) args.each { |arg| case arg when Puppet::TransBucket, Puppet::TransObject # nada else raise Puppet::DevError, "TransBuckets cannot handle objects of type %s" % arg.class end } @children += args end # Convert to a parseable manifest def to_manifest unless self.top unless defined? @keyword and @keyword raise Puppet::DevError, "No keyword; cannot convert to manifest" end end str = nil if self.top str = "%s" else str = "#{@keyword} #{@type} {\n%s\n}" end str % @children.collect { |child| child.to_manifest }.collect { |str| if self.top str else str.gsub(/^/, " ") # indent everything once end }.join("\n\n") # and throw in a blank line end def to_yaml_properties instance_variables end def to_type(parent = nil) # this container will contain the equivalent of all objects at # this level #container = Puppet::Component.new(:name => @name, :type => @type) #unless defined? @name # raise Puppet::DevError, "TransBuckets must have names" #end unless defined? @type Puppet.debug "TransBucket '%s' has no type" % @name end usetrans = true if usetrans tmpname = nil # Nodes have the same name and type if self.name tmpname = "%s[%s]" % [@type, self.name] else tmpname = @type end trans = TransObject.new(tmpname, :component) if defined? @parameters @parameters.each { |param,value| Puppet.debug "Defining %s on %s of type %s" % [param,@name,@type] trans[param] = value } else #Puppet.debug "%s[%s] has no parameters" % [@type, @name] end - container = Puppet.type(:component).create(trans) + container = Puppet::Type::Component.create(trans) else hash = { :name => self.name, :type => @type } if defined? @parameters @parameters.each { |param,value| Puppet.debug "Defining %s on %s of type %s" % [param,@name,@type] hash[param] = value } else #Puppet.debug "%s[%s] has no parameters" % [@type, @name] end #if parent # hash[:parent] = parent #end - container = Puppet.type(:component).create(hash) + container = Puppet::Type::Component.create(hash) end #Puppet.info container.inspect if parent parent.push container end # unless we successfully created the container, return an error unless container Puppet.warning "Got no container back" return nil end self.each { |child| # the fact that we descend here means that we are # always going to execute depth-first # which is _probably_ a good thing, but one never knows... unless child.is_a?(Puppet::TransBucket) or child.is_a?(Puppet::TransObject) raise Puppet::DevError, "TransBucket#to_type cannot handle objects of type %s" % child.class end # Now just call to_type on them with the container as a parent begin child.to_type(container) rescue => detail - # We don't do anything on failures, since we assume it's - # been logged elsewhere. + if Puppet[:trace] and ! detail.is_a?(Puppet::Error) + puts detail.backtrace + end + Puppet.err detail.to_s end } # at this point, no objects at are level are still Transportable # objects return container end def param(param,value) unless defined? @parameters @parameters = {} end @parameters[param] = value end end #------------------------------------------------------------ end # $Id$ diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 320aa83a0..55421eb4c 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,2678 +1,2651 @@ require 'puppet' require 'puppet/log' require 'puppet/element' require 'puppet/event' require 'puppet/metric' require 'puppet/type/state' require 'puppet/parameter' require 'puppet/util' require 'puppet/autoload' # see the bottom of the file for the rest of the inclusions module Puppet # The type is unknown class UnknownTypeError < Puppet::Error; end class UnknownProviderError < Puppet::Error; end class Type < Puppet::Element # Types (which map to elements in the languages) are entirely composed of # attribute value pairs. Generally, Puppet calls any of these things an # 'attribute', but these attributes always take one of three specific # forms: parameters, metaparams, or states. # In naming methods, I have tried to consistently name the method so # that it is clear whether it operates on all attributes (thus has 'attr' in # the method name, or whether it operates on a specific type of attributes. attr_accessor :children attr_reader :provider attr_accessor :file, :line attr_reader :tags, :parent attr_writer :implicit, :title def implicit? if defined? @implicit and @implicit return true else return false end end include Enumerable # class methods dealing with Type management public # the Type class attribute accessors class << self attr_reader :name, :states attr_accessor :providerloader attr_writer :defaultprovider include Enumerable, Puppet::Util::ClassGen end # iterate across all of the subclasses of Type def self.eachtype @types.each do |name, type| # Only consider types that have names #if ! type.parameters.empty? or ! type.validstates.empty? yield type #end end end # Create the 'ensure' class. This is a separate method so other types # can easily call it and create their own 'ensure' values. def self.ensurable(&block) if block_given? self.newstate(:ensure, :parent => Puppet::State::Ensure, &block) else self.newstate(:ensure, :parent => Puppet::State::Ensure) do self.defaultvalues end end end # Should we add the 'ensure' state to this class? def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. #ens = [:create, :destroy].inject { |set, method| ens = [:exists?, :create, :destroy].inject { |set, method| set &&= self.public_method_defined?(method) } #puts "%s ensurability: %s" % [self.name, ens] return ens end # all of the variables that must be initialized for each subclass def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @providers = Hash.new @defaults = {} unless defined? @parameters @parameters = [] end @validstates = {} @states = [] @parameters = [] @paramhash = {} @paramdoc = Hash.new { |hash,key| if key.is_a?(String) key = key.intern end if hash.include?(key) hash[key] else "Param Documentation for %s not found" % key end } unless defined? @doc @doc = "" end unless defined? @states @states = [] end end # Load all types. Only currently used for documentation. def self.loadall typeloader.loadall end # Do an on-demand plugin load def self.loadplugin(name) unless Puppet[:pluginpath].split(":").include?(Puppet[:plugindest]) Puppet.notice "Adding plugin destination %s to plugin search path" % Puppet[:plugindest] Puppet[:pluginpath] += ":" + Puppet[:plugindest] end Puppet[:pluginpath].split(":").each do |dir| file = ::File.join(dir, name.to_s + ".rb") if FileTest.exists?(file) begin load file Puppet.info "loaded %s" % file return true rescue LoadError => detail Puppet.info "Could not load plugin %s: %s" % [file, detail] return false end end end end # Define a new type. def self.newtype(name, parent = nil, &block) # First make sure we don't have a method sitting around name = symbolize(name) newmethod = "new#{name.to_s}" # Used for method manipulation. selfobj = metaclass() @types ||= {} if @types.include?(name) if self.respond_to?(newmethod) # Remove the old newmethod selfobj.send(:remove_method,newmethod) end end # Then create the class. klass = genclass(name, :parent => (parent || Puppet::Type), :overwrite => true, :hash => @types, &block ) # Now define a "new" method for convenience. if self.respond_to? newmethod # Refuse to overwrite existing methods like 'newparam' or 'newtype'. Puppet.warning "'new#{name.to_s}' method already exists; skipping" else selfobj.send(:define_method, newmethod) do |*args| klass.create(*args) end end # If they've got all the necessary methods defined and they haven't # already added the state, then do so now. if klass.ensurable? and ! klass.validstate?(:ensure) klass.ensurable end # Now set up autoload any providers that might exist for this type. klass.providerloader = Puppet::Autoload.new(klass, "puppet/provider/#{klass.name.to_s}" ) # We have to load everything so that we can figure out the default type. klass.providerloader.loadall() klass end # Return a Type instance by name. def self.type(name) @types ||= {} if name.is_a?(String) name = name.intern end unless @types.include? name if typeloader.load(name) unless @types.include? name Puppet.warning "Loaded puppet/type/#{name} but no class was created" end else # If we can't load it from there, try loading it as a plugin. loadplugin(name) end end @types[name] end def self.typeloader unless defined? @typeloader @typeloader = Puppet::Autoload.new(self, "puppet/type", :wrap => false ) end @typeloader end # class methods dealing with type instance management public # Create an alias. We keep these in a separate hash so that we don't encounter # the objects multiple times when iterating over them. def self.alias(name, obj) if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( "Cannot create alias %s: object already exists" % [name] ) end end if @aliases.include?(name) unless @aliases[name] == obj raise Puppet::Error.new( "Object %s already has alias %s" % [@aliases[name].name, name] ) end end @aliases[name] = obj end # retrieve a named instance of the current type def self.[](name) if @objects.has_key?(name) return @objects[name] elsif @aliases.has_key?(name) return @aliases[name] else return nil end end # add an instance by name to the class list of instances def self.[]=(name,object) newobj = nil if object.is_a?(Puppet::Type) newobj = object else raise Puppet::DevError, "must pass a Puppet::Type object" end if exobj = @objects.has_key?(name) and self.isomorphic? msg = "Object '%s[%s]' already exists" % [name, newobj.class.name] if exobj.file and exobj.line msg += ("in file %s at line %s" % [object.file, object.line]) end if object.file and object.line msg += ("and cannot be redefined in file %s at line %s" % [object.file, object.line]) end error = Puppet::Error.new(msg) else #Puppet.info("adding %s of type %s to class list" % # [name,object.class]) @objects[name] = newobj end end # remove all type instances; this is mostly only useful for testing def self.allclear Puppet::Event::Subscription.clear @types.each { |name, type| type.clear } end # remove all of the instances of a single type def self.clear if defined? @objects @objects.each do |name, obj| obj.remove(true) end @objects.clear end if defined? @aliases @aliases.clear end end # remove a specified object def self.delete(object) return unless defined? @objects if @objects.include?(object.title) @objects.delete(object.title) end if @aliases.include?(object.title) @aliases.delete(object.title) end end # iterate across each of the type's instances def self.each return unless defined? @objects @objects.each { |name,instance| yield instance } end # does the type have an object with the given name? def self.has_key?(name) return @objects.has_key?(name) end # Allow an outside party to specify the 'is' value for a state. The # arguments are an array because you can't use parens with 'is=' calls. # Most classes won't use this. def is=(ary) param, value = ary if param.is_a?(String) param = param.intern end if self.class.validstate?(param) unless @states.include?(param) self.newstate(param) end @states[param].is = value else self[param] = value end end # class and instance methods dealing with parameters and states public # Find the namevar def self.namevar unless defined? @namevar params = @parameters.find_all { |param| param.isnamevar? or param.name == :name } if params.length > 1 raise Puppet::DevError, "Found multiple namevars for %s" % self.name elsif params.length == 1 @namevar = params[0].name else raise Puppet::DevError, "No namevar for %s" % self.name end end @namevar end # Copy an existing class parameter. This allows other types to avoid # duplicating a parameter definition, and is mostly used by subclasses # of the File class. def self.copyparam(klass, name) param = klass.attrclass(name) unless param raise Puppet::DevError, "Class %s has no param %s" % [klass, name] end @parameters << param @parameters.each { |p| @paramhash[name] = p } if param.isnamevar? @namevar = param.name end end # Create a new metaparam. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newmetaparam(name, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = symbolize(name) param = genclass(name, :parent => Puppet::Parameter, :prefix => "MetaParam", :hash => @@metaparamhash, :array => @@metaparams, &block ) param.ismetaparameter return param end def self.eachmetaparam @@metaparams.each { |p| yield p.name } end # Find the default provider. def self.defaultprovider unless defined? @defaultprovider and @defaultprovider suitable = suitableprovider() # Find which providers are a default for this system. defaults = suitable.find_all { |provider| provider.default? } # If we don't have any default we use suitable providers defaults = suitable if defaults.empty? max = defaults.collect { |provider| provider.defaultnum }.max defaults = defaults.find_all { |provider| provider.defaultnum == max } retval = nil if defaults.length > 1 Puppet.warning( "Found multiple default providers for %s: %s; using %s" % [self.name, defaults.collect { |i| i.name.to_s }.join(", "), defaults[0].name] ) retval = defaults.shift elsif defaults.length == 1 retval = defaults.shift else raise Puppet::DevError, "Could not find a default provider for %s" % self.name end @defaultprovider = retval end return @defaultprovider end # Retrieve a provider by name. def self.provider(name) name = Puppet::Util.symbolize(name) # If we don't have it yet, try loading it. unless @providers.has_key?(name) @providerloader.load(name) end return @providers[name] end # Just list all of the providers. def self.providers @providers.keys end def self.validprovider?(name) name = Puppet::Util.symbolize(name) return (@providers.has_key?(name) && @providers[name].suitable?) end # Create a new provider of a type. This method must be called # directly on the type that it's implementing. def self.provide(name, options = {}, &block) name = Puppet::Util.symbolize(name) model = self parent = if pname = options[:parent] if pname.is_a? Class pname else if provider = self.provider(pname) provider else raise Puppet::DevError, "Could not find parent provider %s of %s" % [pname, name] end end else Puppet::Type::Provider end self.providify provider = genclass(name, :parent => parent, :hash => @providers, :prefix => "Provider", :block => block, :attributes => { :model => model } ) return provider end # Make sure we have a :provider parameter defined. Only gets called if there # are providers. def self.providify return if @paramhash.has_key? :provider model = self newparam(:provider) do desc "The specific backend for #{self.name.to_s} to use. You will seldom need to specify this -- Puppet will usually discover the appropriate provider for your platform." # This is so we can refer back to the type to get a list of # providers for documentation. class << self attr_accessor :parenttype end # We need to add documentation for each provider. def self.doc @doc + " Available providers are:\n\n" + parenttype().providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| "* **%s**: %s" % [i, parenttype().provider(i).doc] }.join("\n") end defaultto { @parent.class.defaultprovider.name } validate do |value| value = value[0] if value.is_a? Array if provider = @parent.class.provider(value) unless provider.suitable? raise ArgumentError, "Provider '%s' is not functional on this platform" % [value] end else raise ArgumentError, "Invalid %s provider '%s'" % [@parent.class.name, value] end end munge do |provider| provider = provider[0] if provider.is_a? Array if provider.is_a? String provider = provider.intern end @parent.provider = provider provider end end.parenttype = self end def self.unprovide(name) if @providers.has_key? name if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end @providers.delete(name) end end # Return an array of all of the suitable providers. def self.suitableprovider @providers.find_all { |name, provider| provider.suitable? }.collect { |name, provider| provider } end def provider=(name) if klass = self.class.provider(name) @provider = klass.new(self) else raise UnknownProviderError, "Could not find %s provider of %s" % [name, self.class.name] end end # Create a new parameter. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newparam(name, options = {}, &block) param = genclass(name, :parent => options[:parent] || Puppet::Parameter, :attributes => { :element => self }, :block => block, :prefix => "Parameter", :array => @parameters, :hash => @paramhash ) # These might be enabled later. # define_method(name) do # @parameters[name].value # end # # define_method(name.to_s + "=") do |value| # newparam(param, value) # end if param.isnamevar? @namevar = param.name end return param end # Create a new state. The first parameter must be the name of the state; # this is how users will refer to the state when creating new instances. # The second parameter is a hash of options; the options are: # * :parent: The parent class for the state. Defaults to Puppet::State. # * :retrieve: The method to call on the provider or @parent object (if # the provider is not set) to retrieve the current value. def self.newstate(name, options = {}, &block) name = symbolize(name) # This is here for types that might still have the old method of defining # a parent class. unless options.is_a? Hash raise Puppet::DevError, "Options must be a hash, not %s" % options.inspect end if @validstates.include?(name) raise Puppet::DevError, "Class %s already has a state named %s" % [self.name, name] end # We have to create our own, new block here because we want to define # an initial :retrieve method, if told to, and then eval the passed # block if available. s = genclass(name, :parent => options[:parent] || Puppet::State, :hash => @validstates ) do # If they've passed a retrieve method, then override the retrieve # method on the class. if options[:retrieve] define_method(:retrieve) do instance_variable_set( "@is", provider.send(options[:retrieve]) ) end end if block class_eval(&block) end end # If it's the 'ensure' state, always put it first. if name == :ensure @states.unshift s else @states << s end # define_method(name) do # @states[name].should # end # # define_method(name.to_s + "=") do |value| # newstate(name, :should => value) # end return s end # Specify a block for generating a list of objects to autorequire. This # makes it so that you don't have to manually specify things that you clearly # require. def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Yield each of those autorequires in turn, yo. def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Return the parameter names def self.parameters return [] unless defined? @parameters @parameters.collect { |klass| klass.name } end # Find the metaparameter class associated with a given metaparameter name. def self.metaparamclass(name) @@metaparamhash[symbolize(name)] end # Find the parameter class associated with a given parameter name. def self.paramclass(name) @paramhash[name] end # Find the class associated with any given attribute. def self.attrclass(name) @attrclasses ||= {} # We cache the value, since this method gets called such a huge number # of times (as in, hundreds of thousands in a given run). unless @attrclasses.include?(name) @attrclasses[name] = case self.attrtype(name) when :state: @validstates[name] when :meta: @@metaparamhash[name] when :param: @paramhash[name] end end @attrclasses[name] end def self.to_s if defined? @name "Puppet::Type::" + @name.to_s.capitalize else super end end # Create a block to validate that our object is set up entirely. This will # be run before the object is operated on. def self.validate(&block) define_method(:validate, &block) #@validate = block end # does the name reflect a valid state? def self.validstate?(name) name = name.intern if name.is_a? String if @validstates.include?(name) return @validstates[name] else return false end end # Return the list of validstates def self.validstates return {} unless defined? @states return @validstates.keys end # Return the state class associated with a name def self.statebyname(name) @validstates[name] end # does the name reflect a valid parameter? def self.validparameter?(name) unless defined? @parameters raise Puppet::DevError, "Class %s has not defined parameters" % self end if @paramhash.include?(name) or @@metaparamhash.include?(name) return true else return false end end # What type of parameter are we dealing with? Cache the results, because # this method gets called so many times. def self.attrtype(attr) @attrtypes ||= {} unless @attrtypes.include?(attr) @attrtypes[attr] = case when @validstates.include?(attr): :state when @@metaparamhash.include?(attr): :meta when @paramhash.include?(attr): :param else raise Puppet::DevError, "Invalid attribute '%s' for class '%s'" % [attr, self.name] end end @attrtypes[attr] end # All parameters, in the appropriate order. The namevar comes first, # then the states, then the params and metaparams in the order they # were specified in the files. def self.allattrs # now get all of the arguments, in a specific order # Cache this, since it gets called so many times namevar = self.namevar order = [namevar] order << [self.states.collect { |state| state.name }, self.parameters, self.metaparams].flatten.reject { |param| # we don't want our namevar in there multiple times param == namevar } order.flatten! return order end # A similar function but one that yields the name, type, and class. # This is mainly so that setdefaults doesn't call quite so many functions. def self.eachattr(*ary) # now get all of the arguments, in a specific order # Cache this, since it gets called so many times if ary.empty? ary = nil end self.states.each { |state| yield(state, :state) if ary.nil? or ary.include?(state.name) } @parameters.each { |param| yield(param, :param) if ary.nil? or ary.include?(param.name) } @@metaparams.each { |param| yield(param, :meta) if ary.nil? or ary.include?(param.name) } end def self.validattr?(name) - if name.is_a?(String) - name = name.intern - end + name = symbolize(name) if self.validstate?(name) or self.validparameter?(name) or self.metaparam?(name) return true else return false end end # abstract accessing parameters and states, and normalize # access to always be symbols, not strings # This returns a value, not an object. It returns the 'is' # value, but you can also specifically return 'is' and 'should' # values using 'object.is(:state)' or 'object.should(:state)'. def [](name) if name.is_a?(String) name = name.intern end if name == :name name = self.class.namevar end case self.class.attrtype(name) when :state if @states.include?(name) return @states[name].is else return nil end when :meta if @metaparams.include?(name) return @metaparams[name].value else if default = self.class.metaparamclass(name).default return default else return nil end end when :param if @parameters.include?(name) return @parameters[name].value else if default = self.class.paramclass(name).default return default else return nil end end else raise TypeError.new("Invalid parameter %s(%s)" % [name, name.inspect]) end end # Abstract setting parameters and states, and normalize # access to always be symbols, not strings. This sets the 'should' # value on states, and otherwise just sets the appropriate parameter. def []=(name,value) if name.is_a?(String) name = name.intern end if name == :name name = self.class.namevar end if value.nil? raise Puppet::Error.new("Got nil value for %s" % name) end case self.class.attrtype(name) when :state if value.is_a?(Puppet::State) self.debug "'%s' got handed a state for '%s'" % [self,name] @states[name] = value else if @states.include?(name) @states[name].should = value else # newstate returns true if it successfully created the state, # false otherwise; I just don't know what to do with that # fact. unless newstate(name, :should => value) #self.info "%s failed" % name end end end when :meta self.newmetaparam(self.class.metaparamclass(name), value) when :param klass = self.class.attrclass(name) # if they've got a method to handle the parameter, then do it that way self.newparam(klass, value) else raise Puppet::Error, "Invalid parameter %s" % [name] end end # remove a state from the object; useful in testing or in cleanup # when an error has been encountered def delete(attr) case attr when Puppet::Type if @children.include?(attr) @children.delete(attr) end else if @states.has_key?(attr) @states.delete(attr) elsif @parameters.has_key?(attr) @parameters.delete(attr) elsif @metaparams.has_key?(attr) @metaparams.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end end # iterate across all children, and then iterate across states # we do children first so we're sure that all dependent objects # are checked first # we ignore parameters here, because they only modify how work gets # done, they don't ever actually result in work specifically def each # we want to return the states in the order that each type # specifies it, because it may (as in the case of File#create) # be important if self.class.depthfirst? @children.each { |child| yield child } end self.eachstate { |state| yield state } unless self.class.depthfirst? @children.each { |child| yield child } end end # Recurse deeply through the tree, but only yield types, not states. def delve(&block) self.each do |obj| if obj.is_a? Puppet::Type obj.delve(&block) end end block.call(self) end # iterate across the existing states def eachstate # states() is a private method states().each { |state| yield state } end - def devfail(msg) - self.fail(Puppet::DevError, msg) - end - - # Throw an error, defaulting to a Puppet::Error - def fail(*args) - type = nil - if args[0].is_a?(Class) - type = args.shift - else - type = Puppet::Error - end - - error = type.new(args.join(" ")) - - if defined? @line and @line - error.line = @line - end - - if defined? @file and @file - error.file = @file - end - - raise error - end - # retrieve the 'is' value for a specified state def is(state) if @states.include?(state) return @states[state].is else return nil end end # retrieve the 'should' value for a specified state def should(state) if @states.include?(state) return @states[state].should else return nil end end # create a log at specified level def log(msg) Puppet::Log.create( :level => @metaparams[:loglevel].value, :message => msg, :source => self ) end # is the instance a managed instance? A 'yes' here means that # the instance was created from the language, vs. being created # in order resolve other questions, such as finding a package # in a list def managed? # Once an object is managed, it always stays managed; but an object # that is listed as unmanaged might become managed later in the process, # so we have to check that every time if defined? @managed and @managed return @managed else @managed = false states.each { |state| if state.should and ! state.class.unmanaged @managed = true break end } return @managed end end # Create a new parameter. def newparam(klass, value = nil) newattr(:param, klass, value) end # Create a new parameter or metaparameter. We'll leave the calling # method to store it appropriately. def newmetaparam(klass, value = nil) newattr(:meta, klass, value) end # The base function that the others wrap. def newattr(type, klass, value = nil) # This should probably be a bit, um, different, but... if type == :state return newstate(klass) end param = klass.new param.parent = self unless value.nil? param.value = value end case type when :meta @metaparams[klass.name] = param when :param @parameters[klass.name] = param else self.devfail("Invalid param type %s" % type) end return param end # create a new state def newstate(name, hash = {}) stateklass = nil if name.is_a?(Class) stateklass = name name = stateklass.name else stateklass = self.class.validstate?(name) unless stateklass self.fail("Invalid state %s" % name) end end if @states.include?(name) hash.each { |var,value| @states[name].send(var.to_s + "=", value) } else #Puppet.warning "Creating state %s for %s" % # [stateklass.name,self.name] begin hash[:parent] = self # make sure the state doesn't have any errors newstate = stateklass.new(hash) @states[name] = newstate return newstate rescue Puppet::Error => detail # the state failed, so just ignore it self.warning "State %s failed: %s" % [name, detail] return false rescue Puppet::DevError => detail # the state failed, so just ignore it self.err "State %s failed: %s" % [name, detail] return false rescue => detail # the state failed, so just ignore it self.err "State %s failed: %s (%s)" % [name, detail, detail.class] return false end end end # return the value of a parameter def parameter(name) unless name.is_a? Symbol name = name.intern end return @parameters[name].value end def parent=(parent) if self.parentof?(parent) devfail "%s[%s] is already the parent of %s[%s]" % [self.class.name, self.title, parent.class.name, parent.title] end @parent = parent end # Add a hook for testing for recursion. def parentof?(child) if (self == child) debug "parent is equal to child" return true elsif defined? @parent and @parent.parentof?(child) debug "My parent is parent of child" return true elsif @children.include?(child) debug "child is already in children array" return true else return false end end def push(*childs) unless defined? @children @children = [] end childs.each { |child| # Make sure we don't have any loops here. if parentof?(child) devfail "Already the parent of %s[%s]" % [child.class.name, child.title] end unless child.is_a?(Puppet::Element) self.debug "Got object of type %s" % child.class self.devfail( "Containers can only contain Puppet::Elements, not %s" % child.class ) end @children.push(child) child.parent = self } end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # Our children remove themselves from our @children array (else the object # we called this on at the top would not be removed), so we duplicate the # array and iterate over that. If we don't do this, only half of the # objects get removed. @children.dup.each { |child| child.remove(rmdeps) } @children.clear # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. [@states, @parameters, @metaparams].each do |hash| hash.each do |name, obj| obj.remove end hash.clear end if rmdeps Puppet::Event::Subscription.dependencies(self).each { |dep| #info "Deleting dependency %s" % dep #begin # self.unsubscribe(dep) #rescue # # ignore failed unsubscribes #end dep.delete } Puppet::Event::Subscription.subscribers(self).each { |dep| #info "Unsubscribing from %s" % dep begin dep.unsubscribe(self) rescue # ignore failed unsubscribes end } end self.class.delete(self) if defined? @parent and @parent @parent.delete(self) @parent = nil end # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end # Is the named state defined? def statedefined?(name) unless name.is_a? Symbol name = name.intern end return @states.include?(name) end # return an actual type by name; to return the value, use 'inst[name]' # FIXME this method should go away def state(name) unless name.is_a? Symbol name = name.intern end return @states[name] end private def states #debug "%s has %s states" % [self,@states.length] tmpstates = [] self.class.states.each { |state| if @states.include?(state.name) tmpstates.push(@states[state.name]) end } unless tmpstates.length == @states.length self.devfail( "Something went very wrong with tmpstates creation" ) end return tmpstates end # instance methods related to instance intrinsics # e.g., initialize() and name() public # Force users to call this, so that we can merge objects if # necessary. FIXME This method should be responsible for most of the # error handling. def self.create(args) # Don't modify the original hash; instead, create a duplicate and modify it. # We have to dup and use the ! so that it stays a TransObject if it is # one. hash = args.dup symbolizehash!(hash) # If we're the base class, then pass the info on appropriately if self == Puppet::Type type = nil if hash.is_a? TransObject type = hash.type else # If we're using the type to determine object type, then delete it if type = hash[:type] hash.delete(:type) end end if type if typeklass = self.type(type) return typeklass.create(hash) else raise Puppet::Error, "Unknown type %s" % type end else raise Puppet::Error, "No type found for %s" % hash.inspect end end # Handle this new object being implicit implicit = hash[:implicit] || false if hash.include?(:implicit) hash.delete(:implicit) end name = nil unless hash.is_a? TransObject hash = self.hash2trans(hash) end # XXX This will have to change when transobjects change to using titles title = hash.name #Puppet.debug "Creating %s[%s]" % [self.name, title] # if the object already exists if self.isomorphic? and retobj = self[title] # if only one of our objects is implicit, then it's easy to see # who wins -- the non-implicit one. if retobj.implicit? and ! implicit Puppet.notice "Removing implicit %s" % retobj.title # Remove all of the objects, but do not remove their subscriptions. retobj.remove(false) # now pass through and create the new object elsif implicit Puppet.notice "Ignoring implicit %s" % title return retobj else # If only one of the objects is being managed, then merge them if retobj.managed? raise Puppet::Error, "%s '%s' is already being managed" % [self.name, title] else retobj.merge(hash) return retobj end # We will probably want to support merging of some kind in # the future, but for now, just throw an error. #retobj.merge(hash) #return retobj end end # create it anew # if there's a failure, destroy the object if it got that far, but raise # the error. begin obj = new(hash) rescue => detail Puppet.err "Could not create %s: %s" % [title, detail.to_s] if obj obj.remove(true) elsif obj = self[title] obj.remove(true) end raise end if implicit obj.implicit = true end # Store the object by title self[obj.title] = obj return obj end # Convert a hash to a TransObject. def self.hash2trans(hash) title = nil if hash.include? :title title = hash[:title] hash.delete(:title) elsif hash.include? self.namevar title = hash[self.namevar] hash.delete(self.namevar) if hash.include? :name raise ArgumentError, "Cannot provide both name and %s to %s" % [self.namevar, self.name] end elsif hash[:name] title = hash[:name] hash.delete :name end unless title raise Puppet::Error, "You must specify a title for objects of type %s" % self.to_s end if hash.include? :type unless self.validattr? :type hash.delete :type end end # okay, now make a transobject out of hash begin trans = TransObject.new(title, self.name.to_s) hash.each { |param, value| trans[param] = value } rescue => detail raise Puppet::Error, "Could not create %s: %s" % [name, detail] end return trans end def self.implicitcreate(hash) unless hash.include?(:implicit) hash[:implicit] = true end if obj = self.create(hash) obj.implicit = true return obj else return nil end end # Is this type's name isomorphic with the object? That is, if the # name conflicts, does it necessarily mean that the objects conflict? # Defaults to true. def self.isomorphic? if defined? @isomorphic return @isomorphic else return true end end # and then make 'new' private class << self private :new end def initvars @children = [] @evalcount = 0 @tags = [] # callbacks are per object and event @callbacks = Hash.new { |chash, key| chash[key] = {} } # states and parameters are treated equivalently from the outside: # as name-value pairs (using [] and []=) # internally, however, parameters are merely a hash, while states # point to State objects # further, the lists of valid states and parameters are defined # at the class level unless defined? @states @states = Hash.new(false) end unless defined? @parameters @parameters = Hash.new(false) end unless defined? @metaparams @metaparams = Hash.new(false) end # set defalts @noop = false # keeping stats for the total number of changes, and how many were # completely sync'ed # this isn't really sufficient either, because it adds lots of special # cases such as failed changes # it also doesn't distinguish between changes from the current transaction # vs. changes over the process lifetime @totalchanges = 0 @syncedchanges = 0 @failedchanges = 0 @inited = true end # initialize the type instance def initialize(hash) unless defined? @inited self.initvars end namevar = self.class.namevar orighash = hash # If we got passed a transportable object, we just pull a bunch of info # directly from it. This is the main object instantiation mechanism. if hash.is_a?(Puppet::TransObject) #self[:name] = hash[:name] [:file, :line, :tags].each { |getter| if hash.respond_to?(getter) setter = getter.to_s + "=" if val = hash.send(getter) self.send(setter, val) end end } # XXX This will need to change when transobjects change to titles. @title = hash.name hash = hash.to_hash elsif hash[:title] # XXX This should never happen @title = hash[:title] hash.delete(:title) end # Before anything else, set our parent if it was included if hash.include?(:parent) @parent = hash[:parent] hash.delete(:parent) end # Munge up the namevar stuff so we only have one value. hash = self.argclean(hash) # If we've got both a title via some other mechanism, set it as an alias. # if defined? @title and @title and ! hash[:name] # if aliases = hash[:alias] # aliases = [aliases] unless aliases.is_a? Array # aliases << @title # hash[:alias] = aliases # else # hash[:alias] = @title # end # end # Let's do the name first, because some things need to happen once # we have the name but before anything else attrs = self.class.allattrs if hash.include?(namevar) #self.send(namevar.to_s + "=", hash[namevar]) self[namevar] = hash[namevar] hash.delete(namevar) if attrs.include?(namevar) attrs.delete(namevar) else self.devfail "My namevar isn\'t a valid attribute...?" end else self.devfail "I was not passed a namevar" end # If the name and title differ, set up an alias if self.name != self.title if obj = self.class[self.name] if self.class.isomorphic? raise Puppet::Error, "%s already exists with name %s" % [obj.title, self.name] end else self.class.alias(self.name, self) end end # The information to cache to disk. We have to do this after # the name is set because it uses the name and/or path, but before # everything else is set because the states need to be able to # retrieve their stored info. #@cache = Puppet::Storage.cache(self) # This is all of our attributes except the namevar. attrs.each { |attr| if hash.include?(attr) begin self[attr] = hash[attr] rescue ArgumentError, Puppet::Error, TypeError raise rescue => detail self.devfail( "Could not set %s on %s: %s" % [attr, self.class.name, detail] ) end hash.delete attr end } # While this could theoretically be set after all of the objects are # created, it seems to make more sense to set them immediately. self.setdefaults if hash.length > 0 self.debug hash.inspect self.fail("Class %s does not accept argument(s) %s" % [self.class.name, hash.keys.join(" ")]) end if self.respond_to?(:validate) self.validate end end # Figure out of there are any objects we can automatically add as # dependencies. def autorequire self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. next unless typeobj = Puppet.type(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) unless list.is_a?(Array) list = [list] end # Collect the current prereqs list.each { |dep| obj = nil # Support them passing objects directly, to save some effort. if dep.is_a? Puppet::Type type = dep.class.name obj = dep # Now change our dependency to just the string, instead of # the object itself. dep = dep.title else # Skip autorequires that we aren't managing unless obj = typeobj[dep] next end end # Skip autorequires that we already require next if self.requires?(obj) debug "Autorequiring %s %s" % [obj.class.name, obj.title] self[:require] = [type, dep] } #self.info reqs.inspect #self[:require] = reqs } end # Set up all of our autorequires. def finish self.autorequire # Scheduling has to be done when the whole config is instantiated, so # that file order doesn't matter in finding them. self.schedule end # Return a cached value def cached(name) Puppet::Storage.cache(self)[name] #@cache[name] ||= nil end # Cache a value def cache(name, value) Puppet::Storage.cache(self)[name] = value #@cache[name] = value end # Look up the schedule and set it appropriately. This is done after # the instantiation phase, so that the schedule can be anywhere in the # file. def schedule # If we've already set the schedule, then just move on return if self[:schedule].is_a?(Puppet.type(:schedule)) return unless self[:schedule] # Schedules don't need to be scheduled #return if self.is_a?(Puppet.type(:schedule)) # Nor do components #return if self.is_a?(Puppet.type(:component)) if sched = Puppet.type(:schedule)[self[:schedule]] self[:schedule] = sched else self.fail "Could not find schedule %s" % self[:schedule] end end # Check whether we are scheduled to run right now or not. def scheduled? return true if Puppet[:ignoreschedules] return true unless schedule = self[:schedule] # We use 'checked' here instead of 'synced' because otherwise we'll # end up checking most elements most times, because they will generally # have been synced a long time ago (e.g., a file only gets updated # once a month on the server and its schedule is daily; the last sync time # will have been a month ago, so we'd end up checking every run). return schedule.match?(self.cached(:checked).to_i) end # Add a new tag. def tag(tag) tag = tag.intern if tag.is_a? String unless @tags.include? tag @tags << tag end end # Define the initial list of tags. def tags=(list) list = [list] unless list.is_a? Array @tags = list.collect do |t| case t when String: t.intern when Symbol: t else self.warning "Ignoring tag %s of type %s" % [tag.inspect, tag.class] end end @tags << self.class.name unless @tags.include?(self.class.name) end # Figure out of any of the specified tags apply to this object. This is an # OR operation. def tagged?(tags) tags = [tags] unless tags.is_a? Array tags = tags.collect { |t| t.intern } return tags.find { |tag| @tags.include? tag } end # Is the specified parameter set? def attrset?(type, attr) case type when :state: return @states.include?(attr) when :param: return @parameters.include?(attr) when :meta: return @metaparams.include?(attr) else self.devfail "Invalid set type %s" % [type] end end # def set(name, value) # send(name.to_s + "=", value) # end # # def get(name) # send(name) # end # For any parameters or states that have defaults and have not yet been # set, set them now. def setdefaults(*ary) self.class.eachattr(*ary) { |klass, type| # not many attributes will have defaults defined, so we short-circuit # those away next unless klass.method_defined?(:default) next if self.attrset?(type, klass.name) obj = self.newattr(type, klass) value = obj.default unless value.nil? #self.debug "defaulting %s to %s" % [obj.name, obj.default] obj.value = value else #self.debug "No default for %s" % obj.name # "obj" is a Parameter. self.delete(obj.name) end } end # Merge new information with an existing object, checking for conflicts # and such. This allows for two specifications of the same object and # the same values, but it's pretty limited right now. The result of merging # states is very different from the result of merging parameters or metaparams. # This is currently unused. def merge(hash) hash.each { |param, value| if param.is_a?(String) param = param.intern end # Of course names are the same, duh. next if param == :name or param == self.class.namevar unless value.is_a?(Array) value = [value] end if @states.include?(param) and oldvals = @states[param].shouldorig unless oldvals.is_a?(Array) oldvals = [oldvals] end # If the values are exactly the same, order and everything, # then it's okay. if oldvals == value return true end # take the intersection newvals = oldvals & value if newvals.empty? self.fail "No common values for %s on %s(%s)" % [param, self.class.name, self.title] elsif newvals.length > 1 self.fail "Too many values for %s on %s(%s)" % [param, self.class.name, self.title] else self.debug "Reduced old values %s and new values %s to %s" % [oldvals.inspect, value.inspect, newvals.inspect] @states[param].should = newvals #self.should = newvals return true end else self[param] = value end } # Set the defaults again, just in case. self.setdefaults end # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name return self[:name] # unless defined? @name and @name # namevar = self.class.namevar # if self.class.validparameter?(namevar) # @name = self[:name] # elsif self.class.validstate?(namevar) # @name = self.should(namevar) # else # self.devfail "Could not find namevar %s for %s" % # [namevar, self.class.name] # end # end # # unless @name # self.devfail "Could not find namevar '%s' for %s" % # [self.class.namevar, self.class.name] # end # # return @name end # Retrieve the title of an object. If no title was set separately, # then use the object's name. def title unless defined? @title and @title namevar = self.class.namevar if self.class.validparameter?(namevar) @title = self[:name] elsif self.class.validstate?(namevar) @title = self.should(namevar) else self.devfail "Could not find namevar %s for %s" % [namevar, self.class.name] end end return @title end # fix any namevar => param translations def argclean(oldhash) # This duplication is here because it might be a transobject. hash = oldhash.dup.to_hash if hash.include?(:parent) hash.delete(:parent) end namevar = self.class.namevar # Do a simple translation for those cases where they've passed :name # but that's not our namevar if hash.include? :name and namevar != :name if hash.include? namevar raise ArgumentError, "Cannot provide both name and %s" % namevar end hash[namevar] = hash[:name] hash.delete(:name) end # Make sure we have a name, one way or another unless hash.include? namevar if defined? @title and @title hash[namevar] = @title else raise Puppet::Error, "Was not passed a namevar or title" end end return hash end # retrieve the current value of all contained states def retrieve # it's important to use the method here, as it follows the order # in which they're defined in the object states().each { |state| state.retrieve } end # convert to a string def to_s self.title end # Convert to a transportable object def to_trans # Collect all of the "is" values retrieve() trans = TransObject.new(self.title, self.class.name) states().each do |state| trans[state.name] = state.is end @parameters.each do |name, param| # Avoid adding each instance name as both the name and the namevar next if param.class.isnamevar? and param.value == self.title trans[name] = param.value end @metaparams.each do |name, param| trans[name] = param.value end trans.tags = self.tags # FIXME I'm currently ignoring 'parent' and 'path' return trans end # instance methods dealing with actually doing work public # this is a retarded hack method to get around the difference between # component children and file children def self.depthfirst? if defined? @depthfirst return @depthfirst else return false end end # Retrieve the changes associated with all of the states. def statechanges # If we are changing the existence of the object, then none of # the other states matter. changes = [] if @states.include?(:ensure) and ! @states[:ensure].insync? #self.info "ensuring %s from %s" % # [@states[:ensure].should, @states[:ensure].is] changes = [Puppet::StateChange.new(@states[:ensure])] # Else, if the 'ensure' state is correctly absent, then do # nothing elsif @states.include?(:ensure) and @states[:ensure].is == :absent #self.info "Object is correctly absent" return [] else #if @states.include?(:ensure) # self.info "ensure: Is: %s, Should: %s" % # [@states[:ensure].is, @states[:ensure].should] #else # self.info "no ensure state" #end changes = states().find_all { |state| ! state.insync? }.collect { |state| Puppet::StateChange.new(state) } end if Puppet[:debug] and changes.length > 0 self.debug("Changing " + changes.collect { |ch| ch.state.name }.join(",") ) end changes end # this method is responsible for collecting state changes # we always descend into the children before we evaluate our current # states # this returns any changes resulting from testing, thus 'collect' # rather than 'each' def evaluate now = Time.now #Puppet.err "Evaluating %s" % self.path.join(":") unless defined? @evalcount self.err "No evalcount defined on '%s' of type '%s'" % [self.title,self.class] @evalcount = 0 end @evalcount += 1 changes = [] # this only operates on states, not states + children # it's important that we call retrieve() on the type instance, # not directly on the state, because it allows the type to override # the method, like pfile does self.retrieve # states() is a private method, returning an ordered list unless self.class.depthfirst? changes += statechanges() end changes << @children.collect { |child| ch = child.evaluate child.cache(:checked, now) ch } if self.class.depthfirst? changes += statechanges() end changes.flatten! # now record how many changes we've resulted in if changes.length > 0 self.debug "%s change(s)" % [changes.length] end self.cache(:checked, now) return changes.flatten end # if all contained objects are in sync, then we're in sync # FIXME I don't think this is used on the type instances any more, # it's really only used for testing def insync? insync = true if state = @states[:ensure] if state.insync? and state.should == :absent return true end end states.each { |state| unless state.insync? state.debug("Not in sync: %s vs %s" % [state.is.inspect, state.should.inspect]) insync = false #else # state.debug("In sync") end } #self.debug("%s sync status is %s" % [self,insync]) return insync end # Meta-parameter methods: These methods deal with the results # of specifying metaparameters def self.metaparams @@metaparams.collect { |param| param.name } end # Is the parameter in question a meta-parameter? def self.metaparam?(param) + param = symbolize(param) @@metaparamhash.include?(param) end # Subscription and relationship methods #def addcallback(object, event, method) # @callbacks[object][event] = method #end # Build the dependencies associated with an individual object. def builddepends # Handle the requires if self[:require] self.handledepends(self[:require], :NONE, nil, true) end # And the subscriptions if self[:subscribe] self.handledepends(self[:subscribe], :ALL_EVENTS, :refresh, true) end if self[:notify] self.handledepends(self[:notify], :ALL_EVENTS, :refresh, false) end if self[:before] self.handledepends(self[:before], :NONE, nil, false) end end # return all objects that we depend on def eachdependency Puppet::Event::Subscription.dependencies(self).each { |dep| yield dep.source } end # return all objects subscribed to the current object def eachsubscriber Puppet::Event::Subscription.subscribers(self).each { |sub| yield sub.target } end def handledepends(requires, event, method, up) # Requires are specified in the form of [type, name], so they're always # an array. But we want them to be an array of arrays. unless requires[0].is_a?(Array) requires = [requires] end requires.each { |rname| # we just have a name and a type, and we need to convert it # to an object... type = nil object = nil tname = rname[0] unless type = Puppet::Type.type(tname) self.fail "Could not find type %s" % tname.inspect end name = rname[1] unless object = type[name] self.fail "Could not retrieve object '%s' of type '%s'" % [name,type] end self.debug("subscribes to %s" % [object]) # Are we requiring them, or vice versa? source = target = nil if up source = object target = self else source = self target = object end # ok, both sides of the connection store some information # we store the method to call when a given subscription is # triggered, but the source object decides whether subargs = { :event => event, :source => source, :target => target } if method and target.respond_to?(method) subargs[:callback] = method end Puppet::Event::Subscription.new(subargs) } end def requires?(object) req = false self.eachdependency { |dep| if dep == object req = true break end } return req end def subscribe(hash) hash[:source] = self Puppet::Event::Subscription.new(hash) # add to the correct area #@subscriptions.push sub end def subscribesto?(object) sub = false self.eachsubscriber { |o| if o == object sub = true break end } return sub end # Unsubscribe from a given object, possibly with a specific event. def unsubscribe(object, event = nil) Puppet::Event::Subscription.dependencies(self).find_all { |sub| if event sub.match?(event) else sub.source == object end }.each { |sub| sub.delete } end # we've received an event # we only support local events right now, so we can pass actual # objects around, including the transaction object # the assumption here is that container objects will pass received # methods on to contained objects # i.e., we don't trigger our children, our refresh() method calls # refresh() on our children def trigger(event, source) trans = event.transaction if @callbacks.include?(source) [:ALL_EVENTS, event.event].each { |eventname| if method = @callbacks[source][eventname] if trans.triggered?(self, method) > 0 next end if self.respond_to?(method) self.send(method) end trans.triggered(self, method) end } end end # Documentation methods def self.paramdoc(param) @paramhash[param].doc end def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc end # Add all of the meta parameters. #newmetaparam(:onerror) do # desc "How to handle errors -- roll back innermost # transaction, roll back entire transaction, ignore, etc. Currently # non-functional." #end newmetaparam(:noop) do desc "Boolean flag indicating whether work should actually be done. *true*/**false**" munge do |noop| if noop == "true" or noop == true return true elsif noop == "false" or noop == false return false else self.fail("Invalid noop value '%s'" % noop) end end end newmetaparam(:schedule) do desc "On what schedule the object should be managed. You must create a schedule object, and then reference the name of that object to use that for your schedule: schedule { daily: period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => daily } The creation of the schedule object does not need to appear in the configuration before objects that use it." munge do |name| if schedule = Puppet.type(:schedule)[name] return schedule else return name end end end newmetaparam(:check) do desc "States which should have their values retrieved but which should not actually be modified. This is currently used internally, but will eventually be used for querying, so that you could specify that you wanted to check the install state of all packages, and then query the Puppet client daemon to get reports on all packages." munge do |args| # If they've specified all, collect all known states if args == :all args = @parent.class.states.collect do |state| state.name end end unless args.is_a?(Array) args = [args] end unless defined? @parent self.devfail "No parent for %s, %s?" % [self.class, self.name] end args.each { |state| unless state.is_a?(Symbol) state = state.intern end next if @parent.statedefined?(state) stateklass = @parent.class.validstate?(state) unless stateklass raise Puppet::Error, "%s is not a valid attribute for %s" % [state, self.class.name] end next unless stateklass.checkable? @parent.newstate(state) } end end # For each object we require, subscribe to all events that it generates. We # might reduce the level of subscription eventually, but for now... newmetaparam(:require) do desc "One or more objects that this object depends on. This is used purely for guaranteeing that changes to required objects happen before the dependent object. For instance: # Create the destination directory before you copy things down file { \"/usr/local/scripts\": ensure => directory } file { \"/usr/local/scripts/myscript\": source => \"puppet://server/module/myscript\", mode => 755, require => file[\"/usr/local/scripts\"] } Note that Puppet will autorequire everything that it can, and there are hooks in place so that it's easy for elements to add new ways to autorequire objects, so if you think Puppet could be smarter here, let us know. In fact, the above code was redundant -- Puppet will autorequire any parent directories that are being managed; it will automatically realize that the parent directory should be created before the script is pulled down. Currently, exec elements will autorequire their CWD (if it is specified) plus any fully qualified paths that appear in the command. For instance, if you had an ``exec`` command that ran the ``myscript`` mentioned above, the above code that pulls the file down would be automatically listed as a requirement to the ``exec`` code, so that you would always be running againts the most recent version. " # Take whatever dependencies currently exist and add these. # Note that this probably doesn't behave correctly with unsubscribe. munge do |requires| # We need to be two arrays deep... unless requires.is_a?(Array) requires = [requires] end unless requires[0].is_a?(Array) requires = [requires] end if values = @parent[:require] requires = values + requires end requires end end # For each object we require, subscribe to all events that it generates. # We might reduce the level of subscription eventually, but for now... newmetaparam(:subscribe) do desc "One or more objects that this object depends on. Changes in the subscribed to objects result in the dependent objects being refreshed (e.g., a service will get restarted). For instance: class nagios { file { \"/etc/nagios/nagios.conf\": source => \"puppet://server/module/nagios.conf\", alias => nagconf # just to make things easier for me } service { nagios: running => true, subscribe => file[nagconf] } } " munge do |requires| if values = @parent[:subscribe] requires = values + requires end requires # @parent.handledepends(requires, :ALL_EVENTS, :refresh) end end newmetaparam(:loglevel) do desc "Sets the level that information will be logged. The log levels have the biggest impact when logs are sent to syslog (which is currently the default)." defaultto :notice newvalues(*Puppet::Log.levels) newvalues(:verbose) munge do |loglevel| val = super(loglevel) if val == :verbose val = :info end val end end newmetaparam(:alias) do desc "Creates an alias for the object. Puppet uses this internally when you provide a symbolic name: file { sshdconfig: path => $operatingsystem ? { solaris => \"/usr/local/etc/ssh/sshd_config\", default => \"/etc/ssh/sshd_config\" }, source => \"...\" } service { sshd: subscribe => file[sshdconfig] } When you use this feature, the parser sets ``sshdconfig`` as the name, and the library sets that as an alias for the file so the dependency lookup for ``sshd`` works. You can use this parameter yourself, but note that only the library can use these aliases; for instance, the following code will not work: file { \"/etc/ssh/sshd_config\": owner => root, group => root, alias => sshdconfig } file { sshdconfig: mode => 644 } There's no way here for the Puppet parser to know that these two stanzas should be affecting the same file. See the [language tutorial][] for more information. [language tutorial]: languagetutorial.html " munge do |aliases| unless aliases.is_a?(Array) aliases = [aliases] end @parent.info "Adding aliases %s" % aliases.collect { |a| a.inspect }.join(", ") aliases.each do |other| if obj = @parent.class[other] unless obj == @parent self.fail( "%s can not create alias %s: object already exists" % [@parent.title, other] ) end next end @parent.class.alias(other, @parent) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated element. While all elements are automatically tagged with as much information as possible (e.g., each class and component containing the element), it can be useful to add your own tags to a given element. Tags are currently useful for things like applying a subset of a host's configuration: puppetd --test --tag mytag This way, when you're testing a configuration you can run just the portion you're testing." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @parent.tag(tag) end end end newmetaparam(:notify) do desc %{This parameter is the opposite of **subscribe** -- it sends events to the specified object: file { "/etc/sshd_config": source => "....", notify => service[sshd] } service { sshd: ensure => running } This will restart the sshd service if the sshd config file changes.} # Take whatever dependencies currently exist and add these. munge do |notifies| # We need to be two arrays deep... unless notifies.is_a?(Array) notifies = [notifies] end unless notifies[0].is_a?(Array) notifies = [notifies] end if values = @parent[:notify] notifies = values + notifies end notifies end end newmetaparam(:before) do desc %{This parameter is the opposite of **require** -- it guarantees that the specified object is applied later than the specifying object: file { "/var/nagios/configuration": source => "...", recurse => true, before => exec["nagios-rebuid"] } exec { "nagios-rebuild": command => "/usr/bin/make", cwd => "/var/nagios/configuration" } This will make sure all of the files are up to date before the make command is run.} # Take whatever dependencies currently exist and add these. munge do |notifies| # We need to be two arrays deep... unless notifies.is_a?(Array) notifies = [notifies] end unless notifies[0].is_a?(Array) notifies = [notifies] end if values = @parent[:notify] notifies = values + notifies end notifies end end end # Puppet::Type end require 'puppet/statechange' require 'puppet/provider' require 'puppet/type/component' require 'puppet/type/cron' require 'puppet/type/exec' require 'puppet/type/group' require 'puppet/type/package' require 'puppet/type/pfile' require 'puppet/type/pfilebucket' require 'puppet/type/schedule' require 'puppet/type/service' require 'puppet/type/symlink' require 'puppet/type/user' require 'puppet/type/tidy' require 'puppet/type/parsedtype' require 'puppet/type/notify' # $Id$ diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index d8b8c812f..755c254d2 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,467 +1,469 @@ # 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' # 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 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 # Get the GID of a given group, provided either a GID or a name def self.gid(group) if group =~ /^\d+$/ group = Integer(group) end unless group raise Puppet::DevError, "Invalid group %s" % group.inspect end gid = nil obj = nil # We want to look the group up either way if group.is_a?(Integer) # If this doesn't find anything obj = Puppet.type(:group).find { |gobj| gobj.should(:gid) == group || gobj.is(:gid) == group } unless obj begin gobj = Etc.getgrgid(group) gid = gobj.gid rescue ArgumentError => detail # ignore it; we couldn't find the group end end else if obj = Puppet.type(:group)[group] obj[:check] = [:gid] else obj = Puppet.type(:group).create( :name => group, :check => [:gid] ) end obj.retrieve end if obj gid = obj.should(:gid) || obj.is(:gid) if gid == :absent gid = nil end end return gid end # Get the UID of a given user, whether a UID or name is provided def self.uid(user) uid = nil # if we don't have any user info, warn and GTFO. if !user Puppet.warning "Username provided for lookup is nil" return nil end # One of the unit tests was passing a Passwd struct unless user.is_a?(String) or user.is_a?(Integer) raise Puppet::DevError, "Invalid value for uid: %s" % user.class end if user =~ /^\d+$/ user = Integer(user) end if user.is_a?(Integer) # If this doesn't find anything obj = Puppet.type(:user).find { |uobj| uobj.should(:uid) == user || uobj.is(:uid) == user } unless obj begin uobj = Etc.getpwuid(user) uid = uobj.uid rescue ArgumentError => detail # ignore it; we couldn't find the user end end else unless obj = Puppet.type(:user)[user] obj = Puppet.type(:user).create( :name => user ) end obj[:check] = [:uid, :gid] end if obj obj.retrieve uid = obj.should(:uid) || obj.is(:uid) if uid == :absent uid = nil end end return uid 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 puts caller.join("\n") raise Puppet::DevError, "Failed to provide level" 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 return 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 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/lib/puppet/util/errors.rb b/lib/puppet/util/errors.rb new file mode 100644 index 000000000..ad129ee34 --- /dev/null +++ b/lib/puppet/util/errors.rb @@ -0,0 +1,55 @@ +# Some helper methods for throwing errors. +module Puppet::Util::Errors + # Throw a dev error. + def devfail(msg) + self.fail(Puppet::DevError, msg) + end + + # Add line and file info if available and appropriate. + def adderrorcontext(error, other = nil) + error.line ||= self.line if self.respond_to?(:line) and self.line + error.file ||= self.file if self.respond_to?(:file) and self.file + + if other and other.respond_to?(:backtrace) + error.set_backtrace other.backtrace + end + + return error + end + + # Wrap a call in such a way that we always throw the right exception and keep + # as much context as possible. + def exceptwrap(options = {}) + options[:type] ||= Puppet::DevError + begin + retval = yield + rescue Puppet::Error => detail + raise adderrorcontext(detail) + rescue => detail + message = options[:message] || "%s failed with error %s: %s" % + [self.class, detail.class, detail.to_s] + + error = options[:type].new(message) + # We can't use self.fail here because it always expects strings, + # not exceptions. + raise adderrorcontext(error, detail) + end + + return retval + end + + # Throw an error, defaulting to a Puppet::Error. + def fail(*args) + if args[0].is_a?(Class) + type = args.shift + else + type = Puppet::Error + end + + error = adderrorcontext(type.new(args.join(" "))) + + raise error + end +end + +# $Id$ diff --git a/lib/puppet/util/logging.rb b/lib/puppet/util/logging.rb new file mode 100644 index 000000000..1245e24de --- /dev/null +++ b/lib/puppet/util/logging.rb @@ -0,0 +1,20 @@ +# A module to make logging a bit easier. +require 'puppet/log' + +module Puppet::Util::Logging + # Create a method for each log level. + Puppet::Log.eachlevel do |level| + define_method(level) do |args| + if args.is_a?(Array) + args = args.join(" ") + end + Puppet::Log.create( + :level => level, + :source => self, + :message => args + ) + end + end +end + +# $Id$ diff --git a/lib/puppet/util/methodhelper.rb b/lib/puppet/util/methodhelper.rb index 5643ac245..c229e8efb 100644 --- a/lib/puppet/util/methodhelper.rb +++ b/lib/puppet/util/methodhelper.rb @@ -1,16 +1,37 @@ # Where we store helper methods related to, um, methods. module Puppet::Util::MethodHelper + def requiredopts(*names) + names.each do |name| + if self.send(name).nil? + devfail("%s is a required option for %s" % [name, self.class]) + end + end + end + + # Iterate over a hash, treating each member as an attribute. + def set_options(options) + options.dup.each do |param,value| + method = param.to_s + "=" + unless self.respond_to?(method) + self.fail "Invalid parameter %s to object class %s" % + [param,self.class.to_s] + end + + self.send(method,value) + end + end + # Take a hash and convert all of the keys to symbols if possible. def symbolize_options(options) options.inject({}) do |hash, opts| if opts[0].respond_to? :intern hash[opts[0].intern] = opts[1] else hash[opts[0]] = opts[1] end hash end end end # $Id$ diff --git a/examples/code/failers/badclassnoparam b/test/data/failers/badclassnoparam similarity index 100% rename from examples/code/failers/badclassnoparam rename to test/data/failers/badclassnoparam diff --git a/examples/code/failers/badclassparam b/test/data/failers/badclassparam similarity index 100% rename from examples/code/failers/badclassparam rename to test/data/failers/badclassparam diff --git a/examples/code/failers/badcompnoparam b/test/data/failers/badcompnoparam similarity index 100% rename from examples/code/failers/badcompnoparam rename to test/data/failers/badcompnoparam diff --git a/examples/code/failers/badcompparam b/test/data/failers/badcompparam similarity index 100% rename from examples/code/failers/badcompparam rename to test/data/failers/badcompparam diff --git a/examples/code/failers/badtypeparam b/test/data/failers/badtypeparam similarity index 100% rename from examples/code/failers/badtypeparam rename to test/data/failers/badtypeparam diff --git a/examples/code/failers/noobjectrvalue b/test/data/failers/noobjectrvalue similarity index 100% rename from examples/code/failers/noobjectrvalue rename to test/data/failers/noobjectrvalue diff --git a/examples/code/snippets/aliastest.pp b/test/data/snippets/aliastest.pp similarity index 100% rename from examples/code/snippets/aliastest.pp rename to test/data/snippets/aliastest.pp diff --git a/examples/code/snippets/argumentdefaults b/test/data/snippets/argumentdefaults similarity index 100% rename from examples/code/snippets/argumentdefaults rename to test/data/snippets/argumentdefaults diff --git a/examples/code/snippets/casestatement.pp b/test/data/snippets/casestatement.pp similarity index 100% rename from examples/code/snippets/casestatement.pp rename to test/data/snippets/casestatement.pp diff --git a/examples/code/snippets/classheirarchy.pp b/test/data/snippets/classheirarchy.pp similarity index 100% rename from examples/code/snippets/classheirarchy.pp rename to test/data/snippets/classheirarchy.pp diff --git a/examples/code/snippets/classincludes.pp b/test/data/snippets/classincludes.pp similarity index 100% rename from examples/code/snippets/classincludes.pp rename to test/data/snippets/classincludes.pp diff --git a/examples/code/snippets/classpathtest b/test/data/snippets/classpathtest similarity index 100% rename from examples/code/snippets/classpathtest rename to test/data/snippets/classpathtest diff --git a/examples/code/snippets/componentmetaparams.pp b/test/data/snippets/componentmetaparams.pp similarity index 76% rename from examples/code/snippets/componentmetaparams.pp rename to test/data/snippets/componentmetaparams.pp index 1d2c020c3..7d9f0c2c1 100644 --- a/examples/code/snippets/componentmetaparams.pp +++ b/test/data/snippets/componentmetaparams.pp @@ -1,11 +1,11 @@ file { "/tmp/component1": ensure => file } define thing { file { $name: ensure => file } } thing { "/tmp/component2": - require => file["/tmp/component1"] + require => File["/tmp/component1"] } diff --git a/examples/code/snippets/deepclassheirarchy.pp b/test/data/snippets/deepclassheirarchy.pp similarity index 100% rename from examples/code/snippets/deepclassheirarchy.pp rename to test/data/snippets/deepclassheirarchy.pp diff --git a/examples/code/snippets/defineoverrides.pp b/test/data/snippets/defineoverrides.pp similarity index 85% rename from examples/code/snippets/defineoverrides.pp rename to test/data/snippets/defineoverrides.pp index 1b6561668..a87573a57 100644 --- a/examples/code/snippets/defineoverrides.pp +++ b/test/data/snippets/defineoverrides.pp @@ -1,17 +1,17 @@ # $Id$ $file = "/tmp/defineoverrides1" define myfile($mode) { file { $name: ensure => file, mode => $mode } } class base { myfile { $file: mode => 644 } } class sub inherits base { - myfile { $file: mode => 755 } + Myfile[$file] { mode => 755 } } include sub diff --git a/examples/code/snippets/dirchmod b/test/data/snippets/dirchmod similarity index 100% rename from examples/code/snippets/dirchmod rename to test/data/snippets/dirchmod diff --git a/examples/code/snippets/emptyclass.pp b/test/data/snippets/emptyclass.pp similarity index 100% rename from examples/code/snippets/emptyclass.pp rename to test/data/snippets/emptyclass.pp diff --git a/examples/code/snippets/emptyexec.pp b/test/data/snippets/emptyexec.pp similarity index 100% rename from examples/code/snippets/emptyexec.pp rename to test/data/snippets/emptyexec.pp diff --git a/examples/code/snippets/failmissingexecpath.pp b/test/data/snippets/failmissingexecpath.pp similarity index 74% rename from examples/code/snippets/failmissingexecpath.pp rename to test/data/snippets/failmissingexecpath.pp index ca5b25f4c..aae1a09fa 100644 --- a/examples/code/snippets/failmissingexecpath.pp +++ b/test/data/snippets/failmissingexecpath.pp @@ -1,13 +1,13 @@ define distloc($path) { file { "/tmp/exectesting1": ensure => file } exec { "touch $path": - subscribe => file["/tmp/exectesting1"], + subscribe => File["/tmp/exectesting1"], refreshonly => true } } -distloc { +distloc { yay: path => "/tmp/execdisttesting", } diff --git a/examples/code/snippets/falsevalues.pp b/test/data/snippets/falsevalues.pp similarity index 100% rename from examples/code/snippets/falsevalues.pp rename to test/data/snippets/falsevalues.pp diff --git a/examples/code/snippets/filecreate b/test/data/snippets/filecreate similarity index 100% rename from examples/code/snippets/filecreate rename to test/data/snippets/filecreate diff --git a/examples/code/snippets/implicititeration b/test/data/snippets/implicititeration similarity index 100% rename from examples/code/snippets/implicititeration rename to test/data/snippets/implicititeration diff --git a/examples/code/snippets/multipleinstances b/test/data/snippets/multipleinstances similarity index 100% rename from examples/code/snippets/multipleinstances rename to test/data/snippets/multipleinstances diff --git a/test/data/snippets/multisubs.pp b/test/data/snippets/multisubs.pp new file mode 100644 index 000000000..bcec69e2a --- /dev/null +++ b/test/data/snippets/multisubs.pp @@ -0,0 +1,13 @@ +class base { + file { "/tmp/multisubtest": content => "base", mode => 644 } +} + +class sub1 inherits base { + File["/tmp/multisubtest"] { mode => 755 } +} + +class sub2 inherits base { + File["/tmp/multisubtest"] { content => sub2 } +} + +include sub1, sub2 diff --git a/examples/code/snippets/namevartest b/test/data/snippets/namevartest similarity index 100% rename from examples/code/snippets/namevartest rename to test/data/snippets/namevartest diff --git a/examples/code/snippets/scopetest b/test/data/snippets/scopetest similarity index 50% rename from examples/code/snippets/scopetest rename to test/data/snippets/scopetest index 3d3b31d8a..331491766 100644 --- a/examples/code/snippets/scopetest +++ b/test/data/snippets/scopetest @@ -1,13 +1,13 @@ $mode = 640 define thing { - file { "/tmp/scopetest": ensure => file, mode => $mode } + file { "/tmp/$name": ensure => file, mode => $mode } } class testing { $mode = 755 - thing {} + thing {scopetest: } } include testing diff --git a/examples/code/snippets/selectorvalues.pp b/test/data/snippets/selectorvalues.pp similarity index 100% rename from examples/code/snippets/selectorvalues.pp rename to test/data/snippets/selectorvalues.pp diff --git a/examples/code/snippets/simpledefaults b/test/data/snippets/simpledefaults similarity index 100% rename from examples/code/snippets/simpledefaults rename to test/data/snippets/simpledefaults diff --git a/examples/code/snippets/simpleselector b/test/data/snippets/simpleselector similarity index 100% rename from examples/code/snippets/simpleselector rename to test/data/snippets/simpleselector diff --git a/examples/code/snippets/singleary.pp b/test/data/snippets/singleary.pp similarity index 65% rename from examples/code/snippets/singleary.pp rename to test/data/snippets/singleary.pp index 1a6aebb21..9ce56dd89 100644 --- a/examples/code/snippets/singleary.pp +++ b/test/data/snippets/singleary.pp @@ -1,19 +1,19 @@ # $Id$ file { "/tmp/singleary1": ensure => file } file { "/tmp/singleary2": ensure => file } file { "/tmp/singleary3": ensure => file, - require => [file["/tmp/singleary1"], file["/tmp/singleary2"]] + require => [File["/tmp/singleary1"], File["/tmp/singleary2"]] } file { "/tmp/singleary4": ensure => file, - require => [file["/tmp/singleary1"]] + require => [File["/tmp/singleary1"]] } diff --git a/examples/code/snippets/singlequote.pp b/test/data/snippets/singlequote.pp similarity index 100% rename from examples/code/snippets/singlequote.pp rename to test/data/snippets/singlequote.pp diff --git a/examples/code/snippets/singleselector.pp b/test/data/snippets/singleselector.pp similarity index 100% rename from examples/code/snippets/singleselector.pp rename to test/data/snippets/singleselector.pp diff --git a/examples/code/snippets/tag.pp b/test/data/snippets/tag.pp similarity index 100% rename from examples/code/snippets/tag.pp rename to test/data/snippets/tag.pp diff --git a/examples/code/snippets/tagged.pp b/test/data/snippets/tagged.pp similarity index 95% rename from examples/code/snippets/tagged.pp rename to test/data/snippets/tagged.pp index e3ca93838..7bf90a645 100644 --- a/examples/code/snippets/tagged.pp +++ b/test/data/snippets/tagged.pp @@ -1,35 +1,35 @@ # $Id$ tag testing tag(funtest) -define tagdefine { +class tagdefine { $path = tagged(tagdefine) ? { true => "true", false => "false" } file { "/tmp/taggeddefine$path": ensure => file } } -tagdefine {} +include tagdefine $yayness = tagged(yayness) ? { true => "true", false => "false" } $funtest = tagged(testing) ? { true => "true", false => "false" } $both = tagged(testing, yayness) ? { true => "true", false => "false" } $bothtrue = tagged(testing, testing) ? { true => "true", false => "false" } file { "/tmp/taggedyayness$yayness": ensure => file } file { "/tmp/taggedtesting$funtest": ensure => file } file { "/tmp/taggedboth$both": ensure => file } file { "/tmp/taggedbothtrue$bothtrue": ensure => file } diff --git a/test/executables/puppetmodule.rb b/test/executables/puppetmodule.rb index 0752fed14..6ffd84be3 100755 --- a/test/executables/puppetmodule.rb +++ b/test/executables/puppetmodule.rb @@ -1,52 +1,55 @@ require 'puppet' require 'puppet/server' require 'puppet/sslcertificates' require 'puppettest' class TestPuppetModule < Test::Unit::TestCase include PuppetTest::ExeTest def setup super @module = File.join(basedir, "ext", "module_puppet") end def test_existence assert(FileTest.exists?(@module), "Module does not exist") end def test_execution file = tempfile() createdfile = tempfile() File.open(file, "w") { |f| f.puts "class yaytest { file { \"#{createdfile}\": ensure => file } }" } output = nil cmd = @module cmd += " --verbose" #cmd += " --fqdn %s" % fqdn cmd += " --confdir %s" % Puppet[:confdir] cmd += " --vardir %s" % Puppet[:vardir] if Puppet[:debug] cmd += " --logdest %s" % "console" cmd += " --debug" + cmd += " --trace" else cmd += " --logdest %s" % "/dev/null" end ENV["CFALLCLASSES"] = "yaytest:all" + libsetup + out = nil assert_nothing_raised { - %x{#{cmd + " " + file} 2>&1} + out = %x{#{cmd + " " + file} 2>&1} } - assert($? == 0, "Puppet module exited with code %s" % $?.to_i) + assert($? == 0, "Puppet module exited with code %s: %s" % [$?.to_i, out]) assert(FileTest.exists?(createdfile), "Failed to create config'ed file") end end # $Id$ diff --git a/test/language/ast.rb b/test/language/ast.rb index ab528a49d..731b37cb8 100755 --- a/test/language/ast.rb +++ b/test/language/ast.rb @@ -1,938 +1,499 @@ #!/usr/bin/ruby require 'puppet' +require 'puppet/rails' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/client' require 'puppettest' +require 'puppettest/resourcetesting' +require 'puppettest/parsertesting' +require 'puppettest/railstesting' class TestAST < Test::Unit::TestCase + include PuppetTest::RailsTesting include PuppetTest::ParserTesting + include PuppetTest::ResourceTesting # A fake class that we can use for testing evaluation. class FakeAST attr_writer :evaluate def evaluate(*args) return @evaluate end def initialize(val = nil) if val @evaluate = val end end def safeevaluate(*args) evaluate() end end - # Test that classes behave like singletons - def test_classsingleton - parent = child1 = child2 = nil - children = [] - - # create the parent class - children << classobj("parent") - - # Create child class one - children << classobj("child1", :parentclass => nameobj("parent")) + if defined? ActiveRecord + # Verify that our collection stuff works. + def test_collection + collectable = [] + non = [] + # First put some objects into the database. + bucket = mk_transtree do |object, depth, width| + # and mark some of them collectable + if width % 2 == 1 + object.collectable = true + collectable << object + else + non << object + end + end - # Create child class two - children << classobj("child2", :parentclass => nameobj("parent")) - classes = %w{parent child1 child2} + # Now collect our facts + facts = {} + Facter.each do |fact, value| facts[fact] = value end - # Now call the two classes - assert_nothing_raised("Could not add AST nodes for calling") { - children << AST::ObjectDef.new( - :type => nameobj("child1"), - :name => nameobj("yayness"), - :params => astarray() - ) - children << AST::ObjectDef.new( - :type => nameobj("child2"), - :name => nameobj("booness"), - :params => astarray() - ) + assert_nothing_raised { + Puppet::Rails.init } - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children + # Now try storing our crap + assert_nothing_raised { + host = Puppet::Rails::Host.store( + :objects => bucket, + :facts => facts, + :host => facts["hostname"] ) } - scope = nil - objects = nil - assert_nothing_raised("Could not evaluate") { - scope = Puppet::Parser::Scope.new() - objects = scope.evaluate(:ast => top) + # Now create an ast tree that collects that. They should all be files. + coll = nil + assert_nothing_raised { + coll = AST::Collection.new( + :type => nameobj("file") + ) } - assert_instance_of(Puppet::TransBucket, objects) - - assert_equal(1, scope.find_all { |child| - if child.is_a? Puppet::Parser::Scope - child.lookupobject(:name => "/parent", :type => "file") - else - nil - end - }.length, "Found incorrect number of '/parent' objects") - - assert_equal(classes.sort, scope.classlist.sort) - end - - # Test that 'tagobject' collects all of an object's parameters and stores - # them in one TransObject, rather than many. This is probably a bad idea. - def test_tagobject top = nil - children = [ - fileobj("/etc", "owner" => "root"), - fileobj("/etc", "group" => "root") - ] assert_nothing_raised("Could not create top object") { top = AST::ASTArray.new( - :children => children + :children => [coll] ) } - scope = Puppet::Parser::Scope.new() + objects = nil assert_nothing_raised("Could not evaluate") { - top.evaluate(:scope => scope) - } - - obj = nil - assert_nothing_raised("Could not retrieve file object") { - obj = scope.lookupobject(:name => "/etc", :type => "file") - } - - assert(obj, "could not retrieve file object") - - %w{owner group}.each { |param| - assert(obj.include?(param), "Object did not include %s" % param) + scope = mkscope + objects = scope.evaluate(:ast => top).flatten } + assert(objects.length > 0, "Did not receive any collected objects") + end + else + $stderr.puts "No ActiveRecord -- skipping collection tests" end - # Verify that objects can only have parents of the same type. - def test_validparent - parent = child1 = nil - children = [] - - # create the parent class - children << compobj("parent", :args => AST::ASTArray.new(:children => [])) - - # Create child class one - children << classobj("child1", :parentclass => nameobj("parent")) + def test_if + astif = nil + astelse = nil + fakeelse = FakeAST.new(:else) + faketest = FakeAST.new(true) + fakeif = FakeAST.new(:if) - # Now call the two classes - assert_nothing_raised("Could not add AST nodes for calling") { - children << AST::ObjectDef.new( - :type => nameobj("child1"), - :name => nameobj("yayness"), - :params => astarray() - ) + assert_nothing_raised { + astelse = AST::Else.new(:statements => fakeelse) } - - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children + assert_nothing_raised { + astif = AST::IfStatement.new( + :test => faketest, + :statements => fakeif, + :else => astelse ) } - scope = nil - assert_raise(Puppet::ParseError, "Invalid parent type was allowed") { - scope = Puppet::Parser::Scope.new() - objects = scope.evaluate(:ast => top) + # We initialized it to true, so we should get that first + ret = nil + assert_nothing_raised { + ret = astif.evaluate(:scope => "yay") } - end - - # Verify that nodes don't evaluate code in other node scopes but that their - # facts work outside their scopes. - def test_nodescopes - parent = child1 = nil - topchildren = [] - - # create the parent class - topchildren << classobj("everyone") - - topchildren << classobj("parent") - - - classes = %w{everyone parent} + assert_equal(:if, ret) - # And a variable, so we verify the facts get set at the top + # Now set it to false and check that + faketest.evaluate = false assert_nothing_raised { - children = [] - children << varobj("yaytest", "$hostname") + ret = astif.evaluate(:scope => "yay") } + assert_equal(:else, ret) + end - nodes = [] - - 3.times do |i| - children = [] - - # Create a child class - topchildren << classobj("perchild#{i}", :parentclass => nameobj("parent")) - classes << "perchild%s" - - # Create a child class - children << classobj("child", :parentclass => nameobj("parent")) - - classes << "child" - - ["child", "everyone", "perchild#{i}"].each do |name| - # Now call our child class - assert_nothing_raised { - children << AST::ObjectDef.new( - :type => nameobj(name), - :params => astarray() - ) - } - end + # Make sure our override object behaves "correctly" + def test_override + interp, scope, source = mkclassframing - # and another variable - assert_nothing_raised { - children << varobj("rahtest", "$hostname") - } - - # create the node - nodename = "node#{i}" - nodes << nodename - assert_nothing_raised("Could not create parent object") { - topchildren << AST::NodeDef.new( - :names => nameobj(nodename), - :code => AST::ASTArray.new( - :children => children - ) - ) - } + ref = nil + assert_nothing_raised do + ref = resourceoverride("resource", "yaytest", "one" => "yay", "two" => "boo") end - # Create the wrapper object - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => topchildren - ) - } - - nodes.each_with_index do |node, i| - # Evaluate the parse tree - scope = Puppet::Parser::Scope.new() - args = {:names => [node], :facts => {"hostname" => node}, :ast => top} + ret = nil + assert_nothing_raised do + ret = ref.evaluate :scope => scope + end - # verify that we can evaluate it okay - trans = nil - assert_nothing_raised("Could not retrieve node definition") { - trans = scope.evaluate(args) - } + assert_instance_of(Puppet::Parser::Resource, ret) - assert_equal(node, scope.lookupvar("hostname")) + assert(ret.override?, "Resource was not an override resource") - assert(trans, "Could not retrieve trans objects") + assert(scope.overridetable[ret.ref].include?(ret), + "Was not stored in the override table") + end - # and that we can convert them to type objects - objects = nil - assert_nothing_raised("Could not retrieve node definition") { - objects = trans.to_type - } + # make sure our resourcedefaults ast object works correctly. + def test_resourcedefaults + interp, scope, source = mkclassframing - assert(objects, "Could not retrieve trans objects") + # Now make some defaults for files + args = {:source => "/yay/ness", :group => "yayness"} + assert_nothing_raised do + obj = defaultobj "file", args + obj.evaluate :scope => scope + end - count = 0 - # Make sure the node name gets into the path correctly. - Puppet.type(:file).each { |obj| - count += 1 - assert(obj.path !~ /#{node}\[#{node}\]/, - "Node name appears twice") - } + hash = nil + assert_nothing_raised do + hash = scope.lookupdefaults("file") + end - assert(count > 0, "Did not create any files") + hash.each do |name, value| + assert_instance_of(Symbol, name) # params always convert + assert_instance_of(Puppet::Parser::Resource::Param, value) + end - classes.each do |name| - if name =~ /%s/ - name = name % i - end - assert(Puppet::Type.type(:file)["/#{name}"], "Could not find '#{name}'") - end - Puppet::Type.allclear + args.each do |name, value| + assert(hash[name], "Did not get default %s" % name) + assert_equal(value, hash[name].value) end end - # Verify that classes are correctly defined in node scopes. - def disabled_test_nodeclasslookup - parent = child1 = nil - children = [] - - # create the parent class - children << classobj("parent") - - # Create child class one - children << classobj("child1", :parentclass => nameobj("parent")) - - # Now call the two classes - assert_nothing_raised("Could not add AST nodes for calling") { - children << AST::ObjectDef.new( - :type => nameobj("child1"), - :name => nameobj("yayness"), - :params => astarray() - ) - } - - # create the node - nodename = "mynodename" - node = nil - assert_nothing_raised("Could not create parent object") { - node = AST::NodeDef.new( - :names => nameobj(nodename), - :code => AST::ASTArray.new( - :children => children - ) - ) - } + def test_hostclass + interp, scope, source = mkclassframing - # Create the wrapper object - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => [node] + # Create the class we're testing, first with no parent + klass = interp.newclass "first", + :code => AST::ASTArray.new( + :children => [resourcedef("file", "/tmp", + "owner" => "nobody", "mode" => "755")] ) - } - - # Evaluate the parse tree - scope = nil - assert_nothing_raised("Could not evaluate node") { - scope = Puppet::Parser::Scope.new() - top.evaluate(:scope => scope) - } - - # Verify that, well, nothing really happened, and especially verify - # that the top scope is not a node scope - assert(scope.topscope?, "Scope is not top scope") - assert(! scope.nodescope?, "Scope is mistakenly node scope") - assert(! scope.lookupclass("parent"), "Found parent class in top scope") - # verify we can find our node - assert(scope.node(nodename), "Could not find node") - - # And verify that we can evaluate it okay - objects = nil - assert_nothing_raised("Could not retrieve node definition") { - objects = scope.evalnode(:name => [nodename], :facts => {}) - } - - assert(objects, "Could not retrieve node definition") - - # Because node scopes are temporary (i.e., they get destroyed after the node's - # config is returned) we should not be able to find the node scope. - nodescope = nil - assert_nothing_raised { - nodescope = scope.find { |child| - child.nodescope? - } - } - - assert_nil(nodescope, "Found nodescope") - - # And now verify again that the top scope cannot find the node's definition - # of the parent class - assert(! scope.lookupclass("parent"), "Found parent class in top scope") - - trans = nil - # Verify that we can evaluate the node twice - assert_nothing_raised("Could not retrieve node definition") { - trans = scope.evalnode(:name => [nodename], :facts => {}) - } - - objects = nil - assert_nothing_raised("Could not convert to objects") { - objects = trans.to_type - } - - Puppet.type(:file).each { |obj| - assert(obj.path !~ /#{nodename}\[#{nodename}\]/, - "Node name appears twice") - } - - assert(Puppet::Type.type(:file)["/child1"], "Could not find child") - assert(Puppet::Type.type(:file)["/parent"], "Could not find parent") - end + assert_nothing_raised do + klass.evaluate(:scope => scope) + end - # Test that you can look a host up using multiple names, e.g., an FQDN and - # a short name - def test_multiplenodenames - children = [] + # Then try it again + assert_nothing_raised do + klass.evaluate(:scope => scope) + end - # create a short-name node - shortname = "mynodename" - children << nodedef(shortname) + assert(scope.setclass?(klass), "Class was not considered evaluated") - # And a long-name node - longname = "node.domain.com" - children << nodedef(longname) + tmp = scope.findresource("file[/tmp]") + assert(tmp, "Could not find file /tmp") + assert_equal("nobody", tmp[:owner]) + assert_equal("755", tmp[:mode]) - # Create the wrapper object - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } - - # Evaluate the parse tree - scope = Puppet::Parser::Scope.new() - - # Verify we can find the node via a search list - objects = nil - assert_nothing_raised("Could not retrieve short node definition") { - objects = scope.evaluate( - :names => ["%s.domain.com" % shortname, shortname], :facts => {}, - :ast => top + # Now create a couple more classes. + newbase = interp.newclass "newbase", + :code => AST::ASTArray.new( + :children => [resourcedef("file", "/tmp/other", + "owner" => "nobody", "mode" => "644")] ) - } - assert(objects, "Could not retrieve short node definition") - - scope = Puppet::Parser::Scope.new() - # and then look for the long name - assert_nothing_raised("Could not retrieve long node definition") { - objects = scope.evaluate( - :names => [longname.sub(/\..+/, ''), longname], :facts => {}, - :ast => top + newsub = interp.newclass "newsub", + :parent => "newbase", + :code => AST::ASTArray.new( + :children => [resourcedef("file", "/tmp/yay", + "owner" => "nobody", "mode" => "755"), + resourceoverride("file", "/tmp/other", + "owner" => "daemon") + ] ) - } - assert(objects, "Could not retrieve long node definition") - end - - # Test that a node gets the entire configuration except for work meant for - # another node - def test_fullconfigwithnodes - children = [] - - children << fileobj("/testing") - # create a short-name node - name = "mynodename" - children << nodedef(name) - - # Create the wrapper object - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children + # Override a different variable in the top scope. + moresub = interp.newclass "moresub", + :parent => "newbase", + :code => AST::ASTArray.new( + :children => [resourceoverride("file", "/tmp/other", + "mode" => "755")] ) - } - scope = Puppet::Parser::Scope.new() - - # Verify we can find the node via a search list - objects = nil - assert_nothing_raised("Could not retrieve short node definition") { - objects = scope.evaluate(:names => [name], :facts => {}, :ast => top) - } - assert(objects, "Could not retrieve short node definition") - assert_instance_of(Puppet::TransBucket, objects) - - # And now verify that we got both the top and node objects - assert_nothing_raised("Could not find top-declared object") { - assert_equal("/testing", objects[0].name) - } - - assert_nothing_raised("Could not find node-declared object %s" % - "/%s" % name - ) { - assert_equal("/%s" % name, objects[1][0].name) - } - end - - # Test that we can 'include' variables, not just normal strings. - def test_includevars - children = [] - classes = [] - - # Create our class for testin - klassname = "include" - children << classobj(klassname) - classes << klassname - - # Then add our variable assignment - children << varobj("klassvar", klassname) - - # And finally add our calling of the variable - children << AST::ObjectDef.new( - :type => AST::Variable.new(:value => "klassvar"), - :params => astarray - ) - - # And then create our top object - top = AST::ASTArray.new( - :children => children - ) + assert_nothing_raised do + newsub.evaluate(:scope => scope) + end - # Evaluate the parse tree - scope = nil - objects = nil - assert_nothing_raised("Could not evaluate node") { - scope = Puppet::Parser::Scope.new() - objects = scope.evaluate(:ast => top) - } + assert_nothing_raised do + moresub.evaluate(:scope => scope) + end - # Verify we get the right classlist back - assert_equal(classes.sort, scope.classlist.sort) + assert(scope.setclass?(newbase), "Did not eval newbase") + assert(scope.setclass?(newsub), "Did not eval newsub") - # Verify we can find the node via a search list - #assert_nothing_raised("Could not retrieve objects") { - # objects = scope.to_trans - #} - assert(objects, "Could not retrieve objects") + yay = scope.findresource("file[/tmp/yay]") + assert(yay, "Did not find file /tmp/yay") + assert_equal("nobody", yay[:owner]) + assert_equal("755", yay[:mode]) - assert_nothing_raised("Could not find top-declared object") { - assert_equal("/%s" % klassname, objects[0][0].name) - } + other = scope.findresource("file[/tmp/other]") + assert(other, "Did not find file /tmp/other") + assert_equal("daemon", other[:owner]) + assert_equal("755", other[:mode]) end - # Test that node inheritance works correctly - def test_nodeinheritance - children = [] + def test_component + interp, scope, source = mkclassframing - # create the base node - name = "basenode" - children << nodedef(name) - - # and the sub node - name = "subnode" - children << AST::NodeDef.new( - :names => nameobj(name), - :parentclass => nameobj("basenode"), + # Create a new definition + klass = interp.newdefine "yayness", + :arguments => [["owner", stringobj("nobody")], %w{mode}], :code => AST::ASTArray.new( - :children => [ - varobj("%svar" % name, "%svalue" % name), - fileobj("/%s" % name) - ] + :children => [resourcedef("file", "/tmp/$name", + "owner" => varref("owner"), "mode" => varref("mode"))] ) - ) - #subnode = nodedef(name) - #subnode.parentclass = "basenode" - #children << subnode + # Test validattr? a couple different ways + [:owner, "owner", :schedule, "schedule"].each do |var| + assert(klass.validattr?(var), "%s was not considered valid" % var.inspect) + end - # and the top object - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children + [:random, "random"].each do |var| + assert(! klass.validattr?(var), "%s was considered valid" % var.inspect) + end + # Now call it a couple of times + # First try it without a required param + assert_raise(Puppet::ParseError) do + klass.evaluate(:scope => scope, + :name => "bad", + :arguments => {"owner" => "nobody"} ) - } + end - # Evaluate the parse tree - scope = Puppet::Parser::Scope.new() + # And make sure it didn't create the file + assert_nil(scope.findresource("file[/tmp/bad]"), + "Made file with invalid params") - # Verify we can find the node via a search list - objects = nil - assert_nothing_raised("Could not evaluate node") { - objects = scope.evaluate(:names => [name], :facts => {}, :ast => top) - } - assert(objects, "Could not retrieve node definition") - - assert_nothing_raised { - inner = objects[0] - - # And now verify that we got the subnode file - assert_nothing_raised("Could not find basenode file") { - base = inner[0] - assert_equal("/basenode", base.name) - } - - # and the parent node file - assert_nothing_raised("Could not find subnode file") { - sub = inner[1] - assert_equal("/subnode", sub.name) - } - - inner.each { |obj| - %w{basenode subnode}.each { |tag| - assert(obj.tags.include?(tag), - "%s did not include %s tag" % [obj.name, tag] - ) - } - } - } - end - - def test_typechecking - object = nil - children = [] - type = "deftype" - assert_nothing_raised("Could not add AST nodes for calling") { - object = AST::ObjectDef.new( - :type => nameobj(type), - :name => nameobj("yayness"), - :params => astarray() + assert_nothing_raised do + klass.evaluate(:scope => scope, + :name => "first", + :arguments => {"mode" => "755"} ) - } - - assert_nothing_raised("Typecheck failed") { - object.typecheck(type) - } - - # Add a scope, which makes it think it's evaluating - assert_nothing_raised { - scope = Puppet::Parser::Scope.new() - object.scope = scope - } + end - # Verify an error is thrown when it can't find the type - assert_raise(Puppet::ParseError) { - object.typecheck(type) - } + firstobj = scope.findresource("file[/tmp/first]") + assert(firstobj, "Did not create /tmp/first obj") - # Create child class one - children << classobj(type) - children << object + assert_equal("file", firstobj.type) + assert_equal("/tmp/first", firstobj.title) + assert_equal("nobody", firstobj[:owner]) + assert_equal("755", firstobj[:mode]) - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children + # Make sure we can't evaluate it with the same args + assert_raise(Puppet::ParseError) do + klass.evaluate(:scope => scope, + :name => "first", + :arguments => {"mode" => "755"} ) - } - - scope = nil - assert_nothing_raised("Could not evaluate") { - scope = Puppet::Parser::Scope.new() - objects = top.evaluate(:scope => scope) - } - end + end - def disabled_test_paramcheck - object = nil - children = [] - type = "deftype" - params = %w{param1 param2} - - comp = compobj(type, { - :args => astarray( - argobj("param1", "yay"), - argobj("param2", "rah") - ), - :code => AST::ASTArray.new( - :children => [ - varobj("%svar" % name, "%svalue" % name), - fileobj("/%s" % name) - ] + # Now create another with different args + assert_nothing_raised do + klass.evaluate(:scope => scope, + :name => "second", + :arguments => {"mode" => "755", "owner" => "daemon"} ) - }) - assert_nothing_raised("Could not add AST nodes for calling") { - object = AST::ObjectDef.new( - :type => nameobj(type), - :name => nameobj("yayness"), - :params => astarray( - astarray(stringobj("param1"), stringobj("value1")), - astarray(stringobj("param2"), stringobj("value2")) - ) - ) - } - - # Add a scope, which makes it think it's evaluating - assert_nothing_raised { - scope = Puppet::Parser::Scope.new() - object.scope = scope - } - - # Verify an error is thrown when it can't find the type - assert_raise(Puppet::ParseError) { - object.paramcheck(false, comp) - } + end - # Create child class one - children << classobj(type) - children << object + secondobj = scope.findresource("file[/tmp/second]") + assert(secondobj, "Did not create /tmp/second obj") - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } - - scope = nil - assert_nothing_raised("Could not evaluate") { - scope = Puppet::Parser::Scope.new() - objects = top.evaluate(:scope => scope) - } + assert_equal("file", secondobj.type) + assert_equal("/tmp/second", secondobj.title) + assert_equal("daemon", secondobj[:owner]) + assert_equal("755", secondobj[:mode]) end - def test_setclass - type = "yay" - classes = [type] - children = [] - # Create child class one - children << varobj("variable", "aclass") - children << tagobj(type, varref("variable")) - children << tagobj(type) + def test_node + interp = mkinterp + scope = mkscope(:interp => interp) - classes << "aclass" + # Define a base node + basenode = interp.newnode "basenode", :code => AST::ASTArray.new(:children => [ + resourcedef("file", "/tmp/base", "owner" => "root") + ]) - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } + # Now define a subnode + nodes = interp.newnode ["mynode", "othernode"], + :code => AST::ASTArray.new(:children => [ + resourcedef("file", "/tmp/mynode", "owner" => "root"), + resourcedef("file", "/tmp/basenode", "owner" => "daemon") + ]) - scope = nil - assert_nothing_raised("Could not evaluate") { - scope = Puppet::Parser::Scope.new() - objects = top.evaluate(:scope => scope) - } + assert_instance_of(Array, nodes) + # Make sure we can find them all. + %w{mynode othernode}.each do |node| + assert(interp.nodesearch_code(node), "Could not find %s" % node) + end + mynode = interp.nodesearch_code("mynode") - classes.each do |tag| - assert(scope.classlist.include?(tag), "Did not set class %s" % tag) + # Now try evaluating the node + assert_nothing_raised do + mynode.evaluate :scope => scope end - end + # Make sure that we can find each of the files + myfile = scope.findresource "file[/tmp/mynode]" + assert(myfile, "Could not find file from node") + assert_equal("root", myfile[:owner]) - # Test that we strip the domain off of host names before they are set as classes - def test_nodenamestrip - children = [] + basefile = scope.findresource "file[/tmp/basenode]" + assert(basefile, "Could not find file from base node") + assert_equal("daemon", basefile[:owner]) - longname = "node.domain.com" - children << nodedef(longname) + # Now make sure we can evaluate nodes with parents + child = interp.newnode(%w{child}, :parent => "basenode").shift - # Create the wrapper object - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } - - scope = Puppet::Parser::Scope.new() - - assert_nothing_raised("Could not evaluate node") { - objects = scope.evaluate(:names => [longname], :facts => {}, :ast => top) - } + newscope = mkscope :interp => interp + assert_nothing_raised do + child.evaluate :scope => newscope + end - assert(!scope.classlist.include?("node.domain.com"), - "Node's long name got set") - assert(scope.classlist.include?("node"), "Node's name did not get set") + assert(newscope.findresource("file[/tmp/base]"), + "Could not find base resource") end - # Make sure that deep class parentage works - def test_classparentage - children = [] - files = [] - base = classobj("base") - files << "/base" + def test_collection + interp = mkinterp + scope = mkscope(:interp => interp) - children << base + coll = nil + assert_nothing_raised do + coll = AST::Collection.new(:type => "file", :form => :virtual) + end - parent = "base" - 5.times { |i| - name = "child%s" % i - files << "/%s" % name - children << classobj(name, :parentclass => nameobj(parent)) + assert_instance_of(AST::Collection, coll) - parent = name - } + ret = nil + assert_nothing_raised do + ret = coll.evaluate :scope => scope + end - children << functionobj("include", parent) + assert_instance_of(Puppet::Parser::Collector, ret) - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } + # Now make sure we get it back from the scope + assert_equal([ret], scope.collections) + end - objects = nil - assert_nothing_raised("Could not evaluate") { - scope = Puppet::Parser::Scope.new() - objects = scope.evaluate(:ast => top) - } + def test_virtual_collexp + @interp, @scope, @source = mkclassframing - objects = objects.flatten + # make a resource + resource = mkresource(:type => "file", :title => "/tmp/testing", + :params => {:owner => "root", :group => "bin", :mode => "644"}) - files.each do |file| - assert(objects.find { |o| o.name == file }, - "Could not find file %s" % file) + run_collection_queries(:virtual) do |string, result, query| + code = nil + assert_nothing_raised do + str, code = query.evaluate :scope => @scope + end + + assert_instance_of(Proc, code) + assert_nothing_raised do + assert_equal(result, code.call(resource), + "'#{string}' failed") + end end end - # Make sure we catch names that are specified like parameters. - def test_name_or_param - obj = nil - assert_nothing_raised { - obj = AST::ObjectDef.new( - :type => nameobj("file"), - :params => astarray(AST::ObjectParam.new( - :param => stringobj("name"), - :value => stringobj("yayness") - )) - ) - } - - scope = Puppet::Parser::Scope.new + if defined? ActiveRecord::Base + def test_exported_collexp + railsinit + Puppet[:storeconfigs] = true + @interp, @scope, @source = mkclassframing + + # make a rails resource + railsresource "file", "/tmp/testing", :owner => "root", :group => "bin", + :mode => "644" + + run_collection_queries(:exported) do |string, result, query| + code = nil + str = nil + + # We don't support anything but the title in rails right now + retval = nil + bad = false + # Figure out if the search is for anything rails will ignore + string.scan(/(\w+) [!=]= \w+/) do |s| + unless s[0] == "title" + bad = true + break + end + end - trans = nil - assert_nothing_raised { - trans = scope.evaluate(:ast => obj, :facts => {}) - } + # And if it is, make sure we throw an error. + if bad + assert_raise(Puppet::ParseError, "Evaluated '#{string}'") do + str, code = query.evaluate :scope => @scope + end + next + else + assert_nothing_raised("Could not evaluate '#{string}'") do + str, code = query.evaluate :scope => @scope + end + end + assert_nothing_raised("Could not find resource") do + retval = Puppet::Rails::RailsResource.find(:all, + :include => :rails_parameters, + :conditions => str) + end - transobj = trans.shift - assert(transobj.name, "Name did not convert from param to name") - end + if result + assert_equal(1, retval.length, "Did not find resource with '#{string}'") + res = retval.shift - if defined? ActiveRecord - # Verify that our collection stuff works. - def test_collection - collectable = [] - non = [] - # First put some objects into the database. - bucket = mk_transtree do |object, depth, width| - # and mark some of them collectable - if width % 2 == 1 - object.collectable = true - collectable << object + assert_equal("file", res.restype) + assert_equal("/tmp/testing", res.title) else - non << object + assert_equal(0, retval.length, "found a resource with '#{string}'") end end - - # Now collect our facts - facts = {} - Facter.each do |fact, value| facts[fact] = value end - - assert_nothing_raised { - Puppet::Rails.init - } - - # Now try storing our crap - assert_nothing_raised { - host = Puppet::Rails::Host.store( - :objects => bucket, - :facts => facts, - :host => facts["hostname"] - ) - } - - # Now create an ast tree that collects that. They should all be files. - coll = nil - assert_nothing_raised { - coll = AST::Collection.new( - :type => nameobj("file") - ) - } - - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => [coll] - ) - } - - objects = nil - assert_nothing_raised("Could not evaluate") { - scope = Puppet::Parser::Scope.new() - objects = scope.evaluate(:ast => top).flatten - } - - assert(objects.length > 0, "Did not receive any collected objects") end - else - $stderr.puts "No ActiveRecord -- skipping collection tests" end - # To fix #140. Currently non-functional. - def disabled_test_classreuse - children = [] - - # Create the parent class, with a definition in it. - children << classobj("parent", :code => AST::ASTArray.new( - :file => __FILE__, - :line => __LINE__, - :children => [ - compobj("foo", :args => AST::ASTArray.new( - :children => [nameobj("arg")] - ), - :code => AST::ASTArray.new( - :file => __FILE__, - :line => __LINE__, - :children => [fileobj("/$arg")] - ) - ), - objectdef("foo", "ptest", {"arg" => "parentfoo"}) - ] - )) - - # Create child class, also trying to use that definition - children << classobj("child1", :parentclass => nameobj("parent"), - :code => AST::ASTArray.new( - :file => __FILE__, - :line => __LINE__, - :children => [ - objectdef("foo", "ctest", {"arg" => "childfoo"}) - ] - ) - ) - - # Call the parent first - children << functionobj("include", "parent") - - # Then call the child, and make sure it can look up the definition - children << functionobj("include", "child1") - - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } - - objects = nil - assert_nothing_raised("Could not evaluate") { - scope = Puppet::Parser::Scope.new() - objects = scope.evaluate(:ast => top) - } - end - - def test_if - astif = nil - astelse = nil - fakeelse = FakeAST.new(:else) - faketest = FakeAST.new(true) - fakeif = FakeAST.new(:if) - - assert_nothing_raised { - astelse = AST::Else.new(:statements => fakeelse) - } - assert_nothing_raised { - astif = AST::IfStatement.new( - :test => faketest, - :statements => fakeif, - :else => astelse - ) - } + def run_collection_queries(form) + {true => [%{title == "/tmp/testing"}, %{(title == "/tmp/testing")}, + %{title == "/tmp/testing" and group == bin}, %{title == bin or group == bin}, + %{title == "/tmp/testing" or title == bin}, %{title == "/tmp/testing"}, + %{(title == "/tmp/testing" or title == bin) and group == bin}], + false => [%{title == bin}, %{title == bin or (title == bin and group == bin)}, + %{title != "/tmp/testing"}, %{title != "/tmp/testing" and group != bin}] + }.each do |res, ary| + ary.each do |str| + if form == :virtual + code = "File <| #{str} |>" + else + code = "File <<| #{str} |>>" + end + parser = mkparser + query = nil - # We initialized it to true, so we should get that first - ret = nil - assert_nothing_raised { - ret = astif.evaluate(:scope => "yay") - } - assert_equal(:if, ret) + assert_nothing_raised("Could not parse '#{str}'") do + query = parser.parse(code)[0].query + end - # Now set it to false and check that - faketest.evaluate = false - assert_nothing_raised { - ret = astif.evaluate(:scope => "yay") - } - assert_equal(:else, ret) + yield str, res, query + end + end end end # $Id$ diff --git a/test/language/collector.rb b/test/language/collector.rb new file mode 100755 index 000000000..d7ac059fa --- /dev/null +++ b/test/language/collector.rb @@ -0,0 +1,206 @@ +#!/usr/bin/ruby + +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 + + 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 + assert_nothing_raised do + coll = Puppet::Parser::Collector.new(@scope, "file", nil, :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, :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, :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, :exported) + end + + # Remark this as virtual + exported.virtual = true + + assert_nothing_raised do + ret = coll.evaluate + end + + assert_equal([], ret) + 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, :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, :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/language/functions.rb b/test/language/functions.rb index 2c3246e6a..062971bf1 100755 --- a/test/language/functions.rb +++ b/test/language/functions.rb @@ -1,304 +1,304 @@ #!/usr/bin/ruby require 'puppet' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/client' require 'puppettest' class TestLangFunctions < Test::Unit::TestCase include PuppetTest::ParserTesting def test_functions assert_raise(Puppet::ParseError) do Puppet::Parser::AST::Function.new( :name => "fakefunction", :arguments => AST::ASTArray.new( :children => [nameobj("avalue")] ) ) end assert_nothing_raised do Puppet::Parser::Functions.newfunction(:fakefunction, :rvalue) do |input| return "output %s" % input[0] end end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "fakefunction", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [nameobj("avalue")] ) ) end - scope = Puppet::Parser::Scope.new() + scope = mkscope val = nil assert_nothing_raised do val = func.evaluate(:scope => scope) end assert_equal("output avalue", val) end def test_taggedfunction - scope = Puppet::Parser::Scope.new() + scope = mkscope tag = "yayness" - scope.setclass(tag.object_id, tag) + scope.tag(tag) {"yayness" => true, "booness" => false}.each do |tag, retval| func = taggedobj(tag, :rvalue) val = nil assert_nothing_raised do val = func.evaluate(:scope => scope) end assert_equal(retval, val, "'tagged' returned %s for %s" % [val, tag]) end end def test_failfunction func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "fail", :ftype => :statement, :arguments => AST::ASTArray.new( :children => [stringobj("this is a failure"), stringobj("and another")] ) ) end - scope = Puppet::Parser::Scope.new() + scope = mkscope val = nil assert_raise(Puppet::ParseError) do val = func.evaluate(:scope => scope) end end def test_multipletemplates Dir.mkdir(Puppet[:templatedir]) onep = File.join(Puppet[:templatedir], "one") twop = File.join(Puppet[:templatedir], "two") File.open(onep, "w") do |f| f.puts "template <%= one %>" end File.open(twop, "w") do |f| f.puts "template <%= two %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj("one"), stringobj("two")] ) ) end ast = varobj("output", func) - scope = Puppet::Parser::Scope.new() + scope = mkscope assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("one", "One") assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("two", "Two") assert_nothing_raised do ast.evaluate(:scope => scope) end assert_equal("template One\ntemplate Two\n", scope.lookupvar("output"), "Templates were not handled correctly") end # Now make sure we can fully qualify files, and specify just one def test_singletemplates template = tempfile() File.open(template, "w") do |f| f.puts "template <%= yayness %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj(template)] ) ) end ast = varobj("output", func) - scope = Puppet::Parser::Scope.new() + scope = mkscope assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("yayness", "this is yayness") assert_nothing_raised do ast.evaluate(:scope => scope) end assert_equal("template this is yayness\n", scope.lookupvar("output"), "Templates were not handled correctly") end def test_tempatefunction_cannot_see_scopes template = tempfile() File.open(template, "w") do |f| f.puts "<%= lookupvar('myvar') %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj(template)] ) ) end ast = varobj("output", func) - scope = Puppet::Parser::Scope.new() + scope = mkscope scope.setvar("myvar", "this is yayness") assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end end def test_template_reparses template = tempfile() File.open(template, "w") do |f| f.puts "original text" end manifest = tempfile() file = tempfile() File.open(manifest, "w") do |f| f.puts %{file { "#{file}": content => template("#{template}") }} end interpreter = Puppet::Parser::Interpreter.new( :Manifest => manifest, :UseNodes => false ) parsedate = interpreter.parsedate() objects = nil assert_nothing_raised { objects = interpreter.run("myhost", {}) } fileobj = objects[0] assert_equal("original text\n", fileobj["content"], "Template did not work") Puppet[:filetimeout] = 0 # Have to sleep because one second is the fs's time granularity. sleep(1) # Now modify the template File.open(template, "w") do |f| f.puts "new text" end assert_nothing_raised { objects = interpreter.run("myhost", {}) } newdate = interpreter.parsedate() assert(parsedate != newdate, "Parse date did not change") end def test_template_defined_vars template = tempfile() File.open(template, "w") do |f| f.puts "template <%= yayness %>" end func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => "template", :ftype => :rvalue, :arguments => AST::ASTArray.new( :children => [stringobj(template)] ) ) end ast = varobj("output", func) { "" => "", false => "false", }.each do |string, value| - scope = Puppet::Parser::Scope.new() + scope = mkscope assert_raise(Puppet::ParseError) do ast.evaluate(:scope => scope) end scope.setvar("yayness", string) assert_nothing_raised("An empty string was not a valid variable value") do ast.evaluate(:scope => scope) end assert_equal("template #{value}\n", scope.lookupvar("output"), "%s did not get evaluated correctly" % string.inspect) end end def test_autoloading_functions assert_equal(false, Puppet::Parser::Functions.function(:autofunc), "Got told autofunc already exists") dir = tempfile() $: << dir newpath = File.join(dir, "puppet", "parser", "functions") FileUtils.mkdir_p(newpath) File.open(File.join(newpath, "autofunc.rb"), "w") { |f| f.puts %{ Puppet::Parser::Functions.newfunction(:autofunc, :rvalue) do |vals| Puppet.wanring vals.inspect end } } obj = nil assert_nothing_raised { obj = Puppet::Parser::Functions.function(:autofunc) } assert(obj, "Did not autoload function") assert(Puppet::Parser::Scope.method_defined?(:function_autofunc), "Did not set function correctly") end end # $Id$ diff --git a/test/language/interpreter.rb b/test/language/interpreter.rb index c127ab517..e195e28b3 100755 --- a/test/language/interpreter.rb +++ b/test/language/interpreter.rb @@ -1,392 +1,827 @@ #!/usr/bin/ruby require 'facter' require 'puppet' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/client' require 'puppet/rails' require 'puppettest' +require 'puppettest/resourcetesting' +require 'puppettest/parsertesting' +require 'puppettest/servertest' +require 'puppettest/railstesting' +require 'timeout' class TestInterpreter < Test::Unit::TestCase include PuppetTest include PuppetTest::ServerTest + include PuppetTest::ParserTesting + include PuppetTest::ResourceTesting + include PuppetTest::RailsTesting AST = Puppet::Parser::AST # create a simple manifest that uses nodes to create a file def mknodemanifest(node, file) createdfile = tempfile() File.open(file, "w") { |f| f.puts "node %s { file { \"%s\": ensure => file, mode => 755 } }\n" % [node, createdfile] } return [file, createdfile] end def test_simple file = tempfile() File.open(file, "w") { |f| f.puts "file { \"/etc\": owner => root }" } assert_nothing_raised { Puppet::Parser::Interpreter.new(:Manifest => file) } end def test_reloadfiles hostname = Facter["hostname"].value file = tempfile() # Create a first version createdfile = mknodemanifest(hostname, file) interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new(:Manifest => file) } config = nil assert_nothing_raised { config = interp.run(hostname, {}) } sleep(1) # Now create a new file createdfile = mknodemanifest(hostname, file) newconfig = nil assert_nothing_raised { newconfig = interp.run(hostname, {}) } assert(config != newconfig, "Configs are somehow the same") end if defined? ActiveRecord def test_hoststorage assert_nothing_raised { Puppet[:storeconfigs] = true } file = tempfile() File.open(file, "w") { |f| f.puts "file { \"/etc\": owner => root }" } interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new( :Manifest => file, :UseNodes => false, :ForkSave => false ) } facts = {} Facter.each { |fact, val| facts[fact] = val } objects = nil assert_nothing_raised { objects = interp.run(facts["hostname"], facts) } obj = Puppet::Rails::Host.find_by_name(facts["hostname"]) assert(obj, "Could not find host object") end else $stderr.puts "No ActiveRecord -- skipping collection tests" end if Facter["domain"].value == "madstop.com" begin require 'ldap' $haveldap = true rescue LoadError $stderr.puts "Missing ldap; skipping ldap source tests" $haveldap = false end # Only test ldap stuff on luke's network, since that's the only place we # have data for. if $haveldap 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(node) parent = nil classes = nil @ldap.search( "ou=hosts, dc=madstop, dc=com", 2, "(&(objectclass=puppetclient)(cn=%s))" % node ) do |entry| parent = entry.vals("parentnode").shift classes = entry.vals("puppetclass") || [] end return parent, classes end - def test_ldapnodes + def test_ldapsearch Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com" Puppet[:ldapnodes] = true ldapconnect() - file = tempfile() - files = [] - parentfile = tempfile() + "-parent" - files << parentfile - hostname = Facter["hostname"].value - lparent, lclasses = ldaphost(Facter["hostname"].value) - assert(lclasses, "Did not retrieve info from ldap") - File.open(file, "w") { |f| - f.puts "node #{lparent} { - file { \"#{parentfile}\": ensure => file } -}" - - lclasses.each { |klass| - kfile = tempfile() + "-klass" - files << kfile - f.puts "class #{klass} { file { \"#{kfile}\": ensure => file } }" - } - } - interp = nil - assert_nothing_raised { - interp = Puppet::Parser::Interpreter.new( - :Manifest => file - ) - } - parent = nil - classes = nil - # First make sure we get the default node for unknown hosts - dparent, dclasses = ldaphost("default") + interp = mkinterp :NodeSources => [:ldap, :code] - assert_nothing_raised { - parent, classes = interp.nodesearch("nosuchhostokay") - } + # Make sure we can find 'culain' in ldap + parent, classes = nil + assert_nothing_raised do + parent, classes = interp.ldapsearch("culain") + end - assert_equal(dparent, parent, "Default parent node did not match") - assert_equal(dclasses, classes, "Default parent class list did not match") + realparent, realclasses = ldaphost("culain") + assert_equal(realparent, parent) + assert_equal(realclasses, classes) + end - # Look for a host we know doesn't have a parent - npparent, npclasses = ldaphost("noparent") - assert_nothing_raised { - #parent, classes = interp.nodesearch_ldap("noparent") - parent, classes = interp.nodesearch("noparent") - } + def test_ldapnodes + Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com" + Puppet[:ldapnodes] = true - assert_equal(npparent, parent, "Parent node did not match") - assert_equal(npclasses, classes, "Class list did not match") + ldapconnect() - # Now look for our normal host - assert_nothing_raised { - parent, classes = interp.nodesearch_ldap(hostname) - } + interp = mkinterp :NodeSources => [:ldap, :code] - assert_equal(lparent, parent, "Parent node did not match") - assert_equal(lclasses, classes, "Class list did not match") + # culain uses basenode, so create that + basenode = interp.newnode([:basenode])[0] - objects = nil - assert_nothing_raised { - objects = interp.run(hostname, Puppet::Client::MasterClient.facts) - } + # Make sure we can find 'culain' in ldap + culain = nil + assert_nothing_raised do + culain = interp.nodesearch_ldap("culain") + end - comp = nil - assert_nothing_raised { - comp = objects.to_type - } + assert(culain, "Did not find culain in ldap") - assert_apply(comp) - files.each { |cfile| - @@tmpfiles << cfile - assert(FileTest.exists?(cfile), "Did not make %s" % cfile) - } + assert_nothing_raised do + assert_equal(basenode.fqname.to_s, culain.parentclass.fqname.to_s, + "Did not get parent class") + end end if Puppet::SUIDManager.uid == 0 and Facter["hostname"].value == "culain" def test_ldapreconnect Puppet[:ldapbase] = "ou=hosts, dc=madstop, dc=com" Puppet[:ldapnodes] = true interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new( :Manifest => mktestmanifest() ) } hostname = "culain.madstop.com" # look for our host assert_nothing_raised { parent, classes = interp.nodesearch_ldap(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 = interp.nodesearch_ldap(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 = interp.nodesearch_ldap(hostname) } end else $stderr.puts "Run as root for ldap reconnect tests" end end else $stderr.puts "Not in madstop.com; skipping ldap tests" end # Make sure searchnode behaves as we expect. def test_nodesearch - # First create a fake nodesearch algorithm - i = 0 - bucket = [] - Puppet::Parser::Interpreter.send(:define_method, "nodesearch_fake") do |node| - return nil, nil if node == "default" - - return bucket[0], bucket[1] - end - text = %{ -node nodeparent {} -node othernodeparent {} -class nodeclass {} -class nothernode {} -} - manifest = tempfile() - File.open(manifest, "w") do |f| f.puts text end - interp = nil - assert_nothing_raised { - interp = Puppet::Parser::Interpreter.new( - :Manifest => manifest, - :NodeSources => [:fake] - ) - } - # Make sure it behaves correctly for all forms - [[nil, nil], - ["nodeparent", nil], - [nil, ["nodeclass"]], - [nil, ["nodeclass", "nothernode"]], - ["othernodeparent", ["nodeclass", "nothernode"]],].each do |ary| - # Set the return values - bucket = ary - - # Look them back up - parent, classes = interp.nodesearch("mynode") - - # Basically, just make sure that if we have either or both, - # we get a result back. - assert_equal(ary[0], parent, - "Parent is not %s" % parent) - assert_equal(ary[1], classes, - "Parent is not %s" % parent) - - next if ary == [nil, nil] - # Now make sure we actually get the configuration. This will throw - # an exception if we don't. - assert_nothing_raised do - interp.run("mynode", {}) - end - end - end - - # Make sure nodesearch uses all names, not just one. - def test_nodesearch_multiple_names - bucket = {} - Puppet::Parser::Interpreter.send(:define_method, "nodesearch_multifake") do |node| - if bucket[node] - return *bucket[node] - else - return nil, nil - end + interp = mkinterp + + # Make some nodes + names = %w{node1 node2 node2.domain.com} + interp.newnode names + + nodes = {} + # Make sure we can find them all, using the direct method + names.each do |name| + nodes[name] = interp.nodesearch_code(name) + assert(nodes[name], "Could not find %s" % name) + nodes[name].file = __FILE__ end - manifest = tempfile() - File.open(manifest, "w") do |f| f.puts "" end - interp = nil - assert_nothing_raised { - interp = Puppet::Parser::Interpreter.new( - :Manifest => manifest, - :NodeSources => [:multifake] - ) - } - bucket["name.domain.com"] = [:parent, [:classes]] - - ret = nil - - assert_nothing_raised do - assert_equal bucket["name.domain.com"], - interp.nodesearch("name", "name.domain.com") + # Now let's try it with the nodesearch method + names.each do |name| + node = interp.nodesearch(name) + assert(node, "Could not find #{name} via nodesearch") end + # Now make sure the longest match always wins + node = interp.nodesearch(*%w{node2 node2.domain.com}) + assert(node, "Did not find node2") + assert_equal("node2.domain.com", node.fqname, + "Did not get longest match") end def test_parsedate Puppet[:filetimeout] = 0 main = tempfile() sub = tempfile() mainfile = tempfile() subfile = tempfile() count = 0 updatemain = proc do count += 1 File.open(main, "w") { |f| f.puts "import '#{sub}' file { \"#{mainfile}\": content => #{count} } " } end updatesub = proc do count += 1 File.open(sub, "w") { |f| f.puts "file { \"#{subfile}\": content => #{count} } " } end updatemain.call updatesub.call interp = Puppet::Parser::Interpreter.new( :Manifest => main, :Local => true ) date = interp.parsedate # Now update the site file and make sure we catch it sleep 1 updatemain.call newdate = interp.parsedate assert(date != newdate, "Parsedate was not updated") date = newdate # And then the subfile sleep 1 updatesub.call newdate = interp.parsedate assert(date != newdate, "Parsedate was not updated") end + + # Make sure our node gets added to the node table. + def test_newnode + interp = mkinterp + + # First just try calling it directly + assert_nothing_raised { + interp.newnode("mynode", :code => :yay) + } + + assert_equal(:yay, interp.nodesearch_code("mynode").code) + + # Now make sure that trying to redefine it throws an error. + assert_raise(Puppet::ParseError) { + interp.newnode("mynode", {}) + } + + # Now try one with no code + assert_nothing_raised { + interp.newnode("simplenode", :parent => :foo) + } + + # Make sure trying to get the parentclass throws an error + assert_raise(Puppet::ParseError) do + interp.nodesearch_code("simplenode").parentclass + end + + # Now define the parent node + interp.newnode(:foo) + + # And make sure we get things back correctly + assert_equal("foo", interp.nodesearch_code("simplenode").parentclass.fqname) + assert_nil(interp.nodesearch_code("simplenode").code) + + # Now make sure that trying to redefine it throws an error. + assert_raise(Puppet::ParseError) { + interp.newnode("mynode", {}) + } + + # Test multiple names + names = ["one", "two", "three"] + assert_nothing_raised { + interp.newnode(names, {:code => :yay, :parent => :foo}) + } + + names.each do |name| + assert_equal(:yay, interp.nodesearch_code(name).code) + assert_equal("foo", interp.nodesearch_code(name).parentclass.name) + # Now make sure that trying to redefine it throws an error. + assert_raise(Puppet::ParseError) { + interp.newnode(name, {}) + } + end + end + + # Make sure we're correctly generating a node definition. + def test_gennode + interp = mkinterp + + node = nil + assert_nothing_raised { + node = interp.gennode("nodeA", :classes => %w{classA classB}) + } + + assert_instance_of(Puppet::Parser::AST::Node, node) + + assert_equal("nodeA", node.name) + end + + def test_fqfind + interp = mkinterp + + table = {} + # Define a bunch of things. + %w{a c a::b a::b::c a::c a::b::c::d a::b::c::d::e::f c::d}.each do |string| + table[string] = string + end + + check = proc do |namespace, hash| + hash.each do |thing, result| + assert_equal(result, interp.fqfind(namespace, thing, table), + "Could not find %s in %s" % [thing, namespace]) + end + end + + # Now let's do some test lookups. + + # First do something really simple + check.call "a", "b" => "a::b", "b::c" => "a::b::c", "d" => nil, "::c" => "c" + + check.call "a::b", "c" => "a::b::c", "b" => "a::b", "a" => "a" + + check.call "a::b::c::d::e", "c" => "a::b::c", "::c" => "c", + "c::d" => "a::b::c::d", "::c::d" => "c::d" + + check.call "", "a" => "a", "a::c" => "a::c" + end + + def test_newdefine + interp = mkinterp + + assert_nothing_raised { + interp.newdefine("mydefine", :code => :yay, + :arguments => ["a", stringobj("b")]) + } + + mydefine = interp.finddefine("", "mydefine") + assert(mydefine, "Could not find definition") + assert_equal("mydefine", interp.finddefine("", "mydefine").type) + assert_equal("", mydefine.namespace) + assert_equal("mydefine", mydefine.type) + + assert_raise(Puppet::ParseError) do + interp.newdefine("mydefine", :code => :yay, + :arguments => ["a", stringobj("b")]) + end + + # Now define the same thing in a different scope + assert_nothing_raised { + interp.newdefine("other::mydefine", :code => :other, + :arguments => ["a", stringobj("b")]) + } + other = interp.finddefine("other", "mydefine") + assert(other, "Could not find definition") + assert(interp.finddefine("", "other::mydefine"), + "Could not find other::mydefine") + assert_equal(:other, other.code) + assert_equal("other", other.namespace) + assert_equal("mydefine", other.type) + assert_equal("other::mydefine", other.fqname) + end + + def test_newclass + interp = mkinterp + + mkcode = proc do |ary| + classes = ary.collect do |string| + AST::FlatString.new(:value => string) + end + AST::ASTArray.new(:children => classes) + end + scope = Puppet::Parser::Scope.new(:interp => interp) + + # First make sure that code is being appended + code = mkcode.call(%w{original code}) + + klass = nil + assert_nothing_raised { + klass = interp.newclass("myclass", :code => code) + } + + assert(klass, "Did not return class") + + assert(interp.findclass("", "myclass"), "Could not find definition") + assert_equal("myclass", interp.findclass("", "myclass").type) + assert_equal(%w{original code}, + interp.findclass("", "myclass").code.evaluate(:scope => scope)) + + # Now create the same class name in a different scope + assert_nothing_raised { + klass = interp.newclass("other::myclass", + :code => mkcode.call(%w{something diff})) + } + assert(klass, "Did not return class") + other = interp.findclass("other", "myclass") + assert(other, "Could not find class") + assert(interp.findclass("", "other::myclass"), "Could not find class") + assert_equal("other::myclass", other.fqname) + assert_equal("other", other.namespace) + assert_equal("myclass", other.type) + assert_equal(%w{something diff}, + interp.findclass("other", "myclass").code.evaluate(:scope => scope)) + + # Newclass behaves differently than the others -- it just appends + # the code to the existing class. + code = mkcode.call(%w{something new}) + assert_nothing_raised do + klass = interp.newclass("myclass", :code => code) + end + assert(klass, "Did not return class when appending") + assert_equal(%w{original code something new}, + interp.findclass("", "myclass").code.evaluate(:scope => scope)) + + # Make sure newclass deals correctly with nodes with no code + klass = interp.newclass("nocode") + assert(klass, "Did not return class") + + assert_nothing_raised do + klass = interp.newclass("nocode", :code => mkcode.call(%w{yay test})) + end + assert(klass, "Did not return class with no code") + assert_equal(%w{yay test}, + interp.findclass("", "nocode").code.evaluate(:scope => scope)) + + # Then try merging something into nothing + interp.newclass("nocode2", :code => mkcode.call(%w{foo test})) + assert(klass, "Did not return class with no code") + + assert_nothing_raised do + klass = interp.newclass("nocode2") + end + assert(klass, "Did not return class with no code") + assert_equal(%w{foo test}, + interp.findclass("", "nocode2").code.evaluate(:scope => scope)) + + # And lastly, nothing and nothing + klass = interp.newclass("nocode3") + assert(klass, "Did not return class with no code") + + assert_nothing_raised do + klass = interp.newclass("nocode3") + end + assert(klass, "Did not return class with no code") + assert_nil(interp.findclass("", "nocode3").code) + + end + + # Now make sure we get appropriate behaviour with parent class conflicts. + def test_newclass_parentage + interp = mkinterp + interp.newclass("base1") + interp.newclass("one::two::three") + + # First create it with no parentclass. + assert_nothing_raised { + interp.newclass("sub") + } + assert(interp.findclass("", "sub"), "Could not find definition") + assert_nil(interp.findclass("", "sub").parentclass) + + # Make sure we can't set the parent class to ourself. + assert_raise(Puppet::ParseError) { + interp.newclass("sub", :parent => "sub") + } + + # Now create another one, with a parentclass. + assert_nothing_raised { + interp.newclass("sub", :parent => "base1") + } + + # Make sure we get the right parent class, and make sure it's an object. + assert_equal(interp.findclass("", "base1"), + interp.findclass("", "sub").parentclass) + + # Now make sure we get a failure if we try to conflict. + assert_raise(Puppet::ParseError) { + interp.newclass("sub", :parent => "one::two::three") + } + + # Make sure that failure didn't screw us up in any way. + assert_equal(interp.findclass("", "base1"), + interp.findclass("", "sub").parentclass) + # But make sure we can create a class with a fq parent + assert_nothing_raised { + interp.newclass("another", :parent => "one::two::three") + } + assert_equal(interp.findclass("", "one::two::three"), + interp.findclass("", "another").parentclass) + + end + + def test_namesplit + interp = mkinterp + + assert_nothing_raised do + {"base::sub" => %w{base sub}, + "main" => ["", "main"], + "one::two::three::four" => ["one::two::three", "four"], + }.each do |name, ary| + result = interp.namesplit(name) + assert_equal(ary, result, "%s split to %s" % [name, result]) + end + end + end + + # Make sure you can't have classes and defines with the same name in the + # same scope. + def test_classes_beat_defines + interp = mkinterp + + assert_nothing_raised { + interp.newclass("yay::funtest") + } + + assert_raise(Puppet::ParseError) do + interp.newdefine("yay::funtest") + end + + assert_nothing_raised { + interp.newdefine("yay::yaytest") + } + + assert_raise(Puppet::ParseError) do + interp.newclass("yay::yaytest") + end + end + + # Make sure our whole chain works. + def test_evaluate + interp, scope, source = mkclassframing + + # Create a define that we'll be using + interp.newdefine("wrapper", :code => AST::ASTArray.new(:children => [ + resourcedef("file", varref("name"), "owner" => "root") + ])) + + # Now create a resource that uses that define + define = mkresource(:type => "wrapper", :title => "/tmp/testing", + :scope => scope, :source => source, :params => :none) + + scope.setresource define + + # And a normal resource + scope.setresource mkresource(:type => "file", :title => "/tmp/rahness", + :scope => scope, :source => source, + :params => {:owner => "root"}) + + # Now evaluate everything + objects = nil + interp.usenodes = false + assert_nothing_raised do + objects = interp.evaluate(nil, {}) + end + + assert_instance_of(Puppet::TransBucket, objects) + end + + def test_evaliterate + interp, scope, source = mkclassframing + + # Create a top-level definition that creates a builtin object + interp.newdefine("one", :arguments => [%w{owner}], + :code => AST::ASTArray.new(:children => [ + resourcedef("file", varref("name"), + "owner" => varref("owner") + ) + ]) + ) + + # Create another definition to call that one + interp.newdefine("two", :arguments => [%w{owner}], + :code => AST::ASTArray.new(:children => [ + resourcedef("one", varref("name"), + "owner" => varref("owner") + ) + ]) + ) + + # And then a third + interp.newdefine("three", :arguments => [%w{owner}], + :code => AST::ASTArray.new(:children => [ + resourcedef("two", varref("name"), + "owner" => varref("owner") + ) + ]) + ) + + three = Puppet::Parser::Resource.new( + :type => "three", :title => "/tmp/yayness", + :scope => scope, :source => source, + :params => paramify(source, :owner => "root") + ) + + scope.setresource(three) + + ret = nil + assert_nothing_raised do + ret = scope.unevaluated + end + + + assert_instance_of(Array, ret) + assert(1, ret.length) + assert_equal([three], ret) + + assert(ret.detect { |r| r.ref == "three[/tmp/yayness]"}, + "Did not get three back as unevaluated") + + # Now translate the whole tree + assert_nothing_raised do + interp.evaliterate(scope) + end + + # Now make sure we've got our file + file = scope.findresource "file[/tmp/yayness]" + assert(file, "Could not find file") + + assert_equal("root", file[:owner]) + end + + # Make sure we fail if there are any leftover overrides to perform. + # This would normally mean that someone is trying to override an object + # that does not exist. + def test_failonleftovers + interp, scope, source = mkclassframing + + # Make sure we don't fail, since there are no overrides + assert_nothing_raised do + interp.failonleftovers(scope) + end + + # Add an override, and make sure it causes a failure + over1 = mkresource :scope => scope, :source => source, + :params => {:one => "yay"} + + scope.setoverride(over1) + + assert_raise(Puppet::ParseError) do + interp.failonleftovers(scope) + end + + end + + def test_evalnode + interp = mkinterp + interp.usenodes = false + scope = Parser::Scope.new(:interp => interp) + facts = Facter.to_hash + + # First make sure we get no failures when client is nil + assert_nothing_raised do + interp.evalnode(nil, scope, facts) + end + + # Now define a node + interp.newnode "mynode", :code => AST::ASTArray.new(:children => [ + resourcedef("file", "/tmp/testing", "owner" => "root") + ]) + + # Eval again, and make sure it does nothing + assert_nothing_raised do + interp.evalnode("mynode", scope, facts) + end + + assert_nil(scope.findresource("file[/tmp/testing]"), + "Eval'ed node with nodes off") + + # Now enable usenodes and make sure it works. + interp.usenodes = true + assert_nothing_raised do + interp.evalnode("mynode", scope, facts) + end + file = scope.findresource("file[/tmp/testing]") + + assert_instance_of(Puppet::Parser::Resource, file, + "Could not find file") + end + + # This is mostly used for the cfengine module + def test_specificclasses + interp = mkinterp :Classes => %w{klass1 klass2}, :UseNodes => false + + # Make sure it's not a failure to be missing classes, since + # we're using the cfengine class list, which is huge. + assert_nothing_raised do + interp.evaluate(nil, {}) + end + + interp.newclass("klass1", :code => AST::ASTArray.new(:children => [ + resourcedef("file", "/tmp/klass1", "owner" => "root") + ])) + interp.newclass("klass2", :code => AST::ASTArray.new(:children => [ + resourcedef("file", "/tmp/klass2", "owner" => "root") + ])) + + ret = nil + assert_nothing_raised do + ret = interp.evaluate(nil, {}) + end + + found = ret.flatten.collect do |res| res.name end + + assert(found.include?("/tmp/klass1"), "Did not evaluate klass1") + assert(found.include?("/tmp/klass2"), "Did not evaluate klass2") + end + + if defined? ActiveRecord::Base + # We need to make sure finished objects are stored in the db. + def test_finish_before_store + railsinit + interp = mkinterp + + node = interp.newnode ["myhost"], :code => AST::ASTArray.new(:children => [ + resourcedef("file", "/tmp/yay", :group => "root"), + defaultobj("file", :owner => "root") + ]) + + interp.newclass "myclass", :code => AST::ASTArray.new(:children => [ + ]) + + interp.newclass "sub", :parent => "myclass", + :code => AST::ASTArray.new(:children => [ + resourceoverride("file", "/tmp/yay", :owner => "root") + ] + ) + + # Now do the rails crap + Puppet[:storeconfigs] = true + + interp.evaluate("myhost", {}) + + # And then retrieve the object from rails + res = Puppet::Rails::RailsResource.find_by_restype_and_title("file", "/tmp/yay") + + assert(res, "Did not get resource from rails") + + param = res.rails_parameters.find_by_name("owner") + + assert(param, "Did not find owner param") + + assert_equal("root", param[:value]) + end + end end + +# $Id$ diff --git a/test/language/lexer.rb b/test/language/lexer.rb index 496087ba2..26b85b3b7 100644 --- a/test/language/lexer.rb +++ b/test/language/lexer.rb @@ -1,154 +1,228 @@ require 'puppet' require 'puppet/parser/lexer' require 'puppettest' #%q{service("telnet") = \{ # port => "23", # protocol => "tcp", # name => "telnet", #\} #} => [[:NAME, "service"], [:LPAREN, "("], [:DQUOTE, "\""], [:NAME, "telnet"], [:DQUOTE, "\""], [:RPAREN, ")"], [:EQUALS, "="], [:lbrace, "{"], [:NAME, "port"], [:FARROW, "=>"], [:DQUOTE, "\""], [:NAME, "23"], [:DQUOTE, "\""], [:COMMA, ","], [:NAME, "protocol"], [:FARROW, "=>"], [:DQUOTE, "\""], [:NAME, "tcp"], [:DQUOTE, "\""], [:COMMA, ","], [:NAME, "name"], [:FARROW, "=>"], [:DQUOTE, "\""], [:NAME, "telnet"], [:DQUOTE, "\""], [:COMMA, ","], [:RBRACE, "}"]] class TestLexer < Test::Unit::TestCase include PuppetTest def setup super + mklexer + end + + def mklexer @lexer = Puppet::Parser::Lexer.new() end def test_simple_lex strings = { %q{\\} => [[:BACKSLASH,"\\"],[false,false]], %q{simplest scanner test} => [[:NAME,"simplest"],[:NAME,"scanner"],[:NAME,"test"],[false,false]], %q{returned scanner test } => [[:NAME,"returned"],[:NAME,"scanner"],[:NAME,"test"],[false,false]] } strings.each { |str,ary| @lexer.string = str assert_equal( ary, @lexer.fullscan() ) } end def test_quoted_strings strings = { %q{a simple "scanner" test } => [[:NAME,"a"],[:NAME,"simple"],[:DQTEXT,"scanner"],[:NAME,"test"],[false,false]], %q{a simple 'single quote scanner' test } => [[:NAME,"a"],[:NAME,"simple"],[:SQTEXT,"single quote scanner"],[:NAME,"test"],[false,false]], %q{a harder 'a $b \c"' } => [[:NAME,"a"],[:NAME,"harder"],[:SQTEXT,'a $b \c"'],[false,false]], %q{a harder "scanner test" } => [[:NAME,"a"],[:NAME,"harder"],[:DQTEXT,"scanner test"],[false,false]], %q{a hardest "scanner \"test\"" } => [[:NAME,"a"],[:NAME,"hardest"],[:DQTEXT,'scanner "test"'],[false,false]], %q{a hardestest "scanner \"test\" " } => [[:NAME,"a"],[:NAME,"hardestest"],[:DQTEXT,'scanner "test" '],[false,false]], %q{function("call")} => [[:NAME,"function"],[:LPAREN,"("],[:DQTEXT,'call'],[:RPAREN,")"],[false,false]] } strings.each { |str,array| @lexer.string = str assert_equal( array, @lexer.fullscan() ) } end def test_errors strings = %w{ ^ } strings.each { |str| @lexer.string = str assert_raise(RuntimeError) { @lexer.fullscan() } } end def test_more_error assert_raise(TypeError) { @lexer.fullscan() } end def test_files textfiles() { |file| - @lexer.file = file + lexer = Puppet::Parser::Lexer.new() + lexer.file = file assert_nothing_raised() { - @lexer.fullscan() + lexer.fullscan() } Puppet::Type.allclear } end def test_strings names = %w{this is a bunch of names} types = %w{Many Different Words A Word} words = %w{differently Cased words A a} names.each { |t| @lexer.string = t assert_equal( [[:NAME,t],[false,false]], @lexer.fullscan ) } types.each { |t| @lexer.string = t assert_equal( [[:TYPE,t],[false,false]], @lexer.fullscan ) } end def test_emptystring bit = '$var = ""' assert_nothing_raised { @lexer.string = bit } assert_nothing_raised { @lexer.fullscan } end def test_collectlexing {"@" => :AT, "<|" => :LCOLLECT, "|>" => :RCOLLECT}.each do |string, token| assert_nothing_raised { @lexer.string = string } ret = nil assert_nothing_raised { ret = @lexer.fullscan } assert_equal([[token, string],[false, false]], ret) end end def test_collectabletype string = "@type {" assert_nothing_raised { @lexer.string = string } ret = nil assert_nothing_raised { ret = @lexer.fullscan } assert_equal([[:AT, "@"], [:NAME, "type"], [:LBRACE, "{"], [false,false]],ret) end + + def test_namespace + @lexer.string = %{class myclass} + + assert_nothing_raised { + @lexer.fullscan + } + + assert_equal("myclass", @lexer.namespace) + + assert_nothing_raised do + @lexer.namepop + end + + assert_equal("", @lexer.namespace) + + @lexer.string = "class base { class sub { class more" + + assert_nothing_raised { + @lexer.fullscan + } + + assert_equal("base::sub::more", @lexer.namespace) + + assert_nothing_raised do + @lexer.namepop + end + + assert_equal("base::sub", @lexer.namespace) + + # Now try it with some fq names + mklexer + + @lexer.string = "class base { class sub::more {" + + assert_nothing_raised { + @lexer.fullscan + } + + assert_equal("base::sub::more", @lexer.namespace) + + assert_nothing_raised do + @lexer.namepop + end + + assert_equal("base", @lexer.namespace) + end + + def test_indefine + @lexer.string = %{define me} + + assert_nothing_raised { + @lexer.fullscan + } + + assert(@lexer.indefine?, "Lexer not considered in define") + + # Now make sure we throw an error when trying to nest defines. + assert_raise(Puppet::ParseError) do + @lexer.string = %{define another} + @lexer.fullscan + end + + assert_nothing_raised do + @lexer.indefine = false + end + + assert(! @lexer.indefine?, "Lexer still considered in define") + end end # $Id$ diff --git a/test/language/node.rb b/test/language/node.rb index 791c44874..251e4c4aa 100644 --- a/test/language/node.rb +++ b/test/language/node.rb @@ -1,124 +1,106 @@ require 'puppet' require 'puppet/parser/parser' require 'puppettest' class TestParser < Test::Unit::TestCase include PuppetTest::ParserTesting def setup super Puppet[:parseonly] = true - @parser = Puppet::Parser::Parser.new() end def test_simple_hostname check_parseable "host1" check_parseable "'host2'" check_parseable [ "'host1'", "host2" ] check_parseable [ "'host1'", "'host2'" ] end def test_qualified_hostname check_parseable "'host.example.com'" check_parseable [ "'host.example.com'", "host1" ] check_parseable "'host-1.37examples.example.com'" check_parseable "'svn.23.nu'" check_parseable "'HOST'" end def test_reject_hostname check_nonparseable "host.example.com" check_nonparseable "host@example.com" check_nonparseable "\"host\"" check_nonparseable "'$foo.example.com'" check_nonparseable "'host1 host2'" check_nonparseable "HOST" end AST = Puppet::Parser::AST def check_parseable(hostnames) unless hostnames.is_a?(Array) hostnames = [ hostnames ] end + interp = nil assert_nothing_raised { - @parser.string = "node #{hostnames.join(", ")} { }" + interp = mkinterp :Code => "node #{hostnames.join(", ")} { }" } # Strip quotes hostnames.map! { |s| s.sub(/^'(.*)'$/, "\\1") } - ast = nil + + # parse assert_nothing_raised { - ast = @parser.parse + interp.send(:parsefiles) } - # Verify that the AST has the expected structure - # and that the leaves have the right hostnames in them - assert_kind_of(AST::ASTArray, ast) - assert_equal(1, ast.children.size) - nodedef = ast.children[0] - assert_kind_of(AST::NodeDef, nodedef) - assert_kind_of(AST::ASTArray, nodedef.names) - assert_equal(hostnames.size, nodedef.names.children.size) - hostnames.size.times do |i| - hostnode = nodedef.names.children[i] - assert_kind_of(AST::HostName, hostnode) - assert_equal(hostnames[i], hostnode.value) + + # Now make sure we can look up each of the names + hostnames.each do |name| + assert(interp.nodesearch_code(name), + "Could not find node %s" % name) end end def check_nonparseable(hostname) - assert_nothing_raised { - @parser.string = "node #{hostname} { }" - } - - assert_raise(Puppet::DevError, Puppet::ParseError) { - @parser.parse + interp = nil + assert_raise(Puppet::DevError, Puppet::ParseError, "#{hostname} passed") { + interp = mkinterp :Code => "node #{hostname} { }" + interp.send(:parsefiles) } end # Make sure we can find default nodes if there's no other entry def test_default_node Puppet[:parseonly] = false - @parser = Puppet::Parser::Parser.new() fileA = tempfile() fileB = tempfile() - @parser.string = %{ + code = %{ node mynode { file { "#{fileA}": ensure => file } } node default { file { "#{fileB}": ensure => file } } } - - # First make sure it parses - ast = nil + interp = nil assert_nothing_raised { - ast = @parser.parse - } - - args = { - :ast => ast, - :facts => {}, - :names => ["mynode"] + interp = mkinterp :Code => code } - # Make sure we get a config for "mynode" - trans = nil + # First make sure it parses assert_nothing_raised { - trans = Puppet::Parser::Scope.new.evaluate(args) + interp.send(:parsefiles) } - assert(trans, "Did not get config for mynode") + # Make sure we find our normal node + assert(interp.nodesearch("mynode"), + "Did not find normal node") - args[:names] = ["othernode"] - # Now make sure the default node is used - trans = nil - assert_nothing_raised { - trans = Puppet::Parser::Scope.new.evaluate(args) - } + # Now look for the default node + default = interp.nodesearch("someother") + assert(default, + "Did not find default node") - assert(trans, "Did not get config for default node") + assert_equal("default", default.fqname) end end diff --git a/test/language/parser.rb b/test/language/parser.rb index d6e176870..dbe48616a 100644 --- a/test/language/parser.rb +++ b/test/language/parser.rb @@ -1,499 +1,582 @@ require 'puppet' require 'puppet/parser/parser' require 'puppettest' class TestParser < Test::Unit::TestCase include PuppetTest::ParserTesting def setup super Puppet[:parseonly] = true #@lexer = Puppet::Parser::Lexer.new() - @parser = Puppet::Parser::Parser.new() end def test_each_file textfiles { |file| + parser = mkparser Puppet.debug("parsing %s" % file) if __FILE__ == $0 assert_nothing_raised() { - @parser.file = file - @parser.parse + parser.file = file + parser.parse } Puppet::Type.eachtype { |type| type.each { |obj| assert(obj.file) assert(obj.name) assert(obj.line) } } Puppet::Type.allclear } end def test_failers failers { |file| + parser = mkparser Puppet.debug("parsing failer %s" % file) if __FILE__ == $0 assert_raise(Puppet::ParseError) { - @parser.file = file - ast = @parser.parse - Puppet::Parser::Scope.new.evaluate(:ast => ast) + parser.file = file + ast = parser.parse + scope = mkscope :interp => parser.interp + ast.evaluate :scope => scope } Puppet::Type.allclear } end def test_arrayrvalues - parser = Puppet::Parser::Parser.new() + parser = mkparser ret = nil file = tempfile() assert_nothing_raised { parser.string = "file { \"#{file}\": mode => [755, 640] }" } assert_nothing_raised { ret = parser.parse } end def mkmanifest(file) name = File.join(tmpdir, "file%s" % rand(100)) @@tmpfiles << name File.open(file, "w") { |f| f.puts "file { \"%s\": ensure => file, mode => 755 }\n" % name } end def test_importglobbing basedir = File.join(tmpdir(), "importesting") @@tmpfiles << basedir Dir.mkdir(basedir) subdir = "subdir" Dir.mkdir(File.join(basedir, subdir)) manifest = File.join(basedir, "manifest") File.open(manifest, "w") { |f| f.puts "import \"%s/*\"" % subdir } 4.times { |i| path = File.join(basedir, subdir, "subfile%s" % i) mkmanifest(path) } assert_nothing_raised("Could not parse multiple files") { - parser = Puppet::Parser::Parser.new() + parser = mkparser parser.file = manifest parser.parse } end def test_nonexistent_import basedir = File.join(tmpdir(), "importesting") @@tmpfiles << basedir Dir.mkdir(basedir) manifest = File.join(basedir, "manifest") File.open(manifest, "w") do |f| f.puts "import \" no such file \"" end assert_raise(Puppet::ParseError) { - parser = Puppet::Parser::Parser.new() + parser = mkparser parser.file = manifest parser.parse } end - def test_defaults - basedir = File.join(tmpdir(), "defaulttesting") - @@tmpfiles << basedir - Dir.mkdir(basedir) - - defs1 = { - "testing" => "value" - } - - defs2 = { - "one" => "two", - "three" => "four", - "five" => false, - "seven" => "eight", - "nine" => true, - "eleven" => "twelve" - } - - mkdef = proc { |hash| - hash.collect { |arg, value| - "$%s = %s" % [arg, value] - }.join(", ") - } - manifest = File.join(basedir, "manifest") - File.open(manifest, "w") { |f| - f.puts " - define method(#{mkdef.call(defs1)}, $other) { - $variable = $testing - } - - define othermethod(#{mkdef.call(defs2)}, $goodness) { - $more = less - } - - method { - other => yayness - } - - othermethod { - goodness => rahness - } -" - - } - - ast = nil - assert_nothing_raised("Could not parse multiple files") { - parser = Puppet::Parser::Parser.new() - parser.file = manifest - ast = parser.parse - } - - assert(ast, "Did not receive AST while parsing defaults") - - scope = nil - assert_nothing_raised("Could not evaluate defaults parse tree") { - scope = Puppet::Parser::Scope.new() - scope.name = "parsetest" - scope.type = "parsetest" - objects = scope.evaluate(:ast => ast) - } - - method = nil - othermethod = nil - assert_nothing_raised { - method = scope.find { |child| - child.is_a?(Puppet::Parser::Scope) and child.type == "method" - } - defs1.each { |var, value| - curval = method.lookupvar(var) - assert_equal(value, curval, "Did not get default") - } - } - - assert_nothing_raised { - method = scope.find { |child| - child.is_a?(Puppet::Parser::Scope) and child.type == "othermethod" - } - defs2.each { |var, value| - curval = method.lookupvar(var) - assert_equal(value, curval, "Did not get default") - } - } - end - def test_trailingcomma path = tempfile() str = %{file { "#{path}": ensure => file, } } - parser = Puppet::Parser::Parser.new + parser = mkparser parser.string = str assert_nothing_raised("Could not parse trailing comma") { parser.parse } end def test_importedclasses imported = tempfile() importer = tempfile() made = tempfile() File.open(imported, "w") do |f| f.puts %{class foo { file { "#{made}": ensure => file }}} end File.open(importer, "w") do |f| f.puts %{import "#{imported}"\ninclude foo} end - parser = Puppet::Parser::Parser.new + parser = mkparser parser.file = importer # Make sure it parses fine assert_nothing_raised { parser.parse } # Now make sure it actually does the work assert_creates(importer, made) end # Make sure fully qualified and unqualified files can be imported def test_fqfilesandlocalfiles dir = tempfile() Dir.mkdir(dir) importer = File.join(dir, "site.pp") fullfile = File.join(dir, "full.pp") localfile = File.join(dir, "local.pp") files = [] File.open(importer, "w") do |f| f.puts %{import "#{fullfile}"\ninclude full\nimport "local.pp"\ninclude local} end fullmaker = tempfile() files << fullmaker File.open(fullfile, "w") do |f| f.puts %{class full { file { "#{fullmaker}": ensure => file }}} end localmaker = tempfile() files << localmaker File.open(localfile, "w") do |f| f.puts %{class local { file { "#{localmaker}": ensure => file }}} end - parser = Puppet::Parser::Parser.new + parser = mkparser parser.file = importer # Make sure it parses assert_nothing_raised { parser.parse } # Now make sure it actually does the work assert_creates(importer, *files) end # Make sure the parser adds '.pp' when necessary def test_addingpp dir = tempfile() Dir.mkdir(dir) importer = File.join(dir, "site.pp") localfile = File.join(dir, "local.pp") files = [] File.open(importer, "w") do |f| f.puts %{import "local"\ninclude local} end file = tempfile() files << file File.open(localfile, "w") do |f| f.puts %{class local { file { "#{file}": ensure => file }}} end - parser = Puppet::Parser::Parser.new + parser = mkparser parser.file = importer assert_nothing_raised { parser.parse } end # Make sure that file importing changes file relative names. def test_changingrelativenames dir = tempfile() Dir.mkdir(dir) Dir.mkdir(File.join(dir, "subdir")) top = File.join(dir, "site.pp") subone = File.join(dir, "subdir/subone") subtwo = File.join(dir, "subdir/subtwo") files = [] file = tempfile() files << file File.open(subone + ".pp", "w") do |f| f.puts %{class one { file { "#{file}": ensure => file }}} end otherfile = tempfile() files << otherfile File.open(subtwo + ".pp", "w") do |f| f.puts %{import "subone"\n class two inherits one { file { "#{otherfile}": ensure => file } }} end File.open(top, "w") do |f| f.puts %{import "subdir/subtwo"} end - parser = Puppet::Parser::Parser.new + parser = mkparser parser.file = top assert_nothing_raised { parser.parse } end - # Verify that collectable objects are marked that way. - def test_collectable - Puppet[:storeconfigs] = true - ["@port { ssh: protocols => tcp, number => 22 }", - "@port { ssh: protocols => tcp, number => 22; - smtp: protocols => tcp, number => 25 }"].each do |text| - parser = Puppet::Parser::Parser.new - parser.string = text - - ret = nil - assert_nothing_raised { - ret = parser.parse - } - - assert_instance_of(AST::ASTArray, ret) - - ret.each do |obj| - assert_instance_of(AST::ObjectDef, obj) - assert(obj.collectable, "Object was not marked collectable") - end - end - end - # Defaults are purely syntactical, so it doesn't make sense to be able to # collect them. def test_uncollectabledefaults string = "@Port { protocols => tcp }" - parser = Puppet::Parser::Parser.new - parser.string = string assert_raise(Puppet::ParseError) { - parser.parse + mkparser.parse(string) } end # Verify that we can parse collections def test_collecting Puppet[:storeconfigs] = true - text = "port <| |>" - parser = Puppet::Parser::Parser.new + text = "Port <| |>" + parser = mkparser parser.string = text ret = nil assert_nothing_raised { ret = parser.parse } assert_instance_of(AST::ASTArray, ret) ret.each do |obj| assert_instance_of(AST::Collection, obj) end end def test_emptyfile file = tempfile() File.open(file, "w") do |f| f.puts %{} end - parser = Puppet::Parser::Parser.new + parser = mkparser parser.file = file assert_nothing_raised { parser.parse } end def test_multiple_nodes_named file = tempfile() other = tempfile() File.open(file, "w") do |f| f.puts %{ node nodeA, nodeB { file { "#{other}": ensure => file } } } end - parser = Puppet::Parser::Parser.new + parser = mkparser parser.file = file ast = nil assert_nothing_raised { ast = parser.parse } end def test_emptyarrays str = %{$var = []\n} - parser = Puppet::Parser::Parser.new + parser = mkparser parser.string = str # Make sure it parses fine assert_nothing_raised { parser.parse } end # Make sure function names aren't reserved words. def test_functionnamecollision str = %{tag yayness tag(rahness) file { "/tmp/yayness": tag => "rahness", ensure => exists } } - parser = Puppet::Parser::Parser.new + parser = mkparser parser.string = str # Make sure it parses fine assert_nothing_raised { parser.parse } end def test_metaparams_in_definition_prototypes - parser = Puppet::Parser::Parser.new - - str1 = %{define mydef($schedule) {}} - parser.string = str1 + parser = mkparser - assert_raise(Puppet::ParseError) { - parser.parse - } - - str2 = %{define mydef($schedule = false) {}} - parser.string = str2 assert_raise(Puppet::ParseError) { - parser.parse + parser.parse %{define mydef($schedule) {}} } - str3 = %{define mydef($schedule = daily) {}} - parser.string = str3 - assert_nothing_raised { - parser.parse + parser.parse %{define adef($schedule = false) {}} + parser.parse %{define mydef($schedule = daily) {}} } end def test_parsingif - parser = Puppet::Parser::Parser.new() + parser = mkparser exec = proc do |val| %{exec { "/bin/echo #{val}": logoutput => true }} end str1 = %{if true { #{exec.call("true")} }} ret = nil assert_nothing_raised { ret = parser.parse(str1)[0] } assert_instance_of(Puppet::Parser::AST::IfStatement, ret) str2 = %{if true { #{exec.call("true")} } else { #{exec.call("false")} }} assert_nothing_raised { ret = parser.parse(str2)[0] } assert_instance_of(Puppet::Parser::AST::IfStatement, ret) assert_instance_of(Puppet::Parser::AST::Else, ret.else) end + + def test_hostclass + parser = mkparser + interp = parser.interp + + assert_nothing_raised { + parser.parse %{class myclass { class other {} }} + } + assert(interp.findclass("", "myclass"), "Could not find myclass") + assert(interp.findclass("", "myclass::other"), "Could not find myclass::other") + + assert_nothing_raised { + parser.parse "class base {} + class container { + class deep::sub inherits base {} + }" + } + sub = interp.findclass("", "container::deep::sub") + assert(sub, "Could not find sub") + assert_equal("base", sub.parentclass.type) + end + + def test_topnamespace + parser = mkparser + parser.interp.clear + + # Make sure we put the top-level code into a class called "" in + # the "" namespace + assert_nothing_raised do + out = parser.parse "" + + assert_nil(out) + assert_nil(parser.interp.findclass("", "")) + end + + # Now try something a touch more complicated + parser.interp.clear + assert_nothing_raised do + out = parser.parse "Exec { path => '/usr/bin:/usr/sbin' }" + assert_instance_of(AST::ASTArray, out) + assert_equal("", parser.interp.findclass("", "").type) + assert_equal("", parser.interp.findclass("", "").namespace) + assert_equal(out.object_id, parser.interp.findclass("", "").code.object_id) + end + end + + # Make sure virtual and exported resources work appropriately. + def test_virtualresources + Puppet[:storeconfigs] = true + [:virtual, :exported].each do |form| + parser = mkparser + + if form == :virtual + at = "@" + else + at = "@@" + end + + check = proc do |res| + # Real resources get marked virtual when exported + if form == :virtual or res.is_a?(Puppet::Parser::Resource) + assert(res.virtual, "Resource #{res.class} is not virtual") + end + if form == :virtual + assert(! res.exported, "Resource #{res.type} is exported") + else + assert(res.exported, "Resource #{res.type} is not exported") + end + end + + ret = nil + assert_nothing_raised do + ret = parser.parse("#{at}file { '/tmp/testing': owner => root }") + end + + assert_equal("/tmp/testing", ret[0].title.value) + # We always get an astarray back, so... + assert_instance_of(AST::ResourceDef, ret[0]) + check.call(ret[0]) + + # Now let's try it with multiple resources in the same spec + assert_nothing_raised do + ret = parser.parse("#{at}file { ['/tmp/1', '/tmp/2']: owner => root }") + end + + assert_instance_of(AST::ASTArray, ret) + ret.each do |res| + assert_instance_of(AST::ResourceDef, res) + check.call(res) + end + + # Now evaluate these + scope = mkscope + + klass = scope.interp.newclass "" + scope.source = klass + + assert_nothing_raised do + ret.evaluate :scope => scope + end + + # Make sure we can find both of them + %w{/tmp/1 /tmp/2}.each do |title| + res = scope.findresource("file[#{title}]") + assert(res, "Could not find %s" % title) + check.call(res) + end + end + end + + def test_collections + Puppet[:storeconfigs] = true + [:virtual, :exported].each do |form| + parser = mkparser + + if form == :virtual + arrow = "<||>" + else + arrow = "<<||>>" + end + + check = proc do |coll| + assert_instance_of(AST::Collection, coll) + assert_equal(form, coll.form) + end + + ret = nil + assert_nothing_raised do + ret = parser.parse("File #{arrow}") + end + check.call(ret[0]) + end + end + + def test_collectionexpressions + %w{== !=}.each do |oper| + str = "File <| title #{oper} '/tmp/testing' |>" + + parser = mkparser + + res = nil + assert_nothing_raised do + res = parser.parse(str)[0] + end + + assert_instance_of(AST::Collection, res) + + query = res.query + assert_instance_of(AST::CollExpr, query) + + assert_equal(:virtual, query.form) + assert_equal("title", query.test1.value) + assert_equal("/tmp/testing", query.test2.value) + assert_equal(oper, query.oper) + end + end + + def test_collectionstatements + %w{and or}.each do |joiner| + str = "File <| title == '/tmp/testing' #{joiner} owner == root |>" + + parser = mkparser + + res = nil + assert_nothing_raised do + res = parser.parse(str)[0] + end + + assert_instance_of(AST::Collection, res) + + query = res.query + assert_instance_of(AST::CollExpr, query) + + assert_equal(joiner, query.oper) + assert_instance_of(AST::CollExpr, query.test1) + assert_instance_of(AST::CollExpr, query.test2) + end + end + + def test_collectionstatements_with_parens + [ + "(title == '/tmp/testing' and owner == root) or owner == wheel", + "(title == '/tmp/testing')" + ].each do |test| + str = "File <| #{test} |>" + parser = mkparser + + res = nil + assert_nothing_raised("Could not parse '#{test}'") do + res = parser.parse(str)[0] + end + + assert_instance_of(AST::Collection, res) + + query = res.query + assert_instance_of(AST::CollExpr, query) + + #assert_equal(joiner, query.oper) + #assert_instance_of(AST::CollExpr, query.test1) + #assert_instance_of(AST::CollExpr, query.test2) + end + end end # $Id$ diff --git a/test/language/rails.rb b/test/language/rails.rb deleted file mode 100755 index ada4e0915..000000000 --- a/test/language/rails.rb +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/ruby - -require 'puppet' -require 'puppet/rails' -require 'puppet/parser/interpreter' -require 'puppet/parser/parser' -require 'puppet/client' -require 'puppettest' - -class TestRails < Test::Unit::TestCase - include PuppetTest::ParserTesting - - def test_includerails - assert_nothing_raised { - require 'puppet/rails' - } - end - - # Don't do any tests w/out this class - if defined? ActiveRecord::Base - def test_hostcache - # First make some objects - bucket = mk_transtree do |object, depth, width| - # and mark some of them collectable - if width % 2 == 1 - object.collectable = true - end - end - - # Now collect our facts - facts = {} - Facter.each do |fact, value| facts[fact] = value end - - assert_nothing_raised { - Puppet::Rails.init - } - - # Now try storing our crap - host = nil - assert_nothing_raised { - host = Puppet::Rails::Host.store( - :objects => bucket, - :facts => facts, - :host => facts["hostname"] - ) - } - - assert(host, "Did not create host") - - host = nil - assert_nothing_raised { - host = Puppet::Rails::Host.find_by_name(facts["hostname"]) - } - assert(host, "Could not find host object") - - assert(host.rails_objects, "No objects on host") - - assert_equal(facts["hostname"], host.facts["hostname"], - "Did not retrieve facts") - - inline_test_objectcollection(host) - end - - # This is called from another test, it just makes sense to split it out some - def inline_test_objectcollection(host) - # XXX For some reason, find_all doesn't work here at all. - collectable = [] - host.rails_objects.each do |obj| - if obj.collectable? - collectable << obj - end - end - - assert(collectable.length > 0, "Found no collectable objects") - - collectable.each do |obj| - trans = nil - assert_nothing_raised { - trans = obj.to_trans - } - # Make sure that the objects do not retain their collectable - # nature. - assert(!trans.collectable, "Object from db was collectable") - end - - # Now find all collectable objects directly through database APIs - - list = Puppet::Rails::RailsObject.find_all_by_collectable(true) - - assert_equal(collectable.length, list.length, - "Did not get the right number of objects") - end - else - $stderr.puts "Install Rails for Rails and Caching tests" - end -end - -# $Id$ diff --git a/test/language/resource.rb b/test/language/resource.rb new file mode 100755 index 000000000..12b1e7d2a --- /dev/null +++ b/test/language/resource.rb @@ -0,0 +1,391 @@ +#!/usr/bin/ruby + +require 'puppettest' +require 'puppettest/resourcetesting' +require 'puppettest/railstesting' + +class TestResource < 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 + + def test_initialize + args = {:type => "resource", :title => "testing", + :source => @source, :scope => @scope} + # Check our arg requirements + args.each do |name, value| + try = args.dup.delete(name) + assert_raise(Puppet::DevError) do + Parser::Resource.new(try) + end + end + + args[:params] = paramify @source, :one => "yay", :three => "rah" + + res = nil + assert_nothing_raised do + res = Parser::Resource.new(args) + end + + # Make sure it got the parameters correctly. + assert_equal("yay", res[:one]) + assert_equal("rah", res[:three]) + + assert_equal({:one => "yay", :three => "rah"}, res.to_hash) + end + + def test_override + res = mkresource + + # Now verify we can't override with any random class + assert_raise(Puppet::ParseError) do + res.set paramify(@scope.findclass("other"), "one" => "boo").shift + end + + # And that we can with a subclass + assert_nothing_raised do + res.set paramify(@scope.findclass("sub1"), "one" => "boo").shift + end + + # And that a different subclass can override a different parameter + assert_nothing_raised do + res.set paramify(@scope.findclass("sub2"), "three" => "boo").shift + end + + # But not the same one + assert_raise(Puppet::ParseError) do + res.set paramify(@scope.findclass("sub2"), "one" => "something").shift + end + end + + def test_merge + # Start with the normal one + res = mkresource + + # Now create a resource from a different scope + other = mkresource :source => other, :params => {"one" => "boo"} + + # Make sure we can't merge it + assert_raise(Puppet::ParseError) do + res.merge(other) + end + + # Make one from a subscope + other = mkresource :source => "sub1", :params => {"one" => "boo"} + + # Make sure it merges + assert_nothing_raised do + res.merge(other) + end + + assert_equal("boo", res["one"]) + end + + def test_paramcheck + # First make a builtin resource + res = nil + assert_nothing_raised do + res = Parser::Resource.new :type => "file", :title => tempfile(), + :source => @source, :scope => @scope + end + + %w{path group source schedule subscribe}.each do |param| + assert_nothing_raised("Param %s was considered invalid" % param) do + res.paramcheck(param) + end + end + + %w{this bad noness}.each do |param| + assert_raise(Puppet::ParseError, "%s was considered valid" % param) do + res.paramcheck(param) + end + end + + # Now create a defined resource + assert_nothing_raised do + res = Parser::Resource.new :type => "resource", :title => "yay", + :source => @source, :scope => @scope + end + + %w{one two three schedule subscribe}.each do |param| + assert_nothing_raised("Param %s was considered invalid" % param) do + res.paramcheck(param) + end + end + + %w{this bad noness}.each do |param| + assert_raise(Puppet::ParseError, "%s was considered valid" % param) do + res.paramcheck(param) + end + end + end + + def test_to_trans + # First try translating a builtin resource + res = Parser::Resource.new :type => "file", :title => "/tmp", + :source => @source, :scope => @scope, + :params => paramify(@source, :owner => "nobody", :mode => "644") + + obj = nil + assert_nothing_raised do + obj = res.to_trans + end + + assert_instance_of(Puppet::TransObject, obj) + + assert_equal(obj.type, res.type) + assert_equal(obj.name, res.title) + + # TransObjects use strings, resources use symbols + hash = obj.to_hash.inject({}) { |h,a| h[a[0].intern] = a[1]; h } + assert_equal(hash, res.to_hash) + end + + def test_adddefaults + # Set some defaults at the top level + top = {:one => "fun", :two => "shoe"} + + @scope.setdefaults("resource", paramify(@source, top)) + + # Make a resource at that level + res = Parser::Resource.new :type => "resource", :title => "yay", + :source => @source, :scope => @scope + + # Add the defaults + assert_nothing_raised do + res.adddefaults + end + + # And make sure we got them + top.each do |p, v| + assert_equal(v, res[p]) + end + + # Now got a bit lower + other = @scope.newscope + + # And create a resource + lowerres = Parser::Resource.new :type => "resource", :title => "funtest", + :source => @source, :scope => other + + assert_nothing_raised do + lowerres.adddefaults + end + + # And check + top.each do |p, v| + assert_equal(v, lowerres[p]) + end + + # Now add some of our own defaults + lower = {:one => "shun", :three => "free"} + other.setdefaults("resource", paramify(@source, lower)) + otherres = Parser::Resource.new :type => "resource", :title => "yaytest", + :source => @source, :scope => other + + should = top.dup + # Make sure the lower defaults beat the higher ones. + lower.each do |p, v| should[p] = v end + + otherres.adddefaults + + should.each do |p,v| + assert_equal(v, otherres[p]) + end + end + + def test_evaluate + # Make a definition that we know will, um, do something + @interp.newdefine "evaltest", + :arguments => [%w{one}, ["two", stringobj("755")]], + :code => resourcedef("file", "/tmp", + "owner" => varref("one"), "mode" => varref("two")) + + res = Parser::Resource.new :type => "evaltest", :title => "yay", + :source => @source, :scope => @scope, + :params => paramify(@source, :one => "nobody") + + # Now try evaluating + ret = nil + assert_nothing_raised do + ret = res.evaluate + end + + # Make sure we can find our object now + result = @scope.findresource("file[/tmp]") + + # Now make sure we got the code we expected. + assert_instance_of(Puppet::Parser::Resource, result) + + assert_equal("file", result.type) + assert_equal("/tmp", result.title) + assert_equal("nobody", result["owner"]) + assert_equal("755", result["mode"]) + + # And that we cannot find the old resource + assert_nil(@scope.findresource("evaltest[yay]"), + "Evaluated resource was not deleted") + end + + def test_addoverrides + # First create an override for an object that doesn't yet exist + over1 = mkresource :source => "sub1", :params => {:one => "yay"} + + assert_nothing_raised do + @scope.setoverride(over1) + end + + assert(over1.override, "Override was not marked so") + + # Now make the resource + res = mkresource :source => "base", :params => {:one => "rah", + :three => "foo"} + + # And add it to our scope + @scope.setresource(res) + + # And make sure over1 has not yet taken affect + assert_equal("foo", res[:three], "Lost value") + + # Now add an immediately binding override + over2 = mkresource :source => "sub1", :params => {:three => "yay"} + + assert_nothing_raised do + @scope.setoverride(over2) + end + + # And make sure it worked + assert_equal("yay", res[:three], "Override 2 was ignored") + + # Now add our late-binding override + assert_nothing_raised do + res.addoverrides + end + + # And make sure they're still around + assert_equal("yay", res[:one], "Override 1 lost") + assert_equal("yay", res[:three], "Override 2 lost") + + # And finally, make sure that there are no remaining overrides + assert_nothing_raised do + res.addoverrides + end + end + + def test_proxymethods + res = Parser::Resource.new :type => "evaltest", :title => "yay", + :source => @source, :scope => @scope + + assert_equal("evaltest", res.type) + assert_equal("yay", res.title) + assert_equal(false, res.builtin?) + end + + def test_addmetaparams + res = Parser::Resource.new :type => "evaltest", :title => "yay", + :source => @source, :scope => @scope + + assert_nil(res[:schedule], "Got schedule already") + @scope.setvar("schedule", "daily") + + assert_nothing_raised do + res.addmetaparams + end + + assert_equal("daily", res[:schedule], "Did not get metaparam") + assert_nil(res[:noop], "Got invalid metaparam") + end + + def test_reference_conversion + # First try it as a normal string + ref = Parser::Resource::Reference.new(:type => "file", :title => "/tmp/ref1") + + # Now create an obj that uses it + res = mkresource :type => "file", :title => "/tmp/resource", + :params => {:require => ref} + + trans = nil + assert_nothing_raised do + trans = res.to_trans + end + + assert_instance_of(Array, trans["require"]) + assert_equal(["file", "/tmp/ref1"], trans["require"]) + + # Now try it when using an array of references. + two = Parser::Resource::Reference.new(:type => "file", :title => "/tmp/ref2") + res = mkresource :type => "file", :title => "/tmp/resource2", + :params => {:require => [ref, two]} + + trans = nil + assert_nothing_raised do + trans = res.to_trans + end + + assert_instance_of(Array, trans["require"][0]) + trans["require"].each do |val| + assert_instance_of(Array, val) + assert_equal("file", val[0]) + assert(val[1] =~ /\/tmp\/ref[0-9]/, + "Was %s instead of the file name" % val[1]) + end + end + + # This is a bit of a weird one -- the user should not actually know + # that components exist, so we want references to act like they're not + # builtin + def test_components_are_not_builtin + ref = Parser::Resource::Reference.new(:type => "component", :title => "yay") + + assert_nil(ref.builtintype, "Component was considered builtin") + end + if defined? ActiveRecord::Base + def test_store + railsinit + res = mkresource :type => "file", :title => "/tmp/testing", + :source => @source, :scope => @scope, + :params => {:owner => "root", :mode => "755"} + + # We also need a Rails Host to store under + host = Puppet::Rails::Host.new(:name => Facter.hostname) + + obj = nil + assert_nothing_raised do + obj = res.store(host) + end + + assert_instance_of(Puppet::Rails::RailsResource, obj) + + assert_nothing_raised do + Puppet::Util.benchmark(:info, "Saved host") do + host.save + end + end + + # Now make sure we can find it again + assert_nothing_raised do + obj = Puppet::Rails::RailsResource.find_by_host_id_and_restype_and_title( + host.id, res.type, res.title + ) + end + assert_instance_of(Puppet::Rails::RailsResource, obj) + + # Make sure we get the parameters back + obj.rails_parameters.each do |param| + assert_equal(res[param[:name]], param[:value], + "%s was different" % param[:name]) + end + end + end +end + +# $Id$ diff --git a/test/language/scope.rb b/test/language/scope.rb index 85189627d..b9401e38b 100755 --- a/test/language/scope.rb +++ b/test/language/scope.rb @@ -1,915 +1,591 @@ #!/usr/bin/ruby require 'puppet' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/client' require 'puppettest' +require 'puppettest/parsertesting' +require 'puppettest/resourcetesting' # so, what kind of things do we want to test? # we don't need to test function, since we're confident in the # library tests. We do, however, need to test how things are actually # working in the language. # so really, we want to do things like test that our ast is correct # and test whether we've got things in the right scopes class TestScope < Test::Unit::TestCase include PuppetTest::ParserTesting + include PuppetTest::ResourceTesting def to_ary(hash) hash.collect { |key,value| [key,value] } end def test_variables scope = nil over = "over" scopes = [] vars = [] values = {} ovalues = [] 10.times { |index| # slap some recursion in there - scope = Puppet::Parser::Scope.new(:parent => scope) + scope = mkscope(:parent => scope) scopes.push scope var = "var%s" % index value = rand(1000) ovalue = rand(1000) ovalues.push ovalue vars.push var values[var] = value # set the variable in the current scope assert_nothing_raised { scope.setvar(var,value) } # this should override previous values assert_nothing_raised { scope.setvar(over,ovalue) } assert_equal(value,scope.lookupvar(var)) #puts "%s vars, %s scopes" % [vars.length,scopes.length] i = 0 vars.zip(scopes) { |v,s| # this recurses all the way up the tree as necessary val = nil oval = nil # look up the values using the bottom scope assert_nothing_raised { val = scope.lookupvar(v) oval = scope.lookupvar(over) } # verify they're correct assert_equal(values[v],val) assert_equal(ovalue,oval) # verify that we get the most recent value assert_equal(ovalue,scope.lookupvar(over)) # verify that they aren't available in upper scopes if parent = s.parent val = nil assert_nothing_raised { val = parent.lookupvar(v) } assert_equal("", val, "Did not get empty string on missing var") # and verify that the parent sees its correct value assert_equal(ovalues[i - 1],parent.lookupvar(over)) end i += 1 } } end def test_declarative # set to declarative - top = Puppet::Parser::Scope.new(:declarative => true) - sub = Puppet::Parser::Scope.new(:parent => top) + top = mkscope(:declarative => true) + sub = mkscope(:parent => top) assert_nothing_raised { top.setvar("test","value") } assert_raise(Puppet::ParseError) { top.setvar("test","other") } assert_nothing_raised { sub.setvar("test","later") } assert_raise(Puppet::ParseError) { top.setvar("test","yeehaw") } end def test_notdeclarative # set to not declarative - top = Puppet::Parser::Scope.new(:declarative => false) - sub = Puppet::Parser::Scope.new(:parent => top) + top = mkscope(:declarative => false) + sub = mkscope(:parent => top) assert_nothing_raised { top.setvar("test","value") } assert_nothing_raised { top.setvar("test","other") } assert_nothing_raised { sub.setvar("test","later") } assert_nothing_raised { sub.setvar("test","yayness") } end - def test_defaults - scope = nil - over = "over" - - scopes = [] - vars = [] - values = {} - ovalues = [] + def test_setdefaults + interp, scope, source = mkclassframing - defs = Hash.new { |hash,key| - hash[key] = Hash.new(nil) - } - - prevdefs = Hash.new { |hash,key| - hash[key] = Hash.new(nil) - } - - params = %w{a list of parameters that could be used for defaults} + # The setdefaults method doesn't really check what we're doing, + # so we're just going to use fake defaults here. - types = %w{a set of types that could be used to set defaults} + # First do a simple local lookup + params = paramify(source, :one => "fun", :two => "shoe") + origshould = {} + params.each do |p| origshould[p.name] = p end + assert_nothing_raised do + scope.setdefaults(:file, params) + end - 10.times { |index| - scope = Puppet::Parser::Scope.new(:parent => scope) - scopes.push scope + ret = nil + assert_nothing_raised do + ret = scope.lookupdefaults(:file) + end - tmptypes = [] - - # randomly create defaults for a random set of types - tnum = rand(5) - tnum.times { |t| - # pick a type - #Puppet.debug "Type length is %s" % types.length - #s = rand(types.length) - #Puppet.debug "Type num is %s" % s - #type = types[s] - #Puppet.debug "Type is %s" % s - type = types[rand(types.length)] - if tmptypes.include?(type) - Puppet.debug "Duplicate type %s" % type - redo - else - tmptypes.push type - end + assert_equal(origshould, ret) - Puppet.debug "type is %s" % type + # Now create a subscope and add some more params. + newscope = scope.newscope - d = {} + newparams = paramify(source, :one => "shun", :three => "free") + assert_nothing_raised { + newscope.setdefaults(:file, newparams) + } - # randomly assign some parameters - num = rand(4) - num.times { |n| - param = params[rand(params.length)] - if d.include?(param) - Puppet.debug "Duplicate param %s" % param - redo - else - d[param] = rand(1000) - end - } + # And make sure we get the appropriate ones back + should = {} + params.each do |p| should[p.name] = p end + newparams.each do |p| should[p.name] = p end - # and then add a consistent type - d["always"] = rand(1000) + assert_nothing_raised do + ret = newscope.lookupdefaults(:file) + end - d.each { |var,val| - defs[type][var] = val - } + assert_equal(should, ret) - assert_nothing_raised { - scope.setdefaults(type,to_ary(d)) - } - fdefs = nil - assert_nothing_raised { - fdefs = scope.lookupdefaults(type) - } + # Make sure we still only get the originals from the top scope + assert_nothing_raised do + ret = scope.lookupdefaults(:file) + end - # now, make sure that reassignment fails if we're - # in declarative mode - assert_raise(Puppet::ParseError) { - scope.setdefaults(type,[%w{always funtest}]) - } + assert_equal(origshould, ret) - # assert that we have collected the same values - assert_equal(defs[type],fdefs) + # Now create another scope and make sure we only get the top defaults + otherscope = scope.newscope + assert_equal(origshould, otherscope.lookupdefaults(:file)) - # now assert that our parent still finds the same defaults - # it got last time - if parent = scope.parent - unless prevdefs[type].nil? - assert_equal(prevdefs[type],parent.lookupdefaults(type)) - end - end - d.each { |var,val| - prevdefs[type][var] = val - } - } - } + # And make sure none of the scopes has defaults for other types + [scope, newscope, otherscope].each do |sc| + assert_equal({}, sc.lookupdefaults(:exec)) + end end def test_strinterp - scope = Puppet::Parser::Scope.new() + scope = mkscope() assert_nothing_raised { scope.setvar("test","value") } val = nil assert_nothing_raised { val = scope.strinterp("string ${test}") } assert_equal("string value", val) assert_nothing_raised { val = scope.strinterp("string ${test} ${test} ${test}") } assert_equal("string value value value", val) assert_nothing_raised { val = scope.strinterp("string $test ${test} $test") } assert_equal("string value value value", val) assert_nothing_raised { val = scope.strinterp("string \\$test") } assert_equal("string $test", val) assert_nothing_raised { val = scope.strinterp("\\$test string") } assert_equal("$test string", val) end - # Test some of the host manipulations - def test_hostlookup - top = Puppet::Parser::Scope.new() - - # Create a deep scope tree, so that we know we're doing a deeply recursive - # search. - mid1 = Puppet::Parser::Scope.new(:parent => top) - mid2 = Puppet::Parser::Scope.new(:parent => mid1) - mid3 = Puppet::Parser::Scope.new(:parent => mid2) - child1 = Puppet::Parser::Scope.new(:parent => mid3) - mida = Puppet::Parser::Scope.new(:parent => top) - midb = Puppet::Parser::Scope.new(:parent => mida) - midc = Puppet::Parser::Scope.new(:parent => midb) - child2 = Puppet::Parser::Scope.new(:parent => midc) - - # verify we can set a host - assert_nothing_raised("Could not create host") { - child1.setnode("testing", AST::Node.new( - :type => "testing", - :code => :notused - ) - ) - } - - # Verify we cannot redefine it - assert_raise(Puppet::ParseError, "Duplicate host creation succeeded") { - child2.setnode("testing", AST::Node.new( - :type => "testing", - :code => :notused - ) - ) - } - - # Now verify we can find the host again - host = nil - assert_nothing_raised("Host lookup failed") { - hash = top.node("testing") - host = hash[:node] - } - - assert(host, "Could not find host") - assert(host.code == :notused, "Host is not what we stored") - end - - # Verify that two statements about a file within the same scope tree - # will cause a conflict. - def test_noconflicts - filename = tempfile() - children = [] - - # create the parent class - children << classobj("one", :code => AST::ASTArray.new( - :children => [ - fileobj(filename, "owner" => "root") - ] - )) - - # now create a child class with differ values - children << classobj("two", - :code => AST::ASTArray.new( - :children => [ - fileobj(filename, "owner" => "bin") - ] - )) - - # Now call the child class - assert_nothing_raised("Could not add AST nodes for calling") { - children << AST::ObjectDef.new( - :type => nameobj("two"), - :name => nameobj("yayness"), - :params => astarray() - ) << AST::ObjectDef.new( - :type => nameobj("one"), - :name => nameobj("yayness"), - :params => astarray() - ) - } - - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } - - objects = nil - scope = nil - - # Here's where we should encounter the failure. It should find that - # it has already created an object with that name, and this should result - # in some pukey-pukeyness. - assert_raise(Puppet::ParseError) { - scope = Puppet::Parser::Scope.new() - objects = scope.evaluate(:ast => top) - } - end - - # Verify that statements about the same element within the same scope - # cause a conflict. - def test_failonconflictinsamescope - filename = tempfile() - children = [] - - # Now call the child class - assert_nothing_raised("Could not add AST nodes for calling") { - children << fileobj(filename, "owner" => "root") - children << fileobj(filename, "owner" => "bin") - } - - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } - - objects = nil - scope = nil - - # Here's where we should encounter the failure. It should find that - # it has already created an object with that name, and this should result - # in some pukey-pukeyness. - assert_raise(Puppet::ParseError) { - scope = Puppet::Parser::Scope.new() - scope.top = true - objects = scope.evaluate(:ast => top) - } - end - - # Verify that we override statements that we find within our scope - def test_suboverrides - filename = tempfile() - children = [] - - # create the parent class - children << classobj("parent", :code => AST::ASTArray.new( - :children => [ - fileobj(filename, "owner" => "root") - ] - )) - - # now create a child class with differ values - children << classobj("child", :parentclass => nameobj("parent"), - :code => AST::ASTArray.new( - :children => [ - fileobj(filename, "owner" => "bin") - ] - )) - - # Now call the child class - assert_nothing_raised("Could not add AST nodes for calling") { - children << AST::ObjectDef.new( - :type => nameobj("child"), - :name => nameobj("yayness"), - :params => astarray() - ) - } - - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } - - objects = nil - scope = nil - assert_nothing_raised("Could not evaluate") { - scope = Puppet::Parser::Scope.new() - objects = scope.evaluate(:ast => top) - } - - assert_equal(1, objects.length, "Returned too many objects: %s" % - objects.inspect) - - assert_equal(1, objects[0].length, "Returned too many objects: %s" % - objects[0].inspect) - - assert_nothing_raised { - file = objects[0][0] - assert_equal("bin", file["owner"], "Value did not override correctly") - } - end - - def test_multipletypes - scope = Puppet::Parser::Scope.new() - children = [] - - # create the parent class - children << classobj("aclass") - children << classobj("aclass") - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } - - scope = nil - assert_raise(Puppet::ParseError) { - scope = Puppet::Parser::Scope.new() - objects = top.evaluate(:scope => scope) - } - end - - # Verify that definitions have a different context than classes. - def test_newsubcontext - filename = tempfile() - children = [] - - # Create a component - children << compobj("comp", :code => AST::ASTArray.new( - :children => [ - fileobj(filename, "owner" => "root" ) - ] - )) - - # Now create a class that modifies the same file and also - # calls the component - children << classobj("klass", :code => AST::ASTArray.new( - :children => [ - fileobj(filename, "owner" => "bin" ), - AST::ObjectDef.new( - :type => nameobj("comp"), - :params => astarray() - ) - ] - )) - - # Now call the class - children << AST::ObjectDef.new( - :type => nameobj("klass"), - :params => astarray() - ) - - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } + def test_setclass + interp, scope, source = mkclassframing - trans = nil - scope = nil - #assert_nothing_raised { - assert_raise(Puppet::ParseError, "A conflict was allowed") { - scope = Puppet::Parser::Scope.new() - trans = scope.evaluate(:ast => top) - } - # scope = Puppet::Parser::Scope.new() - # trans = scope.evaluate(:ast => top) - #} - end + base = scope.findclass("base") + assert(base, "Could not find base class") + assert(! scope.setclass?(base), "Class incorrectly set") + assert(! scope.classlist.include?("base"), "Class incorrectly in classlist") + assert_nothing_raised do + scope.setclass base + end - def test_defaultswithmultiplestatements - path = tempfile() + assert(scope.setclass?(base), "Class incorrectly unset") + assert(scope.classlist.include?("base"), "Class not in classlist") - stats = [] - stats << defaultobj("file", "group" => "root") - stats << fileobj(path, "owner" => "root") - stats << fileobj(path, "mode" => "755") + # Now try it with a normal string + assert_raise(Puppet::DevError) do + scope.setclass "string" + end - top = AST::ASTArray.new( - :file => __FILE__, - :line => __LINE__, - :children => stats - ) - scope = Puppet::Parser::Scope.new() - trans = nil - assert_nothing_raised { - trans = scope.evaluate(:ast => top) - } + assert(! scope.setclass?("string"), "string incorrectly set") - obj = trans.find do |obj| obj.is_a? Puppet::TransObject end + # Set "" in the class list, and make sure it doesn't show up in the return + top = scope.findclass("") + assert(top, "Could not find top class") + scope.setclass top - assert(obj, "Could not retrieve file obj") - assert_equal("root", obj["group"], "Default did not take") - assert_equal("root", obj["owner"], "Owner did not take") - assert_equal("755", obj["mode"], "Mode did not take") + assert(! scope.classlist.include?(""), "Class list included empty") end - def test_validclassnames - scope = Puppet::Parser::Scope.new() + def test_validtags + scope = mkscope() - ["a class", "Class", "a.class"].each do |bad| + ["a class", "a.class"].each do |bad| assert_raise(Puppet::ParseError, "Incorrectly allowed %s" % bad.inspect) do - scope.setclass(object_id, bad) + scope.tag(bad) end end - ["a-class", "a_class", "class", "yayNess"].each do |good| + ["a-class", "a_class", "Class", "class", "yayNess"].each do |good| assert_nothing_raised("Incorrectly banned %s" % good.inspect) do - scope.setclass(object_id, good) + scope.tag(good) end end end def test_tagfunction - scope = Puppet::Parser::Scope.new() + scope = mkscope() assert_nothing_raised { scope.function_tag(["yayness", "booness"]) } - assert(scope.classlist.include?("yayness"), "tag 'yayness' did not get set") - assert(scope.classlist.include?("booness"), "tag 'booness' did not get set") + assert(scope.tags.include?("yayness"), "tag 'yayness' did not get set") + assert(scope.tags.include?("booness"), "tag 'booness' did not get set") # Now verify that the 'tagged' function works correctly assert(scope.function_tagged("yayness"), "tagged function incorrectly returned false") assert(scope.function_tagged("booness"), "tagged function incorrectly returned false") assert(! scope.function_tagged("funtest"), "tagged function incorrectly returned true") end def test_includefunction - scope = Puppet::Parser::Scope.new() - - one = tempfile() - two = tempfile() - - children = [] - - children << classobj("one", :code => AST::ASTArray.new( - :children => [ - fileobj(one, "owner" => "root") - ] - )) + interp = mkinterp + scope = mkscope :interp => interp - children << classobj("two", :code => AST::ASTArray.new( - :children => [ - fileobj(two, "owner" => "root") - ] - )) + myclass = interp.newclass "myclass" + otherclass = interp.newclass "otherclass" - children << Puppet::Parser::AST::Function.new( + function = Puppet::Parser::AST::Function.new( :name => "include", :ftype => :statement, :arguments => AST::ASTArray.new( - :children => [nameobj("one"), nameobj("two")] + :children => [nameobj("myclass"), nameobj("otherclass")] ) ) - top = AST::ASTArray.new(:children => children) - - #assert_nothing_raised { - # scope.function_include(["one", "two"]) - #} - - assert_nothing_raised { - scope.evaluate(:ast => top) - } - - - assert(scope.classlist.include?("one"), "tag 'one' did not get set") - assert(scope.classlist.include?("two"), "tag 'two' did not get set") + assert_nothing_raised do + function.evaluate :scope => scope + end - # Now verify that the 'tagged' function works correctly - assert(scope.function_tagged("one"), - "tagged function incorrectly returned false") - assert(scope.function_tagged("two"), - "tagged function incorrectly returned false") + [myclass, otherclass].each do |klass| + assert(scope.setclass?(klass), + "%s was not set" % klass.fqname) + end end def test_definedfunction - scope = Puppet::Parser::Scope.new() - - one = tempfile() - two = tempfile() - - children = [] - - children << classobj("one", :code => AST::ASTArray.new( - :children => [ - fileobj(one, "owner" => "root") - ] - )) - - children << classobj("two", :code => AST::ASTArray.new( - :children => [ - fileobj(two, "owner" => "root") - ] - )) - - top = AST::ASTArray.new(:children => children) + interp = mkinterp + %w{one two}.each do |name| + interp.newdefine name + end - top.evaluate(:scope => scope) + scope = mkscope :interp => interp assert_nothing_raised { %w{one two file user}.each do |type| assert(scope.function_defined([type]), "Class #{type} was not considered defined") end assert(!scope.function_defined(["nopeness"]), "Class 'nopeness' was incorrectly considered defined") } - - - end - - # Make sure components acquire defaults. - def test_defaultswithcomponents - children = [] - - # Create a component - filename = tempfile() - args = AST::ASTArray.new( - :file => tempfile(), - :line => rand(100), - :children => [nameobj("argument")] - ) - children << compobj("comp", :args => args, :code => AST::ASTArray.new( - :children => [ - fileobj(filename, "owner" => varref("argument") ) - ] - )) - - # Create a default - children << defaultobj("comp", "argument" => "yayness") - - # lastly, create an object that calls our third component - children << objectdef("comp", "boo", {"argument" => "parentfoo"}) - - trans = assert_evaluate(children) - - flat = trans.flatten - - assert(!flat.empty?, "Got no objects back") - - assert_equal("parentfoo", flat[0]["owner"], "default did not take") end # Make sure we know what we consider to be truth. def test_truth assert_equal(true, Puppet::Parser::Scope.true?("a string"), "Strings not considered true") assert_equal(true, Puppet::Parser::Scope.true?(true), "True considered true") assert_equal(false, Puppet::Parser::Scope.true?(""), "Empty strings considered true") assert_equal(false, Puppet::Parser::Scope.true?(false), "false considered true") end # Verify scope context is handled correctly. def test_scopeinside - scope = Puppet::Parser::Scope.new() + scope = mkscope() one = :one two = :two # First just test the basic functionality. assert_nothing_raised { scope.inside :one do assert_equal(:one, scope.inside, "Context did not get set") end assert_nil(scope.inside, "Context did not revert") } # Now make sure error settings work. assert_raise(RuntimeError) { scope.inside :one do raise RuntimeError, "This is a failure, yo" end } assert_nil(scope.inside, "Context did not revert") # Now test it a bit deeper in. assert_nothing_raised { scope.inside :one do scope.inside :two do assert_equal(:two, scope.inside, "Context did not get set") end assert_equal(:one, scope.inside, "Context did not get set") end assert_nil(scope.inside, "Context did not revert") } # And lastly, check errors deeper in assert_nothing_raised { scope.inside :one do begin scope.inside :two do raise "a failure" end rescue end assert_equal(:one, scope.inside, "Context did not get set") end assert_nil(scope.inside, "Context did not revert") } end if defined? ActiveRecord - # Verify that we recursively mark as collectable the results of collectable + # Verify that we recursively mark as exported the results of collectable # components. - def test_collectablecomponents + def test_exportedcomponents + interp, scope, source = mkclassframing children = [] args = AST::ASTArray.new( :file => tempfile(), :line => rand(100), :children => [nameobj("arg")] ) + # Create a top-level component - children << compobj("one", :args => args) + interp.newdefine "one", :arguments => [%w{arg}], + :code => AST::ASTArray.new( + :children => [ + resourcedef("file", "/tmp", {"owner" => varref("arg")}) + ] + ) # And a component that calls it - children << compobj("two", :args => args, :code => AST::ASTArray.new( - :children => [ - objectdef("one", "ptest", {"arg" => "parentfoo"}) - ] - )) + interp.newdefine "two", :arguments => [%w{arg}], + :code => AST::ASTArray.new( + :children => [ + resourcedef("one", "ptest", {"arg" => varref("arg")}) + ] + ) # And then a third component that calls the second - children << compobj("three", :args => args, :code => AST::ASTArray.new( - :children => [ - objectdef("two", "yay", {"arg" => "parentfoo"}) - ] - )) + interp.newdefine "three", :arguments => [%w{arg}], + :code => AST::ASTArray.new( + :children => [ + resourcedef("two", "yay", {"arg" => varref("arg")}) + ] + ) # lastly, create an object that calls our third component - obj = objectdef("three", "boo", {"arg" => "parentfoo"}) - - # And mark it as collectable - obj.collectable = true + obj = resourcedef("three", "boo", {"arg" => "parentfoo"}) - children << obj + # And mark it as exported + obj.exported = true - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } + obj.evaluate :scope => scope - trans = nil - scope = nil - assert_nothing_raised { - scope = Puppet::Parser::Scope.new() - trans = scope.evaluate(:ast => top) - } + # And then evaluate it + interp.evaliterate(scope) %w{file}.each do |type| - objects = scope.exported(type) + objects = scope.lookupexported(type) assert(!objects.empty?, "Did not get an exported %s" % type) end end # Verify that we can both store and collect an object in the same # run, whether it's in the same scope as a collection or a different # scope. def test_storeandcollect Puppet[:storeconfigs] = true Puppet::Rails.clear Puppet::Rails.init sleep 1 children = [] file = tempfile() File.open(file, "w") { |f| f.puts " class yay { - @host { myhost: ip => \"192.168.0.2\" } + @@host { myhost: ip => \"192.168.0.2\" } } include yay -@host { puppet: ip => \"192.168.0.3\" } -host <||>" +@@host { puppet: ip => \"192.168.0.3\" } +Host <<||>>" } interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new( :Manifest => file, :UseNodes => false, :ForkSave => false ) } objects = nil # We run it twice because we want to make sure there's no conflict # if we pull it up from the database. 2.times { |i| assert_nothing_raised { - objects = interp.run("localhost", {}) + objects = interp.run("localhost", {"hostname" => "localhost"}) } flat = objects.flatten %w{puppet myhost}.each do |name| assert(flat.find{|o| o.name == name }, "Did not find #{name}") end } end + else + $stderr.puts "No ActiveRecord -- skipping collection tests" + end - # Verify that we cannot override differently exported objects - def test_exportedoverrides - filename = tempfile() - children = [] + # Make sure tags behave appropriately. + def test_tags + interp, scope, source = mkclassframing - obj = fileobj(filename, "owner" => "root") - obj.collectable = true - # create the parent class - children << classobj("parent", :code => AST::ASTArray.new( - :children => [ - obj - ] - )) - - # now create a child class with differ values - children << classobj("child", :parentclass => nameobj("parent"), - :code => AST::ASTArray.new( - :children => [ - fileobj(filename, "owner" => "bin") - ] - )) - - # Now call the child class - assert_nothing_raised("Could not add AST nodes for calling") { - children << AST::ObjectDef.new( - :type => nameobj("child"), - :name => nameobj("yayness"), - :params => astarray() - ) - } + # First make sure we can only set legal tags + ["an invalid tag", "-anotherinvalid", "bad*tag"].each do |tag| + assert_raise(Puppet::ParseError, "Tag #{tag} was considered valid") do + scope.tag tag + end + end - top = nil - assert_nothing_raised("Could not create top object") { - top = AST::ASTArray.new( - :children => children - ) - } + # Now make sure good tags make it through. + tags = %w{good-tag yaytag GoodTag another_tag} + tags.each do |tag| + assert_nothing_raised("Tag #{tag} was considered invalid") do + scope.tag tag + end + end - objects = nil - scope = nil - assert_raise(Puppet::ParseError, "Incorrectly allowed override") { - scope = Puppet::Parser::Scope.new() - objects = scope.evaluate(:ast => top) - } + # And make sure we get each of them. + ptags = scope.tags + tags.each do |tag| + assert(ptags.include?(tag), "missing #{tag}") + end + + + # Now create a subscope and set some tags there + newscope = scope.newscope(:type => 'subscope') + + # set some tags + newscope.tag "onemore", "yaytag" + + # And make sure we get them plus our parent tags + assert_equal((ptags + %w{onemore subscope}).sort, newscope.tags.sort) end - else - $stderr.puts "No ActiveRecord -- skipping collection tests" + + # Make sure we successfully translate objects + def test_translate + interp, scope, source = mkclassframing + + # Create a define that we'll be using + interp.newdefine("wrapper", :code => AST::ASTArray.new(:children => [ + resourcedef("file", varref("name"), "owner" => "root") + ])) + + # Now create a resource that uses that define + define = mkresource(:type => "wrapper", :title => "/tmp/testing", + :scope => scope, :source => source, :params => :none) + + scope.setresource define + + # And a normal resource + scope.setresource mkresource(:type => "file", :title => "/tmp/rahness", + :scope => scope, :source => source, + :params => {:owner => "root"}) + + # Evaluate the the define thing. + define.evaluate + + # Now the scope should have a resource and a subscope. Translate the + # whole thing. + ret = nil + assert_nothing_raised do + ret = scope.translate + end + + assert_instance_of(Puppet::TransBucket, ret) + + ret.each do |obj| + assert(obj.is_a?(Puppet::TransBucket) || obj.is_a?(Puppet::TransObject), + "Got a non-transportable object %s" % obj.class) + end + + rahness = ret.find { |c| c.type == "file" and c.name == "/tmp/rahness" } + assert(rahness, "Could not find top-level file") + assert_equal("root", rahness["owner"]) + + bucket = ret.find { |c| c.class == Puppet::TransBucket and c.name == "/tmp/testing" } + assert(bucket, "Could not find define bucket") + + testing = bucket.find { |c| c.type == "file" and c.name == "/tmp/testing" } + assert(testing, "Could not find define file") + assert_equal("root", testing["owner"]) + end end + +# $Id$ diff --git a/test/language/snippets.rb b/test/language/snippets.rb index 05ac066eb..f2597c8d0 100755 --- a/test/language/snippets.rb +++ b/test/language/snippets.rb @@ -1,552 +1,559 @@ #!/usr/bin/ruby -w require 'puppet' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppet/client' require 'puppet/server' require 'puppettest' class TestSnippets < Test::Unit::TestCase include PuppetTest include ObjectSpace def self.snippetdir - PuppetTest.exampledir "code", "snippets" + PuppetTest.datadir "snippets" end def snippet(name) File.join(self.class.snippetdir, name) end def file2ast(file) parser = Puppet::Parser::Parser.new() parser.file = file ast = parser.parse return ast end def snippet2ast(text) parser = Puppet::Parser::Parser.new() parser.string = text ast = parser.parse return ast end def client args = { :Listen => false } Puppet::Client.new(args) end def ast2scope(ast) interp = Puppet::Parser::Interpreter.new( :ast => ast, :client => client() ) scope = Puppet::Parser::Scope.new() ast.evaluate(scope) return scope end def scope2objs(scope) objs = scope.to_trans end def snippet2scope(snippet) ast = snippet2ast(snippet) scope = ast2scope(ast) end def snippet2objs(snippet) ast = snippet2ast(snippet) scope = ast2scope(ast) objs = scope2objs(scope) end def states(type) states = type.validstates end def metaparams(type) mparams = [] Puppet::Type.eachmetaparam { |param| mparams.push param } mparams end def params(type) params = [] type.parameters.each { |name,state| params.push name } params end def randthing(thing,type) list = self.send(thing,type) list[rand(list.length)] end def randeach(type) [:states, :metaparams, :params].collect { |thing| randthing(thing,type) } end @@snippets = { true => [ %{File { mode => 755 }} ], } def disabled_test_defaults Puppet::Type.eachtype { |type| next if type.name == :puppet or type.name == :component rands = randeach(type) name = type.name.to_s.capitalize [0..1, 0..2].each { |range| params = rands[range] paramstr = params.collect { |param| "%s => fake" % param }.join(", ") str = "%s { %s }" % [name, paramstr] scope = nil assert_nothing_raised { scope = snippet2scope(str) } defaults = nil assert_nothing_raised { defaults = scope.lookupdefaults(name) } p defaults params.each { |param| puts "%s => '%s'" % [name,param] assert(defaults.include?(param)) } } } end # this is here in case no tests get defined; otherwise we get a warning def test_nothing end def snippet_filecreate(trans) %w{a b c d}.each { |letter| file = "/tmp/create%stest" % letter Puppet.info "testing %s" % file assert(Puppet.type(:file)[file], "File %s does not exist" % file) assert(FileTest.exists?(file)) @@tmpfiles << file } %w{a b}.each { |letter| file = "/tmp/create%stest" % letter assert(File.stat(file).mode & 007777 == 0755) } assert_nothing_raised { trans.rollback } %w{a b c d}.each { |letter| file = "/tmp/create%stest" % letter assert(! FileTest.exists?(file), "File %s still exists" % file) } end def snippet_simpledefaults(trans) file = "/tmp/defaulttest" @@tmpfiles << file assert(FileTest.exists?(file), "File %s does not exist" % file) assert(File.stat(file).mode & 007777 == 0755) assert_nothing_raised { trans.rollback } assert(! FileTest.exists?(file), "%s still exists" % file) end def snippet_simpleselector(trans) files = %w{a b c d}.collect { |letter| "/tmp/snippetselect%stest" % letter } @@tmpfiles += files files.each { |file| assert(FileTest.exists?(file), "File %s does not exist" % file) assert(File.stat(file).mode & 007777 == 0755, "File %s is the incorrect mode" % file) @@tmpfiles << file } assert_nothing_raised { trans.rollback } files.each { |file| assert(! FileTest.exists?(file), "%s still exists" % file) } end def snippet_classpathtest(trans) file = "/tmp/classtest" @@tmpfiles << file assert(FileTest.exists?(file)) obj = nil assert_nothing_raised { obj = Puppet.type(:file)[file] } assert_nothing_raised { assert_equal( "//testing/component[componentname]/file=/tmp/classtest", obj.path) #Puppet.err obj.path } assert_nothing_raised { trans.rollback } assert(! FileTest.exists?(file), "%s still exists" % file) end def snippet_argumentdefaults(trans) file1 = "/tmp/argumenttest1" file2 = "/tmp/argumenttest2" @@tmpfiles << file1 @@tmpfiles << file2 assert(FileTest.exists?(file1)) assert(File.stat(file1).mode & 007777 == 0755) assert(FileTest.exists?(file2)) assert(File.stat(file2).mode & 007777 == 0644) end def snippet_casestatement(trans) files = %w{ /tmp/existsfile /tmp/existsfile2 /tmp/existsfile3 /tmp/existsfile4 /tmp/existsfile5 } files.each { |file| assert(FileTest.exists?(file), "File %s is missing" % file) assert(File.stat(file).mode & 007777 == 0755, "File %s is not 755" % file) } assert_nothing_raised { trans.rollback } end def snippet_implicititeration(trans) files = %w{a b c d e f g h}.collect { |l| "/tmp/iteration%stest" % l } files.each { |file| @@tmpfiles << file assert(FileTest.exists?(file), "File %s does not exist" % file) assert(File.stat(file).mode & 007777 == 0755, "File %s is not 755" % file) } assert_nothing_raised { trans.rollback } files.each { |file| assert(! FileTest.exists?(file), "file %s still exists" % file) } end def snippet_multipleinstances(trans) files = %w{a b c}.collect { |l| "/tmp/multipleinstances%s" % l } files.each { |file| @@tmpfiles << file assert(FileTest.exists?(file), "File %s does not exist" % file) assert(File.stat(file).mode & 007777 == 0755, "File %s is not 755" % file) } assert_nothing_raised { trans.rollback } files.each { |file| assert(! FileTest.exists?(file), "file %s still exists" % file) } end def snippet_namevartest(trans) file = "/tmp/testfiletest" dir = "/tmp/testdirtest" @@tmpfiles << file @@tmpfiles << dir assert(FileTest.file?(file), "File %s does not exist" % file) assert(FileTest.directory?(dir), "Directory %s does not exist" % dir) end def snippet_scopetest(trans) file = "/tmp/scopetest" @@tmpfiles << file assert(FileTest.file?(file), "File %s does not exist" % file) assert(File.stat(file).mode & 007777 == 0755, "File %s is not 755" % file) end def snippet_failmissingexecpath(trans) file = "/tmp/exectesting1" execfile = "/tmp/execdisttesting" @@tmpfiles << file @@tmpfiles << execfile assert(!FileTest.exists?(execfile), "File %s exists" % execfile) end def snippet_selectorvalues(trans) nums = %w{1 2 3 4 5} files = nums.collect { |n| "/tmp/selectorvalues%s" % n } files.each { |f| @@tmpfiles << f assert(FileTest.exists?(f), "File %s does not exist" % f) assert(File.stat(f).mode & 007777 == 0755, "File %s is not 755" % f) } end def snippet_singleselector(trans) nums = %w{1 2 3} files = nums.collect { |n| "/tmp/singleselector%s" % n } files.each { |f| @@tmpfiles << f assert(FileTest.exists?(f), "File %s does not exist" % f) assert(File.stat(f).mode & 007777 == 0755, "File %s is not 755" % f) } end def snippet_falsevalues(trans) file = "/tmp/falsevaluesfalse" @@tmpfiles << file assert(FileTest.exists?(file), "File %s does not exist" % file) end def disabled_snippet_classargtest(trans) [1,2].each { |num| file = "/tmp/classargtest%s" % num @@tmpfiles << file assert(FileTest.file?(file), "File %s does not exist" % file) assert(File.stat(file).mode & 007777 == 0755, "File %s is not 755" % file) } end def snippet_classheirarchy(trans) [1,2,3].each { |num| file = "/tmp/classheir%s" % num @@tmpfiles << file assert(FileTest.file?(file), "File %s does not exist" % file) assert(File.stat(file).mode & 007777 == 0755, "File %s is not 755" % file) } end def snippet_singleary(trans) [1,2,3,4].each { |num| file = "/tmp/singleary%s" % num @@tmpfiles << file assert(FileTest.file?(file), "File %s does not exist" % file) } end def snippet_classincludes(trans) [1,2,3].each { |num| file = "/tmp/classincludes%s" % num @@tmpfiles << file assert(FileTest.file?(file), "File %s does not exist" % file) assert(File.stat(file).mode & 007777 == 0755, "File %s is not 755" % file) } end def snippet_componentmetaparams(trans) ["/tmp/component1", "/tmp/component2"].each { |file| assert(FileTest.file?(file), "File %s does not exist" % file) } end def snippet_aliastest(trans) %w{/tmp/aliastest /tmp/aliastest2 /tmp/aliastest3}.each { |file| assert(FileTest.file?(file), "File %s does not exist" % file) } end def snippet_singlequote(trans) { 1 => 'a $quote', 2 => 'some "\yayness\"' }.each { |count, str| path = "/tmp/singlequote%s" % count assert(FileTest.exists?(path), "File %s is missing" % path) text = File.read(path) assert_equal(str, text) } end # There's no way to actually retrieve the list of classes from the # transaction. def snippet_tag(trans) @@tmpfiles << "/tmp/settestingness" end # Make sure that set tags are correctly in place, yo. def snippet_tagged(trans) tags = {"testing" => true, "yayness" => false, "both" => false, "bothtrue" => true, "define" => true} tags.each do |tag, retval| @@tmpfiles << "/tmp/tagged#{tag}true" @@tmpfiles << "/tmp/tagged#{tag}false" assert(FileTest.exists?("/tmp/tagged#{tag}#{retval.to_s}"), "'tagged' did not return %s with %s" % [retval, tag]) end end def snippet_defineoverrides(trans) file = "/tmp/defineoverrides1" assert(FileTest.exists?(file), "File does not exist") assert_equal(0755, filemode(file)) end def snippet_deepclassheirarchy(trans) 5.times { |i| i += 1 file = "/tmp/deepclassheir%s" % i assert(FileTest.exists?(file), "File %s does not exist" % file) } end def snippet_emptyclass(trans) # There's nothing to check other than that it works end def snippet_emptyexec(trans) assert(FileTest.exists?("/tmp/emptyexectest"), "Empty exec was ignored") @@tmpfiles << "/tmp/emptyexextest" end + def snippet_multisubs(trans) + path = "/tmp/multisubtest" + assert(FileTest.exists?(path), "Did not create file") + assert_equal("sub2", File.read(path), "sub2 did not override content") + assert_equal(0755, filemode(path), "sub1 did not override mode") + end + def disabled_snippet_dirchmod(trans) dirs = %w{a b}.collect { |letter| "/tmp/dirchmodtest%s" % letter } @@tmpfiles << dirs dirs.each { |dir| assert(FileTest.directory?(dir)) } assert(File.stat("/tmp/dirchmodtesta").mode & 007777 == 0755) assert(File.stat("/tmp/dirchmodtestb").mode & 007777 == 0700) assert_nothing_raised { trans.rollback } end # Iterate across each of the snippets and create a test. Dir.entries(snippetdir).sort.each { |file| next if file =~ /^\./ mname = "snippet_" + file.sub(/\.pp$/, '') if self.method_defined?(mname) #eval("alias %s %s" % [testname, mname]) testname = ("test_" + mname).intern self.send(:define_method, testname) { # first parse the file server = Puppet::Server::Master.new( :Manifest => snippet(file), :Local => true ) client = Puppet::Client::MasterClient.new( :Master => server, :Cache => false ) assert(client.local) assert_nothing_raised { client.getconfig() } client = Puppet::Client::MasterClient.new( :Master => server, :Cache => false ) assert(client.local) # Now do it again Puppet::Type.allclear assert_nothing_raised { client.getconfig() } trans = nil assert_nothing_raised { trans = client.apply() } Puppet::Type.eachtype { |type| type.each { |obj| # don't worry about this for now #unless obj.name == "puppet[top]" or # obj.is_a?(Puppet.type(:schedule)) # assert(obj.parent, "%s has no parent" % obj.name) #end assert(obj.name) if obj.is_a?(Puppet.type(:file)) @@tmpfiles << obj[:path] end } } assert_nothing_raised { self.send(mname, trans) } client.clear } mname = mname.intern end } end # $Id$ diff --git a/test/language/transportable.rb b/test/language/transportable.rb index 217bb3370..a25500ee8 100755 --- a/test/language/transportable.rb +++ b/test/language/transportable.rb @@ -1,104 +1,105 @@ #!/usr/bin/ruby require 'puppet' require 'puppet/transportable' require 'puppettest' +require 'puppettest/parsertesting' require 'yaml' class TestTransportable < Test::Unit::TestCase include PuppetTest::ParserTesting def test_yamldumpobject obj = mk_transobject obj.to_yaml_properties str = nil assert_nothing_raised { str = YAML.dump(obj) } newobj = nil assert_nothing_raised { newobj = YAML.load(str) } assert(newobj.name, "Object has no name") assert(newobj.type, "Object has no type") end def test_yamldumpbucket objects = %w{/etc/passwd /etc /tmp /var /dev}.collect { |d| mk_transobject(d) } bucket = mk_transbucket(*objects) str = nil assert_nothing_raised { str = YAML.dump(bucket) } newobj = nil assert_nothing_raised { newobj = YAML.load(str) } assert(newobj.name, "Bucket has no name") assert(newobj.type, "Bucket has no type") end # Verify that we correctly strip out collectable objects, since they should # not be sent to the client. def test_collectstrip top = mk_transtree do |object, depth, width| if width % 2 == 1 object.collectable = true end end assert(top.flatten.find_all { |o| o.collectable }.length > 0, "Could not find any collectable objects") # Now strip out the collectable objects top.collectstrip! # And make sure they're actually gone assert_equal(0, top.flatten.find_all { |o| o.collectable }.length, "Still found collectable objects") end # Make sure our 'delve' command is working def test_delve top = mk_transtree do |object, depth, width| if width % 2 == 1 object.collectable = true end end objects = [] buckets = [] collectable = [] count = 0 assert_nothing_raised { top.delve do |object| count += 1 if object.is_a? Puppet::TransBucket buckets << object else objects << object if object.collectable collectable << object end end end } top.flatten.each do |obj| assert(objects.include?(obj), "Missing obj %s[%s]" % [obj.type, obj.name]) end assert_equal(collectable.length, top.flatten.find_all { |o| o.collectable }.length, "Found incorrect number of collectable objects") end end # $Id$ diff --git a/test/lib/puppettest.rb b/test/lib/puppettest.rb index e7ee99c35..c6ce54b93 100644 --- a/test/lib/puppettest.rb +++ b/test/lib/puppettest.rb @@ -1,212 +1,231 @@ require 'puppet' require 'test/unit' module PuppetTest # Find the root of the Puppet tree; this is not the test directory, but # the parent of that dir. - def basedir + def basedir(*list) unless defined? @@basedir case $0 when /rake_test_loader/ @@basedir = File.dirname(Dir.getwd) else dir = nil - if /^#{File::SEPARATOR}.+\.rb/ + if $0 =~ /^#{File::SEPARATOR}.+\.rb/ dir = $0 else dir = File.join(Dir.getwd, $0) end 3.times { dir = File.dirname(dir) } @@basedir = dir end end - @@basedir + if list.empty? + @@basedir + else + File.join(@@basedir, *list) + end end def cleanup(&block) @@cleaners << block end - def datadir - File.join(basedir, "test", "data") + def datadir(*list) + File.join(basedir, "test", "data", *list) end def exampledir(*args) unless defined? @@exampledir @@exampledir = File.join(basedir, "examples") end if args.empty? return @@exampledir else return File.join(@@exampledir, *args) end end module_function :basedir, :datadir, :exampledir + # Rails clobbers RUBYLIB, thanks + def libsetup + curlibs = ENV["RUBYLIB"].split(":") + $:.reject do |dir| dir =~ /^\/usr/ end.each do |dir| + unless curlibs.include?(dir) + curlibs << dir + end + end + + ENV["RUBYLIB"] = curlibs.join(":") + end + def rake? $0 =~ /rake_test_loader/ end def setup @memoryatstart = Puppet::Util.memory if defined? @@testcount @@testcount += 1 else @@testcount = 0 end @configpath = File.join(tmpdir, self.class.to_s + "configdir" + @@testcount.to_s + "/" ) unless defined? $user and $group $user = nonrootuser().uid.to_s $group = nonrootgroup().gid.to_s end Puppet[:user] = $user Puppet[:group] = $group Puppet[:confdir] = @configpath Puppet[:vardir] = @configpath unless File.exists?(@configpath) Dir.mkdir(@configpath) end @@tmpfiles = [@configpath, tmpdir()] @@tmppids = [] @@cleaners = [] # If we're running under rake, then disable debugging and such. if rake? and ! Puppet[:debug] Puppet::Log.close Puppet::Log.newdestination tempfile() Puppet[:httplog] = tempfile() else Puppet::Log.newdestination :console Puppet::Log.level = :debug #$VERBOSE = 1 Puppet.info @method_name + Puppet[:trace] = true end #if $0 =~ /.+\.rb/ or Puppet[:debug] # Puppet::Log.newdestination :console # Puppet::Log.level = :debug # #$VERBOSE = 1 # Puppet.info @method_name #else # Puppet::Log.close # Puppet::Log.newdestination tempfile() # Puppet[:httplog] = tempfile() #end Puppet[:ignoreschedules] = true Puppet[:trace] = true end def tempfile if defined? @@tmpfilenum @@tmpfilenum += 1 else @@tmpfilenum = 1 end f = File.join(self.tmpdir(), self.class.to_s + "_" + @method_name + @@tmpfilenum.to_s) @@tmpfiles << f return f end def tstdir dir = tempfile() Dir.mkdir(dir) return dir end def tmpdir unless defined? @tmpdir and @tmpdir @tmpdir = case Facter["operatingsystem"].value when "Darwin": "/private/tmp" when "SunOS": "/var/tmp" else "/tmp" end @tmpdir = File.join(@tmpdir, "puppettesting") unless File.exists?(@tmpdir) FileUtils.mkdir_p(@tmpdir) File.chmod(01777, @tmpdir) end end @tmpdir end def teardown stopservices @@cleaners.each { |cleaner| cleaner.call() } @@tmpfiles.each { |file| unless file =~ /tmp/ puts "Not deleting tmpfile %s" % file next end if FileTest.exists?(file) system("chmod -R 755 %s" % file) system("rm -rf %s" % file) end } @@tmpfiles.clear @@tmppids.each { |pid| %x{kill -INT #{pid} 2>/dev/null} } @@tmppids.clear Puppet::Type.allclear Puppet::Storage.clear - Puppet::Rails.clear + if defined? Puppet::Rails + Puppet::Rails.clear + end Puppet.clear @memoryatend = Puppet::Util.memory diff = @memoryatend - @memoryatstart if diff > 1000 Puppet.info "%s#%s memory growth (%s to %s): %s" % [self.class, @method_name, @memoryatstart, @memoryatend, diff] end # reset all of the logs Puppet::Log.close # Just in case there are processes waiting to die... require 'timeout' begin Timeout::timeout(5) do Process.waitall end rescue Timeout::Error # just move on end if File.stat("/dev/null").mode & 007777 != 0666 File.open("/tmp/nullfailure", "w") { |f| f.puts self.class } exit(74) end end end require 'puppettest/support' require 'puppettest/filetesting' require 'puppettest/fakes' require 'puppettest/exetest' require 'puppettest/parsertesting' require 'puppettest/servertest' # $Id$ diff --git a/test/lib/puppettest/parsertesting.rb b/test/lib/puppettest/parsertesting.rb index e0606c501..1a6b5b12a 100644 --- a/test/lib/puppettest/parsertesting.rb +++ b/test/lib/puppettest/parsertesting.rb @@ -1,347 +1,358 @@ require 'puppettest' +require 'puppet/rails' module PuppetTest::ParserTesting include PuppetTest AST = Puppet::Parser::AST def astarray(*args) AST::ASTArray.new( - :children => args - ) + :children => args + ) end - def classobj(name, args = {}) - args[:type] ||= nameobj(name) - args[:code] ||= AST::ASTArray.new( - :file => __FILE__, - :line => __LINE__, - :children => [ - varobj("%svar" % name, "%svalue" % name), - fileobj("/%s" % name) - ] - ) - assert_nothing_raised("Could not create class %s" % name) { - return AST::ClassDef.new(args) - } + def mkinterp(args = {}) + args[:Code] ||= "" + args[:Local] ||= true + Puppet::Parser::Interpreter.new(args) + end + + def mkparser + Puppet::Parser::Parser.new(mkinterp) + end + + def mkscope(hash = {}) + hash[:interp] ||= mkinterp + hash[:source] ||= (hash[:interp].findclass("", "") || + hash[:interp].newclass("")) + + unless hash[:source] + raise "Could not find source for scope" + end + Puppet::Parser::Scope.new(hash) + end + + def classobj(name, hash = {}) + hash[:file] ||= __FILE__ + hash[:line] ||= __LINE__ + hash[:type] ||= name + AST::HostClass.new(hash) end def tagobj(*names) args = {} newnames = names.collect do |name| if name.is_a? AST name else nameobj(name) end end args[:type] = astarray(*newnames) assert_nothing_raised("Could not create tag %s" % names.inspect) { return AST::Tag.new(args) } end - def compobj(name, args = {}) - args[:file] ||= tempfile() - args[:line] ||= rand(100) - args[:type] ||= nameobj(name) - args[:args] ||= AST::ASTArray.new( - :file => tempfile(), - :line => rand(100), - :children => [] - ) - args[:code] ||= AST::ASTArray.new( - :file => tempfile(), - :line => rand(100), - :children => [ - varobj("%svar" % name, "%svalue" % name), - fileobj("/%s" % name) - ] - ) - assert_nothing_raised("Could not create compdef %s" % name) { - return AST::CompDef.new(args) + def resourcedef(type, title, params) + unless title.is_a?(AST) + title = stringobj(title) + end + assert_nothing_raised("Could not create %s %s" % [type, title]) { + return AST::ResourceDef.new( + :file => __FILE__, + :line => __LINE__, + :title => title, + :type => type, + :params => resourceinst(params) + ) } end - def objectdef(type, name, params) + def resourceoverride(type, title, params) assert_nothing_raised("Could not create %s %s" % [type, name]) { - return AST::ObjectDef.new( - :file => __FILE__, - :line => __LINE__, - :name => stringobj(name), - :type => nameobj(type), - :params => objectinst(params) - ) + return AST::ResourceOverride.new( + :file => __FILE__, + :line => __LINE__, + :object => resourceref(type, title), + :type => type, + :params => resourceinst(params) + ) + } + end + + def resourceref(type, title) + assert_nothing_raised("Could not create %s %s" % [type, title]) { + return AST::ResourceRef.new( + :file => __FILE__, + :line => __LINE__, + :type => type, + :title => stringobj(title) + ) } end def fileobj(path, hash = {"owner" => "root"}) assert_nothing_raised("Could not create file %s" % path) { - return objectdef("file", path, hash) - # return AST::ObjectDef.new( - # :file => tempfile(), - # :line => rand(100), - # :name => stringobj(path), - # :type => nameobj("file"), - # :params => objectinst(hash) - # ) + return resourcedef("file", path, hash) } end def nameobj(name) assert_nothing_raised("Could not create name %s" % name) { return AST::Name.new( :file => tempfile(), :line => rand(100), :value => name ) } end def typeobj(name) assert_nothing_raised("Could not create type %s" % name) { return AST::Type.new( :file => tempfile(), :line => rand(100), :value => name ) } end def nodedef(name) assert_nothing_raised("Could not create node %s" % name) { return AST::NodeDef.new( :file => tempfile(), :line => rand(100), :names => nameobj(name), :code => AST::ASTArray.new( :children => [ varobj("%svar" % name, "%svalue" % name), fileobj("/%s" % name) ] ) ) } end - def objectinst(hash) - assert_nothing_raised("Could not create object instance") { + def resourceinst(hash) + assert_nothing_raised("Could not create resource instance") { params = hash.collect { |param, value| - objectparam(param, value) + resourceparam(param, value) } - return AST::ObjectInst.new( + return AST::ResourceInst.new( :file => tempfile(), :line => rand(100), :children => params ) } end - def objectparam(param, value) + def resourceparam(param, value) # Allow them to pass non-strings in if value.is_a?(String) value = stringobj(value) end assert_nothing_raised("Could not create param %s" % param) { - return AST::ObjectParam.new( + return AST::ResourceParam.new( :file => tempfile(), :line => rand(100), - :param => nameobj(param), + :param => param, :value => value ) } end def stringobj(value) AST::String.new( :file => tempfile(), :line => rand(100), :value => value ) end def varobj(name, value) unless value.is_a? AST value = stringobj(value) end assert_nothing_raised("Could not create %s code" % name) { return AST::VarDef.new( :file => tempfile(), :line => rand(100), :name => nameobj(name), :value => value ) } end def varref(name) assert_nothing_raised("Could not create %s variable" % name) { return AST::Variable.new( :file => __FILE__, :line => __LINE__, :value => name ) } end def argobj(name, value) assert_nothing_raised("Could not create %s compargument" % name) { return AST::CompArgument.new( :children => [nameobj(name), stringobj(value)] ) } end def defaultobj(type, params) pary = [] params.each { |p,v| - pary << AST::ObjectParam.new( + pary << AST::ResourceParam.new( :file => __FILE__, :line => __LINE__, - :param => nameobj(p), + :param => p, :value => stringobj(v) ) } past = AST::ASTArray.new( :file => __FILE__, :line => __LINE__, :children => pary ) assert_nothing_raised("Could not create defaults for %s" % type) { - return AST::TypeDefaults.new( + return AST::ResourceDefaults.new( :file => __FILE__, :line => __LINE__, - :type => typeobj(type), + :type => type, :params => past ) } end def taggedobj(name, ftype = :statement) functionobj("tagged", name, ftype) end def functionobj(function, name, ftype = :statement) func = nil assert_nothing_raised do func = Puppet::Parser::AST::Function.new( :name => function, :ftype => ftype, :arguments => AST::ASTArray.new( :children => [nameobj(name)] ) ) end return func end # This assumes no nodes def assert_creates(manifest, *files) interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new( :Manifest => manifest, :UseNodes => false ) } config = nil assert_nothing_raised { config = interp.run(Facter["hostname"].value, {}) } comp = nil assert_nothing_raised { comp = config.to_type } assert_apply(comp) files.each do |file| assert(FileTest.exists?(file), "Did not create %s" % file) end end def mk_transobject(file = "/etc/passwd") obj = nil assert_nothing_raised { obj = Puppet::TransObject.new("file", file) obj["owner"] = "root" obj["mode"] = "644" } return obj end - def mk_transbucket(*objects) + def mk_transbucket(*resources) bucket = nil assert_nothing_raised { bucket = Puppet::TransBucket.new bucket.name = "yayname" bucket.type = "yaytype" } - objects.each { |o| bucket << o } + resources.each { |o| bucket << o } return bucket end - # Make a tree of objects, yielding if desired + # Make a tree of resources, yielding if desired def mk_transtree(depth = 4, width = 2) top = nil assert_nothing_raised { top = Puppet::TransBucket.new top.name = "top" top.type = "bucket" } bucket = top file = tempfile() depth.times do |i| - objects = [] + resources = [] width.times do |j| path = tempfile + i.to_s obj = Puppet::TransObject.new("file", path) obj["owner"] = "root" obj["mode"] = "644" # Yield, if they want if block_given? yield(obj, i, j) end - objects << obj + resources << obj end - newbucket = mk_transbucket(*objects) + newbucket = mk_transbucket(*resources) bucket.push newbucket bucket = newbucket end return top end - # Take a list of AST objects, evaluate them, and return the results + # Take a list of AST resources, evaluate them, and return the results def assert_evaluate(children) top = nil assert_nothing_raised("Could not create top object") { top = AST::ASTArray.new( - :children => children - ) + :children => children + ) } trans = nil scope = nil assert_nothing_raised { scope = Puppet::Parser::Scope.new() trans = scope.evaluate(:ast => top) } return trans end end # $Id$ diff --git a/test/lib/puppettest/railstesting.rb b/test/lib/puppettest/railstesting.rb new file mode 100644 index 000000000..3a32d0c9e --- /dev/null +++ b/test/lib/puppettest/railstesting.rb @@ -0,0 +1,34 @@ +module PuppetTest::RailsTesting + Parser = Puppet::Parser + AST = Puppet::Parser::AST + include PuppetTest::ParserTesting + + def railsinit + Puppet::Rails.init + end + + def railsresource(type = "file", title = "/tmp/testing", params = {}) + railsinit + + # We need a host for resources + host = Puppet::Rails::Host.new(:name => Facter.value("hostname")) + + # Now build a resource + resource = host.rails_resources.build( + :title => title, :restype => type, + :exported => true + ) + + # Now add some params + params.each do |param, value| + resource.rails_parameters.build( + :name => param, :value => value + ) + end + + # Now save the whole thing + host.save + end +end + +# $Id$ diff --git a/test/lib/puppettest/resourcetesting.rb b/test/lib/puppettest/resourcetesting.rb new file mode 100644 index 000000000..a7d183ca7 --- /dev/null +++ b/test/lib/puppettest/resourcetesting.rb @@ -0,0 +1,64 @@ +module PuppetTest::ResourceTesting + Parser = Puppet::Parser + AST = Puppet::Parser::AST + def mkclassframing(interp = nil) + interp ||= mkinterp + + interp.newdefine("resource", :arguments => [%w{one}, %w{two value}, %w{three}]) + interp.newclass("") + source = interp.newclass("base") + interp.newclass("sub1", :parent => "base") + interp.newclass("sub2", :parent => "base") + interp.newclass("other") + + scope = Parser::Scope.new(:interp => interp) + scope.source = source + + return interp, scope, source + end + + def mkresource(args = {}) + + if args[:scope] and ! args[:source] + args[:source] = args[:scope].source + end + + unless args[:scope] + unless defined? @scope + raise "Must set @scope to mkresource" + end + end + + {:type => "resource", :title => "testing", + :source => @source, :scope => @scope}.each do |param, value| + args[param] ||= value + end + + unless args[:source].is_a?(Puppet::Parser::AST::HostClass) + args[:source] = args[:scope].findclass(args[:source]) + end + + params = args[:params] || {:one => "yay", :three => "rah"} + if args[:params] == :none + args.delete(:params) + else + args[:params] = paramify args[:source], params + end + + Parser::Resource.new(args) + end + + def param(name, value, source) + Parser::Resource::Param.new(:name => name, :value => value, :source => source) + end + + def paramify(source, hash) + hash.collect do |name, value| + Parser::Resource::Param.new( + :name => name, :value => value, :source => source + ) + end + end +end + +# $Id$ diff --git a/test/lib/puppettest/support/utils.rb b/test/lib/puppettest/support/utils.rb index 00ea1a1c9..ea2d5483c 100644 --- a/test/lib/puppettest/support/utils.rb +++ b/test/lib/puppettest/support/utils.rb @@ -1,147 +1,147 @@ require 'puppettest' module PuppetTest def gcdebug(type) Puppet.warning "%s: %s" % [type, ObjectSpace.each_object(type) { |o| }] end # # TODO: I think this method needs to be renamed to something a little more # explanatory. # def newobj(type, name, hash) transport = Puppet::TransObject.new(name, "file") transport[:path] = path transport[:ensure] = "file" assert_nothing_raised { file = transport.to_type } end # stop any services that might be hanging around def stopservices if stype = Puppet::Type.type(:service) stype.each { |service| service[:ensure] = :stopped service.evaluate } end end # TODO: rewrite this to use the 'etc' module. def setme # retrieve the user name id = %x{id}.chomp if id =~ /uid=\d+\(([^\)]+)\)/ @me = $1 else puts id end unless defined? @me raise "Could not retrieve user name; 'id' did not work" end end def run_events(type, trans, events, msg) case type when :evaluate, :rollback: # things are hunky-dory else raise Puppet::DevError, "Incorrect run_events type" end method = type newevents = nil assert_nothing_raised("Transaction %s %s failed" % [type, msg]) { newevents = trans.send(method).reject { |e| e.nil? }.collect { |e| e.event } } assert_equal(events, newevents, "Incorrect %s %s events" % [type, msg]) return trans end # If there are any fake data files, retrieve them def fakedata(dir) ary = [basedir, "test"] ary += dir.split("/") dir = File.join(ary) unless FileTest.exists?(dir) raise Puppet::DevError, "No fakedata dir %s" % dir end files = Dir.entries(dir).reject { |f| f =~ /^\./ }.collect { |f| File.join(dir, f) } return files end def fakefile(name) ary = [basedir, "test"] ary += name.split("/") file = File.join(ary) unless FileTest.exists?(file) raise Puppet::DevError, "No fakedata file %s" % file end return file end # wrap how to retrieve the masked mode def filemode(file) File.stat(file).mode & 007777 end def memory Puppet::Util.memory end # a list of files that we can parse for testing def textfiles - textdir = File.join(exampledir,"code", "snippets") + textdir = datadir "snippets" Dir.entries(textdir).reject { |f| f =~ /^\./ or f =~ /fail/ }.each { |f| yield File.join(textdir, f) } end def failers - textdir = File.join(exampledir,"code", "failers") + textdir = datadir "failers" # only parse this one file now files = Dir.entries(textdir).reject { |file| file =~ %r{\.swp} }.reject { |file| file =~ %r{\.disabled} }.collect { |file| File.join(textdir,file) }.find_all { |file| FileTest.file?(file) }.sort.each { |file| Puppet.debug "Processing %s" % file yield file } end def newcomp(*ary) name = nil if ary[0].is_a?(String) name = ary.shift else name = ary[0].title end comp = Puppet.type(:component).create(:name => name) ary.each { |item| comp.push item } return comp end end # $Id$ diff --git a/test/other/config.rb b/test/other/config.rb index 0afe8979b..09d6abe3b 100755 --- a/test/other/config.rb +++ b/test/other/config.rb @@ -1,805 +1,802 @@ #!/usr/bin/env ruby require 'puppet' require 'puppet/config' require 'puppettest' +require 'puppettest/parsertesting' class TestConfig < Test::Unit::TestCase include PuppetTest + include PuppetTest::ParserTesting def check_for_users count = Puppet::Type.type(:user).inject(0) { |c,o| c + 1 } assert(count > 0, "Found no users") end def check_to_transportable(config) trans = nil assert_nothing_raised("Could not convert to a transportable") { trans = config.to_transportable } comp = nil assert_nothing_raised("Could not convert transportable to component") { comp = trans.to_type } check_for_users() assert_nothing_raised("Could not retrieve transported config") { comp.retrieve } end def check_to_manifest(config) manifest = nil assert_nothing_raised("Could not convert to a manifest") { manifest = config.to_manifest } Puppet[:parseonly] = true - parser = Puppet::Parser::Parser.new() - objects = nil - assert_nothing_raised("Could not parse generated manifest") { - parser.string = manifest - objects = parser.parse - } - scope = Puppet::Parser::Scope.new - assert_nothing_raised("Could not compile objects") { - scope.evaluate(:ast => objects) - } + interp = nil + assert_nothing_raised do + interp = mkinterp :Code => manifest, :UseNodes => false + end + trans = nil - assert_nothing_raised("Could not convert objects to transportable") { - trans = scope.to_trans - } + assert_nothing_raised do + trans = interp.evaluate(nil, {}) + end assert_nothing_raised("Could not instantiate objects") { trans.to_type } check_for_users() end def check_to_comp(config) comp = nil assert_nothing_raised("Could not convert to a component") { comp = config.to_component } assert_nothing_raised("Could not retrieve component") { comp.retrieve } check_for_users() end def check_to_config(config) newc = config.dup newfile = tempfile() File.open(newfile, "w") { |f| f.print config.to_config } assert_nothing_raised("Could not parse generated configuration") { newc.parse(newfile) } assert_equal(config, newc, "Configurations are not equal") end def mkconfig c = nil assert_nothing_raised { c = Puppet::Config.new } return c end def test_addbools c = mkconfig assert_nothing_raised { c.setdefaults(:testing, :booltest => [true, "testing"]) } assert(c[:booltest]) c = mkconfig assert_nothing_raised { c.setdefaults(:testing, :booltest => ["true", "testing"]) } assert(c[:booltest]) assert_nothing_raised { c[:booltest] = false } assert(! c[:booltest], "Booltest is not false") assert_nothing_raised { c[:booltest] = "false" } assert(! c[:booltest], "Booltest is not false") assert_raise(Puppet::Error) { c[:booltest] = "yayness" } assert_raise(Puppet::Error) { c[:booltest] = "/some/file" } end def test_strings c = mkconfig val = "this is a string" assert_nothing_raised { c.setdefaults(:testing, :strtest => [val, "testing"]) } assert_equal(val, c[:strtest]) # Verify that variables are interpolated assert_nothing_raised { c.setdefaults(:testing, :another => ["another $strtest", "testing"]) } assert_equal("another #{val}", c[:another]) end def test_files c = mkconfig parent = "/puppet" assert_nothing_raised { c.setdefaults(:testing, :parentdir => [parent, "booh"]) } assert_nothing_raised { c.setdefaults(:testing, :child => ["$parent/child", "rah"]) } assert_equal(parent, c[:parentdir]) assert_equal("/puppet/child", File.join(c[:parentdir], "child")) end def test_getset c = mkconfig initial = "an initial value" assert_raise(Puppet::Error) { c[:yayness] = initial } default = "this is a default" assert_nothing_raised { c.setdefaults(:testing, :yayness => [default, "rah"]) } assert_equal(default, c[:yayness]) assert_nothing_raised { c[:yayness] = initial } assert_equal(initial, c[:yayness]) assert_nothing_raised { c.clear } assert_equal(default, c[:yayness]) assert_nothing_raised { c[:yayness] = "not default" } assert_equal("not default", c[:yayness]) end def test_parse text = %{ one = this is a test two = another test owner = root group = root yay = /a/path [section1] attr = value owner = puppet group = puppet attrdir = /some/dir attr3 = $attrdir/other } file = tempfile() File.open(file, "w") { |f| f.puts text } c = mkconfig assert_nothing_raised { c.setdefaults("puppet", :one => ["a", "one"], :two => ["a", "two"], :yay => ["/default/path", "boo"], :mkusers => [true, "uh, yeah"] ) } assert_nothing_raised { c.setdefaults("section1", :attr => ["a", "one"], :attrdir => ["/another/dir", "two"], :attr3 => ["$attrdir/maybe", "boo"] ) } assert_nothing_raised { c.parse(file) } assert_equal("value", c[:attr]) assert_equal("/some/dir", c[:attrdir]) assert_equal(:directory, c.element(:attrdir).type) assert_equal("/some/dir/other", c[:attr3]) elem = nil assert_nothing_raised { elem = c.element(:attr3) } assert(elem) assert_equal("puppet", elem.owner) config = nil assert_nothing_raised { config = c.to_config } assert_nothing_raised("Could not create transportable config") { c.to_transportable } check_to_comp(c) Puppet::Type.allclear check_to_manifest(c) Puppet::Type.allclear check_to_config(c) Puppet::Type.allclear check_to_transportable(c) end def test_arghandling c = mkconfig assert_nothing_raised { c.setdefaults("testing", :onboolean => [true, "An on bool"], :offboolean => [false, "An off bool"], :string => ["a string", "A string arg"], :file => ["/path/to/file", "A file arg"] ) } data = { :onboolean => [true, false], :offboolean => [true, false], :string => ["one string", "another string"], :file => %w{/a/file /another/file} } data.each { |param, values| values.each { |val| opt = nil arg = nil if c.boolean?(param) if val opt = "--%s" % param else opt = "--no-%s" % param end else opt = "--%s" % param arg = val end assert_nothing_raised("Could not handle arg %s with value %s" % [opt, val]) { c.handlearg(opt, arg) } } } end def test_argadding c = mkconfig assert_nothing_raised { c.setdefaults("testing", :onboolean => [true, "An on bool"], :offboolean => [false, "An off bool"], :string => ["a string", "A string arg"], :file => ["/path/to/file", "A file arg"] ) } options = [] c.addargs(options) c.each { |param, obj| opt = "--%s" % param assert(options.find { |ary| ary[0] == opt }, "Argument %s was not added" % opt) if c.boolean?(param) o = "--no-%s" % param assert(options.find { |ary| ary[0] == o }, "Boolean off %s was not added" % o) end } end def test_usesection c = mkconfig dir = tempfile() file = "$mydir/myfile" realfile = File.join(dir, "myfile") otherfile = File.join(dir, "otherfile") section = "testing" assert_nothing_raised { c.setdefaults(section, :mydir => [dir, "A dir arg"], :otherfile => { :default => "$mydir/otherfile", :create => true, :desc => "A file arg" }, :myfile => [file, "A file arg"] ) } assert_nothing_raised("Could not use a section") { c.use(section) } assert_nothing_raised("Could not reuse a section") { c.use(section) } assert(FileTest.directory?(dir), "Did not create directory") assert(FileTest.exists?(otherfile), "Did not create file") assert(!FileTest.exists?(realfile), "Created file") end def test_setdefaultsarray c = mkconfig assert_nothing_raised { c.setdefaults("yay", :a => [false, "some value"], :b => ["/my/file", "a file"] ) } assert_equal(false, c[:a], "Values are not equal") assert_equal("/my/file", c[:b], "Values are not equal") end def test_setdefaultshash c = mkconfig assert_nothing_raised { c.setdefaults("yay", :a => {:default => false, :desc => "some value"}, :b => {:default => "/my/file", :desc => "a file"} ) } assert_equal(false, c[:a], "Values are not equal") assert_equal("/my/file", c[:b], "Values are not equal") end def test_reuse c = mkconfig file = tempfile() section = "testing" assert_nothing_raised { c.setdefaults(section, :myfile => {:default => file, :create => true} ) } assert_nothing_raised("Could not use a section") { c.use(section) } assert(FileTest.exists?(file), "Did not create file") assert(! Puppet::Type.type(:file)[file], "File obj still exists") File.unlink(file) c.reuse assert(FileTest.exists?(file), "Did not create file") end def test_mkusers c = mkconfig file = tempfile() section = "testing" assert_nothing_raised { c.setdefaults(section, :mkusers => [false, "yay"], :myfile => { :default => file, :owner => "pptest", :group => "pptest", :create => true } ) } comp = nil assert_nothing_raised { comp = c.to_component } [:user, :group].each do |type| # The objects might get created internally by Puppet::Util; just # make sure they're not being managed if obj = Puppet.type(type)["pptest"] assert(! obj.managed?, "%s objectis managed" % type) end end comp.each { |o| o.remove } c[:mkusers] = true assert_nothing_raised { c.to_component } assert(Puppet.type(:user)["pptest"], "User object did not get created") assert(Puppet.type(:user)["pptest"].managed?, "User object is not managed." ) assert(Puppet.type(:group)["pptest"], "Group object did not get created") assert(Puppet.type(:group)["pptest"].managed?, "Group object is not managed." ) end def test_notmanagingdev c = mkconfig path = "/dev/testing" c.setdefaults(:test, :file => { :default => path, :mode => 0640, :desc => 'yay' } ) assert_nothing_raised { c.to_component } assert(! Puppet.type(:file)["/dev/testing"], "Created dev file") end def test_groupsetting cfile = tempfile() group = "yayness" File.open(cfile, "w") do |f| f.puts "[#{Puppet.name}] group = #{group} " end config = mkconfig config.setdefaults(Puppet.name, :group => ["puppet", "a group"]) assert_nothing_raised { config.parse(cfile) } assert_equal(group, config[:group], "Group did not take") end # provide a method to modify and create files w/out specifying the info # already stored in a config def test_writingfiles path = tempfile() mode = 0644 config = mkconfig args = { :default => path, :mode => mode } user = nonrootuser() group = nonrootgroup() if Puppet::SUIDManager.uid == 0 args[:owner] = user.name args[:group] = group.name end config.setdefaults(:testing, :myfile => args) assert_nothing_raised { config.write(:myfile) do |file| file.puts "yay" end } assert_equal(mode, filemode(path), "Modes are not equal") # OS X is broken in how it chgrps files if Puppet::SUIDManager.uid == 0 assert_equal(user.uid, File.stat(path).uid, "UIDS are not equal") case Facter["operatingsystem"].value when /BSD/, "Darwin": # nothing else assert_equal(group.gid, File.stat(path).gid, "GIDS are not equal") end end end def test_mkdir path = tempfile() mode = 0755 config = mkconfig args = { :default => path, :mode => mode } user = nonrootuser() group = nonrootgroup() if Puppet::SUIDManager.uid == 0 args[:owner] = user.name args[:group] = group.name end config.setdefaults(:testing, :mydir => args) assert_nothing_raised { config.mkdir(:mydir) } assert_equal(mode, filemode(path), "Modes are not equal") # OS X and *BSD is broken in how it chgrps files if Puppet::SUIDManager.uid == 0 assert_equal(user.uid, File.stat(path).uid, "UIDS are not equal") case Facter["operatingsystem"].value when /BSD/, "Darwin": # nothing else assert_equal(group.gid, File.stat(path).gid, "GIDS are not equal") end end end def test_booleans_and_integers config = mkconfig config.setdefaults(:mysection, :booltest => [false, "yay"], :inttest => [14, "yay"] ) file = tempfile() File.open(file, "w") do |f| f.puts %{ [mysection] booltest = true inttest = 27 } end assert_nothing_raised { config.parse(file) } assert_equal(true, config[:booltest], "Boolean was not converted") assert_equal(27, config[:inttest], "Integer was not converted") # Now make sure that they get converted through handlearg config.handlearg("--inttest", "true") assert_equal(true, config[:inttest], "Boolean was not converted") config.handlearg("--no-booltest", "false") assert_equal(false, config[:booltest], "Boolean was not converted") end # Make sure that tags are ignored when configuring def test_configs_ignore_tags config = mkconfig file = tempfile() config.setdefaults(:mysection, :mydir => [file, "a file"] ) Puppet[:tags] = "yayness" assert_nothing_raised { config.use(:mysection) } assert(FileTest.directory?(file), "Directory did not get created") assert_equal("yayness", Puppet[:tags], "Tags got changed during config") end def test_configs_replace_in_url config = mkconfig config.setdefaults(:mysection, :name => ["yayness", "yay"]) config.setdefaults(:mysection, :url => ["http://$name/rahness", "yay"]) val = nil assert_nothing_raised { val = config[:url] } assert_equal("http://yayness/rahness", val, "Config got messed up") end def test_correct_type_assumptions config = mkconfig file = Puppet::Config::CFile element = Puppet::Config::CElement bool = Puppet::Config::CBoolean # We have to keep these ordered, unfortunately. [ ["/this/is/a/file", file], ["true", bool], [true, bool], ["false", bool], ["server", element], ["http://$server/yay", element], ["$server/yayness", file], ["$server/yayness.conf", file] ].each do |ary| value, type = ary elem = nil assert_nothing_raised { elem = config.newelement( :name => value, :default => value, :section => :yayness ) } assert_instance_of(type, elem, "%s got created as wrong type" % value.inspect) end end # Make sure we correctly reparse our config files but don't lose CLI values. def test_reparse Puppet[:filetimeout] = 0 config = mkconfig() config.setdefaults(:mysection, :default => ["default", "yay"]) config.setdefaults(:mysection, :clichange => ["clichange", "yay"]) config.setdefaults(:mysection, :filechange => ["filechange", "yay"]) file = tempfile() # Set one parameter in the file File.open(file, "w") { |f| f.puts %{[mysection]\nfilechange = filevalue} } assert_nothing_raised { config.parse(file) } # Set another "from the cli" assert_nothing_raised { config.handlearg("clichange", "clivalue") } # And leave the other unset assert_equal("default", config[:default]) assert_equal("filevalue", config[:filechange]) assert_equal("clivalue", config[:clichange]) # Now rewrite the file File.open(file, "w") { |f| f.puts %{[mysection]\nfilechange = newvalue} } cfile = config.file cfile.send("tstamp=".intern, Time.now - 50) # And check all of the values assert_equal("default", config[:default]) assert_equal("clivalue", config[:clichange]) assert_equal("newvalue", config[:filechange]) end def test_parse_removes_quotes config = mkconfig() config.setdefaults(:mysection, :singleq => ["single", "yay"]) config.setdefaults(:mysection, :doubleq => ["double", "yay"]) config.setdefaults(:mysection, :none => ["noquote", "yay"]) config.setdefaults(:mysection, :middle => ["midquote", "yay"]) file = tempfile() # Set one parameter in the file File.open(file, "w") { |f| f.puts %{[mysection]\n singleq = 'one' doubleq = "one" none = one middle = mid"quote } } assert_nothing_raised { config.parse(file) } %w{singleq doubleq none}.each do |p| assert_equal("one", config[p], "%s did not match" % p) end assert_equal('mid"quote', config["middle"], "middle did not match") end def test_timer Puppet[:filetimeout] = 0.1 origpath = tempfile() config = mkconfig() config.setdefaults(:mysection, :paramdir => [tempfile(), "yay"]) file = tempfile() # Set one parameter in the file File.open(file, "w") { |f| f.puts %{[mysection]\n paramdir = #{origpath} } } assert_nothing_raised { config.parse(file) config.use(:mysection) } assert(FileTest.directory?(origpath), "dir did not get created") # Now start the timer assert_nothing_raised { EventLoop.current.monitor_timer config.timer } newpath = tempfile() File.open(file, "w") { |f| f.puts %{[mysection]\n paramdir = #{newpath} } } config.file.send("tstamp=".intern, Time.now - 50) sleep 1 assert_equal(newpath, config["paramdir"], "File did not get reparsed from timer") assert(FileTest.directory?(newpath), "new dir did not get created") end end # $Id$ diff --git a/test/rails/rails.rb b/test/rails/rails.rb new file mode 100755 index 000000000..c780d7698 --- /dev/null +++ b/test/rails/rails.rb @@ -0,0 +1,87 @@ +#!/usr/bin/ruby + +require 'puppet' +require 'puppet/rails' +require 'puppet/parser/interpreter' +require 'puppet/parser/parser' +require 'puppet/client' +require 'puppettest' +require 'puppettest/parsertesting' +require 'puppettest/resourcetesting' +require 'puppettest/railstesting' + +class TestRails < Test::Unit::TestCase + include PuppetTest::ParserTesting + include PuppetTest::ResourceTesting + include PuppetTest::RailsTesting + + def test_includerails + assert_nothing_raised { + require 'puppet/rails' + } + end + + # Don't do any tests w/out this class + if defined? ActiveRecord::Base + def test_hostcache + @interp, @scope, @source = mkclassframing + # First make some objects + resources = [] + 20.times { |i| + resources << mkresource(:type => "file", :title => "/tmp/file#{i.to_s}", + :params => {:owner => "user#{i}"}) + } + + # Now collect our facts + facts = Facter.to_hash + + assert_nothing_raised { + Puppet::Rails.init + } + + # Now try storing our crap + host = nil + assert_nothing_raised { + host = Puppet::Rails::Host.store( + :resources => resources, + :facts => facts, + :name => facts["hostname"], + :classes => ["one", "two::three", "four"] + ) + } + + assert(host, "Did not create host") + + host = nil + assert_nothing_raised { + host = Puppet::Rails::Host.find_by_name(facts["hostname"]) + } + assert(host, "Could not find host object") + + assert(host.rails_resources, "No objects on host") + + assert_equal(facts["hostname"], host.facts["hostname"], + "Did not retrieve facts") + + count = 0 + host.rails_resources.each do |resource| + count += 1 + i = nil + if resource[:title] =~ /file([0-9]+)/ + i = $1 + else + raise "Got weird resource %s" % resource.inspect + end + + assert_equal("user#{i}", + resource.rails_parameters.find_by_name("owner")[:value]) + end + + assert_equal(20, count, "Did not get enough resources") + end + else + $stderr.puts "Install Rails for Rails and Caching tests" + end +end + +# $Id$ diff --git a/test/rails/railsparameter.rb b/test/rails/railsparameter.rb new file mode 100755 index 000000000..7dc7ef5dd --- /dev/null +++ b/test/rails/railsparameter.rb @@ -0,0 +1,50 @@ +#!/usr/bin/ruby + +require 'puppet' +require 'puppet/rails' +require 'puppettest' +require 'puppettest/railstesting' + +class TestRailsParameter < Test::Unit::TestCase + include PuppetTest::RailsTesting + + # Don't do any tests w/out this class + if defined? ActiveRecord::Base + # Create a resource param from a rails parameter + def test_to_resourceparam + railsinit + # First create our parameter + rparam = nil + hash = { :name => :myparam, :value => "myval", + :file => __FILE__, :line => __LINE__} + assert_nothing_raised do + rparam = Puppet::Rails::RailsParameter.new(hash) + end + + assert(rparam, "Did not create rails parameter") + + # The id doesn't get assigned until we save + rparam.save + + # Now create a source + interp = mkinterp + source = interp.newclass "myclass" + + # And try to convert our parameter + pparam = nil + assert_nothing_raised do + pparam = rparam.to_resourceparam(source) + end + + + assert_instance_of(Puppet::Parser::Resource::Param, pparam) + hash.each do |name, value| + assert_equal(value, pparam.send(name), "%s was not equal" % name) + end + end + else + $stderr.puts "Install Rails for Rails and Caching tests" + end +end + +# $Id$ diff --git a/test/rails/railsresource.rb b/test/rails/railsresource.rb new file mode 100755 index 000000000..86c3b4908 --- /dev/null +++ b/test/rails/railsresource.rb @@ -0,0 +1,60 @@ +#!/usr/bin/ruby + +require 'puppet' +require 'puppet/rails' +require 'puppettest' +require 'puppettest/railstesting' +require 'puppettest/resourcetesting' + +class TestRailsResource < Test::Unit::TestCase + include PuppetTest::RailsTesting + include PuppetTest::ResourceTesting + + # Don't do any tests w/out this class + if defined? ActiveRecord::Base + # Create a resource param from a rails parameter + def test_to_resource + railsinit + + # We need a host for resources + host = Puppet::Rails::Host.new(:name => "myhost") + + # Now build a resource + resource = host.rails_resources.build( + :title => "/tmp/to_resource", :restype => "file", + :exported => true + ) + + # Now add some params + {"owner" => "root", "mode" => "644"}.each do |param, value| + resource.rails_parameters.build( + :name => param, :value => value + ) + end + + # Now save the whole thing + host.save + + # Now, try to convert our resource to a real resource + + # We need a scope + interp, scope, source = mkclassframing + + res = nil + assert_nothing_raised do + res = resource.to_resource(scope) + end + + assert_instance_of(Puppet::Parser::Resource, res) + + assert_equal("root", res[:owner]) + assert_equal("644", res[:mode]) + assert_equal("/tmp/to_resource", res.title) + assert_equal(source, res.source) + end + else + $stderr.puts "Install Rails for Rails and Caching tests" + end +end + +# $Id$ diff --git a/test/tagging/tagging.rb b/test/tagging/tagging.rb index 42f04ff4f..b30160bcb 100644 --- a/test/tagging/tagging.rb +++ b/test/tagging/tagging.rb @@ -1,177 +1,166 @@ require 'puppet' require 'puppettest' +require 'puppettest/parsertesting' +require 'puppettest/resourcetesting' class TestTagging < Test::Unit::TestCase include PuppetTest + include PuppetTest::ParserTesting + include PuppetTest::ResourceTesting # Make sure the scopes are getting the right tags def test_scopetags scope = nil assert_nothing_raised { - scope = Puppet::Parser::Scope.new() + scope = mkscope scope.name = "yayness" scope.type = "solaris" } assert_nothing_raised { assert_equal(%w{solaris}, scope.tags, "Incorrect scope tags") } end # Test deeper tags, where a scope gets all of its parent scopes' tags def test_deepscopetags scope = nil assert_nothing_raised { - scope = Puppet::Parser::Scope.new() + scope = mkscope scope.name = "yayness" scope.type = "solaris" scope = scope.newscope scope.name = "booness" scope.type = "apache" } assert_nothing_raised { # Scopes put their own tags first assert_equal(%w{apache solaris}, scope.tags, "Incorrect scope tags") } end # Verify that the tags make their way to the objects def test_objecttags scope = nil assert_nothing_raised { - scope = Puppet::Parser::Scope.new() + scope = mkscope scope.name = "yayness" scope.type = "solaris" } - assert_nothing_raised { - scope.setobject( - :type => "file", - :name => "/etc/passwd", - :arguments => {"owner" => "root"}, - :file => "/yay", - :line => 1 - ) - } + resource = mkresource :type => "file", :title => "/tmp/testing", + :params => {:owner => "root"}, :file => "/yay", :line => 1, + :scope => scope - ast = Puppet::Parser::AST::ASTArray.new({}) - - # We have to use 'evaluate', rather than just calling to_trans directly, - # because scopes do some internal checking to make sure the same object - # is not translated multiple times. - objects = nil assert_nothing_raised { - objects = scope.evaluate(:ast => ast) + scope.setresource(resource) } - # There's only one object, so shift it out - object = objects.shift - assert_nothing_raised { - assert_equal(%w{solaris}, object.tags, + assert_equal(%w{solaris}, resource.tags, "Incorrect tags") } end # Make sure that specifying tags results in only those objects getting # run. def test_tagspecs a = tempfile() b = tempfile() afile = Puppet.type(:file).create( :path => a, :ensure => :file ) afile.tag("a") bfile = Puppet.type(:file).create( :path => b, :ensure => :file ) bfile.tag(:b) # First, make sure they get created when no spec'ed tags assert_events([:file_created,:file_created], afile, bfile) assert(FileTest.exists?(a), "A did not get created") assert(FileTest.exists?(b), "B did not get created") File.unlink(a) File.unlink(b) # Set the tags to a assert_nothing_raised { Puppet[:tags] = "a" } assert_events([:file_created], afile, bfile) assert(FileTest.exists?(a), "A did not get created") assert(!FileTest.exists?(b), "B got created") File.unlink(a) # Set the tags to b assert_nothing_raised { Puppet[:tags] = "b" } assert_events([:file_created], afile, bfile) assert(!FileTest.exists?(a), "A got created") assert(FileTest.exists?(b), "B did not get created") File.unlink(b) # Set the tags to something else assert_nothing_raised { Puppet[:tags] = "c" } assert_events([], afile, bfile) assert(!FileTest.exists?(a), "A got created") assert(!FileTest.exists?(b), "B got created") # Now set both tags assert_nothing_raised { Puppet[:tags] = "b, a" } assert_events([:file_created, :file_created], afile, bfile) assert(FileTest.exists?(a), "A did not get created") assert(FileTest.exists?(b), "B did not get created") File.unlink(a) end def test_metaparamtag path = tempfile() start = %w{some tags} tags = %w{a list of tags} obj = nil assert_nothing_raised do obj = Puppet.type(:file).create( :path => path, :ensure => "file", :tag => start ) end assert(obj, "Did not make object") start.each do |tag| assert(obj.tagged?(tag), "Object was not tagged with %s" % tag) end tags.each do |tag| assert_nothing_raised { obj[:tag] = tag } end tags.each do |tag| assert(obj.tagged?(tag), "Object was not tagged with %s" % tag) end end end # $Id$