diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index 87f849d17..34b13edb7 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,115 +1,119 @@ # Support for modules class Puppet::Module TEMPLATES = "templates" FILES = "files" MANIFESTS = "manifests" # Return an array of paths by splitting the +modulepath+ config # parameter. Only consider paths that are absolute and existing # directories - def self.modulepath - dirs = Puppet[:modulepath].split(":") + def self.modulepath(environment = nil) + dirs = Puppet.config.value(:modulepath, environment).split(":") if ENV["PUPPETLIB"] dirs = ENV["PUPPETLIB"].split(":") + dirs + else end dirs.select do |p| p =~ /^#{File::SEPARATOR}/ && File::directory?(p) end end # Find and return the +module+ that +path+ belongs to. If +path+ is # absolute, or if there is no module whose name is the first component # of +path+, return +nil+ - def self.find(path) - if path =~ %r/^#{File::SEPARATOR}/ + def self.find(modname, environment = nil) + if modname =~ %r/^#{File::SEPARATOR}/ return nil end - modname, rest = path.split(File::SEPARATOR, 2) - return nil if modname.nil? || modname.empty? - - modpath = modulepath.collect { |p| + modpath = modulepath(environment).collect { |p| File::join(p, modname) }.find { |f| File::directory?(f) } return nil unless modpath return self.new(modname, modpath) end # Instance methods # Find the concrete file denoted by +file+. If +file+ is absolute, # return it directly. Otherwise try to find it as a template in a # module. If that fails, return it relative to the +templatedir+ config # param. # In all cases, an absolute path is returned, which does not # necessarily refer to an existing file - def self.find_template(file) + def self.find_template(file, environment = nil) if file =~ /^#{File::SEPARATOR}/ return file end - mod = find(file) + path, file = split_path(file) + mod = find(path, environment) if mod return mod.template(file) else - return File.join(Puppet[:templatedir], file) + return File.join(Puppet.config.value(:templatedir, environment), path, file) end end # Return a list of manifests (as absolute filenames) that match +pat+ # with the current directory set to +cwd+. If the first component of # +pat+ does not contain any wildcards and is an existing module, return # a list of manifests in that module matching the rest of +pat+ # Otherwise, try to find manifests matching +pat+ relative to +cwd+ - def self.find_manifests(pat, cwd = nil) - cwd ||= Dir.getwd - mod = find(pat) + def self.find_manifests(start, options = {}) + cwd = options[:cwd] || Dir.getwd + path, pat = split_path(start) + mod = find(path, options[:environment]) if mod return mod.manifests(pat) else - abspat = File::expand_path(pat, cwd) + abspat = File::expand_path(start, cwd) files = Dir.glob(abspat).reject { |f| FileTest.directory?(f) } if files.size == 0 files = Dir.glob(abspat + ".pp").reject { |f| FileTest.directory?(f) } end return files end end + # Split the path into the module and the rest of the path. + def self.split_path(path) + if path =~ %r/^#{File::SEPARATOR}/ + return nil + end + + modname, rest = path.split(File::SEPARATOR, 2) + return nil if modname.nil? || modname.empty? + return modname, rest + end + attr_reader :name, :path def initialize(name, path) @name = name @path = path end - def strip(file) - n, rest = file.split(File::SEPARATOR, 2) - rest = nil if rest && rest.empty? - return rest - end - def template(file) - return File::join(path, TEMPLATES, strip(file)) + return File::join(path, TEMPLATES, file) end def files return File::join(path, FILES) end - def manifests(pat) - rest = strip(pat) + # Return the list of manifests matching the given glob pattern, + # defaulting to 'init.pp' for empty modules. + def manifests(rest) rest ||= "init.pp" p = File::join(path, MANIFESTS, rest) files = Dir.glob(p) if files.size == 0 files = Dir.glob(p + ".pp") end return files end private :initialize end - -# $Id$ diff --git a/lib/puppet/parser/configuration.rb b/lib/puppet/parser/configuration.rb index 148f4dcd1..a56bfa080 100644 --- a/lib/puppet/parser/configuration.rb +++ b/lib/puppet/parser/configuration.rb @@ -1,548 +1,558 @@ # Created by Luke A. Kanies on 2007-08-13. # Copyright (c) 2007. All rights reserved. require 'puppet/external/gratr/digraph' require 'puppet/external/gratr/import' require 'puppet/external/gratr/dot' require 'puppet/node' require 'puppet/util/errors' # Maintain a graph of scopes, along with a bunch of data # about the individual configuration we're compiling. class Puppet::Parser::Configuration include Puppet::Util include Puppet::Util::Errors attr_reader :topscope, :parser, :node, :facts, :collections attr_accessor :extraction_format attr_writer :ast_nodes # Add a collection to the global list. def add_collection(coll) @collections << coll end # Do we use nodes found in the code, vs. the external node sources? def ast_nodes? defined?(@ast_nodes) and @ast_nodes end # Store the fact that we've evaluated a class, and store a reference to # the scope in which it was evaluated, so that we can look it up later. def class_set(name, scope) if existing = @class_scopes[name] if existing.nodescope? or scope.nodescope? raise Puppet::ParseError, "Cannot have classes, nodes, or definitions with the same name" else raise Puppet::DevError, "Somehow evaluated the same class twice" end end @class_scopes[name] = scope tag(name) end # Return the scope associated with a class. This is just here so # that subclasses can set their parent scopes to be the scope of # their parent class, and it's also used when looking up qualified # variables. def class_scope(klass) # They might pass in either the class or class name if klass.respond_to?(:classname) @class_scopes[klass.classname] else @class_scopes[klass] end end # Return a list of all of the defined classes. def classlist return @class_scopes.keys.reject { |k| k == "" } end # Compile our configuration. This mostly revolves around finding and evaluating classes. # This is the main entry into our configuration. def compile # Set the client's parameters into the top scope. set_node_parameters() evaluate_main() evaluate_ast_node() evaluate_classes() evaluate_generators() fail_on_unevaluated() finish() if Puppet[:storeconfigs] store() end return extract() end # FIXME There are no tests for this. def delete_collection(coll) @collections.delete(coll) if @collections.include?(coll) end # FIXME There are no tests for this. def delete_resource(resource) @resource_table.delete(resource.ref) if @resource_table.include?(resource.ref) @resource_graph.remove_vertex!(resource) if @resource_graph.vertex?(resource) end + # Return the node's environment. + def environment + unless defined? @environment + if node.environment and node.environment != "" + @environment = node.environment + end + end + @environment + end + # Evaluate each class in turn. If there are any classes we can't find, # just tag the configuration and move on. def evaluate_classes(classes = nil) classes ||= node.classes found = [] classes.each do |name| if klass = @parser.findclass("", name) # This will result in class_set getting called, which # will in turn result in tags. Yay. klass.safeevaluate(:scope => topscope) found << name else Puppet.info "Could not find class %s for %s" % [name, node.name] tag(name) end end found end # Make sure we support the requested extraction format. def extraction_format=(value) unless respond_to?("extract_to_%s" % value) raise ArgumentError, "Invalid extraction format %s" % value end @extraction_format = value end # Return a resource by either its ref or its type and title. def findresource(string, name = nil) if name string = "%s[%s]" % [string.capitalize, name] end @resource_table[string] end # Set up our configuration. We require a parser # and a node object; the parser is so we can look up classes # and AST nodes, and the node has all of the client's info, # like facts and environment. def initialize(node, parser, options = {}) @node = node @parser = parser options.each do |param, value| begin send(param.to_s + "=", value) rescue NoMethodError raise ArgumentError, "Configuration objects do not accept %s" % param end end @extraction_format ||= :transportable initvars() end # Create a new scope, with either a specified parent scope or # using the top scope. Adds an edge between the scope and # its parent to the graph. def newscope(parent, options = {}) parent ||= @topscope options[:configuration] = self options[:parser] ||= self.parser scope = Puppet::Parser::Scope.new(options) @scope_graph.add_edge!(parent, scope) scope end # Find the parent of a given scope. Assumes scopes only ever have # one in edge, which will always be true. def parent(scope) if ary = @scope_graph.adjacent(scope, :direction => :in) and ary.length > 0 ary[0] else nil end end # Return any overrides for the given resource. def resource_overrides(resource) @resource_overrides[resource.ref] end # Return a list of all resources. def resources @resource_table.values end # Store a resource override. def store_override(override) override.override = true # If possible, merge the override in immediately. if resource = @resource_table[override.ref] resource.merge(override) else # Otherwise, store the override for later; these # get evaluated in Resource#finish. @resource_overrides[override.ref] << override end end # Store a resource in our resource table. def store_resource(scope, resource) # This might throw an exception verify_uniqueness(resource) # Store it in the global table. @resource_table[resource.ref] = resource # And in the resource graph. At some point, this might supercede # the global resource table, but the table is a lot faster # so it makes sense to maintain for now. @resource_graph.add_edge!(scope, resource) end private # If ast nodes are enabled, then see if we can find and evaluate one. def evaluate_ast_node return unless ast_nodes? # Now see if we can find the node. astnode = nil #nodes = @parser.nodes @node.names.each do |name| break if astnode = @parser.nodes[name.to_s.downcase] end unless astnode astnode = @parser.nodes["default"] end unless astnode raise Puppet::ParseError, "Could not find default node or by name with '%s'" % node.names.join(", ") end astnode.safeevaluate :scope => topscope end # Evaluate our collections and return true if anything returned an object. # The 'true' is used to continue a loop, so it's important. def evaluate_collections return false if @collections.empty? found_something = false exceptwrap do @collections.each do |collection| if collection.evaluate found_something = true end end end return found_something end # Make sure all of our resources have been evaluated into native resources. # We return true if any resources have, so that we know to continue the # evaluate_generators loop. def evaluate_definitions exceptwrap do if ary = unevaluated_resources ary.each do |resource| resource.evaluate end # If we evaluated, let the loop know. return true else return false end end end # Iterate over collections and resources until we're sure that the whole # configuration is evaluated. This is necessary because both collections # and defined resources can generate new resources, which themselves could # be defined resources. def evaluate_generators count = 0 loop do done = true # Call collections first, then definitions. done = false if evaluate_collections done = false if evaluate_definitions break if done if count > 1000 raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host configuration" end end end # Find and evaluate our main object, if possible. def evaluate_main if klass = @parser.findclass("", "") # Set the source, so objects can tell where they were defined. topscope.source = klass klass.safeevaluate :scope => topscope, :nosubscope => true end end # Turn our configuration graph into whatever the client is expecting. def extract send("extract_to_%s" % extraction_format) end # Create the traditional TransBuckets and TransObjects from our configuration # graph. This will hopefully be deprecated soon. def extract_to_transportable top = nil current = nil buckets = {} # I'm *sure* there's a simple way to do this using a breadth-first search # or something, but I couldn't come up with, and this is both fast # and simple, so I'm not going to worry about it too much. @scope_graph.vertices.each do |scope| # For each scope, we need to create a TransBucket, and then # put all of the scope's resources into that bucket, translating # each resource into a TransObject. # Unless the bucket's already been created, make it now and add # it to the cache. unless bucket = buckets[scope] bucket = buckets[scope] = scope.to_trans end # First add any contained scopes @scope_graph.adjacent(scope, :direction => :out).each do |vertex| # If there's not already a bucket, then create and cache it. unless child_bucket = buckets[vertex] child_bucket = buckets[vertex] = vertex.to_trans end bucket.push child_bucket end # Then add the resources. if @resource_graph.vertex?(scope) @resource_graph.adjacent(scope, :direction => :out).each do |vertex| # Some resources don't get translated, e.g., virtual resources. if obj = vertex.to_trans bucket.push obj end end end end # Retrive the bucket for the top-level scope and set the appropriate metadata. result = buckets[topscope] result.copy_type_and_name(topscope) unless classlist.empty? result.classes = classlist end # Clear the cache to encourage the GC buckets.clear return result end # Make sure the entire configuration is evaluated. def fail_on_unevaluated fail_on_unevaluated_overrides fail_on_unevaluated_resource_collections end # If there are any resource overrides remaining, then we could # not find the resource they were supposed to override, so we # want to throw an exception. def fail_on_unevaluated_overrides remaining = [] @resource_overrides.each do |name, overrides| remaining += overrides end unless remaining.empty? fail Puppet::ParseError, "Could not find object(s) %s" % remaining.collect { |o| o.ref }.join(", ") end 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 fail_on_unevaluated_resource_collections remaining = [] @collections.each do |coll| # We're only interested in the 'resource' collections, # which result from direct calls of 'realize'. Anything # else is allowed not to return resources. # Collect all of them, so we have a useful error. 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 realize virtual resources %s" % remaining.join(', ') end end # Make sure all of our resources and such have done any last work # necessary. def finish @resource_table.each { |name, resource| resource.finish if resource.respond_to?(:finish) } end # Set up all of our internal variables. def initvars # The table for storing class singletons. This will only actually # be used by top scopes and node scopes. @class_scopes = {} # The table for all defined resources. @resource_table = {} # The list of objects that will available for export. @exported_resources = {} # The list of overrides. This is used to cache overrides on objects # that don't exist yet. We store an array of each override. @resource_overrides = Hash.new do |overs, ref| overs[ref] = [] end # The list of collections that have been created. This is a global list, # but they each refer back to the scope that created them. @collections = [] # A list of tags we've generated; most class names. @tags = [] # Create our initial scope, our scope graph, and add the initial scope to the graph. @topscope = Puppet::Parser::Scope.new(:configuration => self, :type => "main", :name => "top", :parser => self.parser) # For maintaining scope relationships. @scope_graph = GRATR::Digraph.new @scope_graph.add_vertex!(@topscope) # For maintaining the relationship between scopes and their resources. @resource_graph = GRATR::Digraph.new end # Set the node's parameters into the top-scope as variables. def set_node_parameters node.parameters.each do |param, value| @topscope.setvar(param, value) end end # Store the configuration into the database. def store unless Puppet.features.rails? raise Puppet::Error, "storeconfigs is enabled but rails is unavailable" end unless ActiveRecord::Base.connected? Puppet::Rails.connect end # We used to have hooks here for forking and saving, but I don't # think it's worth retaining at this point. store_to_active_record(@node, @resource_table.values) end # Do the actual storage. def store_to_active_record(node, resources) begin # We store all of the objects, even the collectable ones benchmark(:info, "Stored configuration for #{node.name}") do Puppet::Rails::Host.transaction do Puppet::Rails::Host.store(node, resources) end end rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not store configs: %s" % detail.to_s end end # Add a tag. def tag(*names) names.each do |name| name = name.to_s @tags << name unless @tags.include?(name) end nil end # Return the list of tags. def tags @tags.dup end # Return an array of all of the unevaluated resources. These will be definitions, # which need to get evaluated into native resources. def unevaluated_resources ary = @resource_table.find_all do |name, object| ! object.builtin? and ! object.evaluated? end.collect { |name, object| object } if ary.empty? return nil else return ary end end # Verify that the given resource isn't defined elsewhere. def verify_uniqueness(resource) # Short-curcuit the common case, unless existing_resource = @resource_table[resource.ref] return true end if typeclass = Puppet::Type.type(resource.type) and ! typeclass.isomorphic? Puppet.info "Allowing duplicate %s" % typeclass.name return true end # Either it's a defined type, which are never # isomorphic, or it's a non-isomorphic type, so # we should throw an exception. msg = "Duplicate definition: %s is already defined" % resource.ref if existing_resource.file and existing_resource.line msg << " in file %s at line %s" % [existing_resource.file, existing_resource.line] end if resource.line or resource.file msg << "; cannot redefine" end raise Puppet::ParseError.new(msg) end end diff --git a/lib/puppet/parser/templatewrapper.rb b/lib/puppet/parser/templatewrapper.rb index 9c3c6c0d0..ecef60e43 100644 --- a/lib/puppet/parser/templatewrapper.rb +++ b/lib/puppet/parser/templatewrapper.rb @@ -1,54 +1,54 @@ # 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 - @file = Puppet::Module::find_template(file) + @file = Puppet::Module::find_template(file, @scope.configuration.environment) 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. + # We'll only ever not have a parser in testing, but, eh. if @scope.parser @scope.parser.watch_file(@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/util/config.rb b/lib/puppet/util/config.rb index 796529f32..b6831ba9b 100644 --- a/lib/puppet/util/config.rb +++ b/lib/puppet/util/config.rb @@ -1,1217 +1,1201 @@ require 'puppet' require 'sync' require 'puppet/transportable' require 'getoptlong' # The class for handling configuration files. class Puppet::Util::Config include Enumerable include Puppet::Util @@sync = Sync.new attr_accessor :file attr_reader :timer # Retrieve a config value def [](param) value(param) end # Set a config value. This doesn't set the defaults, it sets the value itself. def []=(param, value) @@sync.synchronize do # yay, thread-safe param = symbolize(param) unless element = @config[param] raise ArgumentError, "Attempt to assign a value to unknown configuration parameter %s" % param.inspect end if element.respond_to?(:munge) value = element.munge(value) end if element.respond_to?(:handle) element.handle(value) end # Reset the name, so it's looked up again. if param == :name @name = nil end @values[:memory][param] = value @values[:cache].clear end return value end # A simplified equality operator. def ==(other) self.each { |myname, myobj| unless other[myname] == value(myname) return false end } return true end # Generate the list of valid arguments, in a format that GetoptLong can # understand, and add them to the passed option list. def addargs(options) # Hackish, but acceptable. Copy the current ARGV for restarting. Puppet.args = ARGV.dup # Add all of the config parameters as valid options. self.each { |name, element| element.getopt_args.each { |args| options << args } } return options end # Turn the config into a transaction and apply it def apply trans = self.to_transportable begin comp = trans.to_type trans = comp.evaluate trans.evaluate comp.remove rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not configure myself: %s" % detail end end # Is our parameter a boolean parameter? def boolean?(param) param = symbolize(param) if @config.include?(param) and @config[param].kind_of? CBoolean return true else return false end end # Remove all set values, potentially skipping cli values. def clear(exceptcli = false) @config.each { |name, obj| unless exceptcli and obj.setbycli obj.clear end } @values.each do |name, values| next if name == :cli and exceptcli @values.delete(name) end # Don't clear the 'used' in this case, since it's a config file reparse, # and we want to retain this info. unless exceptcli @used = [] end @name = nil end # This is mostly just used for testing. def clearused @values[:cache].clear @used = [] end # Do variable interpolation on the value. def convert(value) return value unless value return value unless value.is_a? String newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value| varname = $2 || $1 if pval = self.value(varname) pval else raise Puppet::DevError, "Could not find value for %s" % parent end end return newval end # Return a value's description. def description(name) if obj = @config[symbolize(name)] obj.desc else nil end end def each @config.each { |name, object| yield name, object } end # Iterate over each section name. def eachsection yielded = [] @config.each do |name, object| section = object.section unless yielded.include? section yield section yielded << section end end end # Return an object by name. def element(param) param = symbolize(param) @config[param] end # Handle a command-line argument. def handlearg(opt, value = nil) value = munge_value(value) if value str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end str = str.intern if self.valid?(str) if self.boolean?(str) @values[:cli][str] = bool else @values[:cli][str] = value end else raise ArgumentError, "Invalid argument %s" % opt end end def include?(name) name = name.intern if name.is_a? String @config.include?(name) end # check to see if a short name is already defined def shortinclude?(short) short = short.intern if name.is_a? String @shortnames.include?(short) end # Create a new config object def initialize @config = {} @shortnames = {} @created = [] @searchpath = nil # Keep track of set values. @values = Hash.new { |hash, key| hash[key] = {} } # A central concept of a name. @name = nil end # Return a given object's file metadata. def metadata(param) if obj = @config[symbolize(param)] and obj.is_a?(CFile) return [:owner, :group, :mode].inject({}) do |meta, p| if v = obj.send(p) meta[p] = v end meta end else nil end end # Make a directory with the appropriate user, group, and mode def mkdir(default) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default %s" % default end unless obj.is_a? CFile raise ArgumentError, "Default %s is not a file" % default end Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end # Figure out our name. def name unless @name unless @config[:name] return nil end searchpath.each do |source| next if source == :name break if @name = @values[source][:name] end unless @name @name = convert(@config[:name].default).intern end end @name end # Return all of the parameters associated with a given section. def params(section = nil) if section section = section.intern if section.is_a? String @config.find_all { |name, obj| obj.section == section }.collect { |name, obj| name } else @config.keys end end # Parse the configuration file. def parse(file) clear(true) parse_file(file).each do |area, values| @values[area] = values end # We have to do it in the reverse of the search path, # because multiple sections could set the same value. searchpath.reverse.each do |source| if meta = @values[source][:_meta] set_metadata(meta) end end end # Parse the configuration file. As of May 2007, this is a backward-compatibility method and # will be deprecated soon. def old_parse(file) text = nil if file.is_a? Puppet::Util::LoadedFile @file = file else @file = Puppet::Util::LoadedFile.new(file) end # Don't create a timer for the old style parsing. # settimer() begin text = File.read(@file.file) rescue Errno::ENOENT raise Puppet::Error, "No such file %s" % file rescue Errno::EACCES raise Puppet::Error, "Permission denied to file %s" % file end @values = Hash.new { |names, name| names[name] = {} } # Get rid of the values set by the file, keeping cli values. self.clear(true) section = "puppet" metas = %w{owner group mode} values = Hash.new { |hash, key| hash[key] = {} } text.split(/\n/).each { |line| case line when /^\[(\w+)\]$/: section = $1 # Section names when /^\s*#/: next # Skip comments when /^\s*$/: next # Skip blanks when /^\s*(\w+)\s*=\s*(.+)$/: # settings var = $1.intern if var == :mode value = $2 else value = munge_value($2) end # Only warn if we don't know what this config var is. This # prevents exceptions later on. unless @config.include?(var) or metas.include?(var.to_s) Puppet.warning "Discarded unknown configuration parameter %s" % var.inspect next # Skip this line. end # Mmm, "special" attributes if metas.include?(var.to_s) unless values.include?(section) values[section] = {} end values[section][var.to_s] = value # If the parameter is valid, then set it. if section == Puppet[:name] and @config.include?(var) #@config[var].value = value @values[:main][var] = value end next end # Don't override set parameters, since the file is parsed # after cli arguments are handled. unless @config.include?(var) and @config[var].setbycli Puppet.debug "%s: Setting %s to '%s'" % [section, var, value] - self[var] = value + @values[:main][var] = value end @config[var].section = symbolize(section) metas.each { |meta| if values[section][meta] if @config[var].respond_to?(meta + "=") @config[var].send(meta + "=", values[section][meta]) end end } else raise Puppet::Error, "Could not match line %s" % line end } end # Create a new element. The value is passed in because it's used to determine # what kind of element we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. def newelement(hash) value = hash[:value] || hash[:default] klass = nil if hash[:section] hash[:section] = symbolize(hash[:section]) end case value when true, false, "true", "false": klass = CBoolean when /^\$\w+\//, /^\//: klass = CFile when String, Integer, Float: # nothing klass = CElement else raise Puppet::Error, "Invalid value '%s' for %s" % [value.inspect, hash[:name]] end hash[:parent] = self element = klass.new(hash) return element end # This has to be private, because it doesn't add the elements to @config private :newelement # Iterate across all of the objects in a given section. def persection(section) section = symbolize(section) self.each { |name, obj| if obj.section == section yield obj end } end # Reparse our config file, if necessary. def reparse if defined? @file and @file.changed? Puppet.notice "Reparsing %s" % @file.file @@sync.synchronize do parse(@file) end reuse() end end def reuse return unless defined? @used @@sync.synchronize do # yay, thread-safe @used.each do |section| @used.delete(section) self.use(section) end end end # The order in which to search for values. def searchpath(environment = nil) if environment [:cache, :cli, :memory, environment, :name, :main] else [:cache, :cli, :memory, :name, :main] end end # Get a list of objects per section def sectionlist sectionlist = [] self.each { |name, obj| section = obj.section || "puppet" sections[section] ||= [] unless sectionlist.include?(section) sectionlist << section end sections[section] << obj } return sectionlist, sections end # Convert a single section into transportable objects. def section_to_transportable(section, done = nil, includefiles = true) done ||= Hash.new { |hash, key| hash[key] = {} } objects = [] persection(section) do |obj| if @config[:mkusers] and value(:mkusers) [:owner, :group].each do |attr| type = nil if attr == :owner type = :user else type = attr end # If a user and/or group is set, then make sure we're # managing that object if obj.respond_to? attr and name = obj.send(attr) # Skip root or wheel next if %w{root wheel}.include?(name.to_s) # Skip owners and groups we've already done, but tag # them with our section if necessary if done[type].include?(name) tags = done[type][name].tags unless tags.include?(section) done[type][name].tags = tags << section end elsif newobj = Puppet::Type.type(type)[name] unless newobj.property(:ensure) newobj[:ensure] = "present" end newobj.tag(section) if type == :user newobj[:comment] ||= "%s user" % name end else newobj = Puppet::TransObject.new(name, type.to_s) newobj.tags = ["puppet", "configuration", section] newobj[:ensure] = "present" if type == :user newobj[:comment] ||= "%s user" % name end # Set the group appropriately for the user if type == :user newobj[:gid] = Puppet[:group] end done[type][name] = newobj objects << newobj end end end end if obj.respond_to? :to_transportable next if value(obj.name) =~ /^\/dev/ transobjects = obj.to_transportable transobjects = [transobjects] unless transobjects.is_a? Array transobjects.each do |trans| # transportable could return nil next unless trans unless done[:file].include? trans.name @created << trans.name objects << trans done[:file][trans.name] = trans end end end end bucket = Puppet::TransBucket.new bucket.type = section bucket.push(*objects) bucket.keyword = "class" return bucket end # Set a bunch of defaults in a given section. The sections are actually pretty # pointless, but they help break things up a bit, anyway. def setdefaults(section, defs) section = symbolize(section) defs.each { |name, hash| if hash.is_a? Array unless hash.length == 2 raise ArgumentError, "Defaults specified as an array must contain only the default value and the decription" end tmp = hash hash = {} [:default, :desc].zip(tmp).each { |p,v| hash[p] = v } end name = symbolize(name) hash[:name] = name hash[:section] = section name = hash[:name] if @config.include?(name) raise ArgumentError, "Parameter %s is already defined" % name end tryconfig = newelement(hash) if short = tryconfig.short if other = @shortnames[short] raise ArgumentError, "Parameter %s is already using short name '%s'" % [other.name, short] end @shortnames[short] = tryconfig end @config[name] = tryconfig } end # Create a timer to check whether the file should be reparsed. def settimer if Puppet[:filetimeout] > 0 @timer = Puppet.newtimer( :interval => Puppet[:filetimeout], :tolerance => 1, :start? => true ) do self.reparse() end end end # Convert our list of objects into a component that can be applied. def to_component transport = self.to_transportable return transport.to_type end # Convert our list of objects into a configuration file. def to_config str = %{The configuration file for #{Puppet[:name]}. Note that this file is likely to have unused configuration parameters in it; any parameter that's valid anywhere in Puppet can be in any config file, even if it's not used. Every section can specify three special parameters: owner, group, and mode. These parameters affect the required permissions of any files specified after their specification. Puppet will sometimes use these parameters to check its own configured state, so they can be used to make Puppet a bit more self-managing. Note also that the section names are entirely for human-level organizational purposes; they don't provide separate namespaces. All parameters are in a single namespace. Generated on #{Time.now}. }.gsub(/^/, "# ") # Add a section heading that matches our name. if @config.include?(:name) str += "[%s]\n" % self[:name] end eachsection do |section| persection(section) do |obj| str += obj.to_config + "\n" end end return str end # Convert our configuration into a list of transportable objects. def to_transportable done = Hash.new { |hash, key| hash[key] = {} } topbucket = Puppet::TransBucket.new if defined? @file.file and @file.file topbucket.name = @file.file else topbucket.name = "configtop" end topbucket.type = "puppetconfig" topbucket.top = true # Now iterate over each section eachsection do |section| topbucket.push section_to_transportable(section, done) end topbucket end # Convert to a parseable manifest def to_manifest transport = self.to_transportable manifest = transport.to_manifest + "\n" eachsection { |section| manifest += "include #{section}\n" } return manifest end # Create the necessary objects to use a section. This is idempotent; # you can 'use' a section as many times as you want. def use(*sections) @@sync.synchronize do # yay, thread-safe unless defined? @used @used = [] end runners = sections.collect { |s| symbolize(s) }.find_all { |s| ! @used.include? s } return if runners.empty? bucket = Puppet::TransBucket.new bucket.type = "puppetconfig" bucket.top = true # Create a hash to keep track of what we've done so far. @done = Hash.new { |hash, key| hash[key] = {} } runners.each do |section| bucket.push section_to_transportable(section, @done, false) end objects = bucket.to_type objects.finalize tags = nil if Puppet[:tags] tags = Puppet[:tags] Puppet[:tags] = "" end trans = objects.evaluate trans.ignoretags = true trans.configurator = true trans.evaluate if tags Puppet[:tags] = tags end # Remove is a recursive process, so it's sufficient to just call # it on the component. objects.remove(true) objects = nil runners.each { |s| @used << s } end end def valid?(param) param = symbolize(param) @config.has_key?(param) end # Find the correct value using our search path. Optionally accept an environment # in which to search before the other configuration sections. def value(param, environment = nil) param = symbolize(param) environment = symbolize(environment) if environment # Short circuit to nil for undefined parameters. return nil unless @config.include?(param) # Yay, recursion. self.reparse() unless param == :filetimeout # See if we can find it within our searchable list of values searchpath(environment).each do |source| # Modify the source as necessary. source = case source when :name: self.name else source end # Look for the value. if @values[source].include?(param) val = @values[source][param] # Cache the value, because we do so many parameter lookups. unless source == :cache val = convert(val) @values[:cache][param] = val end return val end end # No normal source, so get the default and cache it val = convert(@config[param].default) @values[:cache][param] = val return val end # Open a file with the appropriate user, group, and mode def write(default, *args) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default %s" % default end unless obj.is_a? CFile raise ArgumentError, "Default %s is not a file" % default end chown = nil if Puppet::Util::SUIDManager.uid == 0 chown = [obj.owner, obj.group] else chown = [nil, nil] end Puppet::Util::SUIDManager.asuser(*chown) do mode = obj.mode || 0640 if args.empty? args << "w" end args << mode File.open(value(obj.name), *args) do |file| yield file end end end # Open a non-default file under a default dir with the appropriate user, # group, and mode def writesub(default, file, *args) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default %s" % default end unless obj.is_a? CFile raise ArgumentError, "Default %s is not a file" % default end chown = nil if Puppet::Util::SUIDManager.uid == 0 chown = [obj.owner, obj.group] else chown = [nil, nil] end Puppet::Util::SUIDManager.asuser(*chown) do mode = obj.mode || 0640 if args.empty? args << "w" end args << mode # Update the umask to make non-executable files Puppet::Util.withumask(File.umask ^ 0111) do File.open(file, *args) do |file| yield file end end end end private # Extract extra setting information for files. def extract_fileinfo(string) result = {} value = string.sub(/\{\s*([^}]+)\s*\}/) do params = $1 params.split(/\s*,\s*/).each do |str| if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/ param, value = $1.intern, $2 result[param] = value unless [:owner, :mode, :group].include?(param) raise ArgumentError, "Invalid file option '%s'" % param end if param == :mode and value !~ /^\d+$/ raise ArgumentError, "File modes must be numbers" end else raise ArgumentError, "Could not parse '%s'" % string end end '' end result[:value] = value.sub(/\s*$/, '') return result return nil end # Convert arguments into booleans, integers, or whatever. def munge_value(value) # Handle different data types correctly return case value when /^false$/i: false when /^true$/i: true when /^\d+$/i: Integer(value) else value.gsub(/^["']|["']$/,'').sub(/\s+$/, '') end end # This is an abstract method that just turns a file in to a hash of hashes. # We mostly need this for backward compatibility -- as of May 2007 we need to # support parsing old files with any section, or new files with just two # valid sections. def parse_file(file) text = read_file(file) # Create a timer so that this file will get checked automatically # and reparsed if necessary. settimer() result = Hash.new { |names, name| names[name] = {} } count = 0 # Default to 'main' for the section. section = :main result[section][:_meta] = {} text.split(/\n/).each { |line| count += 1 case line when /^\s*\[(\w+)\]$/: section = $1.intern # Section names # Add a meta section result[section][:_meta] ||= {} when /^\s*#/: next # Skip comments when /^\s*$/: next # Skip blanks when /^\s*(\w+)\s*=\s*(.+)$/: # settings var = $1.intern # We don't want to munge modes, because they're specified in octal, so we'll # just leave them as a String, since Puppet handles that case correctly. if var == :mode value = $2 else value = munge_value($2) end # Check to see if this is a file argument and it has extra options begin if value.is_a?(String) and options = extract_fileinfo(value) value = options[:value] options.delete(:value) result[section][:_meta][var] = options end result[section][var] = value rescue Puppet::Error => detail detail.file = file detail.line = line raise end else error = Puppet::Error.new("Could not match line %s" % line) error.file = file error.line = line raise error end } return result end # Read the file in. def read_file(file) if file.is_a? Puppet::Util::LoadedFile @file = file else @file = Puppet::Util::LoadedFile.new(file) end begin return File.read(@file.file) rescue Errno::ENOENT raise ArgumentError, "No such file %s" % file rescue Errno::EACCES raise ArgumentError, "Permission denied to file %s" % file end end - # Take all members of a hash and assign their values appropriately. - def set_parameter_hash(params) - params.each do |param, value| - next if param == :_meta - unless @config.include?(param) - Puppet.warning "Discarded unknown configuration parameter %s" % param - next - end - if @config[param].setbycli - Puppet.debug "Ignoring %s set by config file; overridden by cli" % param - else - self[param] = value - end - end - end - # Set file metadata. def set_metadata(meta) meta.each do |var, values| values.each do |param, value| @config[var].send(param.to_s + "=", value) end end end # The base element type. class CElement attr_accessor :name, :section, :default, :parent, :setbycli attr_reader :desc, :short # Unset any set value. def clear @value = nil end def desc=(value) @desc = value.gsub(/^\s*/, '') end # get the arguments in getopt format def getopt_args if short [["--#{name}", "-#{short}", GetoptLong::REQUIRED_ARGUMENT]] else [["--#{name}", GetoptLong::REQUIRED_ARGUMENT]] end end def hook=(block) meta_def :handle, &block end # Create the new element. Pretty much just sets the name. def initialize(args = {}) if args.include?(:parent) self.parent = args[:parent] args.delete(:parent) end args.each do |param, value| method = param.to_s + "=" unless self.respond_to? method raise ArgumentError, "%s does not accept %s" % [self.class, param] end self.send(method, value) end unless self.desc raise ArgumentError, "You must provide a description for the %s config option" % self.name end end def iscreated @iscreated = true end def iscreated? if defined? @iscreated return @iscreated else return false end end def set? if defined? @value and ! @value.nil? return true else return false end end # short name for the celement def short=(value) if value.to_s.length != 1 raise ArgumentError, "Short names can only be one character." end @short = value.to_s end # Convert the object to a config statement. def to_config str = @desc.gsub(/^/, "# ") + "\n" # Add in a statement about the default. if defined? @default and @default str += "# The default value is '%s'.\n" % @default end # If the value has not been overridden, then print it out commented # and unconverted, so it's clear that that's the default and how it # works. value = @parent.value(self.name) if value != @default line = "%s = %s" % [@name, value] else line = "# %s = %s" % [@name, @default] end str += line + "\n" str.gsub(/^/, " ") end # Retrieves the value, or if it's not set, retrieves the default. def value @parent.value(self.name) end end # A file. class CFile < CElement attr_writer :owner, :group attr_accessor :mode, :create def group if defined? @group return @parent.convert(@group) else return nil end end def owner if defined? @owner return @parent.convert(@owner) else return nil end end # Set the type appropriately. Yep, a hack. This supports either naming # the variable 'dir', or adding a slash at the end. def munge(value) if value.to_s =~ /\/$/ @type = :directory return value.sub(/\/$/, '') end return value end # Return the appropriate type. def type value = @parent.value(self.name) if @name.to_s =~ /dir/ return :directory elsif value.to_s =~ /\/$/ return :directory elsif value.is_a? String return :file else return nil end end # Convert the object to a TransObject instance. # FIXME There's no dependency system in place right now; if you use # a section that requires another section, there's nothing done to # correct that for you, at the moment. def to_transportable type = self.type return nil unless type path = @parent.value(self.name).split(File::SEPARATOR) path.shift # remove the leading nil objects = [] obj = Puppet::TransObject.new(self.value, "file") # Only create directories, or files that are specifically marked to # create. if type == :directory or self.create obj[:ensure] = type end [:mode].each { |var| if value = self.send(var) # Don't bother converting the mode, since the file type # can handle it any old way. obj[var] = value end } # Only chown or chgrp when root if Puppet::Util::SUIDManager.uid == 0 [:group, :owner].each { |var| if value = self.send(var) obj[var] = value end } end # And set the loglevel to debug for everything obj[:loglevel] = "debug" # We're not actually modifying any files here, and if we allow a # filebucket to get used here we get into an infinite recursion # trying to set the filebucket up. obj[:backup] = false if self.section obj.tags += ["puppet", "configuration", self.section, self.name] end objects << obj objects end # Make sure any provided variables look up to something. def validate(value) return true unless value.is_a? String value.scan(/\$(\w+)/) { |name| name = $1 unless @parent.include?(name) raise ArgumentError, "Configuration parameter '%s' is undefined" % name end } end end # A simple boolean. class CBoolean < CElement # get the arguments in getopt format def getopt_args if short [["--#{name}", "-#{short}", GetoptLong::NO_ARGUMENT], ["--no-#{name}", GetoptLong::NO_ARGUMENT]] else [["--#{name}", GetoptLong::NO_ARGUMENT], ["--no-#{name}", GetoptLong::NO_ARGUMENT]] end end def munge(value) case value when true, "true": return true when false, "false": return false else raise ArgumentError, "Invalid value '%s' for %s" % [value.inspect, @name] end end end end # $Id$ diff --git a/spec/other/modules.rb b/spec/other/modules.rb new file mode 100755 index 000000000..1afd3d863 --- /dev/null +++ b/spec/other/modules.rb @@ -0,0 +1,150 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../spec_helper' + +describe Puppet::Module, " when building its search path" do + include PuppetTest + + it "should ignore unqualified paths in the search path" do + Puppet[:modulepath] = "something:/my/something" + File.stubs(:directory?).returns(true) + Puppet::Module.modulepath.should == %w{/my/something} + end + + it "should ignore paths that do not exist" do + Puppet[:modulepath] = "/yes:/no" + File.expects(:directory?).with("/yes").returns(true) + File.expects(:directory?).with("/no").returns(false) + Puppet::Module.modulepath.should == %w{/yes} + end + + it "should prepend PUPPETLIB in search path when set" do + Puppet[:modulepath] = "/my/mod:/other/mod" + ENV["PUPPETLIB"] = "/env/mod:/myenv/mod" + File.stubs(:directory?).returns(true) + Puppet::Module.modulepath.should == %w{/env/mod /myenv/mod /my/mod /other/mod} + end + + it "should use the environment-specific search path when a node environment is provided" do + Puppet.config.expects(:value).with(:modulepath, "myenv").returns("/mone:/mtwo") + File.stubs(:directory?).returns(true) + Puppet::Module.modulepath("myenv").should == %w{/mone /mtwo} + end + + after do + ENV["PUPPETLIB"] = nil + end +end + +describe Puppet::Module, " when searching for modules" do + it "should find modules in the search path" do + path = %w{/dir/path} + Puppet::Module.stubs(:modulepath).returns(path) + File.stubs(:directory?).returns(true) + mod = Puppet::Module.find("mymod") + mod.should be_an_instance_of(Puppet::Module) + mod.path.should == "/dir/path/mymod" + end + + it "should not search for fully qualified modules" do + path = %w{/dir/path} + Puppet::Module.expects(:modulepath).never + File.expects(:directory?).never + Puppet::Module.find("/mymod").should be_nil + end + + it "should search for modules in the order specified in the search path" do + Puppet[:modulepath] = "/one:/two:/three" + Puppet::Module.stubs(:modulepath).returns %w{/one /two /three} + File.expects(:directory?).with("/one/mod").returns(false) + File.expects(:directory?).with("/two/mod").returns(true) + File.expects(:directory?).with("/three/mod").never + mod = Puppet::Module.find("mod") + mod.path.should == "/two/mod" + end + + it "should use a node environment if specified" do + Puppet::Module.expects(:modulepath).with("myenv").returns([]) + Puppet::Module.find("mymod", "myenv") + end +end + +describe Puppet::Module, " when searching for templates" do + it "should return fully-qualified templates directly" do + Puppet::Module.expects(:modulepath).never + Puppet::Module.find_template("/my/template").should == "/my/template" + end + + it "should return the template from the first found module" do + Puppet[:modulepath] = "/one:/two" + File.stubs(:directory?).returns(true) + Puppet::Module.find_template("mymod/mytemplate").should == "/one/mymod/templates/mytemplate" + end + + it "should use the main templatedir if no module is found" do + Puppet.config.expects(:value).with(:templatedir, nil).returns("/my/templates") + Puppet::Module.expects(:find).with("mymod", nil).returns(nil) + Puppet::Module.find_template("mymod/mytemplate").should == "/my/templates/mymod/mytemplate" + end + + it "should use the environment templatedir if no module is found and an environment is specified" do + Puppet.config.expects(:value).with(:templatedir, "myenv").returns("/myenv/templates") + Puppet::Module.expects(:find).with("mymod", "myenv").returns(nil) + Puppet::Module.find_template("mymod/mytemplate", "myenv").should == "/myenv/templates/mymod/mytemplate" + end + + it "should use the node environment if specified" do + Puppet.config.expects(:value).with(:modulepath, "myenv").returns("/my/templates") + File.stubs(:directory?).returns(true) + Puppet::Module.find_template("mymod/envtemplate", "myenv").should == "/my/templates/mymod/templates/envtemplate" + end + + after { Puppet.config.clear } +end + +describe Puppet::Module, " when searching for manifests" do + it "should return the manifests from the first found module" do + Puppet[:modulepath] = "/one:/two" + File.stubs(:directory?).returns(true) + Dir.expects(:glob).with("/one/mymod/manifests/init.pp").returns(%w{/one/mymod/manifests/init.pp}) + Puppet::Module.find_manifests("mymod/init.pp").should == ["/one/mymod/manifests/init.pp"] + end + + it "should search the cwd if no module is found" do + Puppet[:modulepath] = "/one:/two" + File.stubs(:find).returns(nil) + cwd = Dir.getwd + Dir.expects(:glob).with("#{cwd}/mymod/init.pp").returns(["#{cwd}/mymod/init.pp"]) + Puppet::Module.find_manifests("mymod/init.pp").should == ["#{cwd}/mymod/init.pp"] + end + + it "should use the node environment if specified" do + Puppet.config.expects(:value).with(:modulepath, "myenv").returns("/env/modules") + File.stubs(:directory?).returns(true) + Dir.expects(:glob).with("/env/modules/mymod/manifests/envmanifest.pp").returns(%w{/env/modules/mymod/manifests/envmanifest.pp}) + Puppet::Module.find_manifests("mymod/envmanifest.pp", :environment => "myenv").should == ["/env/modules/mymod/manifests/envmanifest.pp"] + end + + it "should return all manifests matching the glob pattern" do + Puppet.config.expects(:value).with(:modulepath, nil).returns("/my/modules") + File.stubs(:directory?).returns(true) + Dir.expects(:glob).with("/my/modules/mymod/manifests/yay/*.pp").returns(%w{/one /two}) + Puppet::Module.find_manifests("mymod/yay/*.pp").should == %w{/one /two} + end + + it "should default to the 'init.pp' file in the manifests directory" do + Puppet.config.expects(:value).with(:modulepath, nil).returns("/my/modules") + File.stubs(:directory?).returns(true) + Dir.expects(:glob).with("/my/modules/mymod/manifests/init.pp").returns(%w{my manifest}) + Puppet::Module.find_manifests("mymod").should == %w{my manifest} + end + + after { Puppet.config.clear } +end + +describe Puppet::Module, " when returning files" do + it "should return the path to the module's 'files' directory" do + mod = Puppet::Module.send(:new, "mymod", "/my/mod") + mod.files.should == "/my/mod/files" + end +end diff --git a/test/puppet/modules.rb b/test/puppet/modules.rb deleted file mode 100755 index 15976c64a..000000000 --- a/test/puppet/modules.rb +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env ruby - -$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ - -require 'puppettest' - -class TestModules < Test::Unit::TestCase - include PuppetTest - - def setup - super - @varmods = File::join(Puppet[:vardir], "modules") - FileUtils::mkdir_p(@varmods) - end - - def test_modulepath - Puppet[:modulepath] = "$vardir/modules:/no/such/path/anywhere:.::" - assert_equal([ @varmods ], Puppet::Module.modulepath) - end - - def test_find - assert_nil(Puppet::Module::find("/tmp")) - - file = "testmod/something" - assert_nil(Puppet::Module::find(file)) - - path = File::join(@varmods, "testmod") - FileUtils::mkdir_p(path) - - mod = Puppet::Module::find("testmod") - assert_not_nil(mod) - assert_equal("testmod", mod.name) - assert_equal(path, mod.path) - - mod = Puppet::Module::find(file) - assert_not_nil(mod) - assert_equal("testmod", mod.name) - assert_equal(path, mod.path) - end - - def test_find_template - templ = "testmod/templ.erb" - assert_equal(File::join(Puppet[:templatedir], templ), - Puppet::Module::find_template(templ)) - - templ_path = File::join(@varmods, "testmod", - Puppet::Module::TEMPLATES, "templ.erb") - FileUtils::mkdir_p(File::dirname(templ_path)) - File::open(templ_path, "w") { |f| f.puts "Howdy" } - - assert_equal(templ_path, Puppet::Module::find_template(templ)) - - mod = Puppet::Module::find(templ) - assert_not_nil(mod) - assert_equal(templ_path, mod.template(templ)) - end -end - -# $Id$