diff --git a/lib/puppet/external/base64.rb b/lib/puppet/external/base64.rb deleted file mode 100755 index 57359dc18..000000000 --- a/lib/puppet/external/base64.rb +++ /dev/null @@ -1,19 +0,0 @@ -# a stupid hack class to get rid of all of the warnings but -# still make the encode/decode methods available - -# 1.8.2 has a Base64 class, but 1.8.1 just imports the methods directly -# into Object - -require 'base64' - -unless defined?(Base64) - class Base64 - def Base64.encode64(*args) - Object.method(:encode64).call(*args) - end - - def Base64.decode64(*args) - Object.method(:decode64).call(*args) - end - end -end diff --git a/lib/puppet/forge/repository.rb b/lib/puppet/forge/repository.rb index a7944df98..4b8b6a33a 100644 --- a/lib/puppet/forge/repository.rb +++ b/lib/puppet/forge/repository.rb @@ -1,130 +1,128 @@ require 'net/https' require 'zlib' require 'digest/sha1' require 'uri' require 'puppet/util/http_proxy' require 'puppet/forge/errors' class Puppet::Forge # = Repository # # This class is a file for accessing remote repositories with modules. class Repository include Puppet::Forge::Errors attr_reader :uri, :cache # List of Net::HTTP exceptions to catch NET_HTTP_EXCEPTIONS = [ EOFError, Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EINVAL, Errno::ETIMEDOUT, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError, Zlib::GzipFile::Error, ] # Instantiate a new repository instance rooted at the +url+. # The agent will report +consumer_version+ in the User-Agent to # the repository. def initialize(url, consumer_version) @uri = url.is_a?(::URI) ? url : ::URI.parse(url) @cache = Cache.new(self) @consumer_version = consumer_version end # Return a Net::HTTPResponse read for this +request_path+. def make_http_request(request_path) request = Net::HTTP::Get.new(URI.escape(request_path), { "User-Agent" => user_agent }) if ! @uri.user.nil? && ! @uri.password.nil? request.basic_auth(@uri.user, @uri.password) end return read_response(request) end # Return a Net::HTTPResponse read from this HTTPRequest +request+. # # @param request [Net::HTTPRequest] request to make # @return [Net::HTTPResponse] response from request # @raise [Puppet::Forge::Errors::CommunicationError] if there is a network # related error # @raise [Puppet::Forge::Errors::SSLVerifyError] if there is a problem # verifying the remote SSL certificate def read_response(request) http_object = get_http_object http_object.start do |http| http.request(request) end rescue *NET_HTTP_EXCEPTIONS => e raise CommunicationError.new(:uri => @uri.to_s, :original => e) rescue OpenSSL::SSL::SSLError => e if e.message =~ /certificate verify failed/ raise SSLVerifyError.new(:uri => @uri.to_s, :original => e) else raise e end end # Return a Net::HTTP::Proxy object constructed from the settings provided # accessing the repository. # # This method optionally configures SSL correctly if the URI scheme is # 'https', including setting up the root certificate store so remote server # SSL certificates can be validated. # # @return [Net::HTTP::Proxy] object constructed from repo settings def get_http_object proxy_class = Net::HTTP::Proxy(Puppet::Util::HttpProxy.http_proxy_host, Puppet::Util::HttpProxy.http_proxy_port) proxy = proxy_class.new(@uri.host, @uri.port) if @uri.scheme == 'https' cert_store = OpenSSL::X509::Store.new cert_store.set_default_paths proxy.use_ssl = true proxy.verify_mode = OpenSSL::SSL::VERIFY_PEER proxy.cert_store = cert_store end proxy end # Return the local file name containing the data downloaded from the # repository at +release+ (e.g. "myuser-mymodule"). def retrieve(release) return cache.retrieve(@uri + release) end # Return the URI string for this repository. def to_s return @uri.to_s end # Return the cache key for this repository, this a hashed string based on # the URI. def cache_key return @cache_key ||= [ @uri.to_s.gsub(/[^[:alnum:]]+/, '_').sub(/_$/, ''), Digest::SHA1.hexdigest(@uri.to_s) ].join('-') end def user_agent "#{@consumer_version} Puppet/#{Puppet.version} (#{Facter.value(:operatingsystem)} #{Facter.value(:operatingsystemrelease)}) #{ruby_version}" end private :user_agent def ruby_version - # the patchlevel is not available in ruby 1.8.5 - patch = defined?(RUBY_PATCHLEVEL) ? "-p#{RUBY_PATCHLEVEL}" : "" - "Ruby/#{RUBY_VERSION}#{patch} (#{RUBY_RELEASE_DATE}; #{RUBY_PLATFORM})" + "Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE}; #{RUBY_PLATFORM})" end private :ruby_version end end diff --git a/lib/puppet/interface/documentation.rb b/lib/puppet/interface/documentation.rb index 2fbd34400..ec312512e 100644 --- a/lib/puppet/interface/documentation.rb +++ b/lib/puppet/interface/documentation.rb @@ -1,352 +1,349 @@ class Puppet::Interface # @api private module DocGen # @api private def self.strip_whitespace(text) text.gsub!(/[ \t\f]+$/, '') # We need to identify an indent: the minimum number of whitespace # characters at the start of any line in the text. - # - # Using split rather than each_line is because the later only takes a - # block on Ruby 1.8.5 / Centos, and we support that. --daniel 2011-05-03 indent = text.split(/\n/).map {|x| x.index(/[^\s]/) }.compact.min if indent > 0 then text.gsub!(/^[ \t\f]{0,#{indent}}/, '') end return text end # The documentation attributes all have some common behaviours; previously # we open-coded them across the set of six things, but that seemed # wasteful - especially given that they were literally the same, and had # the same bug hidden in them. # # This feels a bit like overkill, but at least the common code is common # now. --daniel 2011-04-29 # @api private def attr_doc(name, &validate) # Now, which form of the setter do we want, validated or not? get_arg = "value.to_s" if validate define_method(:"_validate_#{name}", validate) get_arg = "_validate_#{name}(#{get_arg})" end # We use module_eval, which I don't like much, because we can't have an # argument to a block with a default value in Ruby 1.8, and I don't like # the side-effects (eg: no argument count validation) of using blocks # without as metheds. When we are 1.9 only (hah!) you can totally # replace this with some up-and-up define_method. --daniel 2011-04-29 module_eval(<<-EOT, __FILE__, __LINE__ + 1) def #{name}(value = nil) self.#{name} = value unless value.nil? @#{name} end def #{name}=(value) @#{name} = Puppet::Interface::DocGen.strip_whitespace(#{get_arg}) end EOT end end # This module can be mixed in to provide a minimal set of # documentation attributes. # @api public module TinyDocs extend Puppet::Interface::DocGen # @!method summary(summary) # Sets a summary of this object. # @api public # @dsl Faces attr_doc :summary do |value| value =~ /\n/ and raise ArgumentError, "Face summary should be a single line; put the long text in 'description' instead." value end # @!method description(description) # Sets the long description of this object. # @param description [String] The description of this object. # @api public # @dsl Faces attr_doc :description # @api private def build_synopsis(face, action = nil, arguments = nil) PrettyPrint.format do |s| s.text("puppet #{face}") s.text(" #{action}") unless action.nil? s.text(" ") options.each do |option| option = get_option(option) wrap = option.required? ? %w{ < > } : %w{ [ ] } s.group(0, *wrap) do option.optparse.each do |item| unless s.current_group.first? s.breakable s.text '|' s.breakable end s.text item end end s.breakable end display_global_options.sort.each do |option| wrap = %w{ [ ] } s.group(0, *wrap) do type = Puppet.settings.setting(option).default type ||= Puppet.settings.setting(option).type.to_s.upcase s.text "--#{option} #{type}" s.breakable end s.breakable end if arguments then s.text arguments.to_s end end end end # This module can be mixed in to provide a full set of documentation # attributes. It is intended to be used for {Puppet::Interface}. # @api public module FullDocs extend Puppet::Interface::DocGen include TinyDocs # @!method examples # @overload examples(text) # Sets examples. # @param text [String] Example text # @api public # @return [void] # @dsl Faces # @overload examples # Returns documentation of examples # @return [String] The examples # @api private attr_doc :examples # @!method notes(text) # @overload notes(text) # Sets optional notes. # @param text [String] The notes # @api public # @return [void] # @dsl Faces # @overload notes # Returns any optional notes # @return [String] The notes # @api private attr_doc :notes # @!method license(text) # @overload license(text) # Sets the license text # @param text [String] the license text # @api public # @return [void] # @dsl Faces # @overload license # Returns the license # @return [String] The license # @api private attr_doc :license attr_doc :short_description # @overload short_description(value) # Sets a short description for this object. # @param value [String, nil] A short description (about a paragraph) # of this component. If `value` is `nil` the short_description # will be set to the shorter of the first paragraph or the first # five lines of {description}. # @return [void] # @api public # @dsl Faces # @overload short_description # Get the short description for this object # @return [String, nil] The short description of this object. If none is # set it will be derived from {description}. Returns `nil` if # {description} is `nil`. # @api private def short_description(value = nil) self.short_description = value unless value.nil? if @short_description.nil? then return nil if @description.nil? lines = @description.split("\n") first_paragraph_break = lines.index('') || 5 grab = [5, first_paragraph_break].min @short_description = lines[0, grab].join("\n") @short_description += ' [...]' if (grab < lines.length and first_paragraph_break >= 5) end @short_description end # @overload author(value) # Adds an author to the documentation for this object. To set # multiple authors, call this once for each author. # @param value [String] the name of the author # @api public # @dsl Faces # @overload author # Returns a list of authors # @return [String, nil] The names of all authors separated by # newlines, or `nil` if no authors have been set. # @api private def author(value = nil) unless value.nil? then unless value.is_a? String raise ArgumentError, 'author must be a string; use multiple statements for multiple authors' end if value =~ /\n/ then raise ArgumentError, 'author should be a single line; use multiple statements for multiple authors' end @authors.push(Puppet::Interface::DocGen.strip_whitespace(value)) end @authors.empty? ? nil : @authors.join("\n") end # Returns a list of authors. See {author}. # @return [String] The list of authors, separated by newlines. # @api private def authors @authors end # @api private def author=(value) # I think it's a bug that this ends up being the exposed # version of `author` on ActionBuilder if Array(value).any? {|x| x =~ /\n/ } then raise ArgumentError, 'author should be a single line; use multiple statements' end @authors = Array(value).map{|x| Puppet::Interface::DocGen.strip_whitespace(x) } end alias :authors= :author= # Sets the copyright owner and year. This returns the copyright # string, so it can be called with no arguments retrieve that string # without side effects. # @param owner [String, Array] The copyright owner or an # array of owners # @param years [Integer, Range, Array>] # The copyright year or years. Years can be specified with integers, # a range of integers, or an array of integers and ranges of # integers. # @return [String] A string describing the copyright on this object. # @api public # @dsl Faces def copyright(owner = nil, years = nil) if years.nil? and not owner.nil? then raise ArgumentError, 'copyright takes the owners names, then the years covered' end self.copyright_owner = owner unless owner.nil? self.copyright_years = years unless years.nil? if self.copyright_years or self.copyright_owner then "Copyright #{self.copyright_years} by #{self.copyright_owner}" else "Unknown copyright owner and years." end end # Sets the copyright owner # @param value [String, Array] The copyright owner or # owners. # @return [String] Comma-separated list of copyright owners # @api private attr_accessor :copyright_owner def copyright_owner=(value) case value when String then @copyright_owner = value when Array then @copyright_owner = value.join(", ") else raise ArgumentError, "copyright owner must be a string or an array of strings" end @copyright_owner end # Sets the copyright year # @param value [Integer, Range, Array] The # copyright year or years. # @return [String] # @api private attr_accessor :copyright_years def copyright_years=(value) years = munge_copyright_year value years = (years.is_a?(Array) ? years : [years]). sort_by do |x| x.is_a?(Range) ? x.first : x end @copyright_years = years.map do |year| if year.is_a? Range then "#{year.first}-#{year.last}" else year end end.join(", ") end # @api private def munge_copyright_year(input) case input when Range then input when Integer then if input < 1970 then fault = "before 1970" elsif input > (future = Time.now.year + 2) then fault = "after #{future}" end if fault then raise ArgumentError, "copyright with a year #{fault} is very strange; did you accidentally add or subtract two years?" end input when String then input.strip.split(/,/).map do |part| part = part.strip if part =~ /^\d+$/ then part.to_i elsif found = part.split(/-/) then unless found.length == 2 and found.all? {|x| x.strip =~ /^\d+$/ } raise ArgumentError, "#{part.inspect} is not a good copyright year or range" end Range.new(found[0].to_i, found[1].to_i) else raise ArgumentError, "#{part.inspect} is not a good copyright year or range" end end when Array then result = [] input.each do |item| item = munge_copyright_year item if item.is_a? Array result.concat item else result << item end end result else raise ArgumentError, "#{input.inspect} is not a good copyright year, set, or range" end end end end diff --git a/lib/puppet/network/formats.rb b/lib/puppet/network/formats.rb index 5c005d718..45d75dc6c 100644 --- a/lib/puppet/network/formats.rb +++ b/lib/puppet/network/formats.rb @@ -1,186 +1,185 @@ require 'puppet/network/format_handler' Puppet::Network::FormatHandler.create_serialized_formats(:yaml) do def intern(klass, text) data = YAML.load(text, :safe => true, :deserialize_symbols => true) return data if data.is_a?(klass) klass.from_pson(data) end def intern_multiple(klass, text) YAML.load(text, :safe => true, :deserialize_symbols => true).collect do |data| if data.is_a?(klass) data else klass.from_pson(data) end end end def render(instance) instance.to_yaml end # Yaml monkey-patches Array, so this works. def render_multiple(instances) instances.to_yaml end - # Unlike core's yaml, ZAML should support 1.8.1 just fine def supported?(klass) true end end # This is a "special" format which is used for the moment only when sending facts # as REST GET parameters (see Puppet::Configurer::FactHandler). # This format combines a yaml serialization, then zlib compression and base64 encoding. Puppet::Network::FormatHandler.create_serialized_formats(:b64_zlib_yaml) do require 'base64' def use_zlib? Puppet.features.zlib? && Puppet[:zlib] end def requiring_zlib if use_zlib? yield else raise Puppet::Error, "the zlib library is not installed or is disabled." end end def intern(klass, text) requiring_zlib do Puppet::Network::FormatHandler.format(:yaml).intern(klass, decode(text)) end end def intern_multiple(klass, text) requiring_zlib do Puppet::Network::FormatHandler.format(:yaml).intern_multiple(klass, decode(text)) end end def render(instance) encode(instance.to_yaml) end def render_multiple(instances) encode(instances.to_yaml) end def supported?(klass) true end def decode(data) Zlib::Inflate.inflate(Base64.decode64(data)) end def encode(text) requiring_zlib do Base64.encode64(Zlib::Deflate.deflate(text, Zlib::BEST_COMPRESSION)) end end end Puppet::Network::FormatHandler.create(:s, :mime => "text/plain", :extension => "txt") # A very low-weight format so it'll never get chosen automatically. Puppet::Network::FormatHandler.create(:raw, :mime => "application/x-raw", :weight => 1) do def intern_multiple(klass, text) raise NotImplementedError end def render_multiple(instances) raise NotImplementedError end # LAK:NOTE The format system isn't currently flexible enough to handle # what I need to support raw formats just for individual instances (rather # than both individual and collections), but we don't yet have enough data # to make a "correct" design. # So, we hack it so it works for singular but fail if someone tries it # on plurals. def supported?(klass) true end end Puppet::Network::FormatHandler.create_serialized_formats(:pson, :weight => 10, :required_methods => [:render_method, :intern_method]) do def intern(klass, text) data_to_instance(klass, PSON.parse(text)) end def intern_multiple(klass, text) PSON.parse(text).collect do |data| data_to_instance(klass, data) end end # PSON monkey-patches Array, so this works. def render_multiple(instances) instances.to_pson end # If they pass class information, we want to ignore it. By default, # we'll include class information but we won't rely on it - we don't # want class names to be required because we then can't change our # internal class names, which is bad. def data_to_instance(klass, data) if data.is_a?(Hash) and d = data['data'] data = d end return data if data.is_a?(klass) klass.from_pson(data) end end # This is really only ever going to be used for Catalogs. Puppet::Network::FormatHandler.create_serialized_formats(:dot, :required_methods => [:render_method]) Puppet::Network::FormatHandler.create(:console, :mime => 'text/x-console-text', :weight => 0) do def json @json ||= Puppet::Network::FormatHandler.format(:pson) end def render(datum) # String to String return datum if datum.is_a? String return datum if datum.is_a? Numeric # Simple hash to table if datum.is_a? Hash and datum.keys.all? { |x| x.is_a? String or x.is_a? Numeric } output = '' column_a = datum.empty? ? 2 : datum.map{ |k,v| k.to_s.length }.max + 2 datum.sort_by { |k,v| k.to_s } .each do |key, value| output << key.to_s.ljust(column_a) output << json.render(value). chomp.gsub(/\n */) { |x| x + (' ' * column_a) } output << "\n" end return output end # Print one item per line for arrays if datum.is_a? Array output = '' datum.each do |item| output << item.to_s output << "\n" end return output end # ...or pretty-print the inspect outcome. return json.render(datum) end def render_multiple(data) data.collect(&:render).join("\n") end end diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 6bc6b2905..ea746a14f 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,639 +1,634 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'forwardable' require 'puppet/parser' require 'puppet/parser/templatewrapper' require 'puppet/resource/type_collection_helper' require 'puppet/util/methodhelper' # This class is part of the internal parser/evaluator/compiler functionality of Puppet. # It is passed between the various classes that participate in evaluation. # None of its methods are API except those that are clearly marked as such. # # @api public class Puppet::Parser::Scope extend Forwardable include Puppet::Util::MethodHelper include Puppet::Resource::TypeCollectionHelper require 'puppet/parser/resource' AST = Puppet::Parser::AST Puppet::Util.logmethods(self) include Enumerable include Puppet::Util::Errors attr_accessor :source, :resource attr_accessor :base, :keyword attr_accessor :top, :translated, :compiler attr_accessor :parent attr_reader :namespaces # Add some alias methods that forward to the compiler, since we reference # them frequently enough to justify the extra method call. def_delegators :compiler, :catalog, :environment # thin wrapper around an ephemeral # symbol table. # when a symbol class Ephemeral extend Forwardable def initialize(parent=nil, local=false) @symbols = {} @parent = parent @local_scope = local end def_delegators :@symbols, :delete, :[]=, :each def [](name) if @symbols.include?(name) or @parent.nil? @symbols[name] else @parent[name] end end def include?(name) bound?(name) or (@parent and @parent.include?(name)) end def bound?(name) @symbols.include?(name) end def is_local_scope? @local_scope end # @return [Ephemeral, Hash, nil] def parent @parent end def add_entries_to(target = {}) @parent.add_entries_to(target) unless @parent.nil? # do not return pure ephemeral ($0-$n) if is_local_scope? @symbols.each do |k, v| if v == :undef target.delete(k) else target[ k ] = v end end end target end end # Initialize a new scope suitable for parser function testing. This method # should be considered a public API for external modules. A shared spec # helper should consume this API method. # # @api protected # def self.new_for_test_harness(node_name) node = Puppet::Node.new(node_name) compiler = Puppet::Parser::Compiler.new(node) scope = new(compiler) scope.source = Puppet::Resource::Type.new(:node, node_name) scope.parent = compiler.topscope scope end def each to_hash.each { |name, value| yield(name, value) } end # Proxy accessors def host compiler.node.name end def facts compiler.node.facts end def include?(name) ! self[name].nil? end # Is the value true? This allows us to control the definition of truth # in one place. def self.true?(value) case value when '' false when :undef false else !!value end end # Coerce value to a number, or return `nil` if it isn't one. def self.number?(value) case value when Numeric value when /^-?\d+(:?\.\d+|(:?\.\d+)?e\d+)$/ value.to_f when /^0x[0-9a-f]+$/i value.to_i(16) when /^0[0-7]+$/ value.to_i(8) when /^-?\d+$/ value.to_i else nil end end # Add to our list of namespaces. def add_namespace(ns) return false if @namespaces.include?(ns) if @namespaces == [""] @namespaces = [ns] else @namespaces << ns end end def find_hostclass(name, options = {}) known_resource_types.find_hostclass(namespaces, name, options) end def find_definition(name) known_resource_types.find_definition(namespaces, name) end # This just delegates directly. def_delegator :compiler, :findresource # Initialize our new scope. Defaults to having no parent. def initialize(compiler, options = {}) if compiler.is_a? Puppet::Parser::Compiler self.compiler = compiler else raise Puppet::DevError, "you must pass a compiler instance to a new scope object" end if n = options.delete(:namespace) @namespaces = [n] else @namespaces = [""] end raise Puppet::DevError, "compiler passed in options" if options.include? :compiler set_options(options) extend_with_functions_module @tags = [] # The symbol table for this scope. This is where we store variables. @symtable = Ephemeral.new(nil, true) @ephemeral = [ Ephemeral.new(@symtable) ] # All of the defaults set for types. It's a hash of hashes, # with the first key being the type, then the second key being # the parameter. @defaults = Hash.new { |dhash,type| dhash[type] = {} } # The table for storing class singletons. This will only actually # be used by top scopes and node scopes. @class_scopes = {} end # Store the fact that we've evaluated a class, and store a reference to # the scope in which it was evaluated, so that we can look it up later. def class_set(name, scope) if parent parent.class_set(name, scope) else @class_scopes[name] = scope end end # Return the scope associated with a class. This is just here so # that subclasses can set their parent scopes to be the scope of # their parent class, and it's also used when looking up qualified # variables. def class_scope(klass) # They might pass in either the class or class name k = klass.respond_to?(:name) ? klass.name : klass @class_scopes[k] || (parent && parent.class_scope(k)) end # Collect all of the defaults set at any higher scopes. # This is a different type of lookup because it's additive -- # it collects all of the defaults, with defaults in closer scopes # overriding those in later scopes. def lookupdefaults(type) values = {} # first collect the values from the parents if parent parent.lookupdefaults(type).each { |var,value| values[var] = value } end # then override them with any current values # this should probably be done differently if @defaults.include?(type) @defaults[type].each { |var,value| values[var] = value } end values end # Look up a defined type. def lookuptype(name) find_definition(name) || find_hostclass(name) end def undef_as(x,v) if v.nil? or v == :undef x else v end end # Lookup a variable within this scope using the Puppet language's # scoping rules. Variables can be qualified using just as in a # manifest. # # @param [String] name the variable name to lookup # # @return Object the value of the variable, or nil if it's not found # # @api public def lookupvar(name, options = {}) unless name.is_a? String raise Puppet::ParseError, "Scope variable name #{name.inspect} is a #{name.class}, not a string" end table = @ephemeral.last if name =~ /^(.*)::(.+)$/ class_name = $1 variable_name = $2 lookup_qualified_variable(class_name, variable_name, options) elsif table.include?(name) table[name] else next_scope = inherited_scope || enclosing_scope if next_scope next_scope.lookupvar(name, options) else nil end end end # Retrieves the variable value assigned to the name given as an argument. The name must be a String, # and namespace can be qualified with '::'. The value is looked up in this scope, its parent scopes, # or in a specific visible named scope. # # @param varname [String] the name of the variable (may be a qualified name using `(ns'::')*varname` # @param options [Hash] Additional options, not part of api. # @return [Object] the value assigned to the given varname # @see #[]= # @api public # def [](varname, options={}) lookupvar(varname, options) end # The scope of the inherited thing of this scope's resource. This could # either be a node that was inherited or the class. # # @return [Puppet::Parser::Scope] The scope or nil if there is not an inherited scope def inherited_scope if has_inherited_class? qualified_scope(resource.resource_type.parent) else nil end end # The enclosing scope (topscope or nodescope) of this scope. # The enclosing scopes are produced when a class or define is included at # some point. The parent scope of the included class or define becomes the # scope in which it was included. The chain of parent scopes is followed # until a node scope or the topscope is found # # @return [Puppet::Parser::Scope] The scope or nil if there is no enclosing scope def enclosing_scope if has_enclosing_scope? if parent.is_topscope? or parent.is_nodescope? parent else parent.enclosing_scope end else nil end end def is_classscope? resource and resource.type == "Class" end def is_nodescope? resource and resource.type == "Node" end def is_topscope? compiler and self == compiler.topscope end def lookup_qualified_variable(class_name, variable_name, position) begin qualified_scope(class_name).lookupvar(variable_name, position) rescue RuntimeError => e location = if position[:lineproc] " at #{position[:lineproc].call}" elsif position[:file] && position[:line] " at #{position[:file]}:#{position[:line]}" else "" end warning "Could not look up qualified variable '#{class_name}::#{variable_name}'; #{e.message}#{location}" nil end end def has_inherited_class? is_classscope? and resource.resource_type.parent end private :has_inherited_class? def has_enclosing_scope? not parent.nil? end private :has_enclosing_scope? def qualified_scope(classname) raise "class #{classname} could not be found" unless klass = find_hostclass(classname) raise "class #{classname} has not been evaluated" unless kscope = class_scope(klass) kscope end private :qualified_scope # Returns a Hash containing all variables and their values, optionally (and # by default) including the values defined in parent. Local values # shadow parent values. Ephemeral scopes for match results ($0 - $n) are not included. # def to_hash(recursive = true) if recursive and parent target = parent.to_hash(recursive) else target = Hash.new end # add all local scopes @ephemeral.last.add_entries_to(target) target end def namespaces @namespaces.dup end # Create a new scope and set these options. def newscope(options = {}) compiler.newscope(self, options) end def parent_module_name return nil unless @parent return nil unless @parent.source @parent.source.module_name end # Set defaults for a type. The typename should already be downcased, # so that the syntax is isolated. We don't do any kind of type-checking # here; instead we let the resource do it when the defaults are used. def define_settings(type, params) table = @defaults[type] # if we got a single param, it'll be in its own array params = [params] unless params.is_a?(Array) params.each { |param| if table.include?(param.name) raise Puppet::ParseError.new("Default already defined for #{type} { #{param.name} }; cannot redefine", param.line, param.file) end table[param.name] = param } end # Set a variable in the current scope. This will override settings # in scopes above, but will not allow variables in the current scope # to be reassigned. # It's preferred that you use self[]= instead of this; only use this # when you need to set options. def setvar(name, value, options = {}) if name =~ /^[0-9]+$/ raise Puppet::ParseError.new("Cannot assign to a numeric match result variable '$#{name}'") unless options[:ephemeral] end unless name.is_a? String raise Puppet::ParseError, "Scope variable name #{name.inspect} is a #{name.class}, not a string" end table = effective_symtable options[:ephemeral] if table.bound?(name) if options[:append] error = Puppet::ParseError.new("Cannot append, variable #{name} is defined in this scope") else error = Puppet::ParseError.new("Cannot reassign variable #{name}") end error.file = options[:file] if options[:file] error.line = options[:line] if options[:line] raise error end if options[:append] table[name] = append_value(undef_as('', self[name]), value) else table[name] = value end table[name] end # Return the effective "table" for setting variables. # This method returns the first ephemeral "table" that acts as a local scope, or this # scope's symtable. If the parameter `use_ephemeral` is true, the "top most" ephemeral "table" # will be returned (irrespective of it being a match scope or a local scope). # # @param use_ephemeral [Boolean] whether the top most ephemeral (of any kind) should be used or not def effective_symtable use_ephemeral s = @ephemeral.last return s if use_ephemeral while s && !(s.is_a?(Hash) || s.is_local_scope?()) s = s.parent end s ? s : @symtable end # Sets the variable value of the name given as an argument to the given value. The value is # set in the current scope and may shadow a variable with the same name in a visible outer scope. # It is illegal to re-assign a variable in the same scope. It is illegal to set a variable in some other # scope/namespace than the scope passed to a method. # # @param varname [String] The variable name to which the value is assigned. Must not contain `::` # @param value [String] The value to assign to the given variable name. # @param options [Hash] Additional options, not part of api. # # @api public # def []=(varname, value, options = {}) setvar(varname, value, options = {}) end def append_value(bound_value, new_value) case new_value when Array bound_value + new_value when Hash bound_value.merge(new_value) else if bound_value.is_a?(Hash) raise ArgumentError, "Trying to append to a hash with something which is not a hash is unsupported" end bound_value + new_value end end private :append_value # Return the tags associated with this scope. def_delegator :resource, :tags # Used mainly for logging def to_s "Scope(#{@resource})" end # remove ephemeral scope up to level def unset_ephemeral_var(level=:all) if level == :all @ephemeral = [ Ephemeral.new(@symtable)] else - # If we ever drop 1.8.6 and lower, this should be replaced by a single - # pop-with-a-count - or if someone more ambitious wants to monkey-patch - # that feature into older rubies. --daniel 2012-07-16 - (@ephemeral.size - level).times do - @ephemeral.pop - end + @ephemeral.pop(@ephemeral.size - level) end end # check if name exists in one of the ephemeral scopes. def ephemeral_include?(name) @ephemeral.any? {|eph| eph.include?(name) } end # Checks whether the variable should be processed in the ephemeral scope or not. # All numerical variables are processed in ephemeral scope at all times, and all other # variables when the ephemeral scope is a local scope. # def ephemeral?(name) @ephemeral.last.is_local_scope? || name =~ /^\d+$/ end def ephemeral_level @ephemeral.size end def new_ephemeral(local_scope = false) @ephemeral.push(Ephemeral.new(@ephemeral.last, local_scope)) end def ephemeral_from(match, file = nil, line = nil) case match when Hash # Create local scope ephemeral and set all values from hash new_ephemeral true match.each {|k,v| setvar(k, v, :file => file, :line => line, :ephemeral => true) } else raise(ArgumentError,"Invalid regex match data. Got a #{match.class}") unless match.is_a?(MatchData) # Create a match ephemeral and set values from match data new_ephemeral false setvar("0", match[0], :file => file, :line => line, :ephemeral => true) match.captures.each_with_index do |m,i| setvar("#{i+1}", m, :file => file, :line => line, :ephemeral => true) end end end def find_resource_type(type) # It still works fine without the type == 'class' short-cut, but it is a lot slower. return nil if ["class", "node"].include? type.to_s.downcase find_builtin_resource_type(type) || find_defined_resource_type(type) end def find_builtin_resource_type(type) Puppet::Type.type(type.to_s.downcase.to_sym) end def find_defined_resource_type(type) environment.known_resource_types.find_definition(namespaces, type.to_s.downcase) end def method_missing(method, *args, &block) method.to_s =~ /^function_(.*)$/ name = $1 super unless name super unless Puppet::Parser::Functions.function(name) # In odd circumstances, this might not end up defined by the previous # method, so we might as well be certain. if respond_to? method send(method, *args) else raise Puppet::DevError, "Function #{name} not defined despite being loaded!" end end def resolve_type_and_titles(type, titles) raise ArgumentError, "titles must be an array" unless titles.is_a?(Array) case type.downcase when "class" # resolve the titles titles = titles.collect do |a_title| hostclass = find_hostclass(a_title) hostclass ? hostclass.name : a_title end when "node" # no-op else # resolve the type resource_type = find_resource_type(type) type = resource_type.name if resource_type end return [type, titles] end private def extend_with_functions_module extend Puppet::Parser::Functions.environment_module(Puppet::Node::Environment.root) extend Puppet::Parser::Functions.environment_module(environment) if environment != Puppet::Node::Environment.root end end diff --git a/lib/puppet/util/backups.rb b/lib/puppet/util/backups.rb index 2df0f6cb1..b1daf78fa 100644 --- a/lib/puppet/util/backups.rb +++ b/lib/puppet/util/backups.rb @@ -1,88 +1,86 @@ require 'find' require 'fileutils' module Puppet::Util::Backups # Deal with backups. def perform_backup(file = nil) # if they specifically don't want a backup, then just say # we're good return true unless self[:backup] # let the path be specified file ||= self[:path] return true unless FileTest.exists?(file) return(self.bucket ? perform_backup_with_bucket(file) : perform_backup_with_backuplocal(file, self[:backup])) end private def perform_backup_with_bucket(fileobj) file = (fileobj.class == String) ? fileobj : fileobj.name case File.lstat(file).ftype when "directory" # we don't need to backup directories when recurse is on return true if self[:recurse] info "Recursively backing up to filebucket" Find.find(self[:path]) { |f| backup_file_with_filebucket(f) if File.file?(f) } when "file"; backup_file_with_filebucket(file) when "link"; end true end def perform_backup_with_backuplocal(fileobj, backup) file = (fileobj.class == String) ? fileobj : fileobj.name newfile = file + backup remove_backup(newfile) begin bfile = file + backup - # Ruby 1.8.1 requires the 'preserve' addition, but - # later versions do not appear to require it. # N.B. cp_r works on both files and directories FileUtils.cp_r(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back #{file} up: #{detail.message}" end end def remove_backup(newfile) if self.class.name == :file and self[:links] != :follow method = :lstat else method = :stat end begin stat = File.send(method, newfile) rescue Errno::ENOENT return end if stat.ftype == "directory" raise Puppet::Error, "Will not remove directory backup #{newfile}; use a filebucket" end info "Removing old backup of type #{stat.ftype}" begin File.unlink(newfile) rescue => detail message = "Could not remove old backup: #{detail}" self.log_exception(detail, message) self.fail message end end def backup_file_with_filebucket(f) sum = self.bucket.backup(f) self.info "Filebucketed #{f} to #{self.bucket.name} with sum #{sum}" return sum end end diff --git a/lib/puppet/util/monkey_patches.rb b/lib/puppet/util/monkey_patches.rb index 7b1bac58f..f73ec98ea 100644 --- a/lib/puppet/util/monkey_patches.rb +++ b/lib/puppet/util/monkey_patches.rb @@ -1,401 +1,253 @@ -require 'puppet/util' module Puppet::Util::MonkeyPatches end begin Process.maxgroups = 1024 rescue Exception # Actually, I just want to ignore it, since various platforms - JRuby, # Windows, and so forth - don't support it, but only because it isn't a # meaningful or implementable concept there. end module RDoc def self.caller(skip=nil) in_gem_wrapper = false Kernel.caller.reject { |call| in_gem_wrapper ||= call =~ /#{Regexp.escape $0}:\d+:in `load'/ } end end require "yaml" require "puppet/util/zaml.rb" class Symbol def <=> (other) self.to_s <=> other.to_s end unless method_defined? '<=>' def intern self end unless method_defined? 'intern' end [Object, Exception, Integer, Struct, Date, Time, Range, Regexp, Hash, Array, Float, String, FalseClass, TrueClass, Symbol, NilClass, Class].each { |cls| cls.class_eval do def to_yaml(ignored=nil) ZAML.dump(self) end end } def YAML.dump(*args) ZAML.dump(*args) end # # Workaround for bug in MRI 1.8.7, see # http://redmine.ruby-lang.org/issues/show/2708 # for details # if RUBY_VERSION == '1.8.7' class NilClass def closed? true end end end class Object # ActiveSupport 2.3.x mixes in a dangerous method # that can cause rspec to fork bomb # and other strange things like that. def daemonize raise NotImplementedError, "Kernel.daemonize is too dangerous, please don't try to use it." end end -# Workaround for yaml_initialize, which isn't supported before Ruby -# 1.8.3. -if RUBY_VERSION == '1.8.1' || RUBY_VERSION == '1.8.2' - YAML.add_ruby_type( /^object/ ) { |tag, val| - type, obj_class = YAML.read_type_class( tag, Object ) - r = YAML.object_maker( obj_class, val ) - if r.respond_to? :yaml_initialize - r.instance_eval { instance_variables.each { |name| remove_instance_variable name } } - r.yaml_initialize(tag, val) - end - r - } -end - -class Fixnum - # Returns the int itself. This method is intended for compatibility to - # character constant in Ruby 1.9. 1.8.5 is missing it; add it. - def ord - self - end unless method_defined? 'ord' -end - -class Array - # Ruby < 1.8.7 doesn't have this method but we use it in tests - def combination(num) - return [] if num < 0 || num > size - return [[]] if num == 0 - return map{|e| [e] } if num == 1 - tmp = self.dup - self[0, size - (num - 1)].inject([]) do |ret, e| - tmp.shift - ret += tmp.combination(num - 1).map{|a| a.unshift(e) } - end - end unless method_defined? :combination - - alias :count :length unless method_defined? :count - - # Ruby 1.8.5 lacks `drop`, which we don't want to lose. - def drop(n) - n = n.to_int - raise ArgumentError, "attempt to drop negative size" if n < 0 - - slice(n, length - n) or [] - end unless method_defined? :drop -end - - class Symbol # So, it turns out that one of the biggest memory allocation hot-spots in # our code was using symbol-to-proc - because it allocated a new instance # every time it was called, rather than caching. # # Changing this means we can see XX memory reduction... if method_defined? :to_proc alias __original_to_proc to_proc def to_proc @my_proc ||= __original_to_proc end else def to_proc @my_proc ||= Proc.new {|*args| args.shift.__send__(self, *args) } end end # Defined in 1.9, absent in 1.8, and used for compatibility in various # places, typically in third party gems. def intern return self end unless method_defined? :intern end class String unless method_defined? :lines require 'puppet/util/monkey_patches/lines' include Puppet::Util::MonkeyPatches::Lines end end require 'fcntl' class IO unless method_defined? :lines require 'puppet/util/monkey_patches/lines' include Puppet::Util::MonkeyPatches::Lines end def self.binread(name, length = nil, offset = 0) File.open(name, 'rb') do |f| f.seek(offset) if offset > 0 f.read(length) end end unless singleton_methods.include?(:binread) def self.binwrite(name, string, offset = nil) # Determine if we should truncate or not. Since the truncate method on a # file handle isn't implemented on all platforms, safer to do this in what # looks like the libc / POSIX flag - which is usually pretty robust. # --daniel 2012-03-11 mode = Fcntl::O_CREAT | Fcntl::O_WRONLY | (offset.nil? ? Fcntl::O_TRUNC : 0) # We have to duplicate the mode because Ruby on Windows is a bit precious, # and doesn't actually carry over the mode. It won't work to just use # open, either, because that doesn't like our system modes and the default # open bits don't do what we need, which is awesome. --daniel 2012-03-30 IO.open(IO::sysopen(name, mode), mode) do |f| # ...seek to our desired offset, then write the bytes. Don't try to # seek past the start of the file, eh, because who knows what platform # would legitimately blow up if we did that. # # Double-check the positioning, too, since destroying data isn't my idea # of a good time. --daniel 2012-03-11 target = [0, offset.to_i].max unless (landed = f.sysseek(target, IO::SEEK_SET)) == target raise "unable to seek to target offset #{target} in #{name}: got to #{landed}" end f.syswrite(string) end end unless singleton_methods.include?(:binwrite) end class Range def intersection(other) raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range) return unless other === self.first || self === other.first start = [self.first, other.first].max if self.exclude_end? && self.last <= other.last start ... self.last elsif other.exclude_end? && self.last >= other.last start ... other.last else start .. [ self.last, other.last ].min end end unless method_defined? :intersection alias_method :&, :intersection unless method_defined? :& end -# Ruby 1.8.5 doesn't have tap -module Kernel - def tap - yield(self) - self - end unless method_defined?(:tap) -end - - ######################################################################## # The return type of `instance_variables` changes between Ruby 1.8 and 1.9 # releases; it used to return an array of strings in the form "@foo", but # now returns an array of symbols in the form :@foo. # # Nothing else in the stack cares which form you get - you can pass the # string or symbol to things like `instance_variable_set` and they will work # transparently. # # Having the same form in all releases of Puppet is a win, though, so we # pick a unification and enforce than on all releases. That way developers # who do set math on them (eg: for YAML rendering) don't have to handle the # distinction themselves. # # In the sane tradition, we bring older releases into conformance with newer # releases, so we return symbols rather than strings, to be more like the # future versions of Ruby are. # # We also carefully support reloading, by only wrapping when we don't # already have the original version of the method aliased away somewhere. if RUBY_VERSION[0,3] == '1.8' unless Object.respond_to?(:puppet_original_instance_variables) # Add our wrapper to the method. class Object alias :puppet_original_instance_variables :instance_variables def instance_variables puppet_original_instance_variables.map(&:to_sym) end end # The one place that Ruby 1.8 assumes something about the return format of # the `instance_variables` method is actually kind of odd, because it uses # eval to get at instance variables of another object. # # This takes the original code and applies replaces instance_eval with # instance_variable_get through it. All other bugs in the original (such # as equality depending on the instance variables having the same order # without any promise from the runtime) are preserved. --daniel 2012-03-11 require 'resolv' class Resolv::DNS::Resource def ==(other) # :nodoc: return self.class == other.class && self.instance_variables == other.instance_variables && self.instance_variables.collect {|name| self.instance_variable_get name} == other.instance_variables.collect {|name| other.instance_variable_get name} end end end end -# The mv method in Ruby 1.8.5 can't mv directories across devices -# File.rename causes "Invalid cross-device link", which is rescued, but in Ruby -# 1.8.5 it tries to recover with a copy and unlink, but the unlink causes the -# error "Is a directory". In newer Rubies remove_entry is used -# The implementation below is what's used in Ruby 1.8.7 and Ruby 1.9 -if RUBY_VERSION == '1.8.5' - require 'fileutils' - - module FileUtils - def mv(src, dest, options = {}) - fu_check_options options, OPT_TABLE['mv'] - fu_output_message "mv#{options[:force] ? ' -f' : ''} #{[src,dest].flatten.join ' '}" if options[:verbose] - return if options[:noop] - fu_each_src_dest(src, dest) do |s, d| - destent = Entry_.new(d, nil, true) - begin - if destent.exist? - if destent.directory? - raise Errno::EEXIST, dest - else - destent.remove_file if rename_cannot_overwrite_file? - end - end - begin - File.rename s, d - rescue Errno::EXDEV - copy_entry s, d, true - if options[:secure] - remove_entry_secure s, options[:force] - else - remove_entry s, options[:force] - end - end - rescue SystemCallError - raise unless options[:force] - end - end - end - module_function :mv - - alias move mv - module_function :move - end -end - -# Ruby 1.8.6 doesn't have it either -# From https://github.com/puppetlabs/hiera/pull/47/files: -# In ruby 1.8.5 Dir does not have mktmpdir defined, so this monkey patches -# Dir to include the 1.8.7 definition of that method if it isn't already defined. -# Method definition borrowed from ruby-1.8.7-p357/lib/ruby/1.8/tmpdir.rb -unless Dir.respond_to?(:mktmpdir) - def Dir.mktmpdir(prefix_suffix=nil, tmpdir=nil) - case prefix_suffix - when nil - prefix = "d" - suffix = "" - when String - prefix = prefix_suffix - suffix = "" - when Array - prefix = prefix_suffix[0] - suffix = prefix_suffix[1] - else - raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}" - end - tmpdir ||= Dir.tmpdir - t = Time.now.strftime("%Y%m%d") - n = nil - begin - path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}" - path << "-#{n}" if n - path << suffix - Dir.mkdir(path, 0700) - rescue Errno::EEXIST - n ||= 0 - n += 1 - retry - end - - if block_given? - begin - yield path - ensure - FileUtils.remove_entry_secure path - end - else - path - end - end -end - # (#19151) Reject all SSLv2 ciphers and handshakes require 'openssl' class OpenSSL::SSL::SSLContext if DEFAULT_PARAMS[:options] DEFAULT_PARAMS[:options] |= OpenSSL::SSL::OP_NO_SSLv2 else DEFAULT_PARAMS[:options] = OpenSSL::SSL::OP_NO_SSLv2 end DEFAULT_PARAMS[:ciphers] << ':!SSLv2' alias __original_initialize initialize private :__original_initialize def initialize(*args) __original_initialize(*args) params = { :options => DEFAULT_PARAMS[:options], :ciphers => DEFAULT_PARAMS[:ciphers], } set_params(params) end end require 'puppet/util/platform' if Puppet::Util::Platform.windows? require 'puppet/util/windows' require 'openssl' class OpenSSL::X509::Store alias __original_set_default_paths set_default_paths def set_default_paths # This can be removed once openssl integrates with windows # cert store, see http://rt.openssl.org/Ticket/Display.html?id=2158 Puppet::Util::Windows::RootCerts.instance.each do |x509| add_cert(x509) end __original_set_default_paths end end end diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb index 353dc5e7f..208094b7e 100755 --- a/spec/unit/provider_spec.rb +++ b/spec/unit/provider_spec.rb @@ -1,667 +1,656 @@ #! /usr/bin/env ruby require 'spec_helper' def existing_command Puppet.features.microsoft_windows? ? "cmd" : "echo" end describe Puppet::Provider do before :each do Puppet::Type.newtype(:test) do newparam(:name) { isnamevar } end end after :each do Puppet::Type.rmtype(:test) end let :type do Puppet::Type.type(:test) end let :provider do type.provide(:default) {} end subject { provider } describe "has command" do it "installs a method to run the command specified by the path" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") allow_creation_of(echo_command) provider = provider_of do has_command(:echo, "/bin/echo") end provider.echo("an argument") end it "installs a command that is run with a given environment" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") allow_creation_of(echo_command, { :EV => "value", :OTHER => "different" }) provider = provider_of do has_command(:echo, "/bin/echo") do environment :EV => "value", :OTHER => "different" end end provider.echo("an argument") end it "is required by default" do provider = provider_of do has_command(:does_not_exist, "/does/not/exist") end provider.should_not be_suitable end it "is required by default" do provider = provider_of do has_command(:does_exist, File.expand_path("/exists/somewhere")) end file_exists_and_is_executable(File.expand_path("/exists/somewhere")) provider.should be_suitable end it "can be specified as optional" do provider = provider_of do has_command(:does_not_exist, "/does/not/exist") do is_optional end end provider.should be_suitable end end describe "has required commands" do it "installs methods to run executables by path" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") ls_command = expect_command_executed(:ls, "/bin/ls") allow_creation_of(echo_command) allow_creation_of(ls_command) provider = provider_of do commands :echo => "/bin/echo", :ls => "/bin/ls" end provider.echo("an argument") provider.ls end it "allows the provider to be suitable if the executable is present" do provider = provider_of do commands :always_exists => File.expand_path("/this/command/exists") end file_exists_and_is_executable(File.expand_path("/this/command/exists")) provider.should be_suitable end it "does not allow the provider to be suitable if the executable is not present" do provider = provider_of do commands :does_not_exist => "/this/command/does/not/exist" end provider.should_not be_suitable end end describe "has optional commands" do it "installs methods to run executables" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") ls_command = expect_command_executed(:ls, "/bin/ls") allow_creation_of(echo_command) allow_creation_of(ls_command) provider = provider_of do optional_commands :echo => "/bin/echo", :ls => "/bin/ls" end provider.echo("an argument") provider.ls end it "allows the provider to be suitable even if the executable is not present" do provider = provider_of do optional_commands :does_not_exist => "/this/command/does/not/exist" end provider.should be_suitable end end it "makes command methods on demand (deprecated)" do Puppet::Util.expects(:which).with("/not/a/command").returns("/not/a/command") Puppet::Util::Execution.expects(:execute).with(["/not/a/command"], {}) provider = provider_of do @commands[:echo] = "/not/a/command" end provider.stubs(:which).with("/not/a/command").returns("/not/a/command") provider.make_command_methods(:echo) provider.echo end it "should have a specifity class method" do Puppet::Provider.should respond_to(:specificity) end it "should consider two defaults to be higher specificity than one default" do one = provider_of do defaultfor :osfamily => "solaris" end two = provider_of do defaultfor :osfamily => "solaris", :operatingsystemrelease => "5.10" end two.specificity.should > one.specificity end it "should be Comparable" do res = Puppet::Type.type(:notify).new(:name => "res") # Normally I wouldn't like the stubs, but the only way to name a class # otherwise is to assign it to a constant, and that hurts more here in # testing world. --daniel 2012-01-29 a = Class.new(Puppet::Provider).new(res) a.class.stubs(:name).returns "Puppet::Provider::Notify::A" b = Class.new(Puppet::Provider).new(res) b.class.stubs(:name).returns "Puppet::Provider::Notify::B" c = Class.new(Puppet::Provider).new(res) c.class.stubs(:name).returns "Puppet::Provider::Notify::C" [[a, b, c], [a, c, b], [b, a, c], [b, c, a], [c, a, b], [c, b, a]].each do |this| this.sort.should == [a, b, c] end a.should be < b a.should be < c b.should be > a b.should be < c c.should be > a c.should be > b [a, b, c].each {|x| a.should be <= x } [a, b, c].each {|x| c.should be >= x } b.should be_between(a, c) end context "when creating instances" do context "with a resource" do let :resource do type.new(:name => "fred") end subject { provider.new(resource) } it "should set the resource correctly" do subject.resource.must equal resource end it "should set the name from the resource" do subject.name.should == resource.name end end context "with a hash" do subject { provider.new(:name => "fred") } it "should set the name" do subject.name.should == "fred" end it "should not have a resource" do subject.resource.should be_nil end end context "with no arguments" do subject { provider.new } it "should raise an internal error if asked for the name" do expect { subject.name }.to raise_error Puppet::DevError end it "should not have a resource" do subject.resource.should be_nil end end end context "when confining" do it "should be suitable by default" do subject.should be_suitable end it "should not be default by default" do subject.should_not be_default end { { :true => true } => true, { :true => false } => false, { :false => false } => true, { :false => true } => false, { :operatingsystem => Facter.value(:operatingsystem) } => true, { :operatingsystem => :yayness } => false, { :nothing => :yayness } => false, { :exists => Puppet::Util.which(existing_command) } => true, { :exists => "/this/file/does/not/exist" } => false, { :true => true, :exists => Puppet::Util.which(existing_command) } => true, { :true => true, :exists => "/this/file/does/not/exist" } => false, { :operatingsystem => Facter.value(:operatingsystem), :exists => Puppet::Util.which(existing_command) } => true, { :operatingsystem => :yayness, :exists => Puppet::Util.which(existing_command) } => false, { :operatingsystem => Facter.value(:operatingsystem), :exists => "/this/file/does/not/exist" } => false, { :operatingsystem => :yayness, :exists => "/this/file/does/not/exist" } => false, }.each do |confines, result| it "should confine #{confines.inspect} to #{result}" do confines.each {|test, value| subject.confine test => value } subject.send(result ? :should : :should_not, be_suitable) end end it "should not override a confine even if a second has the same type" do subject.confine :true => false subject.should_not be_suitable subject.confine :true => true subject.should_not be_suitable end it "should not be suitable if any confine fails" do subject.confine :true => false subject.should_not be_suitable 10.times do subject.confine :true => true subject.should_not be_suitable end end end context "default providers" do let :os do Facter.value(:operatingsystem) end it { should respond_to :specificity } it "should find the default provider" do type.provide(:nondefault) {} subject.defaultfor :operatingsystem => os subject.name.should == type.defaultprovider.name end it "should consider any true value enough to be default" do alternate = type.provide(:alternate) {} subject.defaultfor :operatingsystem => [:one, :two, :three, os] subject.name.should == type.defaultprovider.name subject.should be_default alternate.should_not be_default end it "should not be default if the confine doesn't match" do subject.should_not be_default subject.defaultfor :operatingsystem => :one subject.should_not be_default end it "should consider two defaults to be higher specificity than one default" do one = type.provide(:one) do defaultfor :osfamily => "solaris" end two = type.provide(:two) do defaultfor :osfamily => "solaris", :operatingsystemrelease => "5.10" end two.specificity.should > one.specificity end it "should consider a subclass more specific than its parent class" do parent = type.provide(:parent) child = type.provide(:child, :parent => parent) child.specificity.should > parent.specificity end end context "provider commands" do it "should raise for unknown commands" do expect { subject.command(:something) }.to raise_error Puppet::DevError end it "should handle command inheritance" do parent = type.provide("parent") child = type.provide("child", :parent => parent.name) command = Puppet::Util.which('sh') || Puppet::Util.which('cmd.exe') parent.commands :sh => command FileTest.should be_exists parent.command(:sh) parent.command(:sh).should =~ /#{Regexp.escape(command)}$/ FileTest.should be_exists child.command(:sh) child.command(:sh).should =~ /#{Regexp.escape(command)}$/ end it "#1197: should find commands added in the same run" do subject.commands :testing => "puppet-bug-1197" subject.command(:testing).should be_nil subject.stubs(:which).with("puppet-bug-1197").returns("/puppet-bug-1197") subject.command(:testing).should == "/puppet-bug-1197" # Ideally, we would also test that `suitable?` returned the right thing # here, but it is impossible to get access to the methods that do that # without digging way down into the implementation. --daniel 2012-03-20 end context "with optional commands" do before :each do subject.optional_commands :cmd => "/no/such/binary/exists" end it { should be_suitable } it "should not be suitable if a mandatory command is also missing" do subject.commands :foo => "/no/such/binary/either" subject.should_not be_suitable end it "should define a wrapper for the command" do subject.should respond_to :cmd end it "should return nil if the command is requested" do subject.command(:cmd).should be_nil end it "should raise if the command is invoked" do expect { subject.cmd }.to raise_error Puppet::Error, /Command cmd is missing/ end end end context "execution" do before :each do Puppet.expects(:deprecation_warning).never end it "delegates instance execute to Puppet::Util::Execution" do Puppet::Util::Execution.expects(:execute).with("a_command", { :option => "value" }) provider.new.send(:execute, "a_command", { :option => "value" }) end it "delegates class execute to Puppet::Util::Execution" do Puppet::Util::Execution.expects(:execute).with("a_command", { :option => "value" }) provider.send(:execute, "a_command", { :option => "value" }) end it "delegates instance execpipe to Puppet::Util::Execution" do block = Proc.new { } Puppet::Util::Execution.expects(:execpipe).with("a_command", true, block) provider.new.send(:execpipe, "a_command", true, block) end it "delegates class execpipe to Puppet::Util::Execution" do block = Proc.new { } Puppet::Util::Execution.expects(:execpipe).with("a_command", true, block) provider.send(:execpipe, "a_command", true, block) end it "delegates instance execfail to Puppet::Util::Execution" do Puppet::Util::Execution.expects(:execfail).with("a_command", "an exception to raise") provider.new.send(:execfail, "a_command", "an exception to raise") end it "delegates class execfail to Puppet::Util::Execution" do Puppet::Util::Execution.expects(:execfail).with("a_command", "an exception to raise") provider.send(:execfail, "a_command", "an exception to raise") end end context "mk_resource_methods" do before :each do type.newproperty(:prop1) type.newproperty(:prop2) type.newparam(:param1) type.newparam(:param2) end fields = %w{prop1 prop2 param1 param2} - # This is needed for Ruby 1.8.5, which throws an exception that the - # default rescue doesn't catch if the method isn't present. Also, it has - # no convenient predicate for them, which equally hurts. - def has_method?(object, name) - begin - return true if object.instance_method(name) - rescue Exception - return false - end - end - fields.each do |name| it "should add getter methods for #{name}" do expect { subject.mk_resource_methods }. - to change { has_method?(subject, name) }. + to change { subject.method_defined?(name) }. from(false).to(true) end it "should add setter methods for #{name}" do method = name + '=' expect { subject.mk_resource_methods }. - to change { has_method?(subject, name) }. + to change { subject.method_defined?(name) }. from(false).to(true) end end context "with an instance" do subject { provider.mk_resource_methods; provider.new(nil) } fields.each do |name| context name do it "should default to :absent" do subject.send(name).should == :absent end it "should update when set" do expect { subject.send(name + '=', "hello") }. to change { subject.send(name) }. from(:absent).to("hello") end end end end end context "source" do it "should default to the provider name" do subject.source.should == :default end it "should default to the provider name for a child provider" do type.provide(:sub, :parent => subject.name).source.should == :sub end it "should override if requested" do provider = type.provide(:sub, :parent => subject.name, :source => subject.source) provider.source.should == subject.source end it "should override to anything you want" do expect { subject.source = :banana }.to change { subject.source }. from(:default).to(:banana) end end context "features" do before :each do type.feature :numeric, '', :methods => [:one, :two] type.feature :alpha, '', :methods => [:a, :b] type.feature :nomethods, '' end { :no => { :alpha => false, :numeric => false, :methods => [] }, :numeric => { :alpha => false, :numeric => true, :methods => [:one, :two] }, :alpha => { :alpha => true, :numeric => false, :methods => [:a, :b] }, :all => { :alpha => true, :numeric => true, :methods => [:a, :b, :one, :two] }, :alpha_and_partial => { :alpha => true, :numeric => false, :methods => [:a, :b, :one] }, :numeric_and_partial => { :alpha => false, :numeric => true, :methods => [:a, :one, :two] }, :all_partial => { :alpha => false, :numeric => false, :methods => [:a, :one] }, :other_and_none => { :alpha => false, :numeric => false, :methods => [:foo, :bar] }, :other_and_alpha => { :alpha => true, :numeric => false, :methods => [:foo, :bar, :a, :b] }, }.each do |name, setup| context "with #{name.to_s.gsub('_', ' ')} features" do let :provider do provider = type.provide(name) setup[:methods].map do |method| provider.send(:define_method, method) do true end end type.provider(name) end let :numeric? do setup[:numeric] ? :should : :should_not end let :alpha? do setup[:alpha] ? :should : :should_not end subject { provider } it { should respond_to :has_features } it { should respond_to :has_feature } context "provider class" do it { should respond_to :nomethods? } it { should_not be_nomethods } it { should respond_to :numeric? } it { subject.send(numeric?, be_numeric) } it { subject.send(numeric?, be_satisfies(:numeric)) } it { should respond_to :alpha? } it { subject.send(alpha?, be_alpha) } it { subject.send(alpha?, be_satisfies(:alpha)) } end context "provider instance" do subject { provider.new } it { should respond_to :numeric? } it { subject.send(numeric?, be_numeric) } it { subject.send(numeric?, be_satisfies(:numeric)) } it { should respond_to :alpha? } it { subject.send(alpha?, be_alpha) } it { subject.send(alpha?, be_satisfies(:alpha)) } end end end context "feature with no methods" do before :each do type.feature :undemanding, '' end it { should respond_to :undemanding? } context "when the feature is not declared" do it { should_not be_undemanding } it { should_not be_satisfies :undemanding } end context "when the feature is declared" do before :each do subject.has_feature :undemanding end it { should be_undemanding } it { should be_satisfies :undemanding } end end context "supports_parameter?" do before :each do type.newparam(:no_feature) type.newparam(:one_feature, :required_features => :alpha) type.newparam(:two_features, :required_features => [:alpha, :numeric]) end let :providers do { :zero => type.provide(:zero), :one => type.provide(:one) do has_features :alpha end, :two => type.provide(:two) do has_features :alpha, :numeric end } end { :zero => { :yes => [:no_feature], :no => [:one_feature, :two_features] }, :one => { :yes => [:no_feature, :one_feature], :no => [:two_features] }, :two => { :yes => [:no_feature, :one_feature, :two_features], :no => [] } }.each do |name, data| data[:yes].each do |param| it "should support #{param} with provider #{name}" do providers[name].should be_supports_parameter param end end data[:no].each do |param| it "should not support #{param} with provider #{name}" do providers[name].should_not be_supports_parameter param end end end end end def provider_of(options = {}, &block) type = Puppet::Type.newtype(:dummy) do provide(:dummy, options, &block) end type.provider(:dummy) end def expect_command_executed(name, path, *args) command = Puppet::Provider::Command.new(name, path, Puppet::Util, Puppet::Util::Execution) command.expects(:execute).with(*args) command end def allow_creation_of(command, environment = {}) Puppet::Provider::Command.stubs(:new).with(command.name, command.executable, Puppet::Util, Puppet::Util::Execution, { :failonfail => true, :combine => true, :custom_environment => environment }).returns(command) end def file_exists_and_is_executable(path) FileTest.expects(:file?).with(path).returns(true) FileTest.expects(:executable?).with(path).returns(true) end end