lib/puppet/macros.rb
require 'puppet'
require 'puppet/util/autoload'
# Monkey patches to Scope, so we have convenient access to some methods from
# macros.
class Puppet::Parser::Scope
# Call macro registered with {Puppet::Macros.newmacro}
#
# @param name [String] macro name, e.g. `'foo::bar'`,
# @param args [Array] arguments to be provided to the macro,
# @param options [Hash] additional options, see {Puppet::Macros.call_macro}
# @param env [Puppet::Node::Environment] puppet environment,
# @return the result of macro evaluation
def call_macro(name, args = [], options = {}, env = Puppet::Macros.default_environment)
# FIXME: I believe I should use environment that is bound to scope, not the
# Macros.default_environment, but at the moment I have no idea where to
# find it
Puppet::Macros.call_macro(self,name,args,options,env)
end
# Convert puppet value to ruby.
#
# This converts empty strings and :undefs to nil.
#
# @param v [String|Symbol] value to be converted
# @return converted value
def pp2r(v)
(v.equal?(:undef) or v == '') ? nil : v
end
# Convert value from facter to ruby.
#
# This converts empty strings and :undefined symbol to nil.
#
# @param v [String|Symbol] value to be converted
# @return converted value
def fr2r(v)
(v.equal?(:undefined) or v == '') ? nil : v
end
end
module Puppet::Macros; end
# Utility module for {Puppet::Macros}
module Puppet::Macros::DefaultEnvironment
# This tries to ensure compatibility with different versions of Puppet
# @api private
if Puppet.respond_to?(:lookup)
def default_environment
Puppet.lookup(:current_environment)
end
else
begin
require 'puppet/context'
def default_environment
Puppet::Context.lookup(:current_environment)
end
rescue LoadError
begin
require 'puppet/node/environment'
def default_environment
Puppet::Node::Environment.current
end
rescue LoadError
def default_environment
nil
end
end
end
end
end
# Utility module for {Puppet::Macros}
module Puppet::Macros::Validation
# Validate name
#
# @param name [Object] the name to be validated
# @raise [ArgumentError]
# @api private
def validate_name(name, errclass = ArgumentError)
unless valid_name?(name)
raise errclass, "Invalid macro name #{name.inspect}"
end
end
# @api private
def macro_arities_by_parameters(macro)
arg_kinds = macro.parameters.map{|kind,name| kind}
[arg_kinds.count(:req), arg_kinds.include?(:rest) ? :inf : arg_kinds.size]
end
# @api private
def macro_arities_by_arity(macro)
arity = macro.arity
(arity>=0) ? [arity, arity] : [arity.abs-1, :inf]
end
# @api private
def macro_arities(macro)
# Using macro.parameters is far more reliable than macro.arity, but
# parameters are missing in ruby<=1.8.
if macro.respond_to?(:parameters)
macro_arities_by_parameters(macro)
else
macro_arities_by_arity(macro)
end
end
# @api private
def check_macro_arity(macro, macro_args, errclass = ArgumentError)
min_arity, max_arity = macro_arities(macro)
argn = macro_args.size
if min_arity == max_arity
if argn != min_arity
raise errclass, "Wrong number of arguments (#{argn} for #{min_arity})"
end
elsif argn < min_arity
raise errclass, "Wrong number of arguments (#{argn} for minimum #{min_arity})"
elsif (not max_arity.equal?(:inf)) and (argn > max_arity)
raise errclass, "Wrong number of arguments (#{argn} for maximum #{max_arity})"
end
end
end
module Puppet::Macros::ToLambda
def to_lambda(block)
if Puppet::Util::Package.versioncmp(RUBY_VERSION,"1.9") >=0
unless block.lambda?
# This code is based on: https://github.com/schneems/proc_to_lambda
# See also https://github.com/schneems/proc_to_lambda/issues/1
if RUBY_ENGINE && RUBY_ENGINE == "jruby"
lambda(&block)
else
Object.new.define_singleton_method(:_, &block).to_proc
end
end
else
block
end
end
end
# This object keeps track of macros defined within a single puppet
# environment. Existing hashes (macros for existing environments) may be
# retrieved with {macros} method.
module Puppet::Macros
class << self
include DefaultEnvironment
include Validation
include ToLambda
# Regular expession used to validate macro names.
MACRO_NAME_RE = /^[a-z_][a-z0-9_]*(?:::[a-z_][a-z0-9_]*)*$/
# Check whether **name** is a valid macro name.
#
# @param name a name to be validated
# @return [Boolean] `true` if the **name** is valid, or `false` otherwise
# @api private
def valid_name?(name)
name.is_a?(String) and MACRO_NAME_RE.match(name)
end
# Define new preprocessor macro.
#
# A preprocessor macro is a callable object, which can be executed from
# within a puppet manifest.
#
# **Example:**
#
# Definition:
#
# ```ruby
# # puppet/macros/apache/package.rb
# Puppet::Parser.Macro.newmacro 'apache::package' do
# setcode do
# case os = fact(:osfamily)
# when 'FreeBSD'
# 'www/apache22'
# when 'Debian'
# 'apache2'
# else
# raise Puppet::Error, "#{os} is not supported"
# end
# end
# end
# ```
#
# Usage:
#
# ```puppet
# # manifest.pp
# $package = determine('apache::package')
# ````
#
# @param name [String] macro name
# @param options [Hash] additional options
# @param block [Proc] macro definition
#
# @option options environment [Puppet::Node::Environment] an environment
# this macro belongs to, defautls to `default_environment`
def newmacro(name,options={},&block)
env = options[:environment] || default_environment
Puppet.debug "overwritting macro #{name}" if macro(name,env,false)
validate_name(name)
macros(env)[name] = to_lambda(block)
end
# Get a hash of registered macros.
#
# @param env [Puppet::Node::Environment] puppet environment
# @return [Hash] a hash with registered parser macros
# @api private
def macros(env = default_environment)
@macros ||= {}
@macros[env] ||= {}
end
# Retrieve single macro from {macros}
#
# @param name [String] macro name,
# @param env [Puppet::Node::Environment] puppet environment,
def macro(name, env = default_environment, auto = true)
root = Puppet::Node::Environment.root
macros(env)[name] || macros(root)[name] || (auto ? load(name,env) : nil)
end
# Accessor for singleton autoloader
# @api private
def autoloader
unless @autoloader
# Patched autoloader whith 'loadall' searching recursivelly
@autoloader = Puppet::Util::Autoload.new(
self, "puppet/macros", :wrap => false
)
class << @autoloader
def loadall
self.class.loadall(File.join(@path,"**"))
end
def files_to_load
self.class.files_to_load(File.join(@path,"**"))
end
unless instance_methods.include?(:expand) or instance_methods.include?('expand')
# Fix for puppet 2.7, which does not have the Autoload.expand
# method
def expand(name)
::File.join(@path, name.to_s)
end
end
end
end
@autoloader
end
# Load single macro from file
#
# @param name [String] macro name, e.g. `'foo::bar'`,
# @param env [Puppet::Node::Environment] puppet environment,
# @return [Macro|nil]
# @api private
def load(name, env = default_environment)
# This block autoloads appropriate file each time a missing macro is
# requested from hash.
path = name.split('::').join('/')
load_from_file(name, path, env)
end
# Load single file possibly containing macro definition.
#
# @param name [String] name of the macro to be loaded
# @param path [String] path to macro file, relative to
# **puppet/macros** and without `.rb` suffix,
# @param env [Puppet::Node::Environment] puppet environment,
# @return [Macro|nil]
# @api private
def load_from_file(name, path, env = default_environment)
if autoloader.load(path, env)
# the autoloaded code should add its macro to macros
unless m = self.macro(name,env,false)
Puppet.debug("#{autoloader.expand(path).inspect} loaded but it " +
"didn't define macro #{name.inspect}")
end
m
else
Puppet.debug("could not autoload #{autoloader.expand(path).inspect}")
nil
end
end
# Autoload all existing macro definitions in current environment
#
# @param env [Puppet::Node::Environment] puppet environment,
# @return [Array] an array of loaded files
def loadall
autoloader.loadall
end
# @api private
def get_macro(name, env = default_environment, errclass = Puppet::Error)
unless macro = self.macro(name,env)
raise errclass, "Undefined macro #{name}"
end
macro
end
# Fix error messages to indicate number of arguments to parser function
# instead of the number of arguments to macro and prepend the function
# name.
# @param func [Symbol|String] function name,
# @param msg [String] original message from callee,
# @param n [Integer] number of arguments shifted from function's arglist,
# @return [String] fixed message
# @api private
def fix_error_msg(func, msg, n = 0)
re = /^Wrong number of arguments \(([0-9]+) for (minimum |maximum )?([0-9]+)\)$/
if m = re.match(msg)
msg = "Wrong number of arguments (#{n+Integer(m.captures[0])} " +
"for #{m.captures[1]}#{n+Integer(m.captures[2])})"
end
"#{func}(): #{msg}"
end
# Call a macro.
#
# @param scope [Puppet::Parser::Scope] scope of the calling function
# @param name [String] name of the macro to be invoked
# @param args [Array] arguments to be provided to teh macro
# @param options [Hash] additional options
# @param env [Puppet::Node::Environment] environment
# @option options :a_err [Class] an exception to be raised when argument
# validation fails, defaults to **ArgumentError**
# @option options :l_err [Class] an exception to be raised when macro
# lookup fails, defaults to **Puppet::Error**
# @return the value of macro (result of evaluation)
def call_macro(scope, name, args, options = {}, env = default_environment)
validate_name(name, options[:a_err] || ArgumentError)
macro = get_macro(name, env, options[:l_err] || Puppet::Error)
check_macro_arity(macro, args, options[:a_err] || ArgumentError)
scope.instance_exec(*args,¯o)
end
# Call a macro from parser function.
#
# This method is dedicated to be called from a parser function. It's used
# by `determine` and `invoke`, but may be used by other custom functions as
# well. This method checks the arguments and in case of validation error
# raises Puppet::ParseError with appropriate message. If there is error
# in number of arguments to macro, the exception message will reflect the
# number of arguments to function, not the macro.
#
#
# @param scope [Puppet::Parser::Scope] scope of the calling function
# @param func_name [Symbol|String] name of the calling function
# @param func_args [Array] arguments, as provided to calling function
# @param n [Integer] number of extra arguments shifted from function's
# argument list
# @param env [Puppet::Node::Environment] environment
# @return the value of macro (result of evaluation)
def call_macro_from_func(scope, func_name, func_args, n = 0, env = default_environment)
begin
if(func_args.size == 0)
raise Puppet::ParseError, "Wrong number of arguments (0) - missing macro name"
end
options = { :a_err => Puppet::ParseError, :l_err => Puppet::ParseError }
call_macro(scope, func_args[0], func_args[1..-1], options, env)
rescue Puppet::ParseError => err
msg = fix_error_msg(func_name, err.message, 1+n)
raise Puppet::ParseError, msg, err.backtrace
end
end
end
end