diff --git a/lib/puppet/file_collection.rb b/lib/puppet/file_collection.rb index a7bdd04a8..42ebfd477 100644 --- a/lib/puppet/file_collection.rb +++ b/lib/puppet/file_collection.rb @@ -1,30 +1,19 @@ -# A simple way to turn file names into singletons, -# so we don't have tons of copies of each file path around. +# This was a simple way to turn file names into singletons, +# +# The theory was, like: +# 1. Turn filenames into singletons. +# 2. ???? +# 3. Huge memory savings! +# +# In practice it used several MB more memory overall, and it cost more CPU +# time, and it added complexity to the code. Which was awesome. +# +# So, I gutted it. It doesn't do anything any more, but we retain the +# external form that people included so that they don't explode so much. +# +# This should be removed from the system after a graceful deprecation period, +# probably about the time that a version of Puppet containing this change is +# the last supported version. --daniel 2012-07-17 class Puppet::FileCollection require 'puppet/file_collection/lookup' - - def self.collection - @collection - end - - def initialize - @paths = [] - @inverse = {} - end - - def index(path) - if i = @inverse[path] - return i - else - @paths << path - i = @inverse[path] = @paths.length - 1 - return i - end - end - - def path(index) - @paths[index] - end - - @collection = self.new end diff --git a/lib/puppet/file_collection/lookup.rb b/lib/puppet/file_collection/lookup.rb index 30679480d..598b1842c 100644 --- a/lib/puppet/file_collection/lookup.rb +++ b/lib/puppet/file_collection/lookup.rb @@ -1,20 +1,10 @@ require 'puppet/file_collection' -# A simple module for looking up file paths and indexes -# in a file collection. module Puppet::FileCollection::Lookup - attr_accessor :line, :file_index - - def file_collection - Puppet::FileCollection.collection - end - - def file=(path) - @file_index = file_collection.index(path) - end - - def file - return nil unless file_index - file_collection.path(file_index) - end + # Yeah, this is all the external interface that was added to the folks who + # included this really was. Thankfully. + # + # See the comments in `puppet/file_collection.rb` for the annotated version, + # or just port your code away from this by adding the accessors on your own. + attr_accessor :line, :file end diff --git a/lib/puppet/network/rights.rb b/lib/puppet/network/rights.rb index 6fde181c9..4d68a95ef 100755 --- a/lib/puppet/network/rights.rb +++ b/lib/puppet/network/rights.rb @@ -1,276 +1,275 @@ require 'puppet/network/authstore' require 'puppet/error' module Puppet::Network # this exception is thrown when a request is not authenticated class AuthorizationError < Puppet::Error; end # Define a set of rights and who has access to them. # There are two types of rights: # * named rights (ie a common string) # * path based rights (which are matched on a longest prefix basis) class Rights # We basically just proxy directly to our rights. Each Right stores # its own auth abilities. [:allow, :deny, :restrict_method, :restrict_environment, :restrict_authenticated].each do |method| define_method(method) do |name, *args| if obj = self[name] obj.send(method, *args) else raise ArgumentError, "Unknown right '#{name}'" end end end # Check that name is allowed or not def allowed?(name, *args) !is_forbidden_and_why?(name, :node => args[0], :ip => args[1]) end def is_request_forbidden_and_why?(indirection, method, key, params) methods_to_check = if method == :head # :head is ok if either :find or :save is ok. [:find, :save] else [method] end authorization_failure_exceptions = methods_to_check.map do |method| is_forbidden_and_why?("/#{indirection}/#{key}", params.merge({:method => method})) end if authorization_failure_exceptions.include? nil # One of the methods we checked is ok, therefore this request is ok. nil else # Just need to return any of the failure exceptions. authorization_failure_exceptions.first end end def is_forbidden_and_why?(name, args = {}) res = :nomatch right = @rights.find do |acl| found = false # an acl can return :dunno, which means "I'm not qualified to answer your question, # please ask someone else". This is used when for instance an acl matches, but not for the # current rest method, where we might think some other acl might be more specific. if match = acl.match?(name) args[:match] = match if (res = acl.allowed?(args[:node], args[:ip], args)) != :dunno # return early if we're allowed return nil if res # we matched, select this acl found = true end end found end # if we end here, then that means we either didn't match # or failed, in any case will throw an error to the outside world if name =~ /^\// or right # we're a patch ACL, let's fail msg = "#{(args[:node].nil? ? args[:ip] : "#{args[:node]}(#{args[:ip]})")} access to #{name} [#{args[:method]}]" msg += " authenticated " if args[:authenticated] error = AuthorizationError.new("Forbidden request: #{msg}") if right error.file = right.file error.line = right.line end else # there were no rights allowing/denying name # if name is not a path, let's throw raise ArgumentError, "Unknown namespace right '#{name}'" end error end def initialize @rights = [] end def [](name) @rights.find { |acl| acl == name } end def include?(name) @rights.include?(name) end def each @rights.each { |r| yield r.name,r } end # Define a new right to which access can be provided. def newright(name, line=nil, file=nil) add_right( Right.new(name, line, file) ) end private def add_right(right) if right.acl_type == :name and include?(right.key) raise ArgumentError, "Right '%s' already exists" end @rights << right sort_rights right end def sort_rights @rights.sort! end # Retrieve a right by name. def right(name) self[name] end # A right. class Right < Puppet::Network::AuthStore - include Puppet::FileCollection::Lookup - attr_accessor :name, :key, :acl_type attr_accessor :methods, :environment, :authentication + attr_accessor :line, :file ALL = [:save, :destroy, :find, :search] Puppet::Util.logmethods(self, true) def initialize(name, line, file) @methods = [] @environment = [] @authentication = true # defaults to authenticated @name = name @line = line || 0 @file = file case name when Symbol @acl_type = :name @key = name when /^\[(.+)\]$/ @acl_type = :name @key = $1.intern if name.is_a?(String) when /^\// @acl_type = :regex @key = Regexp.new("^" + Regexp.escape(name)) @methods = ALL when /^~/ # this is a regex @acl_type = :regex @name = name.gsub(/^~\s+/,'') @key = Regexp.new(@name) @methods = ALL else raise ArgumentError, "Unknown right type '#{name}'" end super() end def to_s "access[#{@name}]" end # There's no real check to do at this point def valid? true end def regex? acl_type == :regex end # does this right is allowed for this triplet? # if this right is too restrictive (ie we don't match this access method) # then return :dunno so that upper layers have a chance to try another right # tailored to the given method def allowed?(name, ip, args = {}) return :dunno if acl_type == :regex and not @methods.include?(args[:method]) return :dunno if acl_type == :regex and @environment.size > 0 and not @environment.include?(args[:environment]) return :dunno if acl_type == :regex and not @authentication.nil? and args[:authenticated] != @authentication begin # make sure any capture are replaced if needed interpolate(args[:match]) if acl_type == :regex and args[:match] res = super(name,ip) ensure reset_interpolation if acl_type == :regex end res end # restrict this right to some method only def restrict_method(m) m = m.intern if m.is_a?(String) raise ArgumentError, "'#{m}' is not an allowed value for method directive" unless ALL.include?(m) # if we were allowing all methods, then starts from scratch if @methods === ALL @methods = [] end raise ArgumentError, "'#{m}' is already in the '#{name}' ACL" if @methods.include?(m) @methods << m end def restrict_environment(env) env = Puppet::Node::Environment.new(env) raise ArgumentError, "'#{env}' is already in the '#{name}' ACL" if @environment.include?(env) @environment << env end def restrict_authenticated(authentication) case authentication when "yes", "on", "true", true authentication = true when "no", "off", "false", false authentication = false when "all","any", :all, :any authentication = nil else raise ArgumentError, "'#{name}' incorrect authenticated value: #{authentication}" end @authentication = authentication end def match?(key) # if we are a namespace compare directly return self.key == namespace_to_key(key) if acl_type == :name # otherwise match with the regex self.key.match(key) end def namespace_to_key(key) key = key.intern if key.is_a?(String) key end # this is where all the magic happens. # we're sorting the rights array with this scheme: # * namespace rights are all in front # * regex path rights are then all queued in file order def <=>(rhs) # move namespace rights at front return self.acl_type == :name ? -1 : 1 if self.acl_type != rhs.acl_type # sort by creation order (ie first match appearing in the file will win) # that is don't sort, in which case the sort algorithm will order in the # natural array order (ie the creation order) 0 end def ==(name) return(acl_type == :name ? self.key == namespace_to_key(name) : self.name == name.gsub(/^~\s+/,'')) end end end end diff --git a/lib/puppet/parser/ast.rb b/lib/puppet/parser/ast.rb index 8918d22e9..e4f7aaf61 100644 --- a/lib/puppet/parser/ast.rb +++ b/lib/puppet/parser/ast.rb @@ -1,142 +1,139 @@ # the parent class for all of our syntactical objects require 'puppet' require 'puppet/util/autoload' -require 'puppet/file_collection/lookup' # 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::FileCollection::Lookup - include Puppet::Util::Errors include Puppet::Util::MethodHelper include Puppet::Util::Docs - attr_accessor :parent, :scope + attr_accessor :parent, :scope, :file, :line def inspect "( #{self.class} #{self.to_s} #{@children.inspect} )" end # don't fetch lexer comment by default def use_docs self.class.use_docs end # allow our subclass to specify they want documentation class << self attr_accessor :use_docs def associates_doc self.use_docs = true end end # Does this ast object set something? If so, it gets evaluated first. def self.settor? if defined?(@settor) @settor else false end end # Evaluate the current object. Just a stub method, since the subclass # should override this method. # of the contained children and evaluates them in turn, returning a # list of all of the collected values, rejecting nil values def evaluate(*options) raise Puppet::DevError, "Did not override #evaluate in #{self.class}" 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(*options) # We duplicate code here, rather than using exceptwrap, because this # is called so many times during parsing. begin return self.evaluate(*options) rescue Puppet::Error => detail raise adderrorcontext(detail) rescue => detail error = Puppet::Error.new(detail.to_s) # We can't use self.fail here because it always expects strings, # not exceptions. raise adderrorcontext(error, detail) end 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) set_options(args) end # evaluate ourselves, and match def evaluate_match(value, scope) obj = self.safeevaluate(scope) obj = obj.downcase if obj.respond_to?(:downcase) value = value.downcase if value.respond_to?(:downcase) obj = Puppet::Parser::Scope.number?(obj) || obj value = Puppet::Parser::Scope.number?(value) || value # "" == undef for case/selector/if obj == value or (obj == "" and value == :undef) or (obj == :undef and value == "") end end # And include all of the AST subclasses. require 'puppet/parser/ast/arithmetic_operator' require 'puppet/parser/ast/astarray' require 'puppet/parser/ast/asthash' require 'puppet/parser/ast/boolean_operator' require 'puppet/parser/ast/branch' require 'puppet/parser/ast/caseopt' require 'puppet/parser/ast/casestatement' require 'puppet/parser/ast/collection' require 'puppet/parser/ast/collexpr' require 'puppet/parser/ast/comparison_operator' require 'puppet/parser/ast/definition' require 'puppet/parser/ast/else' require 'puppet/parser/ast/function' require 'puppet/parser/ast/hostclass' require 'puppet/parser/ast/ifstatement' require 'puppet/parser/ast/in_operator' require 'puppet/parser/ast/leaf' require 'puppet/parser/ast/match_operator' require 'puppet/parser/ast/minus' require 'puppet/parser/ast/node' require 'puppet/parser/ast/nop' require 'puppet/parser/ast/not' require 'puppet/parser/ast/relationship' require 'puppet/parser/ast/resource' require 'puppet/parser/ast/resource_defaults' require 'puppet/parser/ast/resource_instance' require 'puppet/parser/ast/resource_override' require 'puppet/parser/ast/resource_reference' require 'puppet/parser/ast/resourceparam' require 'puppet/parser/ast/selector' require 'puppet/parser/ast/tag' require 'puppet/parser/ast/vardef' diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index bf967cfa8..9347c5fc5 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,342 +1,341 @@ require 'puppet/resource' # The primary difference between this class and its # parent is that this class has rules on who can set # parameters class Puppet::Parser::Resource < Puppet::Resource require 'puppet/parser/resource/param' require 'puppet/util/tagging' - require 'puppet/file_collection/lookup' require 'puppet/parser/yaml_trimmer' require 'puppet/resource/type_collection_helper' - include Puppet::FileCollection::Lookup include Puppet::Resource::TypeCollectionHelper include Puppet::Util include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging include Puppet::Util::Tagging include Puppet::Parser::YamlTrimmer attr_accessor :source, :scope, :collector_id attr_accessor :virtual, :override, :translated, :catalog, :evaluated + attr_accessor :file, :line attr_reader :exported, :parameters # Determine whether the provided parameter name is a relationship parameter. def self.relationship_parameter?(name) @relationship_names ||= Puppet::Type.relationship_params.collect { |p| p.name } @relationship_names.include?(name) end # Set up some boolean test methods def translated?; !!@translated; end def override?; !!@override; end def evaluated?; !!@evaluated; end def [](param) param = symbolize(param) if param == :title return self.title end if @parameters.has_key?(param) @parameters[param].value else nil end end def []=(param, value) set_parameter(param, value) end def eachparam @parameters.each do |name, param| yield param end end def environment scope.environment end # Process the stage metaparameter for a class. A containment edge # is drawn from the class to the stage. The stage for containment # defaults to main, if none is specified. def add_edge_to_stage return unless self.type.to_s.downcase == "class" unless stage = catalog.resource(:stage, self[:stage] || (scope && scope.resource && scope.resource[:stage]) || :main) raise ArgumentError, "Could not find stage #{self[:stage] || :main} specified by #{self}" end self[:stage] ||= stage.title unless stage.title == :main catalog.add_edge(stage, self) end # Retrieve the associated definition and evaluate it. def evaluate return if evaluated? @evaluated = true if klass = resource_type and ! builtin_type? finish evaluated_code = klass.evaluate_code(self) return evaluated_code elsif builtin? devfail "Cannot evaluate a builtin type (#{type})" else self.fail "Cannot find definition #{type}" end end # Mark this resource as both exported and virtual, # or remove the exported mark. def exported=(value) if value @virtual = true @exported = value else @exported = value end end # Do any finishing work on this object, called before evaluation or # before storage/translation. def finish return if finished? @finished = true add_defaults add_metaparams add_scope_tags validate end # Has this resource already been finished? def finished? @finished end def initialize(*args) raise ArgumentError, "Resources require a scope" unless args.last[:scope] super @source ||= scope.source end # Is this resource modeling an isomorphic resource type? def isomorphic? if builtin_type? return resource_type.isomorphic? else return true end end # Merge an override resource in. This will throw exceptions if # any overrides aren't allowed. def merge(resource) # Test the resource scope, to make sure the resource is even allowed # to override. unless self.source.object_id == resource.source.object_id || resource.source.child_of?(self.source) raise Puppet::ParseError.new("Only subclasses can override parameters", resource.line, resource.file) end # Some of these might fail, but they'll fail in the way we want. resource.parameters.each do |name, param| override_parameter(param) end end # Unless we're running >= 0.25, we're in compat mode. def metaparam_compatibility_mode? ! (catalog and ver = (catalog.client_version||'0.0.0').split(".") and (ver[0] > "0" or ver[1].to_i >= 25)) end def name self[:name] || self.title end # A temporary occasion, until I get paths in the scopes figured out. def path to_s end # Define a parameter in our resource. # if we ever receive a parameter named 'tag', set # the resource tags with its value. def set_parameter(param, value = nil) if ! value.nil? param = Puppet::Parser::Resource::Param.new( :name => param, :value => value, :source => self.source ) elsif ! param.is_a?(Puppet::Parser::Resource::Param) raise ArgumentError, "Must pass a parameter or all necessary values" end tag(*param.value) if param.name == :tag # And store it in our parameter hash. @parameters[param.name] = param end def to_hash @parameters.inject({}) do |hash, ary| param = ary[1] # Skip "undef" values. hash[param.name] = param.value if param.value != :undef hash end end # Create a Puppet::Resource instance from this parser resource. # We plan, at some point, on not needing to do this conversion, but # it's sufficient for now. def to_resource result = Puppet::Resource.new(type, title) to_hash.each do |p, v| if v.is_a?(Puppet::Resource) v = Puppet::Resource.new(v.type, v.title) elsif v.is_a?(Array) # flatten resource references arrays v = v.flatten if v.flatten.find { |av| av.is_a?(Puppet::Resource) } v = v.collect do |av| av = Puppet::Resource.new(av.type, av.title) if av.is_a?(Puppet::Resource) av end end # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. result[p] = if v.is_a?(Array) and v.length == 1 v[0] else v end end result.file = self.file result.line = self.line result.exported = self.exported result.virtual = self.virtual result.tag(*self.tags) result end # Translate our object to a transportable object. def to_trans return nil if virtual? to_resource.to_trans end # Convert this resource to a RAL resource. We hackishly go via the # transportable stuff. def to_ral to_resource.to_ral end private # Add default values from our definition. def add_defaults scope.lookupdefaults(self.type).each do |name, param| unless @parameters.include?(name) self.debug "Adding default for #{name}" @parameters[name] = param.dup end end end def add_backward_compatible_relationship_param(name) # Skip metaparams for which we get no value. return unless val = scope.lookupvar(name.to_s) and val != :undefined # The default case: just set the value set_parameter(name, val) and return unless @parameters[name] # For relationship params, though, join the values (a la #446). @parameters[name].value = [@parameters[name].value, val].flatten 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 add_metaparams compat_mode = metaparam_compatibility_mode? Puppet::Type.eachmetaparam do |name| next unless self.class.relationship_parameter?(name) add_backward_compatible_relationship_param(name) if compat_mode end end def add_scope_tags if scope_resource = scope.resource tag(*scope_resource.tags) end end # Accept a parameter from an override. def override_parameter(param) # This can happen if the override is defining a new parameter, rather # than replacing an existing one. (set_parameter(param) and return) unless current = @parameters[param.name] # The parameter is already set. Fail if they're not allowed to override it. unless param.source.child_of?(current.source) puts caller if Puppet[:trace] msg = "Parameter '#{param.name}' is already set on #{self}" msg += " by #{current.source}" if current.source.to_s != "" if current.file or current.line fields = [] fields << current.file if current.file fields << current.line.to_s if current.line msg += " at #{fields.join(":")}" end msg += "; cannot redefine" raise Puppet::ParseError.new(msg, param.line, param.file) end # If we've gotten this far, we're allowed to override. # Merge with previous value, if the parameter was generated with the +> # syntax. It's important that we use a copy of the new param instance # here, not the old one, and not the original new one, so that the source # is registered correctly for later overrides but the values aren't # implcitly shared when multiple resources are overrriden at once (see # ticket #3556). if param.add param = param.dup param.value = [current.value, param.value].flatten end set_parameter(param) end # Make sure the resource's parameters are all valid for the type. def validate @parameters.each do |name, param| validate_parameter(name) end rescue => detail fail Puppet::ParseError, detail.to_s end private def extract_parameters(params) params.each do |param| # Don't set the same parameter twice self.fail Puppet::ParseError, "Duplicate parameter '#{param.name}' for on #{self}" if @parameters[param.name] set_parameter(param) end end end diff --git a/lib/puppet/parser/resource/param.rb b/lib/puppet/parser/resource/param.rb index c28322337..8031845c6 100644 --- a/lib/puppet/parser/resource/param.rb +++ b/lib/puppet/parser/resource/param.rb @@ -1,27 +1,25 @@ -require 'puppet/file_collection/lookup' require 'puppet/parser/yaml_trimmer' # The parameters we stick in Resources. class Puppet::Parser::Resource::Param - attr_accessor :name, :value, :source, :add include Puppet::Util include Puppet::Util::Errors include Puppet::Util::MethodHelper - - include Puppet::FileCollection::Lookup include Puppet::Parser::YamlTrimmer + attr_accessor :name, :value, :source, :add, :file, :line + def initialize(hash) set_options(hash) requiredopts(:name, :value) @name = symbolize(@name) end def line_to_i line ? Integer(line) : nil end def to_s "#{self.name} => #{self.value}" end end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 27f8cab43..c7162297b 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,1976 +1,1977 @@ require 'puppet' require 'puppet/util/log' require 'puppet/util/metric' require 'puppet/property' require 'puppet/parameter' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/metatype/manager' require 'puppet/util/errors' require 'puppet/util/log_paths' require 'puppet/util/logging' -require 'puppet/file_collection/lookup' require 'puppet/util/tagging' # see the bottom of the file for the rest of the inclusions module Puppet class Type include Puppet::Util include Puppet::Util::Errors include Puppet::Util::LogPaths include Puppet::Util::Logging - include Puppet::FileCollection::Lookup include Puppet::Util::Tagging ############################### # Comparing type instances. include Comparable def <=>(other) # We only order against other types, not arbitrary objects. return nil unless other.is_a? Puppet::Type # Our natural order is based on the reference name we use when comparing # against other type instances. self.ref <=> other.ref end ############################### # Code related to resource type attributes. class << self include Puppet::Util::ClassGen include Puppet::Util::Warnings attr_reader :properties end def self.states warnonce "The states method is deprecated; use properties" properties end # All parameters, in the appropriate order. The key_attributes come first, then # the provider, then the properties, and finally the params and metaparams # in the order they were specified in the files. def self.allattrs key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams end # Retrieve an attribute alias, if there is one. def self.attr_alias(param) @attr_aliases[symbolize(param)] end # Create an alias to an existing attribute. This will cause the aliased # attribute to be valid when setting and retrieving values on the instance. def self.set_attr_alias(hash) hash.each do |new, old| @attr_aliases[symbolize(new)] = symbolize(old) end 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 :property; @validproperties[name] when :meta; @@metaparamhash[name] when :param; @paramhash[name] end end @attrclasses[name] 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 @validproperties.include?(attr); :property when @paramhash.include?(attr); :param when @@metaparamhash.include?(attr); :meta end end @attrtypes[attr] end def self.eachmetaparam @@metaparams.each { |p| yield p.name } 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.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block) else self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do self.defaultvalues end end end # Should we add the 'ensure' property to this class? def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. [:exists?, :create, :destroy].all? { |method| self.public_method_defined?(method) } end # These `apply_to` methods are horrible. They should really be implemented # as part of the usual system of constraints that apply to a type and # provider pair, but were implemented as a separate shadow system. # # We should rip them out in favour of a real constraint pattern around the # target device - whatever that looks like - and not have this additional # magic here. --daniel 2012-03-08 def self.apply_to_device @apply_to = :device end def self.apply_to_host @apply_to = :host end def self.apply_to_all @apply_to = :both end def self.apply_to @apply_to ||= :host end def self.can_apply_to(target) [ target == :device ? :device : :host, :both ].include?(apply_to) end # Deal with any options passed into parameters. def self.handle_param_options(name, options) # If it's a boolean parameter, create a method to test the value easily if options[:boolean] define_method(name.to_s + "?") do val = self[name] if val == :true or val == true return true end end end end # Is the parameter in question a meta-parameter? def self.metaparam?(param) @@metaparamhash.include?(symbolize(param)) end # Find the metaparameter class associated with a given metaparameter name. def self.metaparamclass(name) @@metaparamhash[symbolize(name)] end def self.metaparams @@metaparams.collect { |param| param.name } end def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc 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, options = {}, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = symbolize(name) param = genclass( name, :parent => options[:parent] || Puppet::Parameter, :prefix => "MetaParam", :hash => @@metaparamhash, :array => @@metaparams, :attributes => options[:attributes], &block ) # Grr. param.required_features = options[:required_features] if options[:required_features] handle_param_options(name, options) param.metaparam = true param end def self.key_attribute_parameters @key_attribute_parameters ||= ( params = @parameters.find_all { |param| param.isnamevar? or param.name == :name } ) end def self.key_attributes # This is a cache miss around 0.05 percent of the time. --daniel 2012-07-17 @key_attributes_cache ||= key_attribute_parameters.collect { |p| p.name } end def self.title_patterns case key_attributes.length when 0; [] when 1; [ [ /(.*)/m, [ [key_attributes.first] ] ] ] else raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes" end end def uniqueness_key self.class.key_attributes.sort_by { |attribute_name| attribute_name.to_s }.map{ |attribute_name| self[attribute_name] } 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) options[:attributes] ||= {} param = genclass( name, :parent => options[:parent] || Puppet::Parameter, :attributes => options[:attributes], :block => block, :prefix => "Parameter", :array => @parameters, :hash => @paramhash ) handle_param_options(name, options) # Grr. param.required_features = options[:required_features] if options[:required_features] param.isnamevar if options[:namevar] param end def self.newstate(name, options = {}, &block) Puppet.warning "newstate() has been deprecrated; use newproperty(#{name})" newproperty(name, options, &block) end # Create a new property. The first parameter must be the name of the property; # this is how users will refer to the property when creating new instances. # The second parameter is a hash of options; the options are: # * :parent: The parent class for the property. Defaults to Puppet::Property. # * :retrieve: The method to call on the provider or @parent object (if # the provider is not set) to retrieve the current value. def self.newproperty(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 #{options.inspect}" end raise Puppet::DevError, "Class #{self.name} already has a property named #{name}" if @validproperties.include?(name) if parent = options[:parent] options.delete(:parent) else parent = Puppet::Property 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. prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do # If they've passed a retrieve method, then override the retrieve # method on the class. if options[:retrieve] define_method(:retrieve) do provider.send(options[:retrieve]) end end class_eval(&block) if block end # If it's the 'ensure' property, always put it first. if name == :ensure @properties.unshift prop else @properties << prop end prop end def self.paramdoc(param) @paramhash[param].doc end # Return the parameter names def self.parameters return [] unless defined?(@parameters) @parameters.collect { |klass| klass.name } end # Find the parameter class associated with a given parameter name. def self.paramclass(name) @paramhash[name] end # Return the property class associated with a name def self.propertybyname(name) @validproperties[name] end def self.validattr?(name) name = symbolize(name) return true if name == :name @validattrs ||= {} unless @validattrs.include?(name) @validattrs[name] = !!(self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name)) end @validattrs[name] end # does the name reflect a valid property? def self.validproperty?(name) name = symbolize(name) @validproperties.include?(name) && @validproperties[name] end # Return the list of validproperties def self.validproperties return {} unless defined?(@parameters) @validproperties.keys end # does the name reflect a valid parameter? def self.validparameter?(name) raise Puppet::DevError, "Class #{self} has not defined parameters" unless defined?(@parameters) !!(@paramhash.include?(name) or @@metaparamhash.include?(name)) end # This is a forward-compatibility method - it's the validity interface we'll use in Puppet::Resource. def self.valid_parameter?(name) validattr?(name) end # Return either the attribute alias or the attribute. def attr_alias(name) name = symbolize(name) if synonym = self.class.attr_alias(name) return synonym else return name end end # Are we deleting this resource? def deleting? obj = @parameters[:ensure] and obj.should == :absent end # Create a new property if it is valid but doesn't exist # Returns: true if a new parameter was added, false otherwise def add_property_parameter(prop_name) if self.class.validproperty?(prop_name) && !@parameters[prop_name] self.newattr(prop_name) return true end false end # # The name_var is the key_attribute in the case that there is only one. # def name_var key_attributes = self.class.key_attributes (key_attributes.length == 1) && key_attributes.first end # abstract accessing parameters and properties, 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(:property)' or 'object.should(:property)'. def [](name) name = attr_alias(name) fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name) if name == :name && nv = name_var name = nv end if obj = @parameters[name] # Note that if this is a property, then the value is the "should" value, # not the current value. obj.value else return nil end end # Abstract setting parameters and properties, and normalize # access to always be symbols, not strings. This sets the 'should' # value on properties, and otherwise just sets the appropriate parameter. def []=(name,value) name = attr_alias(name) fail("Invalid parameter #{name}") unless self.class.validattr?(name) if name == :name && nv = name_var name = nv end raise Puppet::Error.new("Got nil value for #{name}") if value.nil? property = self.newattr(name) if property begin # make sure the parameter doesn't have any errors property.value = value rescue => detail error = Puppet::Error.new("Parameter #{name} failed on #{ref}: #{detail}") error.set_backtrace(detail.backtrace) raise error end end nil end # remove a property from the object; useful in testing or in cleanup # when an error has been encountered def delete(attr) attr = symbolize(attr) if @parameters.has_key?(attr) @parameters.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end # iterate across the existing properties def eachproperty # properties is a private method properties.each { |property| yield property } end # Create a transaction event. Called by Transaction or by # a property. def event(options = {}) Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags}.merge(options)) end # retrieve the 'should' value for a specified property def should(name) name = attr_alias(name) (prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil end # Create the actual attribute instance. Requires either the attribute # name or class as the first argument, then an optional hash of # attributes to set during initialization. def newattr(name) if name.is_a?(Class) klass = name name = klass.name end unless klass = self.class.attrclass(name) raise Puppet::Error, "Resource type #{self.class.name} does not support parameter #{name}" end if provider and ! provider.class.supports_parameter?(klass) missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) } debug "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name] return nil end return @parameters[name] if @parameters.include?(name) @parameters[name] = klass.new(:resource => self) end # return the value of a parameter def parameter(name) @parameters[name.to_sym] end def parameters @parameters.dup end # Is the named property defined? def propertydefined?(name) name = name.intern unless name.is_a? Symbol @parameters.include?(name) end # Return an actual property instance by name; to return the value, use 'resource[param]' # LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method, # this one should probably go away at some point. def property(name) (obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property)) ? obj : nil end # For any parameters or properties that have defaults and have not yet been # set, set them now. This method can be handed a list of attributes, # and if so it will only set defaults for those attributes. def set_default(attr) return unless klass = self.class.attrclass(attr) return unless klass.method_defined?(:default) return if @parameters.include?(klass.name) return unless parameter = newattr(klass.name) if value = parameter.default and ! value.nil? parameter.value = value else @parameters.delete(parameter.name) end end # Convert our object to a hash. This just includes properties. def to_hash rethash = {} @parameters.each do |name, obj| rethash[name] = obj.value end rethash end def type self.class.name end # Return a specific value for an attribute. def value(name) name = attr_alias(name) (obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil end def version return 0 unless catalog catalog.version end # Return all of the property objects, in the order specified in the # class. def properties self.class.properties.collect { |prop| @parameters[prop.name] }.compact 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 def isomorphic? self.class.isomorphic? 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 @managed return @managed else @managed = false properties.each { |property| s = property.should if s and ! property.class.unmanaged @managed = true break end } return @managed end end ############################### # Code related to the container behaviour. def depthfirst? false end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. @parameters.each do |name, obj| obj.remove end @parameters.clear @parent = nil # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end ############################### # Code related to evaluating the resources. def ancestors [] end # Flush the provider, if it supports it. This is called by the # transaction. def flush self.provider.flush if self.provider and self.provider.respond_to?(:flush) 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?(is) insync = true if property = @parameters[:ensure] unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '#{property.name}'" end ensureis = is[property] if property.safe_insync?(ensureis) and property.should == :absent return true end end properties.each { |property| unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '#{property.name}'" end propis = is[property] unless property.safe_insync?(propis) property.debug("Not in sync: #{propis.inspect} vs #{property.should.inspect}") insync = false #else # property.debug("In sync") end } #self.debug("#{self} sync status is #{insync}") insync end # retrieve the current value of all contained properties def retrieve fail "Provider #{provider.class.name} is not functional on this host" if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable? result = Puppet::Resource.new(type, title) # Provide the name, so we know we'll always refer to a real thing result[:name] = self[:name] unless self[:name] == title if ensure_prop = property(:ensure) or (self.class.validattr?(:ensure) and ensure_prop = newattr(:ensure)) result[:ensure] = ensure_state = ensure_prop.retrieve else ensure_state = nil end properties.each do |property| next if property.name == :ensure if ensure_state == :absent result[property] = :absent else result[property] = property.retrieve end end result end def retrieve_resource resource = retrieve resource = Resource.new(type, title, :parameters => resource) if resource.is_a? Hash resource end # Get a hash of the current properties. Returns a hash with # the actual property instance as the key and the current value # as the, um, value. def currentpropvalues # It's important to use the 'properties' method here, as it follows the order # in which they're defined in the class. It also guarantees that 'ensure' # is the first property, which is important for skipping 'retrieve' on # all the properties if the resource is absent. ensure_state = false return properties.inject({}) do | prophash, property| if property.name == :ensure ensure_state = property.retrieve prophash[property] = ensure_state else if ensure_state == :absent prophash[property] = :absent else prophash[property] = property.retrieve end end prophash end end # Are we running in noop mode? def noop? # If we're not a host_config, we're almost certainly part of # Settings, and we want to ignore 'noop' return false if catalog and ! catalog.host_config? if defined?(@noop) @noop else Puppet[:noop] end end def noop noop? end ############################### # Code related to managing resource instances. require 'puppet/transportable' # retrieve a named instance of the current type def self.[](name) raise "Global resource access is deprecated" @objects[name] || @aliases[name] end # add an instance by name to the class list of instances def self.[]=(name,object) raise "Global resource storage is deprecated" newobj = nil if object.is_a?(Puppet::Type) newobj = object else raise Puppet::DevError, "must pass a Puppet::Type object" end if exobj = @objects[name] and self.isomorphic? msg = "Object '#{newobj.class.name}[#{name}]' already exists" msg += ("in file #{object.file} at line #{object.line}") if exobj.file and exobj.line msg += ("and cannot be redefined in file #{object.file} at line #{object.line}") if object.file and object.line error = Puppet::Error.new(msg) raise error else #Puppet.info("adding %s of type %s to class list" % # [name,object.class]) @objects[name] = newobj end end # 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) raise "Global resource aliasing is deprecated" if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( "Cannot create alias #{name}: object already exists" ) end end if @aliases.include?(name) unless @aliases[name] == obj raise Puppet::Error.new( "Object #{@aliases[name].name} already has alias #{name}" ) end end @aliases[name] = obj end # remove all of the instances of a single type def self.clear raise "Global resource removal is deprecated" if defined?(@objects) @objects.each do |name, obj| obj.remove(true) end @objects.clear end @aliases.clear if defined?(@aliases) end # Force users to call this, so that we can merge objects if # necessary. def self.create(args) # LAK:DEP Deprecation notice added 12/17/2008 Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new" new(args) end # remove a specified object def self.delete(resource) raise "Global resource removal is deprecated" return unless defined?(@objects) @objects.delete(resource.title) if @objects.include?(resource.title) @aliases.delete(resource.title) if @aliases.include?(resource.title) if @aliases.has_value?(resource) names = [] @aliases.each do |name, otherres| if otherres == resource names << name end end names.each { |name| @aliases.delete(name) } end end # iterate across each of the type's instances def self.each raise "Global resource iteration is deprecated" 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) raise "Global resource access is deprecated" @objects.has_key?(name) end # Retrieve all known instances. Either requires providers or must be overridden. def self.instances raise Puppet::DevError, "#{self.name} has no providers and has not overridden 'instances'" if provider_hash.empty? # Put the default provider first, then the rest of the suitable providers. provider_instances = {} providers_by_source.collect do |provider| all_properties = self.properties.find_all do |property| provider.supports_parameter?(property) end.collect do |property| property.name end provider.instances.collect do |instance| # We always want to use the "first" provider instance we find, unless the resource # is already managed and has a different provider set if other = provider_instances[instance.name] Puppet.warning "%s %s found in both %s and %s; skipping the %s version" % [self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name] next end provider_instances[instance.name] = instance result = new(:name => instance.name, :provider => instance) properties.each { |name| result.newattr(name) } result end end.flatten.compact end # Return a list of one suitable provider per source, with the default provider first. def self.providers_by_source # Put the default provider first (can be nil), then the rest of the suitable providers. sources = [] [defaultprovider, suitableprovider].flatten.uniq.collect do |provider| next if provider.nil? next if sources.include?(provider.source) sources << provider.source provider end.compact end # Convert a simple hash into a Resource instance. def self.hash2resource(hash) hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result } title = hash.delete(:title) title ||= hash[:name] title ||= hash[key_attributes.first] if key_attributes.length == 1 raise Puppet::Error, "Title or name must be provided" unless title # Now create our resource. resource = Puppet::Resource.new(self.name, title) [:catalog].each do |attribute| if value = hash[attribute] hash.delete(attribute) resource.send(attribute.to_s + "=", value) end end hash.each do |param, value| resource[param] = value end resource end # Create the path for logging and such. def pathbuilder if p = parent [p.pathbuilder, self.ref].flatten else [self.ref] end end ############################### # Add all of the meta parameters. newmetaparam(:noop) do desc "Boolean flag indicating whether work should actually be done." newvalues(:true, :false) munge do |value| case value when true, :true, "true"; @resource.noop = true when false, :false, "false"; @resource.noop = false 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." end newmetaparam(:audit) do desc "Marks a subset of this resource's unmanaged attributes for auditing. Accepts an attribute name, an array of attribute names, or `all`. Auditing a resource attribute has two effects: First, whenever a catalog is applied with puppet apply or puppet agent, Puppet will check whether that attribute of the resource has been modified, comparing its current value to the previous run; any change will be logged alongside any actions performed by Puppet while applying the catalog. Secondly, marking a resource attribute for auditing will include that attribute in inspection reports generated by puppet inspect; see the puppet inspect documentation for more details. Managed attributes for a resource can also be audited, but note that changes made by Puppet will be logged as additional modifications. (I.e. if a user manually edits a file whose contents are audited and managed, puppet agent's next two runs will both log an audit notice: the first run will log the user's edit and then revert the file to the desired state, and the second run will log the edit made by Puppet.)" validate do |list| list = Array(list).collect {|p| p.to_sym} unless list == [:all] list.each do |param| next if @resource.class.validattr?(param) fail "Cannot audit #{param}: not a valid attribute for #{resource}" end end end munge do |args| properties_to_audit(args).each do |param| next unless resource.class.validproperty?(param) resource.newattr(param) end end def all_properties resource.class.properties.find_all do |property| resource.provider.nil? or resource.provider.class.supports_parameter?(property) end.collect do |property| property.name end end def properties_to_audit(list) if !list.kind_of?(Array) && list.to_sym == :all list = all_properties else list = Array(list).collect { |p| p.to_sym } end end end newmetaparam(:check) do desc "Audit specified attributes of resources over time, and report if any have changed. This parameter has been deprecated in favor of 'audit'." munge do |args| resource.warning "'check' attribute is deprecated; use 'audit' instead" resource[:audit] = args 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::Util::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 title: 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 title, and the library sets that as an alias for the file so the dependency lookup in `Service['sshd']` works. You can use this metaparameter 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 Guide](http://docs.puppetlabs.com/guides/language_guide.html) for more information. " munge do |aliases| aliases = [aliases] unless aliases.is_a?(Array) raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog aliases.each do |other| if obj = @resource.catalog.resource(@resource.class.name, other) unless obj.object_id == @resource.object_id self.fail("#{@resource.title} can not create alias #{other}: object already exists") end next end # Newschool, add it to the catalog. @resource.catalog.alias(@resource, other) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated resource. While all resources are automatically tagged with as much information as possible (e.g., each class and definition containing the resource), it can be useful to add your own tags to a given resource. Multiple tags can be specified as an array: file {'/etc/hosts': ensure => file, source => 'puppet:///modules/site/hosts', mode => 0644, tag => ['bootstrap', 'minimumrun', 'mediumrun'], } Tags are useful for things like applying a subset of a host's configuration with [the `tags` setting](/references/latest/configuration.html#tags): puppet agent --test --tags bootstrap This way, you can easily isolate the portion of the configuration you're trying to test." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @resource.tag(tag) end end end class RelationshipMetaparam < Puppet::Parameter class << self attr_accessor :direction, :events, :callback, :subclasses end @subclasses = [] def self.inherited(sub) @subclasses << sub end def munge(references) references = [references] unless references.is_a?(Array) references.collect do |ref| if ref.is_a?(Puppet::Resource) ref else Puppet::Resource.new(ref) end end end def validate_relationship @value.each do |ref| unless @resource.catalog.resource(ref.to_s) description = self.class.direction == :in ? "dependency" : "dependent" fail "Could not find #{description} #{ref} for #{resource.ref}" end end end # Create edges from each of our relationships. :in # relationships are specified by the event-receivers, and :out # relationships are specified by the event generator. This # way 'source' and 'target' are consistent terms in both edges # and events -- that is, an event targets edges whose source matches # the event's source. The direction of the relationship determines # which resource is applied first and which resource is considered # to be the event generator. def to_edges @value.collect do |reference| reference.catalog = resource.catalog # Either of the two retrieval attempts could have returned # nil. unless related_resource = reference.resolve self.fail "Could not retrieve dependency '#{reference}' of #{@resource.ref}" end # Are we requiring them, or vice versa? See the method docs # for futher info on this. if self.class.direction == :in source = related_resource target = @resource else source = @resource target = related_resource end if method = self.class.callback subargs = { :event => self.class.events, :callback => method } self.debug("subscribes to #{related_resource.ref}") else # If there's no callback, there's no point in even adding # a label. subargs = nil self.debug("requires #{related_resource.ref}") end rel = Puppet::Relationship.new(source, target, subargs) end end end def self.relationship_params RelationshipMetaparam.subclasses end # Note that the order in which the relationships params is defined # matters. The labelled params (notify and subcribe) must be later, # so that if both params are used, those ones win. It's a hackish # solution, but it works. newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) do desc "References to 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\"] } Multiple dependencies can be specified by providing a comma-separated list of resources, enclosed in square brackets: require => [ File[\"/usr/local\"], 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 resources 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 resources 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. " end newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do desc "References to one or more objects that this object depends on. This metaparameter creates a dependency relationship like **require,** and also causes the dependent object to be refreshed when the subscribed object is changed. For instance: class nagios { file { 'nagconf': path => \"/etc/nagios/nagios.conf\" source => \"puppet://server/module/nagios.conf\", } service { 'nagios': ensure => running, subscribe => File['nagconf'] } } Currently the `exec`, `mount` and `service` types support refreshing. " end newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do desc %{References to one or more objects that depend on this object. 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.} end newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do desc %{References to one or more objects that depend on this object. This parameter is the opposite of **subscribe** --- it creates a dependency relationship like **before,** and also causes the dependent object(s) to be refreshed when this object is changed. For instance: file { "/etc/sshd_config": source => "....", notify => Service['sshd'] } service { 'sshd': ensure => running } This will restart the sshd service if the sshd config file changes.} end newmetaparam(:stage) do desc %{Which run stage a given resource should reside in. This just creates a dependency on or from the named milestone. For instance, saying that this is in the 'bootstrap' stage creates a dependency on the 'bootstrap' milestone. By default, all classes get directly added to the 'main' stage. You can create new stages as resources: stage { ['pre', 'post']: } To order stages, use standard relationships: stage { 'pre': before => Stage['main'] } Or use the new relationship syntax: Stage['pre'] -> Stage['main'] -> Stage['post'] Then use the new class parameters to specify a stage: class { 'foo': stage => 'pre' } Stages can only be set on classes, not individual resources. This will fail: file { '/foo': stage => 'pre', ensure => file } } end ############################### # All of the provider plumbing for the resource types. require 'puppet/provider' require 'puppet/util/provider_features' # Add the feature handling module. extend Puppet::Util::ProviderFeatures attr_reader :provider # the Type class attribute accessors class << self attr_accessor :providerloader attr_writer :defaultprovider end # Find the default provider. def self.defaultprovider return @defaultprovider if @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.specificity }.max defaults = defaults.find_all { |provider| provider.specificity == max } if defaults.length > 1 Puppet.warning( "Found multiple default providers for #{self.name}: #{defaults.collect { |i| i.name.to_s }.join(", ")}; using #{defaults[0].name}" ) end @defaultprovider = defaults.shift unless defaults.empty? end def self.provider_hash_by_type(type) @provider_hashes ||= {} @provider_hashes[type] ||= {} end def self.provider_hash Puppet::Type.provider_hash_by_type(self.name) 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. @providerloader.load(name) unless provider_hash.has_key?(name) provider_hash[name] end # Just list all of the providers. def self.providers provider_hash.keys end def self.validprovider?(name) name = Puppet::Util.symbolize(name) (provider_hash.has_key?(name) && provider_hash[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) if unprovide(name) Puppet.debug "Reloading #{name} #{self.name} provider" end parent = if pname = options[:parent] options.delete(:parent) if pname.is_a? Class pname else if provider = self.provider(pname) provider else raise Puppet::DevError, "Could not find parent provider #{pname} of #{name}" end end else Puppet::Provider end options[:resource_type] ||= self self.providify provider = genclass( name, :parent => parent, :hash => provider_hash, :prefix => "Provider", :block => block, :include => feature_module, :extend => feature_module, :attributes => options ) 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 newparam(:provider) do # We're using a hacky way to get the name of our type, since there doesn't # seem to be a correct way to introspect this at the time this code is run. # We expect that the class in which this code is executed will be something # like Puppet::Type::Ssh_authorized_key::ParameterProvider. desc <<-EOT The specific backend to use for this `#{self.to_s.split('::')[2].downcase}` resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. EOT # 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 # Since we're mixing @doc with text from other sources, we must normalize # its indentation with scrub. But we don't need to manually scrub the # provider's doc string, since markdown_definitionlist sanitizes its inputs. scrub(@doc) + "Available providers are:\n\n" + parenttype.providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| markdown_definitionlist( i, scrub(parenttype().provider(i).doc) ) }.join end defaultto { prov = @resource.class.defaultprovider prov.name if prov } validate do |provider_class| provider_class = provider_class[0] if provider_class.is_a? Array provider_class = provider_class.class.name if provider_class.is_a?(Puppet::Provider) unless provider = @resource.class.provider(provider_class) raise ArgumentError, "Invalid #{@resource.class.name} provider '#{provider_class}'" end end munge do |provider| provider = provider[0] if provider.is_a? Array provider = provider.intern if provider.is_a? String @resource.provider = provider if provider.is_a?(Puppet::Provider) provider.class.name else provider end end end.parenttype = self end def self.unprovide(name) if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end rmclass(name, :hash => provider_hash, :prefix => "Provider") end # Return an array of all of the suitable providers. def self.suitableprovider providerloader.loadall if provider_hash.empty? provider_hash.find_all { |name, provider| provider.suitable? }.collect { |name, provider| provider }.reject { |p| p.name == :fake } # For testing end def suitable? # If we don't use providers, then we consider it suitable. return true unless self.class.paramclass(:provider) # We have a provider and it is suitable. return true if provider && provider.class.suitable? # We're using the default provider and there is one. if !provider and self.class.defaultprovider self.provider = self.class.defaultprovider.name return true end # We specified an unsuitable provider, or there isn't any suitable # provider. false end def provider=(name) if name.is_a?(Puppet::Provider) @provider = name @provider.resource = self elsif klass = self.class.provider(name) @provider = klass.new(self) else raise ArgumentError, "Could not find #{name} provider of #{self.class.name}" end end ############################### # All of the relationship code. # 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 # Figure out of there are any objects we can automatically add as # dependencies. def autorequire(rel_catalog = nil) rel_catalog ||= catalog raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog reqs = [] 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(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) list = [list] unless list.is_a?(Array) # Collect the current prereqs list.each { |dep| # Support them passing objects directly, to save some effort. unless dep.is_a? Puppet::Type # Skip autorequires that we aren't managing unless dep = rel_catalog.resource(type, dep) next end end reqs << Puppet::Relationship.new(dep, self) } } reqs end # Build the dependencies associated with an individual object. def builddepends # Handle the requires self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.to_edges end end.flatten.reject { |r| r.nil? } end # Define the initial list of tags. def tags=(list) tag(self.class.name) tag(*list) end # Types (which map to resources 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 properties. # 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_writer :title attr_writer :noop include Enumerable # class methods dealing with Type management public # the Type class attribute accessors class << self attr_reader :name attr_accessor :self_refresh include Enumerable, Puppet::Util::ClassGen include Puppet::MetaType::Manager include Puppet::Util include Puppet::Util::Logging 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 @defaults = {} @parameters ||= [] @validproperties = {} @properties = [] @parameters = [] @paramhash = {} @attr_aliases = {} @paramdoc = Hash.new { |hash,key| key = key.intern if key.is_a?(String) if hash.include?(key) hash[key] else "Param Documentation for #{key} not found" end } @doc ||= "" 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 + # Origin information. + attr_accessor :file, :line + # The catalog that this resource is stored in. attr_accessor :catalog # is the resource exported attr_accessor :exported # is the resource virtual (it should not :-)) attr_accessor :virtual # create a log at specified level def log(msg) Puppet::Util::Log.create( :level => @parameters[:loglevel].value, :message => msg, :source => self ) end # instance methods related to instance intrinsics # e.g., initialize and name public attr_reader :original_parameters # initialize the type instance def initialize(resource) raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject) resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource) # The list of parameter/property instances. @parameters = {} # Set the title first, so any failures print correctly. if resource.type.to_s.downcase.to_sym == self.class.name self.title = resource.title else # This should only ever happen for components self.title = resource.ref end [:file, :line, :catalog, :exported, :virtual].each do |getter| setter = getter.to_s + "=" if val = resource.send(getter) self.send(setter, val) end end @tags = resource.tags @original_parameters = resource.to_hash set_name(@original_parameters) set_default(:provider) set_parameters(@original_parameters) self.validate if self.respond_to?(:validate) end private # Set our resource's name. def set_name(hash) self[name_var] = hash.delete(name_var) if name_var end # Set all of the parameters from a hash, in the appropriate order. def set_parameters(hash) # Use the order provided by allattrs, but add in any # extra attributes from the resource so we get failures # on invalid attributes. no_values = [] (self.class.allattrs + hash.keys).uniq.each do |attr| begin # Set any defaults immediately. This is mostly done so # that the default provider is available for any other # property validation. if hash.has_key?(attr) self[attr] = hash[attr] else no_values << attr end rescue ArgumentError, Puppet::Error, TypeError raise rescue => detail error = Puppet::DevError.new( "Could not set #{attr} on #{self.class.name}: #{detail}") error.set_backtrace(detail.backtrace) raise error end end no_values.each do |attr| set_default(attr) end end public # Set up all of our autorequires. def finish # Make sure all of our relationships are valid. Again, must be done # when the entire catalog is instantiated. self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.validate_relationship end end.flatten.reject { |r| r.nil? } end # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name self[:name] end # Look up our parent in the catalog, if we have one. def parent return nil unless catalog unless defined?(@parent) if parents = catalog.adjacent(self, :direction => :in) # We should never have more than one parent, so let's just ignore # it if we happen to. @parent = parents.shift else @parent = nil end end @parent end # Return the "type[name]" style reference. def ref # memoizing this is worthwhile ~ 3 percent of calls are the "first time # around" in an average run of Puppet. --daniel 2012-07-17 @ref ||= "#{self.class.name.to_s.capitalize}[#{self.title}]" end def self_refresh? self.class.self_refresh end # Mark that we're purging. def purging @purging = true end # Is this resource being purged? Used by transactions to forbid # deletion when there are dependencies. def purging? if defined?(@purging) @purging else false end end # Retrieve the title of an object. If no title was set separately, # then use the object's name. def title unless @title if self.class.validparameter?(name_var) @title = self[:name] elsif self.class.validproperty?(name_var) @title = self.should(name_var) else self.devfail "Could not find namevar #{name_var} for #{self.class.name}" end end @title end # convert to a string def to_s self.ref end # Convert to a transportable object def to_trans(ret = true) trans = TransObject.new(self.title, self.class.name) values = retrieve_resource values.each do |name, value| name = name.name if name.respond_to? :name trans[name] = value end @parameters.each do |name, param| # Avoid adding each instance name twice next if param.class.isnamevar? and param.value == self.title # We've already got property values next if param.is_a?(Puppet::Property) trans[name] = param.value end trans.tags = self.tags # FIXME I'm currently ignoring 'parent' and 'path' trans end def to_resource # this 'type instance' versus 'resource' distinction seems artificial # I'd like to see it collapsed someday ~JW self.to_trans.to_resource end def virtual?; !!@virtual; end def exported?; !!@exported; end def appliable_to_device? self.class.can_apply_to(:device) end def appliable_to_host? self.class.can_apply_to(:host) end end end require 'puppet/provider' # Always load these types. Puppet::Type.type(:component) diff --git a/spec/unit/file_collection/lookup_spec.rb b/spec/unit/file_collection/lookup_spec.rb deleted file mode 100755 index 2b0f8bfab..000000000 --- a/spec/unit/file_collection/lookup_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env rspec -require 'spec_helper' -require 'puppet/file_collection/lookup' - -class LookupTester - include Puppet::FileCollection::Lookup -end - -describe Puppet::FileCollection::Lookup do - before do - @tester = LookupTester.new - - @file_collection = mock 'file_collection' - Puppet::FileCollection.stubs(:collection).returns @file_collection - end - - it "should use the file collection to determine the index of the file name" do - @file_collection.expects(:index).with("/my/file").returns 50 - - @tester.file = "/my/file" - @tester.file_index.should == 50 - end - - it "should return nil as the file name if no index is set" do - @tester.file.should be_nil - end - - it "should use the file collection to convert the index to a file name" do - @file_collection.expects(:path).with(25).returns "/path/to/file" - - @tester.file_index = 25 - - @tester.file.should == "/path/to/file" - end - - it "should support a line attribute" do - @tester.line = 50 - @tester.line.should == 50 - end - - it "should default to the global file collection" do - Puppet::FileCollection.expects(:collection).returns "collection" - @tester.file_collection.should == "collection" - end -end diff --git a/spec/unit/file_collection_spec.rb b/spec/unit/file_collection_spec.rb deleted file mode 100755 index 518763629..000000000 --- a/spec/unit/file_collection_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env rspec -require 'spec_helper' - -require 'puppet/file_collection' - -describe Puppet::FileCollection do - before do - @collection = Puppet::FileCollection.new - end - - it "should be able to convert a file name into an index" do - @collection.index("/my/file").should be_instance_of(Fixnum) - end - - it "should be able to convert an index into a file name" do - index = @collection.index("/path/to/file") - @collection.path(index).should == "/path/to/file" - end - - it "should always give the same file name for a given index" do - index = @collection.index("/path/to/file") - @collection.path(index).should == @collection.path(index) - end - - it "should always give the same index for a given file name" do - @collection.index("/my/file").should == @collection.index("/my/file") - end - - it "should always correctly relate a file name and its index even when multiple files are in the collection" do - indexes = %w{a b c d e f}.inject({}) do |hash, letter| - hash[letter] = @collection.index("/path/to/file/#{letter}") - hash - end - - indexes.each do |letter, index| - @collection.index("/path/to/file/#{letter}").should == indexes[letter] - @collection.path(index).should == @collection.path(index) - end - end - - it "should return nil as the file name when an unknown index is provided" do - @collection.path(50).should be_nil - end - - it "should provide a global collection" do - Puppet::FileCollection.collection.should be_instance_of(Puppet::FileCollection) - end - - it "should reuse the global collection" do - Puppet::FileCollection.collection.should equal(Puppet::FileCollection.collection) - end -end diff --git a/spec/unit/parser/ast_spec.rb b/spec/unit/parser/ast_spec.rb index 4b2e1bba1..38336c457 100755 --- a/spec/unit/parser/ast_spec.rb +++ b/spec/unit/parser/ast_spec.rb @@ -1,110 +1,105 @@ #!/usr/bin/env ruby -S rspec require 'spec_helper' require 'puppet/parser/ast' describe Puppet::Parser::AST do - - it "should use the file lookup module" do - Puppet::Parser::AST.ancestors.should be_include(Puppet::FileCollection::Lookup) - end - it "should have a doc accessor" do ast = Puppet::Parser::AST.new({}) ast.should respond_to(:doc) end it "should have a use_docs accessor to indicate it wants documentation" do ast = Puppet::Parser::AST.new({}) ast.should respond_to(:use_docs) end [ Puppet::Parser::AST::Collection, Puppet::Parser::AST::Else, Puppet::Parser::AST::Function, Puppet::Parser::AST::IfStatement, Puppet::Parser::AST::Resource, Puppet::Parser::AST::ResourceDefaults, Puppet::Parser::AST::ResourceOverride, Puppet::Parser::AST::VarDef ].each do |k| it "#{k}.use_docs should return true" do ast = k.new({}) ast.use_docs.should be_true end end describe "when initializing" do it "should store the doc argument if passed" do ast = Puppet::Parser::AST.new(:doc => "documentation") ast.doc.should == "documentation" end end end describe 'AST Generic Child' do before :each do @value = stub 'value' class Evaluateable < Puppet::Parser::AST attr_accessor :value def safeevaluate(*options) return value end end @evaluateable = Evaluateable.new(:value => @value) @scope = stubs 'scope' end describe "when evaluate_match is called" do it "should evaluate itself" do @evaluateable.expects(:safeevaluate).with(@scope) @evaluateable.evaluate_match("value", @scope) end it "should match values by equality" do @value.expects(:==).with("value").returns(true) @evaluateable.evaluate_match("value", @scope) end it "should downcase the evaluated value if wanted" do @value.expects(:downcase).returns("value") @evaluateable.evaluate_match("value", @scope) end it "should convert values to number" do Puppet::Parser::Scope.expects(:number?).with(@value).returns(2) Puppet::Parser::Scope.expects(:number?).with("23").returns(23) @evaluateable.evaluate_match("23", @scope) end it "should compare 'numberized' values" do two = stub_everything 'two' one = stub_everything 'one' Puppet::Parser::Scope.stubs(:number?).with(@value).returns(one) Puppet::Parser::Scope.stubs(:number?).with("2").returns(two) one.expects(:==).with(two) @evaluateable.evaluate_match("2", @scope) end it "should match undef if value is an empty string" do @evaluateable.value = '' @evaluateable.evaluate_match(:undef, @scope).should be_true end it "should downcase the parameter value if wanted" do parameter = stub 'parameter' parameter.expects(:downcase).returns("value") @evaluateable.evaluate_match(parameter, @scope) end it "should match '' if value is undef" do @evaluateable.value = :undef @evaluateable.evaluate_match('', @scope).should be_true end end end diff --git a/spec/unit/parser/resource_spec.rb b/spec/unit/parser/resource_spec.rb index 90ed4dba4..101b9c6f4 100755 --- a/spec/unit/parser/resource_spec.rb +++ b/spec/unit/parser/resource_spec.rb @@ -1,659 +1,655 @@ #!/usr/bin/env rspec require 'spec_helper' # LAK: FIXME This is just new tests for resources; I have # not moved all tests over yet. describe Puppet::Parser::Resource do before do @node = Puppet::Node.new("yaynode") @known_resource_types = Puppet::Resource::TypeCollection.new("env") @compiler = Puppet::Parser::Compiler.new(@node) @compiler.environment.stubs(:known_resource_types).returns @known_resource_types @source = newclass "" @scope = @compiler.topscope end def mkresource(args = {}) args[:source] ||= @source args[:scope] ||= @scope params = args[:parameters] || {:one => "yay", :three => "rah"} if args[:parameters] == :none args.delete(:parameters) elsif not args[:parameters].is_a? Array args[:parameters] = paramify(args[:source], params) end Puppet::Parser::Resource.new("resource", "testing", args) end def param(name, value, source) Puppet::Parser::Resource::Param.new(:name => name, :value => value, :source => source) end def paramify(source, hash) hash.collect do |name, value| Puppet::Parser::Resource::Param.new( :name => name, :value => value, :source => source ) end end def newclass(name) @known_resource_types.add Puppet::Resource::Type.new(:hostclass, name) end def newdefine(name) @known_resource_types.add Puppet::Resource::Type.new(:definition, name) end def newnode(name) @known_resource_types.add Puppet::Resource::Type.new(:node, name) end - it "should use the file lookup module" do - Puppet::Parser::Resource.ancestors.should be_include(Puppet::FileCollection::Lookup) - end - it "should get its environment from its scope" do scope = stub 'scope', :source => stub("source"), :namespaces => nil scope.expects(:environment).returns("foo").at_least_once Puppet::Parser::Resource.new("file", "whatever", :scope => scope).environment.should == "foo" end it "should use the resource type collection helper module" do Puppet::Parser::Resource.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper) end it "should use the scope's environment as its environment" do @scope.expects(:environment).returns("myenv").at_least_once Puppet::Parser::Resource.new("file", "whatever", :scope => @scope).environment.should == "myenv" end it "should be isomorphic if it is builtin and models an isomorphic type" do Puppet::Type.type(:file).expects(:isomorphic?).returns(true) @resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true end it "should not be isomorphic if it is builtin and models a non-isomorphic type" do Puppet::Type.type(:file).expects(:isomorphic?).returns(false) @resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source).isomorphic?.should be_false end it "should be isomorphic if it is not builtin" do newdefine "whatever" @resource = Puppet::Parser::Resource.new("whatever", "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true end it "should have a array-indexing method for retrieving parameter values" do @resource = mkresource @resource[:one].should == "yay" end it "should use a Puppet::Resource for converting to a ral resource" do trans = mock 'resource', :to_ral => "yay" @resource = mkresource @resource.expects(:to_resource).returns trans @resource.to_ral.should == "yay" end it "should be able to use the indexing operator to access parameters" do resource = Puppet::Parser::Resource.new("resource", "testing", :source => "source", :scope => @scope) resource["foo"] = "bar" resource["foo"].should == "bar" end it "should return the title when asked for a parameter named 'title'" do Puppet::Parser::Resource.new("resource", "testing", :source => @source, :scope => @scope)[:title].should == "testing" end describe "when initializing" do before do @arguments = {:scope => @scope} end it "should fail unless #{name.to_s} is specified", :'fails_on_ruby_1.9.2' => true do lambda { Puppet::Parser::Resource.new('file', '/my/file') }.should raise_error(ArgumentError) end it "should set the reference correctly" do res = Puppet::Parser::Resource.new("resource", "testing", @arguments) res.ref.should == "Resource[testing]" end it "should be tagged with user tags" do tags = [ "tag1", "tag2" ] @arguments[:parameters] = [ param(:tag, tags , :source) ] res = Puppet::Parser::Resource.new("resource", "testing", @arguments) (res.tags & tags).should == tags end end describe "when evaluating" do before do @node = Puppet::Node.new "test-node" @compiler = Puppet::Parser::Compiler.new @node @catalog = Puppet::Resource::Catalog.new source = stub('source') source.stubs(:module_name) @scope = Puppet::Parser::Scope.new(:compiler => @compiler, :source => source) @catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => @scope)) end it "should evaluate the associated AST definition" do definition = newdefine "mydefine" res = Puppet::Parser::Resource.new("mydefine", "whatever", :scope => @scope, :source => @source, :catalog => @catalog) definition.expects(:evaluate_code).with(res) res.evaluate end it "should evaluate the associated AST class" do @class = newclass "myclass" res = Puppet::Parser::Resource.new("class", "myclass", :scope => @scope, :source => @source, :catalog => @catalog) @class.expects(:evaluate_code).with(res) res.evaluate end it "should evaluate the associated AST node" do nodedef = newnode("mynode") res = Puppet::Parser::Resource.new("node", "mynode", :scope => @scope, :source => @source, :catalog => @catalog) nodedef.expects(:evaluate_code).with(res) res.evaluate end it "should add an edge to any specified stage for class resources", :'fails_on_ruby_1.9.2' => true do @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", '') other_stage = Puppet::Parser::Resource.new(:stage, "other", :scope => @scope, :catalog => @catalog) @compiler.add_resource(@scope, other_stage) resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) resource[:stage] = 'other' @compiler.add_resource(@scope, resource) resource.evaluate @compiler.catalog.edge?(other_stage, resource).should be_true end it "should fail if an unknown stage is specified", :'fails_on_ruby_1.9.2' => true do @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", '') resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) resource[:stage] = 'other' lambda { resource.evaluate }.should raise_error(ArgumentError, /Could not find stage other specified by/) end it "should add edges from the class resources to the parent's stage if no stage is specified", :'fails_on_ruby_1.9.2' => true do main = @compiler.catalog.resource(:stage, :main) foo_stage = Puppet::Parser::Resource.new(:stage, :foo_stage, :scope => @scope, :catalog => @catalog) @compiler.add_resource(@scope, foo_stage) @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", '') resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) resource[:stage] = 'foo_stage' @compiler.add_resource(@scope, resource) resource.evaluate @compiler.catalog.should be_edge(foo_stage, resource) end it "should allow edges to propagate multiple levels down the scope hierarchy" do Puppet[:code] = <<-MANIFEST stage { before: before => Stage[main] } class alpha { include beta } class beta { include gamma } class gamma { } class { alpha: stage => before } MANIFEST catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone') # Stringify them to make for easier lookup edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]} edges.should include(["Stage[before]", "Class[Alpha]"]) edges.should include(["Stage[before]", "Class[Beta]"]) edges.should include(["Stage[before]", "Class[Gamma]"]) end it "should use the specified stage even if the parent scope specifies one" do Puppet[:code] = <<-MANIFEST stage { before: before => Stage[main], } stage { after: require => Stage[main], } class alpha { class { beta: stage => after } } class beta { } class { alpha: stage => before } MANIFEST catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone') edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]} edges.should include(["Stage[before]", "Class[Alpha]"]) edges.should include(["Stage[after]", "Class[Beta]"]) end it "should add edges from top-level class resources to the main stage if no stage is specified", :'fails_on_ruby_1.9.2' => true do main = @compiler.catalog.resource(:stage, :main) @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", '') resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) @compiler.add_resource(@scope, resource) resource.evaluate @compiler.catalog.should be_edge(main, resource) end end describe "when finishing" do before do @class = newclass "myclass" @nodedef = newnode("mynode") @resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source) end it "should do nothing if it has already been finished" do @resource.finish @resource.expects(:add_metaparams).never @resource.finish end it "should add all defaults available from the scope" do @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => param(:owner, "default", @resource.source)) @resource.finish @resource[:owner].should == "default" end it "should not replace existing parameters with defaults" do @resource.set_parameter :owner, "oldvalue" @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => :replaced) @resource.finish @resource[:owner].should == "oldvalue" end it "should add a copy of each default, rather than the actual default parameter instance" do newparam = param(:owner, "default", @resource.source) other = newparam.dup other.value = "other" newparam.expects(:dup).returns(other) @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => newparam) @resource.finish @resource[:owner].should == "other" end it "should be running in metaparam compatibility mode if running a version below 0.25" do catalog = stub 'catalog', :client_version => "0.24.8" @resource.stubs(:catalog).returns catalog @resource.should be_metaparam_compatibility_mode end it "should be running in metaparam compatibility mode if running no client version is available" do catalog = stub 'catalog', :client_version => nil @resource.stubs(:catalog).returns catalog @resource.should be_metaparam_compatibility_mode end it "should not be running in metaparam compatibility mode if running a version at or above 0.25" do catalog = stub 'catalog', :client_version => "0.25.0" @resource.stubs(:catalog).returns catalog @resource.should_not be_metaparam_compatibility_mode end it "should not copy relationship metaparams when not in metaparam compatibility mode" do @scope.setvar("require", "bar") @resource.stubs(:metaparam_compatibility_mode?).returns false @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].should be_nil end it "should copy relationship metaparams when in metaparam compatibility mode" do @scope.setvar("require", "bar") @resource.stubs(:metaparam_compatibility_mode?).returns true @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].should == "bar" end it "should stack relationship metaparams when in metaparam compatibility mode" do @resource.set_parameter("require", "foo") @scope.setvar("require", "bar") @resource.stubs(:metaparam_compatibility_mode?).returns true @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].should == ["foo", "bar"] end end describe "when being tagged" do before do @scope_resource = stub 'scope_resource', :tags => %w{srone srtwo} @scope.stubs(:resource).returns @scope_resource @resource = Puppet::Parser::Resource.new("file", "yay", :scope => @scope, :source => mock('source')) end it "should get tagged with the resource type" do @resource.tags.should be_include("file") end it "should get tagged with the title" do @resource.tags.should be_include("yay") end it "should get tagged with each name in the title if the title is a qualified class name" do resource = Puppet::Parser::Resource.new("file", "one::two", :scope => @scope, :source => mock('source')) resource.tags.should be_include("one") resource.tags.should be_include("two") end it "should get tagged with each name in the type if the type is a qualified class name" do resource = Puppet::Parser::Resource.new("one::two", "whatever", :scope => @scope, :source => mock('source')) resource.tags.should be_include("one") resource.tags.should be_include("two") end it "should not get tagged with non-alphanumeric titles" do resource = Puppet::Parser::Resource.new("file", "this is a test", :scope => @scope, :source => mock('source')) resource.tags.should_not be_include("this is a test") end it "should fail on tags containing '*' characters" do lambda { @resource.tag("bad*tag") }.should raise_error(Puppet::ParseError) end it "should fail on tags starting with '-' characters" do lambda { @resource.tag("-badtag") }.should raise_error(Puppet::ParseError) end it "should fail on tags containing ' ' characters" do lambda { @resource.tag("bad tag") }.should raise_error(Puppet::ParseError) end it "should allow alpha tags" do lambda { @resource.tag("good_tag") }.should_not raise_error(Puppet::ParseError) end end describe "when merging overrides" do before do @source = "source1" @resource = mkresource :source => @source @override = mkresource :source => @source end it "should fail when the override was not created by a parent class" do @override.source = "source2" @override.source.expects(:child_of?).with("source1").returns(false) lambda { @resource.merge(@override) }.should raise_error(Puppet::ParseError) end it "should succeed when the override was created in the current scope" do @resource.source = "source3" @override.source = @resource.source @override.source.expects(:child_of?).with("source3").never params = {:a => :b, :c => :d} @override.expects(:parameters).returns(params) @resource.expects(:override_parameter).with(:b) @resource.expects(:override_parameter).with(:d) @resource.merge(@override) end it "should succeed when a parent class created the override" do @resource.source = "source3" @override.source = "source4" @override.source.expects(:child_of?).with("source3").returns(true) params = {:a => :b, :c => :d} @override.expects(:parameters).returns(params) @resource.expects(:override_parameter).with(:b) @resource.expects(:override_parameter).with(:d) @resource.merge(@override) end it "should add new parameters when the parameter is not set" do @source.stubs(:child_of?).returns true @override.set_parameter(:testing, "value") @resource.merge(@override) @resource[:testing].should == "value" end it "should replace existing parameter values" do @source.stubs(:child_of?).returns true @resource.set_parameter(:testing, "old") @override.set_parameter(:testing, "value") @resource.merge(@override) @resource[:testing].should == "value" end it "should add values to the parameter when the override was created with the '+>' syntax" do @source.stubs(:child_of?).returns true param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "testing", :source => @resource.source) param.add = true @override.set_parameter(param) @resource.set_parameter(:testing, "other") @resource.merge(@override) @resource[:testing].should == %w{other testing} end it "should not merge parameter values when multiple resources are overriden with '+>' at once " do @resource_2 = mkresource :source => @source @resource. set_parameter(:testing, "old_val_1") @resource_2.set_parameter(:testing, "old_val_2") @source.stubs(:child_of?).returns true param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "new_val", :source => @resource.source) param.add = true @override.set_parameter(param) @resource. merge(@override) @resource_2.merge(@override) @resource [:testing].should == %w{old_val_1 new_val} @resource_2[:testing].should == %w{old_val_2 new_val} end it "should promote tag overrides to real tags" do @source.stubs(:child_of?).returns true param = Puppet::Parser::Resource::Param.new(:name => :tag, :value => "testing", :source => @resource.source) @override.set_parameter(param) @resource.merge(@override) @resource.tagged?("testing").should be_true end end it "should be able to be converted to a normal resource" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source @resource.should respond_to(:to_resource) end it "should use its resource converter to convert to a transportable resource" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source newresource = Puppet::Resource.new(:file, "/my") Puppet::Resource.expects(:new).returns(newresource) newresource.expects(:to_trans).returns "mytrans" @resource.to_trans.should == "mytrans" end it "should return nil if converted to a transportable resource and it is virtual" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source @resource.expects(:virtual?).returns true @resource.to_trans.should be_nil end describe "when being converted to a resource" do before do @parser_resource = mkresource :scope => @scope, :parameters => {:foo => "bar", :fee => "fum"} end it "should create an instance of Puppet::Resource" do @parser_resource.to_resource.should be_instance_of(Puppet::Resource) end it "should set the type correctly on the Puppet::Resource" do @parser_resource.to_resource.type.should == @parser_resource.type end it "should set the title correctly on the Puppet::Resource" do @parser_resource.to_resource.title.should == @parser_resource.title end it "should copy over all of the parameters" do result = @parser_resource.to_resource.to_hash # The name will be in here, also. result[:foo].should == "bar" result[:fee].should == "fum" end it "should copy over the tags" do @parser_resource.tag "foo" @parser_resource.tag "bar" @parser_resource.to_resource.tags.should == @parser_resource.tags end it "should copy over the line" do @parser_resource.line = 40 @parser_resource.to_resource.line.should == 40 end it "should copy over the file" do @parser_resource.file = "/my/file" @parser_resource.to_resource.file.should == "/my/file" end it "should copy over the 'exported' value" do @parser_resource.exported = true @parser_resource.to_resource.exported.should be_true end it "should copy over the 'virtual' value" do @parser_resource.virtual = true @parser_resource.to_resource.virtual.should be_true end it "should convert any parser resource references to Puppet::Resource instances" do ref = Puppet::Resource.new("file", "/my/file") @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ref} result = @parser_resource.to_resource result[:fee].should == Puppet::Resource.new(:file, "/my/file") end it "should convert any parser resource references to Puppet::Resource instances even if they are in an array" do ref = Puppet::Resource.new("file", "/my/file") @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ["a", ref]} result = @parser_resource.to_resource result[:fee].should == ["a", Puppet::Resource.new(:file, "/my/file")] end it "should convert any parser resource references to Puppet::Resource instances even if they are in an array of array, and even deeper" do ref1 = Puppet::Resource.new("file", "/my/file1") ref2 = Puppet::Resource.new("file", "/my/file2") @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ["a", [ref1,ref2]]} result = @parser_resource.to_resource result[:fee].should == ["a", Puppet::Resource.new(:file, "/my/file1"), Puppet::Resource.new(:file, "/my/file2")] end it "should fail if the same param is declared twice" do lambda do @parser_resource = mkresource :source => @source, :parameters => [ Puppet::Parser::Resource::Param.new( :name => :foo, :value => "bar", :source => @source ), Puppet::Parser::Resource::Param.new( :name => :foo, :value => "baz", :source => @source ) ] end.should raise_error(Puppet::ParseError) end end describe "when validating" do it "should check each parameter" do resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => stub("source") resource[:one] = :two resource[:three] = :four resource.expects(:validate_parameter).with(:one) resource.expects(:validate_parameter).with(:three) resource.send(:validate) end it "should raise a parse error when there's a failure" do resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => stub("source") resource[:one] = :two resource.expects(:validate_parameter).with(:one).raises ArgumentError lambda { resource.send(:validate) }.should raise_error(Puppet::ParseError) end end describe "when setting parameters" do before do @source = newclass "foobar" @resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => @source end it "should accept Param instances and add them to the parameter list" do param = Puppet::Parser::Resource::Param.new :name => "foo", :value => "bar", :source => @source @resource.set_parameter(param) @resource["foo"].should == "bar" end it "should fail when provided a parameter name but no value" do lambda { @resource.set_parameter("myparam") }.should raise_error(ArgumentError) end it "should allow parameters to be set to 'false'" do @resource.set_parameter("myparam", false) @resource["myparam"].should be_false end it "should use its source when provided a parameter name and value" do @resource.set_parameter("myparam", "myvalue") @resource["myparam"].should == "myvalue" end end # part of #629 -- the undef keyword. Make sure 'undef' params get skipped. it "should not include 'undef' parameters when converting itself to a hash" do resource = Puppet::Parser::Resource.new "file", "/tmp/testing", :source => mock("source"), :scope => mock("scope") resource[:owner] = :undef resource[:mode] = "755" resource.to_hash[:owner].should be_nil end end