module Puppet::Util
MultiMatch allows multiple values to be tested at once in a case expression. This class is needed since Array does not implement the === operator to mean “each v === other.each v”.
This class is useful in situations when the Puppet Type System cannot be used (e.g. in Logging, since it needs to be able to log very early in the initialization cycle of puppet)
Typically used with the constants NOT_NIL TUPLE TRIPLE
which test against single NOT_NIL value, Array with two NOT_NIL, and Array with three NOT_NIL
Constants
- ALNUM
- ALPHA
From github.com/ruby/ruby/blob/v2_7_3/lib/uri/rfc2396_parser.rb#L24-L46
- AbsolutePathPosix
- AbsolutePathWindows
- DEFAULT_POSIX_MODE
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`, `staging_location`
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.
The staging_location is a location to render the temporary file before moving the file to it's final location.
- DEFAULT_WINDOWS_MODE
- ESCAPED
- HEX
- HttpProxy
for backwards compatibility
- PUPPET_STACK_INSERTION_FRAME
- RESERVED
- RFC_3986_URI_REGEX
- UNRESERVED
- UNSAFE
Public Class Methods
# File lib/puppet/util.rb 255 def absolute_path?(path, platform=nil) 256 unless path.is_a?(String) 257 Puppet.warning("Cannot check if #{path} is an absolute path because it is a '#{path.class}' and not a String'") 258 return false 259 end 260 261 # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard 262 # library uses that to test what platform it's on. Normally in Puppet we 263 # would use Puppet.features.microsoft_windows?, but this method needs to 264 # be called during the initialization of features so it can't depend on 265 # that. 266 # 267 # @deprecated Use ruby's built-in methods to determine if a path is absolute. 268 platform ||= Puppet::Util::Platform.windows? ? :windows : :posix 269 regex = case platform 270 when :windows 271 AbsolutePathWindows 272 when :posix 273 AbsolutePathPosix 274 else 275 raise Puppet::DevError, _("unknown platform %{platform} in absolute_path") % { platform: platform } 276 end 277 278 !! (path =~ regex) 279 end
execute a block of work and based on the logging level provided, log the provided message with the seconds taken The message 'msg' should include string ' in %{seconds} seconds' as part of the message and any content should escape any percent signs '%' so that they are not interpreted as formatting commands
escaped_str = str.gsub(/%/, '%%')
@param msg [String] the message to be formated to assigned the %{seconds} seconds take to execute,
other percent signs '%' need to be escaped
@param level [Symbol] the logging level for this message @param object [Object] The object use for logging the message
# File lib/puppet/util.rb 167 def benchmark(*args) 168 msg = args.pop 169 level = args.pop 170 object = if args.empty? 171 if respond_to?(level) 172 self 173 else 174 Puppet 175 end 176 else 177 args.pop 178 end 179 180 #TRANSLATORS 'benchmark' is a method name and should not be translated 181 raise Puppet::DevError, _("Failed to provide level to benchmark") unless level 182 183 unless level == :none or object.respond_to? level 184 raise Puppet::DevError, _("Benchmarked object does not respond to %{value}") % { value: level } 185 end 186 187 # Only benchmark if our log level is high enough 188 if level != :none and Puppet::Util::Log.sendlevel?(level) 189 seconds = Benchmark.realtime { 190 yield 191 } 192 object.send(level, msg % { seconds: "%0.2f" % seconds }) 193 return seconds 194 else 195 yield 196 end 197 end
Change the process to a different user
# File lib/puppet/util.rb 107 def self.chuser 108 group = Puppet[:group] 109 if group 110 begin 111 Puppet::Util::SUIDManager.change_group(group, true) 112 rescue => detail 113 Puppet.warning _("could not change to group %{group}: %{detail}") % { group: group.inspect, detail: detail } 114 $stderr.puts _("could not change to group %{group}") % { group: group.inspect } 115 116 # Don't exit on failed group changes, since it's 117 # not fatal 118 #exit(74) 119 end 120 end 121 122 user = Puppet[:user] 123 if user 124 begin 125 Puppet::Util::SUIDManager.change_user(user, true) 126 rescue => detail 127 $stderr.puts _("Could not change to user %{user}: %{detail}") % { user: user, detail: detail } 128 exit(74) 129 end 130 end 131 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 59 def clear_environment(mode = default_env) 60 ENV.clear 61 end
# File lib/puppet/util.rb 38 def create_erb(content) 39 ERB.new(content, trim_mode: '-') 40 end
# File lib/puppet/util.rb 31 def default_env 32 Puppet.features.microsoft_windows? ? 33 :windows : 34 :posix 35 end
# File lib/puppet/util.rb 719 def deterministic_rand(seed,max) 720 deterministic_rand_int(seed, max).to_s 721 end
# File lib/puppet/util.rb 724 def deterministic_rand_int(seed,max) 725 Random.new(seed).rand(max) 726 end
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
# File lib/puppet/util.rb 700 def exit_on_fail(message, code = 1) 701 yield 702 # First, we need to check and see if we are catching a SystemExit error. These will be raised 703 # when we daemonize/fork, and they do not necessarily indicate a failure case. 704 rescue SystemExit => err 705 raise err 706 707 # Now we need to catch *any* other kind of exception, because we may be calling third-party 708 # code (e.g. webrick), and we have no idea what they might throw. 709 rescue Exception => err 710 ## NOTE: when debugging spec failures, these two lines can be very useful 711 #puts err.inspect 712 #puts Puppet::Util.pretty_backtrace(err.backtrace) 713 Puppet.log_exception(err, "#{message}: #{err}") 714 Puppet::Util::Log.force_flushqueue() 715 exit(code) 716 end
arguments may be a Ruby stack, with an optional Puppet stack argument, or just a Puppet stack. stacks may be an Array of Strings “/foo.rb:0 in `blah'” or an Array of Arrays that represent a frame: [“/foo.pp”, 0]
# File lib/puppet/util.rb 532 def self.format_backtrace_array(primary_stack, puppetstack = []) 533 primary_stack.flat_map do |frame| 534 frame = format_puppetstack_frame(frame) if frame.is_a?(Array) 535 primary_frame = resolve_stackframe(frame) 536 537 if primary_frame =~ PUPPET_STACK_INSERTION_FRAME && !puppetstack.empty? 538 [resolve_stackframe(format_puppetstack_frame(puppetstack.shift)), 539 primary_frame] 540 else 541 primary_frame 542 end 543 end 544 end
# File lib/puppet/util.rb 556 def self.format_puppetstack_frame(file_and_lineno) 557 file_and_lineno.join(':') 558 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 45 def get_env(name, mode = default_env) 46 ENV[name] 47 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 52 def get_environment(mode = default_env) 53 ENV.to_hash 54 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.
# File lib/puppet/util.rb 136 def self.logmethods(klass, useself = true) 137 Puppet::Util::Log.eachlevel { |level| 138 klass.send(:define_method, level, proc { |args| 139 args = args.join(" ") if args.is_a?(Array) 140 if useself 141 142 Puppet::Util::Log.create( 143 :level => level, 144 :source => self, 145 :message => args 146 ) 147 else 148 149 Puppet::Util::Log.create( 150 :level => level, 151 :message => args 152 ) 153 end 154 }) 155 } 156 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 73 def merge_environment(env_hash, mode = default_env) 74 ENV.merge!(hash.transform_keys(&:to_s)) 75 end
Convert a path to a file URI
# File lib/puppet/util.rb 283 def path_to_uri(path) 284 return unless path 285 286 params = { :scheme => 'file' } 287 288 if Puppet::Util::Platform.windows? 289 path = path.tr('\\', '/') 290 291 unc = /^\/\/([^\/]+)(\/.+)/.match(path) 292 if unc 293 params[:host] = unc[1] 294 path = unc[2] 295 elsif path =~ /^[a-z]:\//i 296 path = '/' + path 297 end 298 end 299 300 # have to split *after* any relevant escaping 301 params[:path], params[:query] = uri_encode(path).split('?') 302 search_for_fragment = params[:query] ? :query : :path 303 if params[search_for_fragment].include?('#') 304 params[search_for_fragment], _, params[:fragment] = params[search_for_fragment].rpartition('#') 305 end 306 307 begin 308 URI::Generic.build(params) 309 rescue => detail 310 raise Puppet::Error, _("Failed to convert '%{path}' to URI: %{detail}") % { path: path, detail: detail }, detail.backtrace 311 end 312 end
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)
# File lib/puppet/util.rb 524 def self.pretty_backtrace(backtrace = caller(1), puppetstack = []) 525 format_backtrace_array(backtrace, puppetstack).join("\n") 526 end
# File lib/puppet/util.rb 586 def replace_file(file, default_mode, staging_location: nil, validate_callback: nil, &block) 587 raise Puppet::DevError, _("replace_file requires a block") unless block_given? 588 589 if default_mode 590 unless valid_symbolic_mode?(default_mode) 591 raise Puppet::DevError, _("replace_file default_mode: %{default_mode} is invalid") % { default_mode: default_mode } 592 end 593 594 mode = symbolic_mode_to_int(normalize_symbolic_mode(default_mode)) 595 else 596 if Puppet::Util::Platform.windows? 597 mode = DEFAULT_WINDOWS_MODE 598 else 599 mode = DEFAULT_POSIX_MODE 600 end 601 end 602 603 begin 604 file = Puppet::FileSystem.pathname(file) 605 606 # encoding for Uniquefile is not important here because the caller writes to it as it sees fit 607 if staging_location 608 tempfile = Puppet::FileSystem::Uniquefile.new(Puppet::FileSystem.basename_string(file), staging_location) 609 else 610 tempfile = Puppet::FileSystem::Uniquefile.new(Puppet::FileSystem.basename_string(file), Puppet::FileSystem.dir_string(file)) 611 end 612 613 614 effective_mode = 615 if !Puppet::Util::Platform.windows? 616 # Grab the current file mode, and fall back to the defaults. 617 618 if Puppet::FileSystem.exist?(file) 619 stat = Puppet::FileSystem.lstat(file) 620 tempfile.chown(stat.uid, stat.gid) 621 stat.mode 622 else 623 mode 624 end 625 end 626 627 # OK, now allow the caller to write the content of the file. 628 yield tempfile 629 630 if effective_mode 631 # We only care about the bottom four slots, which make the real mode, 632 # and not the rest of the platform stat call fluff and stuff. 633 tempfile.chmod(effective_mode & 07777) 634 end 635 636 # Now, make sure the data (which includes the mode) is safe on disk. 637 tempfile.flush 638 begin 639 tempfile.fsync 640 rescue NotImplementedError 641 # fsync may not be implemented by Ruby on all platforms, but 642 # there is absolutely no recovery path if we detect that. So, we just 643 # ignore the return code. 644 # 645 # However, don't be fooled: that is accepting that we are running in 646 # an unsafe fashion. If you are porting to a new platform don't stub 647 # that out. 648 end 649 650 tempfile.close 651 652 if validate_callback 653 validate_callback.call(tempfile.path) 654 end 655 656 if Puppet::Util::Platform.windows? 657 # Windows ReplaceFile needs a file to exist, so touch handles this 658 if !Puppet::FileSystem.exist?(file) 659 Puppet::FileSystem.touch(file) 660 if mode 661 Puppet::Util::Windows::Security.set_mode(mode, Puppet::FileSystem.path_string(file)) 662 end 663 end 664 # Yes, the arguments are reversed compared to the rename in the rest 665 # of the world. 666 Puppet::Util::Windows::File.replace_file(FileSystem.path_string(file), tempfile.path) 667 668 else 669 # MRI Ruby checks for this and raises an error, while JRuby removes the directory 670 # and replaces it with a file. This makes the our version of replace_file() consistent 671 if Puppet::FileSystem.exist?(file) && Puppet::FileSystem.directory?(file) 672 raise Errno::EISDIR, _("Is a directory: %{directory}") % { directory: file } 673 end 674 File.rename(tempfile.path, Puppet::FileSystem.path_string(file)) 675 end 676 ensure 677 # in case an error occurred before we renamed the temp file, make sure it 678 # gets deleted 679 if tempfile 680 tempfile.close! 681 end 682 end 683 684 685 # Ideally, we would now fsync the directory as well, but Ruby doesn't 686 # have support for that, and it doesn't matter /that/ much... 687 688 # Return something true, and possibly useful. 689 file 690 end
# File lib/puppet/util.rb 546 def self.resolve_stackframe(frame) 547 _, path, rest = /^(.*):(\d+.*)$/.match(frame).to_a 548 if path 549 path = Pathname(path).realpath rescue path 550 "#{path}:#{rest}" 551 else 552 frame 553 end 554 end
# File lib/puppet/util.rb 455 def rfc2396_escape(str) 456 str.gsub(UNSAFE) do |match| 457 tmp = String.new 458 match.each_byte do |uc| 459 tmp << sprintf('%%%02X', uc) 460 end 461 tmp 462 end.force_encoding(Encoding::US_ASCII) 463 end
# File lib/puppet/util.rb 473 def safe_posix_fork(stdin=$stdin, stdout=$stdout, stderr=$stderr, &block) 474 child_pid = Kernel.fork do 475 STDIN.reopen(stdin) 476 STDOUT.reopen(stdout) 477 STDERR.reopen(stderr) 478 479 $stdin = STDIN 480 $stdout = STDOUT 481 $stderr = STDERR 482 483 begin 484 Dir.foreach('/proc/self/fd') do |f| 485 if f != '.' && f != '..' && f.to_i >= 3 486 IO::new(f.to_i).close rescue nil 487 end 488 end 489 rescue Errno::ENOENT, Errno::ENOTDIR # /proc/self/fd not found, /proc/self not a dir 490 3.upto(256){|fd| IO::new(fd).close rescue nil} 491 end 492 493 block.call if block 494 end 495 child_pid 496 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 66 def set_env(name, value = nil, mode = default_env) 67 ENV[name] = value 68 end
Executes a block of code, wrapped around Facter.load_external(false) and Facter.load_external(true) which will cause Facter to not evaluate external facts.
# File lib/puppet/util.rb 731 def skip_external_facts 732 return yield unless Puppet.runtime[:facter].load_external? 733 734 begin 735 Puppet.runtime[:facter].load_external(false) 736 yield 737 ensure 738 Puppet.runtime[:facter].load_external(true) 739 end 740 end
# File lib/puppet/util.rb 499 def symbolizehash(hash) 500 newhash = {} 501 hash.each do |name, val| 502 name = name.intern if name.respond_to? :intern 503 newhash[name] = val 504 end 505 newhash 506 end
Just benchmark, with no logging.
# File lib/puppet/util.rb 510 def thinmark 511 seconds = Benchmark.realtime { 512 yield 513 } 514 515 seconds 516 end
Percent-encodes a URI string per RFC3986 - tools.ietf.org/html/rfc3986
Properly handles escaping rules for paths, query strings and fragments independently
The output is safe to pass to URI.parse or URI::Generic.build and will correctly round-trip through URI.unescape
@param [String path] A URI string that may be in the form of:
http://foo.com/bar?query file://tmp/foo bar //foo.com/bar?query /bar?query bar?query bar . C:\Windows\Temp Note that with no specified scheme, authority or query parameter delimiter ? that a naked string will be treated as a path. Note that if query parameters need to contain data such as & or = that this method should not be used, as there is no way to differentiate query parameter data from query delimiters when multiple parameters are specified
@param [Hash{Symbol=>String} opts] Options to alter encoding @option opts [Array<Symbol>] :allow_fragment defaults to false. When false
will treat # as part of a path or query and not a fragment delimiter
@return [String] a new string containing appropriate portions of the URI
encoded per the rules of RFC3986. In particular, path will not encode +, but will encode space as %20 query will encode + as %2B and space as %20 fragment behaves like query
# File lib/puppet/util.rb 409 def uri_encode(path, opts = { :allow_fragment => false }) 410 raise ArgumentError.new(_('path may not be nil')) if path.nil? 411 412 # ensure string starts as UTF-8 for the sake of Ruby 1.9.3 413 encoded = String.new.encode!(Encoding::UTF_8) 414 415 # parse uri into named matches, then reassemble properly encoded 416 parts = path.match(RFC_3986_URI_REGEX) 417 418 encoded += parts[:scheme] unless parts[:scheme].nil? 419 encoded += parts[:authority] unless parts[:authority].nil? 420 421 # path requires space to be encoded as %20 (NEVER +) 422 # + should be left unencoded 423 # URI::parse and URI::Generic.build don't like paths encoded with CGI.escape 424 # URI.escape does not change / to %2F and : to %3A like CGI.escape 425 # 426 encoded += rfc2396_escape(parts[:path]) unless parts[:path].nil? 427 428 # each query parameter 429 if !parts[:query].nil? 430 query_string = parts[:query].split('&').map do |pair| 431 # can optionally be separated by an = 432 pair.split('=').map do |v| 433 uri_query_encode(v) 434 end.join('=') 435 end.join('&') 436 encoded += '?' + query_string 437 end 438 439 encoded += ((opts[:allow_fragment] ? '#' : '%23') + uri_query_encode(parts[:fragment])) unless parts[:fragment].nil? 440 441 encoded 442 end
Percent-encodes a URI query parameter per RFC3986 - tools.ietf.org/html/rfc3986
The output will correctly round-trip through URI.unescape
@param [String query_string] A URI query parameter that may contain reserved
characters that must be percent encoded for the key or value to be properly decoded as part of a larger query string: query encodes as : query query_with_special=chars like&and * and# plus+this encodes as: query_with_special%3Dchars%20like%26and%20%2A%20and%23%20plus%2Bthis Note: Also usable by fragments, but not suitable for paths
@return [String] a new string containing an encoded query string per the
rules of RFC3986. In particular, query will encode + as %2B and space as %20
# File lib/puppet/util.rb 359 def uri_query_encode(query_string) 360 return nil if query_string.nil? 361 362 # query can encode space to %20 OR + 363 # + MUST be encoded as %2B 364 # in RFC3968 both query and fragment are defined as: 365 # = *( pchar / "/" / "?" ) 366 # CGI.escape turns space into + which is the most backward compatible 367 # however it doesn't roundtrip through URI.unescape which prefers %20 368 CGI.escape(query_string).gsub('+', '%20') 369 end
Get the path component of a URI
# File lib/puppet/util.rb 316 def uri_to_path(uri) 317 return unless uri.is_a?(URI) 318 319 # CGI.unescape doesn't handle space rules properly in uri paths 320 # URI.unescape does, but returns strings in their original encoding 321 path = uri_unescape(uri.path.encode(Encoding::UTF_8)) 322 323 if Puppet::Util::Platform.windows? && uri.scheme == 'file' 324 if uri.host && !uri.host.empty? 325 path = "//#{uri.host}" + path # UNC 326 else 327 path.sub!(/^\//, '') 328 end 329 end 330 331 path 332 end
# File lib/puppet/util.rb 466 def uri_unescape(str) 467 enc = str.encoding 468 enc = Encoding::UTF_8 if enc == Encoding::US_ASCII 469 str.gsub(ESCAPED) { [$&[1, 2]].pack('H2').force_encoding(enc) } 470 end
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.
# File lib/puppet/util.rb 207 def which(bin) 208 if absolute_path?(bin) 209 return bin if FileTest.file? bin and FileTest.executable? bin 210 else 211 exts = ENV['PATHEXT'] 212 exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] 213 ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| 214 begin 215 dest = File.expand_path(File.join(dir, bin)) 216 rescue ArgumentError => e 217 # if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get 218 # an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error 219 # was thrown. 220 if e.to_s =~ /HOME/ and (ENV['HOME'].nil? || ENV['HOME'] == "") 221 # if we get here they have a tilde in their PATH. We'll issue a single warning about this and then 222 # ignore this path element and carry on with our lives. 223 #TRANSLATORS PATH and HOME are environment variables and should not be translated 224 Puppet::Util::Warnings.warnonce(_("PATH contains a ~ character, and HOME is not set; ignoring PATH element '%{dir}'.") % { dir: dir }) 225 elsif e.to_s =~ /doesn't exist|can't find user/ 226 # ...otherwise, we just skip the non-existent entry, and do nothing. 227 #TRANSLATORS PATH is an environment variable and should not be translated 228 Puppet::Util::Warnings.warnonce(_("Couldn't expand PATH containing a ~ character; ignoring PATH element '%{dir}'.") % { dir: dir }) 229 else 230 raise 231 end 232 else 233 if Puppet::Util::Platform.windows? && File.extname(dest).empty? 234 exts.each do |ext| 235 destext = File.expand_path(dest + ext) 236 return destext if FileTest.file? destext and FileTest.executable? destext 237 end 238 end 239 return dest if FileTest.file? dest and FileTest.executable? dest 240 end 241 end 242 end 243 nil 244 end
Run some code with a specific environment. Resets the environment back to what it was at the end of the code.
@param hash [Hash{String,Symbol => String}] Environment variables to override the current environment. @param mode [Symbol] ignored
# File lib/puppet/util.rb 83 def withenv(hash, mode = :posix) 84 saved = ENV.to_hash 85 begin 86 ENV.merge!(hash.transform_keys(&:to_s)) 87 yield 88 ensure 89 ENV.replace(saved) 90 end 91 end
Execute a given chunk of code with a new umask.
# File lib/puppet/util.rb 95 def self.withumask(mask) 96 cur = File.umask(mask) 97 98 begin 99 yield 100 ensure 101 File.umask(cur) 102 end 103 end
Private Instance Methods
# File lib/puppet/util.rb 255 def absolute_path?(path, platform=nil) 256 unless path.is_a?(String) 257 Puppet.warning("Cannot check if #{path} is an absolute path because it is a '#{path.class}' and not a String'") 258 return false 259 end 260 261 # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard 262 # library uses that to test what platform it's on. Normally in Puppet we 263 # would use Puppet.features.microsoft_windows?, but this method needs to 264 # be called during the initialization of features so it can't depend on 265 # that. 266 # 267 # @deprecated Use ruby's built-in methods to determine if a path is absolute. 268 platform ||= Puppet::Util::Platform.windows? ? :windows : :posix 269 regex = case platform 270 when :windows 271 AbsolutePathWindows 272 when :posix 273 AbsolutePathPosix 274 else 275 raise Puppet::DevError, _("unknown platform %{platform} in absolute_path") % { platform: platform } 276 end 277 278 !! (path =~ regex) 279 end
execute a block of work and based on the logging level provided, log the provided message with the seconds taken The message 'msg' should include string ' in %{seconds} seconds' as part of the message and any content should escape any percent signs '%' so that they are not interpreted as formatting commands
escaped_str = str.gsub(/%/, '%%')
@param msg [String] the message to be formated to assigned the %{seconds} seconds take to execute,
other percent signs '%' need to be escaped
@param level [Symbol] the logging level for this message @param object [Object] The object use for logging the message
# File lib/puppet/util.rb 167 def benchmark(*args) 168 msg = args.pop 169 level = args.pop 170 object = if args.empty? 171 if respond_to?(level) 172 self 173 else 174 Puppet 175 end 176 else 177 args.pop 178 end 179 180 #TRANSLATORS 'benchmark' is a method name and should not be translated 181 raise Puppet::DevError, _("Failed to provide level to benchmark") unless level 182 183 unless level == :none or object.respond_to? level 184 raise Puppet::DevError, _("Benchmarked object does not respond to %{value}") % { value: level } 185 end 186 187 # Only benchmark if our log level is high enough 188 if level != :none and Puppet::Util::Log.sendlevel?(level) 189 seconds = Benchmark.realtime { 190 yield 191 } 192 object.send(level, msg % { seconds: "%0.2f" % seconds }) 193 return seconds 194 else 195 yield 196 end 197 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 59 def clear_environment(mode = default_env) 60 ENV.clear 61 end
# File lib/puppet/util.rb 38 def create_erb(content) 39 ERB.new(content, trim_mode: '-') 40 end
# File lib/puppet/util.rb 31 def default_env 32 Puppet.features.microsoft_windows? ? 33 :windows : 34 :posix 35 end
# File lib/puppet/util.rb 719 def deterministic_rand(seed,max) 720 deterministic_rand_int(seed, max).to_s 721 end
# File lib/puppet/util.rb 724 def deterministic_rand_int(seed,max) 725 Random.new(seed).rand(max) 726 end
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
# File lib/puppet/util.rb 700 def exit_on_fail(message, code = 1) 701 yield 702 # First, we need to check and see if we are catching a SystemExit error. These will be raised 703 # when we daemonize/fork, and they do not necessarily indicate a failure case. 704 rescue SystemExit => err 705 raise err 706 707 # Now we need to catch *any* other kind of exception, because we may be calling third-party 708 # code (e.g. webrick), and we have no idea what they might throw. 709 rescue Exception => err 710 ## NOTE: when debugging spec failures, these two lines can be very useful 711 #puts err.inspect 712 #puts Puppet::Util.pretty_backtrace(err.backtrace) 713 Puppet.log_exception(err, "#{message}: #{err}") 714 Puppet::Util::Log.force_flushqueue() 715 exit(code) 716 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 45 def get_env(name, mode = default_env) 46 ENV[name] 47 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 52 def get_environment(mode = default_env) 53 ENV.to_hash 54 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 73 def merge_environment(env_hash, mode = default_env) 74 ENV.merge!(hash.transform_keys(&:to_s)) 75 end
Convert a path to a file URI
# File lib/puppet/util.rb 283 def path_to_uri(path) 284 return unless path 285 286 params = { :scheme => 'file' } 287 288 if Puppet::Util::Platform.windows? 289 path = path.tr('\\', '/') 290 291 unc = /^\/\/([^\/]+)(\/.+)/.match(path) 292 if unc 293 params[:host] = unc[1] 294 path = unc[2] 295 elsif path =~ /^[a-z]:\//i 296 path = '/' + path 297 end 298 end 299 300 # have to split *after* any relevant escaping 301 params[:path], params[:query] = uri_encode(path).split('?') 302 search_for_fragment = params[:query] ? :query : :path 303 if params[search_for_fragment].include?('#') 304 params[search_for_fragment], _, params[:fragment] = params[search_for_fragment].rpartition('#') 305 end 306 307 begin 308 URI::Generic.build(params) 309 rescue => detail 310 raise Puppet::Error, _("Failed to convert '%{path}' to URI: %{detail}") % { path: path, detail: detail }, detail.backtrace 311 end 312 end
# File lib/puppet/util.rb 586 def replace_file(file, default_mode, staging_location: nil, validate_callback: nil, &block) 587 raise Puppet::DevError, _("replace_file requires a block") unless block_given? 588 589 if default_mode 590 unless valid_symbolic_mode?(default_mode) 591 raise Puppet::DevError, _("replace_file default_mode: %{default_mode} is invalid") % { default_mode: default_mode } 592 end 593 594 mode = symbolic_mode_to_int(normalize_symbolic_mode(default_mode)) 595 else 596 if Puppet::Util::Platform.windows? 597 mode = DEFAULT_WINDOWS_MODE 598 else 599 mode = DEFAULT_POSIX_MODE 600 end 601 end 602 603 begin 604 file = Puppet::FileSystem.pathname(file) 605 606 # encoding for Uniquefile is not important here because the caller writes to it as it sees fit 607 if staging_location 608 tempfile = Puppet::FileSystem::Uniquefile.new(Puppet::FileSystem.basename_string(file), staging_location) 609 else 610 tempfile = Puppet::FileSystem::Uniquefile.new(Puppet::FileSystem.basename_string(file), Puppet::FileSystem.dir_string(file)) 611 end 612 613 614 effective_mode = 615 if !Puppet::Util::Platform.windows? 616 # Grab the current file mode, and fall back to the defaults. 617 618 if Puppet::FileSystem.exist?(file) 619 stat = Puppet::FileSystem.lstat(file) 620 tempfile.chown(stat.uid, stat.gid) 621 stat.mode 622 else 623 mode 624 end 625 end 626 627 # OK, now allow the caller to write the content of the file. 628 yield tempfile 629 630 if effective_mode 631 # We only care about the bottom four slots, which make the real mode, 632 # and not the rest of the platform stat call fluff and stuff. 633 tempfile.chmod(effective_mode & 07777) 634 end 635 636 # Now, make sure the data (which includes the mode) is safe on disk. 637 tempfile.flush 638 begin 639 tempfile.fsync 640 rescue NotImplementedError 641 # fsync may not be implemented by Ruby on all platforms, but 642 # there is absolutely no recovery path if we detect that. So, we just 643 # ignore the return code. 644 # 645 # However, don't be fooled: that is accepting that we are running in 646 # an unsafe fashion. If you are porting to a new platform don't stub 647 # that out. 648 end 649 650 tempfile.close 651 652 if validate_callback 653 validate_callback.call(tempfile.path) 654 end 655 656 if Puppet::Util::Platform.windows? 657 # Windows ReplaceFile needs a file to exist, so touch handles this 658 if !Puppet::FileSystem.exist?(file) 659 Puppet::FileSystem.touch(file) 660 if mode 661 Puppet::Util::Windows::Security.set_mode(mode, Puppet::FileSystem.path_string(file)) 662 end 663 end 664 # Yes, the arguments are reversed compared to the rename in the rest 665 # of the world. 666 Puppet::Util::Windows::File.replace_file(FileSystem.path_string(file), tempfile.path) 667 668 else 669 # MRI Ruby checks for this and raises an error, while JRuby removes the directory 670 # and replaces it with a file. This makes the our version of replace_file() consistent 671 if Puppet::FileSystem.exist?(file) && Puppet::FileSystem.directory?(file) 672 raise Errno::EISDIR, _("Is a directory: %{directory}") % { directory: file } 673 end 674 File.rename(tempfile.path, Puppet::FileSystem.path_string(file)) 675 end 676 ensure 677 # in case an error occurred before we renamed the temp file, make sure it 678 # gets deleted 679 if tempfile 680 tempfile.close! 681 end 682 end 683 684 685 # Ideally, we would now fsync the directory as well, but Ruby doesn't 686 # have support for that, and it doesn't matter /that/ much... 687 688 # Return something true, and possibly useful. 689 file 690 end
# File lib/puppet/util.rb 455 def rfc2396_escape(str) 456 str.gsub(UNSAFE) do |match| 457 tmp = String.new 458 match.each_byte do |uc| 459 tmp << sprintf('%%%02X', uc) 460 end 461 tmp 462 end.force_encoding(Encoding::US_ASCII) 463 end
# File lib/puppet/util.rb 473 def safe_posix_fork(stdin=$stdin, stdout=$stdout, stderr=$stderr, &block) 474 child_pid = Kernel.fork do 475 STDIN.reopen(stdin) 476 STDOUT.reopen(stdout) 477 STDERR.reopen(stderr) 478 479 $stdin = STDIN 480 $stdout = STDOUT 481 $stderr = STDERR 482 483 begin 484 Dir.foreach('/proc/self/fd') do |f| 485 if f != '.' && f != '..' && f.to_i >= 3 486 IO::new(f.to_i).close rescue nil 487 end 488 end 489 rescue Errno::ENOENT, Errno::ENOTDIR # /proc/self/fd not found, /proc/self not a dir 490 3.upto(256){|fd| IO::new(fd).close rescue nil} 491 end 492 493 block.call if block 494 end 495 child_pid 496 end
@deprecated Use ENV instead @api private
# File lib/puppet/util.rb 66 def set_env(name, value = nil, mode = default_env) 67 ENV[name] = value 68 end
Executes a block of code, wrapped around Facter.load_external(false) and Facter.load_external(true) which will cause Facter to not evaluate external facts.
# File lib/puppet/util.rb 731 def skip_external_facts 732 return yield unless Puppet.runtime[:facter].load_external? 733 734 begin 735 Puppet.runtime[:facter].load_external(false) 736 yield 737 ensure 738 Puppet.runtime[:facter].load_external(true) 739 end 740 end
# File lib/puppet/util.rb 499 def symbolizehash(hash) 500 newhash = {} 501 hash.each do |name, val| 502 name = name.intern if name.respond_to? :intern 503 newhash[name] = val 504 end 505 newhash 506 end
Just benchmark, with no logging.
# File lib/puppet/util.rb 510 def thinmark 511 seconds = Benchmark.realtime { 512 yield 513 } 514 515 seconds 516 end
Percent-encodes a URI string per RFC3986 - tools.ietf.org/html/rfc3986
Properly handles escaping rules for paths, query strings and fragments independently
The output is safe to pass to URI.parse or URI::Generic.build and will correctly round-trip through URI.unescape
@param [String path] A URI string that may be in the form of:
http://foo.com/bar?query file://tmp/foo bar //foo.com/bar?query /bar?query bar?query bar . C:\Windows\Temp Note that with no specified scheme, authority or query parameter delimiter ? that a naked string will be treated as a path. Note that if query parameters need to contain data such as & or = that this method should not be used, as there is no way to differentiate query parameter data from query delimiters when multiple parameters are specified
@param [Hash{Symbol=>String} opts] Options to alter encoding @option opts [Array<Symbol>] :allow_fragment defaults to false. When false
will treat # as part of a path or query and not a fragment delimiter
@return [String] a new string containing appropriate portions of the URI
encoded per the rules of RFC3986. In particular, path will not encode +, but will encode space as %20 query will encode + as %2B and space as %20 fragment behaves like query
# File lib/puppet/util.rb 409 def uri_encode(path, opts = { :allow_fragment => false }) 410 raise ArgumentError.new(_('path may not be nil')) if path.nil? 411 412 # ensure string starts as UTF-8 for the sake of Ruby 1.9.3 413 encoded = String.new.encode!(Encoding::UTF_8) 414 415 # parse uri into named matches, then reassemble properly encoded 416 parts = path.match(RFC_3986_URI_REGEX) 417 418 encoded += parts[:scheme] unless parts[:scheme].nil? 419 encoded += parts[:authority] unless parts[:authority].nil? 420 421 # path requires space to be encoded as %20 (NEVER +) 422 # + should be left unencoded 423 # URI::parse and URI::Generic.build don't like paths encoded with CGI.escape 424 # URI.escape does not change / to %2F and : to %3A like CGI.escape 425 # 426 encoded += rfc2396_escape(parts[:path]) unless parts[:path].nil? 427 428 # each query parameter 429 if !parts[:query].nil? 430 query_string = parts[:query].split('&').map do |pair| 431 # can optionally be separated by an = 432 pair.split('=').map do |v| 433 uri_query_encode(v) 434 end.join('=') 435 end.join('&') 436 encoded += '?' + query_string 437 end 438 439 encoded += ((opts[:allow_fragment] ? '#' : '%23') + uri_query_encode(parts[:fragment])) unless parts[:fragment].nil? 440 441 encoded 442 end
Percent-encodes a URI query parameter per RFC3986 - tools.ietf.org/html/rfc3986
The output will correctly round-trip through URI.unescape
@param [String query_string] A URI query parameter that may contain reserved
characters that must be percent encoded for the key or value to be properly decoded as part of a larger query string: query encodes as : query query_with_special=chars like&and * and# plus+this encodes as: query_with_special%3Dchars%20like%26and%20%2A%20and%23%20plus%2Bthis Note: Also usable by fragments, but not suitable for paths
@return [String] a new string containing an encoded query string per the
rules of RFC3986. In particular, query will encode + as %2B and space as %20
# File lib/puppet/util.rb 359 def uri_query_encode(query_string) 360 return nil if query_string.nil? 361 362 # query can encode space to %20 OR + 363 # + MUST be encoded as %2B 364 # in RFC3968 both query and fragment are defined as: 365 # = *( pchar / "/" / "?" ) 366 # CGI.escape turns space into + which is the most backward compatible 367 # however it doesn't roundtrip through URI.unescape which prefers %20 368 CGI.escape(query_string).gsub('+', '%20') 369 end
Get the path component of a URI
# File lib/puppet/util.rb 316 def uri_to_path(uri) 317 return unless uri.is_a?(URI) 318 319 # CGI.unescape doesn't handle space rules properly in uri paths 320 # URI.unescape does, but returns strings in their original encoding 321 path = uri_unescape(uri.path.encode(Encoding::UTF_8)) 322 323 if Puppet::Util::Platform.windows? && uri.scheme == 'file' 324 if uri.host && !uri.host.empty? 325 path = "//#{uri.host}" + path # UNC 326 else 327 path.sub!(/^\//, '') 328 end 329 end 330 331 path 332 end
# File lib/puppet/util.rb 466 def uri_unescape(str) 467 enc = str.encoding 468 enc = Encoding::UTF_8 if enc == Encoding::US_ASCII 469 str.gsub(ESCAPED) { [$&[1, 2]].pack('H2').force_encoding(enc) } 470 end
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.
# File lib/puppet/util.rb 207 def which(bin) 208 if absolute_path?(bin) 209 return bin if FileTest.file? bin and FileTest.executable? bin 210 else 211 exts = ENV['PATHEXT'] 212 exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] 213 ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| 214 begin 215 dest = File.expand_path(File.join(dir, bin)) 216 rescue ArgumentError => e 217 # if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get 218 # an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error 219 # was thrown. 220 if e.to_s =~ /HOME/ and (ENV['HOME'].nil? || ENV['HOME'] == "") 221 # if we get here they have a tilde in their PATH. We'll issue a single warning about this and then 222 # ignore this path element and carry on with our lives. 223 #TRANSLATORS PATH and HOME are environment variables and should not be translated 224 Puppet::Util::Warnings.warnonce(_("PATH contains a ~ character, and HOME is not set; ignoring PATH element '%{dir}'.") % { dir: dir }) 225 elsif e.to_s =~ /doesn't exist|can't find user/ 226 # ...otherwise, we just skip the non-existent entry, and do nothing. 227 #TRANSLATORS PATH is an environment variable and should not be translated 228 Puppet::Util::Warnings.warnonce(_("Couldn't expand PATH containing a ~ character; ignoring PATH element '%{dir}'.") % { dir: dir }) 229 else 230 raise 231 end 232 else 233 if Puppet::Util::Platform.windows? && File.extname(dest).empty? 234 exts.each do |ext| 235 destext = File.expand_path(dest + ext) 236 return destext if FileTest.file? destext and FileTest.executable? destext 237 end 238 end 239 return dest if FileTest.file? dest and FileTest.executable? dest 240 end 241 end 242 end 243 nil 244 end
Run some code with a specific environment. Resets the environment back to what it was at the end of the code.
@param hash [Hash{String,Symbol => String}] Environment variables to override the current environment. @param mode [Symbol] ignored
# File lib/puppet/util.rb 83 def withenv(hash, mode = :posix) 84 saved = ENV.to_hash 85 begin 86 ENV.merge!(hash.transform_keys(&:to_s)) 87 yield 88 ensure 89 ENV.replace(saved) 90 end 91 end