diff --git a/lib/puppet/metatype/manager.rb b/lib/puppet/metatype/manager.rb index 23c7f4639..c65b59d79 100644 --- a/lib/puppet/metatype/manager.rb +++ b/lib/puppet/metatype/manager.rb @@ -1,178 +1,178 @@ require 'puppet' require 'puppet/util/classgen' require 'puppet/node/environment' # This module defines methods dealing with Type management. # This module gets included into the Puppet::Type class, it's just split out here for clarity. # @api public # module Puppet::MetaType module Manager include Puppet::Util::ClassGen # An implementation specific method that removes all type instances during testing. # @note Only use this method for testing purposes. # @api private # def allclear @types.each { |name, type| type.clear } end # Iterates over all already loaded Type subclasses. # @yield [t] a block receiving each type # @yieldparam t [Puppet::Type] each defined type # @yieldreturn [Object] the last returned object is also returned from this method # @return [Object] the last returned value from the block. def eachtype @types.each do |name, type| # Only consider types that have names #if ! type.parameters.empty? or ! type.validproperties.empty? yield type #end end end # Loads all types. # @note Should only be used for purposes such as generating documentation as this is potentially a very # expensive operation. # @return [void] # def loadall typeloader.loadall end # Defines a new type or redefines an existing type with the given name. # A convenience method on the form `new` where name is the name of the type is also created. # (If this generated method happens to clash with an existing method, a warning is issued and the original # method is kept). # # @param name [String] the name of the type to create or redefine. # @param options [Hash] options passed on to {Puppet::Util::ClassGen#genclass} as the option `:attributes`. # @option options [Puppet::Type] # Puppet::Type. This option is not passed on as an attribute to genclass. # @yield [ ] a block evaluated in the context of the created class, thus allowing further detailing of # that class. # @return [Class] the created subclass # @see Puppet::Util::ClassGen.genclass # # @dsl type # @api public def newtype(name, options = {}, &block) # Handle backward compatibility unless options.is_a?(Hash) Puppet.warning "Puppet::Type.newtype(#{name}) now expects a hash as the second argument, not #{options.inspect}" end # First make sure we don't have a method sitting around name = name.intern newmethod = "new#{name}" # Used for method manipulation. selfobj = singleton_class @types ||= {} if @types.include?(name) if self.respond_to?(newmethod) # Remove the old newmethod selfobj.send(:remove_method,newmethod) end end options = symbolize_options(options) # Then create the class. klass = genclass( name, :parent => Puppet::Type, :overwrite => true, :hash => @types, :attributes => options, &block ) # Now define a "new" method for convenience. if self.respond_to? newmethod # Refuse to overwrite existing methods like 'newparam' or 'newtype'. Puppet.warning "'new#{name.to_s}' method already exists; skipping" else selfobj.send(:define_method, newmethod) do |*args| klass.new(*args) end end # If they've got all the necessary methods defined and they haven't # already added the property, then do so now. klass.ensurable if klass.ensurable? and ! klass.validproperty?(:ensure) # Now set up autoload any providers that might exist for this type. klass.providerloader = Puppet::Util::Autoload.new(klass, "puppet/provider/#{klass.name.to_s}") # We have to load everything so that we can figure out the default provider. klass.providerloader.loadall klass.providify unless klass.providers.empty? klass end # Removes an existing type. # @note Only use this for testing. # @api private def rmtype(name) # Then create the class. rmclass(name, :hash => @types) singleton_class.send(:remove_method, "new#{name}") if respond_to?("new#{name}") end # Returns a Type instance by name. # This will load the type if not already defined. # @param [String, Symbol] name of the wanted Type # @return [Puppet::Type, nil] the type or nil if the type was not defined and could not be loaded # def type(name) # Avoid loading if name obviously is not a type name if name.to_s.include?(':') return nil end @types ||= {} # We are overwhelmingly symbols here, which usually match, so it is worth # having this special-case to return quickly. Like, 25K symbols vs. 300 # strings in this method. --daniel 2012-07-17 return @types[name] if @types[name] # Try mangling the name, if it is a string. if name.is_a? String name = name.downcase.intern return @types[name] if @types[name] end # Try loading the type. if typeloader.load(name, Puppet.lookup(:current_environment)) Puppet.warning "Loaded puppet/type/#{name} but no class was created" unless @types.include? name end # ...and I guess that is that, eh. return @types[name] end # Creates a loader for Puppet types. # Defaults to an instance of {Puppet::Util::Autoload} if no other auto loader has been set. # @return [Puppet::Util::Autoload] the loader to use. # @api private def typeloader unless defined?(@typeloader) - @typeloader = Puppet::Util::Autoload.new(self, "puppet/type", :wrap => false) + @typeloader = Puppet::Util::Autoload.new(self, "puppet/type") end @typeloader end end end diff --git a/lib/puppet/parser/functions.rb b/lib/puppet/parser/functions.rb index be104d810..60d488f84 100644 --- a/lib/puppet/parser/functions.rb +++ b/lib/puppet/parser/functions.rb @@ -1,262 +1,260 @@ require 'puppet/util/autoload' require 'puppet/parser/scope' # A module for managing parser functions. Each specified function # is added to a central module that then gets included into the Scope # class. # # @api public module Puppet::Parser::Functions Environment = Puppet::Node::Environment class << self include Puppet::Util end # Reset the list of loaded functions. # # @api private def self.reset @modules = {} # Runs a newfunction to create a function for each of the log levels Puppet::Util::Log.levels.each do |level| newfunction(level, :environment => Puppet.lookup(:root_environment), :doc => "Log a message on the server at level #{level.to_s}.") do |vals| send(level, vals.join(" ")) end end end # Accessor for singleton autoloader # # @api private def self.autoloader - @autoloader ||= Puppet::Util::Autoload.new( - self, "puppet/parser/functions", :wrap => false - ) + @autoloader ||= Puppet::Util::Autoload.new(self, "puppet/parser/functions") end # Get the module that functions are mixed into corresponding to an # environment # # @api private def self.environment_module(env) @modules[env.name] ||= Module.new do @metadata = {} def self.all_function_info @metadata end def self.get_function_info(name) @metadata[name] end def self.add_function_info(name, info) @metadata[name] = info end end end # Create a new Puppet DSL function. # # **The {newfunction} method provides a public API.** # # This method is used both internally inside of Puppet to define parser # functions. For example, template() is defined in # {file:lib/puppet/parser/functions/template.rb template.rb} using the # {newfunction} method. Third party Puppet modules such as # [stdlib](https://forge.puppetlabs.com/puppetlabs/stdlib) use this method to # extend the behavior and functionality of Puppet. # # See also [Docs: Custom # Functions](http://docs.puppetlabs.com/guides/custom_functions.html) # # @example Define a new Puppet DSL Function # >> Puppet::Parser::Functions.newfunction(:double, :arity => 1, # :doc => "Doubles an object, typically a number or string.", # :type => :rvalue) {|i| i[0]*2 } # => {:arity=>1, :type=>:rvalue, # :name=>"function_double", # :doc=>"Doubles an object, typically a number or string."} # # @example Invoke the double function from irb as is done in RSpec examples: # >> require 'puppet_spec/scope' # >> scope = PuppetSpec::Scope.create_test_scope_for_node('example') # => Scope() # >> scope.function_double([2]) # => 4 # >> scope.function_double([4]) # => 8 # >> scope.function_double([]) # ArgumentError: double(): Wrong number of arguments given (0 for 1) # >> scope.function_double([4,8]) # ArgumentError: double(): Wrong number of arguments given (2 for 1) # >> scope.function_double(["hello"]) # => "hellohello" # # @param [Symbol] name the name of the function represented as a ruby Symbol. # The {newfunction} method will define a Ruby method based on this name on # the parser scope instance. # # @param [Proc] block the block provided to the {newfunction} method will be # executed when the Puppet DSL function is evaluated during catalog # compilation. The arguments to the function will be passed as an array to # the first argument of the block. The return value of the block will be # the return value of the Puppet DSL function for `:rvalue` functions. # # @option options [:rvalue, :statement] :type (:statement) the type of function. # Either `:rvalue` for functions that return a value, or `:statement` for # functions that do not return a value. # # @option options [String] :doc ('') the documentation for the function. # This string will be extracted by documentation generation tools. # # @option options [Integer] :arity (-1) the # [arity](http://en.wikipedia.org/wiki/Arity) of the function. When # specified as a positive integer the function is expected to receive # _exactly_ the specified number of arguments. When specified as a # negative number, the function is expected to receive _at least_ the # absolute value of the specified number of arguments incremented by one. # For example, a function with an arity of `-4` is expected to receive at # minimum 3 arguments. A function with the default arity of `-1` accepts # zero or more arguments. A function with an arity of 2 must be provided # with exactly two arguments, no more and no less. Added in Puppet 3.1.0. # # @option options [Puppet::Node::Environment] :environment (nil) can # explicitly pass the environment we wanted the function added to. Only used # to set logging functions in root environment # # @return [Hash] describing the function. # # @api public def self.newfunction(name, options = {}, &block) name = name.intern environment = options[:environment] || Puppet.lookup(:current_environment) Puppet.warning "Overwriting previous definition for function #{name}" if get_function(name, environment) arity = options[:arity] || -1 ftype = options[:type] || :statement unless ftype == :statement or ftype == :rvalue raise Puppet::DevError, "Invalid statement type #{ftype.inspect}" end # the block must be installed as a method because it may use "return", # which is not allowed from procs. real_fname = "real_function_#{name}" environment_module(environment).send(:define_method, real_fname, &block) fname = "function_#{name}" env_module = environment_module(environment) env_module.send(:define_method, fname) do |*args| Puppet::Util::Profiler.profile("Called #{name}", [:functions, name]) do if args[0].is_a? Array if arity >= 0 and args[0].size != arity raise ArgumentError, "#{name}(): Wrong number of arguments given (#{args[0].size} for #{arity})" elsif arity < 0 and args[0].size < (arity+1).abs raise ArgumentError, "#{name}(): Wrong number of arguments given (#{args[0].size} for minimum #{(arity+1).abs})" end self.send(real_fname, args[0]) else raise ArgumentError, "custom functions must be called with a single array that contains the arguments. For example, function_example([1]) instead of function_example(1)" end end end func = {:arity => arity, :type => ftype, :name => fname} func[:doc] = options[:doc] if options[:doc] env_module.add_function_info(name, func) func end # Determine if a function is defined # # @param [Symbol] name the function # @param [Puppet::Node::Environment] environment the environment to find the function in # # @return [Symbol, false] The name of the function if it's defined, # otherwise false. # # @api public def self.function(name, environment = Puppet.lookup(:current_environment)) name = name.intern func = nil unless func = get_function(name, environment) autoloader.load(name, environment) func = get_function(name, environment) end if func func[:name] else false end end def self.functiondocs(environment = Puppet.lookup(:current_environment)) autoloader.loadall ret = "" merged_functions(environment).sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, hash| ret << "#{name}\n#{"-" * name.to_s.length}\n" if hash[:doc] ret << Puppet::Util::Docs.scrub(hash[:doc]) else ret << "Undocumented.\n" end ret << "\n\n- *Type*: #{hash[:type]}\n\n" end ret end # Determine whether a given function returns a value. # # @param [Symbol] name the function # @param [Puppet::Node::Environment] environment The environment to find the function in # @return [Boolean] whether it is an rvalue function # # @api public def self.rvalue?(name, environment = Puppet.lookup(:current_environment)) func = get_function(name, environment) func ? func[:type] == :rvalue : false end # Return the number of arguments a function expects. # # @param [Symbol] name the function # @param [Puppet::Node::Environment] environment The environment to find the function in # @return [Integer] The arity of the function. See {newfunction} for # the meaning of negative values. # # @api public def self.arity(name, environment = Puppet.lookup(:current_environment)) func = get_function(name, environment) func ? func[:arity] : -1 end class << self private def merged_functions(environment) root = environment_module(Puppet.lookup(:root_environment)) env = environment_module(environment) root.all_function_info.merge(env.all_function_info) end def get_function(name, environment) environment_module(environment).get_function_info(name.intern) || environment_module(Puppet.lookup(:root_environment)).get_function_info(name.intern) end end end diff --git a/lib/puppet/pops/binder/bindings_loader.rb b/lib/puppet/pops/binder/bindings_loader.rb index c57bb3cb6..109e1c68f 100644 --- a/lib/puppet/pops/binder/bindings_loader.rb +++ b/lib/puppet/pops/binder/bindings_loader.rb @@ -1,88 +1,88 @@ require 'rgen/metamodel_builder' # The ClassLoader provides a Class instance given a class name or a meta-type. # If the class is not already loaded, it is loaded using the Puppet Autoloader. # This means it can load a class from a gem, or from puppet modules. # class Puppet::Pops::Binder::BindingsLoader @confdir = Puppet.settings[:confdir] # @autoloader = Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings", :wrap => false) # Returns a XXXXX given a fully qualified class name. # Lookup of class is never relative to the calling namespace. # @param name [String, Array, Array, Puppet::Pops::Types::PAnyType] A fully qualified # class name String (e.g. '::Foo::Bar', 'Foo::Bar'), a PAnyType, or a fully qualified name in Array form where each part # is either a String or a Symbol, e.g. `%w{Puppetx Puppetlabs SomeExtension}`. # @return [Class, nil] the looked up class or nil if no such class is loaded # @raise ArgumentError If the given argument has the wrong type # @api public # def self.provide(scope, name) case name when String provide_from_string(scope, name) when Array provide_from_name_path(scope, name.join('::'), name) else raise ArgumentError, "Cannot provide a bindings from a '#{name.class.name}'" end end # If loadable name exists relative to a a basedir or not. Returns the loadable path as a side effect. # @return [String, nil] a loadable path for the given name, or nil # def self.loadable?(basedir, name) # note, "lib" is added by the autoloader # paths_for_name(name).find {|p| Puppet::FileSystem.exist?(File.join(basedir, "lib/puppet/bindings", p)+'.rb') } end private def self.loader() unless Puppet.settings[:confdir] == @confdir @confdir = Puppet.settings[:confdir] == @confdir - @autoloader = Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings", :wrap => false) + @autoloader = Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings") end @autoloader end def self.provide_from_string(scope, name) name_path = name.split('::') # always from the root, so remove an empty first segment if name_path[0].empty? name_path = name_path[1..-1] end provide_from_name_path(scope, name, name_path) end def self.provide_from_name_path(scope, name, name_path) # If bindings is already loaded, try this first result = Puppet::Bindings.resolve(scope, name) unless result # Attempt to load it using the auto loader paths_for_name(name).find {|path| loader.load(path) } result = Puppet::Bindings.resolve(scope, name) end result end def self.paths_for_name(fq_name) [de_camel(fq_name), downcased_path(fq_name)] end def self.downcased_path(fq_name) fq_name.to_s.gsub(/::/, '/').downcase end def self.de_camel(fq_name) fq_name.to_s.gsub(/::/, '/'). gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). gsub(/([a-z\d])([A-Z])/,'\1_\2'). tr("-", "_"). downcase end end diff --git a/lib/puppet/pops/types/class_loader.rb b/lib/puppet/pops/types/class_loader.rb index 1011f4715..21ce5c4bd 100644 --- a/lib/puppet/pops/types/class_loader.rb +++ b/lib/puppet/pops/types/class_loader.rb @@ -1,129 +1,129 @@ require 'rgen/metamodel_builder' # The ClassLoader provides a Class instance given a class name or a meta-type. # If the class is not already loaded, it is loaded using the Puppet Autoloader. # This means it can load a class from a gem, or from puppet modules. # class Puppet::Pops::Types::ClassLoader - @autoloader = Puppet::Util::Autoload.new("ClassLoader", "", :wrap => false) + @autoloader = Puppet::Util::Autoload.new("ClassLoader", "") # Returns a Class given a fully qualified class name. # Lookup of class is never relative to the calling namespace. # @param name [String, Array, Array, Puppet::Pops::Types::PAnyType] A fully qualified # class name String (e.g. '::Foo::Bar', 'Foo::Bar'), a PAnyType, or a fully qualified name in Array form where each part # is either a String or a Symbol, e.g. `%w{Puppetx Puppetlabs SomeExtension}`. # @return [Class, nil] the looked up class or nil if no such class is loaded # @raise ArgumentError If the given argument has the wrong type # @api public # def self.provide(name) case name when String provide_from_string(name) when Array provide_from_name_path(name.join('::'), name) when Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PType provide_from_type(name) else raise ArgumentError, "Cannot provide a class from a '#{name.class.name}'" end end private def self.provide_from_type(type) case type when Puppet::Pops::Types::PRuntimeType raise ArgumentError.new("Only Runtime type 'ruby' is supported, got #{type.runtime}") unless type.runtime == :ruby provide_from_string(type.runtime_type_name) when Puppet::Pops::Types::PBooleanType # There is no other thing to load except this Enum meta type RGen::MetamodelBuilder::MMBase::Boolean when Puppet::Pops::Types::PType # TODO: PType should has a type argument (a PAnyType) so the Class' class could be returned # (but this only matters in special circumstances when meta programming has been used). Class when Puppet::Pops::Type::POptionalType # cannot make a distinction between optional and its type provide_from_type(type.optional_type) # Although not expected to be the first choice for getting a concrete class for these # types, these are of value if the calling logic just has a reference to type. # when Puppet::Pops::Types::PArrayType ; Array when Puppet::Pops::Types::PTupleType ; Array when Puppet::Pops::Types::PHashType ; Hash when Puppet::Pops::Types::PStructType ; Hash when Puppet::Pops::Types::PRegexpType ; Regexp when Puppet::Pops::Types::PIntegerType ; Integer when Puppet::Pops::Types::PStringType ; String when Puppet::Pops::Types::PPatternType ; String when Puppet::Pops::Types::PEnumType ; String when Puppet::Pops::Types::PFloatType ; Float when Puppet::Pops::Types::PNilType ; NilClass when Puppet::Pops::Types::PCallableType ; Proc else nil end end def self.provide_from_string(name) name_path = name.split('::') # always from the root, so remove an empty first segment if name_path[0].empty? name_path = name_path[1..-1] end provide_from_name_path(name, name_path) end def self.provide_from_name_path(name, name_path) # If class is already loaded, try this first result = find_class(name_path) unless result.is_a?(Class) # Attempt to load it using the auto loader loaded_path = nil if paths_for_name(name).find {|path| loaded_path = path; @autoloader.load(path) } result = find_class(name_path) unless result.is_a?(Class) raise RuntimeError, "Loading of #{name} using relative path: '#{loaded_path}' did not create expected class" end end end return nil unless result.is_a?(Class) result end def self.find_class(name_path) name_path.reduce(Object) do |ns, name| begin ns.const_get(name) rescue NameError return nil end end end def self.paths_for_name(fq_name) [de_camel(fq_name), downcased_path(fq_name)] end def self.downcased_path(fq_name) fq_name.to_s.gsub(/::/, '/').downcase end def self.de_camel(fq_name) fq_name.to_s.gsub(/::/, '/'). gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). gsub(/([a-z\d])([A-Z])/,'\1_\2'). tr("-", "_"). downcase end end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index c6edf4e44..0bec40b67 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,518 +1,495 @@ # A module to collect utility functions. require 'English' require 'puppet/error' require 'puppet/util/execution_stub' require 'uri' require 'pathname' require 'ostruct' require 'puppet/util/platform' require 'puppet/util/symbolic_file_mode' require 'puppet/file_system/uniquefile' require 'securerandom' module Puppet module Util require 'puppet/util/monkey_patches' require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX extend Puppet::Util::SymbolicFileMode def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end # Run some code with a specific environment. Resets the environment back to # what it was at the end of the code. def self.withenv(hash) saved = ENV.to_hash hash.each do |name, val| ENV[name.to_s] = val end yield ensure ENV.clear saved.each do |name, val| ENV[name] = val end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end - # Proxy a bunch of methods to another object. - def self.classproxy(klass, objmethod, *methods) - classobj = class << klass; self; end - methods.each do |method| - classobj.send(:define_method, method) do |*args| - obj = self.send(objmethod) - - obj.send(method, *args) - end - end - end - - # Proxy a bunch of methods to another object. - def self.proxy(klass, objmethod, *methods) - methods.each do |method| - klass.send(:define_method, method) do |*args| - obj = self.send(objmethod) - - obj.send(method, *args) - end - end - end - def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end module_function :benchmark # Resolve a path for an executable to the absolute path. This tries to behave # in the same manner as the unix `which` command and uses the `PATH` # environment variable. # # @api public # @param bin [String] the name of the executable to find. # @return [String] the absolute path to the found executable. def which(bin) if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| begin dest = File.expand_path(File.join(dir, bin)) rescue ArgumentError => e # if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get # an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error # was thrown. if e.to_s =~ /HOME/ and (ENV['HOME'].nil? || ENV['HOME'] == "") # if we get here they have a tilde in their PATH. We'll issue a single warning about this and then # ignore this path element and carry on with our lives. Puppet::Util::Warnings.warnonce("PATH contains a ~ character, and HOME is not set; ignoring PATH element '#{dir}'.") elsif e.to_s =~ /doesn't exist|can't find user/ # ...otherwise, we just skip the non-existent entry, and do nothing. Puppet::Util::Warnings.warnonce("Couldn't expand PATH containing a ~ character; ignoring PATH element '#{dir}'.") else raise end else if Puppet.features.microsoft_windows? && File.extname(dest).empty? exts = ENV['PATHEXT'] exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] exts.each do |ext| destext = File.expand_path(dest + ext) return destext if FileTest.file? destext and FileTest.executable? destext end end return dest if FileTest.file? dest and FileTest.executable? dest end end end nil end module_function :which # Determine in a platform-specific way whether a path is absolute. This # defaults to the local platform if none is specified. # # Escape once for the string literal, and once for the regex. slash = '[\\\\/]' label = '[^\\\\/]+' AbsolutePathWindows = %r!^(?:(?:[A-Z]:#{slash})|(?:#{slash}#{slash}#{label}#{slash}#{label})|(?:#{slash}#{slash}\?#{slash}#{label}))!io AbsolutePathPosix = %r!^/! def absolute_path?(path, platform=nil) # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard # library uses that to test what platform it's on. Normally in Puppet we # would use Puppet.features.microsoft_windows?, but this method needs to # be called during the initialization of features so it can't depend on # that. platform ||= Puppet::Util::Platform.windows? ? :windows : :posix regex = case platform when :windows AbsolutePathWindows when :posix AbsolutePathPosix else raise Puppet::DevError, "unknown platform #{platform} in absolute_path" end !! (path =~ regex) end module_function :absolute_path? # Convert a path to a file URI def path_to_uri(path) return unless path params = { :scheme => 'file' } if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') if unc = /^\/\/([^\/]+)(\/.+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i path = '/' + path end end params[:path] = URI.escape(path) begin URI::Generic.build(params) rescue => detail raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}", detail.backtrace end end module_function :path_to_uri # Get the path component of a URI def uri_to_path(uri) return unless uri.is_a?(URI) path = URI.unescape(uri.path) if Puppet.features.microsoft_windows? and uri.scheme == 'file' if uri.host path = "//#{uri.host}" + path # UNC else path.sub!(/^\//, '') end end path end module_function :uri_to_path def safe_posix_fork(stdin=$stdin, stdout=$stdout, stderr=$stderr, &block) child_pid = Kernel.fork do $stdin.reopen(stdin) $stdout.reopen(stdout) $stderr.reopen(stderr) 3.upto(256){|fd| IO::new(fd).close rescue nil} block.call if block end child_pid end module_function :safe_posix_fork def symbolizehash(hash) newhash = {} hash.each do |name, val| name = name.intern if name.respond_to? :intern newhash[name] = val end newhash end module_function :symbolizehash # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :thinmark # Because IO#binread is only available in 1.9 def binread(file) Puppet.deprecation_warning("Puppet::Util.binread is deprecated. Read the file without this method as it will be removed in a future version.") File.open(file, 'rb') { |f| f.read } end module_function :binread # utility method to get the current call stack and format it to a human-readable string (which some IDEs/editors # will recognize as links to the line numbers in the trace) def self.pretty_backtrace(backtrace = caller(1)) backtrace.collect do |line| _, path, rest = /^(.*):(\d+.*)$/.match(line).to_a # If the path doesn't exist - like in one test, and like could happen in # the world - we should just tolerate it and carry on. --daniel 2012-09-05 # Also, if we don't match, just include the whole line. if path path = Pathname(path).realpath rescue path "#{path}:#{rest}" else line end end.join("\n") end # Replace a file, securely. This takes a block, and passes it the file # handle of a file open for writing. Write the replacement content inside # the block and it will safely replace the target file. # # This method will make no changes to the target file until the content is # successfully written and the block returns without raising an error. # # As far as possible the state of the existing file, such as mode, is # preserved. This works hard to avoid loss of any metadata, but will result # in an inode change for the file. # # Arguments: `filename`, `default_mode` # # The filename is the file we are going to replace. # # The default_mode is the mode to use when the target file doesn't already # exist; if the file is present we copy the existing mode/owner/group values # across. The default_mode can be expressed as an octal integer, a numeric string (ie '0664') # or a symbolic file mode. DEFAULT_POSIX_MODE = 0644 DEFAULT_WINDOWS_MODE = nil def replace_file(file, default_mode, &block) raise Puppet::DevError, "replace_file requires a block" unless block_given? if default_mode unless valid_symbolic_mode?(default_mode) raise Puppet::DevError, "replace_file default_mode: #{default_mode} is invalid" end mode = symbolic_mode_to_int(normalize_symbolic_mode(default_mode)) else if Puppet.features.microsoft_windows? mode = DEFAULT_WINDOWS_MODE else mode = DEFAULT_POSIX_MODE end end begin file = Puppet::FileSystem.pathname(file) tempfile = Puppet::FileSystem::Uniquefile.new(Puppet::FileSystem.basename_string(file), Puppet::FileSystem.dir_string(file)) # Set properties of the temporary file before we write the content, because # Tempfile doesn't promise to be safe from reading by other people, just # that it avoids races around creating the file. # # Our Windows emulation is pretty limited, and so we have to carefully # and specifically handle the platform, which has all sorts of magic. # So, unlike Unix, we don't pre-prep security; we use the default "quite # secure" tempfile permissions instead. Magic happens later. if !Puppet.features.microsoft_windows? # Grab the current file mode, and fall back to the defaults. effective_mode = if Puppet::FileSystem.exist?(file) stat = Puppet::FileSystem.lstat(file) tempfile.chown(stat.uid, stat.gid) stat.mode else mode end if effective_mode # We only care about the bottom four slots, which make the real mode, # and not the rest of the platform stat call fluff and stuff. tempfile.chmod(effective_mode & 07777) end end # OK, now allow the caller to write the content of the file. yield tempfile # Now, make sure the data (which includes the mode) is safe on disk. tempfile.flush begin tempfile.fsync rescue NotImplementedError # fsync may not be implemented by Ruby on all platforms, but # there is absolutely no recovery path if we detect that. So, we just # ignore the return code. # # However, don't be fooled: that is accepting that we are running in # an unsafe fashion. If you are porting to a new platform don't stub # that out. end tempfile.close if Puppet.features.microsoft_windows? # Windows ReplaceFile needs a file to exist, so touch handles this if !Puppet::FileSystem.exist?(file) Puppet::FileSystem.touch(file) if mode Puppet::Util::Windows::Security.set_mode(mode, Puppet::FileSystem.path_string(file)) end end # Yes, the arguments are reversed compared to the rename in the rest # of the world. Puppet::Util::Windows::File.replace_file(FileSystem.path_string(file), tempfile.path) else File.rename(tempfile.path, Puppet::FileSystem.path_string(file)) end ensure # in case an error occurred before we renamed the temp file, make sure it # gets deleted if tempfile tempfile.close! end end # Ideally, we would now fsync the directory as well, but Ruby doesn't # have support for that, and it doesn't matter /that/ much... # Return something true, and possibly useful. file end module_function :replace_file # Executes a block of code, wrapped with some special exception handling. Causes the ruby interpreter to # exit if the block throws an exception. # # @api public # @param [String] message a message to log if the block fails # @param [Integer] code the exit code that the ruby interpreter should return if the block fails # @yield def exit_on_fail(message, code = 1) yield # First, we need to check and see if we are catching a SystemExit error. These will be raised # when we daemonize/fork, and they do not necessarily indicate a failure case. rescue SystemExit => err raise err # Now we need to catch *any* other kind of exception, because we may be calling third-party # code (e.g. webrick), and we have no idea what they might throw. rescue Exception => err ## NOTE: when debugging spec failures, these two lines can be very useful #puts err.inspect #puts Puppet::Util.pretty_backtrace(err.backtrace) Puppet.log_exception(err, "Could not #{message}: #{err}") Puppet::Util::Log.force_flushqueue() exit(code) end module_function :exit_on_fail def deterministic_rand(seed,max) if defined?(Random) == 'constant' && Random.class == Class Random.new(seed).rand(max).to_s else srand(seed) result = rand(max).to_s srand() result end end module_function :deterministic_rand end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/autoload.rb b/lib/puppet/util/autoload.rb index 7173fe906..3aa6692a1 100644 --- a/lib/puppet/util/autoload.rb +++ b/lib/puppet/util/autoload.rb @@ -1,231 +1,221 @@ require 'pathname' require 'puppet/util/rubygems' require 'puppet/util/warnings' require 'puppet/util/methodhelper' # Autoload paths, either based on names or all at once. class Puppet::Util::Autoload include Puppet::Util::MethodHelper - @autoloaders = {} @loaded = {} class << self - attr_reader :autoloaders attr_accessor :loaded - private :autoloaders, :loaded def gem_source @gem_source ||= Puppet::Util::RubyGems::Source.new end # Has a given path been loaded? This is used for testing whether a # changed file should be loaded or just ignored. This is only # used in network/client/master, when downloading plugins, to # see if a given plugin is currently loaded and thus should be # reloaded. def loaded?(path) path = cleanpath(path).chomp('.rb') loaded.include?(path) end # Save the fact that a given path has been loaded. This is so # we can load downloaded plugins if they've already been loaded # into memory. def mark_loaded(name, file) name = cleanpath(name).chomp('.rb') ruby_file = name + ".rb" $LOADED_FEATURES << ruby_file unless $LOADED_FEATURES.include?(ruby_file) loaded[name] = [file, File.mtime(file)] end def changed?(name) name = cleanpath(name).chomp('.rb') return true unless loaded.include?(name) file, old_mtime = loaded[name] environment = Puppet.lookup(:current_environment) return true unless file == get_file(name, environment) begin old_mtime.to_i != File.mtime(file).to_i rescue Errno::ENOENT true end end # Load a single plugin by name. We use 'load' here so we can reload a # given plugin. def load_file(name, env) file = get_file(name.to_s, env) return false unless file begin mark_loaded(name, file) - Kernel.load file, @wrap + Kernel.load file return true rescue SystemExit,NoMemoryError raise rescue Exception => detail message = "Could not autoload #{name}: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end end def loadall(path) # Load every instance of everything we can find. files_to_load(path).each do |file| name = file.chomp(".rb") load_file(name, nil) unless loaded?(name) end end def reload_changed loaded.keys.each { |file| load_file(file, nil) if changed?(file) } end # Get the correct file to load for a given path # returns nil if no file is found def get_file(name, env) name = name + '.rb' unless name =~ /\.rb$/ path = search_directories(env).find { |dir| Puppet::FileSystem.exist?(File.join(dir, name)) } path and File.join(path, name) end def files_to_load(path) search_directories(nil).map {|dir| files_in_dir(dir, path) }.flatten.uniq end def files_in_dir(dir, path) dir = Pathname.new(File.expand_path(dir)) Dir.glob(File.join(dir, path, "*.rb")).collect do |file| Pathname.new(file).relative_path_from(dir).to_s end end def module_directories(env) # We're using a per-thread cache of module directories so that we don't # scan the filesystem each time we try to load something. This is reset # at the beginning of compilation and at the end of an agent run. $env_module_directories ||= {} # This is a little bit of a hack. Basically, the autoloader is being # called indirectly during application bootstrapping when we do things # such as check "features". However, during bootstrapping, we haven't # yet parsed all of the command line parameters nor the config files, # and thus we don't yet know with certainty what the module path is. # This should be irrelevant during bootstrapping, because anything that # we are attempting to load during bootstrapping should be something # that we ship with puppet, and thus the module path is irrelevant. # # In the long term, I think the way that we want to handle this is to # have the autoloader ignore the module path in all cases where it is # not specifically requested (e.g., by a constructor param or # something)... because there are very few cases where we should # actually be loading code from the module path. However, until that # happens, we at least need a way to prevent the autoloader from # attempting to access the module path before it is initialized. For # now we are accomplishing that by calling the # "app_defaults_initialized?" method on the main puppet Settings object. # --cprice 2012-03-16 if Puppet.settings.app_defaults_initialized? env ||= Puppet.lookup(:environments).get(Puppet[:environment]) if env # if the app defaults have been initialized then it should be safe to access the module path setting. $env_module_directories[env] ||= env.modulepath.collect do |dir| Dir.entries(dir).reject { |f| f =~ /^\./ }.collect { |f| File.join(dir, f, "lib") } end.flatten.find_all do |d| FileTest.directory?(d) end else [] end else # if we get here, the app defaults have not been initialized, so we basically use an empty module path. [] end end def libdirs() # See the comments in #module_directories above. Basically, we need to be careful not to try to access the # libdir before we know for sure that all of the settings have been initialized (e.g., during bootstrapping). if (Puppet.settings.app_defaults_initialized?) Puppet[:libdir].split(File::PATH_SEPARATOR) else [] end end def gem_directories gem_source.directories end def search_directories(env) [gem_directories, module_directories(env), libdirs(), $LOAD_PATH].flatten end # Normalize a path. This converts ALT_SEPARATOR to SEPARATOR on Windows # and eliminates unnecessary parts of a path. def cleanpath(path) # There are two cases here because cleanpath does not handle absolute # paths correctly on windows (c:\ and c:/ are treated as distinct) but # we don't want to convert relative paths to absolute if Puppet::Util.absolute_path?(path) File.expand_path(path) else Pathname.new(path).cleanpath.to_s end end end - # Send [] and []= to the @autoloaders hash - Puppet::Util.classproxy self, :autoloaders, "[]", "[]=" - - attr_accessor :object, :path, :objwarn, :wrap + attr_accessor :object, :path def initialize(obj, path, options = {}) @path = path.to_s raise ArgumentError, "Autoload paths cannot be fully qualified" if Puppet::Util.absolute_path?(@path) @object = obj - self.class[obj] = self - set_options(options) - - @wrap = true unless defined?(@wrap) end def load(name, env = nil) self.class.load_file(expand(name), env) end # Load all instances from a path of Autoload.search_directories matching the # relative path this Autoloader was initialized with. For example, if we # have created a Puppet::Util::Autoload for Puppet::Type::User with a path of # 'puppet/provider/user', the search_directories path will be searched for # all ruby files matching puppet/provider/user/*.rb and they will then be # loaded from the first directory in the search path providing them. So # earlier entries in the search path may shadow later entries. # # This uses require, rather than load, so that already-loaded files don't get # reloaded unnecessarily. def loadall self.class.loadall(@path) end def loaded?(name) self.class.loaded?(expand(name)) end def changed?(name) self.class.changed?(expand(name)) end def files_to_load self.class.files_to_load(@path) end def expand(name) ::File.join(@path, name.to_s) end end diff --git a/lib/puppet/util/network_device/base.rb b/lib/puppet/util/network_device/base.rb index 8f971efdf..495194099 100644 --- a/lib/puppet/util/network_device/base.rb +++ b/lib/puppet/util/network_device/base.rb @@ -1,27 +1,23 @@ require 'puppet/util/autoload' require 'uri' require 'puppet/util/network_device/transport' require 'puppet/util/network_device/transport/base' class Puppet::Util::NetworkDevice::Base attr_accessor :url, :transport def initialize(url, options = {}) @url = URI.parse(url) - @autoloader = Puppet::Util::Autoload.new( - self, - "puppet/util/network_device/transport", - :wrap => false - ) + @autoloader = Puppet::Util::Autoload.new(self, "puppet/util/network_device/transport") if @autoloader.load(@url.scheme) @transport = Puppet::Util::NetworkDevice::Transport.const_get(@url.scheme.capitalize).new(options[:debug]) @transport.host = @url.host @transport.port = @url.port || case @url.scheme ; when "ssh" ; 22 ; when "telnet" ; 23 ; end @transport.user = @url.user @transport.password = @url.password end end end diff --git a/spec/integration/util/autoload_spec.rb b/spec/integration/util/autoload_spec.rb index c352fea9e..997e43074 100755 --- a/spec/integration/util/autoload_spec.rb +++ b/spec/integration/util/autoload_spec.rb @@ -1,107 +1,102 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/util/autoload' require 'fileutils' class AutoloadIntegrator @things = [] def self.newthing(name) @things << name end def self.thing?(name) @things.include? name end def self.clear @things.clear end end require 'puppet_spec/files' describe Puppet::Util::Autoload do include PuppetSpec::Files def with_file(name, *path) path = File.join(*path) # Now create a file to load File.open(path, "w") { |f| f.puts "\nAutoloadIntegrator.newthing(:#{name.to_s})\n" } yield File.delete(path) end def with_loader(name, path) dir = tmpfile(name + path) $LOAD_PATH << dir Dir.mkdir(dir) rbdir = File.join(dir, path.to_s) Dir.mkdir(rbdir) loader = Puppet::Util::Autoload.new(name, path) yield rbdir, loader Dir.rmdir(rbdir) Dir.rmdir(dir) $LOAD_PATH.pop AutoloadIntegrator.clear end - it "should make instances available by the loading class" do - loader = Puppet::Util::Autoload.new("foo", "bar") - Puppet::Util::Autoload["foo"].should == loader - end - it "should not fail when asked to load a missing file" do Puppet::Util::Autoload.new("foo", "bar").load(:eh).should be_false end it "should load and return true when it successfully loads a file" do with_loader("foo", "bar") { |dir,loader| with_file(:mything, dir, "mything.rb") { loader.load(:mything).should be_true loader.class.should be_loaded("bar/mything") AutoloadIntegrator.should be_thing(:mything) } } end it "should consider a file loaded when asked for the name without an extension" do with_loader("foo", "bar") { |dir,loader| with_file(:noext, dir, "noext.rb") { loader.load(:noext) loader.class.should be_loaded("bar/noext") } } end it "should consider a file loaded when asked for the name with an extension" do with_loader("foo", "bar") { |dir,loader| with_file(:noext, dir, "withext.rb") { loader.load(:withext) loader.class.should be_loaded("bar/withext.rb") } } end it "should be able to load files directly from modules" do ## modulepath can't be used until after app settings are initialized, so we need to simulate that: Puppet.settings.expects(:app_defaults_initialized?).returns(true).at_least_once modulepath = tmpfile("autoload_module_testing") libdir = File.join(modulepath, "mymod", "lib", "foo") FileUtils.mkdir_p(libdir) file = File.join(libdir, "plugin.rb") Puppet.override(:environments => Puppet::Environments::Static.new(Puppet::Node::Environment.create(:production, [modulepath]))) do with_loader("foo", "foo") do |dir, loader| with_file(:plugin, file.split("/")) do loader.load(:plugin) loader.class.should be_loaded("foo/plugin.rb") end end end end end