Current File : //opt/puppetlabs/puppet/lib/ruby/vendor_ruby/puppet/pal/pal_impl.rb |
# Puppet as a Library "PAL"
# Yes, this requires all of puppet for now because 'settings' and many other things...
require_relative '../../puppet'
require_relative '../../puppet/parser/script_compiler'
require_relative '../../puppet/parser/catalog_compiler'
module Puppet
# This is the main entry point for "Puppet As a Library" PAL.
# This file should be required instead of "puppet"
# Initially, this will require ALL of puppet - over time this will change as the monolithical "puppet" is broken up
# into smaller components.
#
# @example Running a snippet of Puppet Language code
# require 'puppet_pal'
# result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: ['/tmp/testmodules']) do |pal|
# pal.evaluate_script_string('1+2+3')
# end
# # The result is the value 6
#
# @example Calling a function
# require 'puppet_pal'
# result = Puppet::Pal.in_tmp_environment('pal_env', modulepath: ['/tmp/testmodules']) do |pal|
# pal.call_function('mymodule::myfunction', 10, 20)
# end
# # The result is what 'mymodule::myfunction' returns
#
# @api public
module Pal
# Defines a context in which multiple operations in an env with a script compiler can be performed in a given block.
# The calls that takes place to PAL inside of the given block are all with the same instance of the compiler.
# The parameter `configured_by_env` makes it possible to either use the configuration in the environment, or specify
# `manifest_file` or `code_string` manually. If neither is given, an empty `code_string` is used.
#
# @example define a script compiler without any initial logic
# pal.with_script_compiler do | compiler |
# # do things with compiler
# end
#
# @example define a script compiler with a code_string containing initial logic
# pal.with_script_compiler(code_string: '$myglobal_var = 42') do | compiler |
# # do things with compiler
# end
#
# @param configured_by_env [Boolean] when true the environment's settings are used, otherwise the given `manifest_file` or `code_string`
# @param manifest_file [String] a Puppet Language file to load and evaluate before calling the given block, mutually exclusive with `code_string`
# @param code_string [String] a Puppet Language source string to load and evaluate before calling the given block, mutually exclusive with `manifest_file`
# @param facts [Hash] optional map of fact name to fact value - if not given will initialize the facts (which is a slow operation)
# If given at the environment level, the facts given here are merged with higher priority.
# @param variables [Hash] optional map of fully qualified variable name to value. If given at the environment level, the variables
# given here are merged with higher priority.
# @param set_local_facts [Boolean] when true, the $facts, $server_facts, and $trusted variables are set for the scope.
# @param block [Proc] the block performing operations on compiler
# @return [Object] what the block returns
# @yieldparam [Puppet::Pal::ScriptCompiler] compiler, a ScriptCompiler to perform operations on.
#
def self.with_script_compiler(
configured_by_env: false,
manifest_file: nil,
code_string: nil,
facts: {},
variables: {},
set_local_facts: true,
&block
)
# TRANSLATORS: do not translate variable name strings in these assertions
assert_mutually_exclusive(manifest_file, code_string, 'manifest_file', 'code_string')
assert_non_empty_string(manifest_file, 'manifest_file', true)
assert_non_empty_string(code_string, 'code_string', true)
assert_type(T_BOOLEAN, configured_by_env, "configured_by_env", false)
if configured_by_env
unless manifest_file.nil? && code_string.nil?
# TRANSLATORS: do not translate the variable names in this error message
raise ArgumentError, _("manifest_file or code_string cannot be given when configured_by_env is true")
end
# Use the manifest setting
manifest_file = Puppet[:manifest]
else
# An "undef" code_string is the only way to override Puppet[:manifest] & Puppet[:code] settings since an
# empty string is taken as Puppet[:code] not being set.
#
if manifest_file.nil? && code_string.nil?
code_string = 'undef'
end
end
previous_tasks_value = Puppet[:tasks]
previous_code_value = Puppet[:code]
Puppet[:tasks] = true
# After the assertions, if code_string is non nil - it has the highest precedence
Puppet[:code] = code_string unless code_string.nil?
# If manifest_file is nil, the #main method will use the env configured manifest
# to do things in the block while a Script Compiler is in effect
main(
manifest: manifest_file,
facts: facts,
variables: variables,
internal_compiler_class: :script,
set_local_facts: set_local_facts,
&block
)
ensure
Puppet[:tasks] = previous_tasks_value
Puppet[:code] = previous_code_value
end
# Evaluates a Puppet Language script string.
# @param code_string [String] a snippet of Puppet Language source code
# @return [Object] what the Puppet Language code_string evaluates to
# @deprecated Use {#with_script_compiler} and then evaluate_string on the given compiler - to be removed in 1.0 version
#
def self.evaluate_script_string(code_string)
# prevent the default loading of Puppet[:manifest] which is the environment's manifest-dir by default settings
# by setting code_string to 'undef'
with_script_compiler do |compiler|
compiler.evaluate_string(code_string)
end
end
# Evaluates a Puppet Language script (.pp) file.
# @param manifest_file [String] a file with Puppet Language source code
# @return [Object] what the Puppet Language manifest_file contents evaluates to
# @deprecated Use {#with_script_compiler} and then evaluate_file on the given compiler - to be removed in 1.0 version
#
def self.evaluate_script_manifest(manifest_file)
with_script_compiler do |compiler|
compiler.evaluate_file(manifest_file)
end
end
# Defines a context in which multiple operations in an env with a catalog producing compiler can be performed
# in a given block.
# The calls that takes place to PAL inside of the given block are all with the same instance of the compiler.
# The parameter `configured_by_env` makes it possible to either use the configuration in the environment, or specify
# `manifest_file` or `code_string` manually. If neither is given, an empty `code_string` is used.
#
# @example define a catalog compiler without any initial logic
# pal.with_catalog_compiler do | compiler |
# # do things with compiler
# end
#
# @example define a catalog compiler with a code_string containing initial logic
# pal.with_catalog_compiler(code_string: '$myglobal_var = 42') do | compiler |
# # do things with compiler
# end
#
# @param configured_by_env [Boolean] when true the environment's settings are used, otherwise the
# given `manifest_file` or `code_string`
# @param manifest_file [String] a Puppet Language file to load and evaluate before calling the given block, mutually exclusive
# with `code_string`
# @param code_string [String] a Puppet Language source string to load and evaluate before calling the given block, mutually
# exclusive with `manifest_file`
# @param facts [Hash] optional map of fact name to fact value - if not given will initialize the facts (which is a slow operation)
# If given at the environment level, the facts given here are merged with higher priority.
# @param variables [Hash] optional map of fully qualified variable name to value. If given at the environment level, the variables
# given here are merged with higher priority.
# @param block [Proc] the block performing operations on compiler
# @return [Object] what the block returns
# @yieldparam [Puppet::Pal::CatalogCompiler] compiler, a CatalogCompiler to perform operations on.
#
def self.with_catalog_compiler(
configured_by_env: false,
manifest_file: nil,
code_string: nil,
facts: {},
variables: {},
target_variables: {},
&block
)
# TRANSLATORS: do not translate variable name strings in these assertions
assert_mutually_exclusive(manifest_file, code_string, 'manifest_file', 'code_string')
assert_non_empty_string(manifest_file, 'manifest_file', true)
assert_non_empty_string(code_string, 'code_string', true)
assert_type(T_BOOLEAN, configured_by_env, "configured_by_env", false)
if configured_by_env
unless manifest_file.nil? && code_string.nil?
# TRANSLATORS: do not translate the variable names in this error message
raise ArgumentError, _("manifest_file or code_string cannot be given when configured_by_env is true")
end
# Use the manifest setting
manifest_file = Puppet[:manifest]
else
# An "undef" code_string is the only way to override Puppet[:manifest] & Puppet[:code] settings since an
# empty string is taken as Puppet[:code] not being set.
#
if manifest_file.nil? && code_string.nil?
code_string = 'undef'
end
end
# We need to make sure to set these back when we're done
previous_tasks_value = Puppet[:tasks]
previous_code_value = Puppet[:code]
Puppet[:tasks] = false
# After the assertions, if code_string is non nil - it has the highest precedence
Puppet[:code] = code_string unless code_string.nil?
# If manifest_file is nil, the #main method will use the env configured manifest
# to do things in the block while a Script Compiler is in effect
main(
manifest: manifest_file,
facts: facts,
variables: variables,
target_variables: target_variables,
internal_compiler_class: :catalog,
set_local_facts: false,
&block
)
ensure
# Clean up after ourselves
Puppet[:tasks] = previous_tasks_value
Puppet[:code] = previous_code_value
end
# Defines the context in which to perform puppet operations (evaluation, etc)
# The code to evaluate in this context is given in a block.
#
# @param env_name [String] a name to use for the temporary environment - this only shows up in errors
# @param modulepath [Array<String>] an array of directory paths containing Puppet modules, may be empty, defaults to empty array
# @param settings_hash [Hash] a hash of settings - currently not used for anything, defaults to empty hash
# @param facts [Hash] optional map of fact name to fact value - if not given will initialize the facts (which is a slow operation)
# @param variables [Hash] optional map of fully qualified variable name to value
# @return [Object] returns what the given block returns
# @yieldparam [Puppet::Pal] context, a context that responds to Puppet::Pal methods
#
def self.in_tmp_environment(env_name,
modulepath: [],
settings_hash: {},
facts: nil,
variables: {},
&block
)
assert_non_empty_string(env_name, _("temporary environment name"))
# TRANSLATORS: do not translate variable name string in these assertions
assert_optionally_empty_array(modulepath, 'modulepath')
unless block_given?
raise ArgumentError, _("A block must be given to 'in_tmp_environment'") # TRANSLATORS 'in_tmp_environment' is a name, do not translate
end
env = Puppet::Node::Environment.create(env_name, modulepath)
in_environment_context(
Puppet::Environments::Static.new(env), # The tmp env is the only known env
env, facts, variables, &block
)
end
# Defines the context in which to perform puppet operations (evaluation, etc)
# The code to evaluate in this context is given in a block.
#
# The name of an environment (env_name) is always given. The location of that environment on disk
# is then either constructed by:
# * searching a given envpath where name is a child of a directory on that path, or
# * it is the directory given in env_dir (which must exist).
#
# The env_dir and envpath options are mutually exclusive.
#
# @param env_name [String] the name of an existing environment
# @param modulepath [Array<String>] an array of directory paths containing Puppet modules, overrides the modulepath of an existing env.
# Defaults to `{env_dir}/modules` if `env_dir` is given,
# @param pre_modulepath [Array<String>] like modulepath, but is prepended to the modulepath
# @param post_modulepath [Array<String>] like modulepath, but is appended to the modulepath
# @param settings_hash [Hash] a hash of settings - currently not used for anything, defaults to empty hash
# @param env_dir [String] a reference to a directory being the named environment (mutually exclusive with `envpath`)
# @param envpath [String] a path of directories in which there are environments to search for `env_name` (mutually exclusive with `env_dir`).
# Should be a single directory, or several directories separated with platform specific `File::PATH_SEPARATOR` character.
# @param facts [Hash] optional map of fact name to fact value - if not given will initialize the facts (which is a slow operation)
# @param variables [Hash] optional map of fully qualified variable name to value
# @return [Object] returns what the given block returns
# @yieldparam [Puppet::Pal] context, a context that responds to Puppet::Pal methods
#
# @api public
def self.in_environment(env_name,
modulepath: nil,
pre_modulepath: [],
post_modulepath: [],
settings_hash: {},
env_dir: nil,
envpath: nil,
facts: nil,
variables: {},
&block
)
# TRANSLATORS terms in the assertions below are names of terms in code
assert_non_empty_string(env_name, 'env_name')
assert_optionally_empty_array(modulepath, 'modulepath', true)
assert_optionally_empty_array(pre_modulepath, 'pre_modulepath', false)
assert_optionally_empty_array(post_modulepath, 'post_modulepath', false)
assert_mutually_exclusive(env_dir, envpath, 'env_dir', 'envpath')
unless block_given?
raise ArgumentError, _("A block must be given to 'in_environment'") # TRANSLATORS 'in_environment' is a name, do not translate
end
if env_dir
unless Puppet::FileSystem.exist?(env_dir)
raise ArgumentError, _("The environment directory '%{env_dir}' does not exist") % { env_dir: env_dir }
end
# a nil modulepath for env_dir means it should use its ./modules directory
mid_modulepath = modulepath.nil? ? [Puppet::FileSystem.expand_path(File.join(env_dir, 'modules'))] : modulepath
env = Puppet::Node::Environment.create(env_name, pre_modulepath + mid_modulepath + post_modulepath)
environments = Puppet::Environments::StaticDirectory.new(env_name, env_dir, env) # The env being used is the only one...
else
assert_non_empty_string(envpath, 'envpath')
# The environment is resolved against the envpath. This is setup without a basemodulepath
# The modulepath defaults to the 'modulepath' in the found env when "Directories" is used
#
if envpath.is_a?(String) && envpath.include?(File::PATH_SEPARATOR)
# potentially more than one directory to search
env_loaders = Puppet::Environments::Directories.from_path(envpath, [])
environments = Puppet::Environments::Combined.new(*env_loaders)
else
environments = Puppet::Environments::Directories.new(envpath, [])
end
env = environments.get(env_name)
if env.nil?
raise ArgumentError, _("No directory found for the environment '%{env_name}' on the path '%{envpath}'") % { env_name: env_name, envpath: envpath }
end
# A given modulepath should override the default
mid_modulepath = modulepath.nil? ? env.modulepath : modulepath
env_path = env.configuration.path_to_env
env = env.override_with(:modulepath => pre_modulepath + mid_modulepath + post_modulepath)
# must configure this in case logic looks up the env by name again (otherwise the looked up env does
# not have the same effective modulepath).
environments = Puppet::Environments::StaticDirectory.new(env_name, env_path, env) # The env being used is the only one...
end
in_environment_context(environments, env, facts, variables, &block)
end
# Prepares the puppet context with pal information - and delegates to the block
# No set up is performed at this step - it is delayed until it is known what the
# operation is going to be (for example - using a ScriptCompiler).
#
def self.in_environment_context(environments, env, facts, variables, &block)
# Create a default node to use (may be overridden later)
node = Puppet::Node.new(Puppet[:node_name_value], :environment => env)
Puppet.override(
environments: environments, # The env being used is the only one...
pal_env: env, # provide as convenience
pal_current_node: node, # to allow it to be picked up instead of created
pal_variables: variables, # common set of variables across several inner contexts
pal_facts: facts # common set of facts across several inner contexts (or nil)
) do
# DELAY: prepare_node_facts(node, facts)
return block.call(self)
end
end
private_class_method :in_environment_context
# Prepares the node for use by giving it node_facts (if given)
# If a hash of facts values is given, then the operation of creating a node with facts is much
# speeded up (as getting a fresh set of facts is avoided in a later step).
#
def self.prepare_node_facts(node, facts)
# Prepare the node with facts if it does not already have them
if node.facts.nil?
node_facts = facts.nil? ? nil : Puppet::Node::Facts.new(Puppet[:node_name_value], facts)
node.fact_merge(node_facts)
# Add server facts so $server_facts[environment] exists when doing a puppet script
# SCRIPT TODO: May be needed when running scripts under orchestrator. Leave it for now.
#
node.add_server_facts({})
end
end
private_class_method :prepare_node_facts
def self.add_variables(scope, variables)
return if variables.nil?
unless variables.is_a?(Hash)
raise ArgumentError, _("Given variables must be a hash, got %{type}") % { type: variables.class }
end
rich_data_t = Puppet::Pops::Types::TypeFactory.rich_data
variables.each_pair do |k,v|
unless k =~ Puppet::Pops::Patterns::VAR_NAME
raise ArgumentError, _("Given variable '%{varname}' has illegal name") % { varname: k }
end
unless rich_data_t.instance?(v)
raise ArgumentError, _("Given value for '%{varname}' has illegal type - got: %{type}") % { varname: k, type: v.class }
end
scope.setvar(k, v)
end
end
private_class_method :add_variables
# The main routine for script compiler
# Picks up information from the puppet context and configures a script compiler which is given to
# the provided block
#
def self.main(
manifest: nil,
facts: {},
variables: {},
target_variables: {},
internal_compiler_class: nil,
set_local_facts: true
)
# Configure the load path
env = Puppet.lookup(:pal_env)
env.each_plugin_directory do |dir|
$LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
end
# Puppet requires Facter, which initializes its lookup paths. Reset Facter to
# pickup the new $LOAD_PATH.
Puppet.runtime[:facter].reset
node = Puppet.lookup(:pal_current_node)
pal_facts = Puppet.lookup(:pal_facts)
pal_variables = Puppet.lookup(:pal_variables)
overrides = {}
unless facts.nil? || facts.empty?
pal_facts = pal_facts.merge(facts)
overrides[:pal_facts] = pal_facts
end
prepare_node_facts(node, pal_facts)
configured_environment = node.environment || Puppet.lookup(:current_environment)
apply_environment = manifest ?
configured_environment.override_with(:manifest => manifest) :
configured_environment
# Modify the node descriptor to use the special apply_environment.
# It is based on the actual environment from the node, or the locally
# configured environment if the node does not specify one.
# If a manifest file is passed on the command line, it overrides
# the :manifest setting of the apply_environment.
node.environment = apply_environment
# TRANSLATORS, the string "For puppet PAL" is not user facing
Puppet.override({:current_environment => apply_environment}, "For puppet PAL") do
begin
node.sanitize()
compiler = create_internal_compiler(internal_compiler_class, node)
case internal_compiler_class
when :script
pal_compiler = ScriptCompiler.new(compiler)
overrides[:pal_script_compiler] = overrides[:pal_compiler] = pal_compiler
when :catalog
pal_compiler = CatalogCompiler.new(compiler)
overrides[:pal_catalog_compiler] = overrides[:pal_compiler] = pal_compiler
end
# When scripting the trusted data are always local; default is to set them anyway
# When compiling for a catalog, the catalog compiler does this
if set_local_facts
compiler.topscope.set_trusted(node.trusted_data)
# Server facts are always about the local node's version etc.
compiler.topscope.set_server_facts(node.server_facts)
# Set $facts for the node running the script
facts_hash = node.facts.nil? ? {} : node.facts.values
compiler.topscope.set_facts(facts_hash)
# create the $settings:: variables
compiler.topscope.merge_settings(node.environment.name, false)
end
# Make compiler available to Puppet#lookup and injection in functions
# TODO: The compiler instances should be available under non PAL use as well!
# TRANSLATORS: Do not translate, symbolic name
Puppet.override(overrides, "PAL::with_#{internal_compiler_class}_compiler") do
compiler.compile do | compiler_yield |
# In case the variables passed to the compiler are PCore types defined in modules, they
# need to be deserialized and added from within the this scope, so that loaders are
# available during deserizlization.
pal_variables = Puppet::Pops::Serialization::FromDataConverter.convert(pal_variables)
variables = Puppet::Pops::Serialization::FromDataConverter.convert(variables)
# Merge together target variables and plan variables. This will also shadow any
# collisions with facts and emit a warning.
topscope_vars = pal_variables.merge(merge_vars(target_variables, variables, node.facts.values))
add_variables(compiler.topscope, topscope_vars)
# wrap the internal compiler to prevent it from leaking in the PAL API
if block_given?
yield(pal_compiler)
end
end
end
rescue Puppet::Error
# already logged and handled by the compiler, including Puppet::ParseErrorWithIssue
raise
rescue => detail
Puppet.log_exception(detail)
raise
end
end
end
private_class_method :main
# Warn and remove variables that will be shadowed by facts of the same
# name, which are set in scope earlier.
def self.merge_vars(target_vars, vars, facts)
# First, shadow plan and target variables by facts of the same name
vars = shadow_vars(facts || {}, vars, 'fact', 'plan variable')
target_vars = shadow_vars(facts || {}, target_vars, 'fact', 'target variable')
# Then, shadow target variables by plan variables of the same name
target_vars = shadow_vars(vars, target_vars, 'plan variable', 'target variable')
target_vars.merge(vars)
end
private_class_method :merge_vars
def self.shadow_vars(vars, other_vars, vars_type, other_vars_type)
collisions, valid = other_vars.partition do |k, _|
vars.include?(k)
end
if collisions.any?
names = collisions.map { |k, _| "$#{k}" }.join(', ')
plural = collisions.length == 1 ? '' : 's'
Puppet.warning(
"#{other_vars_type.capitalize}#{plural} #{names} will be overridden by "\
"#{vars_type}#{plural} of the same name in the apply block"
)
end
valid.to_h
end
private_class_method :shadow_vars
def self.create_internal_compiler(compiler_class_reference, node)
case compiler_class_reference
when :script
Puppet::Parser::ScriptCompiler.new(node.environment, node.name)
when :catalog
Puppet::Parser::CatalogCompiler.new(node)
else
raise ArgumentError, "Internal Error: Invalid compiler type requested."
end
end
T_STRING = Puppet::Pops::Types::PStringType::NON_EMPTY
T_STRING_ARRAY = Puppet::Pops::Types::TypeFactory.array_of(T_STRING)
T_ANY_ARRAY = Puppet::Pops::Types::TypeFactory.array_of_any
T_BOOLEAN = Puppet::Pops::Types::PBooleanType::DEFAULT
T_GENERIC_TASK_HASH = Puppet::Pops::Types::TypeFactory.hash_kv(
Puppet::Pops::Types::TypeFactory.pattern(/\A[a-z][a-z0-9_]*\z/), Puppet::Pops::Types::TypeFactory.data)
def self.assert_type(type, value, what, allow_nil=false)
Puppet::Pops::Types::TypeAsserter.assert_instance_of(nil, type, value, allow_nil) { _('Puppet Pal: %{what}') % {what: what} }
end
def self.assert_non_empty_string(s, what, allow_nil=false)
assert_type(T_STRING, s, what, allow_nil)
end
def self.assert_optionally_empty_array(a, what, allow_nil=false)
assert_type(T_STRING_ARRAY, a, what, allow_nil)
end
private_class_method :assert_optionally_empty_array
def self.assert_mutually_exclusive(a, b, a_term, b_term)
if a && b
raise ArgumentError, _("Cannot use '%{a_term}' and '%{b_term}' at the same time") % { a_term: a_term, b_term: b_term }
end
end
private_class_method :assert_mutually_exclusive
def self.assert_block_given(block)
if block.nil?
raise ArgumentError, _("A block must be given")
end
end
private_class_method :assert_block_given
end
end