diff options
author | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
---|---|---|
committer | Seth Chisamore <schisamo@opscode.com> | 2012-10-30 10:39:35 -0400 |
commit | 24dc69a9a97e82a6e4207de68d6dcc664178249b (patch) | |
tree | 19bb289c9f88b4bbab066bc56b95d6d222fd5c35 /lib/chef/mixin | |
parent | 9348c1c9c80ee757354d624b7dc1b78ebc7605c4 (diff) | |
download | chef-24dc69a9a97e82a6e4207de68d6dcc664178249b.tar.gz |
[OC-3564] move core Chef to the repo root \o/ \m/
The opscode/chef repository now only contains the core Chef library code
used by chef-client, knife and chef-solo!
Diffstat (limited to 'lib/chef/mixin')
24 files changed, 2273 insertions, 0 deletions
diff --git a/lib/chef/mixin/check_helper.rb b/lib/chef/mixin/check_helper.rb new file mode 100644 index 0000000000..b3a7835e09 --- /dev/null +++ b/lib/chef/mixin/check_helper.rb @@ -0,0 +1,31 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + module Mixin + module CheckHelper + def set_if_args(thing, arguments) + raise ArgumentError, "Must call set_if_args with a block!" unless Kernel.block_given? + if arguments != nil + yield(arguments) + else + thing + end + end + end + end +end diff --git a/lib/chef/mixin/checksum.rb b/lib/chef/mixin/checksum.rb new file mode 100644 index 0000000000..7b716b6285 --- /dev/null +++ b/lib/chef/mixin/checksum.rb @@ -0,0 +1,32 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'digest/sha2' +require 'chef/checksum_cache' + +class Chef + module Mixin + module Checksum + + def checksum(file) + Chef::ChecksumCache.checksum_for_file(file) + end + + end + end +end diff --git a/lib/chef/mixin/command.rb b/lib/chef/mixin/command.rb new file mode 100644 index 0000000000..55c028ff5f --- /dev/null +++ b/lib/chef/mixin/command.rb @@ -0,0 +1,164 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/log' +require 'chef/exceptions' +require 'tmpdir' +require 'fcntl' +require 'etc' + +class Chef + module Mixin + module Command + extend self + + # NOTE: run_command is deprecated in favor of using Chef::Shellout which now comes from the mixlib-shellout gem. NOTE # + + if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'chef/mixin/command/windows' + include ::Chef::Mixin::Command::Windows + extend ::Chef::Mixin::Command::Windows + else + require 'chef/mixin/command/unix' + include ::Chef::Mixin::Command::Unix + extend ::Chef::Mixin::Command::Unix + end + + # === Parameters + # args<Hash>: A number of required and optional arguments + # command<String>, <Array>: A complete command with options to execute or a command and options as an Array + # creates<String>: The absolute path to a file that prevents the command from running if it exists + # cwd<String>: Working directory to execute command in, defaults to Dir.tmpdir + # timeout<String>: How many seconds to wait for the command to execute before timing out + # returns<String>: The single exit value command is expected to return, otherwise causes an exception + # ignore_failure<Boolean>: Whether to raise an exception on failure, or just return the status + # output_on_failure<Boolean>: Return output in raised exception regardless of Log.level + # + # user<String>: The UID or user name of the user to execute the command as + # group<String>: The GID or group name of the group to execute the command as + # environment<Hash>: Pairs of environment variable names and their values to set before execution + # + # === Returns + # Returns the exit status of args[:command] + def run_command(args={}) + command_output = "" + + args[:ignore_failure] ||= false + args[:output_on_failure] ||= false + + # TODO: This is the wrong place for this responsibility. + if args.has_key?(:creates) + if File.exists?(args[:creates]) + Chef::Log.debug("Skipping #{args[:command]} - creates #{args[:creates]} exists.") + return false + end + end + + status, stdout, stderr = output_of_command(args[:command], args) + command_output << "STDOUT: #{stdout}" + command_output << "STDERR: #{stderr}" + handle_command_failures(status, command_output, args) + + status + end + + def output_of_command(command, args) + Chef::Log.debug("Executing #{command}") + stderr_string, stdout_string, status = "", "", nil + + exec_processing_block = lambda do |pid, stdin, stdout, stderr| + stdout_string, stderr_string = stdout.string.chomp, stderr.string.chomp + end + + args[:cwd] ||= Dir.tmpdir + unless ::File.directory?(args[:cwd]) + raise Chef::Exceptions::Exec, "#{args[:cwd]} does not exist or is not a directory" + end + + Dir.chdir(args[:cwd]) do + if args[:timeout] + begin + Timeout.timeout(args[:timeout]) do + status = popen4(command, args, &exec_processing_block) + end + rescue Timeout::Error => e + Chef::Log.error("#{command} exceeded timeout #{args[:timeout]}") + raise(e) + end + else + status = popen4(command, args, &exec_processing_block) + end + + Chef::Log.debug("---- Begin output of #{command} ----") + Chef::Log.debug("STDOUT: #{stdout_string}") + Chef::Log.debug("STDERR: #{stderr_string}") + Chef::Log.debug("---- End output of #{command} ----") + Chef::Log.debug("Ran #{command} returned #{status.exitstatus}") + end + + return status, stdout_string, stderr_string + end + + def handle_command_failures(status, command_output, opts={}) + unless opts[:ignore_failure] + opts[:returns] ||= 0 + unless Array(opts[:returns]).include?(status.exitstatus) + # if the log level is not debug, through output of command when we fail + output = "" + if Chef::Log.level == :debug || opts[:output_on_failure] + output << "\n---- Begin output of #{opts[:command]} ----\n" + output << command_output.to_s + output << "\n---- End output of #{opts[:command]} ----\n" + end + raise Chef::Exceptions::Exec, "#{opts[:command]} returned #{status.exitstatus}, expected #{opts[:returns]}#{output}" + end + end + end + + # Call #run_command but set LC_ALL to the system's current environment so it doesn't get changed to C. + # + # === Parameters + # args<Hash>: A number of required and optional arguments that will be handed out to #run_command + # + # === Returns + # Returns the result of #run_command + def run_command_with_systems_locale(args={}) + args[:environment] ||= {} + args[:environment]["LC_ALL"] = ENV["LC_ALL"] + run_command args + end + + # def popen4(cmd, args={}, &b) + # @@os_handler.popen4(cmd, args, &b) + # end + + # module_function :popen4 + + def chdir_or_tmpdir(dir, &block) + dir ||= Dir.tmpdir + unless File.directory?(dir) + raise Chef::Exceptions::Exec, "#{dir} does not exist or is not a directory" + end + Dir.chdir(dir) do + block.call + end + end + + end + end +end diff --git a/lib/chef/mixin/command/unix.rb b/lib/chef/mixin/command/unix.rb new file mode 100644 index 0000000000..b63a02663b --- /dev/null +++ b/lib/chef/mixin/command/unix.rb @@ -0,0 +1,220 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + module Mixin + module Command + module Unix + # This is taken directly from Ara T Howard's Open4 library, and then + # modified to suit the needs of Chef. Any bugs here are most likely + # my own, and not Ara's. + # + # The original appears in external/open4.rb in its unmodified form. + # + # Thanks Ara! + def popen4(cmd, args={}, &b) + # Ruby 1.8 suffers from intermittent segfaults believed to be due to GC while IO.select + # See CHEF-2916 / CHEF-1305 + GC.disable + + # Waitlast - this is magic. + # + # Do we wait for the child process to die before we yield + # to the block, or after? That is the magic of waitlast. + # + # By default, we are waiting before we yield the block. + args[:waitlast] ||= false + + args[:user] ||= nil + unless args[:user].kind_of?(Integer) + args[:user] = Etc.getpwnam(args[:user]).uid if args[:user] + end + args[:group] ||= nil + unless args[:group].kind_of?(Integer) + args[:group] = Etc.getgrnam(args[:group]).gid if args[:group] + end + args[:environment] ||= {} + + # Default on C locale so parsing commands output can be done + # independently of the node's default locale. + # "LC_ALL" could be set to nil, in which case we also must ignore it. + unless args[:environment].has_key?("LC_ALL") + args[:environment]["LC_ALL"] = "C" + end + + pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe + + verbose = $VERBOSE + begin + $VERBOSE = nil + ps.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + + cid = fork { + pw.last.close + STDIN.reopen pw.first + pw.first.close + + pr.first.close + STDOUT.reopen pr.last + pr.last.close + + pe.first.close + STDERR.reopen pe.last + pe.last.close + + STDOUT.sync = STDERR.sync = true + + if args[:group] + Process.egid = args[:group] + Process.gid = args[:group] + end + + if args[:user] + Process.euid = args[:user] + Process.uid = args[:user] + end + + args[:environment].each do |key,value| + ENV[key] = value + end + + if args[:umask] + umask = ((args[:umask].respond_to?(:oct) ? args[:umask].oct : args[:umask].to_i) & 007777) + File.umask(umask) + end + + begin + if cmd.kind_of?(Array) + exec(*cmd) + else + exec(cmd) + end + raise 'forty-two' + rescue Exception => e + Marshal.dump(e, ps.last) + ps.last.flush + end + ps.last.close unless (ps.last.closed?) + exit! + } + ensure + $VERBOSE = verbose + end + + [pw.first, pr.last, pe.last, ps.last].each{|fd| fd.close} + + begin + e = Marshal.load ps.first + raise(Exception === e ? e : "unknown failure!") + rescue EOFError # If we get an EOF error, then the exec was successful + 42 + ensure + ps.first.close + end + + pw.last.sync = true + + pi = [pw.last, pr.first, pe.first] + + if b + begin + if args[:waitlast] + b[cid, *pi] + # send EOF so that if the child process is reading from STDIN + # it will actually finish up and exit + pi[0].close_write + Process.waitpid2(cid).last + else + # This took some doing. + # The trick here is to close STDIN + # Then set our end of the childs pipes to be O_NONBLOCK + # Then wait for the child to die, which means any IO it + # wants to do must be done - it's dead. If it isn't, + # it's because something totally skanky is happening, + # and we don't care. + o = StringIO.new + e = StringIO.new + + pi[0].close + + stdout = pi[1] + stderr = pi[2] + + stdout.sync = true + stderr.sync = true + + stdout.fcntl(Fcntl::F_SETFL, pi[1].fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK) + stderr.fcntl(Fcntl::F_SETFL, pi[2].fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK) + + stdout_finished = false + stderr_finished = false + + results = nil + + while !stdout_finished || !stderr_finished + begin + channels_to_watch = [] + channels_to_watch << stdout if !stdout_finished + channels_to_watch << stderr if !stderr_finished + ready = IO.select(channels_to_watch, nil, nil, 1.0) + rescue Errno::EAGAIN + ensure + results = Process.waitpid2(cid, Process::WNOHANG) + if results + stdout_finished = true + stderr_finished = true + end + end + + if ready && ready.first.include?(stdout) + line = results ? stdout.gets(nil) : stdout.gets + if line + o.write(line) + else + stdout_finished = true + end + end + if ready && ready.first.include?(stderr) + line = results ? stderr.gets(nil) : stderr.gets + if line + e.write(line) + else + stderr_finished = true + end + end + end + results = Process.waitpid2(cid) unless results + o.rewind + e.rewind + b[cid, pi[0], o, e] + results.last + end + ensure + pi.each{|fd| fd.close unless fd.closed?} + end + else + [cid, pw.last, pr.first, pe.first] + end + ensure + GC.enable + end + + end + end + end +end diff --git a/lib/chef/mixin/command/windows.rb b/lib/chef/mixin/command/windows.rb new file mode 100644 index 0000000000..e3d0cfdb18 --- /dev/null +++ b/lib/chef/mixin/command/windows.rb @@ -0,0 +1,76 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: Doug MacEachern (<dougm@vmware.com>) +# Copyright:: Copyright (c) 2010 VMware, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +if RUBY_VERSION =~ /^1\.8/ + require 'win32/open3' +else + require 'open3' +end + +class Chef + module Mixin + module Command + module Windows + def popen4(cmd, args={}, &b) + + # By default, we are waiting before we yield the block. + args[:waitlast] ||= false + + #XXX :user, :group, :environment support? + + Open3.popen3(cmd) do |stdin,stdout,stderr,cid| + if b + if args[:waitlast] + b[cid, stdin, stdout, stderr] + # send EOF so that if the child process is reading from STDIN + # it will actually finish up and exit + stdin.close_write + else + o = StringIO.new + e = StringIO.new + + stdin.close + + stdout.sync = true + stderr.sync = true + + line = stdout.gets(nil) + if line + o.write(line) + end + line = stderr.gets(nil) + if line + e.write(line) + end + o.rewind + e.rewind + b[cid, stdin, o, e] + end + else + [cid, stdin, stdout, stderr] + end + end + $? + end + + end + end + end +end diff --git a/lib/chef/mixin/convert_to_class_name.rb b/lib/chef/mixin/convert_to_class_name.rb new file mode 100644 index 0000000000..7b4ec7ad3f --- /dev/null +++ b/lib/chef/mixin/convert_to_class_name.rb @@ -0,0 +1,65 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + module Mixin + module ConvertToClassName + extend self + + def convert_to_class_name(str) + str = str.dup + str.gsub!(/[^A-Za-z0-9_]/,'_') + rname = nil + regexp = %r{^(.+?)(_(.+))?$} + + mn = str.match(regexp) + if mn + rname = mn[1].capitalize + + while mn && mn[3] + mn = mn[3].match(regexp) + rname << mn[1].capitalize if mn + end + end + + rname + end + + def convert_to_snake_case(str, namespace=nil) + str = str.dup + str.sub!(/^#{namespace}(\:\:)?/, '') if namespace + str.gsub!(/[A-Z]/) {|s| "_" + s} + str.downcase! + str.sub!(/^\_/, "") + str + end + + def snake_case_basename(str) + with_namespace = convert_to_snake_case(str) + with_namespace.split("::").last.sub(/^_/, '') + end + + def filename_to_qualified_string(base, filename) + file_base = File.basename(filename, ".rb") + base.to_s + (file_base == 'default' ? '' : "_#{file_base}") + end + + end + end +end diff --git a/lib/chef/mixin/create_path.rb b/lib/chef/mixin/create_path.rb new file mode 100644 index 0000000000..9b5dba14f2 --- /dev/null +++ b/lib/chef/mixin/create_path.rb @@ -0,0 +1,57 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + module Mixin + module CreatePath + + # Creates a given path, including all directories that lead up to it. + # Like mkdir_p, but without the leaking. + # + # === Parameters + # file_path<String, Array>:: A string that represents the path to create, + # or an Array with the path-parts. + # + # === Returns + # The created file_path. + def create_path(file_path) + unless file_path.kind_of?(String) || file_path.kind_of?(Array) + raise ArgumentError, "file_path must be a string or an array!" + end + + if file_path.kind_of?(String) + file_path = File.expand_path(file_path).split(File::SEPARATOR) + file_path.shift if file_path[0] == '' + # Check if path starts with a separator or drive letter (Windows) + unless file_path[0].match("^#{File::SEPARATOR}|^[a-zA-Z]:") + file_path[0] = "#{File::SEPARATOR}#{file_path[0]}" + end + end + + file_path.each_index do |i| + create_path = File.join(file_path[0, i + 1]) + unless File.directory?(create_path) + Chef::Log.debug("Creating directory #{create_path}") + Dir.mkdir(create_path) + end + end + File.expand_path(File.join(file_path)) + end + + end + end +end diff --git a/lib/chef/mixin/deep_merge.rb b/lib/chef/mixin/deep_merge.rb new file mode 100644 index 0000000000..c5bbc8d9e6 --- /dev/null +++ b/lib/chef/mixin/deep_merge.rb @@ -0,0 +1,142 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Steve Midgley (http://www.misuse.org/science) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# Copyright:: Copyright (c) 2008 Steve Midgley +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + module Mixin + # == Chef::Mixin::DeepMerge + # Implements a deep merging algorithm for nested data structures. + # ==== Notice: + # This code was originally imported from deep_merge by Steve Midgley. + # deep_merge is available under the MIT license from + # http://trac.misuse.org/science/wiki/DeepMerge + module DeepMerge + + class InvalidSubtractiveMerge < ArgumentError; end + + + OLD_KNOCKOUT_PREFIX = "!merge:".freeze + + # Regex to match the "knockout prefix" that was used to indicate + # subtractive merging in Chef 10.x and previous. Subtractive merging is + # removed as of Chef 11, but we detect attempted use of it and raise an + # error (see: raise_if_knockout_used!) + OLD_KNOCKOUT_MATCH = %r[!merge].freeze + + extend self + + def merge(first, second) + first = Mash.new(first) unless first.kind_of?(Mash) + second = Mash.new(second) unless second.kind_of?(Mash) + + DeepMerge.deep_merge(second, first) + end + + # Inherited roles use the knockout_prefix array subtraction functionality + # This is likely to go away in Chef >= 0.11 + def role_merge(first, second) + first = Mash.new(first) unless first.kind_of?(Mash) + second = Mash.new(second) unless second.kind_of?(Mash) + + DeepMerge.deep_merge(second, first) + end + + class InvalidParameter < StandardError; end + + # Deep Merge core documentation. + # deep_merge! method permits merging of arbitrary child elements. The two top level + # elements must be hashes. These hashes can contain unlimited (to stack limit) levels + # of child elements. These child elements to not have to be of the same types. + # Where child elements are of the same type, deep_merge will attempt to merge them together. + # Where child elements are not of the same type, deep_merge will skip or optionally overwrite + # the destination element with the contents of the source element at that level. + # So if you have two hashes like this: + # source = {:x => [1,2,3], :y => 2} + # dest = {:x => [4,5,'6'], :y => [7,8,9]} + # dest.deep_merge!(source) + # Results: {:x => [1,2,3,4,5,'6'], :y => 2} + # By default, "deep_merge!" will overwrite any unmergeables and merge everything else. + # To avoid this, use "deep_merge" (no bang/exclamation mark) + def deep_merge!(source, dest) + # if dest doesn't exist, then simply copy source to it + if dest.nil? + dest = source; return dest + end + + raise_if_knockout_used!(source) + raise_if_knockout_used!(dest) + case source + when nil + dest + when Hash + source.each do |src_key, src_value| + if dest.kind_of?(Hash) + if dest[src_key] + dest[src_key] = deep_merge!(src_value, dest[src_key]) + else # dest[src_key] doesn't exist so we take whatever source has + raise_if_knockout_used!(src_value) + dest[src_key] = src_value + end + else # dest isn't a hash, so we overwrite it completely + dest = source + end + end + when Array + if dest.kind_of?(Array) + dest = dest | source + else + dest = source + end + when String + dest = source + else # src_hash is not an array or hash, so we'll have to overwrite dest + dest = source + end + dest + end # deep_merge! + + # Checks for attempted use of subtractive merge, which was removed for + # Chef 11.0. If subtractive merge use is detected, will raise an + # InvalidSubtractiveMerge exception. + def raise_if_knockout_used!(obj) + if uses_knockout?(obj) + raise InvalidSubtractiveMerge, "subtractive merge with !merge is no longer supported" + end + end + + # Checks for attempted use of subtractive merge in +obj+. + def uses_knockout?(obj) + case obj + when String + obj =~ OLD_KNOCKOUT_MATCH + when Array + obj.any? {|element| element.respond_to?(:gsub) && element =~ OLD_KNOCKOUT_MATCH } + else + false + end + end + + def deep_merge(source, dest) + deep_merge!(source.dup, dest.dup) + end + + end + end +end + + diff --git a/lib/chef/mixin/deprecation.rb b/lib/chef/mixin/deprecation.rb new file mode 100644 index 0000000000..cc85c4e976 --- /dev/null +++ b/lib/chef/mixin/deprecation.rb @@ -0,0 +1,65 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + module Mixin + module Deprecation + class DeprecatedObjectProxyBase + KEEPERS = %w{__id__ __send__ instance_eval == equal? initialize object_id} + instance_methods.each { |method_name| undef_method(method_name) unless KEEPERS.include?(method_name.to_s)} + end + + class DeprecatedInstanceVariable < DeprecatedObjectProxyBase + def initialize(target, ivar_name, level=nil) + @target, @ivar_name = target, ivar_name + @level ||= :warn + end + + def method_missing(method_name, *args, &block) + log_deprecation_msg(caller[0..3]) + @target.send(method_name, *args, &block) + end + + def inspect + @target.inspect + end + + private + + def log_deprecation_msg(*called_from) + called_from = called_from.flatten + log("Accessing #{@ivar_name} by the variable @#{@ivar_name} is deprecated. Support will be removed in a future release.") + log("Please update your cookbooks to use #{@ivar_name} in place of @#{@ivar_name}. Accessed from:") + called_from.each {|l| log(l)} + end + + def log(msg) + # WTF: I don't get the log prefix (i.e., "[timestamp] LEVEL:") if I + # send to Chef::Log. No one but me should use method_missing, ever. + Chef::Log.logger.send(@level, msg) + end + + end + + def deprecated_ivar(obj, name, level=nil) + DeprecatedInstanceVariable.new(obj, name, level) + end + + end + end +end diff --git a/lib/chef/mixin/enforce_ownership_and_permissions.rb b/lib/chef/mixin/enforce_ownership_and_permissions.rb new file mode 100644 index 0000000000..9c1e4dda93 --- /dev/null +++ b/lib/chef/mixin/enforce_ownership_and_permissions.rb @@ -0,0 +1,39 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/file_access_control' + +class Chef + module Mixin + module EnforceOwnershipAndPermissions + + def access_controls + @access_controls ||= Chef::FileAccessControl.new(current_resource, new_resource, self) + end + + # will set the proper user, group and + # permissions using a platform specific + # version of Chef::FileAccessControl + def enforce_ownership_and_permissions + access_controls.set_all + new_resource.updated_by_last_action(true) if access_controls.modified? + end + + end + end +end diff --git a/lib/chef/mixin/file_class.rb b/lib/chef/mixin/file_class.rb new file mode 100644 index 0000000000..ed2cda47db --- /dev/null +++ b/lib/chef/mixin/file_class.rb @@ -0,0 +1,46 @@ +# +# Author:: Mark Mzyk <mmzyk@opscode.com> +# Author:: Seth Chisamore <schisamo@opscode.com> +# Author:: Bryan McLellan <btm@opscode.com> +# Copyright:: Copyright (c) 2011-2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + module Mixin + module FileClass + + def file_class + @host_os_file ||= if Chef::Platform.windows? + require 'chef/win32/file' + begin + Chef::ReservedNames::Win32::File.verify_links_supported! + rescue Chef::Exceptions::Win32APIFunctionNotImplemented => e + message = "Link resource is not supported on this version of Windows" + message << ": #{node[:kernel][:name]}" if node + message << " (#{node[:platform_version]})" if node + Chef::Log.fatal(message) + raise e + end + Chef::ReservedNames::Win32::File + else + ::File + end + end + end + end +end + + diff --git a/lib/chef/mixin/from_file.rb b/lib/chef/mixin/from_file.rb new file mode 100644 index 0000000000..609fe1de55 --- /dev/null +++ b/lib/chef/mixin/from_file.rb @@ -0,0 +1,50 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + module Mixin + module FromFile + + # Loads a given ruby file, and runs instance_eval against it in the context of the current + # object. + # + # Raises an IOError if the file cannot be found, or is not readable. + def from_file(filename) + if File.exists?(filename) && File.readable?(filename) + self.instance_eval(IO.read(filename), filename, 1) + else + raise IOError, "Cannot open or read #{filename}!" + end + end + + # Loads a given ruby file, and runs class_eval against it in the context of the current + # object. + # + # Raises an IOError if the file cannot be found, or is not readable. + def class_from_file(filename) + if File.exists?(filename) && File.readable?(filename) + self.class_eval(IO.read(filename), filename, 1) + else + raise IOError, "Cannot open or read #{filename}!" + end + end + + end + end +end diff --git a/lib/chef/mixin/get_source_from_package.rb b/lib/chef/mixin/get_source_from_package.rb new file mode 100644 index 0000000000..6d5cb56a27 --- /dev/null +++ b/lib/chef/mixin/get_source_from_package.rb @@ -0,0 +1,42 @@ +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# +# mixin to make this syntax work without specifying a source: +# +# gem_pacakge "/tmp/foo-x.y.z.gem" +# rpm_package "/tmp/foo-x.y-z.rpm" +# dpkg_package "/tmp/foo-x.y.z.deb" +# + +class Chef + module Mixin + module GetSourceFromPackage + def initialize(new_resource, run_context) + super + # if we're passed something that looks like a filesystem path, with no source, use it + # - require at least one '/' in the path to avoid gem_package "foo" breaking if a file named 'foo' exists in the cwd + if new_resource.source.nil? && new_resource.package_name.match(/#{::File::SEPARATOR}/) && ::File.exists?(new_resource.package_name) + Chef::Log.debug("No package source specified, but #{new_resource.package_name} exists on the filesystem, copying to package source") + new_resource.source(@new_resource.package_name) + end + end + end + end +end + diff --git a/lib/chef/mixin/language.rb b/lib/chef/mixin/language.rb new file mode 100644 index 0000000000..3aa6a6d800 --- /dev/null +++ b/lib/chef/mixin/language.rb @@ -0,0 +1,36 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/dsl/platform_introspection' +require 'chef/dsl/data_query' + +class Chef + module Mixin + + # == [DEPRECATED] Chef::Mixin::Language + # This module is deprecated and remains only for backwards compatibility. + # + # See Chef::DSL::PlatformIntrospection and Chef::DSL::DataQuery + module Language + + include Chef::DSL::PlatformIntrospection + include Chef::DSL::DataQuery + + end + end +end diff --git a/lib/chef/mixin/language_include_attribute.rb b/lib/chef/mixin/language_include_attribute.rb new file mode 100644 index 0000000000..283773b25d --- /dev/null +++ b/lib/chef/mixin/language_include_attribute.rb @@ -0,0 +1,29 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/dsl/include_attribute' + +class Chef + module Mixin + + # DEPRECATED: This is just here for compatibility, use + # Chef::DSL::IncludeAttribute instead. + LanguageIncludeAttribute = Chef::DSL::IncludeAttribute + end +end + diff --git a/lib/chef/mixin/language_include_recipe.rb b/lib/chef/mixin/language_include_recipe.rb new file mode 100644 index 0000000000..0566046560 --- /dev/null +++ b/lib/chef/mixin/language_include_recipe.rb @@ -0,0 +1,26 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/dsl/include_recipe' + +class Chef + module Mixin + LanguageIncludeRecipe = Chef::DSL::IncludeRecipe + end +end + diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb new file mode 100644 index 0000000000..649224f978 --- /dev/null +++ b/lib/chef/mixin/params_validate.rb @@ -0,0 +1,225 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + + module Mixin + module ParamsValidate + + # Takes a hash of options, along with a map to validate them. Returns the original + # options hash, plus any changes that might have been made (through things like setting + # default values in the validation map) + # + # For example: + # + # validate({ :one => "neat" }, { :one => { :kind_of => String }}) + # + # Would raise an exception if the value of :one above is not a kind_of? string. Valid + # map options are: + # + # :default:: Sets the default value for this parameter. + # :callbacks:: Takes a hash of Procs, which should return true if the argument is valid. + # The key will be inserted into the error message if the Proc does not return true: + # "Option #{key}'s value #{value} #{message}!" + # :kind_of:: Ensure that the value is a kind_of?(Whatever). If passed an array, it will ensure + # that the value is one of those types. + # :respond_to:: Ensure that the value has a given method. Takes one method name or an array of + # method names. + # :required:: Raise an exception if this parameter is missing. Valid values are true or false, + # by default, options are not required. + # :regex:: Match the value of the paramater against a regular expression. + # :equal_to:: Match the value of the paramater with ==. An array means it can be equal to any + # of the values. + def validate(opts, map) + #-- + # validate works by taking the keys in the validation map, assuming it's a hash, and + # looking for _pv_:symbol as methods. Assuming it find them, it calls the right + # one. + #++ + raise ArgumentError, "Options must be a hash" unless opts.kind_of?(Hash) + raise ArgumentError, "Validation Map must be a hash" unless map.kind_of?(Hash) + + map.each do |key, validation| + unless key.kind_of?(Symbol) || key.kind_of?(String) + raise ArgumentError, "Validation map keys must be symbols or strings!" + end + case validation + when true + _pv_required(opts, key) + when false + true + when Hash + validation.each do |check, carg| + check_method = "_pv_#{check.to_s}" + if self.respond_to?(check_method, true) + self.send(check_method, opts, key, carg) + else + raise ArgumentError, "Validation map has unknown check: #{check}" + end + end + end + end + opts + end + + def set_or_return(symbol, arg, validation) + iv_symbol = "@#{symbol.to_s}".to_sym + map = { + symbol => validation + } + + if arg == nil && self.instance_variable_defined?(iv_symbol) == true + self.instance_variable_get(iv_symbol) + else + opts = validate({ symbol => arg }, { symbol => validation }) + self.instance_variable_set(iv_symbol, opts[symbol]) + end + end + + private + + # Return the value of a parameter, or nil if it doesn't exist. + def _pv_opts_lookup(opts, key) + if opts.has_key?(key.to_s) + opts[key.to_s] + elsif opts.has_key?(key.to_sym) + opts[key.to_sym] + else + nil + end + end + + # Raise an exception if the parameter is not found. + def _pv_required(opts, key, is_required=true) + if is_required + if (opts.has_key?(key.to_s) && !opts[key.to_s].nil?) || + (opts.has_key?(key.to_sym) && !opts[key.to_sym].nil?) + true + else + raise Exceptions::ValidationFailed, "Required argument #{key} is missing!" + end + end + end + + def _pv_equal_to(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + passes = false + Array(to_be).each do |tb| + passes = true if value == tb + end + unless passes + raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}." + end + end + end + + # Raise an exception if the parameter is not a kind_of?(to_be) + def _pv_kind_of(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + passes = false + Array(to_be).each do |tb| + passes = true if value.kind_of?(tb) + end + unless passes + raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}." + end + end + end + + # Raise an exception if the parameter does not respond to a given set of methods. + def _pv_respond_to(opts, key, method_name_list) + value = _pv_opts_lookup(opts, key) + unless value.nil? + Array(method_name_list).each do |method_name| + unless value.respond_to?(method_name) + raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!" + end + end + end + end + + # Assert that parameter returns false when passed a predicate method. + # For example, :cannot_be => :blank will raise a Exceptions::ValidationFailed + # error value.blank? returns a 'truthy' (not nil or false) value. + # + # Note, this will *PASS* if the object doesn't respond to the method. + # So, to make sure a value is not nil and not blank, you need to do + # both :cannot_be => :blank *and* :cannot_be => :nil (or :required => true) + def _pv_cannot_be(opts, key, predicate_method_base_name) + value = _pv_opts_lookup(opts, key) + predicate_method = (predicate_method_base_name.to_s + "?").to_sym + + if value.respond_to?(predicate_method) + if value.send(predicate_method) + raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" + end + end + end + + # Assign a default value to a parameter. + def _pv_default(opts, key, default_value) + value = _pv_opts_lookup(opts, key) + if value == nil + opts[key] = default_value + end + end + + # Check a parameter against a regular expression. + def _pv_regex(opts, key, regex) + value = _pv_opts_lookup(opts, key) + if value != nil + passes = false + [ regex ].flatten.each do |r| + if value != nil + if r.match(value.to_s) + passes = true + end + end + end + unless passes + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}" + end + end + end + + # Check a parameter against a hash of proc's. + def _pv_callbacks(opts, key, callbacks) + raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash) + value = _pv_opts_lookup(opts, key) + if value != nil + callbacks.each do |message, zeproc| + if zeproc.call(value) != true + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!" + end + end + end + end + + # Allow a parameter to default to @name + def _pv_name_attribute(opts, key, is_name_attribute=true) + if is_name_attribute + if opts[key] == nil + opts[key] = self.instance_variable_get("@name") + end + end + end + end + end +end + diff --git a/lib/chef/mixin/path_sanity.rb b/lib/chef/mixin/path_sanity.rb new file mode 100644 index 0000000000..1d324f54e9 --- /dev/null +++ b/lib/chef/mixin/path_sanity.rb @@ -0,0 +1,67 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + module Mixin + module PathSanity + + def enforce_path_sanity(env=ENV) + if Chef::Config[:enforce_path_sanity] + path_separator = Chef::Platform.windows? ? ';' : ':' + existing_paths = env["PATH"].split(path_separator) + # ensure the Ruby and Gem bindirs are included + # mainly for 'full-stack' Chef installs + paths_to_add = [] + paths_to_add << ruby_bindir unless sane_paths.include?(ruby_bindir) + paths_to_add << gem_bindir unless sane_paths.include?(gem_bindir) + paths_to_add << sane_paths if sane_paths + paths_to_add.flatten!.compact! + paths_to_add.each do |sane_path| + unless existing_paths.include?(sane_path) + env_path = env["PATH"].dup + env_path << path_separator unless env["PATH"].empty? + env_path << sane_path + env["PATH"] = env_path + end + end + end + end + + private + + def sane_paths + @sane_paths ||= begin + if Chef::Platform.windows? + %w[] + else + %w[/usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin] + end + end + end + + def ruby_bindir + RbConfig::CONFIG['bindir'] + end + + def gem_bindir + Gem.bindir + end + + end + end +end diff --git a/lib/chef/mixin/recipe_definition_dsl_core.rb b/lib/chef/mixin/recipe_definition_dsl_core.rb new file mode 100644 index 0000000000..ff422d892f --- /dev/null +++ b/lib/chef/mixin/recipe_definition_dsl_core.rb @@ -0,0 +1,33 @@ +#-- +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2008, 2009 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +### +# NOTE: This file and constant are here only for backwards compatibility. +# New code should use Chef::DSL::Recipe instead. +# +# This constant (module name) will eventually be deprecated and then removed. +### + +require 'chef/dsl/recipe' + +class Chef + module Mixin + RecipeDefinitionDSLCore = Chef::DSL::Recipe + end +end diff --git a/lib/chef/mixin/securable.rb b/lib/chef/mixin/securable.rb new file mode 100644 index 0000000000..47c388b239 --- /dev/null +++ b/lib/chef/mixin/securable.rb @@ -0,0 +1,180 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + module Mixin + module Securable + + def owner(arg=nil) + set_or_return( + :owner, + arg, + :regex => Chef::Config[:user_valid_regex] + ) + end + + alias :user :owner + + def group(arg=nil) + set_or_return( + :group, + arg, + :regex => Chef::Config[:group_valid_regex] + ) + end + + def mode(arg=nil) + set_or_return( + :mode, + arg, + :callbacks => { + "not in valid numeric range" => lambda { |m| + if m.kind_of?(String) + m =~ /^0/ || m="0#{m}" + end + + # Windows does not support the sticky or setuid bits + if Chef::Platform.windows? + Integer(m)<=0777 && Integer(m)>=0 + else + Integer(m)<=07777 && Integer(m)>=0 + end + }, + } + ) + end + + # TODO should this be separated into different files? + if RUBY_PLATFORM =~ /mswin|mingw|windows/ + + # === rights_attribute + # "meta-method" for dynamically creating rights attributes on resources. + # + # Multiple rights attributes can be declared. This enables resources to + # have multiple rights attributes with separate runtime states. + # + # For example, +Chef::Resource::RemoteDirectory+ supports different + # rights on the directories and files by declaring separate rights + # attributes for each (rights and files_rights). + # + # ==== User Level API + # Given a resource that calls + # + # rights_attribute(:rights) + # + # Then the resource DSL could be used like this: + # + # rights :read, ["Administrators","Everyone"] + # rights :deny, "Pinky" + # rights :full_control, "Users", :applies_to_children => true + # rights :write, "John Keiser", :applies_to_children => :containers_only, :applies_to_self => false, :one_level_deep => true + # + # ==== Internal Data Structure + # rights attributes support multiple right declarations + # in a single resource block--the data will be merged + # into a single internal hash. + # + # The internal representation is a hash with the following keys: + # + # * `:permissions`: Integer of Windows permissions flags, 1..2^32 + # or one of `[:full_control, :modify, :read_execute, :read, :write]` + # * `:principals`: String or Array of Strings represnting usernames on + # the system. + # * `:applies_to_children` (optional): Boolean + # * `:applies_to_self` (optional): Boolean + # * `:one_level_deep` (optional): Boolean + # + def self.rights_attribute(name) + + # equivalent to something like: + # def rights(permissions=nil, principals=nil, args_hash=nil) + define_method(name) do |*args| + # Ruby 1.8 compat: default the arguments + permissions = args.length >= 1 ? args[0] : nil + principals = args.length >= 2 ? args[1] : nil + args_hash = args.length >= 3 ? args[2] : nil + raise ArgumentError.new("wrong number of arguments (#{args.length} for 3)") if args.length >= 4 + + rights = self.instance_variable_get("@#{name.to_s}".to_sym) + unless permissions.nil? + input = { + :permissions => permissions, + :principals => principals + } + input.merge!(args_hash) unless args_hash.nil? + + validations = {:permissions => { :required => true }, + :principals => { :required => true, :kind_of => [String, Array] }, + :applies_to_children => { :equal_to => [ true, false, :containers_only, :objects_only ]}, + :applies_to_self => { :kind_of => [ TrueClass, FalseClass ] }, + :one_level_deep => { :kind_of => [ TrueClass, FalseClass ] } + } + validate(input, validations) + + [ permissions ].flatten.each do |permission| + if permission.is_a?(Integer) + if permission < 0 || permission > 1<<32 + raise ArgumentError, "permissions flags must be positive and <= 32 bits (#{permission})" + end + elsif !([:full_control, :modify, :read_execute, :read, :write].include?(permission.to_sym)) + raise ArgumentError, "permissions parameter must be :full_control, :modify, :read_execute, :read, :write or an integer representing Windows permission flags" + end + end + + [ principals ].flatten.each do |principal| + if !principal.is_a?(String) + raise ArgumentError, "principals parameter must be a string or array of strings representing usernames" + end + end + + if input[:applies_to_children] == false + if input[:applies_to_self] == false + raise ArgumentError, "'rights' attribute must specify either :applies_to_children or :applies_to_self." + end + if input[:one_level_deep] == true + raise ArgumentError, "'rights' attribute specified :one_level_deep without specifying :applies_to_children." + end + end + rights ||= [] + rights << input + end + set_or_return( + name, + rights, + {} + ) + end + end + + # create a default 'rights' attribute + rights_attribute(:rights) + rights_attribute(:deny_rights) + + def inherits(arg=nil) + set_or_return( + :inherits, + arg, + :kind_of => [ TrueClass, FalseClass ] + ) + end + + end # Windows-specific + + end + end +end diff --git a/lib/chef/mixin/shell_out.rb b/lib/chef/mixin/shell_out.rb new file mode 100644 index 0000000000..4eaa509f8b --- /dev/null +++ b/lib/chef/mixin/shell_out.rb @@ -0,0 +1,69 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'chef/shell_out' +require 'chef/config' + +class Chef + module Mixin + module ShellOut + + def shell_out(*command_args) + cmd = Mixlib::ShellOut.new(*run_command_compatible_options(command_args)) + if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.debug? + cmd.live_stream = STDOUT + end + cmd.run_command + cmd + end + + def shell_out!(*command_args) + cmd= shell_out(*command_args) + cmd.error! + cmd + end + + DEPRECATED_OPTIONS = + [ [:command_log_level, :log_level], + [:command_log_prepend, :log_tag] ] + + # CHEF-3090: Deprecate command_log_level and command_log_prepend + # Patterned after https://github.com/opscode/chef/commit/e1509990b559984b43e428d4d801c394e970f432 + def run_command_compatible_options(command_args) + return command_args unless command_args.last.is_a?(Hash) + + _command_args = command_args.dup + _options = _command_args.last + + DEPRECATED_OPTIONS.each do |old_option, new_option| + # Edge case: someone specifies :command_log_level and 'command_log_level' in the option hash + next unless value = _options.delete(old_option) || _options.delete(old_option.to_s) + deprecate_option old_option, new_option + _options[new_option] = value + end + + return _command_args + end + + private + + def deprecate_option(old_option, new_option) + Chef::Log.logger.warn "DEPRECATION: Chef::Mixin::ShellOut option :#{old_option} is deprecated. Use :#{new_option}" + end + end + end +end diff --git a/lib/chef/mixin/template.rb b/lib/chef/mixin/template.rb new file mode 100644 index 0000000000..78148d2577 --- /dev/null +++ b/lib/chef/mixin/template.rb @@ -0,0 +1,100 @@ +#-- +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'tempfile' +require 'erubis' + +class Chef + module Mixin + module Template + + module ChefContext + def node + return @node if @node + raise "Could not find a value for node. If you are explicitly setting variables in a template, " + + "include a node variable if you plan to use it." + end + end + + ::Erubis::Context.send(:include, ChefContext) + + # Render a template with Erubis. Takes a template as a string, and a + # context hash. + def render_template(template, context) + begin + eruby = Erubis::Eruby.new(template) + output = eruby.evaluate(context) + rescue Object => e + raise TemplateError.new(e, template, context) + end + Tempfile.open("chef-rendered-template") do |tempfile| + tempfile.print(output) + tempfile.close + yield tempfile + end + end + + class TemplateError < RuntimeError + attr_reader :original_exception, :context + SOURCE_CONTEXT_WINDOW = 2 + + def initialize(original_exception, template, context) + @original_exception, @template, @context = original_exception, template, context + end + + def message + @original_exception.message + end + + def line_number + @line_number ||= $1.to_i if original_exception.backtrace.find {|line| line =~ /\(erubis\):(\d+)/ } + end + + def source_location + "on line ##{line_number}" + end + + def source_listing + @source_listing ||= begin + lines = @template.split(/\n/) + if line_number + line_index = line_number - 1 + beginning_line = line_index <= SOURCE_CONTEXT_WINDOW ? 0 : line_index - SOURCE_CONTEXT_WINDOW + source_size = SOURCE_CONTEXT_WINDOW * 2 + 1 + else + beginning_line = 0 + source_size = lines.length + end + contextual_lines = lines[beginning_line, source_size] + output = [] + contextual_lines.each_with_index do |line, index| + line_number = (index+beginning_line+1).to_s.rjust(3) + output << "#{line_number}: #{line}" + end + output.join("\n") + end + end + + def to_s + "\n\n#{self.class} (#{message}) #{source_location}:\n\n" + + "#{source_listing}\n\n #{original_exception.backtrace.join("\n ")}\n\n" + end + end + end + end +end diff --git a/lib/chef/mixin/why_run.rb b/lib/chef/mixin/why_run.rb new file mode 100644 index 0000000000..22c58c1e54 --- /dev/null +++ b/lib/chef/mixin/why_run.rb @@ -0,0 +1,339 @@ +# +# Author:: Dan DeLeo ( <dan@opscode.com> ) +# Author:: Marc Paradise ( <marc@opscode.com> ) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + module Mixin + module WhyRun + + # ==ConvergeActions + # ConvergeActions implements the logic for why run. A ConvergeActions + # object wraps a collection of actions, which consist of a descriptive + # string and a block/Proc. Actions are executed by calling #converge! + # When why_run mode is enabled, each action's description will be + # printed, but the block will not be called. Conversely, in normal mode, + # the block is called, but the message is not printed. + # + # In general, this class should be accessed through the API provided by + # Chef::Provider. + class ConvergeActions + attr_reader :actions + + def initialize(resource, run_context, action) + @resource, @run_context = resource, run_context + @actions = [] + end + + def events + @run_context.events + end + + # Adds an action to the list. +descriptions+ can either be an Array of + # Strings, or a single String describing the action; +block+ is a + # block/proc that implements the action. + def add_action(descriptions, &block) + @actions << [descriptions, block] + end + + # True if there are no actions to execute. + def empty? + @actions.empty? + end + + # Iterate over the actions, and either print the action's message, or + # run its code block, depending on whether why_run mode is active. + def converge! + @actions.each do |descriptions, block| + if !Chef::Config[:why_run] + block.call + end + events.resource_update_applied(@resource, @action, descriptions) + end + end + end + + # == ResourceRequirements + # ResourceRequirements provides a framework for making assertions about + # the host system's state. It also provides a mechanism for making + # assumptions about what the system's state might have been when running + # in why run mode. + # + # For example, consider a recipe that consists of a package resource and + # a service resource. If the service's init script is installed by the + # package, and Chef is running in why run mode, then the service resource + # would fail when attempting to run `/etc/init.d/software-name status`. + # In order to provide a more useful approximation of what would happen in + # a real chef run, we want to instead assume that the service was created + # but isn't running. The logic would look like this: + # + # # Hypothetical service provider demonstrating why run assumption logic. + # # This isn't the actual API, it just shows the logic. + # class HypotheticalServiceProvider < Chef::Provider + # + # def load_current_resource + # # Make sure we have the init script available: + # if ::File.exist?("/etc/init.d/some-service" + # # If the init script exists, proceed as normal: + # status_cmd = shell_out("/etc/init.d/some-service status") + # if status_cmd.success? + # @current_resource.status(:running) + # else + # @current_resource.status(:stopped) + # end + # else + # if whyrun_mode? + # # If the init script is not available, and we're in why run mode, + # # assume that some previous action would've created it: + # log("warning: init script '/etc/init.d/some-service' is not available") + # log("warning: assuming that the init script would have been created, assuming the state of 'some-service' is 'stopped'") + # @current_resource.status(:stopped) + # else + # raise "expected init script /etc/init.d/some-service doesn't exist" + # end + # end + # end + # + # end + # + # In short, the code above does the following: + # * runs a test to determine if a requirement is met: + # `::File.exist?("/etc/init.d/some-service"` + # * raises an error if the requirement is not met, and we're not in why + # run mode. + # * if we *are* in why run mode, print a message explaining the + # situation, and run some code that makes an assumption about what the + # state of the system would be. In this case, we also skip the normal + # `load_current_resource` logic + # * when the requirement *is* met, we run the normal `load_current_resource` + # logic + # + # ResourceRequirements encapsulates the above logic in a more declarative API. + # + # === Examples + # Assertions and assumptions should be created through the WhyRun#assert + # method, which gets mixed in to providers. See that method's + # documentation for examples. + class ResourceRequirements + + # Implements the logic for a single assertion/assumption. See the + # documentation for ResourceRequirements for full discussion. + class Assertion + class AssertionFailure < RuntimeError + end + + def initialize + @block_action = false + @assertion_proc = nil + @failure_message = nil + @whyrun_message = nil + @resource_modifier = nil + @assertion_failed = false + @exception_type = AssertionFailure + end + + # Defines the code block that determines if a requirement is met. The + # block should return a truthy value to indicate that the requirement + # is met, and a falsey value if the requirement is not met. + # # in a provider: + # assert(:some_action) do |a| + # # This provider requires the file /tmp/foo to exist: + # a.assertion { ::File.exist?("/tmp/foo") } + # end + def assertion(&assertion_proc) + @assertion_proc = assertion_proc + end + + # Defines the failure message, and optionally the Exception class to + # use when a requirement is not met. It works like `raise`: + # # in a provider: + # assert(:some_action) do |a| + # # This example shows usage with 1 or 2 args by calling #failure_message twice. + # # In practice you should only call this once per Assertion. + # + # # Set the Exception class explicitly + # a.failure_message(Chef::Exceptions::MissingRequiredFile, "File /tmp/foo doesn't exist") + # # Fallback to the default error class (AssertionFailure) + # a.failure_message("File /tmp/foo" doesn't exist") + # end + def failure_message(*args) + case args.size + when 1 + @failure_message = args[0] + when 2 + @exception_type, @failure_message = args[0], args[1] + else + raise ArgumentError, "#{self.class}#failure_message takes 1 or 2 arguments, you gave #{args.inspect}" + end + end + + # Defines a message and optionally provides a code block to execute + # when the requirement is not met and Chef is executing in why run + # mode + # + # If no failure_message is provided (above), then execution + # will be allowed to continue in both whyrun an dnon-whyrun + # mode + # + # With a service resource that requires /etc/init.d/service-name to exist: + # # in a provider + # assert(:start, :restart) do |a| + # a.assertion { ::File.exist?("/etc/init.d/service-name") } + # a.whyrun("Init script '/etc/init.d/service-name' doesn't exist, assuming a prior action would have created it.") do + # # blindly assume that the service exists but is stopped in why run mode: + # @new_resource.status(:stopped) + # end + # end + def whyrun(message, &resource_modifier) + @whyrun_message = message + @resource_modifier = resource_modifier + end + + # Prevents associated actions from being invoked in whyrun mode. + # This will also stop further processing of assertions for a given action. + # + # An example from the template provider: if the source template doesn't exist + # we can't parse it in the action_create block of template - something that we do + # even in whyrun mode. Because the soruce template may have been created in an earlier + # step, we still want to keep going in whyrun mode. + # + # assert(:create, :create_if_missing) do |a| + # a.assertion { File::exists?(@new_resource.source) } + # a.whyrun "Template source file does not exist, assuming it would have been created." + # a.block_action! + # end + # + def block_action! + @block_action = true + end + + def block_action? + @block_action + end + + def assertion_failed? + @assertion_failed + end + + + # Runs the assertion/assumption logic. Will raise an Exception of the + # type specified in #failure_message (or AssertionFailure by default) + # if the requirement is not met and Chef is not running in why run + # mode. An exception will also be raised if running in why run mode + # and no why run message or block has been declared. + def run(action, events, resource) + if !@assertion_proc || !@assertion_proc.call + @assertion_failed = true + if Chef::Config[:why_run] && @whyrun_message + events.provider_requirement_failed(action, resource, @exception_type, @failure_message) + events.whyrun_assumption(action, resource, @whyrun_message) if @whyrun_message + @resource_modifier.call if @resource_modifier + else + if @failure_message + events.provider_requirement_failed(action, resource, @exception_type, @failure_message) + raise @exception_type, @failure_message + end + end + end + end + end + + def initialize(resource, run_context) + @resource, @run_context = resource, run_context + @assertions = Hash.new {|h,k| h[k] = [] } + @blocked_actions = [] + end + + def events + @run_context.events + end + + # Check to see if a given action is blocked by a failed assertion + # + # Takes the action name to be verified. + def action_blocked?(action) + @blocked_actions.include?(action) + end + + # Define a new Assertion. + # + # Takes a list of action names for which the assertion should be made. + # ==== Examples: + # A File provider that requires the parent directory to exist: + # + # assert(:create, :create_if_missing) do |a| + # parent_dir = File.basename(@new_resource.path) + # a.assertion { ::File.directory?(parent_dir) } + # a.failure_message(Exceptions::ParentDirectoryDoesNotExist, + # "Can't create file #{@new_resource.path}: parent directory #{parent_dir} doesn't exist") + # a.why_run("assuming parent directory #{parent_dir} would have been previously created" + # end + # + # A service provider that requires the init script to exist: + # + # assert(:start, :restart) do |a| + # a.assertion { ::File.exist?(@new_resource.init_script) } + # a.failure_message(Exceptions::MissingInitScript, + # "Can't check status of #{@new_resource}: init script #{@new_resource.init_script} is missing") + # a.why_run("Assuming init script would have been created and service is stopped") do + # @current_resource.status(:stopped) + # end + # end + # + # A File provider that will error out if you don't have permissions do + # delete the file, *even in why run mode*: + # + # assert(:delete) do |a| + # a.assertion { ::File.writable?(@new_resource.path) } + # a.failure_message(Exceptions::InsufficientPrivileges, + # "You don't have sufficient privileges to delete #{@new_resource.path}") + # end + # + # A Template provider that will prevent action execution but continue the run in + # whyrun mode if the template source is not available. + # assert(:create, :create_if_missing) do |a| + # a.assertion { File::exist?(@new_resource.source) } + # a.failure_message Chef::Exceptions::TemplateError, "Template #{@new_resource.source} could not be found exist." + # a.whyrun "Template source #{@new_resource.source} does not exist. Assuming it would have been created." + # a.block_action! + # end + # + # assert(:delete) do |a| + # a.assertion { ::File.writable?(@new_resource.path) } + # a.failure_message(Exceptions::InsufficientPrivileges, + # "You don't have sufficient privileges to delete #{@new_resource.path}") + # end + def assert(*actions) + assertion = Assertion.new + yield assertion + actions.each {|action| @assertions[action] << assertion } + end + + # Run the assertion and assumption logic. + def run(action) + @assertions[action.to_sym].each do |a| + a.run(action, events, @resource) + if a.assertion_failed? and a.block_action? + @blocked_actions << action + return + end + end + end + end + end + end +end diff --git a/lib/chef/mixin/xml_escape.rb b/lib/chef/mixin/xml_escape.rb new file mode 100644 index 0000000000..dac2f0c6af --- /dev/null +++ b/lib/chef/mixin/xml_escape.rb @@ -0,0 +1,140 @@ +#-- +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2009 Opscode, Inc. +# Copyright:: Copyright (c) 2005 Sam Ruby +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#-- +# Portions of this code are adapted from Sam Ruby's xchar.rb +# http://intertwingly.net/stories/2005/09/28/xchar.rb +# +# Such code appears here under Sam's original MIT license, while portions of +# this file are covered by the above Apache License. For a completely MIT +# licensed version, please see Sam's original. +# +# Thanks, Sam! +# +# Copyright (c) 2005, Sam Ruby +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +require 'chef/log' + +begin + require 'fast_xs' +rescue LoadError + Chef::Log.info "The fast_xs gem is not installed, slower pure ruby XML escaping will be used." +end + +class Chef + module Mixin + module XMLEscape + + module PureRuby + extend self + + CP1252 = { + 128 => 8364, # euro sign + 130 => 8218, # single low-9 quotation mark + 131 => 402, # latin small letter f with hook + 132 => 8222, # double low-9 quotation mark + 133 => 8230, # horizontal ellipsis + 134 => 8224, # dagger + 135 => 8225, # double dagger + 136 => 710, # modifier letter circumflex accent + 137 => 8240, # per mille sign + 138 => 352, # latin capital letter s with caron + 139 => 8249, # single left-pointing angle quotation mark + 140 => 338, # latin capital ligature oe + 142 => 381, # latin capital letter z with caron + 145 => 8216, # left single quotation mark + 146 => 8217, # right single quotation mark + 147 => 8220, # left double quotation mark + 148 => 8221, # right double quotation mark + 149 => 8226, # bullet + 150 => 8211, # en dash + 151 => 8212, # em dash + 152 => 732, # small tilde + 153 => 8482, # trade mark sign + 154 => 353, # latin small letter s with caron + 155 => 8250, # single right-pointing angle quotation mark + 156 => 339, # latin small ligature oe + 158 => 382, # latin small letter z with caron + 159 => 376 # latin capital letter y with diaeresis + } + + # http://www.w3.org/TR/REC-xml/#dt-chardata + PREDEFINED = { + 38 => '&', # ampersand + 60 => '<', # left angle bracket + 62 => '>' # right angle bracket + } + + # http://www.w3.org/TR/REC-xml/#charsets + VALID = [[0x9, 0xA, 0xD], (0x20..0xD7FF), + (0xE000..0xFFFD), (0x10000..0x10FFFF)] + + def xml_escape(unescaped_str) + begin + unescaped_str.unpack("U*").map {|char| xml_escape_char!(char)}.join + rescue + unescaped_str.unpack("C*").map {|char| xml_escape_char!(char)}.join + end + end + + private + + def xml_escape_char!(char) + char = CP1252[char] || char + char = 42 unless VALID.detect {|range| range.include? char} + char = PREDEFINED[char] || (char<128 ? char.chr : "&##{char};") + end + end + + module FastXS + extend self + + def xml_escape(string) + string.fast_xs + end + + end + + if "strings".respond_to?(:fast_xs) + include FastXS + extend FastXS + else + include PureRuby + extend PureRuby + end + end + end +end |