require 'puppet'
require 'timeout'
require 'puppet/rails'
require 'puppet/util/methodhelper'
require 'puppet/parser/parser'
require 'puppet/parser/scope'

# The interpreter'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.
class Puppet::Parser::Interpreter
    class NodeDef
        include Puppet::Util::MethodHelper
        attr_accessor :name, :classes, :parameters, :source

        def evaluate(options)
            begin
                parameters.each do |param, value|
                    # Don't try to override facts with these parameters
                    options[:scope].setvar(param, value) unless options[:scope].lookupvar(param, false) != :undefined
                end

                # Also, set the 'nodename', since it might not be obvious how the node was looked up
                options[:scope].setvar("nodename", @name) unless options[:scope].lookupvar(@nodename, false) != :undefined
            rescue => detail
                raise Puppet::ParseError, "Could not set parameters for %s: %s" % [name, detail]
            end

            # Then evaluate the classes.
            begin
                options[:scope].function_include(classes.find_all { |c| options[:scope].findclass(c) })
            rescue => detail
                puts detail.backtrace
                raise Puppet::ParseError, "Could not evaluate classes for %s: %s" % [name, detail]
            end
        end

        def initialize(args)
            set_options(args)

            raise Puppet::DevError, "NodeDefs require names" unless self.name

            if self.classes.is_a?(String)
                @classes = [@classes]
            else
                @classes ||= []
            end
            @parameters ||= {}
        end

        def safeevaluate(args)
            evaluate(args)
        end
    end

    include Puppet::Util

    attr_accessor :usenodes

    class << self
        attr_writer :ldap
    end

    # just shorten the constant path a bit, using what amounts to an alias
    AST = Puppet::Parser::AST

    include Puppet::Util::Errors

    # Create an ldap connection.  This is a class method so others can call
    # it and use the same variables and such.
    def self.ldap
        unless defined? @ldap and @ldap
            if Puppet[:ldapssl]
                @ldap = LDAP::SSLConn.new(Puppet[:ldapserver], Puppet[:ldapport])
            elsif Puppet[:ldaptls]
                @ldap = LDAP::SSLConn.new(
                    Puppet[:ldapserver], Puppet[:ldapport], true
                )
            else
                @ldap = LDAP::Conn.new(Puppet[:ldapserver], Puppet[:ldapport])
            end
            @ldap.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
            @ldap.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON)
            @ldap.simple_bind(Puppet[:ldapuser], Puppet[:ldappassword])
        end

        return @ldap
    end

    # Make sure we don't have any remaining collections that specifically
    # look for resources, because we want to consider those to be
    # parse errors.
    def check_resource_collections(scope)
        remaining = []
        scope.collections.each do |coll|
            if r = coll.resources
                if r.is_a?(Array)
                    remaining += r
                else
                    remaining << r
                end
            end
        end
        unless remaining.empty?
            raise Puppet::ParseError, "Failed to find virtual resources %s" %
                remaining.join(', ')
        end
    end

    # Iteratively evaluate all of the objects.  This finds all of the objects
    # that represent definitions and evaluates the definitions appropriately.
    # It also adds defaults and overrides as appropriate.
    def evaliterate(scope)
        count = 0
        loop do
            count += 1
            done = true
            # First perform collections, so we can collect defined types.
            if coll = scope.collections and ! coll.empty?
                exceptwrap do
                    coll.each do |c|
                        # Only keep the loop going if we actually successfully
                        # collected something.
                        if o = c.evaluate
                            done = false
                        end
                    end
                end
            end
            
            # Then evaluate any defined types.
            if ary = scope.unevaluated
                ary.each do |resource|
                    resource.evaluate
                end
                # If we evaluated, then loop through again.
                done = false
            end
            break if done
            
            if count > 1000
                raise Puppet::ParseError, "Got 1000 class levels, which is unsupported"
            end
        end
    end

    # Evaluate a specific node.
    def evalnode(client, scope, facts)
        return unless self.usenodes

        unless client
            raise Puppet::Error,
                "Cannot evaluate nodes with a nil client"
        end
        names = [client]

        # Make sure both the fqdn and the short name of the
        # host can be used in the manifest
        if client =~ /\./
            names << client.sub(/\..+/,'')
        else
            names << "#{client}.#{facts['domain']}"
        end

        if names.empty?
            raise Puppet::Error,
                "Cannot evaluate nodes with a nil client"
        end

        # Look up our node object.
        if nodeclass = nodesearch(*names)
            nodeclass.safeevaluate :scope => scope
        else
            raise Puppet::Error, "Could not find %s with names %s" %
                [client, names.join(", ")]
        end
    end

    # Evaluate all of the code we can find that's related to our client.
    def evaluate(client, facts)

        scope = Puppet::Parser::Scope.new(:interp => self) # no parent scope
        scope.name = "top"
        scope.type = "main"

        scope.host = client || facts["hostname"] || Facter.value(:hostname)

        classes = @classes.dup

        # Okay, first things first.  Set our facts.
        scope.setfacts(facts)

        # Everyone will always evaluate the top-level class, if there is one.
        if klass = findclass("", "")
            # Set the source, so objects can tell where they were defined.
            scope.source = klass
            klass.safeevaluate :scope => scope, :nosubscope => true
        end

        # Next evaluate the node.  We pass the facts so they can be used
        # when building the list of names for which to search.
        evalnode(client, scope, facts)

        # If we were passed any classes, evaluate those.
        if classes
            classes.each do |klass|
                if klassobj = findclass("", klass)
                    klassobj.safeevaluate :scope => scope
                end
            end
        end

        # That was the first pass evaluation.  Now iteratively evaluate
        # until we've gotten rid of all of everything or thrown an error.
        evaliterate(scope)

        # Now make sure we fail if there's anything left to do
        failonleftovers(scope)

        # Now 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 => scope.host,
                :facts => facts
            }
            unless scope.classlist.empty?
                args[:classes] = scope.classlist
            end

            storeconfigs(args)
        end

        # Now, finally, convert our scope tree + resources into a tree of
        # buckets and objects.
        objects = scope.translate

        # Add the class list
        unless scope.classlist.empty?
            objects.classes = scope.classlist
        end

        return objects
    end

    # Fail if there any overrides left to perform.
    def failonleftovers(scope)
        overrides = scope.overrides
        unless overrides.empty?
            fail Puppet::ParseError,
                "Could not find object(s) %s" % overrides.collect { |o|
                    o.ref
                }.join(", ")
        end

        # Now check that there aren't any extra resource collections.
        check_resource_collections(scope)
    end

    # Create proxy methods, so the scopes can call the interpreter, since
    # they don't have access to the parser.
    def findclass(namespace, name)
        @parser.findclass(namespace, name)
    end
    def finddefine(namespace, name)
        @parser.finddefine(namespace, name)
    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


        if Puppet[:ldapnodes]
            # Nodes in the file override nodes in ldap.
            @nodesource = :ldap
        elsif Puppet[:external_nodes] != "none"
            @nodesource = :external
        else
            # By default, we only search for parsed nodes.
            @nodesource = :code
        end

        @setup = false

        # Set it to either the value or nil.  This is currently only used
        # by the cfengine module.
        @classes = hash[:Classes] || []

        @local = hash[:Local] || false

        if hash.include?(:ForkSave)
            @forksave = hash[:ForkSave]
        else
            # This is just too dangerous right now.  Sorry, it's going
            # to have to be slow.
            @forksave = false
        end

        # The class won't always be defined during testing.
        if Puppet[:storeconfigs] 
            if Puppet.features.rails?
                Puppet::Rails.init
            else
                raise Puppet::Error, "Rails is missing; cannot store configurations"
            end
        end

        @files = []

        # Create our parser object
        parsefiles
    end

    # Find the ldap node, return the class list and parent node specially,
    # and everything else in a parameter hash.
    def ldapsearch(node)
        unless defined? @ldap and @ldap
            setup_ldap()
            unless @ldap
                Puppet.info "Skipping ldap source; no ldap connection"
                return nil
            end
        end

        filter = Puppet[:ldapstring]
        classattrs = Puppet[:ldapclassattrs].split("\s*,\s*")
        if Puppet[:ldapattrs] == "all"
            # A nil value here causes all attributes to be returned.
            search_attrs = nil
        else
            search_attrs = classattrs + Puppet[:ldapattrs].split("\s*,\s*")
        end
        pattr = nil
        if pattr = Puppet[:ldapparentattr]
            if pattr == ""
                pattr = nil
            else
                search_attrs << pattr unless search_attrs.nil?
            end
        end

        if filter =~ /%s/
            filter = filter.gsub(/%s/, node)
        end

        parent = nil
        classes = []
        parameters = nil

        found = false
        count = 0

        begin
            # We're always doing a sub here; oh well.
            @ldap.search(Puppet[:ldapbase], 2, filter, search_attrs) 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

                classattrs.each { |attr|
                    if values = entry.vals(attr)
                        values.each do |v| classes << v end
                    end
                }

                parameters = entry.to_hash.inject({}) do |hash, ary|
                    if ary[1].length == 1
                        hash[ary[0]] = ary[1].shift
                    else
                        hash[ary[0]] = ary[1]
                    end
                    hash
                end
            end
        rescue => detail
            if count == 0
                # Try reconnecting to ldap
                @ldap = nil
                setup_ldap()
                retry
            else
                raise Puppet::Error, "LDAP Search failed: %s" % detail
            end
        end

        classes.flatten!

        if classes.empty?
            classes = nil
        end

        if parent or classes or parameters
            return parent, classes, parameters
        else
            return nil
        end
    end

    # Pass these methods through to the parser.
    [:newclass, :newdefine, :newnode].each do |name|
        define_method(name) do |*args|
            @parser.send(name, *args)
        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::Util::LoadedFile
                file = Puppet::Util::LoadedFile.new(file)
            end
            @files << file
        end
    end

    # Search for our node in the various locations.
    def nodesearch(*nodes)
        nodes = nodes.collect { |n| n.to_s.downcase }

        method = "nodesearch_%s" % @nodesource
        # 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)
                if obj.is_a?(AST::Node)
                    nsource = obj.file
                else
                    nsource = obj.source
                end
                Puppet.info "Found %s in %s" % [node, nsource]
                return obj
            end
        end

        # If they made it this far, we haven't found anything, so look for a
        # default node.
        unless nodes.include?("default")
            if defobj = self.nodesearch("default")
                Puppet.notice "Using default node for %s" % [nodes[0]]
                return defobj
            end
        end

        return nil
    end

    # See if our node was defined in the code.
    def nodesearch_code(name)
        @parser.nodes[name]
    end
    
    # Look for external node definitions.
    def nodesearch_external(name)
        return nil unless Puppet[:external_nodes] != "none"
        
        begin
            output = Puppet::Util.execute([Puppet[:external_nodes], name])
        rescue Puppet::ExecutionFailure => detail
            if $?.exitstatus == 1
                return nil
            else
                Puppet.err "Could not retrieve external node information for %s: %s" % [name, detail]
            end
            return nil
        end
        
        if output =~ /\A\s*\Z/ # all whitespace
            Puppet.debug "Empty response for %s from external node source" % name
            return nil
        end

        begin
            result = YAML.load(output).inject({}) { |hash, data| hash[symbolize(data[0])] = data[1]; hash }
        rescue => detail
            raise Puppet::Error, "Could not load external node results for %s: %s" % [name, detail]
        end

        node_args = {:source => "external node source", :name => name}
        set = false
        [:parameters, :classes].each do |param|
            if value = result[param]
                node_args[param] = value
                set = true
            end
        end

        if set
            return NodeDef.new(node_args)
        else
            return nil
        end
    end

    # Look for our node in ldap.
    def nodesearch_ldap(node)
        unless ary = ldapsearch(node)
            return nil
        end
        parent, classes, parameters = ary

        while parent
            parent, tmpclasses, tmpparams = ldapsearch(parent)
            classes += tmpclasses if tmpclasses
            tmpparams.each do |param, value|
                # Specifically test for whether it's set, so false values are handled
                # correctly.
                parameters[param] = value unless parameters.include?(param)
            end
        end

        return NodeDef.new(:name => node, :classes => classes, :source => "ldap", :parameters => parameters)
    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
            method = "setup_%s" % @nodesource.to_s
            if respond_to? method
                exceptwrap :type => Puppet::Error,
                        :message => "Could not set up node source %s" % @nodesource 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
        unless Puppet.features.ldap?
            Puppet.notice(
                "Could not set up LDAP Connection: Missing ruby/ldap libraries"
            )
            @ldap = nil
            return
        end

        begin
            @ldap = self.class.ldap()
        rescue => detail
            raise Puppet::Error, "Could not connect to LDAP: %s" % detail
        end
    end

    def scope
        return @scope
    end

    private

    # Check whether any of our files have changed.
    def checkfiles
        if @files.find { |f| f.changed?  }
            @parsedate = Time.now.to_i
        end
    end

    # Parse the files, generating our parse tree.  This automatically
    # reparses only if files are updated, so it's safe to call multiple
    # times.
    def parsefiles
        # First check whether there are updates to any non-puppet files
        # like templates.  If we need to reparse, this will get quashed,
        # but it needs to be done first in case there's no reparse
        # but there are other file changes.
        checkfiles()

        # Check if the parser should reparse.
        if @file
            if defined? @parser
                if stamp = @parser.reparse?
                    Puppet.notice "Reloading files"
                else
                    return false
                end
            end

            unless FileTest.exists?(@file)
                # If we've already parsed, then we're ok.
                if findclass("", "")
                    return
                else
                    raise Puppet::Error, "Manifest %s must exist" % @file
                end
            end
        end

        # Create a new parser, just to keep things fresh.  Don't replace our
        # current parser until we know weverything works.
        newparser = Puppet::Parser::Parser.new()
        if @code
            newparser.string = @code
        else
            newparser.file = @file
        end

        # Parsing stores all classes and defines and such in their
        # various tables, so we don't worry about the return.
        begin
            if @local
                newparser.parse
            else
                benchmark(:info, "Parsed manifest") do
                    newparser.parse
                end
            end
            # We've gotten this far, so it's ok to swap the parsers.
            oldparser = @parser
            @parser = newparser
            if oldparser
                oldparser.clear
            end

            # Mark when we parsed, so we can check freshness
            @parsedate = Time.now.to_i
        rescue => detail
            if Puppet[:trace]
                puts detail.backtrace
            end
            Puppet.err "Could not parse; using old configuration: %s" % detail
        end
    end

    # Store the configs into the database.
    def storeconfigs(hash)
        unless Puppet.features.rails?
            raise Puppet::Error,
                "storeconfigs is enabled but rails is unavailable"
        end

        unless ActiveRecord::Base.connected?
            Puppet::Rails.connect
        end

        # Fork the storage, since we don't need the client waiting
        # on that.  How do I avoid this duplication?
        if @forksave
            fork {
                # We store all of the objects, even the collectable ones
                benchmark(:info, "Stored configuration for #{hash[:name]}") do
                    # Try to batch things a bit, by putting them into
                    # a transaction
                    Puppet::Rails::Host.transaction do
                        Puppet::Rails::Host.store(hash)
                    end
                end
            }
        else
            begin
                # We store all of the objects, even the collectable ones
                benchmark(:info, "Stored configuration for #{hash[:name]}") do
                    Puppet::Rails::Host.transaction do
                        Puppet::Rails::Host.store(hash)
                    end
                end
            rescue => detail
                if Puppet[:trace]
                    puts detail.backtrace
                end
                Puppet.err "Could not store configs: %s" % detail.to_s
            end
        end
    end
end

# $Id: interpreter.rb 2742 2007-08-03 23:49:53Z luke $


syntax highlighted by Code2HTML, v. 0.9.1